diff --git a/src/GridLayers.h b/src/GridLayers.h index 44531ae..6edb01d 100644 --- a/src/GridLayers.h +++ b/src/GridLayers.h @@ -5,6 +5,7 @@ #include #include #include +#include // Forward declarations class UIGrid; @@ -23,6 +24,7 @@ enum class GridLayerType { class GridLayer { public: GridLayerType type; + std::string name; // #150 - Layer name for GridPoint property access int z_index; // Negative = below entities, >= 0 = above entities int grid_x, grid_y; // Dimensions UIGrid* parent_grid; // Parent grid reference diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 72c5683..ef5de71 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -5,7 +5,8 @@ #include "UIEntity.h" #include "Profiler.h" #include -#include // #142 - for std::floor +#include // #142 - for std::floor +#include // #150 - for strcmp // UIDrawable methods now in UIBase.h UIGrid::UIGrid() @@ -159,84 +160,14 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom); int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom); - //sprite.setScale(sf::Vector2f(zoom, zoom)); - sf::RectangleShape r; // for colors and overlays - r.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom)); - r.setOutlineThickness(0); - int x_limit = left_edge + width_sq + 2; if (x_limit > grid_x) x_limit = grid_x; int y_limit = top_edge + height_sq + 2; if (y_limit > grid_y) y_limit = grid_y; - // base layer - bottom color, tile sprite ("ground") - int cellsRendered = 0; - - // #123 - Use chunk-based rendering for large grids - if (use_chunks && chunk_manager) { - // Get visible chunks based on cell coordinate bounds - float right_edge = left_edge + width_sq + 2; - float bottom_edge = top_edge + height_sq + 2; - auto visible_chunks = chunk_manager->getVisibleChunks(left_edge, top_edge, right_edge, bottom_edge); - - for (auto* chunk : visible_chunks) { - // Re-render dirty chunks to their cached textures - if (chunk->dirty) { - chunk->renderToTexture(cell_width, cell_height, ptex); - } - - // Calculate pixel position for this chunk's sprite - float chunk_pixel_x = (chunk->world_x * cell_width - left_spritepixels) * zoom; - float chunk_pixel_y = (chunk->world_y * cell_height - top_spritepixels) * zoom; - - // Set up and draw the chunk sprite - chunk->cached_sprite.setPosition(chunk_pixel_x, chunk_pixel_y); - chunk->cached_sprite.setScale(zoom, zoom); - renderTexture.draw(chunk->cached_sprite); - - cellsRendered += chunk->width * chunk->height; - } - } else { - // Original cell-by-cell rendering for small grids - for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); - x < x_limit; //x < view_width; - x+=1) - { - //for (float y = (top_edge >= 0 ? top_edge : 0); - for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); - y < y_limit; //y < view_height; - y+=1) - { - auto pixel_pos = sf::Vector2f( - (x*cell_width - left_spritepixels) * zoom, - (y*cell_height - top_spritepixels) * zoom ); - - auto gridpoint = at(std::floor(x), std::floor(y)); - - //sprite.setPosition(pixel_pos); - - r.setPosition(pixel_pos); - r.setFillColor(gridpoint.color); - renderTexture.draw(r); - - // tilesprite - only draw if texture is available - // if discovered but not visible, set opacity to 90% - // if not discovered... just don't draw it? - if (ptex && gridpoint.tilesprite != -1) { - sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);; - renderTexture.draw(sprite); - } - - cellsRendered++; - } - } - } - - // Record how many cells were rendered - Resources::game->metrics.gridCellsRendered += cellsRendered; - - // #147 - Render dynamic layers with z_index < 0 (below entities) + // #150 - Layers are now the sole source of grid rendering (base layer removed) + // Render layers with z_index < 0 (below entities) sortLayers(); for (auto& layer : layers) { if (layer->z_index >= 0) break; // Stop at layers that go above entities @@ -450,20 +381,42 @@ PyObjectsEnum UIGrid::derived_type() } // #147 - Layer management methods -std::shared_ptr UIGrid::addColorLayer(int z_index) { +std::shared_ptr UIGrid::addColorLayer(int z_index, const std::string& name) { auto layer = std::make_shared(z_index, grid_x, grid_y, this); + layer->name = name; layers.push_back(layer); layers_need_sort = true; return layer; } -std::shared_ptr UIGrid::addTileLayer(int z_index, std::shared_ptr texture) { +std::shared_ptr UIGrid::addTileLayer(int z_index, std::shared_ptr texture, const std::string& name) { auto layer = std::make_shared(z_index, grid_x, grid_y, this, texture); + layer->name = name; layers.push_back(layer); layers_need_sort = true; return layer; } +std::shared_ptr UIGrid::getLayerByName(const std::string& name) { + for (auto& layer : layers) { + if (layer->name == name) { + return layer; + } + } + return nullptr; +} + +bool UIGrid::isProtectedLayerName(const std::string& name) { + // #150 - These names are reserved for GridPoint properties + static const std::vector protected_names = { + "walkable", "transparent", "color", "color_overlay" + }; + for (const auto& pn : protected_names) { + if (name == pn) return true; + } + return false; +} + void UIGrid::removeLayer(std::shared_ptr layer) { auto it = std::find(layers.begin(), layers.end(), layer); if (it != layers.end()) { @@ -779,6 +732,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { PyObject* textureObj = nullptr; PyObject* fill_color = nullptr; PyObject* click_handler = nullptr; + PyObject* layers_obj = nullptr; // #150 - layers dict float center_x = 0.0f, center_y = 0.0f; float zoom = 1.0f; // perspective is now handled via properties, not init args @@ -788,21 +742,23 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { const char* name = nullptr; float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; int grid_x = 2, grid_y = 2; // Default to 2x2 grid - + // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { "pos", "size", "grid_size", "texture", // Positional args (as per spec) // Keyword-only args "fill_color", "click", "center_x", "center_y", "zoom", "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y", + "layers", // #150 - layers dict parameter nullptr }; - + // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffii", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffiiO", const_cast(kwlist), &pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional &fill_color, &click_handler, ¢er_x, ¢er_y, &zoom, - &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) { + &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y, + &layers_obj)) { return -1; } @@ -935,7 +891,55 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { } self->data->click_register(click_handler); } - + + // #150 - Handle layers dict + // Default: {"tilesprite": "tile"} when layers not provided + // Empty dict: no rendering layers (entity storage + pathfinding only) + if (layers_obj == nullptr) { + // Default layer: single TileLayer named "tilesprite" + self->data->addTileLayer(0, texture_ptr, "tilesprite"); + } else if (layers_obj != Py_None) { + if (!PyDict_Check(layers_obj)) { + PyErr_SetString(PyExc_TypeError, "layers must be a dict mapping names to types ('color' or 'tile')"); + return -1; + } + + PyObject* key; + PyObject* value; + Py_ssize_t pos = 0; + int layer_z = 0; // Auto-increment z_index 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; + } + + const char* layer_name = PyUnicode_AsUTF8(key); + const char* layer_type = PyUnicode_AsUTF8(value); + + // Check for protected names + if (UIGrid::isProtectedLayerName(layer_name)) { + PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer_name); + return -1; + } + + if (strcmp(layer_type, "color") == 0) { + self->data->addColorLayer(layer_z++, layer_name); + } else if (strcmp(layer_type, "tile") == 0) { + self->data->addTileLayer(layer_z++, texture_ptr, layer_name); + } else { + PyErr_Format(PyExc_ValueError, "Unknown layer type '%s' (expected 'color' or 'tile')", layer_type); + return -1; + } + } + } + // else: layers_obj is Py_None - explicit empty, no layers created + // Initialize weak reference list self->weakreflist = NULL; diff --git a/src/UIGrid.h b/src/UIGrid.h index 34efa55..55718b9 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -95,11 +95,15 @@ public: std::vector> layers; bool layers_need_sort = true; // Dirty flag for z_index sorting - // Layer management - std::shared_ptr addColorLayer(int z_index); - std::shared_ptr addTileLayer(int z_index, std::shared_ptr texture = nullptr); + // Layer management (#150 - extended with names) + std::shared_ptr addColorLayer(int z_index, const std::string& name = ""); + std::shared_ptr addTileLayer(int z_index, std::shared_ptr texture = nullptr, const std::string& name = ""); void removeLayer(std::shared_ptr layer); void sortLayers(); + std::shared_ptr getLayerByName(const std::string& name); + + // #150 - Protected layer names (reserved for GridPoint properties) + static bool isProtectedLayerName(const std::string& name); // Background rendering sf::Color fill_color; diff --git a/src/UIGridPoint.cpp b/src/UIGridPoint.cpp index 201fb27..a893651 100644 --- a/src/UIGridPoint.cpp +++ b/src/UIGridPoint.cpp @@ -1,5 +1,7 @@ #include "UIGridPoint.h" #include "UIGrid.h" +#include "GridLayers.h" // #150 - for GridLayerType, ColorLayer, TileLayer +#include // #150 - for strcmp UIGridPoint::UIGridPoint() : color(1.0f, 1.0f, 1.0f), color_overlay(0.0f, 0.0f, 0.0f), walkable(false), transparent(false), @@ -193,9 +195,103 @@ PyObject* UIGridPointState::repr(PyUIGridPointStateObject* self) { if (!self->data) ss << ""; else { auto gps = self->data; - ss << ""; } std::string repr_str = ss.str(); return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); } + +// #150 - Dynamic attribute access for named layers +PyObject* UIGridPoint::getattro(PyUIGridPointObject* self, PyObject* name) { + // First try standard attribute lookup (built-in properties) + PyObject* result = PyObject_GenericGetAttr((PyObject*)self, name); + if (result != nullptr || !PyErr_ExceptionMatches(PyExc_AttributeError)) { + return result; + } + + // Clear the AttributeError and check for layer name + PyErr_Clear(); + + if (!self->grid) { + PyErr_SetString(PyExc_RuntimeError, "GridPoint has no parent grid"); + return nullptr; + } + + const char* attr_name = PyUnicode_AsUTF8(name); + if (!attr_name) return nullptr; + + // Look up layer by name + auto layer = self->grid->getLayerByName(attr_name); + if (!layer) { + PyErr_Format(PyExc_AttributeError, "'GridPoint' object has no attribute '%s'", attr_name); + return nullptr; + } + + int x = self->data->grid_x; + int y = self->data->grid_y; + + // Get value based on layer type + if (layer->type == GridLayerType::Color) { + auto color_layer = std::static_pointer_cast(layer); + return sfColor_to_PyObject(color_layer->at(x, y)); + } else if (layer->type == GridLayerType::Tile) { + auto tile_layer = std::static_pointer_cast(layer); + return PyLong_FromLong(tile_layer->at(x, y)); + } + + PyErr_SetString(PyExc_RuntimeError, "Unknown layer type"); + return nullptr; +} + +int UIGridPoint::setattro(PyUIGridPointObject* self, PyObject* name, PyObject* value) { + // First try standard attribute setting (built-in properties) + // We need to check if this is a known attribute first + const char* attr_name = PyUnicode_AsUTF8(name); + if (!attr_name) return -1; + + // Check if it's a built-in property (defined in getsetters) + for (PyGetSetDef* gsd = UIGridPoint::getsetters; gsd->name != nullptr; gsd++) { + if (strcmp(gsd->name, attr_name) == 0) { + // It's a built-in property, use standard setter + return PyObject_GenericSetAttr((PyObject*)self, name, value); + } + } + + // Not a built-in property - try layer lookup + if (!self->grid) { + PyErr_SetString(PyExc_RuntimeError, "GridPoint has no parent grid"); + return -1; + } + + auto layer = self->grid->getLayerByName(attr_name); + if (!layer) { + PyErr_Format(PyExc_AttributeError, "'GridPoint' object has no attribute '%s'", attr_name); + return -1; + } + + int x = self->data->grid_x; + int y = self->data->grid_y; + + // Set value based on layer type + if (layer->type == GridLayerType::Color) { + auto color_layer = std::static_pointer_cast(layer); + sf::Color color = PyObject_to_sfColor(value); + if (PyErr_Occurred()) return -1; + color_layer->at(x, y) = color; + color_layer->markDirty(); + return 0; + } else if (layer->type == GridLayerType::Tile) { + auto tile_layer = std::static_pointer_cast(layer); + if (!PyLong_Check(value)) { + PyErr_SetString(PyExc_TypeError, "Tile layer values must be integers"); + return -1; + } + tile_layer->at(x, y) = PyLong_AsLong(value); + tile_layer->markDirty(); + return 0; + } + + PyErr_SetString(PyExc_RuntimeError, "Unknown layer type"); + return -1; +} diff --git a/src/UIGridPoint.h b/src/UIGridPoint.h index d02ad31..0f117ff 100644 --- a/src/UIGridPoint.h +++ b/src/UIGridPoint.h @@ -52,6 +52,10 @@ public: static PyObject* get_bool_member(PyUIGridPointObject* self, void* closure); static int set_color(PyUIGridPointObject* self, PyObject* value, void* closure); static PyObject* repr(PyUIGridPointObject* self); + + // #150 - Dynamic property access for named layers + static PyObject* getattro(PyUIGridPointObject* self, PyObject* name); + static int setattro(PyUIGridPointObject* self, PyObject* name, PyObject* value); }; // UIGridPointState - entity-specific info for each cell @@ -73,6 +77,9 @@ namespace mcrfpydef { .tp_basicsize = sizeof(PyUIGridPointObject), .tp_itemsize = 0, .tp_repr = (reprfunc)UIGridPoint::repr, + // #150 - Dynamic attribute access for named layers + .tp_getattro = (getattrofunc)UIGridPoint::getattro, + .tp_setattro = (setattrofunc)UIGridPoint::setattro, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = "UIGridPoint object", .tp_getset = UIGridPoint::getsetters, diff --git a/tests/regression/issue_147_grid_layers.py b/tests/regression/issue_147_grid_layers.py index c3e5d6e..3f1b8b9 100644 --- a/tests/regression/issue_147_grid_layers.py +++ b/tests/regression/issue_147_grid_layers.py @@ -21,13 +21,13 @@ def run_test(runtime): ui = mcrfpy.sceneUI("test") texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - # Create grid - grid = mcrfpy.Grid(pos=(50, 50), size=(400, 300), grid_size=(20, 15), texture=texture) + # Create grid with explicit empty layers (#150 migration) + grid = mcrfpy.Grid(pos=(50, 50), size=(400, 300), grid_size=(20, 15), texture=texture, layers={}) ui.append(grid) print("\n--- Test 1: Initial state (no layers) ---") if len(grid.layers) == 0: - print(" PASS: Grid starts with no layers") + print(" PASS: Grid starts with no layers (layers={})") else: print(f" FAIL: Expected 0 layers, got {len(grid.layers)}") sys.exit(1)