From 109bc21d906ce0ff85cecf0baa27d1ab814de2db Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 4 Apr 2026 04:34:11 -0400 Subject: [PATCH] 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 --- src/GridData.h | 6 + src/GridLayers.cpp | 106 +++++++----- src/McRFPy_API.cpp | 12 +- src/PyAnimation.cpp | 9 + src/UIDrawable.cpp | 63 ++++++- src/UIEntity.cpp | 92 +++++++--- src/UIFrame.cpp | 5 +- src/UIGrid.h | 2 +- src/UIGridView.cpp | 412 +++++++++++++++++++++++++++++++++++++------- src/UIGridView.h | 55 +++++- 10 files changed, 616 insertions(+), 146 deletions(-) diff --git a/src/GridData.h b/src/GridData.h index 2c98c36..2f5d15a 100644 --- a/src/GridData.h +++ b/src/GridData.h @@ -26,6 +26,7 @@ class DijkstraMap; class UIEntity; class UIDrawable; +class UIGridView; class PyTexture; class GridData { @@ -130,6 +131,11 @@ public: std::shared_ptr>> children; bool children_need_sort = true; + // ========================================================================= + // #252 - Owning GridView back-reference (for Entity.grid → GridView lookup) + // ========================================================================= + std::weak_ptr owning_view; + protected: // Initialize grid storage (flat or chunked) and TCOD map void initStorage(int gx, int gy, GridData* parent_ref); diff --git a/src/GridLayers.cpp b/src/GridLayers.cpp index 6f6dd56..f5d452e 100644 --- a/src/GridLayers.cpp +++ b/src/GridLayers.cpp @@ -1,5 +1,6 @@ #include "GridLayers.h" #include "UIGrid.h" +#include "UIGridView.h" #include "UIEntity.h" #include "PyColor.h" #include "PyTexture.h" @@ -1677,25 +1678,31 @@ int PyGridLayerAPI::ColorLayer_set_grid(PyColorLayerObject* self, PyObject* valu return 0; } - // Validate it's a Grid - auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); - if (!mcrfpy_module) return -1; - - 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); + // Validate it's a Grid (GridView) or internal _GridData + if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType) && + !PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) { PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None"); return -1; } - Py_DECREF(grid_type); - PyUIGridObject* py_grid = (PyUIGridObject*)value; + // Extract UIGrid shared_ptr from Grid (UIGridView) or _GridData (UIGrid) + std::shared_ptr 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(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 - if (self->grid.get() == py_grid->data.get()) { + if (self->grid.get() == target_grid.get()) { 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 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()) { existing->parent_grid = nullptr; - py_grid->data->removeLayer(existing); + target_grid->removeLayer(existing); } } // Lazy allocation: resize if layer is (0,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); - } else if (self->data->grid_x != py_grid->data->grid_w || - self->data->grid_y != py_grid->data->grid_h) { + self->data->resize(target_grid->grid_w, target_grid->grid_h); + } else if (self->data->grid_x != target_grid->grid_w || + self->data->grid_y != target_grid->grid_h) { PyErr_Format(PyExc_ValueError, "Layer size (%d, %d) does not match Grid size (%d, %d)", 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(); return -1; } // Link to new grid - self->data->parent_grid = py_grid->data.get(); - py_grid->data->layers.push_back(self->data); - py_grid->data->layers_need_sort = true; - self->grid = py_grid->data; + self->data->parent_grid = target_grid.get(); + target_grid->layers.push_back(self->data); + target_grid->layers_need_sort = true; + self->grid = target_grid; return 0; } @@ -2326,25 +2333,30 @@ int PyGridLayerAPI::TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, return 0; } - // Validate it's a Grid - auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); - if (!mcrfpy_module) return -1; - - 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); + // Validate it's a Grid (GridView) or internal _GridData + if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType) && + !PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) { PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None"); return -1; } - Py_DECREF(grid_type); - PyUIGridObject* py_grid = (PyUIGridObject*)value; + // Extract UIGrid shared_ptr from Grid (UIGridView) or _GridData (UIGrid) + std::shared_ptr target_grid; + if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) { + auto* pyview = (PyUIGridViewObject*)value; + if (pyview->data->grid_data) { + target_grid = std::static_pointer_cast(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 - if (self->grid.get() == py_grid->data.get()) { + if (self->grid.get() == target_grid.get()) { 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 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()) { existing->parent_grid = nullptr; - py_grid->data->removeLayer(existing); + target_grid->removeLayer(existing); } } // Lazy allocation: resize if layer is (0,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); - } else if (self->data->grid_x != py_grid->data->grid_w || - self->data->grid_y != py_grid->data->grid_h) { + self->data->resize(target_grid->grid_w, target_grid->grid_h); + } else if (self->data->grid_x != target_grid->grid_w || + self->data->grid_y != target_grid->grid_h) { PyErr_Format(PyExc_ValueError, "Layer size (%d, %d) does not match Grid size (%d, %d)", 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(); return -1; } // Link to new grid - self->data->parent_grid = py_grid->data.get(); - py_grid->data->layers.push_back(std::static_pointer_cast(self->data)); - py_grid->data->layers_need_sort = true; - self->grid = py_grid->data; + self->data->parent_grid = target_grid.get(); + target_grid->layers.push_back(std::static_pointer_cast(self->data)); + target_grid->layers_need_sort = true; + self->grid = target_grid; // Inherit grid texture if TileLayer has none (#254) if (!self->data->texture) { - self->data->texture = py_grid->data->getTexture(); + self->data->texture = target_grid->getTexture(); } return 0; diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 2a28d50..3ddfb56 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -482,9 +482,9 @@ PyObject* PyInit_mcrfpy() &PyDrawableType, /*UI widgets*/ - &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType, + &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType, - &mcrfpydef::PyUIGridViewType, + &mcrfpydef::PyUIGridViewType, // #252: GridView IS the primary "Grid" type /*3D entities*/ &mcrfpydef::PyEntity3DType, &mcrfpydef::PyEntityCollection3DType, @@ -547,6 +547,9 @@ PyObject* PyInit_mcrfpy() // Types that are used internally but NOT exported to module namespace (#189) // These still need PyType_Ready() but are not added to module 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*/ &PyUIGridPointType, &PyUIGridPointStateType, @@ -682,6 +685,11 @@ PyObject* PyInit_mcrfpy() 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 McRFPy_API::default_font = std::make_shared("assets/JetbrainsMono.ttf"); McRFPy_API::default_texture = std::make_shared("assets/kenney_tinydungeon.png", 16, 16); diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index 38ac6fc..a8bbd72 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -244,6 +244,15 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args, PyObject* 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)) { PyUIGridObject* grid = (PyUIGridObject*)target_obj; if (grid->data) { diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index 67e71cc..d82dfe3 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -36,6 +36,8 @@ static UIDrawable* extractDrawable(PyObject* self, PyObjectsEnum objtype) { return ((PyUICircleObject*)self)->data.get(); case PyObjectsEnum::UIARC: return ((PyUIArcObject*)self)->data.get(); + case PyObjectsEnum::UIGRIDVIEW: + return ((PyUIGridViewObject*)self)->data.get(); default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); return nullptr; @@ -59,6 +61,8 @@ static std::shared_ptr extractDrawableShared(PyObject* self, PyObjec return ((PyUICircleObject*)self)->data; case PyObjectsEnum::UIARC: return ((PyUIArcObject*)self)->data; + case PyObjectsEnum::UIGRIDVIEW: + return ((PyUIGridViewObject*)self)->data; default: return nullptr; } @@ -279,6 +283,12 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) { else ptr = NULL; break; + case PyObjectsEnum::UIGRIDVIEW: + if (((PyUIGridViewObject*)self)->data->click_callable) + ptr = ((PyUIGridViewObject*)self)->data->click_callable->borrow(); + else + ptr = NULL; + break; default: PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click"); return NULL; @@ -316,6 +326,9 @@ int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) { case PyObjectsEnum::UIARC: target = (((PyUIArcObject*)self)->data.get()); break; + case PyObjectsEnum::UIGRIDVIEW: + target = (((PyUIGridViewObject*)self)->data.get()); + break; default: PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _set_click"); return -1; @@ -1033,7 +1046,7 @@ void UIDrawable::removeFromParent() { } 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(p); auto& children = *grid->children; @@ -1045,6 +1058,19 @@ void UIDrawable::removeFromParent() { } grid->children_need_sort = true; } + else if (p->derived_type() == PyObjectsEnum::UIGRIDVIEW) { + auto view = std::static_pointer_cast(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(); } @@ -1416,6 +1442,9 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) { case PyObjectsEnum::UIARC: drawable = ((PyUIArcObject*)self)->data; break; + case PyObjectsEnum::UIGRIDVIEW: + drawable = ((PyUIGridViewObject*)self)->data; + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); 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) bool is_frame = PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIFrameType); 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); - 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"); return -1; } @@ -1476,6 +1506,13 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) { auto frame = ((PyUIFrameObject*)value)->data; children_ptr = &frame->children; 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) { auto grid = ((PyUIGridObject*)value)->data; children_ptr = &grid->children; @@ -1624,6 +1661,10 @@ PyObject* UIDrawable::get_on_enter(PyObject* self, void* closure) { if (((PyUIArcObject*)self)->data->on_enter_callable) ptr = ((PyUIArcObject*)self)->data->on_enter_callable->borrow(); break; + case PyObjectsEnum::UIGRIDVIEW: + if (((PyUIGridViewObject*)self)->data->on_enter_callable) + ptr = ((PyUIGridViewObject*)self)->data->on_enter_callable->borrow(); + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter"); return NULL; @@ -1661,6 +1702,9 @@ int UIDrawable::set_on_enter(PyObject* self, PyObject* value, void* closure) { case PyObjectsEnum::UIARC: target = ((PyUIArcObject*)self)->data.get(); break; + case PyObjectsEnum::UIGRIDVIEW: + target = ((PyUIGridViewObject*)self)->data.get(); + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_enter"); return -1; @@ -1708,6 +1752,10 @@ PyObject* UIDrawable::get_on_exit(PyObject* self, void* closure) { if (((PyUIArcObject*)self)->data->on_exit_callable) ptr = ((PyUIArcObject*)self)->data->on_exit_callable->borrow(); break; + case PyObjectsEnum::UIGRIDVIEW: + if (((PyUIGridViewObject*)self)->data->on_exit_callable) + ptr = ((PyUIGridViewObject*)self)->data->on_exit_callable->borrow(); + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit"); return NULL; @@ -1745,6 +1793,9 @@ int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) { case PyObjectsEnum::UIARC: target = ((PyUIArcObject*)self)->data.get(); break; + case PyObjectsEnum::UIGRIDVIEW: + target = ((PyUIGridViewObject*)self)->data.get(); + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_exit"); return -1; @@ -1801,6 +1852,10 @@ PyObject* UIDrawable::get_on_move(PyObject* self, void* closure) { if (((PyUIArcObject*)self)->data->on_move_callable) ptr = ((PyUIArcObject*)self)->data->on_move_callable->borrow(); break; + case PyObjectsEnum::UIGRIDVIEW: + if (((PyUIGridViewObject*)self)->data->on_move_callable) + ptr = ((PyUIGridViewObject*)self)->data->on_move_callable->borrow(); + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move"); return NULL; @@ -1838,6 +1893,9 @@ int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) { case PyObjectsEnum::UIARC: target = ((PyUIArcObject*)self)->data.get(); break; + case PyObjectsEnum::UIGRIDVIEW: + target = ((PyUIGridViewObject*)self)->data.get(); + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance for on_move"); return -1; @@ -2189,6 +2247,7 @@ PyObject* UIDrawable::py_realign(PyObject* self, PyObject* args) { 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::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::PyUILineType)) objtype = PyObjectsEnum::UILINE; else if (PyObject_IsInstance(self, (PyObject*)&mcrfpydef::PyUICircleType)) objtype = PyObjectsEnum::UICIRCLE; diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index f6f30b3..a752bf3 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -1,5 +1,6 @@ #include "UIEntity.h" #include "UIGrid.h" +#include "UIGridView.h" // #252: Entity.grid accepts GridView #include "UIGridPathfinding.h" #include "McRFPy_API.h" #include @@ -221,8 +222,9 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - // Handle grid argument - if (grid_obj && !PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType)) { + // Handle grid argument - accept both internal _GridData and GridView (unified Grid) + 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"); return -1; } @@ -303,15 +305,24 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { // Handle grid attachment if (grid_obj) { - PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj; - self->data->grid = pygrid->data; - // Append entity to grid's entity list - pygrid->data->entities->push_back(self->data); - // Insert into spatial hash for O(1) cell queries (#253) - pygrid->data->spatial_hash.insert(self->data); - - // Don't initialize gridstate here - lazy initialization to support large numbers of entities - // gridstate will be initialized when visibility is updated or accessed + std::shared_ptr grid_ptr; + if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridViewType)) { + // #252: GridView (unified Grid) - extract internal UIGrid + PyUIGridViewObject* pyview = (PyUIGridViewObject*)grid_obj; + if (pyview->data->grid_data) { + grid_ptr = std::shared_ptr( + pyview->data->grid_data, static_cast(pyview->data->grid_data.get())); + } + } 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; } @@ -664,15 +675,43 @@ PyObject* UIEntity::get_grid(PyUIEntityObject* self, void* closure) 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) { PyObject* cached = PythonObjectCache::getInstance().lookup(grid->serial_number); if (cached) { - return cached; // Already INCREF'd by lookup + return cached; } } - // No cached wrapper — allocate a new one auto grid_type = &mcrfpydef::PyUIGridType; 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->weakreflist = NULL; - // Register in cache so future accesses return the same wrapper if (grid->serial_number == 0) { grid->serial_number = PythonObjectCache::getInstance().assignSerial(); } @@ -722,14 +760,24 @@ int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure) return 0; } - // Value must be a Grid - if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) { + // #252: Accept both internal _GridData and GridView (unified Grid) + std::shared_ptr new_grid; + if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) { + PyUIGridViewObject* pyview = (PyUIGridViewObject*)value; + if (pyview->data->grid_data) { + new_grid = std::shared_ptr( + pyview->data->grid_data, static_cast(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"); return -1; } - auto new_grid = ((PyUIGridObject*)value)->data; - // Remove from old grid first (if any) if (self->data->grid && self->data->grid != new_grid) { 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 - // Create a temporary PyUIGridObject wrapper for the grid - auto grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"); - if (!grid_type) return NULL; + // Create a temporary PyUIGridObject wrapper for the grid (internal _GridData type) + auto* grid_type = &mcrfpydef::PyUIGridType; auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0); - Py_DECREF(grid_type); if (!pyGrid) return NULL; new (&pyGrid->data) std::shared_ptr(grid); diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index bb25547..159725c 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -745,7 +745,8 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) if (!PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIFrameType) && !PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUICaptionType) && !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); PyErr_SetString(PyExc_TypeError, "children must contain only Frame, Caption, Sprite, or Grid objects"); return -1; @@ -759,6 +760,8 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) drawable = ((PyUICaptionObject*)child)->data; } else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUISpriteType)) { drawable = ((PyUISpriteObject*)child)->data; + } else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridViewType)) { + drawable = ((PyUIGridViewObject*)child)->data; } else if (PyObject_IsInstance(child, (PyObject*)&mcrfpydef::PyUIGridType)) { drawable = ((PyUIGridObject*)child)->data; } diff --git a/src/UIGrid.h b/src/UIGrid.h index 841e973..fee9220 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -168,7 +168,7 @@ extern PyMethodDef UIGrid_all_methods[]; namespace mcrfpydef { inline PyTypeObject PyUIGridType = { .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_itemsize = 0, .tp_dealloc = (destructor)[](PyObject* self) diff --git a/src/UIGridView.cpp b/src/UIGridView.cpp index 1e2f3a9..d1ee84c 100644 --- a/src/UIGridView.cpp +++ b/src/UIGridView.cpp @@ -311,11 +311,17 @@ bool UIGridView::setProperty(const std::string& name, float value) if (name == "center_y") { center_y = value; return true; } if (name == "zoom") { zoom = value; return true; } if (name == "camera_rotation") { camera_rotation = value; return true; } + if (setShaderProperty(name, value)) return true; return UIDrawable::setProperty(name, 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); } @@ -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 == "zoom") { value = zoom; return true; } if (name == "camera_rotation") { value = camera_rotation; return true; } + if (getShaderProperty(name, value)) return true; return UIDrawable::getProperty(name, value); } 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); } 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 UIDrawable::hasProperty(name); } @@ -344,77 +358,188 @@ bool UIGridView::hasProperty(const std::string& name) const // 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) { - static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr}; - PyObject* grid_obj = nullptr; - PyObject* pos_obj = nullptr; - PyObject* size_obj = nullptr; - float zoom_val = 1.0f; - PyObject* fill_obj = nullptr; - const char* name = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOfOz", const_cast(kwlist), - &grid_obj, &pos_obj, &size_obj, &zoom_val, &fill_obj, &name)) { - return -1; + // Determine mode by checking for 'grid' kwarg + PyObject* grid_kwarg = nullptr; + if (kwds) { + grid_kwarg = PyDict_GetItemString(kwds, "grid"); // borrowed ref } - self->data->zoom = zoom_val; - if (name) self->data->UIDrawable::name = std::string(name); + bool explicit_view = (grid_kwarg && grid_kwarg != Py_None); - // Parse grid - if (grid_obj && grid_obj != Py_None) { - if (PyObject_IsInstance(grid_obj, (PyObject*)&mcrfpydef::PyUIGridType)) { - PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj; - // Create aliasing shared_ptr: shares ownership with UIGrid, points to GridData base - self->data->grid_data = std::shared_ptr( - pygrid->data, static_cast(pygrid->data.get())); - self->data->ptex = pygrid->data->getTexture(); - } else { - PyErr_SetString(PyExc_TypeError, "grid must be a Grid object"); + if (explicit_view) { + // Mode 1: View of existing grid data + static const char* kwlist[] = {"grid", "pos", "size", "zoom", "fill_color", "name", nullptr}; + PyObject* grid_obj = nullptr; + PyObject* pos_obj = nullptr; + PyObject* size_obj = nullptr; + float zoom_val = 1.0f; + PyObject* fill_obj = nullptr; + const char* name_str = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOfOz", const_cast(kwlist), + &grid_obj, &pos_obj, &size_obj, &zoom_val, &fill_obj, &name_str)) { 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( + pygrid->data, static_cast(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( + pygrid->data, static_cast(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 - 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); - } + // Set owning_view back-reference on the GridData + self->data->grid_data->owning_view = self->data; - // Parse size - 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(); - } + self->data->ensureRenderTextureSize(); + Py_DECREF(grid_py); 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; } PyObject* UIGridView::repr(PyUIGridViewObject* self) { std::ostringstream ss; - ss << "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 { - ss << " grid=None"; + ss << " (no data)"; } ss << " pos=(" << self->data->box.getPosition().x << ", " << self->data->box.getPosition().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()); } +// ========================================================================= +// #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 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) { if (value == Py_None) { + if (self->data->grid_data) { + self->data->grid_data->owning_view.reset(); + } self->data->grid_data = nullptr; return 0; } - if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) { - PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None"); - return -1; + // Accept internal _GridData (UIGrid) objects + if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridType)) { + PyUIGridObject* pygrid = (PyUIGridObject*)value; + if (self->data->grid_data) { + self->data->grid_data->owning_view.reset(); + } + self->data->grid_data = std::shared_ptr( + pygrid->data, static_cast(pygrid->data.get())); + self->data->ptex = pygrid->data->getTexture(); + self->data->grid_data->owning_view = self->data; + return 0; } - PyUIGridObject* pygrid = (PyUIGridObject*)value; - self->data->grid_data = std::shared_ptr( - pygrid->data, static_cast(pygrid->data.get())); - self->data->ptex = pygrid->data->getTexture(); - return 0; + // Accept GridView (unified Grid) objects - share their grid_data + if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyUIGridViewType)) { + PyUIGridViewObject* pyview = (PyUIGridViewObject*)value; + if (pyview->data->grid_data) { + 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) @@ -523,14 +743,54 @@ PyObject* UIGridView::get_texture(PyUIGridViewObject* self, void* closure) 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 PyMethodDef UIGridView_all_methods[] = { + UIDRAWABLE_METHODS, {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[] = { - {"grid", (getter)UIGridView::get_grid, (setter)UIGridView::set_grid, - "The Grid data this view renders.", NULL}, + {"grid_data", (getter)UIGridView::get_grid, (setter)UIGridView::set_grid, + "The underlying grid data object (for multi-view scenarios).", NULL}, {"center", (getter)UIGridView::get_center, (setter)UIGridView::set_center, "Camera center point in pixel coordinates.", NULL}, {"zoom", (getter)UIGridView::get_zoom, (setter)UIGridView::set_zoom, @@ -539,5 +799,33 @@ PyGetSetDef UIGridView::getsetters[] = { "Background fill color.", NULL}, {"texture", (getter)UIGridView::get_texture, 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} }; diff --git a/src/UIGridView.h b/src/UIGridView.h index b8f605d..db054bd 100644 --- a/src/UIGridView.h +++ b/src/UIGridView.h @@ -86,8 +86,16 @@ public: // Python API // ========================================================================= 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); + // #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 int set_grid(PyUIGridViewObject* self, PyObject* value, 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 int set_fill_color(PyUIGridViewObject* self, PyObject* value, 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 PyGetSetDef getsetters[]; @@ -106,9 +116,12 @@ public: extern PyMethodDef UIGridView_all_methods[]; 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 = { .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_itemsize = 0, .tp_dealloc = (destructor)[](PyObject* self) @@ -118,27 +131,53 @@ namespace mcrfpydef { if (obj->weakreflist != NULL) { 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(); Py_TYPE(self)->tp_free(self); }, .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_doc = PyDoc_STR( - "GridView(grid=None, pos=None, size=None, **kwargs)\n\n" - "A rendering view for a Grid's data. Multiple GridViews can display\n" - "the same Grid with different camera positions, zoom levels, etc.\n\n" + "Grid(grid_size=None, pos=None, size=None, texture=None, **kwargs)\n\n" + "A grid-based UI element for tile-based rendering and entity management.\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" - " 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" - " 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" + " 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" - " center (tuple): Camera center (x, y) in pixels. Default: grid center\n" - " fill_color (Color): Background color. Default: dark gray\n"), + " visible (bool): Visibility. Default: True\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 { + 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; }, .tp_clear = [](PyObject* self) -> int { + PyUIGridViewObject* obj = (PyUIGridViewObject*)self; + if (obj->data) { + obj->data->click_unregister(); + } return 0; }, .tp_methods = UIGridView_all_methods,