diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index 39ccc37..26d1d26 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -5,7 +5,8 @@ #include PyTexture::PyTexture(std::string filename, int sprite_w, int sprite_h) -: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0) +: source(filename), sprite_width(sprite_w), sprite_height(sprite_h), sheet_width(0), sheet_height(0), + display_width(-1), display_height(-1), display_offset_x(0), display_offset_y(0) { texture = sf::Texture(); if (!texture.loadFromFile(source)) { @@ -81,9 +82,14 @@ sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s) // Return an empty sprite return sf::Sprite(); } - + int tx = index % sheet_width, ty = index / sheet_width; - auto ir = sf::IntRect(tx * sprite_width, ty * sprite_height, sprite_width, sprite_height); + // #235: Apply display bounds within the cell + int dw = getDisplayWidth(); + int dh = getDisplayHeight(); + auto ir = sf::IntRect(tx * sprite_width + display_offset_x, + ty * sprite_height + display_offset_y, + dw, dh); auto sprite = sf::Sprite(texture, ir); sprite.setPosition(pos); sprite.setScale(s); @@ -138,21 +144,45 @@ Py_hash_t PyTexture::hash(PyObject* obj) int PyTexture::init(PyTextureObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = { "filename", "sprite_width", "sprite_height", nullptr }; + static const char* keywords[] = { "filename", "sprite_width", "sprite_height", + "display_size", "display_origin", nullptr }; char* filename; int sprite_width, sprite_height; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii", const_cast(keywords), &filename, &sprite_width, &sprite_height)) + PyObject* display_size_obj = nullptr; + PyObject* display_origin_obj = nullptr; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "sii|OO", const_cast(keywords), + &filename, &sprite_width, &sprite_height, &display_size_obj, &display_origin_obj)) return -1; - + // Create the texture object self->data = std::make_shared(filename, sprite_width, sprite_height); - + // Check if the texture failed to load (sheet dimensions will be 0) if (self->data->sheet_width == 0 || self->data->sheet_height == 0) { PyErr_Format(PyExc_IOError, "Failed to load texture from file: %s", filename); return -1; } - + + // #235: Parse optional display bounds + if (display_size_obj && display_size_obj != Py_None) { + int dw, dh; + if (!PyArg_ParseTuple(display_size_obj, "ii", &dw, &dh)) { + PyErr_SetString(PyExc_TypeError, "display_size must be a (width, height) tuple"); + return -1; + } + self->data->display_width = dw; + self->data->display_height = dh; + } + if (display_origin_obj && display_origin_obj != Py_None) { + int ox, oy; + if (!PyArg_ParseTuple(display_origin_obj, "ii", &ox, &oy)) { + PyErr_SetString(PyExc_TypeError, "display_origin must be an (x, y) tuple"); + return -1; + } + self->data->display_offset_x = ox; + self->data->display_offset_y = oy; + } + return 0; } @@ -191,6 +221,26 @@ PyObject* PyTexture::get_source(PyTextureObject* self, void* closure) return PyUnicode_FromString(self->data->source.c_str()); } +PyObject* PyTexture::get_display_width(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->getDisplayWidth()); +} + +PyObject* PyTexture::get_display_height(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->getDisplayHeight()); +} + +PyObject* PyTexture::get_display_offset_x(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->display_offset_x); +} + +PyObject* PyTexture::get_display_offset_y(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->display_offset_y); +} + PyGetSetDef PyTexture::getsetters[] = { {"sprite_width", (getter)PyTexture::get_sprite_width, NULL, MCRF_PROPERTY(sprite_width, "Width of each sprite in pixels (int, read-only). Specified during texture initialization."), NULL}, @@ -204,6 +254,14 @@ PyGetSetDef PyTexture::getsetters[] = { MCRF_PROPERTY(sprite_count, "Total number of sprites in the texture sheet (int, read-only). Equals sheet_width * sheet_height."), NULL}, {"source", (getter)PyTexture::get_source, NULL, MCRF_PROPERTY(source, "Source filename path (str, read-only). The path used to load this texture."), NULL}, + {"display_width", (getter)PyTexture::get_display_width, NULL, + MCRF_PROPERTY(display_width, "Display width of sprite content within each cell (int, read-only). Defaults to sprite_width."), NULL}, + {"display_height", (getter)PyTexture::get_display_height, NULL, + MCRF_PROPERTY(display_height, "Display height of sprite content within each cell (int, read-only). Defaults to sprite_height."), NULL}, + {"display_offset_x", (getter)PyTexture::get_display_offset_x, NULL, + MCRF_PROPERTY(display_offset_x, "X offset of sprite content within each cell (int, read-only). Default 0."), NULL}, + {"display_offset_y", (getter)PyTexture::get_display_offset_y, NULL, + MCRF_PROPERTY(display_offset_y, "Y offset of sprite content within each cell (int, read-only). Default 0."), NULL}, {NULL} // Sentinel }; diff --git a/src/PyTexture.h b/src/PyTexture.h index 3d520df..2e6b3ff 100644 --- a/src/PyTexture.h +++ b/src/PyTexture.h @@ -17,10 +17,14 @@ private: int sheet_width, sheet_height; // Private default constructor for factory methods - PyTexture() : source(""), sprite_width(0), sprite_height(0), sheet_width(0), sheet_height(0) {} + PyTexture() : source(""), sprite_width(0), sprite_height(0), sheet_width(0), sheet_height(0), + display_width(-1), display_height(-1), display_offset_x(0), display_offset_y(0) {} public: int sprite_width, sprite_height; // just use them read only, OK? + // #235: Display bounds for non-uniform sprite content within cells + int display_width, display_height; // -1 = same as sprite_width/height + int display_offset_x, display_offset_y; // offset within cell to content area PyTexture(std::string filename, int sprite_w, int sprite_h); // #144: Factory method to create texture from rendered content (snapshot) @@ -42,6 +46,10 @@ public: static int init(PyTextureObject*, PyObject*, PyObject*); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); + // Effective display dimensions (resolves -1 defaults) + int getDisplayWidth() const { return display_width >= 0 ? display_width : sprite_width; } + int getDisplayHeight() const { return display_height >= 0 ? display_height : sprite_height; } + // Getters for properties static PyObject* get_sprite_width(PyTextureObject* self, void* closure); static PyObject* get_sprite_height(PyTextureObject* self, void* closure); @@ -49,6 +57,10 @@ public: static PyObject* get_sheet_height(PyTextureObject* self, void* closure); static PyObject* get_sprite_count(PyTextureObject* self, void* closure); static PyObject* get_source(PyTextureObject* self, void* closure); + static PyObject* get_display_width(PyTextureObject* self, void* closure); + static PyObject* get_display_height(PyTextureObject* self, void* closure); + static PyObject* get_display_offset_x(PyTextureObject* self, void* closure); + static PyObject* get_display_offset_y(PyTextureObject* self, void* closure); static PyGetSetDef getsetters[]; @@ -69,17 +81,22 @@ namespace mcrfpydef { .tp_hash = PyTexture::hash, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR( - "Texture(filename: str, sprite_width: int = 0, sprite_height: int = 0)\n\n" + "Texture(filename: str, sprite_width: int = 0, sprite_height: int = 0, " + "display_size: tuple = None, display_origin: tuple = None)\n\n" "A texture atlas for sprites and tiles.\n\n" "Args:\n" " filename: Path to an image file (PNG, BMP, etc.).\n" " sprite_width: Width of each sprite cell in pixels (0 = full image).\n" - " sprite_height: Height of each sprite cell in pixels (0 = full image).\n\n" + " sprite_height: Height of each sprite cell in pixels (0 = full image).\n" + " display_size: Optional (w, h) actual content size within each cell.\n" + " display_origin: Optional (x, y) content offset within each cell.\n\n" "Properties:\n" " sprite_width, sprite_height (int, read-only): Cell dimensions.\n" " sheet_width, sheet_height (int, read-only): Grid dimensions in cells.\n" " sprite_count (int, read-only): Total number of sprite cells.\n" " source (str, read-only): File path used to load this texture.\n" + " display_width, display_height (int, read-only): Content size within cells.\n" + " display_offset_x, display_offset_y (int, read-only): Content offset within cells.\n" ), .tp_getset = PyTexture::getsetters, //.tp_base = &PyBaseObject_Type, diff --git a/tests/unit/texture_display_bounds_test.py b/tests/unit/texture_display_bounds_test.py new file mode 100644 index 0000000..57a919e --- /dev/null +++ b/tests/unit/texture_display_bounds_test.py @@ -0,0 +1,60 @@ +"""Test texture display bounds for non-uniform sprite content (#235). + +Verifies that Texture accepts display_size and display_origin parameters +to crop sprite rendering to a sub-region within each atlas cell. +""" +import mcrfpy +import sys + +def test_default_display_bounds(): + """Without display bounds, display dims equal sprite dims.""" + tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + assert tex.display_width == 16, f"Expected 16, got {tex.display_width}" + assert tex.display_height == 16, f"Expected 16, got {tex.display_height}" + assert tex.display_offset_x == 0, f"Expected 0, got {tex.display_offset_x}" + assert tex.display_offset_y == 0, f"Expected 0, got {tex.display_offset_y}" + print(" PASS: default display bounds") + +def test_custom_display_size(): + """display_size crops sprite content within cells.""" + tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16, + display_size=(12, 14)) + assert tex.display_width == 12, f"Expected 12, got {tex.display_width}" + assert tex.display_height == 14, f"Expected 14, got {tex.display_height}" + assert tex.sprite_width == 16, f"sprite_width should be unchanged: {tex.sprite_width}" + assert tex.sprite_height == 16, f"sprite_height should be unchanged: {tex.sprite_height}" + print(" PASS: custom display size") + +def test_custom_display_origin(): + """display_origin offsets content within cells.""" + tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16, + display_size=(12, 14), display_origin=(2, 1)) + assert tex.display_offset_x == 2, f"Expected 2, got {tex.display_offset_x}" + assert tex.display_offset_y == 1, f"Expected 1, got {tex.display_offset_y}" + print(" PASS: custom display origin") + +def test_display_bounds_sprite_creation(): + """Sprites created from bounded textures should work in UI elements.""" + tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16, + display_size=(12, 14), display_origin=(2, 1)) + sprite = mcrfpy.Sprite(pos=(10, 10), texture=tex, sprite_index=0) + assert sprite is not None, "Sprite creation with display bounds failed" + print(" PASS: sprite creation with display bounds") + +def test_display_bounds_in_grid(): + """Entities using bounded textures should render in grids.""" + tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16, + display_size=(12, 14), display_origin=(2, 1)) + grid = mcrfpy.Grid(grid_size=(10, 10), texture=tex) + entity = mcrfpy.Entity(grid_pos=(3, 3), texture=tex, sprite_index=5, grid=grid) + assert entity is not None, "Entity creation with display bounds failed" + print(" PASS: entity with display bounds in grid") + +print("Testing #235: Texture display bounds...") +test_default_display_bounds() +test_custom_display_size() +test_custom_display_origin() +test_display_bounds_sprite_creation() +test_display_bounds_in_grid() +print("All #235 tests passed.") +sys.exit(0)