14 KiB
Object Selection System Architecture
Overview
The Object Selection System provides comprehensive selection functionality for the dungeon editor. It's designed following the Single Responsibility Principle, separating selection state management from input handling and rendering.
Version: 1.0
Date: 2025-11-26
Location: src/app/editor/dungeon/object_selection.{h,cc}
Architecture Diagram
┌─────────────────────────────────────────────────────┐
│ DungeonEditorV2 (Main Editor) │
│ - Coordinates all components │
│ - Card-based UI system │
│ - Handles Save/Load/Undo/Redo │
└────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ DungeonCanvasViewer (Canvas Rendering) │
│ - Room graphics display │
│ - Layer management (BG1/BG2) │
│ - Sprite rendering │
└────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ DungeonObjectInteraction (Input Handling) │
│ - Mouse input processing │
│ - Keyboard shortcut handling │
│ - Coordinate conversion │
│ - Drag operations │
│ - Copy/Paste/Delete │
└────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ ObjectSelection (Selection State) │
│ - Selection state management │
│ - Multi-selection logic │
│ - Rectangle selection │
│ - Visual rendering │
│ - Bounding box calculations │
└─────────────────────────────────────────────────────┘
Component Responsibilities
ObjectSelection (New Component)
Purpose: Manages selection state and provides selection operations
Key Responsibilities:
- Single object selection
- Multi-selection (Shift, Ctrl modifiers)
- Rectangle drag selection
- Select all functionality
- Visual feedback rendering
- Coordinate conversion utilities
Design Principles:
- Stateless Operations: Pure functions where possible
- Composable: Can be integrated into any editor
- Testable: All operations have unit tests
- Type-Safe: Uses enums instead of magic booleans
DungeonObjectInteraction (Enhanced)
Purpose: Handles user input and coordinates object manipulation
Integration Points:
// Before (scattered state)
std::vector<size_t> selected_object_indices_;
bool object_select_active_;
ImVec2 object_select_start_;
ImVec2 object_select_end_;
// After (delegated to ObjectSelection)
ObjectSelection selection_;
Selection Modes
The system supports four distinct selection modes:
1. Single Selection (Default)
Trigger: Left click on object Behavior: Replace current selection with clicked object Use Case: Basic object selection
selection_.SelectObject(index, ObjectSelection::SelectionMode::Single);
2. Add Selection (Shift+Click)
Trigger: Shift + Left click Behavior: Add object to existing selection Use Case: Building multi-object selections incrementally
selection_.SelectObject(index, ObjectSelection::SelectionMode::Add);
3. Toggle Selection (Ctrl+Click)
Trigger: Ctrl + Left click Behavior: Toggle object in/out of selection Use Case: Fine-tuning selections by removing specific objects
selection_.SelectObject(index, ObjectSelection::SelectionMode::Toggle);
4. Rectangle Selection (Drag)
Trigger: Right click + drag Behavior: Select all objects within rectangle Use Case: Bulk selection of objects
selection_.BeginRectangleSelection(x, y);
selection_.UpdateRectangleSelection(x, y);
selection_.EndRectangleSelection(objects, mode);
Keyboard Shortcuts
| Shortcut | Action | Implementation |
|---|---|---|
| Left Click | Select single object | SelectObject(index, Single) |
| Shift + Click | Add to selection | SelectObject(index, Add) |
| Ctrl + Click | Toggle in selection | SelectObject(index, Toggle) |
| Right Drag | Rectangle select | Begin/Update/EndRectangleSelection() |
| Ctrl + A | Select all | SelectAll(count) |
| Delete | Delete selected | HandleDeleteSelected() |
| Ctrl + C | Copy selected | HandleCopySelected() |
| Ctrl + V | Paste objects | HandlePasteObjects() |
| Esc | Clear selection | ClearSelection() |
Visual Feedback
Selected Objects
- Border: Pulsing animated outline (yellow-gold)
- Handles: Four corner handles (cyan-white at 0.85f alpha)
- Animation: Sinusoidal pulse at 4 Hz
// Animation formula
float pulse = 0.7f + 0.3f * std::sin(ImGui::GetTime() * 4.0f);
Rectangle Selection
- Border: Accent color at 0.85f alpha (high visibility)
- Fill: Accent color at 0.15f alpha (subtle background)
- Thickness: 2.0f pixels
Entity Visibility Standards
All entity rendering follows yaze's visibility standards:
- High-contrast colors: Bright yellow-gold, cyan-white
- Alpha value: 0.85f for primary visibility
- Background alpha: 0.15f for fills
Coordinate Systems
Room Coordinates (Tiles)
- Range: 0-63 (64x64 tile rooms)
- Unit: Tiles
- Origin: Top-left corner (0, 0)
Canvas Coordinates (Pixels)
- Range: 0-511 (unscaled, 8 pixels per tile)
- Unit: Pixels
- Origin: Top-left corner (0, 0)
- Scale: Subject to canvas zoom (global_scale)
Conversion Functions
// Tile → Pixel (unscaled)
std::pair<int, int> RoomToCanvasCoordinates(int room_x, int room_y) {
return {room_x * 8, room_y * 8};
}
// Pixel → Tile (unscaled)
std::pair<int, int> CanvasToRoomCoordinates(int canvas_x, int canvas_y) {
return {canvas_x / 8, canvas_y / 8};
}
Bounding Box Calculation
Objects have variable sizes based on their size_ field:
// Object size encoding
uint8_t size_h = (object.size_ & 0x0F); // Horizontal size
uint8_t size_v = (object.size_ >> 4) & 0x0F; // Vertical size
// Dimensions (in tiles)
int width = size_h + 1;
int height = size_v + 1;
Examples:
size_ |
Width | Height | Total |
|---|---|---|---|
0x00 |
1 | 1 | 1x1 |
0x11 |
2 | 2 | 2x2 |
0x23 |
4 | 3 | 4x3 |
0xFF |
16 | 16 | 16x16 |
Theme Integration
All colors are sourced from AgentUITheme:
const auto& theme = AgentUI::GetTheme();
// Selection colors
theme.dungeon_selection_primary // Yellow-gold (pulsing border)
theme.dungeon_selection_secondary // Cyan (secondary elements)
theme.dungeon_selection_handle // Cyan-white (corner handles)
theme.accent_color // UI accent (rectangle selection)
Critical Rule: NEVER use hardcoded ImVec4 colors. Always use theme system.
Implementation Details
Selection State Storage
Uses std::set<size_t> for selected indices:
Advantages:
- O(log n) insertion/deletion
- O(log n) lookup
- Automatic sorting
- No duplicates
Trade-offs:
- Slightly higher memory overhead than vector
- Justified by performance and correctness guarantees
Rectangle Selection Algorithm
Intersection Test:
bool IsObjectInRectangle(const RoomObject& object,
int min_x, int min_y, int max_x, int max_y) {
auto [obj_x, obj_y, obj_width, obj_height] = GetObjectBounds(object);
int obj_min_x = obj_x;
int obj_max_x = obj_x + obj_width - 1;
int obj_min_y = obj_y;
int obj_max_y = obj_y + obj_height - 1;
bool x_overlap = (obj_min_x <= max_x) && (obj_max_x >= min_x);
bool y_overlap = (obj_min_y <= max_y) && (obj_max_y >= min_y);
return x_overlap && y_overlap;
}
This uses standard axis-aligned bounding box (AABB) intersection.
Testing Strategy
Unit Tests
Location: test/unit/object_selection_test.cc
Coverage:
- Single selection (replace existing)
- Multi-selection (Shift+click add)
- Toggle selection (Ctrl+click toggle)
- Rectangle selection (all modes)
- Select all
- Coordinate conversion
- Bounding box calculation
- Callback invocation
Test Patterns:
// Setup
ObjectSelection selection;
std::vector<RoomObject> objects = CreateTestObjects();
// Action
selection.SelectObject(0, ObjectSelection::SelectionMode::Single);
// Verify
EXPECT_TRUE(selection.IsObjectSelected(0));
EXPECT_EQ(selection.GetSelectionCount(), 1);
Integration Points
Test integration with:
DungeonObjectInteractionfor input handlingDungeonCanvasViewerfor renderingDungeonEditorV2for undo/redo
Performance Characteristics
| Operation | Complexity | Notes |
|---|---|---|
SelectObject |
O(log n) | Set insertion |
IsObjectSelected |
O(log n) | Set lookup |
GetSelectedIndices |
O(n) | Convert set to vector |
SelectObjectsInRect |
O(m * log n) | m objects checked |
DrawSelectionHighlights |
O(k) | k selected objects |
Where:
- n = total objects in selection
- m = total objects in room
- k = selected object count
API Examples
Single Selection
// Replace selection with object 5
selection_.SelectObject(5, ObjectSelection::SelectionMode::Single);
Building Multi-Selection
// Start with object 0
selection_.SelectObject(0, ObjectSelection::SelectionMode::Single);
// Add objects 2, 4, 6
selection_.SelectObject(2, ObjectSelection::SelectionMode::Add);
selection_.SelectObject(4, ObjectSelection::SelectionMode::Add);
selection_.SelectObject(6, ObjectSelection::SelectionMode::Add);
// Toggle object 4 (remove it)
selection_.SelectObject(4, ObjectSelection::SelectionMode::Toggle);
// Result: Objects 0, 2, 6 selected
Rectangle Selection
// Begin selection at (10, 10)
selection_.BeginRectangleSelection(10, 10);
// Update to (50, 50) as user drags
selection_.UpdateRectangleSelection(50, 50);
// Complete selection (add mode)
selection_.EndRectangleSelection(objects, ObjectSelection::SelectionMode::Add);
Working with Selected Objects
// Get all selected indices (sorted)
auto indices = selection_.GetSelectedIndices();
// Get primary (first) selection
if (auto primary = selection_.GetPrimarySelection()) {
size_t index = primary.value();
// Use primary object...
}
// Check selection state
if (selection_.HasSelection()) {
size_t count = selection_.GetSelectionCount();
// Process selected objects...
}
Integration Guide
See OBJECT_SELECTION_INTEGRATION.md for step-by-step integration instructions.
Key Steps:
- Add
ObjectSelectionmember toDungeonObjectInteraction - Update input handling to use selection modes
- Replace manual selection state with
ObjectSelectionAPI - Implement click selection helper
- Update rendering to use
ObjectSelection::Draw*()methods
Future Enhancements
Planned Features
- Lasso Selection: Free-form polygon selection
- Selection Filters: Filter by object type, layer, size
- Selection History: Undo/redo for selection changes
- Selection Groups: Named selections (e.g., "All Chests")
- Smart Selection: Select similar objects (by type/size)
- Marquee Zoom: Zoom to fit selected objects
API Extensions
// Future API ideas
void SelectByType(int16_t object_id);
void SelectByLayer(RoomObject::LayerType layer);
void SelectSimilar(size_t reference_index);
void SaveSelectionGroup(const std::string& name);
void LoadSelectionGroup(const std::string& name);
Debugging
Enable Debug Logging
// In object_selection.cc
#define SELECTION_DEBUG_LOGGING
// Logs will appear like:
// [ObjectSelection] SelectObject: index=5, mode=Single
// [ObjectSelection] Selection count: 3
Visual Debugging
Use the Debug Controls card in the dungeon editor:
- Enable "Show Object Bounds"
- Filter by object type/layer
- Inspect selection state in real-time
Common Issues
Issue: Objects not selecting on click Solution: Check object bounds calculation, verify coordinate conversion
Issue: Selection persists after clear
Solution: Ensure NotifySelectionChanged() is called
Issue: Visual artifacts during drag Solution: Verify canvas scale is applied correctly in rendering
References
- ZScream: Reference implementation for dungeon object selection
- ImGui Test Engine: Automated UI testing framework
- yaze Canvas System:
src/app/gui/canvas/canvas.h - Theme System:
src/app/editor/agent/agent_ui_theme.h
Changelog
Version 1.0 (2025-11-26)
- Initial implementation
- Single/multi/rectangle selection
- Visual feedback with theme integration
- Comprehensive unit test coverage
- Integration with DungeonObjectInteraction
Maintainer: yaze development team Last Updated: 2025-11-26