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

View file

@ -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<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
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<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
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<GridLayer>(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<GridLayer>(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;

View file

@ -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<PyFont>("assets/JetbrainsMono.ttf");
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;
}
}
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) {

View file

@ -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<UIDrawable> 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<UIGrid>(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<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();
}
@ -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;

View file

@ -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 <algorithm>
@ -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<UIGrid> 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<UIGrid>(
pyview->data->grid_data, static_cast<UIGrid*>(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<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");
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<UIGrid>(grid);

View file

@ -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;
}

View file

@ -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)

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 == "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<char**>(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<GridData>(
pygrid->data, static_cast<GridData*>(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<char**>(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<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
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 << "<GridView";
ss << "<Grid";
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 {
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<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;
self->data->grid_data = std::shared_ptr<GridData>(
pygrid->data, static_cast<GridData*>(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}
};

View file

@ -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,