feat: Migrate Grid to user-driven layer rendering (closes #150)
- Add `layers` dict parameter to Grid constructor for explicit layer definitions
- `layers={"ground": "color", "terrain": "tile"}` creates named layers
- `layers={}` creates empty grid (entities + pathfinding only)
- Default creates single TileLayer named "tilesprite" for backward compat
- Implement dynamic GridPoint property access via layer names
- `grid.at(x,y).layer_name = value` routes to corresponding layer
- Protected names (walkable, transparent, etc.) still use GridPoint
- Remove base layer rendering from UIGrid::render()
- Layers are now the sole source of grid rendering
- Old chunk_manager remains for GridPoint data access
- FOV overlay unchanged
- Update test to use explicit `layers={}` parameter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9469c04b01
commit
a258613faa
|
|
@ -5,6 +5,7 @@
|
|||
#include <SFML/Graphics.hpp>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// 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
|
||||
|
|
|
|||
158
src/UIGrid.cpp
158
src/UIGrid.cpp
|
|
@ -5,7 +5,8 @@
|
|||
#include "UIEntity.h"
|
||||
#include "Profiler.h"
|
||||
#include <algorithm>
|
||||
#include <cmath> // #142 - for std::floor
|
||||
#include <cmath> // #142 - for std::floor
|
||||
#include <cstring> // #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<ColorLayer> UIGrid::addColorLayer(int z_index) {
|
||||
std::shared_ptr<ColorLayer> UIGrid::addColorLayer(int z_index, const std::string& name) {
|
||||
auto layer = std::make_shared<ColorLayer>(z_index, grid_x, grid_y, this);
|
||||
layer->name = name;
|
||||
layers.push_back(layer);
|
||||
layers_need_sort = true;
|
||||
return layer;
|
||||
}
|
||||
|
||||
std::shared_ptr<TileLayer> UIGrid::addTileLayer(int z_index, std::shared_ptr<PyTexture> texture) {
|
||||
std::shared_ptr<TileLayer> UIGrid::addTileLayer(int z_index, std::shared_ptr<PyTexture> texture, const std::string& name) {
|
||||
auto layer = std::make_shared<TileLayer>(z_index, grid_x, grid_y, this, texture);
|
||||
layer->name = name;
|
||||
layers.push_back(layer);
|
||||
layers_need_sort = true;
|
||||
return layer;
|
||||
}
|
||||
|
||||
std::shared_ptr<GridLayer> 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<std::string> 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<GridLayer> 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
|
||||
|
|
@ -795,14 +749,16 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
// 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<char**>(kwlist),
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffiiO", const_cast<char**>(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;
|
||||
}
|
||||
|
||||
|
|
@ -936,6 +892,54 @@ 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;
|
||||
|
||||
|
|
|
|||
10
src/UIGrid.h
10
src/UIGrid.h
|
|
@ -95,11 +95,15 @@ public:
|
|||
std::vector<std::shared_ptr<GridLayer>> layers;
|
||||
bool layers_need_sort = true; // Dirty flag for z_index sorting
|
||||
|
||||
// Layer management
|
||||
std::shared_ptr<ColorLayer> addColorLayer(int z_index);
|
||||
std::shared_ptr<TileLayer> addTileLayer(int z_index, std::shared_ptr<PyTexture> texture = nullptr);
|
||||
// Layer management (#150 - extended with names)
|
||||
std::shared_ptr<ColorLayer> addColorLayer(int z_index, const std::string& name = "");
|
||||
std::shared_ptr<TileLayer> addTileLayer(int z_index, std::shared_ptr<PyTexture> texture = nullptr, const std::string& name = "");
|
||||
void removeLayer(std::shared_ptr<GridLayer> layer);
|
||||
void sortLayers();
|
||||
std::shared_ptr<GridLayer> 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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#include "UIGridPoint.h"
|
||||
#include "UIGrid.h"
|
||||
#include "GridLayers.h" // #150 - for GridLayerType, ColorLayer, TileLayer
|
||||
#include <cstring> // #150 - for strcmp
|
||||
|
||||
UIGridPoint::UIGridPoint()
|
||||
: color(1.0f, 1.0f, 1.0f), color_overlay(0.0f, 0.0f, 0.0f), walkable(false), transparent(false),
|
||||
|
|
@ -199,3 +201,97 @@ PyObject* UIGridPointState::repr(PyUIGridPointStateObject* self) {
|
|||
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<ColorLayer>(layer);
|
||||
return sfColor_to_PyObject(color_layer->at(x, y));
|
||||
} else if (layer->type == GridLayerType::Tile) {
|
||||
auto tile_layer = std::static_pointer_cast<TileLayer>(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<ColorLayer>(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<TileLayer>(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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue