Grid/GridView API unification: mcrfpy.Grid now returns GridView, closes #252

mcrfpy.Grid() now creates a GridView that internally owns a GridData (UIGrid).
The old UIGrid type is renamed to _GridData (internal). Attribute access on Grid
delegates to the underlying UIGrid via tp_getattro/tp_setattro, so all existing
Grid properties (grid_w, grid_h, entities, cells, layers, etc.) work transparently.

Key changes:
- GridView init has two modes: factory (Grid(grid_size=...)) and explicit view
  (Grid(grid=existing_grid, ...)) for future multi-view support
- Entity.grid getter returns GridView wrapper via owning_view back-reference
- Entity.grid setter accepts GridView objects
- GridLayer set_grid handles GridView (extracts underlying UIGrid)
- UIDrawable::removeFromParent handles UIGRIDVIEW type correctly
- UIFrame children init accepts GridView objects
- Animation system supports GridView (center, zoom, shader.* properties)
- PythonObjectCache registration preserves subclass identity
- All 263 tests pass (100%)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-04-04 04:34:11 -04:00
commit 109bc21d90
10 changed files with 616 additions and 146 deletions

View file

@ -26,6 +26,7 @@
class DijkstraMap; class DijkstraMap;
class UIEntity; class UIEntity;
class UIDrawable; class UIDrawable;
class UIGridView;
class PyTexture; class PyTexture;
class GridData { class GridData {
@ -130,6 +131,11 @@ public:
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children; std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
bool children_need_sort = true; bool children_need_sort = true;
// =========================================================================
// #252 - Owning GridView back-reference (for Entity.grid → GridView lookup)
// =========================================================================
std::weak_ptr<UIGridView> owning_view;
protected: protected:
// Initialize grid storage (flat or chunked) and TCOD map // Initialize grid storage (flat or chunked) and TCOD map
void initStorage(int gx, int gy, GridData* parent_ref); void initStorage(int gx, int gy, GridData* parent_ref);

View file

@ -1,5 +1,6 @@
#include "GridLayers.h" #include "GridLayers.h"
#include "UIGrid.h" #include "UIGrid.h"
#include "UIGridView.h"
#include "UIEntity.h" #include "UIEntity.h"
#include "PyColor.h" #include "PyColor.h"
#include "PyTexture.h" #include "PyTexture.h"
@ -1677,25 +1678,31 @@ int PyGridLayerAPI::ColorLayer_set_grid(PyColorLayerObject* self, PyObject* valu
return 0; return 0;
} }
// Validate it's a Grid // Validate it's a Grid (GridView) or internal _GridData
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType) &&
if (!mcrfpy_module) return -1; !PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
auto* grid_type = PyObject_GetAttrString(mcrfpy_module, "Grid");
Py_DECREF(mcrfpy_module);
if (!grid_type) return -1;
if (!PyObject_IsInstance(value, grid_type)) {
Py_DECREF(grid_type);
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None"); PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
return -1; return -1;
} }
Py_DECREF(grid_type);
PyUIGridObject* py_grid = (PyUIGridObject*)value; // Extract UIGrid shared_ptr from Grid (UIGridView) or _GridData (UIGrid)
std::shared_ptr<UIGrid> target_grid;
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
auto* pyview = (PyUIGridViewObject*)value;
if (pyview->data->grid_data) {
// GridView's grid_data is aliased from a UIGrid, cast back
target_grid = std::static_pointer_cast<UIGrid>(pyview->data->grid_data);
}
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
target_grid = ((PyUIGridObject*)value)->data;
}
if (!target_grid) {
PyErr_SetString(PyExc_RuntimeError, "Grid has no data");
return -1;
}
// Check if already attached to this grid // Check if already attached to this grid
if (self->grid.get() == py_grid->data.get()) { if (self->grid.get() == target_grid.get()) {
return 0; // Nothing to do return 0; // Nothing to do
} }
@ -1714,31 +1721,31 @@ int PyGridLayerAPI::ColorLayer_set_grid(PyColorLayerObject* self, PyObject* valu
// Handle name collision - unlink existing layer with same name // Handle name collision - unlink existing layer with same name
if (!self->data->name.empty()) { if (!self->data->name.empty()) {
auto existing = py_grid->data->getLayerByName(self->data->name); auto existing = target_grid->getLayerByName(self->data->name);
if (existing && existing.get() != self->data.get()) { if (existing && existing.get() != self->data.get()) {
existing->parent_grid = nullptr; existing->parent_grid = nullptr;
py_grid->data->removeLayer(existing); target_grid->removeLayer(existing);
} }
} }
// Lazy allocation: resize if layer is (0,0) // Lazy allocation: resize if layer is (0,0)
if (self->data->grid_x == 0 && self->data->grid_y == 0) { if (self->data->grid_x == 0 && self->data->grid_y == 0) {
self->data->resize(py_grid->data->grid_w, py_grid->data->grid_h); self->data->resize(target_grid->grid_w, target_grid->grid_h);
} else if (self->data->grid_x != py_grid->data->grid_w || } else if (self->data->grid_x != target_grid->grid_w ||
self->data->grid_y != py_grid->data->grid_h) { self->data->grid_y != target_grid->grid_h) {
PyErr_Format(PyExc_ValueError, PyErr_Format(PyExc_ValueError,
"Layer size (%d, %d) does not match Grid size (%d, %d)", "Layer size (%d, %d) does not match Grid size (%d, %d)",
self->data->grid_x, self->data->grid_y, self->data->grid_x, self->data->grid_y,
py_grid->data->grid_w, py_grid->data->grid_h); target_grid->grid_w, target_grid->grid_h);
self->grid.reset(); self->grid.reset();
return -1; return -1;
} }
// Link to new grid // Link to new grid
self->data->parent_grid = py_grid->data.get(); self->data->parent_grid = target_grid.get();
py_grid->data->layers.push_back(self->data); target_grid->layers.push_back(self->data);
py_grid->data->layers_need_sort = true; target_grid->layers_need_sort = true;
self->grid = py_grid->data; self->grid = target_grid;
return 0; return 0;
} }
@ -2326,25 +2333,30 @@ int PyGridLayerAPI::TileLayer_set_grid(PyTileLayerObject* self, PyObject* value,
return 0; return 0;
} }
// Validate it's a Grid // Validate it's a Grid (GridView) or internal _GridData
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType) &&
if (!mcrfpy_module) return -1; !PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
auto* grid_type = PyObject_GetAttrString(mcrfpy_module, "Grid");
Py_DECREF(mcrfpy_module);
if (!grid_type) return -1;
if (!PyObject_IsInstance(value, grid_type)) {
Py_DECREF(grid_type);
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None"); PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
return -1; return -1;
} }
Py_DECREF(grid_type);
PyUIGridObject* py_grid = (PyUIGridObject*)value; // Extract UIGrid shared_ptr from Grid (UIGridView) or _GridData (UIGrid)
std::shared_ptr<UIGrid> target_grid;
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
auto* pyview = (PyUIGridViewObject*)value;
if (pyview->data->grid_data) {
target_grid = std::static_pointer_cast<UIGrid>(pyview->data->grid_data);
}
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
target_grid = ((PyUIGridObject*)value)->data;
}
if (!target_grid) {
PyErr_SetString(PyExc_RuntimeError, "Grid has no data");
return -1;
}
// Check if already attached to this grid // Check if already attached to this grid
if (self->grid.get() == py_grid->data.get()) { if (self->grid.get() == target_grid.get()) {
return 0; // Nothing to do return 0; // Nothing to do
} }
@ -2363,35 +2375,35 @@ int PyGridLayerAPI::TileLayer_set_grid(PyTileLayerObject* self, PyObject* value,
// Handle name collision - unlink existing layer with same name // Handle name collision - unlink existing layer with same name
if (!self->data->name.empty()) { if (!self->data->name.empty()) {
auto existing = py_grid->data->getLayerByName(self->data->name); auto existing = target_grid->getLayerByName(self->data->name);
if (existing && existing.get() != self->data.get()) { if (existing && existing.get() != self->data.get()) {
existing->parent_grid = nullptr; existing->parent_grid = nullptr;
py_grid->data->removeLayer(existing); target_grid->removeLayer(existing);
} }
} }
// Lazy allocation: resize if layer is (0,0) // Lazy allocation: resize if layer is (0,0)
if (self->data->grid_x == 0 && self->data->grid_y == 0) { if (self->data->grid_x == 0 && self->data->grid_y == 0) {
self->data->resize(py_grid->data->grid_w, py_grid->data->grid_h); self->data->resize(target_grid->grid_w, target_grid->grid_h);
} else if (self->data->grid_x != py_grid->data->grid_w || } else if (self->data->grid_x != target_grid->grid_w ||
self->data->grid_y != py_grid->data->grid_h) { self->data->grid_y != target_grid->grid_h) {
PyErr_Format(PyExc_ValueError, PyErr_Format(PyExc_ValueError,
"Layer size (%d, %d) does not match Grid size (%d, %d)", "Layer size (%d, %d) does not match Grid size (%d, %d)",
self->data->grid_x, self->data->grid_y, self->data->grid_x, self->data->grid_y,
py_grid->data->grid_w, py_grid->data->grid_h); target_grid->grid_w, target_grid->grid_h);
self->grid.reset(); self->grid.reset();
return -1; return -1;
} }
// Link to new grid // Link to new grid
self->data->parent_grid = py_grid->data.get(); self->data->parent_grid = target_grid.get();
py_grid->data->layers.push_back(std::static_pointer_cast<GridLayer>(self->data)); target_grid->layers.push_back(std::static_pointer_cast<GridLayer>(self->data));
py_grid->data->layers_need_sort = true; target_grid->layers_need_sort = true;
self->grid = py_grid->data; self->grid = target_grid;
// Inherit grid texture if TileLayer has none (#254) // Inherit grid texture if TileLayer has none (#254)
if (!self->data->texture) { if (!self->data->texture) {
self->data->texture = py_grid->data->getTexture(); self->data->texture = target_grid->getTexture();
} }
return 0; return 0;

View file

@ -482,9 +482,9 @@ PyObject* PyInit_mcrfpy()
&PyDrawableType, &PyDrawableType,
/*UI widgets*/ /*UI widgets*/
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType, &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType,
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType, &PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
&mcrfpydef::PyUIGridViewType, &mcrfpydef::PyUIGridViewType, // #252: GridView IS the primary "Grid" type
/*3D entities*/ /*3D entities*/
&mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType, &mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType,
@ -547,6 +547,9 @@ PyObject* PyInit_mcrfpy()
// Types that are used internally but NOT exported to module namespace (#189) // Types that are used internally but NOT exported to module namespace (#189)
// These still need PyType_Ready() but are not added to module // These still need PyType_Ready() but are not added to module
PyTypeObject* internal_types[] = { PyTypeObject* internal_types[] = {
/*#252: internal grid data type - UIGrid is now internal, GridView is "Grid"*/
&PyUIGridType,
/*game map & perspective data - returned by Grid.at() but not directly instantiable*/ /*game map & perspective data - returned by Grid.at() but not directly instantiable*/
&PyUIGridPointType, &PyUIGridPointStateType, &PyUIGridPointType, &PyUIGridPointStateType,
@ -682,6 +685,11 @@ PyObject* PyInit_mcrfpy()
t = internal_types[i]; t = internal_types[i];
} }
// #252: Add "GridView" as an alias for the Grid type (which is PyUIGridViewType)
// This allows both mcrfpy.Grid(...) and mcrfpy.GridView(...) to work
Py_INCREF(&mcrfpydef::PyUIGridViewType);
PyModule_AddObject(m, "GridView", (PyObject*)&mcrfpydef::PyUIGridViewType);
// Add default_font and default_texture to module // Add default_font and default_texture to module
McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf"); McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
McRFPy_API::default_texture = std::make_shared<PyTexture>("assets/kenney_tinydungeon.png", 16, 16); McRFPy_API::default_texture = std::make_shared<PyTexture>("assets/kenney_tinydungeon.png", 16, 16);

View file

@ -244,6 +244,15 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args, PyObject*
handled = true; handled = true;
} }
} }
else if (PyObject_IsInstance(target_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
// #252: GridView (unified Grid) - animate the view directly
PyUIGridViewObject* view = (PyUIGridViewObject*)target_obj;
if (view->data) {
self->data->start(view->data);
AnimationManager::getInstance().addAnimation(self->data, conflict_mode);
handled = true;
}
}
else if (PyObject_IsInstance(target_obj, (PyObject*)&mcrfpydef::PyUIGridType)) { else if (PyObject_IsInstance(target_obj, (PyObject*)&mcrfpydef::PyUIGridType)) {
PyUIGridObject* grid = (PyUIGridObject*)target_obj; PyUIGridObject* grid = (PyUIGridObject*)target_obj;
if (grid->data) { if (grid->data) {

View file

@ -36,6 +36,8 @@ static UIDrawable* extractDrawable(PyObject* self, PyObjectsEnum objtype) {
return ((PyUICircleObject*)self)->data.get(); return ((PyUICircleObject*)self)->data.get();
case PyObjectsEnum::UIARC: case PyObjectsEnum::UIARC:
return ((PyUIArcObject*)self)->data.get(); return ((PyUIArcObject*)self)->data.get();
case PyObjectsEnum::UIGRIDVIEW:
return ((PyUIGridViewObject*)self)->data.get();
default: default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return nullptr; return nullptr;
@ -59,6 +61,8 @@ static std::shared_ptr<UIDrawable> extractDrawableShared(PyObject* self, PyObjec
return ((PyUICircleObject*)self)->data; return ((PyUICircleObject*)self)->data;
case PyObjectsEnum::UIARC: case PyObjectsEnum::UIARC:
return ((PyUIArcObject*)self)->data; return ((PyUIArcObject*)self)->data;
case PyObjectsEnum::UIGRIDVIEW:
return ((PyUIGridViewObject*)self)->data;
default: default:
return nullptr; return nullptr;
} }
@ -279,6 +283,12 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
else else
ptr = NULL; ptr = NULL;
break; break;
case PyObjectsEnum::UIGRIDVIEW:
if (((PyUIGridViewObject*)self)->data->click_callable)
ptr = ((PyUIGridViewObject*)self)->data->click_callable->borrow();
else
ptr = NULL;
break;
default: default:
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click"); PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
return NULL; return NULL;
@ -316,6 +326,9 @@ int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) {
case PyObjectsEnum::UIARC: case PyObjectsEnum::UIARC:
target = (((PyUIArcObject*)self)->data.get()); target = (((PyUIArcObject*)self)->data.get());
break; break;
case PyObjectsEnum::UIGRIDVIEW:
target = (((PyUIGridViewObject*)self)->data.get());
break;
default: default:
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _set_click"); PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _set_click");
return -1; return -1;
@ -1033,7 +1046,7 @@ void UIDrawable::removeFromParent() {
} }
frame->children_need_sort = true; frame->children_need_sort = true;
} }
else if (p->derived_type() == PyObjectsEnum::UIGRID || p->derived_type() == PyObjectsEnum::UIGRIDVIEW) { else if (p->derived_type() == PyObjectsEnum::UIGRID) {
auto grid = std::static_pointer_cast<UIGrid>(p); auto grid = std::static_pointer_cast<UIGrid>(p);
auto& children = *grid->children; auto& children = *grid->children;
@ -1045,6 +1058,19 @@ void UIDrawable::removeFromParent() {
} }
grid->children_need_sort = true; grid->children_need_sort = true;
} }
else if (p->derived_type() == PyObjectsEnum::UIGRIDVIEW) {
auto view = std::static_pointer_cast<UIGridView>(p);
if (view->grid_data && view->grid_data->children) {
auto& children = *view->grid_data->children;
for (auto it = children.begin(); it != children.end(); ++it) {
if (it->get() == this) {
children.erase(it);
break;
}
}
view->grid_data->children_need_sort = true;
}
}
parent.reset(); parent.reset();
} }
@ -1416,6 +1442,9 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
case PyObjectsEnum::UIARC: case PyObjectsEnum::UIARC:
drawable = ((PyUIArcObject*)self)->data; drawable = ((PyUIArcObject*)self)->data;
break; break;
case PyObjectsEnum::UIGRIDVIEW:
drawable = ((PyUIGridViewObject*)self)->data;
break;
default: default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance");
return -1; return -1;
@ -1430,9 +1459,10 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
// Value must be a Frame, Grid, or Scene (things with children collections) // Value must be a Frame, Grid, or Scene (things with children collections)
bool is_frame = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIFrameType); bool is_frame = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIFrameType);
bool is_grid = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType); bool is_grid = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType);
bool is_gridview = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType);
bool is_scene = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PySceneType); bool is_scene = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PySceneType);
if (!is_frame && !is_grid && !is_scene) { if (!is_frame && !is_grid && !is_gridview && !is_scene) {
PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Grid, Scene, or None"); PyErr_SetString(PyExc_TypeError, "parent must be a Frame, Grid, Scene, or None");
return -1; return -1;
} }
@ -1476,6 +1506,13 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) {
auto frame = ((PyUIFrameObject*)value)->data; auto frame = ((PyUIFrameObject*)value)->data;
children_ptr = &frame->children; children_ptr = &frame->children;
new_parent = frame; new_parent = frame;
} else if (is_gridview) {
// #252: GridView (unified Grid) - access children through grid_data
auto view = ((PyUIGridViewObject*)value)->data;
if (view->grid_data) {
children_ptr = &view->grid_data->children;
}
new_parent = view;
} else if (is_grid) { } else if (is_grid) {
auto grid = ((PyUIGridObject*)value)->data; auto grid = ((PyUIGridObject*)value)->data;
children_ptr = &grid->children; children_ptr = &grid->children;
@ -1624,6 +1661,10 @@ PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) {
if (((PyUIArcObject*)self)->data->on_enter_callable) if (((PyUIArcObject*)self)->data->on_enter_callable)
ptr = ((PyUIArcObject*)self)->data->on_enter_callable->borrow(); ptr = ((PyUIArcObject*)self)->data->on_enter_callable->borrow();
break; break;
case PyObjectsEnum::UIGRIDVIEW:
if (((PyUIGridViewObject*)self)->data->on_enter_callable)
ptr = ((PyUIGridViewObject*)self)->data->on_enter_callable->borrow();
break;
default: default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter"); PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter");
return NULL; return NULL;
@ -1661,6 +1702,9 @@ int UIDrawable::set_on_enter(PyObject* self, PyObject* value, void* closure) {
case PyObjectsEnum::UIARC: case PyObjectsEnum::UIARC:
target = ((PyUIArcObject*)self)->data.get(); target = ((PyUIArcObject*)self)->data.get();
break; break;
case PyObjectsEnum::UIGRIDVIEW:
target = ((PyUIGridViewObject*)self)->data.get();
break;
default: default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter"); PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter");
return -1; return -1;
@ -1708,6 +1752,10 @@ PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) {
if (((PyUIArcObject*)self)->data->on_exit_callable) if (((PyUIArcObject*)self)->data->on_exit_callable)
ptr = ((PyUIArcObject*)self)->data->on_exit_callable->borrow(); ptr = ((PyUIArcObject*)self)->data->on_exit_callable->borrow();
break; break;
case PyObjectsEnum::UIGRIDVIEW:
if (((PyUIGridViewObject*)self)->data->on_exit_callable)
ptr = ((PyUIGridViewObject*)self)->data->on_exit_callable->borrow();
break;
default: default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit"); PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit");
return NULL; return NULL;
@ -1745,6 +1793,9 @@ int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) {
case PyObjectsEnum::UIARC: case PyObjectsEnum::UIARC:
target = ((PyUIArcObject*)self)->data.get(); target = ((PyUIArcObject*)self)->data.get();
break; break;
case PyObjectsEnum::UIGRIDVIEW:
target = ((PyUIGridViewObject*)self)->data.get();
break;
default: default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit"); PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit");
return -1; return -1;
@ -1801,6 +1852,10 @@ PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) {
if (((PyUIArcObject*)self)->data->on_move_callable) if (((PyUIArcObject*)self)->data->on_move_callable)
ptr = ((PyUIArcObject*)self)->data->on_move_callable->borrow(); ptr = ((PyUIArcObject*)self)->data->on_move_callable->borrow();
break; break;
case PyObjectsEnum::UIGRIDVIEW:
if (((PyUIGridViewObject*)self)->data->on_move_callable)
ptr = ((PyUIGridViewObject*)self)->data->on_move_callable->borrow();
break;
default: default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move"); PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move");
return NULL; return NULL;
@ -1838,6 +1893,9 @@ int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) {
case PyObjectsEnum::UIARC: case PyObjectsEnum::UIARC:
target = ((PyUIArcObject*)self)->data.get(); target = ((PyUIArcObject*)self)->data.get();
break; break;
case PyObjectsEnum::UIGRIDVIEW:
target = ((PyUIGridViewObject*)self)->data.get();
break;
default: default:
PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move"); PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move");
return -1; return -1;
@ -2189,6 +2247,7 @@ PyObject* UIDrawable::py_realign(PyObject* self, PyObject* args) {
if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIFrameType)) objtype = PyObjectsEnum::UIFRAME; if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIFrameType)) objtype = PyObjectsEnum::UIFRAME;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICaptionType)) objtype = PyObjectsEnum::UICAPTION; else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICaptionType)) objtype = PyObjectsEnum::UICAPTION;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUISpriteType)) objtype = PyObjectsEnum::UISPRITE; else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUISpriteType)) objtype = PyObjectsEnum::UISPRITE;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIGridViewType)) objtype = PyObjectsEnum::UIGRIDVIEW;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIGridType)) objtype = PyObjectsEnum::UIGRID; else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUIGridType)) objtype = PyObjectsEnum::UIGRID;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUILineType)) objtype = PyObjectsEnum::UILINE; else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUILineType)) objtype = PyObjectsEnum::UILINE;
else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICircleType)) objtype = PyObjectsEnum::UICIRCLE; else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICircleType)) objtype = PyObjectsEnum::UICIRCLE;

View file

@ -1,5 +1,6 @@
#include "UIEntity.h" #include "UIEntity.h"
#include "UIGrid.h" #include "UIGrid.h"
#include "UIGridView.h" // #252: Entity.grid accepts GridView
#include "UIGridPathfinding.h" #include "UIGridPathfinding.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include <algorithm> #include <algorithm>
@ -221,8 +222,9 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
texture_ptr = McRFPy_API::default_texture; texture_ptr = McRFPy_API::default_texture;
} }
// Handle grid argument // Handle grid argument - accept both internal _GridData and GridView (unified Grid)
if (grid_obj && !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType)) { if (grid_obj && !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType) &&
!PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance"); PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
return -1; return -1;
} }
@ -303,15 +305,24 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
// Handle grid attachment // Handle grid attachment
if (grid_obj) { if (grid_obj) {
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj; std::shared_ptr<UIGrid> grid_ptr;
self->data->grid = pygrid->data; if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
// Append entity to grid's entity list // #252: GridView (unified Grid) - extract internal UIGrid
pygrid->data->entities->push_back(self->data); PyUIGridViewObject* pyview = (PyUIGridViewObject*)grid_obj;
// Insert into spatial hash for O(1) cell queries (#253) if (pyview->data->grid_data) {
pygrid->data->spatial_hash.insert(self->data); grid_ptr = std::shared_ptr<UIGrid>(
pyview->data->grid_data, static_cast<UIGrid*>(pyview->data->grid_data.get()));
// Don't initialize gridstate here - lazy initialization to support large numbers of entities }
// gridstate will be initialized when visibility is updated or accessed } else {
// Internal _GridData type
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
grid_ptr = pygrid->data;
}
if (grid_ptr) {
self->data->grid = grid_ptr;
grid_ptr->entities->push_back(self->data);
grid_ptr->spatial_hash.insert(self->data);
}
} }
return 0; return 0;
} }
@ -664,15 +675,43 @@ PyObject* UIEntity::get_grid(PyUIEntityObject* self, void* closure)
auto& grid = self->data->grid; auto& grid = self->data->grid;
// Check cache first — preserves identity (entity.grid is entity.grid) // #252: If the grid has an owning GridView, return that instead.
// This preserves the unified Grid API where entity.grid returns the same
// object the user created via mcrfpy.Grid(...).
auto owning_view = grid->owning_view.lock();
if (owning_view) {
// Check cache for the GridView
if (owning_view->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(owning_view->serial_number);
if (cached) return cached;
}
auto view_type = &mcrfpydef::PyUIGridViewType;
auto pyView = (PyUIGridViewObject*)view_type->tp_alloc(view_type, 0);
if (pyView) {
pyView->data = owning_view;
pyView->weakreflist = NULL;
if (owning_view->serial_number == 0) {
owning_view->serial_number = PythonObjectCache::getInstance().assignSerial();
}
PyObject* weakref = PyWeakref_NewRef((PyObject*)pyView, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(owning_view->serial_number, weakref);
Py_DECREF(weakref);
}
}
return (PyObject*)pyView;
}
// Fallback: return internal _GridData wrapper (no owning view)
if (grid->serial_number != 0) { if (grid->serial_number != 0) {
PyObject* cached = PythonObjectCache::getInstance().lookup(grid->serial_number); PyObject* cached = PythonObjectCache::getInstance().lookup(grid->serial_number);
if (cached) { if (cached) {
return cached; // Already INCREF'd by lookup return cached;
} }
} }
// No cached wrapper — allocate a new one
auto grid_type = &mcrfpydef::PyUIGridType; auto grid_type = &mcrfpydef::PyUIGridType;
auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0); auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
@ -680,7 +719,6 @@ PyObject* UIEntity::get_grid(PyUIEntityObject* self, void* closure)
pyGrid->data = grid; pyGrid->data = grid;
pyGrid->weakreflist = NULL; pyGrid->weakreflist = NULL;
// Register in cache so future accesses return the same wrapper
if (grid->serial_number == 0) { if (grid->serial_number == 0) {
grid->serial_number = PythonObjectCache::getInstance().assignSerial(); grid->serial_number = PythonObjectCache::getInstance().assignSerial();
} }
@ -722,14 +760,24 @@ int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
return 0; return 0;
} }
// Value must be a Grid // #252: Accept both internal _GridData and GridView (unified Grid)
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) { std::shared_ptr<UIGrid> new_grid;
if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
PyUIGridViewObject* pyview = (PyUIGridViewObject*)value;
if (pyview->data->grid_data) {
new_grid = std::shared_ptr<UIGrid>(
pyview->data->grid_data, static_cast<UIGrid*>(pyview->data->grid_data.get()));
} else {
PyErr_SetString(PyExc_ValueError, "Grid has no data");
return -1;
}
} else if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
new_grid = ((PyUIGridObject*)value)->data;
} else {
PyErr_SetString(PyExc_TypeError, "grid must be a Grid or None"); PyErr_SetString(PyExc_TypeError, "grid must be a Grid or None");
return -1; return -1;
} }
auto new_grid = ((PyUIGridObject*)value)->data;
// Remove from old grid first (if any) // Remove from old grid first (if any)
if (self->data->grid && self->data->grid != new_grid) { if (self->data->grid && self->data->grid != new_grid) {
self->data->grid->spatial_hash.remove(self->data); self->data->grid->spatial_hash.remove(self->data);
@ -921,11 +969,9 @@ PyObject* UIEntity::find_path(PyUIEntityObject* self, PyObject* args, PyObject*
} }
// Build args to delegate to Grid.find_path // Build args to delegate to Grid.find_path
// Create a temporary PyUIGridObject wrapper for the grid // Create a temporary PyUIGridObject wrapper for the grid (internal _GridData type)
auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"); auto* grid_type = &mcrfpydef::PyUIGridType;
if (!grid_type) return NULL;
auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0); auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
Py_DECREF(grid_type);
if (!pyGrid) return NULL; if (!pyGrid) return NULL;
new (&pyGrid->data) std::shared_ptr<UIGrid>(grid); new (&pyGrid->data) std::shared_ptr<UIGrid>(grid);

View file

@ -745,7 +745,8 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
if (!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIFrameType) && if (!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIFrameType) &&
!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUICaptionType) && !PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUICaptionType) &&
!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUISpriteType) && !PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUISpriteType) &&
!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridType)) { !PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridType) &&
!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
Py_DECREF(child); Py_DECREF(child);
PyErr_SetString(PyExc_TypeError, "children must contain only Frame, Caption, Sprite, or Grid objects"); PyErr_SetString(PyExc_TypeError, "children must contain only Frame, Caption, Sprite, or Grid objects");
return -1; return -1;
@ -759,6 +760,8 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
drawable = ((PyUICaptionObject*)child)->data; drawable = ((PyUICaptionObject*)child)->data;
} else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUISpriteType)) { } else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUISpriteType)) {
drawable = ((PyUISpriteObject*)child)->data; drawable = ((PyUISpriteObject*)child)->data;
} else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
drawable = ((PyUIGridViewObject*)child)->data;
} else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridType)) { } else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridType)) {
drawable = ((PyUIGridObject*)child)->data; drawable = ((PyUIGridObject*)child)->data;
} }

View file

@ -168,7 +168,7 @@ extern PyMethodDef UIGrid_all_methods[];
namespace mcrfpydef { namespace mcrfpydef {
inline PyTypeObject PyUIGridType = { inline PyTypeObject PyUIGridType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Grid", .tp_name = "mcrfpy._GridData", // #252: internal type, "Grid" is now GridView
.tp_basicsize = sizeof(PyUIGridObject), .tp_basicsize = sizeof(PyUIGridObject),
.tp_itemsize = 0, .tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) .tp_dealloc = (destructor)[](PyObject* self)

View file

@ -311,11 +311,17 @@ bool UIGridView::setProperty(const std::string& name, float value)
if (name == "center_y") { center_y = value; return true; } if (name == "center_y") { center_y = value; return true; }
if (name == "zoom") { zoom = value; return true; } if (name == "zoom") { zoom = value; return true; }
if (name == "camera_rotation") { camera_rotation = value; return true; } if (name == "camera_rotation") { camera_rotation = value; return true; }
if (setShaderProperty(name, value)) return true;
return UIDrawable::setProperty(name, value); return UIDrawable::setProperty(name, value);
} }
bool UIGridView::setProperty(const std::string& name, const sf::Vector2f& value) bool UIGridView::setProperty(const std::string& name, const sf::Vector2f& value)
{ {
if (name == "center") {
center_x = value.x;
center_y = value.y;
return true;
}
return UIDrawable::setProperty(name, value); return UIDrawable::setProperty(name, value);
} }
@ -325,17 +331,25 @@ bool UIGridView::getProperty(const std::string& name, float& value) const
if (name == "center_y") { value = center_y; return true; } if (name == "center_y") { value = center_y; return true; }
if (name == "zoom") { value = zoom; return true; } if (name == "zoom") { value = zoom; return true; }
if (name == "camera_rotation") { value = camera_rotation; return true; } if (name == "camera_rotation") { value = camera_rotation; return true; }
if (getShaderProperty(name, value)) return true;
return UIDrawable::getProperty(name, value); return UIDrawable::getProperty(name, value);
} }
bool UIGridView::getProperty(const std::string& name, sf::Vector2f& value) const bool UIGridView::getProperty(const std::string& name, sf::Vector2f& value) const
{ {
if (name == "center") {
value = sf::Vector2f(center_x, center_y);
return true;
}
return UIDrawable::getProperty(name, value); return UIDrawable::getProperty(name, value);
} }
bool UIGridView::hasProperty(const std::string& name) const bool UIGridView::hasProperty(const std::string& name) const
{ {
if (name == "center_x" || name == "center_y" || name == "zoom" || name == "camera_rotation") if (name == "center_x" || name == "center_y" || name == "zoom" || name == "camera_rotation" || name == "center")
return true;
// #106: Shader uniform properties
if (hasShaderProperty(name))
return true; return true;
return UIDrawable::hasProperty(name); return UIDrawable::hasProperty(name);
} }
@ -344,77 +358,188 @@ bool UIGridView::hasProperty(const std::string& name) const
// Python API // Python API
// ========================================================================= // =========================================================================
// #252: Grid/GridView API Unification
// Two construction modes:
// Mode 1 (view): Grid(grid=existing_grid, pos=..., size=...)
// Mode 2 (factory): Grid(grid_size=..., pos=..., texture=..., ...)
//
// Mode 2 creates a UIGrid internally and copies rendering state to GridView.
// Attribute access delegates to the underlying UIGrid for data operations.
int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds) int UIGridView::init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
{ {
static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr}; // Determine mode by checking for 'grid' kwarg
PyObject* grid_obj = nullptr; PyObject* grid_kwarg = nullptr;
PyObject* pos_obj = nullptr; if (kwds) {
PyObject* size_obj = nullptr; grid_kwarg = PyDict_GetItemString(kwds, "grid"); // borrowed ref
float zoom_val = 1.0f;
PyObject* fill_obj = nullptr;
const char* name = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOfOz", const_cast<char**>(kwlist),
&grid_obj, &pos_obj, &size_obj, &zoom_val, &fill_obj, &name)) {
return -1;
} }
self->data->zoom = zoom_val; bool explicit_view = (grid_kwarg && grid_kwarg != Py_None);
if (name) self->data->UIDrawable::name = std::string(name);
// Parse grid if (explicit_view) {
if (grid_obj && grid_obj != Py_None) { // Mode 1: View of existing grid data
if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType)) { static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr};
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj; PyObject* grid_obj = nullptr;
// Create aliasing shared_ptr: shares ownership with UIGrid, points to GridData base PyObject* pos_obj = nullptr;
self->data->grid_data = std::shared_ptr<GridData>( PyObject* size_obj = nullptr;
pygrid->data, static_cast<GridData*>(pygrid->data.get())); float zoom_val = 1.0f;
self->data->ptex = pygrid->data->getTexture(); PyObject* fill_obj = nullptr;
} else { const char* name_str = nullptr;
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object");
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOfOz", const_cast<char**>(kwlist),
&grid_obj, &pos_obj, &size_obj, &zoom_val, &fill_obj, &name_str)) {
return -1; return -1;
} }
self->data->zoom = zoom_val;
if (name_str) self->data->UIDrawable::name = std::string(name_str);
// Accept both internal _GridData and GridView (unified Grid) as grid source
if (grid_obj && grid_obj != Py_None) {
if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType)) {
// Internal _GridData object
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
self->data->grid_data = std::shared_ptr<GridData>(
pygrid->data, static_cast<GridData*>(pygrid->data.get()));
self->data->ptex = pygrid->data->getTexture();
} else if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
// Another GridView (unified Grid) - share its grid_data
PyUIGridViewObject* pyview = (PyUIGridViewObject*)grid_obj;
if (pyview->data->grid_data) {
self->data->grid_data = pyview->data->grid_data;
self->data->ptex = pyview->data->ptex;
}
} else {
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object");
return -1;
}
}
if (pos_obj && pos_obj != Py_None) {
sf::Vector2f pos = PyObject_to_sfVector2f(pos_obj);
if (PyErr_Occurred()) return -1;
self->data->position = pos;
self->data->box.setPosition(pos);
}
if (size_obj && size_obj != Py_None) {
sf::Vector2f size = PyObject_to_sfVector2f(size_obj);
if (PyErr_Occurred()) return -1;
self->data->box.setSize(size);
}
if (fill_obj && fill_obj != Py_None) {
self->data->fill_color = PyColor::fromPy(fill_obj);
if (PyErr_Occurred()) return -1;
}
if (self->data->grid_data) {
self->data->center_camera();
self->data->ensureRenderTextureSize();
}
self->weakreflist = NULL;
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref);
}
}
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
return 0;
} else {
// Mode 2: Factory mode - create UIGrid internally
return init_with_data(self, args, kwds);
}
}
int UIGridView::init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds)
{
// Create a temporary UIGrid using the user's kwargs.
// This reuses UIGrid::init's complex parsing for grid_size, texture, layers, etc.
// Remove 'grid' key from kwds if present (e.g. grid=None)
PyObject* filtered_kwds = kwds ? PyDict_Copy(kwds) : PyDict_New();
if (!filtered_kwds) return -1;
if (PyDict_DelItemString(filtered_kwds, "grid") < 0) {
PyErr_Clear(); // OK if "grid" wasn't in dict
}
// Create UIGrid via Python type system, forwarding positional args
PyObject* grid_type = (PyObject*)&mcrfpydef::PyUIGridType;
PyObject* grid_py = PyObject_Call(grid_type, args, filtered_kwds);
Py_DECREF(filtered_kwds);
if (!grid_py) return -1;
PyUIGridObject* pygrid = (PyUIGridObject*)grid_py;
// Take UIGrid data via aliasing shared_ptr
self->data->grid_data = std::shared_ptr<GridData>(
pygrid->data, static_cast<GridData*>(pygrid->data.get()));
self->data->ptex = pygrid->data->getTexture();
// Copy rendering state from UIGrid to GridView
self->data->position = pygrid->data->position;
self->data->box.setPosition(pygrid->data->box.getPosition());
self->data->box.setSize(pygrid->data->box.getSize());
self->data->zoom = pygrid->data->zoom;
self->data->center_x = pygrid->data->center_x;
self->data->center_y = pygrid->data->center_y;
self->data->fill_color = sf::Color(pygrid->data->box.getFillColor());
self->data->visible = pygrid->data->visible;
self->data->opacity = pygrid->data->opacity;
self->data->z_index = pygrid->data->z_index;
self->data->name = pygrid->data->name;
self->data->camera_rotation = pygrid->data->camera_rotation;
// Copy alignment
self->data->align_type = pygrid->data->align_type;
self->data->align_margin = pygrid->data->align_margin;
self->data->align_horiz_margin = pygrid->data->align_horiz_margin;
self->data->align_vert_margin = pygrid->data->align_vert_margin;
// Copy click callback if set
if (pygrid->data->click_callable) {
PyObject* cb = pygrid->data->click_callable->borrow();
if (cb && cb != Py_None) {
self->data->click_register(cb);
}
} }
// Parse pos // Set owning_view back-reference on the GridData
if (pos_obj && pos_obj != Py_None) { self->data->grid_data->owning_view = self->data;
sf::Vector2f pos = PyObject_to_sfVector2f(pos_obj);
if (PyErr_Occurred()) return -1;
self->data->position = pos;
self->data->box.setPosition(pos);
}
// Parse size self->data->ensureRenderTextureSize();
if (size_obj && size_obj != Py_None) {
sf::Vector2f size = PyObject_to_sfVector2f(size_obj);
if (PyErr_Occurred()) return -1;
self->data->box.setSize(size);
}
// Parse fill_color
if (fill_obj && fill_obj != Py_None) {
self->data->fill_color = PyColor::fromPy(fill_obj);
if (PyErr_Occurred()) return -1;
}
// Center camera on grid if we have one
if (self->data->grid_data) {
self->data->center_camera();
self->data->ensureRenderTextureSize();
}
Py_DECREF(grid_py);
self->weakreflist = NULL; self->weakreflist = NULL;
// Register in Python object cache
if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
Py_DECREF(weakref);
}
}
self->data->is_python_subclass = (PyObject*)Py_TYPE(self) != (PyObject*)&mcrfpydef::PyUIGridViewType;
return 0; return 0;
} }
PyObject* UIGridView::repr(PyUIGridViewObject* self) PyObject* UIGridView::repr(PyUIGridViewObject* self)
{ {
std::ostringstream ss; std::ostringstream ss;
ss << "<GridView"; ss << "<Grid";
if (self->data->grid_data) { if (self->data->grid_data) {
ss << " grid=(" << self->data->grid_data->grid_w << "x" << self->data->grid_data->grid_h << ")"; ss << " (" << self->data->grid_data->grid_w << "x" << self->data->grid_data->grid_h << ")";
} else { } else {
ss << " grid=None"; ss << " (no data)";
} }
ss << " pos=(" << self->data->box.getPosition().x << ", " << self->data->box.getPosition().y << ")" ss << " pos=(" << self->data->box.getPosition().x << ", " << self->data->box.getPosition().y << ")"
<< " size=(" << self->data->box.getSize().x << ", " << self->data->box.getSize().y << ")" << " size=(" << self->data->box.getSize().x << ", " << self->data->box.getSize().y << ")"
@ -422,6 +547,78 @@ PyObject* UIGridView::repr(PyUIGridViewObject* self)
return PyUnicode_FromString(ss.str().c_str()); return PyUnicode_FromString(ss.str().c_str());
} }
// =========================================================================
// #252: Attribute delegation to underlying Grid (UIGrid)
// =========================================================================
PyObject* UIGridView::get_grid_pyobj(PyUIGridViewObject* self)
{
if (!self->data || !self->data->grid_data) {
return nullptr;
}
return get_grid(self, nullptr); // Returns new ref to internal UIGrid wrapper
}
PyObject* UIGridView::getattro(PyObject* self, PyObject* name)
{
// First try normal attribute lookup on GridView (own getsetters + methods)
PyObject* result = PyObject_GenericGetAttr(self, name);
if (result) return result;
// Only delegate on AttributeError, not other exceptions
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) return nullptr;
PyUIGridViewObject* view = (PyUIGridViewObject*)self;
if (!view->data || !view->data->grid_data) {
// No grid data - can't delegate, return original AttributeError
return nullptr;
}
// Clear error and try the internal Grid object
PyErr_Clear();
PyObject* grid = get_grid_pyobj(view);
if (!grid) {
PyErr_Format(PyExc_AttributeError,
"'Grid' object has no attribute '%.200s'",
PyUnicode_AsUTF8(name));
return nullptr;
}
result = PyObject_GetAttr(grid, name);
Py_DECREF(grid);
return result; // Returns result or propagates Grid's AttributeError
}
int UIGridView::setattro(PyObject* self, PyObject* name, PyObject* value)
{
// First try normal attribute set on GridView (own getsetters)
int result = PyObject_GenericSetAttr(self, name, value);
if (result == 0) return 0;
// Only delegate on AttributeError
if (!PyErr_ExceptionMatches(PyExc_AttributeError)) return -1;
PyUIGridViewObject* view = (PyUIGridViewObject*)self;
if (!view->data || !view->data->grid_data) {
return -1; // No grid data, return original error
}
PyErr_Clear();
PyObject* grid = get_grid_pyobj(view);
if (!grid) {
PyErr_Format(PyExc_AttributeError,
"'Grid' object has no attribute '%.200s'",
PyUnicode_AsUTF8(name));
return -1;
}
result = PyObject_SetAttr(grid, name, value);
Py_DECREF(grid);
return result;
}
// Property getters/setters // Property getters/setters
PyObject* UIGridView::get_grid(PyUIGridViewObject* self, void* closure) PyObject* UIGridView::get_grid(PyUIGridViewObject* self, void* closure)
{ {
@ -460,18 +657,41 @@ PyObject* UIGridView::get_grid(PyUIGridViewObject* self, void* closure)
int UIGridView::set_grid(PyUIGridViewObject* self, PyObject* value, void* closure) int UIGridView::set_grid(PyUIGridViewObject* self, PyObject* value, void* closure)
{ {
if (value == Py_None) { if (value == Py_None) {
if (self->data->grid_data) {
self->data->grid_data->owning_view.reset();
}
self->data->grid_data = nullptr; self->data->grid_data = nullptr;
return 0; return 0;
} }
if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) { // Accept internal _GridData (UIGrid) objects
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None"); if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) {
return -1; PyUIGridObject* pygrid = (PyUIGridObject*)value;
if (self->data->grid_data) {
self->data->grid_data->owning_view.reset();
}
self->data->grid_data = std::shared_ptr<GridData>(
pygrid->data, static_cast<GridData*>(pygrid->data.get()));
self->data->ptex = pygrid->data->getTexture();
self->data->grid_data->owning_view = self->data;
return 0;
} }
PyUIGridObject* pygrid = (PyUIGridObject*)value; // Accept GridView (unified Grid) objects - share their grid_data
self->data->grid_data = std::shared_ptr<GridData>( if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) {
pygrid->data, static_cast<GridData*>(pygrid->data.get())); PyUIGridViewObject* pyview = (PyUIGridViewObject*)value;
self->data->ptex = pygrid->data->getTexture(); if (pyview->data->grid_data) {
return 0; if (self->data->grid_data) {
self->data->grid_data->owning_view.reset();
}
self->data->grid_data = pyview->data->grid_data;
self->data->ptex = pyview->data->ptex;
// Don't override owning_view - original owner keeps it
return 0;
}
self->data->grid_data = nullptr;
return 0;
}
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
return -1;
} }
PyObject* UIGridView::get_center(PyUIGridViewObject* self, void* closure) PyObject* UIGridView::get_center(PyUIGridViewObject* self, void* closure)
@ -523,14 +743,54 @@ PyObject* UIGridView::get_texture(PyUIGridViewObject* self, void* closure)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
// Float member getters/setters for GridView-specific float members (center_x, center_y, zoom, camera_rotation)
PyObject* UIGridView::get_float_member_gv(PyUIGridViewObject* self, void* closure)
{
auto member_offset = (int)(intptr_t)closure;
float value;
switch (member_offset) {
case 4: value = self->data->center_x; break;
case 5: value = self->data->center_y; break;
case 6: value = self->data->zoom; break;
case 7: value = self->data->camera_rotation; break;
default: PyErr_SetString(PyExc_RuntimeError, "Invalid member offset"); return NULL;
}
return PyFloat_FromDouble(value);
}
int UIGridView::set_float_member_gv(PyUIGridViewObject* self, PyObject* value, void* closure)
{
float val;
if (PyFloat_Check(value)) val = PyFloat_AsDouble(value);
else if (PyLong_Check(value)) val = PyLong_AsLong(value);
else { PyErr_SetString(PyExc_TypeError, "Value must be a number"); return -1; }
auto member_offset = (int)(intptr_t)closure;
switch (member_offset) {
case 4: self->data->center_x = val; break;
case 5: self->data->center_y = val; break;
case 6: self->data->zoom = val; break;
case 7: self->data->camera_rotation = val; break;
default: PyErr_SetString(PyExc_RuntimeError, "Invalid member offset"); return -1;
}
self->data->markDirty();
return 0;
}
// #252: PyObjectType typedef for UIDRAWABLE_* macros
typedef PyUIGridViewObject PyObjectType;
// Methods and getsetters arrays // Methods and getsetters arrays
PyMethodDef UIGridView_all_methods[] = { PyMethodDef UIGridView_all_methods[] = {
UIDRAWABLE_METHODS,
{NULL} {NULL}
}; };
// GridView getsetters for view-specific AND UIDrawable base properties.
// Data properties (entities, grid_size, layers, etc.) are accessed via getattro delegation.
PyGetSetDef UIGridView::getsetters[] = { PyGetSetDef UIGridView::getsetters[] = {
{"grid", (getter)UIGridView::get_grid, (setter)UIGridView::set_grid, {"grid_data", (getter)UIGridView::get_grid, (setter)UIGridView::set_grid,
"The Grid data this view renders.", NULL}, "The underlying grid data object (for multi-view scenarios).", NULL},
{"center", (getter)UIGridView::get_center, (setter)UIGridView::set_center, {"center", (getter)UIGridView::get_center, (setter)UIGridView::set_center,
"Camera center point in pixel coordinates.", NULL}, "Camera center point in pixel coordinates.", NULL},
{"zoom", (getter)UIGridView::get_zoom, (setter)UIGridView::set_zoom, {"zoom", (getter)UIGridView::get_zoom, (setter)UIGridView::set_zoom,
@ -539,5 +799,33 @@ PyGetSetDef UIGridView::getsetters[] = {
"Background fill color.", NULL}, "Background fill color.", NULL},
{"texture", (getter)UIGridView::get_texture, NULL, {"texture", (getter)UIGridView::get_texture, NULL,
"Texture used for tile rendering (read-only).", NULL}, "Texture used for tile rendering (read-only).", NULL},
// UIDrawable base properties - applied to GridView (the rendered object)
{"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos,
"Position of the grid as Vector", (void*)PyObjectsEnum::UIGRIDVIEW},
{"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member,
"top-left corner X-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRIDVIEW << 8 | 0)},
{"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member,
"top-left corner Y-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRIDVIEW << 8 | 1)},
{"w", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member,
"visible widget width", (void*)((intptr_t)PyObjectsEnum::UIGRIDVIEW << 8 | 2)},
{"h", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member,
"visible widget height", (void*)((intptr_t)PyObjectsEnum::UIGRIDVIEW << 8 | 3)},
{"center_x", (getter)UIGridView::get_float_member_gv, (setter)UIGridView::set_float_member_gv,
"center of the view X-coordinate", (void*)4},
{"center_y", (getter)UIGridView::get_float_member_gv, (setter)UIGridView::set_float_member_gv,
"center of the view Y-coordinate", (void*)5},
{"camera_rotation", (getter)UIGridView::get_float_member_gv, (setter)UIGridView::set_float_member_gv,
"Rotation of grid contents around camera center (degrees).", (void*)7},
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
"Callable executed when object is clicked.", (void*)PyObjectsEnum::UIGRIDVIEW},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
"Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UIGRIDVIEW},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,
"Name for finding elements", (void*)PyObjectsEnum::UIGRIDVIEW},
UIDRAWABLE_GETSETTERS,
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRIDVIEW),
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIGRIDVIEW),
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIGRIDVIEW),
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIGRIDVIEW),
{NULL} {NULL}
}; };

View file

@ -86,8 +86,16 @@ public:
// Python API // Python API
// ========================================================================= // =========================================================================
static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds); static int init(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
static int init_with_data(PyUIGridViewObject* self, PyObject* args, PyObject* kwds);
static PyObject* repr(PyUIGridViewObject* self); static PyObject* repr(PyUIGridViewObject* self);
// #252 - Attribute delegation to underlying Grid
static PyObject* getattro(PyObject* self, PyObject* name);
static int setattro(PyObject* self, PyObject* name, PyObject* value);
// Helper: get the underlying Grid as a Python object (borrowed-like via cache)
static PyObject* get_grid_pyobj(PyUIGridViewObject* self);
static PyObject* get_grid(PyUIGridViewObject* self, void* closure); static PyObject* get_grid(PyUIGridViewObject* self, void* closure);
static int set_grid(PyUIGridViewObject* self, PyObject* value, void* closure); static int set_grid(PyUIGridViewObject* self, PyObject* value, void* closure);
static PyObject* get_center(PyUIGridViewObject* self, void* closure); static PyObject* get_center(PyUIGridViewObject* self, void* closure);
@ -97,6 +105,8 @@ public:
static PyObject* get_fill_color(PyUIGridViewObject* self, void* closure); static PyObject* get_fill_color(PyUIGridViewObject* self, void* closure);
static int set_fill_color(PyUIGridViewObject* self, PyObject* value, void* closure); static int set_fill_color(PyUIGridViewObject* self, PyObject* value, void* closure);
static PyObject* get_texture(PyUIGridViewObject* self, void* closure); static PyObject* get_texture(PyUIGridViewObject* self, void* closure);
static PyObject* get_float_member_gv(PyUIGridViewObject* self, void* closure);
static int set_float_member_gv(PyUIGridViewObject* self, PyObject* value, void* closure);
static PyMethodDef methods[]; static PyMethodDef methods[];
static PyGetSetDef getsetters[]; static PyGetSetDef getsetters[];
@ -106,9 +116,12 @@ public:
extern PyMethodDef UIGridView_all_methods[]; extern PyMethodDef UIGridView_all_methods[];
namespace mcrfpydef { namespace mcrfpydef {
// #252: GridView is the primary user-facing type. "mcrfpy.Grid" is an alias.
// Grid() auto-creates a GridData (UIGrid internally); GridView(grid=...) wraps existing data.
// Attribute access delegates to underlying Grid for data properties/methods.
inline PyTypeObject PyUIGridViewType = { inline PyTypeObject PyUIGridViewType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.GridView", .tp_name = "mcrfpy.Grid", // #252: primary name is Grid
.tp_basicsize = sizeof(PyUIGridViewObject), .tp_basicsize = sizeof(PyUIGridViewObject),
.tp_itemsize = 0, .tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self) .tp_dealloc = (destructor)[](PyObject* self)
@ -118,27 +131,53 @@ namespace mcrfpydef {
if (obj->weakreflist != NULL) { if (obj->weakreflist != NULL) {
PyObject_ClearWeakRefs(self); PyObject_ClearWeakRefs(self);
} }
// Clear owning_view back-reference before releasing grid_data
if (obj->data && obj->data->grid_data) {
obj->data->grid_data->owning_view.reset();
}
obj->data.reset(); obj->data.reset();
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
}, },
.tp_repr = (reprfunc)UIGridView::repr, .tp_repr = (reprfunc)UIGridView::repr,
.tp_getattro = UIGridView::getattro, // #252: attribute delegation to Grid
.tp_setattro = UIGridView::setattro, // #252: attribute delegation to Grid
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.tp_doc = PyDoc_STR( .tp_doc = PyDoc_STR(
"GridView(grid=None, pos=None, size=None, **kwargs)\n\n" "Grid(grid_size=None, pos=None, size=None, texture=None, **kwargs)\n\n"
"A rendering view for a Grid's data. Multiple GridViews can display\n" "A grid-based UI element for tile-based rendering and entity management.\n"
"the same Grid with different camera positions, zoom levels, etc.\n\n" "Creates and owns grid data (cells, entities, layers) with an integrated\n"
"rendering view (camera, zoom, perspective).\n\n"
"Can also be constructed as a view of existing grid data:\n"
" Grid(grid=existing_grid, pos=..., size=...)\n\n"
"Args:\n" "Args:\n"
" grid (Grid): The Grid to render. Required.\n" " grid_size (tuple): Grid dimensions as (grid_w, grid_h). Default: (2, 2)\n"
" pos (tuple): Position as (x, y). Default: (0, 0)\n" " pos (tuple): Position as (x, y). Default: (0, 0)\n"
" size (tuple): Size as (w, h). Default: (256, 256)\n\n" " size (tuple): Size as (w, h). Default: auto-calculated\n"
" texture (Texture): Tile texture atlas. Default: default texture\n\n"
"Keyword Args:\n" "Keyword Args:\n"
" grid (Grid): Existing Grid to view (creates view of shared data).\n"
" fill_color (Color): Background fill color.\n"
" on_click (callable): Click event handler.\n"
" center_x, center_y (float): Camera center coordinates.\n"
" zoom (float): Zoom level. Default: 1.0\n" " zoom (float): Zoom level. Default: 1.0\n"
" center (tuple): Camera center (x, y) in pixels. Default: grid center\n" " visible (bool): Visibility. Default: True\n"
" fill_color (Color): Background color. Default: dark gray\n"), " opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" z_index (int): Rendering order. Default: 0\n"
" name (str): Element name.\n"
" layers (list): List of ColorLayer/TileLayer objects.\n"),
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int { .tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
PyUIGridViewObject* obj = (PyUIGridViewObject*)self;
if (obj->data && obj->data->click_callable) {
PyObject* callback = obj->data->click_callable->borrow();
if (callback && callback != Py_None) Py_VISIT(callback);
}
return 0; return 0;
}, },
.tp_clear = [](PyObject* self) -> int { .tp_clear = [](PyObject* self) -> int {
PyUIGridViewObject* obj = (PyUIGridViewObject*)self;
if (obj->data) {
obj->data->click_unregister();
}
return 0; return 0;
}, },
.tp_methods = UIGridView_all_methods, .tp_methods = UIGridView_all_methods,