Files
yaze/docs/internal/architecture/object-selection-integration.md

8.5 KiB

Object Selection System Integration Guide

Overview

The ObjectSelection class provides a clean, composable selection system for dungeon objects. It follows the Single Responsibility Principle by focusing solely on selection state management and operations, while leaving input handling and canvas interaction to DungeonObjectInteraction.

Architecture

DungeonCanvasViewer
  └── DungeonObjectInteraction (handles input, coordinates)
       └── ObjectSelection (manages selection state)

Integration Steps

1. Add ObjectSelection to DungeonObjectInteraction

File: src/app/editor/dungeon/dungeon_object_interaction.h

#include "object_selection.h"

class DungeonObjectInteraction {
 public:
  // ... existing code ...

  // Expose selection system
  ObjectSelection& selection() { return selection_; }
  const ObjectSelection& selection() const { return selection_; }

 private:
  // Replace existing selection state with ObjectSelection
  ObjectSelection selection_;

  // Remove these (now handled by ObjectSelection):
  // std::vector<size_t> selected_object_indices_;
  // bool object_select_active_;
  // ImVec2 object_select_start_;
  // ImVec2 object_select_end_;
};

2. Update HandleCanvasMouseInput Method

File: src/app/editor/dungeon/dungeon_object_interaction.cc

void DungeonObjectInteraction::HandleCanvasMouseInput() {
  const ImGuiIO& io = ImGui::GetIO();

  if (!canvas_->IsMouseHovering()) {
    return;
  }

  ImVec2 mouse_pos = io.MousePos;
  ImVec2 canvas_pos = canvas_->zero_point();
  ImVec2 canvas_mouse_pos = ImVec2(mouse_pos.x - canvas_pos.x,
                                   mouse_pos.y - canvas_pos.y);

  // Determine selection mode based on modifiers
  ObjectSelection::SelectionMode mode = ObjectSelection::SelectionMode::Single;
  if (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) {
    mode = ObjectSelection::SelectionMode::Add;
  } else if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
    mode = ObjectSelection::SelectionMode::Toggle;
  }

  // Handle left click - single object selection or object placement
  if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
    if (object_loaded_) {
      // Place object at click position
      auto [room_x, room_y] = CanvasToRoomCoordinates(
          static_cast<int>(canvas_mouse_pos.x),
          static_cast<int>(canvas_mouse_pos.y));
      PlaceObjectAtPosition(room_x, room_y);
    } else {
      // Try to select object at cursor position
      TrySelectObjectAtCursor(static_cast<int>(canvas_mouse_pos.x),
                              static_cast<int>(canvas_mouse_pos.y), mode);
    }
  }

  // Handle right click drag - rectangle selection
  if (ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !object_loaded_) {
    selection_.BeginRectangleSelection(static_cast<int>(canvas_mouse_pos.x),
                                       static_cast<int>(canvas_mouse_pos.y));
  }

  if (selection_.IsRectangleSelectionActive()) {
    if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
      selection_.UpdateRectangleSelection(static_cast<int>(canvas_mouse_pos.x),
                                          static_cast<int>(canvas_mouse_pos.y));
    }

    if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
      if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
        auto& room = (*rooms_)[current_room_id_];
        selection_.EndRectangleSelection(room.GetTileObjects(), mode);
      } else {
        selection_.CancelRectangleSelection();
      }
    }
  }

  // Handle Ctrl+A - Select All
  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
      ImGui::IsKeyPressed(ImGuiKey_A)) {
    if (rooms_ && current_room_id_ >= 0 && current_room_id_ < 296) {
      auto& room = (*rooms_)[current_room_id_];
      selection_.SelectAll(room.GetTileObjects().size());
    }
  }

  // Handle dragging selected objects (if any selected and not placing)
  if (selection_.HasSelection() && !object_loaded_) {
    HandleObjectDragging(canvas_mouse_pos);
  }
}

3. Add Helper Method for Click Selection

void DungeonObjectInteraction::TrySelectObjectAtCursor(
    int canvas_x, int canvas_y, ObjectSelection::SelectionMode mode) {
  if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296) {
    return;
  }

  auto& room = (*rooms_)[current_room_id_];
  const auto& objects = room.GetTileObjects();

  // Convert canvas coordinates to room coordinates
  auto [room_x, room_y] = CanvasToRoomCoordinates(canvas_x, canvas_y);

  // Find object at cursor (check in reverse order to prioritize top objects)
  for (int i = objects.size() - 1; i >= 0; --i) {
    auto [obj_x, obj_y, obj_width, obj_height] =
        ObjectSelection::GetObjectBounds(objects[i]);

    // Check if cursor is within object bounds
    if (room_x >= obj_x && room_x < obj_x + obj_width &&
        room_y >= obj_y && room_y < obj_y + obj_height) {
      selection_.SelectObject(i, mode);
      return;
    }
  }

  // No object found - clear selection if Single mode
  if (mode == ObjectSelection::SelectionMode::Single) {
    selection_.ClearSelection();
  }
}

4. Update Rendering Methods

Replace existing selection highlight methods:

void DungeonObjectInteraction::DrawSelectionHighlights() {
  if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296) {
    return;
  }

  auto& room = (*rooms_)[current_room_id_];
  selection_.DrawSelectionHighlights(canvas_, room.GetTileObjects());
}

void DungeonObjectInteraction::DrawSelectBox() {
  selection_.DrawRectangleSelectionBox(canvas_);
}

5. Update Delete/Copy/Paste Operations

void DungeonObjectInteraction::HandleDeleteSelected() {
  if (!selection_.HasSelection() || !rooms_) {
    return;
  }
  if (current_room_id_ < 0 || current_room_id_ >= 296) {
    return;
  }

  if (mutation_hook_) {
    mutation_hook_();
  }

  auto& room = (*rooms_)[current_room_id_];

  // Get sorted indices in descending order
  auto indices = selection_.GetSelectedIndices();
  std::sort(indices.rbegin(), indices.rend());

  // Delete from highest index to lowest (avoid index shifts)
  for (size_t index : indices) {
    room.RemoveTileObject(index);
  }

  selection_.ClearSelection();

  if (cache_invalidation_callback_) {
    cache_invalidation_callback_();
  }
}

void DungeonObjectInteraction::HandleCopySelected() {
  if (!selection_.HasSelection() || !rooms_) {
    return;
  }
  if (current_room_id_ < 0 || current_room_id_ >= 296) {
    return;
  }

  auto& room = (*rooms_)[current_room_id_];
  const auto& objects = room.GetTileObjects();

  clipboard_.clear();
  for (size_t index : selection_.GetSelectedIndices()) {
    if (index < objects.size()) {
      clipboard_.push_back(objects[index]);
    }
  }

  has_clipboard_data_ = !clipboard_.empty();
}

Keyboard Shortcuts

The selection system supports standard keyboard shortcuts:

Shortcut Action
Left Click Select single object (replace selection)
Shift + Left Click Add object to selection
Ctrl + Left Click Toggle object in selection
Right Click + Drag Rectangle selection
Ctrl + A Select all objects
Delete Delete selected objects
Ctrl + C Copy selected objects
Ctrl + V Paste objects

Visual Feedback

The selection system provides clear visual feedback:

  1. Selected Objects: Pulsing animated border with corner handles
  2. Rectangle Selection: Semi-transparent box with colored border
  3. Multiple Selection: All selected objects highlighted simultaneously

Testing

See test/unit/object_selection_test.cc for comprehensive unit tests covering:

  • Single selection
  • Multi-selection (Shift/Ctrl)
  • Rectangle selection
  • Select all
  • Coordinate conversion
  • Bounding box calculation

Benefits of This Design

  1. Separation of Concerns: Selection logic is isolated from input handling
  2. Testability: Pure functions for selection operations
  3. Reusability: ObjectSelection can be used in other editors
  4. Maintainability: Clear API with well-defined responsibilities
  5. Performance: Uses std::set for O(log n) lookups and automatic sorting
  6. Type Safety: Uses enum for selection modes instead of booleans
  7. Theme Integration: All colors sourced from AgentUITheme

Future Enhancements

Potential future improvements:

  • Lasso selection (free-form polygon)
  • Selection filters (by object type, layer)
  • Selection history (undo/redo selection changes)
  • Selection groups (named selections)
  • Marquee zoom (zoom to selected objects)