feat: Phase 1 - safe constructors and _Drawable foundation

Closes #7 - Make all UI class constructors safe:
- Added safe default constructors for UISprite, UIGrid, UIEntity, UICaption
- Initialize all members to predictable values
- Made Python init functions accept no arguments
- Added x,y properties to UIEntity

Closes #71 - Create _Drawable Python base class:
- Created PyDrawable.h/cpp with base type (not yet inherited by UI types)
- Registered in module initialization

Closes #87 - Add visible property:
- Added bool visible=true to UIDrawable base class
- All render methods check visibility before drawing

Closes #88 - Add opacity property:
- Added float opacity=1.0 to UIDrawable base class
- UICaption and UISprite apply opacity to alpha channel

Closes #89 - Add get_bounds() method:
- Virtual method returns sf::FloatRect(x,y,w,h)
- Implemented in Frame, Caption, Sprite, Grid

Closes #98 - Add move() and resize() methods:
- move(dx,dy) for relative movement
- resize(w,h) for absolute sizing
- Caption resize is no-op (size controlled by font)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-07-06 00:13:39 -04:00
commit f1b354e47d
15 changed files with 531 additions and 161 deletions

View file

@ -5,7 +5,12 @@
#include "PyVector.h"
UIEntity::UIEntity() {} // this will not work lol. TODO remove default constructor by finding the shared pointer inits that use it
UIEntity::UIEntity()
: self(nullptr), grid(nullptr), position(0.0f, 0.0f), collision_pos(0, 0)
{
// Initialize sprite with safe defaults (sprite has its own safe constructor now)
// gridstate vector starts empty since we don't know grid dimensions
}
UIEntity::UIEntity(UIGrid& grid)
: gridstate(grid.grid_x * grid.grid_y)
@ -67,25 +72,43 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
//static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr };
//float x = 0.0f, y = 0.0f, scale = 1.0f;
static const char* keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr };
PyObject* pos;
PyObject* pos = NULL; // Must initialize to NULL for optional arguments
float scale = 1.0f;
int sprite_index = -1;
int sprite_index = 0; // Default to sprite index 0 instead of -1
PyObject* texture = NULL;
PyObject* grid = NULL;
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O",
// const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid))
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO",
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiO",
const_cast<char**>(keywords), &pos, &texture, &sprite_index, &grid))
{
return -1;
}
PyVectorObject* pos_result = PyVector::from_arg(pos);
if (!pos_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
// Handle position - default to (0, 0) if not provided
PyVectorObject* pos_result = nullptr;
if (pos && pos != Py_None) {
pos_result = PyVector::from_arg(pos);
if (!pos_result)
{
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
return -1;
}
} else {
// Create default position (0, 0)
PyObject* vector_class = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
if (vector_class) {
PyObject* pos_obj = PyObject_CallFunction(vector_class, "ff", 0.0f, 0.0f);
Py_DECREF(vector_class);
if (pos_obj) {
pos_result = (PyVectorObject*)pos_obj;
}
}
if (!pos_result) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create default position vector");
return -1;
}
}
// check types for texture
@ -104,10 +127,11 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
texture_ptr = McRFPy_API::default_texture;
}
if (!texture_ptr) {
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
return -1;
}
// Allow creation without texture for testing purposes
// if (!texture_ptr) {
// PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
// return -1;
// }
if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
@ -124,8 +148,19 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
Py_INCREF(self);
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
if (texture_ptr) {
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
} else {
// Create an empty sprite for testing
self->data->sprite = UISprite();
}
self->data->position = pos_result->data;
// Clean up the position object if we created it
if (!pos || pos == Py_None) {
Py_DECREF(pos_result);
}
if (grid != NULL) {
PyUIGridObject* pygrid = (PyUIGridObject*)grid;
self->data->grid = pygrid->data;
@ -244,6 +279,50 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl
return 0;
}
PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure)
{
auto member_ptr = reinterpret_cast<long>(closure);
if (member_ptr == 0) // x
return PyFloat_FromDouble(self->data->position.x);
else if (member_ptr == 1) // y
return PyFloat_FromDouble(self->data->position.y);
else
{
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
return nullptr;
}
}
int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* closure)
{
float val;
auto member_ptr = reinterpret_cast<long>(closure);
if (PyFloat_Check(value))
{
val = PyFloat_AsDouble(value);
}
else if (PyLong_Check(value))
{
val = PyLong_AsLong(value);
}
else
{
PyErr_SetString(PyExc_TypeError, "Value must be a floating point number.");
return -1;
}
if (member_ptr == 0) // x
{
self->data->position.x = val;
self->data->collision_pos.x = static_cast<int>(val);
}
else if (member_ptr == 1) // y
{
self->data->position.y = val;
self->data->collision_pos.y = static_cast<int>(val);
}
return 0;
}
PyMethodDef UIEntity::methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O},
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
@ -256,6 +335,8 @@ PyGetSetDef UIEntity::getsetters[] = {
{"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL},
{"sprite_index", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display", NULL},
{"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display (deprecated: use sprite_index)", NULL},
{"x", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity x position", (void*)0},
{"y", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity y position", (void*)1},
{NULL} /* Sentinel */
};