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:
parent
a61f05229f
commit
109bc21d90
10 changed files with 616 additions and 146 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue