grid layer API modernization
This commit is contained in:
parent
b66b934d8f
commit
001cc6efd6
5 changed files with 977 additions and 184 deletions
|
|
@ -803,17 +803,23 @@ PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = {
|
||||||
"Whether the layer is rendered.", NULL},
|
"Whether the layer is rendered.", NULL},
|
||||||
{"grid_size", (getter)PyGridLayerAPI::ColorLayer_get_grid_size, NULL,
|
{"grid_size", (getter)PyGridLayerAPI::ColorLayer_get_grid_size, NULL,
|
||||||
"Layer dimensions as (width, height) tuple.", NULL},
|
"Layer dimensions as (width, height) tuple.", NULL},
|
||||||
|
{"name", (getter)PyGridLayerAPI::ColorLayer_get_name, NULL,
|
||||||
|
"Layer name (str, read-only). Used for Grid.layer(name) lookup.", NULL},
|
||||||
|
{"grid", (getter)PyGridLayerAPI::ColorLayer_get_grid,
|
||||||
|
(setter)PyGridLayerAPI::ColorLayer_set_grid,
|
||||||
|
"Parent Grid or None. Setting manages layer association and handles lazy allocation.", NULL},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
|
int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* kwlist[] = {"z_index", "grid_size", NULL};
|
static const char* kwlist[] = {"z_index", "name", "grid_size", NULL};
|
||||||
int z_index = -1;
|
int z_index = -1;
|
||||||
|
const char* name_str = nullptr;
|
||||||
PyObject* grid_size_obj = nullptr;
|
PyObject* grid_size_obj = nullptr;
|
||||||
int grid_x = 0, grid_y = 0;
|
int grid_x = 0, grid_y = 0;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iO", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izO", const_cast<char**>(kwlist),
|
||||||
&z_index, &grid_size_obj)) {
|
&z_index, &name_str, &grid_size_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -836,8 +842,11 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the layer (will be attached to grid via add_layer)
|
// Create the layer (will be attached to grid via add_layer or layer.grid setter)
|
||||||
self->data = std::make_shared<ColorLayer>(z_index, grid_x, grid_y, nullptr);
|
self->data = std::make_shared<ColorLayer>(z_index, grid_x, grid_y, nullptr);
|
||||||
|
if (name_str) {
|
||||||
|
self->data->name = name_str;
|
||||||
|
}
|
||||||
self->grid.reset();
|
self->grid.reset();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -1598,12 +1607,137 @@ PyObject* PyGridLayerAPI::ColorLayer_get_grid_size(PyColorLayerObject* self, voi
|
||||||
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
|
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject* PyGridLayerAPI::ColorLayer_get_name(PyColorLayerObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return PyUnicode_FromString(self->data->name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyGridLayerAPI::ColorLayer_get_grid(PyColorLayerObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check actual parent_grid pointer - it may have been cleared by name collision handling
|
||||||
|
if (!self->data->parent_grid) {
|
||||||
|
// Sync Python wrapper's state if the C++ layer was unlinked externally
|
||||||
|
self->grid.reset();
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->grid) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Python Grid wrapper for the parent grid
|
||||||
|
auto* grid_type = (PyTypeObject*)PyObject_GetAttrString(
|
||||||
|
PyImport_ImportModule("mcrfpy"), "Grid");
|
||||||
|
if (!grid_type) return NULL;
|
||||||
|
|
||||||
|
PyUIGridObject* py_grid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
|
||||||
|
Py_DECREF(grid_type);
|
||||||
|
if (!py_grid) return NULL;
|
||||||
|
|
||||||
|
py_grid->data = self->grid;
|
||||||
|
py_grid->weakreflist = NULL;
|
||||||
|
return (PyObject*)py_grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PyGridLayerAPI::ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle None - unlink from current grid
|
||||||
|
if (value == Py_None || value == NULL) {
|
||||||
|
if (self->grid) {
|
||||||
|
self->data->parent_grid = nullptr;
|
||||||
|
self->grid->removeLayer(self->data);
|
||||||
|
self->grid.reset();
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
Py_DECREF(grid_type);
|
||||||
|
|
||||||
|
PyUIGridObject* py_grid = (PyUIGridObject*)value;
|
||||||
|
|
||||||
|
// Check if already attached to this grid
|
||||||
|
if (self->grid.get() == py_grid->data.get()) {
|
||||||
|
return 0; // Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlink from old grid if any
|
||||||
|
if (self->grid) {
|
||||||
|
self->data->parent_grid = nullptr;
|
||||||
|
self->grid->removeLayer(self->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for protected names
|
||||||
|
if (!self->data->name.empty() && UIGrid::isProtectedLayerName(self->data->name)) {
|
||||||
|
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", self->data->name.c_str());
|
||||||
|
self->grid.reset();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle name collision - unlink existing layer with same name
|
||||||
|
if (!self->data->name.empty()) {
|
||||||
|
auto existing = py_grid->data->getLayerByName(self->data->name);
|
||||||
|
if (existing && existing.get() != self->data.get()) {
|
||||||
|
existing->parent_grid = nullptr;
|
||||||
|
py_grid->data->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) {
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
|
PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
if (!self->data) {
|
if (!self->data) {
|
||||||
ss << "<ColorLayer (invalid)>";
|
ss << "<ColorLayer (invalid)>";
|
||||||
} else {
|
} else {
|
||||||
ss << "<ColorLayer z_index=" << self->data->z_index
|
ss << "<ColorLayer";
|
||||||
|
if (!self->data->name.empty()) {
|
||||||
|
ss << " '" << self->data->name << "'";
|
||||||
|
}
|
||||||
|
ss << " z_index=" << self->data->z_index
|
||||||
<< " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
|
<< " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
|
||||||
<< " visible=" << (self->data->visible ? "True" : "False") << ">";
|
<< " visible=" << (self->data->visible ? "True" : "False") << ">";
|
||||||
}
|
}
|
||||||
|
|
@ -1679,18 +1813,24 @@ PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = {
|
||||||
"Texture atlas for tile sprites.", NULL},
|
"Texture atlas for tile sprites.", NULL},
|
||||||
{"grid_size", (getter)PyGridLayerAPI::TileLayer_get_grid_size, NULL,
|
{"grid_size", (getter)PyGridLayerAPI::TileLayer_get_grid_size, NULL,
|
||||||
"Layer dimensions as (width, height) tuple.", NULL},
|
"Layer dimensions as (width, height) tuple.", NULL},
|
||||||
|
{"name", (getter)PyGridLayerAPI::TileLayer_get_name, NULL,
|
||||||
|
"Layer name (str, read-only). Used for Grid.layer(name) lookup.", NULL},
|
||||||
|
{"grid", (getter)PyGridLayerAPI::TileLayer_get_grid,
|
||||||
|
(setter)PyGridLayerAPI::TileLayer_set_grid,
|
||||||
|
"Parent Grid or None. Setting manages layer association and handles lazy allocation.", NULL},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
|
int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* kwlist[] = {"z_index", "texture", "grid_size", NULL};
|
static const char* kwlist[] = {"z_index", "name", "texture", "grid_size", NULL};
|
||||||
int z_index = -1;
|
int z_index = -1;
|
||||||
|
const char* name_str = nullptr;
|
||||||
PyObject* texture_obj = nullptr;
|
PyObject* texture_obj = nullptr;
|
||||||
PyObject* grid_size_obj = nullptr;
|
PyObject* grid_size_obj = nullptr;
|
||||||
int grid_x = 0, grid_y = 0;
|
int grid_x = 0, grid_y = 0;
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iOO", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|izOO", const_cast<char**>(kwlist),
|
||||||
&z_index, &texture_obj, &grid_size_obj)) {
|
&z_index, &name_str, &texture_obj, &grid_size_obj)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1736,6 +1876,9 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb
|
||||||
|
|
||||||
// Create the layer
|
// Create the layer
|
||||||
self->data = std::make_shared<TileLayer>(z_index, grid_x, grid_y, nullptr, texture);
|
self->data = std::make_shared<TileLayer>(z_index, grid_x, grid_y, nullptr, texture);
|
||||||
|
if (name_str) {
|
||||||
|
self->data->name = name_str;
|
||||||
|
}
|
||||||
self->grid.reset();
|
self->grid.reset();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -2099,12 +2242,137 @@ PyObject* PyGridLayerAPI::TileLayer_get_grid_size(PyTileLayerObject* self, void*
|
||||||
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
|
return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject* PyGridLayerAPI::TileLayer_get_name(PyTileLayerObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return PyUnicode_FromString(self->data->name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyGridLayerAPI::TileLayer_get_grid(PyTileLayerObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check actual parent_grid pointer - it may have been cleared by name collision handling
|
||||||
|
if (!self->data->parent_grid) {
|
||||||
|
// Sync Python wrapper's state if the C++ layer was unlinked externally
|
||||||
|
self->grid.reset();
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->grid) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Python Grid wrapper for the parent grid
|
||||||
|
auto* grid_type = (PyTypeObject*)PyObject_GetAttrString(
|
||||||
|
PyImport_ImportModule("mcrfpy"), "Grid");
|
||||||
|
if (!grid_type) return NULL;
|
||||||
|
|
||||||
|
PyUIGridObject* py_grid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
|
||||||
|
Py_DECREF(grid_type);
|
||||||
|
if (!py_grid) return NULL;
|
||||||
|
|
||||||
|
py_grid->data = self->grid;
|
||||||
|
py_grid->weakreflist = NULL;
|
||||||
|
return (PyObject*)py_grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PyGridLayerAPI::TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle None - unlink from current grid
|
||||||
|
if (value == Py_None || value == NULL) {
|
||||||
|
if (self->grid) {
|
||||||
|
self->data->parent_grid = nullptr;
|
||||||
|
self->grid->removeLayer(self->data);
|
||||||
|
self->grid.reset();
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "grid must be a Grid object or None");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
Py_DECREF(grid_type);
|
||||||
|
|
||||||
|
PyUIGridObject* py_grid = (PyUIGridObject*)value;
|
||||||
|
|
||||||
|
// Check if already attached to this grid
|
||||||
|
if (self->grid.get() == py_grid->data.get()) {
|
||||||
|
return 0; // Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlink from old grid if any
|
||||||
|
if (self->grid) {
|
||||||
|
self->data->parent_grid = nullptr;
|
||||||
|
self->grid->removeLayer(self->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for protected names
|
||||||
|
if (!self->data->name.empty() && UIGrid::isProtectedLayerName(self->data->name)) {
|
||||||
|
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", self->data->name.c_str());
|
||||||
|
self->grid.reset();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle name collision - unlink existing layer with same name
|
||||||
|
if (!self->data->name.empty()) {
|
||||||
|
auto existing = py_grid->data->getLayerByName(self->data->name);
|
||||||
|
if (existing && existing.get() != self->data.get()) {
|
||||||
|
existing->parent_grid = nullptr;
|
||||||
|
py_grid->data->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) {
|
||||||
|
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);
|
||||||
|
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;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
PyObject* PyGridLayerAPI::TileLayer_repr(PyTileLayerObject* self) {
|
PyObject* PyGridLayerAPI::TileLayer_repr(PyTileLayerObject* self) {
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
if (!self->data) {
|
if (!self->data) {
|
||||||
ss << "<TileLayer (invalid)>";
|
ss << "<TileLayer (invalid)>";
|
||||||
} else {
|
} else {
|
||||||
ss << "<TileLayer z_index=" << self->data->z_index
|
ss << "<TileLayer";
|
||||||
|
if (!self->data->name.empty()) {
|
||||||
|
ss << " '" << self->data->name << "'";
|
||||||
|
}
|
||||||
|
ss << " z_index=" << self->data->z_index
|
||||||
<< " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
|
<< " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
|
||||||
<< " visible=" << (self->data->visible ? "True" : "False")
|
<< " visible=" << (self->data->visible ? "True" : "False")
|
||||||
<< " texture=" << (self->data->texture ? "set" : "None") << ">";
|
<< " texture=" << (self->data->texture ? "set" : "None") << ">";
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,9 @@ public:
|
||||||
static PyObject* ColorLayer_get_visible(PyColorLayerObject* self, void* closure);
|
static PyObject* ColorLayer_get_visible(PyColorLayerObject* self, void* closure);
|
||||||
static int ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure);
|
static int ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure);
|
static PyObject* ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure);
|
||||||
|
static PyObject* ColorLayer_get_name(PyColorLayerObject* self, void* closure);
|
||||||
|
static PyObject* ColorLayer_get_grid(PyColorLayerObject* self, void* closure);
|
||||||
|
static int ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* ColorLayer_repr(PyColorLayerObject* self);
|
static PyObject* ColorLayer_repr(PyColorLayerObject* self);
|
||||||
|
|
||||||
// TileLayer methods
|
// TileLayer methods
|
||||||
|
|
@ -229,6 +232,9 @@ public:
|
||||||
static PyObject* TileLayer_get_texture(PyTileLayerObject* self, void* closure);
|
static PyObject* TileLayer_get_texture(PyTileLayerObject* self, void* closure);
|
||||||
static int TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure);
|
static int TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* TileLayer_get_grid_size(PyTileLayerObject* self, void* closure);
|
static PyObject* TileLayer_get_grid_size(PyTileLayerObject* self, void* closure);
|
||||||
|
static PyObject* TileLayer_get_name(PyTileLayerObject* self, void* closure);
|
||||||
|
static PyObject* TileLayer_get_grid(PyTileLayerObject* self, void* closure);
|
||||||
|
static int TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* TileLayer_repr(PyTileLayerObject* self);
|
static PyObject* TileLayer_repr(PyTileLayerObject* self);
|
||||||
|
|
||||||
// Method and getset arrays
|
// Method and getset arrays
|
||||||
|
|
@ -253,21 +259,24 @@ namespace mcrfpydef {
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
|
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("ColorLayer(z_index=-1, grid_size=None)\n\n"
|
.tp_doc = PyDoc_STR("ColorLayer(z_index=-1, name=None, grid_size=None)\n\n"
|
||||||
"A grid layer that stores RGBA colors per cell for background/overlay effects.\n\n"
|
"A grid layer that stores RGBA colors per cell for background/overlay effects.\n\n"
|
||||||
"ColorLayers are typically created via Grid.add_layer('color', ...) rather than\n"
|
"ColorLayers can be created standalone and attached to a Grid via add_layer()\n"
|
||||||
"instantiated directly. When attached to a Grid, the layer inherits rendering\n"
|
"or passed to the Grid constructor's layers parameter. Layers with size (0, 0)\n"
|
||||||
"parameters and can participate in FOV (field of view) calculations.\n\n"
|
"automatically resize to match the Grid when attached.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" z_index (int): Render order relative to entities. Negative values render\n"
|
" z_index (int): Render order relative to entities. Negative values render\n"
|
||||||
" below entities (as backgrounds), positive values render above entities\n"
|
" below entities (as backgrounds), positive values render above entities\n"
|
||||||
" (as overlays). Default: -1 (background)\n"
|
" (as overlays). Default: -1 (background)\n"
|
||||||
" grid_size (tuple): Dimensions as (width, height). If None, the layer will\n"
|
" name (str): Layer name for Grid.layer(name) lookup. Default: None\n"
|
||||||
" inherit the parent Grid's dimensions when attached. Default: None\n\n"
|
" grid_size (tuple): Dimensions as (width, height). If None or (0, 0), the\n"
|
||||||
|
" layer will auto-resize when attached to a Grid. Default: None\n\n"
|
||||||
"Attributes:\n"
|
"Attributes:\n"
|
||||||
" z_index (int): Layer z-order relative to entities (read/write)\n"
|
" z_index (int): Layer z-order relative to entities (read/write)\n"
|
||||||
|
" name (str): Layer name for lookup (read-only)\n"
|
||||||
" visible (bool): Whether layer is rendered (read/write)\n"
|
" visible (bool): Whether layer is rendered (read/write)\n"
|
||||||
" grid_size (tuple): Layer dimensions as (width, height) (read-only)\n\n"
|
" grid_size (tuple): Layer dimensions as (width, height) (read-only)\n"
|
||||||
|
" grid (Grid): Parent Grid or None. Setting manages layer association.\n\n"
|
||||||
"Methods:\n"
|
"Methods:\n"
|
||||||
" at(x, y) -> Color: Get the color at cell position (x, y)\n"
|
" at(x, y) -> Color: Get the color at cell position (x, y)\n"
|
||||||
" set(x, y, color): Set the color at cell position (x, y)\n"
|
" set(x, y, color): Set the color at cell position (x, y)\n"
|
||||||
|
|
@ -276,11 +285,10 @@ namespace mcrfpydef {
|
||||||
" draw_fov(...): Draw FOV-based visibility colors\n"
|
" draw_fov(...): Draw FOV-based visibility colors\n"
|
||||||
" apply_perspective(entity, ...): Bind layer to entity for automatic FOV updates\n\n"
|
" apply_perspective(entity, ...): Bind layer to entity for automatic FOV updates\n\n"
|
||||||
"Example:\n"
|
"Example:\n"
|
||||||
" grid = mcrfpy.Grid(grid_size=(20, 15), texture=my_texture,\n"
|
" fog = mcrfpy.ColorLayer(z_index=-1, name='fog')\n"
|
||||||
" pos=(50, 50), size=(640, 480))\n"
|
" grid = mcrfpy.Grid(grid_size=(20, 15), layers=[fog])\n"
|
||||||
" layer = grid.add_layer('color', z_index=-1)\n"
|
" fog.fill(mcrfpy.Color(40, 40, 40)) # Dark gray background\n"
|
||||||
" layer.fill(mcrfpy.Color(40, 40, 40)) # Dark gray background\n"
|
" grid.layer('fog').set(5, 5, mcrfpy.Color(255, 0, 0, 128))"),
|
||||||
" layer.set(5, 5, mcrfpy.Color(255, 0, 0, 128)) # Semi-transparent red cell"),
|
|
||||||
.tp_methods = PyGridLayerAPI::ColorLayer_methods,
|
.tp_methods = PyGridLayerAPI::ColorLayer_methods,
|
||||||
.tp_getset = PyGridLayerAPI::ColorLayer_getsetters,
|
.tp_getset = PyGridLayerAPI::ColorLayer_getsetters,
|
||||||
.tp_init = (initproc)PyGridLayerAPI::ColorLayer_init,
|
.tp_init = (initproc)PyGridLayerAPI::ColorLayer_init,
|
||||||
|
|
@ -304,25 +312,28 @@ namespace mcrfpydef {
|
||||||
},
|
},
|
||||||
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
|
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("TileLayer(z_index=-1, texture=None, grid_size=None)\n\n"
|
.tp_doc = PyDoc_STR("TileLayer(z_index=-1, name=None, texture=None, grid_size=None)\n\n"
|
||||||
"A grid layer that stores sprite indices per cell for tile-based rendering.\n\n"
|
"A grid layer that stores sprite indices per cell for tile-based rendering.\n\n"
|
||||||
"TileLayers are typically created via Grid.add_layer('tile', ...) rather than\n"
|
"TileLayers can be created standalone and attached to a Grid via add_layer()\n"
|
||||||
"instantiated directly. Each cell stores an integer index into the layer's\n"
|
"or passed to the Grid constructor's layers parameter. Layers with size (0, 0)\n"
|
||||||
"sprite atlas texture. An index of -1 means no tile (transparent/empty).\n\n"
|
"automatically resize to match the Grid when attached.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" z_index (int): Render order relative to entities. Negative values render\n"
|
" z_index (int): Render order relative to entities. Negative values render\n"
|
||||||
" below entities (as backgrounds), positive values render above entities\n"
|
" below entities (as backgrounds), positive values render above entities\n"
|
||||||
" (as overlays). Default: -1 (background)\n"
|
" (as overlays). Default: -1 (background)\n"
|
||||||
|
" name (str): Layer name for Grid.layer(name) lookup. Default: None\n"
|
||||||
" texture (Texture): Sprite atlas containing tile images. The texture's\n"
|
" texture (Texture): Sprite atlas containing tile images. The texture's\n"
|
||||||
" sprite_size determines individual tile dimensions. Required for\n"
|
" sprite_size determines individual tile dimensions. Required for\n"
|
||||||
" rendering; can be set after creation. Default: None\n"
|
" rendering; can be set after creation. Default: None\n"
|
||||||
" grid_size (tuple): Dimensions as (width, height). If None, the layer will\n"
|
" grid_size (tuple): Dimensions as (width, height). If None or (0, 0), the\n"
|
||||||
" inherit the parent Grid's dimensions when attached. Default: None\n\n"
|
" layer will auto-resize when attached to a Grid. Default: None\n\n"
|
||||||
"Attributes:\n"
|
"Attributes:\n"
|
||||||
" z_index (int): Layer z-order relative to entities (read/write)\n"
|
" z_index (int): Layer z-order relative to entities (read/write)\n"
|
||||||
|
" name (str): Layer name for lookup (read-only)\n"
|
||||||
" visible (bool): Whether layer is rendered (read/write)\n"
|
" visible (bool): Whether layer is rendered (read/write)\n"
|
||||||
" texture (Texture): Sprite atlas for tile images (read/write)\n"
|
" texture (Texture): Sprite atlas for tile images (read/write)\n"
|
||||||
" grid_size (tuple): Layer dimensions as (width, height) (read-only)\n\n"
|
" grid_size (tuple): Layer dimensions as (width, height) (read-only)\n"
|
||||||
|
" grid (Grid): Parent Grid or None. Setting manages layer association.\n\n"
|
||||||
"Methods:\n"
|
"Methods:\n"
|
||||||
" at(x, y) -> int: Get the tile index at cell position (x, y)\n"
|
" at(x, y) -> int: Get the tile index at cell position (x, y)\n"
|
||||||
" set(x, y, index): Set the tile index at cell position (x, y)\n"
|
" set(x, y, index): Set the tile index at cell position (x, y)\n"
|
||||||
|
|
@ -332,12 +343,10 @@ namespace mcrfpydef {
|
||||||
" -1: No tile (transparent/empty cell)\n"
|
" -1: No tile (transparent/empty cell)\n"
|
||||||
" 0+: Index into the texture's sprite atlas (row-major order)\n\n"
|
" 0+: Index into the texture's sprite atlas (row-major order)\n\n"
|
||||||
"Example:\n"
|
"Example:\n"
|
||||||
" grid = mcrfpy.Grid(grid_size=(20, 15), texture=my_texture,\n"
|
" terrain = mcrfpy.TileLayer(z_index=-2, name='terrain', texture=tileset)\n"
|
||||||
" pos=(50, 50), size=(640, 480))\n"
|
" grid = mcrfpy.Grid(grid_size=(20, 15), layers=[terrain])\n"
|
||||||
" layer = grid.add_layer('tile', z_index=1, texture=overlay_texture)\n"
|
" terrain.fill(0) # Fill with tile index 0\n"
|
||||||
" layer.fill(-1) # Clear layer (all transparent)\n"
|
" grid.layer('terrain').set(5, 5, 42) # Place tile 42 at (5, 5)"),
|
||||||
" layer.set(5, 5, 42) # Place tile index 42 at position (5, 5)\n"
|
|
||||||
" layer.fill_rect(0, 0, 20, 1, 10) # Top row filled with tile 10"),
|
|
||||||
.tp_methods = PyGridLayerAPI::TileLayer_methods,
|
.tp_methods = PyGridLayerAPI::TileLayer_methods,
|
||||||
.tp_getset = PyGridLayerAPI::TileLayer_getsetters,
|
.tp_getset = PyGridLayerAPI::TileLayer_getsetters,
|
||||||
.tp_init = (initproc)PyGridLayerAPI::TileLayer_init,
|
.tp_init = (initproc)PyGridLayerAPI::TileLayer_init,
|
||||||
|
|
|
||||||
459
src/UIGrid.cpp
459
src/UIGrid.cpp
|
|
@ -924,51 +924,185 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
self->data->click_register(click_handler);
|
self->data->click_register(click_handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// #150 - Handle layers dict
|
// #150 - Handle layers parameter
|
||||||
// Default: {"tilesprite": "tile"} when layers not provided
|
// Default: single TileLayer named "tilesprite" when layers not provided
|
||||||
// Empty dict: no rendering layers (entity storage + pathfinding only)
|
// Empty list/None: no rendering layers (entity storage + pathfinding only)
|
||||||
|
// List of layer objects: add each layer with lazy allocation
|
||||||
if (layers_obj == nullptr) {
|
if (layers_obj == nullptr) {
|
||||||
// Default layer: single TileLayer named "tilesprite" (z_index -1 = below entities)
|
// Default layer: single TileLayer named "tilesprite" (z_index -1 = below entities)
|
||||||
self->data->addTileLayer(-1, texture_ptr, "tilesprite");
|
self->data->addTileLayer(-1, texture_ptr, "tilesprite");
|
||||||
} else if (layers_obj != Py_None) {
|
} else if (layers_obj != Py_None) {
|
||||||
if (!PyDict_Check(layers_obj)) {
|
// Accept any iterable of layer objects
|
||||||
PyErr_SetString(PyExc_TypeError, "layers must be a dict mapping names to types ('color' or 'tile')");
|
PyObject* iterator = PyObject_GetIter(layers_obj);
|
||||||
|
if (!iterator) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "layers must be an iterable of ColorLayer or TileLayer objects");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* key;
|
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||||
PyObject* value;
|
if (!mcrfpy_module) {
|
||||||
Py_ssize_t pos = 0;
|
Py_DECREF(iterator);
|
||||||
int layer_z = -1; // Start at -1 (below entities), decrement for each layer
|
|
||||||
|
|
||||||
while (PyDict_Next(layers_obj, &pos, &key, &value)) {
|
|
||||||
if (!PyUnicode_Check(key)) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "Layer names must be strings");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!PyUnicode_Check(value)) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "Layer types must be strings ('color' or 'tile')");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* layer_name = PyUnicode_AsUTF8(key);
|
auto* color_layer_type = PyObject_GetAttrString(mcrfpy_module, "ColorLayer");
|
||||||
const char* layer_type = PyUnicode_AsUTF8(value);
|
auto* tile_layer_type = PyObject_GetAttrString(mcrfpy_module, "TileLayer");
|
||||||
|
Py_DECREF(mcrfpy_module);
|
||||||
|
|
||||||
|
if (!color_layer_type || !tile_layer_type) {
|
||||||
|
if (color_layer_type) Py_DECREF(color_layer_type);
|
||||||
|
if (tile_layer_type) Py_DECREF(tile_layer_type);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* item;
|
||||||
|
while ((item = PyIter_Next(iterator)) != NULL) {
|
||||||
|
std::shared_ptr<GridLayer> layer;
|
||||||
|
|
||||||
|
if (PyObject_IsInstance(item, color_layer_type)) {
|
||||||
|
PyColorLayerObject* py_layer = (PyColorLayerObject*)item;
|
||||||
|
if (!py_layer->data) {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already attached to another grid
|
||||||
|
if (py_layer->grid) {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Layer is already attached to another Grid");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer = py_layer->data;
|
||||||
|
|
||||||
// Check for protected names
|
// Check for protected names
|
||||||
if (UIGrid::isProtectedLayerName(layer_name)) {
|
if (!layer->name.empty() && UIGrid::isProtectedLayerName(layer->name)) {
|
||||||
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer_name);
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer->name.c_str());
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(layer_type, "color") == 0) {
|
// Handle name collision
|
||||||
self->data->addColorLayer(layer_z--, layer_name);
|
if (!layer->name.empty()) {
|
||||||
} else if (strcmp(layer_type, "tile") == 0) {
|
auto existing = self->data->getLayerByName(layer->name);
|
||||||
self->data->addTileLayer(layer_z--, texture_ptr, layer_name);
|
if (existing) {
|
||||||
} else {
|
existing->parent_grid = nullptr;
|
||||||
PyErr_Format(PyExc_ValueError, "Unknown layer type '%s' (expected 'color' or 'tile')", layer_type);
|
self->data->removeLayer(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy allocation: resize if layer is (0,0)
|
||||||
|
if (layer->grid_x == 0 && layer->grid_y == 0) {
|
||||||
|
layer->resize(self->data->grid_w, self->data->grid_h);
|
||||||
|
} else if (layer->grid_x != self->data->grid_w || layer->grid_y != self->data->grid_h) {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Layer size (%d, %d) does not match Grid size (%d, %d)",
|
||||||
|
layer->grid_x, layer->grid_y, self->data->grid_w, self->data->grid_h);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Link to grid
|
||||||
|
layer->parent_grid = self->data.get();
|
||||||
|
self->data->layers.push_back(layer);
|
||||||
|
py_layer->grid = self->data;
|
||||||
|
|
||||||
|
} else if (PyObject_IsInstance(item, tile_layer_type)) {
|
||||||
|
PyTileLayerObject* py_layer = (PyTileLayerObject*)item;
|
||||||
|
if (!py_layer->data) {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if already attached to another grid
|
||||||
|
if (py_layer->grid) {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Layer is already attached to another Grid");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer = py_layer->data;
|
||||||
|
|
||||||
|
// Check for protected names
|
||||||
|
if (!layer->name.empty() && UIGrid::isProtectedLayerName(layer->name)) {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer->name.c_str());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle name collision
|
||||||
|
if (!layer->name.empty()) {
|
||||||
|
auto existing = self->data->getLayerByName(layer->name);
|
||||||
|
if (existing) {
|
||||||
|
existing->parent_grid = nullptr;
|
||||||
|
self->data->removeLayer(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy allocation: resize if layer is (0,0)
|
||||||
|
if (layer->grid_x == 0 && layer->grid_y == 0) {
|
||||||
|
layer->resize(self->data->grid_w, self->data->grid_h);
|
||||||
|
} else if (layer->grid_x != self->data->grid_w || layer->grid_y != self->data->grid_h) {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Layer size (%d, %d) does not match Grid size (%d, %d)",
|
||||||
|
layer->grid_x, layer->grid_y, self->data->grid_w, self->data->grid_h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link to grid
|
||||||
|
layer->parent_grid = self->data.get();
|
||||||
|
self->data->layers.push_back(layer);
|
||||||
|
py_layer->grid = self->data;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "layers must contain only ColorLayer or TileLayer objects");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_DECREF(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_DECREF(iterator);
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->layers_need_sort = true;
|
||||||
}
|
}
|
||||||
// else: layers_obj is Py_None - explicit empty, no layers created
|
// else: layers_obj is Py_None - explicit empty, no layers created
|
||||||
|
|
||||||
|
|
@ -1456,74 +1590,151 @@ PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* k
|
||||||
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root)
|
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root)
|
||||||
|
|
||||||
// #147 - Layer system Python API
|
// #147 - Layer system Python API
|
||||||
PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args) {
|
||||||
static const char* kwlist[] = {"type", "z_index", "texture", NULL};
|
PyObject* layer_obj;
|
||||||
const char* type_str = nullptr;
|
if (!PyArg_ParseTuple(args, "O", &layer_obj)) {
|
||||||
int z_index = -1;
|
|
||||||
PyObject* texture_obj = nullptr;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|iO", const_cast<char**>(kwlist),
|
|
||||||
&type_str, &z_index, &texture_obj)) {
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string type(type_str);
|
|
||||||
|
|
||||||
if (type == "color") {
|
|
||||||
auto layer = self->data->addColorLayer(z_index);
|
|
||||||
|
|
||||||
// Create Python ColorLayer object
|
|
||||||
auto* color_layer_type = (PyTypeObject*)PyObject_GetAttrString(
|
|
||||||
PyImport_ImportModule("mcrfpy"), "ColorLayer");
|
|
||||||
if (!color_layer_type) return NULL;
|
|
||||||
|
|
||||||
PyColorLayerObject* py_layer = (PyColorLayerObject*)color_layer_type->tp_alloc(color_layer_type, 0);
|
|
||||||
Py_DECREF(color_layer_type);
|
|
||||||
if (!py_layer) return NULL;
|
|
||||||
|
|
||||||
py_layer->data = layer;
|
|
||||||
py_layer->grid = self->data;
|
|
||||||
return (PyObject*)py_layer;
|
|
||||||
|
|
||||||
} else if (type == "tile") {
|
|
||||||
// Parse texture
|
|
||||||
std::shared_ptr<PyTexture> texture;
|
|
||||||
if (texture_obj && texture_obj != Py_None) {
|
|
||||||
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||||
if (!mcrfpy_module) return NULL;
|
if (!mcrfpy_module) return NULL;
|
||||||
|
|
||||||
auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture");
|
auto* color_layer_type = PyObject_GetAttrString(mcrfpy_module, "ColorLayer");
|
||||||
|
auto* tile_layer_type = PyObject_GetAttrString(mcrfpy_module, "TileLayer");
|
||||||
Py_DECREF(mcrfpy_module);
|
Py_DECREF(mcrfpy_module);
|
||||||
if (!texture_type) return NULL;
|
|
||||||
|
|
||||||
if (!PyObject_IsInstance(texture_obj, texture_type)) {
|
if (!color_layer_type || !tile_layer_type) {
|
||||||
Py_DECREF(texture_type);
|
if (color_layer_type) Py_DECREF(color_layer_type);
|
||||||
PyErr_SetString(PyExc_TypeError, "texture must be a Texture object");
|
if (tile_layer_type) Py_DECREF(tile_layer_type);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
Py_DECREF(texture_type);
|
|
||||||
texture = ((PyTextureObject*)texture_obj)->data;
|
std::shared_ptr<GridLayer> layer;
|
||||||
|
PyObject* py_layer_ref = nullptr;
|
||||||
|
|
||||||
|
if (PyObject_IsInstance(layer_obj, color_layer_type)) {
|
||||||
|
PyColorLayerObject* py_layer = (PyColorLayerObject*)layer_obj;
|
||||||
|
if (!py_layer->data) {
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto layer = self->data->addTileLayer(z_index, texture);
|
// Check if already attached to another grid
|
||||||
|
if (py_layer->grid && py_layer->grid.get() != self->data.get()) {
|
||||||
// Create Python TileLayer object
|
Py_DECREF(color_layer_type);
|
||||||
auto* tile_layer_type = (PyTypeObject*)PyObject_GetAttrString(
|
|
||||||
PyImport_ImportModule("mcrfpy"), "TileLayer");
|
|
||||||
if (!tile_layer_type) return NULL;
|
|
||||||
|
|
||||||
PyTileLayerObject* py_layer = (PyTileLayerObject*)tile_layer_type->tp_alloc(tile_layer_type, 0);
|
|
||||||
Py_DECREF(tile_layer_type);
|
Py_DECREF(tile_layer_type);
|
||||||
if (!py_layer) return NULL;
|
PyErr_SetString(PyExc_ValueError, "Layer is already attached to another Grid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
py_layer->data = layer;
|
layer = py_layer->data;
|
||||||
|
py_layer_ref = layer_obj;
|
||||||
|
|
||||||
|
// Check for protected names
|
||||||
|
if (!layer->name.empty() && UIGrid::isProtectedLayerName(layer->name)) {
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer->name.c_str());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle name collision - unlink existing layer with same name
|
||||||
|
if (!layer->name.empty()) {
|
||||||
|
auto existing = self->data->getLayerByName(layer->name);
|
||||||
|
if (existing && existing.get() != layer.get()) {
|
||||||
|
existing->parent_grid = nullptr;
|
||||||
|
self->data->removeLayer(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy allocation: resize if layer is (0,0)
|
||||||
|
if (layer->grid_x == 0 && layer->grid_y == 0) {
|
||||||
|
layer->resize(self->data->grid_w, self->data->grid_h);
|
||||||
|
} else if (layer->grid_x != self->data->grid_w || layer->grid_y != self->data->grid_h) {
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Layer size (%d, %d) does not match Grid size (%d, %d)",
|
||||||
|
layer->grid_x, layer->grid_y, self->data->grid_w, self->data->grid_h);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link to grid
|
||||||
|
layer->parent_grid = self->data.get();
|
||||||
|
self->data->layers.push_back(layer);
|
||||||
|
self->data->layers_need_sort = true;
|
||||||
|
py_layer->grid = self->data;
|
||||||
|
|
||||||
|
} else if (PyObject_IsInstance(layer_obj, tile_layer_type)) {
|
||||||
|
PyTileLayerObject* py_layer = (PyTileLayerObject*)layer_obj;
|
||||||
|
if (!py_layer->data) {
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already attached to another grid
|
||||||
|
if (py_layer->grid && py_layer->grid.get() != self->data.get()) {
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Layer is already attached to another Grid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer = py_layer->data;
|
||||||
|
py_layer_ref = layer_obj;
|
||||||
|
|
||||||
|
// Check for protected names
|
||||||
|
if (!layer->name.empty() && UIGrid::isProtectedLayerName(layer->name)) {
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer->name.c_str());
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle name collision - unlink existing layer with same name
|
||||||
|
if (!layer->name.empty()) {
|
||||||
|
auto existing = self->data->getLayerByName(layer->name);
|
||||||
|
if (existing && existing.get() != layer.get()) {
|
||||||
|
existing->parent_grid = nullptr;
|
||||||
|
self->data->removeLayer(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lazy allocation: resize if layer is (0,0)
|
||||||
|
if (layer->grid_x == 0 && layer->grid_y == 0) {
|
||||||
|
layer->resize(self->data->grid_w, self->data->grid_h);
|
||||||
|
} else if (layer->grid_x != self->data->grid_w || layer->grid_y != self->data->grid_h) {
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Layer size (%d, %d) does not match Grid size (%d, %d)",
|
||||||
|
layer->grid_x, layer->grid_y, self->data->grid_w, self->data->grid_h);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link to grid
|
||||||
|
layer->parent_grid = self->data.get();
|
||||||
|
self->data->layers.push_back(layer);
|
||||||
|
self->data->layers_need_sort = true;
|
||||||
py_layer->grid = self->data;
|
py_layer->grid = self->data;
|
||||||
return (PyObject*)py_layer;
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
PyErr_SetString(PyExc_ValueError, "type must be 'color' or 'tile'");
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "layer must be a ColorLayer or TileLayer");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Py_DECREF(color_layer_type);
|
||||||
|
Py_DECREF(tile_layer_type);
|
||||||
|
|
||||||
|
// Return the layer object (incref it since we're returning a reference)
|
||||||
|
Py_INCREF(py_layer_ref);
|
||||||
|
return py_layer_ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIGrid::py_remove_layer(PyUIGridObject* self, PyObject* args) {
|
PyObject* UIGrid::py_remove_layer(PyUIGridObject* self, PyObject* args) {
|
||||||
|
|
@ -1532,6 +1743,22 @@ PyObject* UIGrid::py_remove_layer(PyUIGridObject* self, PyObject* args) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's a string (layer name)
|
||||||
|
if (PyUnicode_Check(layer_obj)) {
|
||||||
|
const char* name_str = PyUnicode_AsUTF8(layer_obj);
|
||||||
|
if (!name_str) return NULL;
|
||||||
|
|
||||||
|
auto layer = self->data->getLayerByName(std::string(name_str));
|
||||||
|
if (!layer) {
|
||||||
|
PyErr_Format(PyExc_KeyError, "Layer '%s' not found", name_str);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer->parent_grid = nullptr;
|
||||||
|
self->data->removeLayer(layer);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||||
if (!mcrfpy_module) return NULL;
|
if (!mcrfpy_module) return NULL;
|
||||||
|
|
||||||
|
|
@ -1541,7 +1768,11 @@ PyObject* UIGrid::py_remove_layer(PyUIGridObject* self, PyObject* args) {
|
||||||
Py_DECREF(color_layer_type);
|
Py_DECREF(color_layer_type);
|
||||||
Py_DECREF(mcrfpy_module);
|
Py_DECREF(mcrfpy_module);
|
||||||
auto* py_layer = (PyColorLayerObject*)layer_obj;
|
auto* py_layer = (PyColorLayerObject*)layer_obj;
|
||||||
|
if (py_layer->data) {
|
||||||
|
py_layer->data->parent_grid = nullptr;
|
||||||
self->data->removeLayer(py_layer->data);
|
self->data->removeLayer(py_layer->data);
|
||||||
|
py_layer->grid.reset();
|
||||||
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
if (color_layer_type) Py_DECREF(color_layer_type);
|
if (color_layer_type) Py_DECREF(color_layer_type);
|
||||||
|
|
@ -1552,25 +1783,29 @@ PyObject* UIGrid::py_remove_layer(PyUIGridObject* self, PyObject* args) {
|
||||||
Py_DECREF(tile_layer_type);
|
Py_DECREF(tile_layer_type);
|
||||||
Py_DECREF(mcrfpy_module);
|
Py_DECREF(mcrfpy_module);
|
||||||
auto* py_layer = (PyTileLayerObject*)layer_obj;
|
auto* py_layer = (PyTileLayerObject*)layer_obj;
|
||||||
|
if (py_layer->data) {
|
||||||
|
py_layer->data->parent_grid = nullptr;
|
||||||
self->data->removeLayer(py_layer->data);
|
self->data->removeLayer(py_layer->data);
|
||||||
|
py_layer->grid.reset();
|
||||||
|
}
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
if (tile_layer_type) Py_DECREF(tile_layer_type);
|
if (tile_layer_type) Py_DECREF(tile_layer_type);
|
||||||
|
|
||||||
Py_DECREF(mcrfpy_module);
|
Py_DECREF(mcrfpy_module);
|
||||||
PyErr_SetString(PyExc_TypeError, "layer must be a ColorLayer or TileLayer");
|
PyErr_SetString(PyExc_TypeError, "layer must be a string (layer name), ColorLayer, or TileLayer");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIGrid::get_layers(PyUIGridObject* self, void* closure) {
|
PyObject* UIGrid::get_layers(PyUIGridObject* self, void* closure) {
|
||||||
self->data->sortLayers();
|
self->data->sortLayers();
|
||||||
|
|
||||||
PyObject* list = PyList_New(self->data->layers.size());
|
PyObject* tuple = PyTuple_New(self->data->layers.size());
|
||||||
if (!list) return NULL;
|
if (!tuple) return NULL;
|
||||||
|
|
||||||
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||||
if (!mcrfpy_module) {
|
if (!mcrfpy_module) {
|
||||||
Py_DECREF(list);
|
Py_DECREF(tuple);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1581,7 +1816,7 @@ PyObject* UIGrid::get_layers(PyUIGridObject* self, void* closure) {
|
||||||
if (!color_layer_type || !tile_layer_type) {
|
if (!color_layer_type || !tile_layer_type) {
|
||||||
if (color_layer_type) Py_DECREF(color_layer_type);
|
if (color_layer_type) Py_DECREF(color_layer_type);
|
||||||
if (tile_layer_type) Py_DECREF(tile_layer_type);
|
if (tile_layer_type) Py_DECREF(tile_layer_type);
|
||||||
Py_DECREF(list);
|
Py_DECREF(tuple);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1608,26 +1843,29 @@ PyObject* UIGrid::get_layers(PyUIGridObject* self, void* closure) {
|
||||||
if (!py_layer) {
|
if (!py_layer) {
|
||||||
Py_DECREF(color_layer_type);
|
Py_DECREF(color_layer_type);
|
||||||
Py_DECREF(tile_layer_type);
|
Py_DECREF(tile_layer_type);
|
||||||
Py_DECREF(list);
|
Py_DECREF(tuple);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyList_SET_ITEM(list, i, py_layer); // Steals reference
|
PyTuple_SET_ITEM(tuple, i, py_layer); // Steals reference
|
||||||
}
|
}
|
||||||
|
|
||||||
Py_DECREF(color_layer_type);
|
Py_DECREF(color_layer_type);
|
||||||
Py_DECREF(tile_layer_type);
|
Py_DECREF(tile_layer_type);
|
||||||
return list;
|
return tuple;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIGrid::py_layer(PyUIGridObject* self, PyObject* args) {
|
PyObject* UIGrid::py_layer(PyUIGridObject* self, PyObject* args) {
|
||||||
int z_index;
|
const char* name_str;
|
||||||
if (!PyArg_ParseTuple(args, "i", &z_index)) {
|
if (!PyArg_ParseTuple(args, "s", &name_str)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& layer : self->data->layers) {
|
auto layer = self->data->getLayerByName(std::string(name_str));
|
||||||
if (layer->z_index == z_index) {
|
if (!layer) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||||
if (!mcrfpy_module) return NULL;
|
if (!mcrfpy_module) return NULL;
|
||||||
|
|
||||||
|
|
@ -1656,10 +1894,6 @@ PyObject* UIGrid::py_layer(PyUIGridObject* self, PyObject* args) {
|
||||||
obj->grid = self->data;
|
obj->grid = self->data;
|
||||||
return (PyObject*)obj;
|
return (PyObject*)obj;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_RETURN_NONE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #115 - Spatial hash query for entities in radius
|
// #115 - Spatial hash query for entities in radius
|
||||||
|
|
@ -2061,12 +2295,12 @@ PyMethodDef UIGrid::methods[] = {
|
||||||
"Clear all cached Dijkstra maps.\n\n"
|
"Clear all cached Dijkstra maps.\n\n"
|
||||||
"Call this after modifying grid cell walkability to ensure pathfinding\n"
|
"Call this after modifying grid cell walkability to ensure pathfinding\n"
|
||||||
"uses updated walkability data."},
|
"uses updated walkability data."},
|
||||||
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS,
|
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS,
|
||||||
"add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer"},
|
"add_layer(layer: ColorLayer | TileLayer) -> ColorLayer | TileLayer"},
|
||||||
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
|
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
|
||||||
"remove_layer(layer: ColorLayer | TileLayer) -> None"},
|
"remove_layer(name_or_layer: str | ColorLayer | TileLayer) -> None"},
|
||||||
{"layer", (PyCFunction)UIGrid::py_layer, METH_VARARGS,
|
{"layer", (PyCFunction)UIGrid::py_layer, METH_VARARGS,
|
||||||
"layer(z_index: int) -> ColorLayer | TileLayer | None"},
|
"layer(name: str) -> ColorLayer | TileLayer | None"},
|
||||||
{"entities_in_radius", (PyCFunction)UIGrid::py_entities_in_radius, METH_VARARGS | METH_KEYWORDS,
|
{"entities_in_radius", (PyCFunction)UIGrid::py_entities_in_radius, METH_VARARGS | METH_KEYWORDS,
|
||||||
"entities_in_radius(pos: tuple|Vector, radius: float) -> list[Entity]\n\n"
|
"entities_in_radius(pos: tuple|Vector, radius: float) -> list[Entity]\n\n"
|
||||||
"Query entities within radius using spatial hash (O(k) where k = nearby entities).\n\n"
|
"Query entities within radius using spatial hash (O(k) where k = nearby entities).\n\n"
|
||||||
|
|
@ -2166,27 +2400,34 @@ PyMethodDef UIGrid_all_methods[] = {
|
||||||
"Clear all cached Dijkstra maps.\n\n"
|
"Clear all cached Dijkstra maps.\n\n"
|
||||||
"Call this after modifying grid cell walkability to ensure pathfinding\n"
|
"Call this after modifying grid cell walkability to ensure pathfinding\n"
|
||||||
"uses updated walkability data."},
|
"uses updated walkability data."},
|
||||||
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS,
|
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS,
|
||||||
"add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer\n\n"
|
"add_layer(layer: ColorLayer | TileLayer) -> ColorLayer | TileLayer\n\n"
|
||||||
"Add a new layer to the grid.\n\n"
|
"Add a layer to the grid.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" type: Layer type ('color' or 'tile')\n"
|
" layer: A ColorLayer or TileLayer object. Layers with size (0, 0) are\n"
|
||||||
" z_index: Render order. Negative = below entities, >= 0 = above entities. Default: -1\n"
|
" automatically resized to match the grid. Named layers replace\n"
|
||||||
" texture: Texture for tile layers. Required for 'tile' type.\n\n"
|
" any existing layer with the same name.\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" The created ColorLayer or TileLayer object."},
|
" The added layer object.\n\n"
|
||||||
|
"Raises:\n"
|
||||||
|
" ValueError: If layer is already attached to another grid, or if\n"
|
||||||
|
" layer size doesn't match grid (and isn't (0,0)).\n"
|
||||||
|
" TypeError: If argument is not a ColorLayer or TileLayer."},
|
||||||
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
|
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
|
||||||
"remove_layer(layer: ColorLayer | TileLayer) -> None\n\n"
|
"remove_layer(name_or_layer: str | ColorLayer | TileLayer) -> None\n\n"
|
||||||
"Remove a layer from the grid.\n\n"
|
"Remove a layer from the grid.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" layer: The layer to remove."},
|
" name_or_layer: Either a layer name (str) or the layer object itself.\n\n"
|
||||||
|
"Raises:\n"
|
||||||
|
" KeyError: If name is provided but no layer with that name exists.\n"
|
||||||
|
" TypeError: If argument is not a string, ColorLayer, or TileLayer."},
|
||||||
{"layer", (PyCFunction)UIGrid::py_layer, METH_VARARGS,
|
{"layer", (PyCFunction)UIGrid::py_layer, METH_VARARGS,
|
||||||
"layer(z_index: int) -> ColorLayer | TileLayer | None\n\n"
|
"layer(name: str) -> ColorLayer | TileLayer | None\n\n"
|
||||||
"Get a layer by its z_index.\n\n"
|
"Get a layer by its name.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" z_index: The z_index of the layer to find.\n\n"
|
" name: The name of the layer to find.\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" The layer with the specified z_index, or None if not found."},
|
" The layer with the specified name, or None if not found."},
|
||||||
{"entities_in_radius", (PyCFunction)UIGrid::py_entities_in_radius, METH_VARARGS | METH_KEYWORDS,
|
{"entities_in_radius", (PyCFunction)UIGrid::py_entities_in_radius, METH_VARARGS | METH_KEYWORDS,
|
||||||
"entities_in_radius(pos: tuple|Vector, radius: float) -> list[Entity]\n\n"
|
"entities_in_radius(pos: tuple|Vector, radius: float) -> list[Entity]\n\n"
|
||||||
"Query entities within radius using spatial hash (O(k) where k = nearby entities).\n\n"
|
"Query entities within radius using spatial hash (O(k) where k = nearby entities).\n\n"
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,7 @@ public:
|
||||||
static PyObject* get_hovered_cell(PyUIGridObject* self, void* closure);
|
static PyObject* get_hovered_cell(PyUIGridObject* self, void* closure);
|
||||||
|
|
||||||
// #147 - Layer system Python API
|
// #147 - Layer system Python API
|
||||||
static PyObject* py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_add_layer(PyUIGridObject* self, PyObject* args);
|
||||||
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
|
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
|
||||||
static PyObject* get_layers(PyUIGridObject* self, void* closure);
|
static PyObject* get_layers(PyUIGridObject* self, void* closure);
|
||||||
static PyObject* py_layer(PyUIGridObject* self, PyObject* args);
|
static PyObject* py_layer(PyUIGridObject* self, PyObject* args);
|
||||||
|
|
|
||||||
275
tests/unit/grid_layer_api_test.py
Normal file
275
tests/unit/grid_layer_api_test.py
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
"""
|
||||||
|
Unit tests for the modernized Grid Layer API.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
1. Layer name in constructor
|
||||||
|
2. Lazy allocation (size (0,0) auto-resizes)
|
||||||
|
3. layer.grid property (getter and setter)
|
||||||
|
4. grid.layer(name) lookup
|
||||||
|
5. grid.layers returns tuple
|
||||||
|
6. add_layer accepts layer objects
|
||||||
|
7. remove_layer by name and by layer object
|
||||||
|
8. Name collision handling
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_layer_name_in_constructor():
|
||||||
|
"""Test that layers can be created with names."""
|
||||||
|
print("Test 1: Layer name in constructor...")
|
||||||
|
|
||||||
|
color_layer = mcrfpy.ColorLayer(name='fog', z_index=-1)
|
||||||
|
assert color_layer.name == 'fog', f"Expected 'fog', got {color_layer.name}"
|
||||||
|
|
||||||
|
tile_layer = mcrfpy.TileLayer(name='terrain', z_index=-2)
|
||||||
|
assert tile_layer.name == 'terrain', f"Expected 'terrain', got {tile_layer.name}"
|
||||||
|
|
||||||
|
# Name should appear in repr
|
||||||
|
assert 'fog' in repr(color_layer), f"Name not in repr: {repr(color_layer)}"
|
||||||
|
assert 'terrain' in repr(tile_layer), f"Name not in repr: {repr(tile_layer)}"
|
||||||
|
|
||||||
|
# Layers without name should work too
|
||||||
|
unnamed = mcrfpy.ColorLayer(z_index=0)
|
||||||
|
assert unnamed.name == '', f"Expected empty string, got {unnamed.name}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_lazy_allocation():
|
||||||
|
"""Test that layers with size (0,0) auto-resize when attached to grid."""
|
||||||
|
print("Test 2: Lazy allocation...")
|
||||||
|
|
||||||
|
layer = mcrfpy.ColorLayer(name='test')
|
||||||
|
assert layer.grid_size == (0, 0), f"Expected (0,0), got {layer.grid_size}"
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(grid_size=(10, 8), layers=[layer])
|
||||||
|
assert layer.grid_size == (10, 8), f"Expected (10,8), got {layer.grid_size}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_layer_grid_property():
|
||||||
|
"""Test that layer.grid property works for getting and setting."""
|
||||||
|
print("Test 3: layer.grid property...")
|
||||||
|
|
||||||
|
layer = mcrfpy.ColorLayer(name='overlay', z_index=0)
|
||||||
|
assert layer.grid is None, f"Expected None, got {layer.grid}"
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[])
|
||||||
|
layer.grid = grid
|
||||||
|
|
||||||
|
assert layer.grid is not None, "layer.grid should not be None after assignment"
|
||||||
|
assert len(grid.layers) == 1, f"Expected 1 layer, got {len(grid.layers)}"
|
||||||
|
assert layer.grid_size == (5, 5), f"Expected (5,5), got {layer.grid_size}"
|
||||||
|
|
||||||
|
# Unlink by setting to None
|
||||||
|
layer.grid = None
|
||||||
|
assert layer.grid is None, "layer.grid should be None after unsetting"
|
||||||
|
assert len(grid.layers) == 0, f"Expected 0 layers, got {len(grid.layers)}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_grid_layer_name_lookup():
|
||||||
|
"""Test that grid.layer(name) finds layers by name."""
|
||||||
|
print("Test 4: grid.layer(name) lookup...")
|
||||||
|
|
||||||
|
fog = mcrfpy.ColorLayer(name='fog', z_index=-1)
|
||||||
|
terrain = mcrfpy.TileLayer(name='terrain', z_index=-2)
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[fog, terrain])
|
||||||
|
|
||||||
|
retrieved_fog = grid.layer('fog')
|
||||||
|
assert retrieved_fog is not None, "Should find 'fog' layer"
|
||||||
|
assert retrieved_fog.name == 'fog', f"Expected 'fog', got {retrieved_fog.name}"
|
||||||
|
|
||||||
|
retrieved_terrain = grid.layer('terrain')
|
||||||
|
assert retrieved_terrain is not None, "Should find 'terrain' layer"
|
||||||
|
assert retrieved_terrain.name == 'terrain', f"Expected 'terrain', got {retrieved_terrain.name}"
|
||||||
|
|
||||||
|
# Non-existent name should return None
|
||||||
|
result = grid.layer('nonexistent')
|
||||||
|
assert result is None, f"Expected None for nonexistent, got {result}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_layers_returns_tuple():
|
||||||
|
"""Test that grid.layers returns a tuple (immutable)."""
|
||||||
|
print("Test 5: grid.layers returns tuple...")
|
||||||
|
|
||||||
|
fog = mcrfpy.ColorLayer(name='fog')
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[fog])
|
||||||
|
|
||||||
|
layers = grid.layers
|
||||||
|
assert isinstance(layers, tuple), f"Expected tuple, got {type(layers)}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_add_layer_accepts_objects():
|
||||||
|
"""Test that add_layer accepts ColorLayer and TileLayer objects."""
|
||||||
|
print("Test 6: add_layer accepts objects...")
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[])
|
||||||
|
assert len(grid.layers) == 0, f"Expected 0 layers, got {len(grid.layers)}"
|
||||||
|
|
||||||
|
new_layer = mcrfpy.TileLayer(name='overlay', z_index=1)
|
||||||
|
returned = grid.add_layer(new_layer)
|
||||||
|
|
||||||
|
assert len(grid.layers) == 1, f"Expected 1 layer, got {len(grid.layers)}"
|
||||||
|
assert new_layer.grid is not None, "Layer should be attached to grid"
|
||||||
|
assert returned is new_layer, "Should return the same layer object"
|
||||||
|
|
||||||
|
# Add another layer
|
||||||
|
color = mcrfpy.ColorLayer(name='highlights', z_index=2)
|
||||||
|
grid.add_layer(color)
|
||||||
|
assert len(grid.layers) == 2, f"Expected 2 layers, got {len(grid.layers)}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_remove_layer_by_name():
|
||||||
|
"""Test that remove_layer accepts layer name as string."""
|
||||||
|
print("Test 7a: remove_layer by name...")
|
||||||
|
|
||||||
|
fog = mcrfpy.ColorLayer(name='fog')
|
||||||
|
terrain = mcrfpy.TileLayer(name='terrain')
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[fog, terrain])
|
||||||
|
|
||||||
|
assert len(grid.layers) == 2
|
||||||
|
|
||||||
|
grid.remove_layer('fog')
|
||||||
|
assert len(grid.layers) == 1, f"Expected 1 layer, got {len(grid.layers)}"
|
||||||
|
assert grid.layer('fog') is None, "fog should be removed"
|
||||||
|
assert grid.layer('terrain') is not None, "terrain should remain"
|
||||||
|
|
||||||
|
# Removing non-existent should raise KeyError
|
||||||
|
try:
|
||||||
|
grid.remove_layer('nonexistent')
|
||||||
|
assert False, "Should raise KeyError for nonexistent layer"
|
||||||
|
except KeyError:
|
||||||
|
pass # Expected
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_remove_layer_by_object():
|
||||||
|
"""Test that remove_layer accepts layer object."""
|
||||||
|
print("Test 7b: remove_layer by object...")
|
||||||
|
|
||||||
|
fog = mcrfpy.ColorLayer(name='fog')
|
||||||
|
terrain = mcrfpy.TileLayer(name='terrain')
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[fog, terrain])
|
||||||
|
|
||||||
|
grid.remove_layer(terrain)
|
||||||
|
assert len(grid.layers) == 1, f"Expected 1 layer, got {len(grid.layers)}"
|
||||||
|
assert terrain.grid is None, "Removed layer should have grid=None"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_name_collision_replaces():
|
||||||
|
"""Test that adding a layer with existing name replaces the old layer."""
|
||||||
|
print("Test 8: Name collision replaces old layer...")
|
||||||
|
|
||||||
|
old = mcrfpy.ColorLayer(name='fog')
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[old])
|
||||||
|
|
||||||
|
assert len(grid.layers) == 1
|
||||||
|
assert old.grid is not None
|
||||||
|
|
||||||
|
# Add new layer with same name
|
||||||
|
new = mcrfpy.ColorLayer(name='fog')
|
||||||
|
grid.add_layer(new)
|
||||||
|
|
||||||
|
assert len(grid.layers) == 1, f"Expected 1 layer after replacement, got {len(grid.layers)}"
|
||||||
|
assert old.grid is None, "Old layer should be unlinked"
|
||||||
|
assert new.grid is not None, "New layer should be linked"
|
||||||
|
|
||||||
|
# Verify it's the new one
|
||||||
|
retrieved = grid.layer('fog')
|
||||||
|
# Both have same name, but we can check the new one is attached
|
||||||
|
assert retrieved is not None
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_size_validation():
|
||||||
|
"""Test that mismatched sizes raise errors."""
|
||||||
|
print("Test 9: Size validation...")
|
||||||
|
|
||||||
|
# Layer with specific size that doesn't match grid
|
||||||
|
layer = mcrfpy.ColorLayer(name='test', grid_size=(10, 10))
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[])
|
||||||
|
|
||||||
|
try:
|
||||||
|
grid.add_layer(layer)
|
||||||
|
assert False, "Should raise ValueError for size mismatch"
|
||||||
|
except ValueError as e:
|
||||||
|
assert "size" in str(e).lower(), f"Error should mention size: {e}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_protected_names():
|
||||||
|
"""Test that protected names (walkable, transparent) are rejected."""
|
||||||
|
print("Test 10: Protected names...")
|
||||||
|
|
||||||
|
layer = mcrfpy.ColorLayer(name='walkable')
|
||||||
|
grid = mcrfpy.Grid(grid_size=(5, 5), layers=[])
|
||||||
|
|
||||||
|
try:
|
||||||
|
grid.add_layer(layer)
|
||||||
|
assert False, "Should raise ValueError for protected name 'walkable'"
|
||||||
|
except ValueError as e:
|
||||||
|
assert "reserved" in str(e).lower() or "walkable" in str(e).lower(), f"Error: {e}"
|
||||||
|
|
||||||
|
layer2 = mcrfpy.ColorLayer(name='transparent')
|
||||||
|
try:
|
||||||
|
grid.add_layer(layer2)
|
||||||
|
assert False, "Should raise ValueError for protected name 'transparent'"
|
||||||
|
except ValueError as e:
|
||||||
|
assert "reserved" in str(e).lower() or "transparent" in str(e).lower(), f"Error: {e}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def test_already_attached_error():
|
||||||
|
"""Test that attaching a layer to two grids raises error."""
|
||||||
|
print("Test 11: Already attached error...")
|
||||||
|
|
||||||
|
layer = mcrfpy.ColorLayer(name='shared')
|
||||||
|
grid1 = mcrfpy.Grid(grid_size=(5, 5), layers=[layer])
|
||||||
|
grid2 = mcrfpy.Grid(grid_size=(5, 5), layers=[])
|
||||||
|
|
||||||
|
try:
|
||||||
|
grid2.add_layer(layer)
|
||||||
|
assert False, "Should raise ValueError for already attached layer"
|
||||||
|
except ValueError as e:
|
||||||
|
assert "attached" in str(e).lower() or "another" in str(e).lower(), f"Error: {e}"
|
||||||
|
|
||||||
|
print(" PASS")
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""Run all tests."""
|
||||||
|
print("Running Grid Layer API tests...\n")
|
||||||
|
|
||||||
|
test_layer_name_in_constructor()
|
||||||
|
test_lazy_allocation()
|
||||||
|
test_layer_grid_property()
|
||||||
|
test_grid_layer_name_lookup()
|
||||||
|
test_layers_returns_tuple()
|
||||||
|
test_add_layer_accepts_objects()
|
||||||
|
test_remove_layer_by_name()
|
||||||
|
test_remove_layer_by_object()
|
||||||
|
test_name_collision_replaces()
|
||||||
|
test_size_validation()
|
||||||
|
test_protected_names()
|
||||||
|
test_already_attached_error()
|
||||||
|
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print("All tests PASSED!")
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
run_all_tests()
|
||||||
|
sys.exit(0)
|
||||||
|
except AssertionError as e:
|
||||||
|
print(f"\nFAIL: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nERROR: {type(e).__name__}: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue