feat: Add UIDrawable children collection to Grid
Grid now supports a `children` collection for arbitrary UIDrawable elements (speech bubbles, effects, highlights, path visualization, etc.) that automatically transform with the grid's camera (pan/zoom). Key features: - Children positioned in grid-world pixel coordinates - Render after entities, before FOV overlay (proper z-ordering) - Sorted by z_index, culled when outside visible region - Click detection transforms through grid camera - Automatically clipped to grid boundaries via RenderTexture Python API: grid.children.append(caption) # Speech bubble follows grid camera grid.children.append(circle) # Highlight indicator Closes #132 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
311dc02f1d
commit
4d6808e34d
|
|
@ -14,7 +14,10 @@ UIGrid::UIGrid()
|
|||
{
|
||||
// Initialize entities list
|
||||
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
||||
|
||||
|
||||
// Initialize children collection (for UIDrawables like speech bubbles, effects)
|
||||
children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
|
||||
|
||||
// Initialize box with safe defaults
|
||||
box.setSize(sf::Vector2f(0, 0));
|
||||
position = sf::Vector2f(0, 0); // Set base class position
|
||||
|
|
@ -48,6 +51,9 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
|||
center_y = (gy/2) * cell_height;
|
||||
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
||||
|
||||
// Initialize children collection (for UIDrawables like speech bubbles, effects)
|
||||
children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
|
||||
|
||||
box.setSize(_wh);
|
||||
position = _xy; // Set base class position
|
||||
box.setPosition(position); // Sync box position
|
||||
|
|
@ -209,7 +215,38 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
Resources::game->metrics.entitiesRendered += entitiesRendered;
|
||||
Resources::game->metrics.totalEntities += totalEntities;
|
||||
}
|
||||
|
||||
|
||||
// Children layer - UIDrawables in grid-world pixel coordinates
|
||||
// Positioned between entities and FOV overlay for proper z-ordering
|
||||
if (children && !children->empty()) {
|
||||
// Sort by z_index if needed
|
||||
if (children_need_sort) {
|
||||
std::sort(children->begin(), children->end(),
|
||||
[](const auto& a, const auto& b) { return a->z_index < b->z_index; });
|
||||
children_need_sort = false;
|
||||
}
|
||||
|
||||
for (auto& child : *children) {
|
||||
if (!child->visible) continue;
|
||||
|
||||
// Cull children outside visible region (convert pixel pos to cell coords)
|
||||
float child_grid_x = child->position.x / cell_width;
|
||||
float child_grid_y = child->position.y / cell_height;
|
||||
|
||||
if (child_grid_x < left_edge - 2 || child_grid_x >= left_edge + width_sq + 2 ||
|
||||
child_grid_y < top_edge - 2 || child_grid_y >= top_edge + height_sq + 2) {
|
||||
continue; // Not visible, skip rendering
|
||||
}
|
||||
|
||||
// Transform grid-world pixel 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);
|
||||
}
|
||||
}
|
||||
|
||||
// top layer - opacity for discovered / visible status based on perspective
|
||||
// Only render visibility overlay if perspective is enabled
|
||||
|
|
@ -529,10 +566,32 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
|
|||
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
|
||||
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
|
||||
|
||||
// Convert click position to grid coordinates
|
||||
float grid_x = (localPoint.x / zoom + left_spritepixels) / cell_width;
|
||||
float grid_y = (localPoint.y / zoom + top_spritepixels) / cell_height;
|
||||
|
||||
// Convert click position to grid-world pixel coordinates
|
||||
float grid_world_x = localPoint.x / zoom + left_spritepixels;
|
||||
float grid_world_y = localPoint.y / zoom + top_spritepixels;
|
||||
|
||||
// Convert to grid cell coordinates
|
||||
float grid_x = grid_world_x / cell_width;
|
||||
float grid_y = grid_world_y / cell_height;
|
||||
|
||||
// Check children first (they render on top, so they get priority)
|
||||
// Children are positioned in grid-world pixel coordinates
|
||||
if (children && !children->empty()) {
|
||||
// Check in reverse z-order (highest z_index first, rendered last = on top)
|
||||
for (auto it = children->rbegin(); it != children->rend(); ++it) {
|
||||
auto& child = *it;
|
||||
if (!child->visible) continue;
|
||||
|
||||
// Transform click to child's local coordinate space
|
||||
// Children's position is in grid-world pixels
|
||||
sf::Vector2f childLocalPoint = sf::Vector2f(grid_world_x, grid_world_y);
|
||||
|
||||
if (auto target = child->click_at(childLocalPoint)) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check entities in reverse order (assuming they should be checked top to bottom)
|
||||
// Note: entities list is not sorted by z-index currently, but we iterate in reverse
|
||||
// to match the render order assumption
|
||||
|
|
@ -1408,7 +1467,8 @@ PyGetSetDef UIGrid::getsetters[] = {
|
|||
{"size", (getter)UIGrid::get_size, (setter)UIGrid::set_size, "Size of the grid (width, height)", NULL},
|
||||
{"center", (getter)UIGrid::get_center, (setter)UIGrid::set_center, "Grid coordinate at the center of the Grid's view (pan)", NULL},
|
||||
|
||||
{"entities", (getter)UIGrid::get_children, NULL, "EntityCollection of entities on this grid", NULL},
|
||||
{"entities", (getter)UIGrid::get_entities, NULL, "EntityCollection of entities on this grid", NULL},
|
||||
{"children", (getter)UIGrid::get_children, NULL, "UICollection of UIDrawable children (speech bubbles, effects, overlays)", NULL},
|
||||
|
||||
{"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner X-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 0)},
|
||||
{"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner Y-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 1)},
|
||||
|
|
@ -1442,19 +1502,29 @@ PyGetSetDef UIGrid::getsetters[] = {
|
|||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure)
|
||||
PyObject* UIGrid::get_entities(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
// create PyUICollection instance pointing to self->data->children
|
||||
//PyUIEntityCollectionObject* o = (PyUIEntityCollectionObject*)PyUIEntityCollectionType.tp_alloc(&PyUIEntityCollectionType, 0);
|
||||
// Returns EntityCollection for entity management
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "EntityCollection");
|
||||
auto o = (PyUIEntityCollectionObject*)type->tp_alloc(type, 0);
|
||||
if (o) {
|
||||
o->data = self->data->entities; // todone. / BUGFIX - entities isn't a shared pointer on UIGrid, what to do? -- I made it a sp<list<sp<UIEntity>>>
|
||||
o->data = self->data->entities;
|
||||
o->grid = self->data;
|
||||
}
|
||||
return (PyObject*)o;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
// Returns UICollection for UIDrawable children (speech bubbles, effects, overlays)
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection");
|
||||
auto o = (PyUICollectionObject*)type->tp_alloc(type, 0);
|
||||
if (o) {
|
||||
o->data = self->data->children;
|
||||
}
|
||||
return (PyObject*)o;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::repr(PyUIGridObject* self)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,11 @@ public:
|
|||
sf::RenderTexture renderTexture;
|
||||
std::vector<UIGridPoint> points;
|
||||
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
|
||||
|
||||
|
||||
// UIDrawable children collection (speech bubbles, effects, overlays, etc.)
|
||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
|
||||
bool children_need_sort = true; // Dirty flag for z_index sorting
|
||||
|
||||
// Background rendering
|
||||
sf::Color fill_color;
|
||||
|
||||
|
|
@ -118,6 +122,7 @@ public:
|
|||
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyObject* get_entities(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_children(PyUIGridObject* self, void* closure);
|
||||
static PyObject* repr(PyUIGridObject* self);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test Grid.children collection - Issue #132"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def take_screenshot(runtime):
|
||||
"""Take screenshot after render completes"""
|
||||
mcrfpy.delTimer("screenshot")
|
||||
automation.screenshot("test_grid_children_result.png")
|
||||
|
||||
print("Screenshot saved to test_grid_children_result.png")
|
||||
print("PASS - Grid.children test completed")
|
||||
sys.exit(0)
|
||||
|
||||
def run_test(runtime):
|
||||
"""Main test - runs after scene is set up"""
|
||||
mcrfpy.delTimer("test")
|
||||
|
||||
# Get the scene UI
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
|
||||
# Create a grid without texture (uses default 16x16 cells)
|
||||
print("Test 1: Creating Grid with children...")
|
||||
grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240))
|
||||
grid.fill_color = mcrfpy.Color(30, 30, 60)
|
||||
ui.append(grid)
|
||||
|
||||
# Verify entities and children properties exist
|
||||
print(f" grid.entities = {grid.entities}")
|
||||
print(f" grid.children = {grid.children}")
|
||||
|
||||
# Test 2: Add UIDrawable children to the grid
|
||||
print("\nTest 2: Adding UIDrawable children...")
|
||||
|
||||
# Speech bubble style caption - positioned in grid-world pixels
|
||||
# At cell (5, 3) which is 5*16=80, 3*16=48 in pixels
|
||||
caption = mcrfpy.Caption(text="Hello!", pos=(80, 48))
|
||||
caption.fill_color = mcrfpy.Color(255, 255, 200)
|
||||
caption.outline = 1
|
||||
caption.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
grid.children.append(caption)
|
||||
print(f" Added caption at (80, 48)")
|
||||
|
||||
# A highlight circle around cell (10, 7) = (160, 112)
|
||||
# Circle needs center, not pos
|
||||
circle = mcrfpy.Circle(center=(168, 120), radius=20,
|
||||
fill_color=mcrfpy.Color(255, 255, 0, 100),
|
||||
outline_color=mcrfpy.Color(255, 255, 0),
|
||||
outline=2)
|
||||
grid.children.append(circle)
|
||||
print(f" Added highlight circle at (168, 120)")
|
||||
|
||||
# A line indicating a path from (2,2) to (8,6)
|
||||
# In pixels: (32, 32) to (128, 96)
|
||||
line = mcrfpy.Line(start=(32, 32), end=(128, 96),
|
||||
color=mcrfpy.Color(0, 255, 0), thickness=3)
|
||||
grid.children.append(line)
|
||||
print(f" Added path line from (32,32) to (128,96)")
|
||||
|
||||
# An arc for range indicator at (15, 10) = (240, 160)
|
||||
arc = mcrfpy.Arc(center=(240, 160), radius=40, start_angle=0, end_angle=270,
|
||||
color=mcrfpy.Color(255, 0, 255), thickness=4)
|
||||
grid.children.append(arc)
|
||||
print(f" Added range arc at (240, 160)")
|
||||
|
||||
# Test 3: Verify children count
|
||||
print(f"\nTest 3: Verifying children count...")
|
||||
print(f" grid.children count = {len(grid.children)}")
|
||||
assert len(grid.children) == 4, f"Expected 4 children, got {len(grid.children)}"
|
||||
|
||||
# Test 4: Children should be accessible by index
|
||||
print("\nTest 4: Accessing children by index...")
|
||||
child0 = grid.children[0]
|
||||
print(f" grid.children[0] = {child0}")
|
||||
child1 = grid.children[1]
|
||||
print(f" grid.children[1] = {child1}")
|
||||
|
||||
# Test 5: Modify a child's position (should update in grid)
|
||||
print("\nTest 5: Modifying child position...")
|
||||
original_pos = (caption.pos.x, caption.pos.y)
|
||||
caption.pos = mcrfpy.Vector(90, 58)
|
||||
new_pos = (caption.pos.x, caption.pos.y)
|
||||
print(f" Moved caption from {original_pos} to {new_pos}")
|
||||
|
||||
# Test 6: Test z_index for children
|
||||
print("\nTest 6: Testing z_index ordering...")
|
||||
# Add overlapping elements with different z_index
|
||||
frame1 = mcrfpy.Frame(pos=(150, 80), size=(40, 40))
|
||||
frame1.fill_color = mcrfpy.Color(255, 0, 0, 200)
|
||||
frame1.z_index = 10
|
||||
grid.children.append(frame1)
|
||||
|
||||
frame2 = mcrfpy.Frame(pos=(160, 90), size=(40, 40))
|
||||
frame2.fill_color = mcrfpy.Color(0, 255, 0, 200)
|
||||
frame2.z_index = 5 # Lower z_index, rendered first (behind)
|
||||
grid.children.append(frame2)
|
||||
print(f" Added overlapping frames: red z=10, green z=5")
|
||||
|
||||
# Test 7: Test visibility
|
||||
print("\nTest 7: Testing child visibility...")
|
||||
frame3 = mcrfpy.Frame(pos=(50, 150), size=(30, 30))
|
||||
frame3.fill_color = mcrfpy.Color(0, 0, 255)
|
||||
frame3.visible = False
|
||||
grid.children.append(frame3)
|
||||
print(f" Added invisible blue frame (should not appear)")
|
||||
|
||||
# Test 8: Pan the grid and verify children move with it
|
||||
print("\nTest 8: Testing pan (children should follow grid camera)...")
|
||||
# Center the view on cell (10, 7.5) - default was grid center
|
||||
grid.center = (160, 120) # Center on pixel (160, 120)
|
||||
print(f" Centered grid on (160, 120)")
|
||||
|
||||
# Test 9: Test zoom
|
||||
print("\nTest 9: Testing zoom...")
|
||||
grid.zoom = 1.5
|
||||
print(f" Set zoom to 1.5")
|
||||
|
||||
print(f"\nFinal children count: {len(grid.children)}")
|
||||
|
||||
# Schedule screenshot for next frame
|
||||
mcrfpy.setTimer("screenshot", take_screenshot, 100)
|
||||
|
||||
# Create a test scene
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
|
||||
# Schedule test to run after game loop starts
|
||||
mcrfpy.setTimer("test", run_test, 50)
|
||||
Loading…
Reference in New Issue