Add subscript protocol to ColorLayer, TileLayer, and Grid
Implements __getitem__/__setitem__ on ColorLayer, TileLayer, and Grid (GridView) for ergonomic cell access, mirroring the existing pattern on HeightMap and DiscreteMap. Part of the 1.0 API freeze ergonomic pass -- no existing .at() / .set() methods are removed or changed. * ColorLayer[x, y] returns mcrfpy.Color; assignment accepts a Color or a 3/4-tuple via PyColor::fromPy. * TileLayer[x, y] returns / accepts an int (sprite index, -1 transparent). * Grid[x, y] returns the same GridPoint as Grid.at(x, y); assignment raises TypeError because GridPoints are views, not assignable values. * Internal _GridData (PyUIGridType) gets the same TypeError-raising setitem for consistency. Keys are 2-tuples (x, y); anything else raises TypeError. Out-of-bounds coordinates raise IndexError. Subscript on Grid (the user-facing GridView, #252) delegates to its underlying GridData via aliasing shared_ptr, the same way UIGridView::get_grid wraps the data.
This commit is contained in:
parent
3030ac488b
commit
c52a6a0db6
5 changed files with 235 additions and 1 deletions
|
|
@ -945,6 +945,90 @@ PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* arg
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Subscript protocol: layer[x, y] / layer[x, y] = value
|
||||
// =============================================================================
|
||||
|
||||
// Helper: parse a 2-tuple key into (x, y) ints. Sets TypeError on bad keys.
|
||||
static bool parse_subscript_key(PyObject* key, int* x, int* y) {
|
||||
if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"Layer indices must be a 2-tuple (x, y)");
|
||||
return false;
|
||||
}
|
||||
PyObject* x_obj = PyTuple_GetItem(key, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(key, 1);
|
||||
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Layer indices must be integers");
|
||||
return false;
|
||||
}
|
||||
*x = (int)PyLong_AsLong(x_obj);
|
||||
*y = (int)PyLong_AsLong(y_obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_subscript(PyColorLayerObject* self, PyObject* key) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
int x, y;
|
||||
if (!parse_subscript_key(key, &x, &y)) return NULL;
|
||||
|
||||
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
|
||||
PyErr_Format(PyExc_IndexError,
|
||||
"Position (%d, %d) out of bounds for ColorLayer of size (%d, %d)",
|
||||
x, y, self->data->grid_x, self->data->grid_y);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const sf::Color& color = self->data->at(x, y);
|
||||
|
||||
// Wrap as mcrfpy.Color
|
||||
auto* color_type = (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Color");
|
||||
if (!color_type) return NULL;
|
||||
PyColorObject* color_obj = (PyColorObject*)color_type->tp_alloc(color_type, 0);
|
||||
Py_DECREF(color_type);
|
||||
if (!color_obj) return NULL;
|
||||
color_obj->data = color;
|
||||
return (PyObject*)color_obj;
|
||||
}
|
||||
|
||||
int PyGridLayerAPI::ColorLayer_subscript_assign(PyColorLayerObject* self, PyObject* key, PyObject* value) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
if (value == nullptr) {
|
||||
PyErr_SetString(PyExc_TypeError, "cannot delete ColorLayer cells");
|
||||
return -1;
|
||||
}
|
||||
int x, y;
|
||||
if (!parse_subscript_key(key, &x, &y)) return -1;
|
||||
|
||||
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
|
||||
PyErr_Format(PyExc_IndexError,
|
||||
"Position (%d, %d) out of bounds for ColorLayer of size (%d, %d)",
|
||||
x, y, self->data->grid_x, self->data->grid_y);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// PyColor::fromPy accepts Color objects, tuples, lists, ints; sets PyErr on failure.
|
||||
sf::Color color = PyColor::fromPy(value);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
|
||||
self->data->at(x, y) = color;
|
||||
self->data->markDirty(x, y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyMappingMethods PyGridLayerAPI::ColorLayer_mapping_methods = {
|
||||
.mp_length = nullptr,
|
||||
.mp_subscript = (binaryfunc)PyGridLayerAPI::ColorLayer_subscript,
|
||||
.mp_ass_subscript = (objobjargproc)PyGridLayerAPI::ColorLayer_subscript_assign,
|
||||
};
|
||||
|
||||
PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) {
|
||||
PyObject* color_obj;
|
||||
if (!PyArg_ParseTuple(args, "O", &color_obj)) {
|
||||
|
|
@ -1952,6 +2036,64 @@ PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args)
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TileLayer subscript: tl[x, y] / tl[x, y] = index
|
||||
// =============================================================================
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_subscript(PyTileLayerObject* self, PyObject* key) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return NULL;
|
||||
}
|
||||
int x, y;
|
||||
if (!parse_subscript_key(key, &x, &y)) return NULL;
|
||||
|
||||
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
|
||||
PyErr_Format(PyExc_IndexError,
|
||||
"Position (%d, %d) out of bounds for TileLayer of size (%d, %d)",
|
||||
x, y, self->data->grid_x, self->data->grid_y);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return PyLong_FromLong(self->data->at(x, y));
|
||||
}
|
||||
|
||||
int PyGridLayerAPI::TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||
return -1;
|
||||
}
|
||||
if (value == nullptr) {
|
||||
PyErr_SetString(PyExc_TypeError, "cannot delete TileLayer cells");
|
||||
return -1;
|
||||
}
|
||||
int x, y;
|
||||
if (!parse_subscript_key(key, &x, &y)) return -1;
|
||||
|
||||
if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
|
||||
PyErr_Format(PyExc_IndexError,
|
||||
"Position (%d, %d) out of bounds for TileLayer of size (%d, %d)",
|
||||
x, y, self->data->grid_x, self->data->grid_y);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "tile index must be an int");
|
||||
return -1;
|
||||
}
|
||||
int index = (int)PyLong_AsLong(value);
|
||||
|
||||
self->data->at(x, y) = index;
|
||||
self->data->markDirty(x, y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyMappingMethods PyGridLayerAPI::TileLayer_mapping_methods = {
|
||||
.mp_length = nullptr,
|
||||
.mp_subscript = (binaryfunc)PyGridLayerAPI::TileLayer_subscript,
|
||||
.mp_ass_subscript = (objobjargproc)PyGridLayerAPI::TileLayer_subscript_assign,
|
||||
};
|
||||
|
||||
PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) {
|
||||
int index;
|
||||
if (!PyArg_ParseTuple(args, "i", &index)) {
|
||||
|
|
|
|||
|
|
@ -218,6 +218,11 @@ public:
|
|||
static int ColorLayer_set_grid(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* ColorLayer_repr(PyColorLayerObject* self);
|
||||
|
||||
// Subscript protocol: layer[x, y] / layer[x, y] = value
|
||||
static PyObject* ColorLayer_subscript(PyColorLayerObject* self, PyObject* key);
|
||||
static int ColorLayer_subscript_assign(PyColorLayerObject* self, PyObject* key, PyObject* value);
|
||||
static PyMappingMethods ColorLayer_mapping_methods;
|
||||
|
||||
// TileLayer methods
|
||||
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||
|
|
@ -238,6 +243,11 @@ public:
|
|||
static int TileLayer_set_grid(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* TileLayer_repr(PyTileLayerObject* self);
|
||||
|
||||
// Subscript protocol: layer[x, y] / layer[x, y] = value
|
||||
static PyObject* TileLayer_subscript(PyTileLayerObject* self, PyObject* key);
|
||||
static int TileLayer_subscript_assign(PyTileLayerObject* self, PyObject* key, PyObject* value);
|
||||
static PyMappingMethods TileLayer_mapping_methods;
|
||||
|
||||
// Method and getset arrays
|
||||
static PyMethodDef ColorLayer_methods[];
|
||||
static PyGetSetDef ColorLayer_getsetters[];
|
||||
|
|
@ -259,6 +269,7 @@ namespace mcrfpydef {
|
|||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr,
|
||||
.tp_as_mapping = &PyGridLayerAPI::ColorLayer_mapping_methods, // layer[x, y]
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.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"
|
||||
|
|
@ -312,6 +323,7 @@ namespace mcrfpydef {
|
|||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr,
|
||||
.tp_as_mapping = &PyGridLayerAPI::TileLayer_mapping_methods, // layer[x, y]
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.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"
|
||||
|
|
|
|||
|
|
@ -82,10 +82,19 @@ PyObject* UIGrid::subscript(PyUIGridObject* self, PyObject* key)
|
|||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
// Setitem on _GridData / Grid: GridPoints are views, not assignable.
|
||||
static int UIGrid_subscript_assign(PyUIGridObject* self, PyObject* key, PyObject* value)
|
||||
{
|
||||
(void)self; (void)key; (void)value;
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"Grid points are not assignable; modify properties on the returned point");
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyMappingMethods UIGrid::mpmethods = {
|
||||
.mp_length = NULL,
|
||||
.mp_subscript = (binaryfunc)UIGrid::subscript,
|
||||
.mp_ass_subscript = NULL
|
||||
.mp_ass_subscript = (objobjargproc)UIGrid_subscript_assign,
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// UIGridView.cpp - Rendering view for GridData (#252)
|
||||
#include "UIGridView.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridPoint.h"
|
||||
#include "UIEntity.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
|
|
@ -780,6 +781,69 @@ int UIGridView::set_float_member_gv(PyUIGridViewObject* self, PyObject* value, v
|
|||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Subscript protocol: grid[x, y] -> GridPoint (delegates to GridData).
|
||||
// Setitem raises TypeError (GridPoints are views, not assignable).
|
||||
// =========================================================================
|
||||
PyObject* UIGridView::subscript(PyUIGridViewObject* self, PyObject* key)
|
||||
{
|
||||
if (!self->data || !self->data->grid_data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Grid has no underlying data");
|
||||
return NULL;
|
||||
}
|
||||
if (!PyTuple_Check(key) || PyTuple_Size(key) != 2) {
|
||||
PyErr_SetString(PyExc_TypeError, "Grid indices must be a 2-tuple (x, y)");
|
||||
return NULL;
|
||||
}
|
||||
PyObject* x_obj = PyTuple_GetItem(key, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(key, 1);
|
||||
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Grid indices must be integers");
|
||||
return NULL;
|
||||
}
|
||||
int x = (int)PyLong_AsLong(x_obj);
|
||||
int y = (int)PyLong_AsLong(y_obj);
|
||||
|
||||
auto& grid_data = self->data->grid_data;
|
||||
if (x < 0 || x >= grid_data->grid_w) {
|
||||
PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)",
|
||||
x, grid_data->grid_w);
|
||||
return NULL;
|
||||
}
|
||||
if (y < 0 || y >= grid_data->grid_h) {
|
||||
PyErr_Format(PyExc_IndexError, "y index %d is out of range [0, %d)",
|
||||
y, grid_data->grid_h);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Reconstruct shared_ptr<UIGrid> from GridData via aliasing constructor
|
||||
// (mirrors UIGridView::get_grid).
|
||||
auto grid_ptr = static_cast<UIGrid*>(grid_data.get());
|
||||
auto grid_as_uigrid = std::shared_ptr<UIGrid>(grid_data, grid_ptr);
|
||||
|
||||
auto type = &mcrfpydef::PyUIGridPointType;
|
||||
auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0);
|
||||
if (!obj) return NULL;
|
||||
obj->grid = grid_as_uigrid;
|
||||
obj->x = x;
|
||||
obj->y = y;
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
int UIGridView::subscript_assign(PyUIGridViewObject* self, PyObject* key, PyObject* value)
|
||||
{
|
||||
(void)self; (void)key; (void)value;
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"Grid points are not assignable; modify properties on the returned point");
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyMappingMethods UIGridView::mpmethods = {
|
||||
.mp_length = NULL,
|
||||
.mp_subscript = (binaryfunc)UIGridView::subscript,
|
||||
.mp_ass_subscript = (objobjargproc)UIGridView::subscript_assign,
|
||||
};
|
||||
|
||||
// #252: PyObjectType typedef for UIDRAWABLE_* macros
|
||||
typedef PyUIGridViewObject PyObjectType;
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,12 @@ public:
|
|||
|
||||
static PyMethodDef methods[];
|
||||
static PyGetSetDef getsetters[];
|
||||
|
||||
// Subscript protocol: grid[x, y] (delegates to underlying GridData).
|
||||
// Setitem raises TypeError (GridPoints are views).
|
||||
static PyObject* subscript(PyUIGridViewObject* self, PyObject* key);
|
||||
static int subscript_assign(PyUIGridViewObject* self, PyObject* key, PyObject* value);
|
||||
static PyMappingMethods mpmethods;
|
||||
};
|
||||
|
||||
// Forward declaration of methods array
|
||||
|
|
@ -139,6 +145,7 @@ namespace mcrfpydef {
|
|||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
.tp_repr = (reprfunc)UIGridView::repr,
|
||||
.tp_as_mapping = &UIGridView::mpmethods, // grid[x, y] (delegates to GridData)
|
||||
.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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue