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;
|
} PyUICaptionObject;
|
||||||
|
|
||||||
class UIGrid;
|
class UIGrid;
|
||||||
|
class UIGridView;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
std::shared_ptr<UIGrid> data;
|
std::shared_ptr<UIGrid> data;
|
||||||
|
std::shared_ptr<UIGridView> view; // #252: auto-created rendering view (shim)
|
||||||
PyObject* weakreflist; // Weak reference support
|
PyObject* weakreflist; // Weak reference support
|
||||||
} PyUIGridObject;
|
} PyUIGridObject;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
|
#include "UIGridView.h" // #252: GridView shim
|
||||||
#include "UIGridPathfinding.h" // New pathfinding API
|
#include "UIGridPathfinding.h" // New pathfinding API
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "McRFPy_API.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)
|
// #184: Check if this is a Python subclass (for callback method support)
|
||||||
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridType;
|
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
|
return 0; // Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1158,6 +1186,23 @@ int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closur
|
||||||
self->data->zoom = val;
|
self->data->zoom = val;
|
||||||
else if (member_ptr == 7) // camera_rotation
|
else if (member_ptr == 7) // camera_rotation
|
||||||
self->data->camera_rotation = val;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
// TODO (7DRL Day 2, item 5.) return Texture object
|
// TODO (7DRL Day 2, item 5.) return Texture object
|
||||||
|
|
@ -2565,6 +2610,10 @@ PyGetSetDef UIGrid::getsetters[] = {
|
||||||
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
|
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
|
||||||
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
|
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
|
||||||
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIGRID),
|
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 */
|
{NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2594,6 +2643,18 @@ PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure)
|
||||||
return (PyObject*)o;
|
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)
|
PyObject* UIGrid::repr(PyUIGridObject* self)
|
||||||
{
|
{
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
#include "SpatialHash.h"
|
#include "SpatialHash.h"
|
||||||
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
||||||
#include "GridData.h" // #252 - Data layer base class
|
#include "GridData.h" // #252 - Data layer base class
|
||||||
|
#include "UIGridView.h" // #252 - GridView shim
|
||||||
|
|
||||||
// Forward declaration for pathfinding
|
// Forward declaration for pathfinding
|
||||||
class DijkstraMap;
|
class DijkstraMap;
|
||||||
|
|
@ -152,6 +153,7 @@ public:
|
||||||
static int set_on_cell_click(PyUIGridObject* self, PyObject* value, void* closure);
|
static int set_on_cell_click(PyUIGridObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_hovered_cell(PyUIGridObject* self, 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_add_layer(PyUIGridObject* self, PyObject* args);
|
||||||
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
|
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
|
||||||
static PyObject* get_layers(PyUIGridObject* self, void* closure);
|
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_exit_callable.reset();
|
||||||
obj->data->on_cell_click_callable.reset();
|
obj->data->on_cell_click_callable.reset();
|
||||||
}
|
}
|
||||||
|
obj->view.reset(); // #252: release GridView shim
|
||||||
obj->data.reset();
|
obj->data.reset();
|
||||||
Py_TYPE(self)->tp_free(self);
|
Py_TYPE(self)->tp_free(self);
|
||||||
},
|
},
|
||||||
|
|
@ -300,7 +303,11 @@ namespace mcrfpydef {
|
||||||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||||
{
|
{
|
||||||
PyUIGridObject* self = (PyUIGridObject*)type->tp_alloc(type, 0);
|
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;
|
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