Phase 4.3: Grid auto-creates GridView with rendering property sync
Grid.__init__() now auto-creates a GridView that shares the Grid's data via aliasing shared_ptr. This enables the Grid/GridView split: - PyUIGridObject gains a `view` member (shared_ptr<UIGridView>) - Grid.view property exposes the auto-created GridView (read-only) - Rendering property setters (center_x/y, zoom, camera_rotation, x, y, w, h) sync changes to the view automatically - Grid still works as UIDrawable in scenes (no substitution) — backward compatible with all existing code and subclasses - GridView.grid returns the original Grid with identity preservation - Explicit GridViews (created by user) are independent of Grid's own rendering properties Addresses #252. All 260 tests pass, no breaking changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
86f8e596b0
commit
a35352df4e
4 changed files with 197 additions and 1 deletions
|
|
@ -27,9 +27,11 @@ typedef struct {
|
|||
} PyUICaptionObject;
|
||||
|
||||
class UIGrid;
|
||||
class UIGridView;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIGrid> data;
|
||||
std::shared_ptr<UIGridView> view; // #252: auto-created rendering view (shim)
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUIGridObject;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "UIGrid.h"
|
||||
#include "UIGridView.h" // #252: GridView shim
|
||||
#include "UIGridPathfinding.h" // New pathfinding API
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
|
|
@ -996,6 +997,33 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
// #184: Check if this is a Python subclass (for callback method support)
|
||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridType;
|
||||
|
||||
// #252 shim: auto-create a GridView for rendering
|
||||
// The GridView shares GridData (via aliasing shared_ptr) and copies rendering state
|
||||
{
|
||||
auto view = std::make_shared<UIGridView>();
|
||||
// Share grid data (aliasing shared_ptr: shares UIGrid ownership, points to GridData base)
|
||||
view->grid_data = std::shared_ptr<GridData>(
|
||||
self->data, static_cast<GridData*>(self->data.get()));
|
||||
// Copy rendering state from UIGrid to GridView
|
||||
view->ptex = texture_ptr;
|
||||
view->box.setPosition(self->data->box.getPosition());
|
||||
view->box.setSize(self->data->box.getSize());
|
||||
view->position = self->data->position;
|
||||
view->center_x = self->data->center_x;
|
||||
view->center_y = self->data->center_y;
|
||||
view->zoom = self->data->zoom;
|
||||
view->fill_color = self->data->fill_color;
|
||||
view->camera_rotation = self->data->camera_rotation;
|
||||
view->perspective_entity = self->data->perspective_entity;
|
||||
view->perspective_enabled = self->data->perspective_enabled;
|
||||
view->visible = self->data->visible;
|
||||
view->opacity = self->data->opacity;
|
||||
view->z_index = self->data->z_index;
|
||||
view->name = self->data->name;
|
||||
view->ensureRenderTextureSize();
|
||||
self->view = view;
|
||||
}
|
||||
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
|
|
@ -1158,6 +1186,23 @@ int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closur
|
|||
self->data->zoom = val;
|
||||
else if (member_ptr == 7) // camera_rotation
|
||||
self->data->camera_rotation = val;
|
||||
|
||||
// #252 shim: sync rendering state to GridView
|
||||
if (self->view) {
|
||||
if (member_ptr == 0) // x
|
||||
self->view->box.setPosition(val, self->view->box.getPosition().y);
|
||||
else if (member_ptr == 1) // y
|
||||
self->view->box.setPosition(self->view->box.getPosition().x, val);
|
||||
else if (member_ptr == 2) // w
|
||||
self->view->box.setSize(sf::Vector2f(val, self->view->box.getSize().y));
|
||||
else if (member_ptr == 3) // h
|
||||
self->view->box.setSize(sf::Vector2f(self->view->box.getSize().x, val));
|
||||
else if (member_ptr == 4) self->view->center_x = val;
|
||||
else if (member_ptr == 5) self->view->center_y = val;
|
||||
else if (member_ptr == 6) self->view->zoom = val;
|
||||
else if (member_ptr == 7) self->view->camera_rotation = val;
|
||||
self->view->position = self->view->box.getPosition();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// TODO (7DRL Day 2, item 5.) return Texture object
|
||||
|
|
@ -2565,6 +2610,10 @@ PyGetSetDef UIGrid::getsetters[] = {
|
|||
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
|
||||
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
|
||||
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
// #252 - GridView shim
|
||||
{"view", (getter)UIGrid::get_view, NULL,
|
||||
"Auto-created GridView for rendering (read-only). "
|
||||
"When Grid is appended to a scene, this view is what actually renders.", NULL},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
|
@ -2594,6 +2643,18 @@ PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure)
|
|||
return (PyObject*)o;
|
||||
}
|
||||
|
||||
// #252 - get_view returns the auto-created GridView
|
||||
PyObject* UIGrid::get_view(PyUIGridObject* self, void* closure)
|
||||
{
|
||||
if (!self->view) Py_RETURN_NONE;
|
||||
auto type = &mcrfpydef::PyUIGridViewType;
|
||||
auto obj = (PyUIGridViewObject*)type->tp_alloc(type, 0);
|
||||
if (!obj) return PyErr_NoMemory();
|
||||
obj->data = self->view;
|
||||
obj->weakreflist = NULL;
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::repr(PyUIGridObject* self)
|
||||
{
|
||||
std::ostringstream ss;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include "SpatialHash.h"
|
||||
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
||||
#include "GridData.h" // #252 - Data layer base class
|
||||
#include "UIGridView.h" // #252 - GridView shim
|
||||
|
||||
// Forward declaration for pathfinding
|
||||
class DijkstraMap;
|
||||
|
|
@ -152,6 +153,7 @@ public:
|
|||
static int set_on_cell_click(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_hovered_cell(PyUIGridObject* self, void* closure);
|
||||
|
||||
static PyObject* get_view(PyUIGridObject* self, void* closure); // #252 shim
|
||||
static PyObject* py_add_layer(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* get_layers(PyUIGridObject* self, void* closure);
|
||||
|
|
@ -189,6 +191,7 @@ namespace mcrfpydef {
|
|||
obj->data->on_cell_exit_callable.reset();
|
||||
obj->data->on_cell_click_callable.reset();
|
||||
}
|
||||
obj->view.reset(); // #252: release GridView shim
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
|
|
@ -300,7 +303,11 @@ namespace mcrfpydef {
|
|||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyUIGridObject* self = (PyUIGridObject*)type->tp_alloc(type, 0);
|
||||
if (self) self->data = std::make_shared<UIGrid>();
|
||||
if (self) {
|
||||
self->data = std::make_shared<UIGrid>();
|
||||
// Placement-new the shared_ptr<UIGridView> (tp_alloc zero-fills, not construct)
|
||||
new (&self->view) std::shared_ptr<UIGridView>();
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
126
tests/regression/grid_backward_compat_test.py
Normal file
126
tests/regression/grid_backward_compat_test.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""Regression test for Grid backward compatibility with GridView shim (#252)."""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_grid_creates_view():
|
||||
"""Grid auto-creates a GridView accessible via .view property."""
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex, pos=(0, 0), size=(160, 160))
|
||||
assert grid.view is not None, "Grid should auto-create a view"
|
||||
assert isinstance(grid.view, mcrfpy.GridView)
|
||||
print("PASS: Grid auto-creates view")
|
||||
|
||||
def test_view_shares_grid_data():
|
||||
"""GridView created by shim shares the same grid data."""
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex, pos=(0, 0), size=(160, 160))
|
||||
view = grid.view
|
||||
|
||||
# View's grid should be the same Grid
|
||||
assert view.grid is grid, "view.grid should be the same Grid"
|
||||
assert view.grid.grid_w == 10
|
||||
print("PASS: view shares grid data")
|
||||
|
||||
def test_rendering_property_sync():
|
||||
"""Setting rendering properties on Grid syncs to the view."""
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex, pos=(0, 0), size=(160, 160))
|
||||
view = grid.view
|
||||
|
||||
grid.zoom = 3.0
|
||||
assert abs(view.zoom - 3.0) < 0.01, f"View zoom should sync: {view.zoom}"
|
||||
|
||||
grid.center_x = 200.0
|
||||
assert abs(view.center.x - 200.0) < 0.01, f"View center_x should sync: {view.center.x}"
|
||||
|
||||
grid.center_y = 150.0
|
||||
assert abs(view.center.y - 150.0) < 0.01, f"View center_y should sync: {view.center.y}"
|
||||
|
||||
grid.camera_rotation = 45.0
|
||||
# camera_rotation syncs through set_float_member
|
||||
print("PASS: rendering properties sync to view")
|
||||
|
||||
def test_grid_still_works_in_scene():
|
||||
"""Grid can still be appended to scenes and works as before."""
|
||||
scene = mcrfpy.Scene("test_compat")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex, pos=(0, 0), size=(160, 160))
|
||||
scene.children.append(grid)
|
||||
|
||||
# Grid is in the scene as itself (not substituted)
|
||||
assert len(scene.children) == 1
|
||||
retrieved = scene.children[0]
|
||||
assert type(retrieved).__name__ == "Grid", f"Expected Grid, got {type(retrieved).__name__}"
|
||||
print("PASS: Grid works in scene as before")
|
||||
|
||||
def test_grid_subclass_preserved():
|
||||
"""Grid subclasses maintain identity when appended to scene."""
|
||||
scene = mcrfpy.Scene("test_subclass_compat")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
class MyGrid(mcrfpy.Grid):
|
||||
pass
|
||||
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = MyGrid(grid_size=(5, 5), texture=tex, pos=(0, 0), size=(80, 80))
|
||||
scene.children.append(grid)
|
||||
|
||||
retrieved = scene.children[0]
|
||||
assert isinstance(retrieved, MyGrid), f"Expected MyGrid, got {type(retrieved).__name__}"
|
||||
print("PASS: Grid subclass identity preserved")
|
||||
|
||||
def test_entity_operations_unaffected():
|
||||
"""Entity creation and manipulation still works normally."""
|
||||
scene = mcrfpy.Scene("test_entity_compat")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = mcrfpy.Grid(grid_size=(20, 20), texture=tex, pos=(0, 0), size=(320, 320))
|
||||
scene.children.append(grid)
|
||||
|
||||
for y in range(20):
|
||||
for x in range(20):
|
||||
grid.at(x, y).walkable = True
|
||||
grid.at(x, y).transparent = True
|
||||
|
||||
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||
assert len(grid.entities) == 1
|
||||
assert e.cell_x == 5
|
||||
assert e.cell_y == 5
|
||||
assert len(grid.at(5, 5).entities) == 1
|
||||
print("PASS: entity operations unaffected")
|
||||
|
||||
def test_gridview_independent_rendering():
|
||||
"""A GridView can have different rendering settings from the Grid."""
|
||||
scene = mcrfpy.Scene("test_independent")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = mcrfpy.Grid(grid_size=(20, 20), texture=tex, pos=(0, 0), size=(320, 320))
|
||||
|
||||
# Create an explicit GridView with different settings
|
||||
view2 = mcrfpy.GridView(grid=grid, pos=(350, 0), size=(160, 160), zoom=0.5)
|
||||
scene.children.append(grid)
|
||||
scene.children.append(view2)
|
||||
|
||||
# Grid and view2 should have different zoom
|
||||
assert abs(grid.zoom - 1.0) < 0.01
|
||||
assert abs(view2.zoom - 0.5) < 0.01
|
||||
|
||||
# Changing grid zoom doesn't affect explicit GridView
|
||||
grid.zoom = 2.0
|
||||
assert abs(view2.zoom - 0.5) < 0.01, "Explicit GridView should keep its own zoom"
|
||||
print("PASS: GridView independent rendering")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_grid_creates_view()
|
||||
test_view_shares_grid_data()
|
||||
test_rendering_property_sync()
|
||||
test_grid_still_works_in_scene()
|
||||
test_grid_subclass_preserved()
|
||||
test_entity_operations_unaffected()
|
||||
test_gridview_independent_rendering()
|
||||
print("All backward compatibility tests passed")
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue