Add UIDrawable children collection to Grid #132

Open
opened 2025-11-01 07:38:50 +00:00 by john · 0 comments
Owner

Feature Request

Allow Grid to contain arbitrary UIDrawable children (speech bubbles, effects, highlights, etc.) that automatically transform with the grid's camera (pan/zoom), just like Frame contains children.

Use Cases

  • Speech bubbles positioned above entity sprites
  • Visual effects (highlights, selection indicators, range circles)
  • Path visualization (lines, arrows) once #128 and #129 are implemented
  • UI overlays that pan/zoom with the game world (health bars, floating text)

Problem Statement

Currently, Grid can only render:

  1. Grid tiles (via UIGridPoint)
  2. Entities (via entities collection)
  3. FOV overlay

To add visual elements like speech bubbles, you must:

  • Manually calculate transforms every time camera changes (center_x, center_y, zoom)
  • Manually handle position updates when grid moves
  • No automatic clipping to grid boundaries

This violates the "fail early" principle - developers will introduce coordinate bugs.

Proposed Solution

Add a children collection to Grid, following the pattern established by Frame:

# Python API
grid = mcrfpy.Grid(grid_size=(50, 50), ...)
bubble = mcrfpy.Caption(text="Hello!", ...)
bubble.pos = (160, 96)  # Grid-world pixel coordinates

grid.children.append(bubble)
# Done! Bubble now pans/zooms with grid automatically

Design Decisions

Coordinate Space: Children positioned in grid-world pixels (not cells)

  • child.position = (160, 96) = 160 pixels right in grid-world space
  • For 16x16 cells, that's cell (10, 6)
  • Allows sub-pixel positioning (speech bubble at (162.5, 98.3))
  • Consistent with UIDrawable semantics (position always in pixels)
  • Entity positions are special (game objects in cell coords), UIDrawables are visual

Render Order:

1. Fill color
2. Grid tiles (base layer)
3. Entities (middle layer)
4. Children ← NEW - speech bubbles, effects
5. FOV overlay (top layer) ← dims everything equally

Children render after entities but before FOV, so they get dimmed in unexplored areas (correct for roguelike).

Culling: Same pattern as entities

// Cull if outside visible region
float child_grid_x = child->position.x / cell_width;
float child_grid_y = child->position.y / cell_height;

if (child_grid_x < left_edge - 1 || child_grid_x >= left_edge + width_sq + 1 ||
    child_grid_y < top_edge - 1 || child_grid_y >= top_edge + height_sq + 1) {
    continue; // Not visible
}

Implementation Pattern

Based on Frame's existing children pattern (UIFrame.cpp:162-164) and Grid's entity rendering (UIGrid.cpp:198-203):

// In UIGrid.h
class UIGrid : public UIDrawable {
    std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
};

// In UIGrid::render(), after entity layer (line ~211):
// Children layer - arbitrary UIDrawables in grid-world coordinates
if (children_need_sort && !children->empty()) {
    std::sort(children->begin(), children->end(),
        [](const auto& a, const auto& b) { return a->z_index < b->z_index; });
}

for (auto& child : *children) {
    if (!child->visible) continue;

    // Cull if outside visible region (same as entities)
    float child_grid_x = child->position.x / cell_width;
    float child_grid_y = child->position.y / cell_height;
    
    if (child_grid_x < left_edge - 1 || child_grid_x >= left_edge + width_sq + 1 ||
        child_grid_y < top_edge - 1 || child_grid_y >= top_edge + height_sq + 1) {
        continue;
    }

    // Transform grid-world position to RenderTexture pixel position
    auto pixel_pos = sf::Vector2f(
        (child->position.x - left_spritepixels) * zoom,
        (child->position.y - top_spritepixels) * zoom
    );
    
    child->render(pixel_pos, renderTexture);
}

Benefits (Abstraction Criteria Met)

Eliminates bugs: No manual coordinate updates when camera changes

Makes common case trivial:

bubble = Caption(text="Hello!")
bubble.pos = (entity.x * 16, entity.y * 16 - 20)  # 20px above entity
grid.children.append(bubble)
# Automatically pans/zooms/clips with grid

Aligns with domain: Grid-world coordinates match roguelike mental model

Technical Details

No hidden nightmares found:

  • Grid already uses RenderTexture → automatic clipping ✓
  • Entity rendering already has transform pattern → reuse it ✓
  • Frame already solves children collection → proven pattern ✓
  • Click handling needs camera-aware transform (same math as rendering) ✓
  • Nested complexity works (Grid → Frame → Caption) via composition ✓

Performance: Negligible impact

  • RenderTexture batches all draws
  • Culling keeps only visible children
  • Same pattern as existing entity rendering

Optional Helper API

For convenience when working in cell coordinates:

# Helper method to position in cells
grid.add_at_cell(bubble, cell_x=10, cell_y=6, offset_x=0, offset_y=-20)
# Internally: bubble.pos = (10 * cell_width + 0, 6 * cell_height - 20)

Definition of Done

  • UIGrid has std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children member
  • Children render after entities, before FOV overlay
  • Children culled based on camera bounds
  • Children sorted by z_index before rendering
  • Children automatically clipped to grid boundaries (via RenderTexture)
  • Python API: grid.children returns UICollection
  • Click handling transforms through grid camera
  • Documentation/examples showing speech bubble use case
  • Works with #122 (Parent-Child UI System) for global_position calculation
  • Enables #128 (Line/Arc drawing) and #129 (Circle drawing) on grids
  • Foundation for visual effects, range indicators, path previews

Labels

[Major Feature] [priority:tier2-foundation] [system:grid] [system:ui-hierarchy]

## Feature Request Allow Grid to contain arbitrary UIDrawable children (speech bubbles, effects, highlights, etc.) that automatically transform with the grid's camera (pan/zoom), just like Frame contains children. ## Use Cases - **Speech bubbles** positioned above entity sprites - **Visual effects** (highlights, selection indicators, range circles) - **Path visualization** (lines, arrows) once #128 and #129 are implemented - **UI overlays** that pan/zoom with the game world (health bars, floating text) ## Problem Statement Currently, Grid can only render: 1. Grid tiles (via `UIGridPoint`) 2. Entities (via `entities` collection) 3. FOV overlay To add visual elements like speech bubbles, you must: - Manually calculate transforms every time camera changes (center_x, center_y, zoom) - Manually handle position updates when grid moves - No automatic clipping to grid boundaries This violates the "fail early" principle - developers will introduce coordinate bugs. ## Proposed Solution Add a `children` collection to Grid, following the pattern established by Frame: ```python # Python API grid = mcrfpy.Grid(grid_size=(50, 50), ...) bubble = mcrfpy.Caption(text="Hello!", ...) bubble.pos = (160, 96) # Grid-world pixel coordinates grid.children.append(bubble) # Done! Bubble now pans/zooms with grid automatically ``` ### Design Decisions **Coordinate Space:** Children positioned in **grid-world pixels** (not cells) - `child.position = (160, 96)` = 160 pixels right in grid-world space - For 16x16 cells, that's cell (10, 6) - Allows sub-pixel positioning (speech bubble at (162.5, 98.3)) - Consistent with UIDrawable semantics (position always in pixels) - Entity positions are special (game objects in cell coords), UIDrawables are visual **Render Order:** ``` 1. Fill color 2. Grid tiles (base layer) 3. Entities (middle layer) 4. Children ← NEW - speech bubbles, effects 5. FOV overlay (top layer) ← dims everything equally ``` Children render after entities but before FOV, so they get dimmed in unexplored areas (correct for roguelike). **Culling:** Same pattern as entities ```cpp // Cull if outside visible region float child_grid_x = child->position.x / cell_width; float child_grid_y = child->position.y / cell_height; if (child_grid_x < left_edge - 1 || child_grid_x >= left_edge + width_sq + 1 || child_grid_y < top_edge - 1 || child_grid_y >= top_edge + height_sq + 1) { continue; // Not visible } ``` ## Implementation Pattern Based on Frame's existing children pattern (UIFrame.cpp:162-164) and Grid's entity rendering (UIGrid.cpp:198-203): ```cpp // In UIGrid.h class UIGrid : public UIDrawable { std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children; }; // In UIGrid::render(), after entity layer (line ~211): // Children layer - arbitrary UIDrawables in grid-world coordinates if (children_need_sort && !children->empty()) { std::sort(children->begin(), children->end(), [](const auto& a, const auto& b) { return a->z_index < b->z_index; }); } for (auto& child : *children) { if (!child->visible) continue; // Cull if outside visible region (same as entities) float child_grid_x = child->position.x / cell_width; float child_grid_y = child->position.y / cell_height; if (child_grid_x < left_edge - 1 || child_grid_x >= left_edge + width_sq + 1 || child_grid_y < top_edge - 1 || child_grid_y >= top_edge + height_sq + 1) { continue; } // Transform grid-world position to RenderTexture pixel position auto pixel_pos = sf::Vector2f( (child->position.x - left_spritepixels) * zoom, (child->position.y - top_spritepixels) * zoom ); child->render(pixel_pos, renderTexture); } ``` ## Benefits (Abstraction Criteria Met) ✅ **Eliminates bugs:** No manual coordinate updates when camera changes ✅ **Makes common case trivial:** ```python bubble = Caption(text="Hello!") bubble.pos = (entity.x * 16, entity.y * 16 - 20) # 20px above entity grid.children.append(bubble) # Automatically pans/zooms/clips with grid ``` ✅ **Aligns with domain:** Grid-world coordinates match roguelike mental model ## Technical Details **No hidden nightmares found:** - Grid already uses RenderTexture → automatic clipping ✓ - Entity rendering already has transform pattern → reuse it ✓ - Frame already solves children collection → proven pattern ✓ - Click handling needs camera-aware transform (same math as rendering) ✓ - Nested complexity works (Grid → Frame → Caption) via composition ✓ **Performance:** Negligible impact - RenderTexture batches all draws - Culling keeps only visible children - Same pattern as existing entity rendering ## Optional Helper API For convenience when working in cell coordinates: ```python # Helper method to position in cells grid.add_at_cell(bubble, cell_x=10, cell_y=6, offset_x=0, offset_y=-20) # Internally: bubble.pos = (10 * cell_width + 0, 6 * cell_height - 20) ``` ## Definition of Done - [ ] `UIGrid` has `std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children` member - [ ] Children render after entities, before FOV overlay - [ ] Children culled based on camera bounds - [ ] Children sorted by z_index before rendering - [ ] Children automatically clipped to grid boundaries (via RenderTexture) - [ ] Python API: `grid.children` returns UICollection - [ ] Click handling transforms through grid camera - [ ] Documentation/examples showing speech bubble use case ## Related Issues - Works with #122 (Parent-Child UI System) for global_position calculation - Enables #128 (Line/Arc drawing) and #129 (Circle drawing) on grids - Foundation for visual effects, range indicators, path previews ## Labels `[Major Feature]` `[priority:tier2-foundation]` `[system:grid]` `[system:ui-hierarchy]`
john added the
Major Feature
Bugfix
system:performance
priority:tier2-foundation
labels 2025-11-01 07:39:00 +00:00
john added a new dependency 2025-11-01 07:39:20 +00:00
john added a new dependency 2025-11-01 07:39:21 +00:00
john added
system:grid
system:ui-hierarchy
and removed
Bugfix
system:performance
labels 2025-11-01 07:42:44 +00:00
Sign in to join this conversation.
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Reference: john/McRogueFace#132
No description provided.