diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index dee9c1e..1e0c2f6 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -13,7 +13,6 @@ #include "PyFOV.h" #include "PyTransition.h" #include "PyEasing.h" -#include "PyAlignment.h" #include "PyKey.h" #include "PyMouseButton.h" #include "PyInputState.h" @@ -604,13 +603,6 @@ PyObject* PyInit_mcrfpy() PyErr_Clear(); } - // Add Alignment enum class for automatic child positioning - PyObject* alignment_class = PyAlignment::create_enum_class(m); - if (!alignment_class) { - // If enum creation fails, continue without it (non-fatal) - PyErr_Clear(); - } - // Add automation submodule PyObject* automation_module = McRFPy_Automation::init_automation_module(); if (automation_module != NULL) { diff --git a/src/PyAlignment.cpp b/src/PyAlignment.cpp deleted file mode 100644 index 75c4d34..0000000 --- a/src/PyAlignment.cpp +++ /dev/null @@ -1,232 +0,0 @@ -#include "PyAlignment.h" -#include "McRFPy_API.h" - -// Static storage for cached enum class reference -PyObject* PyAlignment::alignment_enum_class = nullptr; - -// Alignment table - maps enum value to name -struct AlignmentEntry { - const char* name; - int value; - AlignmentType type; -}; - -static const AlignmentEntry alignment_table[] = { - {"TOP_LEFT", 0, AlignmentType::TOP_LEFT}, - {"TOP_CENTER", 1, AlignmentType::TOP_CENTER}, - {"TOP_RIGHT", 2, AlignmentType::TOP_RIGHT}, - {"CENTER_LEFT", 3, AlignmentType::CENTER_LEFT}, - {"CENTER", 4, AlignmentType::CENTER}, - {"CENTER_RIGHT", 5, AlignmentType::CENTER_RIGHT}, - {"BOTTOM_LEFT", 6, AlignmentType::BOTTOM_LEFT}, - {"BOTTOM_CENTER", 7, AlignmentType::BOTTOM_CENTER}, - {"BOTTOM_RIGHT", 8, AlignmentType::BOTTOM_RIGHT}, -}; - -// Legacy camelCase names (for backwards compatibility if desired) -static const char* legacy_names[] = { - "topLeft", "topCenter", "topRight", - "centerLeft", "center", "centerRight", - "bottomLeft", "bottomCenter", "bottomRight" -}; - -static const int NUM_ALIGNMENT_ENTRIES = sizeof(alignment_table) / sizeof(alignment_table[0]); - -const char* PyAlignment::alignment_name(AlignmentType value) { - int idx = static_cast(value); - if (idx >= 0 && idx < NUM_ALIGNMENT_ENTRIES) { - return alignment_table[idx].name; - } - return "NONE"; -} - -PyObject* PyAlignment::create_enum_class(PyObject* module) { - // Import IntEnum from enum module - PyObject* enum_module = PyImport_ImportModule("enum"); - if (!enum_module) { - return NULL; - } - - PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum"); - Py_DECREF(enum_module); - if (!int_enum) { - return NULL; - } - - // Create dict of enum members - PyObject* members = PyDict_New(); - if (!members) { - Py_DECREF(int_enum); - return NULL; - } - - // Add all alignment members - for (int i = 0; i < NUM_ALIGNMENT_ENTRIES; i++) { - PyObject* value = PyLong_FromLong(alignment_table[i].value); - if (!value) { - Py_DECREF(members); - Py_DECREF(int_enum); - return NULL; - } - if (PyDict_SetItemString(members, alignment_table[i].name, value) < 0) { - Py_DECREF(value); - Py_DECREF(members); - Py_DECREF(int_enum); - return NULL; - } - Py_DECREF(value); - } - - // Call IntEnum("Alignment", members) to create the enum class - PyObject* name = PyUnicode_FromString("Alignment"); - if (!name) { - Py_DECREF(members); - Py_DECREF(int_enum); - return NULL; - } - - // IntEnum(name, members) using functional API - PyObject* args = PyTuple_Pack(2, name, members); - Py_DECREF(name); - Py_DECREF(members); - if (!args) { - Py_DECREF(int_enum); - return NULL; - } - - PyObject* alignment_class = PyObject_Call(int_enum, args, NULL); - Py_DECREF(args); - Py_DECREF(int_enum); - - if (!alignment_class) { - return NULL; - } - - // Cache the reference for fast type checking - alignment_enum_class = alignment_class; - Py_INCREF(alignment_enum_class); - - // Add docstring to the enum class - static const char* alignment_doc = - "Alignment enum for positioning UI elements relative to parent bounds.\n\n" - "Values:\n" - " TOP_LEFT, TOP_CENTER, TOP_RIGHT\n" - " CENTER_LEFT, CENTER, CENTER_RIGHT\n" - " BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT\n\n" - "Margin Validation Rules:\n" - " Margins define distance from parent edge when aligned.\n\n" - " - CENTER: No margins allowed (raises ValueError if margin != 0)\n" - " - TOP_CENTER, BOTTOM_CENTER: Only vert_margin applies (horiz_margin raises ValueError)\n" - " - CENTER_LEFT, CENTER_RIGHT: Only horiz_margin applies (vert_margin raises ValueError)\n" - " - Corner alignments (TOP_LEFT, etc.): All margins valid\n\n" - "Properties:\n" - " align: Alignment value or None to disable\n" - " margin: General margin for all applicable edges\n" - " horiz_margin: Override for horizontal edge (0 = use general margin)\n" - " vert_margin: Override for vertical edge (0 = use general margin)\n\n" - "Example:\n" - " # Center a panel in the scene\n" - " panel = Frame(size=(200, 100), align=Alignment.CENTER)\n" - " scene.children.append(panel)\n\n" - " # Place button in bottom-right with 10px margin\n" - " button = Frame(size=(80, 30), align=Alignment.BOTTOM_RIGHT, margin=10)\n" - " panel.children.append(button)"; - PyObject* doc = PyUnicode_FromString(alignment_doc); - if (doc) { - PyObject_SetAttrString(alignment_class, "__doc__", doc); - Py_DECREF(doc); - } - - // Add to module - if (PyModule_AddObject(module, "Alignment", alignment_class) < 0) { - Py_DECREF(alignment_class); - alignment_enum_class = nullptr; - return NULL; - } - - return alignment_class; -} - -int PyAlignment::from_arg(PyObject* arg, AlignmentType* out_align, bool* was_none) { - if (was_none) *was_none = false; - - // Accept None -> NONE alignment (no alignment) - if (arg == Py_None) { - if (was_none) *was_none = true; - *out_align = AlignmentType::NONE; - return 1; - } - - // Accept Alignment enum member (check if it's an instance of our enum) - if (alignment_enum_class && PyObject_IsInstance(arg, alignment_enum_class)) { - // IntEnum members have a 'value' attribute - PyObject* value = PyObject_GetAttrString(arg, "value"); - if (!value) { - return 0; - } - long val = PyLong_AsLong(value); - Py_DECREF(value); - if (val == -1 && PyErr_Occurred()) { - return 0; - } - if (val >= 0 && val < NUM_ALIGNMENT_ENTRIES) { - *out_align = alignment_table[val].type; - return 1; - } - PyErr_Format(PyExc_ValueError, - "Invalid Alignment value: %ld. Must be 0-%d.", val, NUM_ALIGNMENT_ENTRIES - 1); - return 0; - } - - // Accept int (for direct enum value access) - if (PyLong_Check(arg)) { - long val = PyLong_AsLong(arg); - if (val == -1 && PyErr_Occurred()) { - return 0; - } - if (val >= 0 && val < NUM_ALIGNMENT_ENTRIES) { - *out_align = alignment_table[val].type; - return 1; - } - PyErr_Format(PyExc_ValueError, - "Invalid alignment value: %ld. Must be 0-%d or use mcrfpy.Alignment enum.", - val, NUM_ALIGNMENT_ENTRIES - 1); - return 0; - } - - // Accept string (for backwards compatibility) - if (PyUnicode_Check(arg)) { - const char* name = PyUnicode_AsUTF8(arg); - if (!name) { - return 0; - } - - // Check legacy camelCase names first - for (int i = 0; i < NUM_ALIGNMENT_ENTRIES; i++) { - if (strcmp(name, legacy_names[i]) == 0) { - *out_align = alignment_table[i].type; - return 1; - } - } - - // Also check enum-style names (TOP_LEFT, CENTER, etc.) - for (int i = 0; i < NUM_ALIGNMENT_ENTRIES; i++) { - if (strcmp(name, alignment_table[i].name) == 0) { - *out_align = alignment_table[i].type; - return 1; - } - } - - // Build error message with available options - PyErr_Format(PyExc_ValueError, - "Unknown alignment: '%s'. Use mcrfpy.Alignment enum (e.g., Alignment.CENTER) " - "or string names: 'topLeft', 'topCenter', 'topRight', 'centerLeft', 'center', " - "'centerRight', 'bottomLeft', 'bottomCenter', 'bottomRight'.", - name); - return 0; - } - - PyErr_SetString(PyExc_TypeError, - "Alignment must be mcrfpy.Alignment enum member, string, int, or None"); - return 0; -} diff --git a/src/PyAlignment.h b/src/PyAlignment.h deleted file mode 100644 index 9ee2e92..0000000 --- a/src/PyAlignment.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once -#include "Common.h" -#include "Python.h" - -// Alignment type enum - used internally in C++ -enum class AlignmentType { - NONE = -1, // No alignment (static positioning) - TOP_LEFT = 0, - TOP_CENTER = 1, - TOP_RIGHT = 2, - CENTER_LEFT = 3, - CENTER = 4, - CENTER_RIGHT = 5, - BOTTOM_LEFT = 6, - BOTTOM_CENTER = 7, - BOTTOM_RIGHT = 8 -}; - -// Module-level Alignment enum class (created at runtime using Python's IntEnum) -// Stored as a module attribute: mcrfpy.Alignment - -class PyAlignment { -public: - // Create the Alignment enum class and add to module - // Returns the enum class (new reference), or NULL on error - static PyObject* create_enum_class(PyObject* module); - - // Helper to extract alignment from Python arg - // Accepts Alignment enum, string, int, or None - // Returns 1 on success, 0 on error (with exception set) - // If arg is None, sets *out_align to NONE and sets *was_none to true - static int from_arg(PyObject* arg, AlignmentType* out_align, bool* was_none = nullptr); - - // Convert alignment enum value to string name - static const char* alignment_name(AlignmentType value); - - // Cached reference to the Alignment enum class for fast type checking - static PyObject* alignment_enum_class; - - // Number of alignment options (excluding NONE) - static const int NUM_ALIGNMENTS = 9; -}; diff --git a/src/PySceneObject.cpp b/src/PySceneObject.cpp index 8104c2a..e388384 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -500,30 +500,6 @@ PyGetSetDef PySceneClass::getsetters[] = { {NULL} }; -// Scene.realign() - recalculate alignment for all children -static PyObject* PySceneClass_realign(PySceneObject* self, PyObject* args) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine"); - return NULL; - } - - auto scene = game->getScene(self->name); - if (!scene || !scene->ui_elements) { - Py_RETURN_NONE; - } - - // Iterate through all UI elements and realign those with alignment set - for (auto& drawable : *scene->ui_elements) { - if (drawable && drawable->align_type != AlignmentType::NONE) { - drawable->applyAlignment(); - } - } - - Py_RETURN_NONE; -} - // Methods PyMethodDef PySceneClass::methods[] = { {"activate", (PyCFunction)activate, METH_VARARGS | METH_KEYWORDS, @@ -545,13 +521,6 @@ PyMethodDef PySceneClass::methods[] = { MCRF_RETURNS("None") MCRF_NOTE("Alternative to setting on_key property. Handler is called for both key press and release events.") )}, - {"realign", (PyCFunction)PySceneClass_realign, METH_NOARGS, - MCRF_METHOD(SceneClass, realign, - MCRF_SIG("()", "None"), - MCRF_DESC("Recalculate alignment for all children with alignment set."), - MCRF_NOTE("Call this after window resize or when game_resolution changes. " - "For responsive layouts, connect this to on_resize callback.") - )}, {NULL} }; diff --git a/src/UIArc.cpp b/src/UIArc.cpp index a92d7ef..9398fdf 100644 --- a/src/UIArc.cpp +++ b/src/UIArc.cpp @@ -1,7 +1,6 @@ #include "UIArc.h" #include "McRFPy_API.h" #include "PythonObjectCache.h" -#include "PyAlignment.h" #include #include @@ -207,12 +206,6 @@ void UIArc::resize(float w, float h) { vertices_dirty = true; } -void UIArc::onPositionChanged() { - // Sync center from position (for alignment system) - center = position; - vertices_dirty = true; -} - // Property setters bool UIArc::setProperty(const std::string& name, float value) { if (name == "radius") { @@ -450,7 +443,6 @@ PyGetSetDef UIArc::getsetters[] = { "Position as a Vector (same as center).", (void*)PyObjectsEnum::UIARC}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIARC), - UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIARC), {NULL} }; @@ -489,23 +481,17 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) { float opacity = 1.0f; int z_index = 0; const char* name = nullptr; - PyObject* align_obj = nullptr; // Alignment enum or None - float margin = 0.0f; - float horiz_margin = -1.0f; - float vert_margin = -1.0f; static const char* kwlist[] = { "center", "radius", "start_angle", "end_angle", "color", "thickness", "on_click", "visible", "opacity", "z_index", "name", - "align", "margin", "horiz_margin", "vert_margin", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifizOfff", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifiz", const_cast(kwlist), ¢er_obj, &radius, &start_angle, &end_angle, &color_obj, &thickness, - &click_handler, &visible, &opacity, &z_index, &name, - &align_obj, &margin, &horiz_margin, &vert_margin)) { + &click_handler, &visible, &opacity, &z_index, &name)) { return -1; } @@ -560,9 +546,6 @@ int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) { self->data->name = name; } - // Process alignment arguments - UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin); - // Register in Python object cache if (self->data->serial_number == 0) { self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); diff --git a/src/UIArc.h b/src/UIArc.h index 888b8ab..d8bef3a 100644 --- a/src/UIArc.h +++ b/src/UIArc.h @@ -76,7 +76,6 @@ public: sf::FloatRect get_bounds() const override; void move(float dx, float dy) override; void resize(float w, float h) override; - void onPositionChanged() override; // Property system for animations bool setProperty(const std::string& name, float value) override; @@ -141,11 +140,7 @@ namespace mcrfpydef { " visible (bool): Visibility state. Default: True\n" " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" " z_index (int): Rendering order. Default: 0\n" - " name (str): Element name for finding. Default: None\n" - " align (Alignment): Alignment relative to parent. Default: None\n" - " margin (float): Margin from parent edge when aligned. Default: 0\n" - " horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n" - " vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n" + " name (str): Element name for finding. Default: None\n\n" "Attributes:\n" " center (Vector): Center position\n" " radius (float): Arc radius\n" @@ -157,10 +152,6 @@ namespace mcrfpydef { " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" " name (str): Element name\n" - " align (Alignment): Alignment relative to parent (or None)\n" - " margin (float): General margin for alignment\n" - " horiz_margin (float): Horizontal margin override\n" - " vert_margin (float): Vertical margin override\n" ), .tp_methods = UIArc_methods, .tp_getset = UIArc::getsetters, diff --git a/src/UIBase.h b/src/UIBase.h index f02fe1c..2a89e15 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -123,38 +123,11 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds) MCRF_RETURNS("Animation object for monitoring progress") \ MCRF_RAISES("ValueError", "If property name is not valid for this drawable type") \ MCRF_NOTE("This is a convenience method that creates an Animation, starts it, and adds it to the AnimationManager.") \ - )}, \ - {"realign", (PyCFunction)UIDrawable::py_realign, METH_NOARGS, \ - MCRF_METHOD(Drawable, realign, \ - MCRF_SIG("()", "None"), \ - MCRF_DESC("Reapply alignment relative to parent, useful for responsive layouts."), \ - MCRF_NOTE("Call this to recalculate position after parent changes size. " \ - "For elements with align=None, this has no effect.") \ )} // Legacy macro for backwards compatibility - same as UIDRAWABLE_METHODS #define UIDRAWABLE_METHODS_FULL UIDRAWABLE_METHODS -// Macro for handling alignment in constructors -// Usage: UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin) -// Returns -1 on error (suitable for use in tp_init functions) -#define UIDRAWABLE_PROCESS_ALIGNMENT(self_data, align_obj, margin, horiz_margin, vert_margin) \ - do { \ - if ((align_obj) && (align_obj) != Py_None) { \ - AlignmentType _align; \ - if (!PyAlignment::from_arg(align_obj, &_align)) { \ - return -1; \ - } \ - if (!UIDrawable::validateMargins(_align, margin, horiz_margin, vert_margin)) { \ - return -1; \ - } \ - (self_data)->align_type = _align; \ - (self_data)->align_margin = margin; \ - (self_data)->align_horiz_margin = horiz_margin; \ - (self_data)->align_vert_margin = vert_margin; \ - } \ - } while (0) - // Property getters/setters for visible and opacity template static PyObject* UIDrawable_get_visible(T* self, void* closure) @@ -257,29 +230,4 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure) "Performance note: Called frequently during movement - keep handlers fast." \ ), (void*)type_enum} -// Alignment system - automatic positioning relative to parent bounds -#define UIDRAWABLE_ALIGNMENT_GETSETTERS(type_enum) \ - {"align", (getter)UIDrawable::get_align, (setter)UIDrawable::set_align, \ - MCRF_PROPERTY(align, \ - "Alignment relative to parent bounds (Alignment enum or None). " \ - "When set, position is automatically calculated when parent is assigned or resized. " \ - "Set to None to disable alignment and use manual positioning." \ - ), (void*)type_enum}, \ - {"margin", (getter)UIDrawable::get_margin, (setter)UIDrawable::set_margin, \ - MCRF_PROPERTY(margin, \ - "General margin from edge when aligned (float). " \ - "Applied to both horizontal and vertical edges unless overridden. " \ - "Invalid for CENTER alignment (raises ValueError)." \ - ), (void*)type_enum}, \ - {"horiz_margin", (getter)UIDrawable::get_horiz_margin, (setter)UIDrawable::set_horiz_margin, \ - MCRF_PROPERTY(horiz_margin, \ - "Horizontal margin override (float, 0 = use general margin). " \ - "Invalid for vertically-centered alignments (TOP_CENTER, BOTTOM_CENTER, CENTER)." \ - ), (void*)type_enum}, \ - {"vert_margin", (getter)UIDrawable::get_vert_margin, (setter)UIDrawable::set_vert_margin, \ - MCRF_PROPERTY(vert_margin, \ - "Vertical margin override (float, 0 = use general margin). " \ - "Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER)." \ - ), (void*)type_enum} - // UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 44d0e84..d6527bd 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -4,7 +4,6 @@ #include "PyVector.h" #include "PyFont.h" #include "PythonObjectCache.h" -#include "PyAlignment.h" // UIDrawable methods now in UIBase.h #include @@ -311,7 +310,6 @@ PyGetSetDef UICaption::getsetters[] = { {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION), - UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICAPTION), {NULL} }; @@ -352,27 +350,21 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) int z_index = 0; const char* name = nullptr; float x = 0.0f, y = 0.0f; - PyObject* align_obj = nullptr; // Alignment enum or None - float margin = 0.0f; - float horiz_margin = -1.0f; - float vert_margin = -1.0f; - + // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { "pos", "font", "text", // Positional args (as per spec) // Keyword-only args "fill_color", "outline_color", "outline", "font_size", "on_click", "visible", "opacity", "z_index", "name", "x", "y", - "align", "margin", "horiz_margin", "vert_margin", nullptr }; - + // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizffOfff", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast(kwlist), &pos_obj, &font, &text, // Positional &fill_color, &outline_color, &outline, &font_size, &click_handler, - &visible, &opacity, &z_index, &name, &x, &y, - &align_obj, &margin, &horiz_margin, &vert_margin)) { + &visible, &opacity, &z_index, &name, &x, &y)) { return -1; } @@ -471,10 +463,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) if (name) { self->data->name = std::string(name); } - - // Process alignment arguments - UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin); - + // Handle click handler if (click_handler && click_handler != Py_None) { if (!PyCallable_Check(click_handler)) { @@ -483,7 +472,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) } self->data->click_register(click_handler); } - + // Initialize weak reference list self->weakreflist = NULL; diff --git a/src/UICaption.h b/src/UICaption.h index da4fe5c..09aa16c 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -91,11 +91,7 @@ namespace mcrfpydef { " z_index (int): Rendering order. Default: 0\n" " name (str): Element name for finding. Default: None\n" " x (float): X position override. Default: 0\n" - " y (float): Y position override. Default: 0\n" - " align (Alignment): Alignment relative to parent. Default: None\n" - " margin (float): Margin from parent edge when aligned. Default: 0\n" - " horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n" - " vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n" + " y (float): Y position override. Default: 0\n\n" "Attributes:\n" " text (str): The displayed text content\n" " x, y (float): Position in pixels\n" @@ -109,11 +105,7 @@ namespace mcrfpydef { " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" " name (str): Element name\n" - " w, h (float): Read-only computed size based on text and font\n" - " align (Alignment): Alignment relative to parent (or None)\n" - " margin (float): General margin for alignment\n" - " horiz_margin (float): Horizontal margin override\n" - " vert_margin (float): Vertical margin override"), + " w, h (float): Read-only computed size based on text and font"), .tp_methods = UICaption_methods, //.tp_members = PyUIFrame_members, .tp_getset = UICaption::getsetters, diff --git a/src/UICircle.cpp b/src/UICircle.cpp index 1e3451b..0f8a22e 100644 --- a/src/UICircle.cpp +++ b/src/UICircle.cpp @@ -4,7 +4,6 @@ #include "PyVector.h" #include "PyColor.h" #include "PythonObjectCache.h" -#include "PyAlignment.h" #include UICircle::UICircle() @@ -396,7 +395,6 @@ PyGetSetDef UICircle::getsetters[] = { "Position as a Vector (same as center).", (void*)PyObjectsEnum::UICIRCLE}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICIRCLE), - UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICIRCLE), {NULL} }; @@ -420,8 +418,7 @@ PyObject* UICircle::repr(PyUICircleObject* self) { int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) { static const char* kwlist[] = { "radius", "center", "fill_color", "outline_color", "outline", - "on_click", "visible", "opacity", "z_index", "name", - "align", "margin", "horiz_margin", "vert_margin", NULL + "on_click", "visible", "opacity", "z_index", "name", NULL }; float radius = 10.0f; @@ -436,15 +433,10 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) { float opacity_val = 1.0f; int z_index = 0; const char* name = NULL; - PyObject* align_obj = NULL; // Alignment enum or None - float margin = 0.0f; - float horiz_margin = -1.0f; - float vert_margin = -1.0f; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfisOfff", (char**)kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfis", (char**)kwlist, &radius, ¢er_obj, &fill_color_obj, &outline_color_obj, &outline, - &click_obj, &visible, &opacity_val, &z_index, &name, - &align_obj, &margin, &horiz_margin, &vert_margin)) { + &click_obj, &visible, &opacity_val, &z_index, &name)) { return -1; } @@ -520,9 +512,6 @@ int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) { self->data->name = name; } - // Process alignment arguments - UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin); - // Register in Python object cache if (self->data->serial_number == 0) { self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); diff --git a/src/UICircle.h b/src/UICircle.h index 5928808..5210b2e 100644 --- a/src/UICircle.h +++ b/src/UICircle.h @@ -129,11 +129,7 @@ namespace mcrfpydef { " visible (bool): Visibility state. Default: True\n" " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" " z_index (int): Rendering order. Default: 0\n" - " name (str): Element name for finding. Default: None\n" - " align (Alignment): Alignment relative to parent. Default: None\n" - " margin (float): Margin from parent edge when aligned. Default: 0\n" - " horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n" - " vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n" + " name (str): Element name for finding. Default: None\n\n" "Attributes:\n" " radius (float): Circle radius\n" " center (Vector): Center position\n" @@ -144,10 +140,6 @@ namespace mcrfpydef { " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" " name (str): Element name\n" - " align (Alignment): Alignment relative to parent (or None)\n" - " margin (float): General margin for alignment\n" - " horiz_margin (float): Horizontal margin override\n" - " vert_margin (float): Vertical margin override\n" ), .tp_methods = UICircle_methods, .tp_getset = UICircle::getsetters, diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index 1accce5..a51d57f 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -14,30 +14,6 @@ #include "PyEasing.h" #include "PySceneObject.h" // #183: For scene parent lookup -// Helper function to extract UIDrawable* from any Python UI object -// Returns nullptr and sets Python error on failure -static UIDrawable* extractDrawable(PyObject* self, PyObjectsEnum objtype) { - switch (objtype) { - case PyObjectsEnum::UIFRAME: - return ((PyUIFrameObject*)self)->data.get(); - case PyObjectsEnum::UICAPTION: - return ((PyUICaptionObject*)self)->data.get(); - case PyObjectsEnum::UISPRITE: - return ((PyUISpriteObject*)self)->data.get(); - case PyObjectsEnum::UIGRID: - return ((PyUIGridObject*)self)->data.get(); - case PyObjectsEnum::UILINE: - return ((PyUILineObject*)self)->data.get(); - case PyObjectsEnum::UICIRCLE: - return ((PyUICircleObject*)self)->data.get(); - case PyObjectsEnum::UIARC: - return ((PyUIArcObject*)self)->data.get(); - default: - PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); - return nullptr; - } -} - UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; } UIDrawable::UIDrawable(const UIDrawable& other) @@ -330,16 +306,68 @@ void UIDrawable::on_move_unregister() PyObject* UIDrawable::get_int(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } return PyLong_FromLong(drawable->z_index); } int UIDrawable::set_int(PyObject* self, PyObject* value, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return -1; + } if (!PyLong_Check(value)) { PyErr_SetString(PyExc_TypeError, "z_index must be an integer"); @@ -378,16 +406,68 @@ void UIDrawable::notifyZIndexChanged() { PyObject* UIDrawable::get_name(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } return PyUnicode_FromString(drawable->name.c_str()); } int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return -1; + } if (value == NULL || value == Py_None) { drawable->name = ""; @@ -444,8 +524,34 @@ void UIDrawable::updateRenderTexture() { PyObject* UIDrawable::get_float_member(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure) >> 8); int member = reinterpret_cast(closure) & 0xFF; - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } switch (member) { case 0: // x @@ -465,8 +571,34 @@ PyObject* UIDrawable::get_float_member(PyObject* self, void* closure) { int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure) >> 8); int member = reinterpret_cast(closure) & 0xFF; - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return -1; + } float val = 0.0f; if (PyFloat_Check(value)) { @@ -508,8 +640,34 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure) PyObject* UIDrawable::get_pos(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } // Create a Python Vector object from position PyObject* module = PyImport_ImportModule("mcrfpy"); @@ -529,8 +687,34 @@ PyObject* UIDrawable::get_pos(PyObject* self, void* closure) { int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return -1; + } // Accept tuple or Vector float x, y; @@ -582,21 +766,11 @@ int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) { void UIDrawable::setParent(std::shared_ptr new_parent) { parent = new_parent; parent_scene.clear(); // #183: Clear scene parent when setting drawable parent - - // Apply alignment when parent is set (if alignment is configured) - if (new_parent && align_type != AlignmentType::NONE) { - applyAlignment(); - } } void UIDrawable::setParentScene(const std::string& scene_name) { parent.reset(); // #183: Clear drawable parent when setting scene parent parent_scene = scene_name; - - // Apply alignment when scene parent is set (if alignment is configured) - if (!scene_name.empty() && align_type != AlignmentType::NONE) { - applyAlignment(); - } } std::shared_ptr UIDrawable::getParent() const { @@ -719,8 +893,34 @@ void UIDrawable::markDirty() { // Python API - get parent drawable PyObject* UIDrawable::get_parent(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } // #183: Check for scene parent first if (!drawable->parent_scene.empty()) { @@ -926,8 +1126,34 @@ int UIDrawable::set_parent(PyObject* self, PyObject* value, void* closure) { // Python API - get global position (read-only) PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } sf::Vector2f global_pos = drawable->get_global_position(); @@ -950,8 +1176,34 @@ PyObject* UIDrawable::get_global_pos(PyObject* self, void* closure) { // #138, #188 - Python API for bounds property - returns (pos, size) as pair of Vectors PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } sf::FloatRect bounds = drawable->get_bounds(); @@ -985,8 +1237,34 @@ PyObject* UIDrawable::get_bounds_py(PyObject* self, void* closure) { // #138, #188 - Python API for global_bounds property - returns (pos, size) as pair of Vectors PyObject* UIDrawable::get_global_bounds_py(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } sf::FloatRect bounds = drawable->get_global_bounds(); @@ -1186,8 +1464,34 @@ int UIDrawable::set_on_exit(PyObject* self, PyObject* value, void* closure) { // #140 - Python API for hovered property (read-only) PyObject* UIDrawable::get_hovered(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } return PyBool_FromLong(drawable->hovered); } @@ -1500,358 +1804,3 @@ void UIDrawable::refreshCallbackCache(PyObject* pyObj) { Py_XDECREF(attr); PyErr_Clear(); } - -// ============================================================================ -// Alignment System Implementation -// ============================================================================ - -void UIDrawable::applyAlignment() { - if (align_type == AlignmentType::NONE) return; - - float pw, ph; // Parent width/height - auto p = parent.lock(); - - if (p) { - // Parent is another UIDrawable (Frame, Grid, etc.) - sf::FloatRect parent_bounds = p->get_bounds(); - pw = parent_bounds.width; - ph = parent_bounds.height; - } else if (!parent_scene.empty()) { - // Parent is a Scene - use window's game resolution - GameEngine* game = McRFPy_API::game; - if (!game) return; - sf::Vector2u resolution = game->getGameResolution(); - pw = static_cast(resolution.x); - ph = static_cast(resolution.y); - } else { - return; // No parent at all = can't align - } - - sf::FloatRect self_bounds = get_bounds(); - float cw = self_bounds.width, ch = self_bounds.height; - - // Use specific margins if set (>= 0), otherwise inherit from general margin - // -1.0 means "inherit", any value >= 0 is an explicit override - float mx = (align_horiz_margin >= 0.0f) ? align_horiz_margin : align_margin; - float my = (align_vert_margin >= 0.0f) ? align_vert_margin : align_margin; - - float x = 0, y = 0; - switch (align_type) { - case AlignmentType::TOP_LEFT: - x = mx; - y = my; - break; - case AlignmentType::TOP_CENTER: - x = (pw - cw) / 2.0f; - y = my; - break; - case AlignmentType::TOP_RIGHT: - x = pw - cw - mx; - y = my; - break; - case AlignmentType::CENTER_LEFT: - x = mx; - y = (ph - ch) / 2.0f; - break; - case AlignmentType::CENTER: - x = (pw - cw) / 2.0f; - y = (ph - ch) / 2.0f; - break; - case AlignmentType::CENTER_RIGHT: - x = pw - cw - mx; - y = (ph - ch) / 2.0f; - break; - case AlignmentType::BOTTOM_LEFT: - x = mx; - y = ph - ch - my; - break; - case AlignmentType::BOTTOM_CENTER: - x = (pw - cw) / 2.0f; - y = ph - ch - my; - break; - case AlignmentType::BOTTOM_RIGHT: - x = pw - cw - mx; - y = ph - ch - my; - break; - default: - return; - } - - // For most drawables, position IS the bounding box top-left corner - // But for Circle and Arc, position is the center, so we need to adjust - float offset_x = 0.0f; - float offset_y = 0.0f; - - // Check if this is a Circle or Arc (where position = center) - auto dtype = derived_type(); - if (dtype == PyObjectsEnum::UICIRCLE || dtype == PyObjectsEnum::UIARC) { - // For these, position is the center, bounds.topLeft is position - radius - // So offset = position - bounds.topLeft = (radius, radius) - offset_x = position.x - self_bounds.left; - offset_y = position.y - self_bounds.top; - } - - position = sf::Vector2f(x + offset_x, y + offset_y); - onPositionChanged(); - markCompositeDirty(); -} - -void UIDrawable::setAlignment(AlignmentType align) { - align_type = align; - if (align != AlignmentType::NONE) { - applyAlignment(); - } -} - -void UIDrawable::realign() { - // Reapply alignment - useful for responsive layouts - if (align_type != AlignmentType::NONE) { - applyAlignment(); - } -} - -PyObject* UIDrawable::py_realign(PyObject* self, PyObject* args) { - PyObjectsEnum objtype = PyObjectsEnum::UIFRAME; // Default, will be set by type check - - // Determine the type from the Python object - PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"); - PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"); - PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"); - PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"); - PyObject* line_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"); - PyObject* circle_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"); - PyObject* arc_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"); - - if (PyObject_IsInstance(self, frame_type)) objtype = PyObjectsEnum::UIFRAME; - else if (PyObject_IsInstance(self, caption_type)) objtype = PyObjectsEnum::UICAPTION; - else if (PyObject_IsInstance(self, sprite_type)) objtype = PyObjectsEnum::UISPRITE; - else if (PyObject_IsInstance(self, grid_type)) objtype = PyObjectsEnum::UIGRID; - else if (PyObject_IsInstance(self, line_type)) objtype = PyObjectsEnum::UILINE; - else if (PyObject_IsInstance(self, circle_type)) objtype = PyObjectsEnum::UICIRCLE; - else if (PyObject_IsInstance(self, arc_type)) objtype = PyObjectsEnum::UIARC; - - Py_XDECREF(frame_type); - Py_XDECREF(caption_type); - Py_XDECREF(sprite_type); - Py_XDECREF(grid_type); - Py_XDECREF(line_type); - Py_XDECREF(circle_type); - Py_XDECREF(arc_type); - - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; - - drawable->realign(); - Py_RETURN_NONE; -} - -bool UIDrawable::validateMargins(AlignmentType align, float margin, float horiz_margin, float vert_margin, bool set_error) { - // Calculate effective margins (-1 means inherit from general margin) - float eff_horiz = (horiz_margin >= 0.0f) ? horiz_margin : margin; - float eff_vert = (vert_margin >= 0.0f) ? vert_margin : margin; - - // CENTER alignment doesn't support any margins - if (align == AlignmentType::CENTER) { - if (margin != 0.0f || eff_horiz != 0.0f || eff_vert != 0.0f) { - if (set_error) { - PyErr_SetString(PyExc_ValueError, - "CENTER alignment does not support margins"); - } - return false; - } - } - - // Horizontally centered alignments don't support horiz_margin override - // (margin is applied vertically only) - if (align == AlignmentType::TOP_CENTER || align == AlignmentType::BOTTOM_CENTER) { - // If horiz_margin is explicitly set (not -1), it must be 0 or error - if (horiz_margin >= 0.0f && horiz_margin != 0.0f) { - if (set_error) { - PyErr_SetString(PyExc_ValueError, - "TOP_CENTER and BOTTOM_CENTER alignments do not support horiz_margin"); - } - return false; - } - } - - // Vertically centered alignments don't support vert_margin override - // (margin is applied horizontally only) - if (align == AlignmentType::CENTER_LEFT || align == AlignmentType::CENTER_RIGHT) { - // If vert_margin is explicitly set (not -1), it must be 0 or error - if (vert_margin >= 0.0f && vert_margin != 0.0f) { - if (set_error) { - PyErr_SetString(PyExc_ValueError, - "CENTER_LEFT and CENTER_RIGHT alignments do not support vert_margin"); - } - return false; - } - } - - return true; -} - -// Python API: get align property -PyObject* UIDrawable::get_align(PyObject* self, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; - - if (drawable->align_type == AlignmentType::NONE) { - Py_RETURN_NONE; - } - - // Return Alignment enum member - if (!PyAlignment::alignment_enum_class) { - PyErr_SetString(PyExc_RuntimeError, "Alignment enum not initialized"); - return NULL; - } - - PyObject* value = PyLong_FromLong(static_cast(drawable->align_type)); - if (!value) return NULL; - - PyObject* result = PyObject_CallFunctionObjArgs(PyAlignment::alignment_enum_class, value, NULL); - Py_DECREF(value); - return result; -} - -// Python API: set align property -int UIDrawable::set_align(PyObject* self, PyObject* value, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; - - if (value == Py_None) { - drawable->align_type = AlignmentType::NONE; - return 0; - } - - AlignmentType align; - if (!PyAlignment::from_arg(value, &align)) { - return -1; - } - - // Validate margins for new alignment - if (!validateMargins(align, drawable->align_margin, drawable->align_horiz_margin, drawable->align_vert_margin)) { - return -1; - } - - drawable->setAlignment(align); - return 0; -} - -// Python API: get margin property -PyObject* UIDrawable::get_margin(PyObject* self, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; - - return PyFloat_FromDouble(drawable->align_margin); -} - -// Python API: set margin property -int UIDrawable::set_margin(PyObject* self, PyObject* value, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; - - float margin = 0.0f; - if (PyFloat_Check(value)) { - margin = static_cast(PyFloat_AsDouble(value)); - } else if (PyLong_Check(value)) { - margin = static_cast(PyLong_AsLong(value)); - } else { - PyErr_SetString(PyExc_TypeError, "margin must be a number"); - return -1; - } - - // Validate margins for current alignment - if (drawable->align_type != AlignmentType::NONE) { - if (!validateMargins(drawable->align_type, margin, drawable->align_horiz_margin, drawable->align_vert_margin)) { - return -1; - } - } - - drawable->align_margin = margin; - if (drawable->align_type != AlignmentType::NONE) { - drawable->applyAlignment(); - } - return 0; -} - -// Python API: get horiz_margin property -PyObject* UIDrawable::get_horiz_margin(PyObject* self, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; - - return PyFloat_FromDouble(drawable->align_horiz_margin); -} - -// Python API: set horiz_margin property -int UIDrawable::set_horiz_margin(PyObject* self, PyObject* value, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; - - float horiz_margin = 0.0f; - if (PyFloat_Check(value)) { - horiz_margin = static_cast(PyFloat_AsDouble(value)); - } else if (PyLong_Check(value)) { - horiz_margin = static_cast(PyLong_AsLong(value)); - } else { - PyErr_SetString(PyExc_TypeError, "horiz_margin must be a number"); - return -1; - } - - // Validate margins for current alignment - if (drawable->align_type != AlignmentType::NONE) { - if (!validateMargins(drawable->align_type, drawable->align_margin, horiz_margin, drawable->align_vert_margin)) { - return -1; - } - } - - drawable->align_horiz_margin = horiz_margin; - if (drawable->align_type != AlignmentType::NONE) { - drawable->applyAlignment(); - } - return 0; -} - -// Python API: get vert_margin property -PyObject* UIDrawable::get_vert_margin(PyObject* self, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; - - return PyFloat_FromDouble(drawable->align_vert_margin); -} - -// Python API: set vert_margin property -int UIDrawable::set_vert_margin(PyObject* self, PyObject* value, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; - - float vert_margin = 0.0f; - if (PyFloat_Check(value)) { - vert_margin = static_cast(PyFloat_AsDouble(value)); - } else if (PyLong_Check(value)) { - vert_margin = static_cast(PyLong_AsLong(value)); - } else { - PyErr_SetString(PyExc_TypeError, "vert_margin must be a number"); - return -1; - } - - // Validate margins for current alignment - if (drawable->align_type != AlignmentType::NONE) { - if (!validateMargins(drawable->align_type, drawable->align_margin, drawable->align_horiz_margin, vert_margin)) { - return -1; - } - } - - drawable->align_vert_margin = vert_margin; - if (drawable->align_type != AlignmentType::NONE) { - drawable->applyAlignment(); - } - return 0; -} diff --git a/src/UIDrawable.h b/src/UIDrawable.h index f627697..66bb691 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -12,7 +12,6 @@ #include "PyColor.h" #include "PyVector.h" #include "PyFont.h" -#include "PyAlignment.h" #include "Resources.h" #include "UIBase.h" @@ -134,39 +133,7 @@ public: // Python API for hit testing (#138) static PyObject* get_bounds_py(PyObject* self, void* closure); static PyObject* get_global_bounds_py(PyObject* self, void* closure); - - // Alignment system - position children relative to parent bounds - AlignmentType align_type = AlignmentType::NONE; - float align_margin = 0.0f; // General margin for all edges - float align_horiz_margin = -1.0f; // Horizontal margin override (-1 = use align_margin) - float align_vert_margin = -1.0f; // Vertical margin override (-1 = use align_margin) - - // Apply alignment: recalculate position from parent bounds - void applyAlignment(); - - // User-callable realignment: reapply alignment to self (useful for responsive layouts) - void realign(); - - // Python API: realign method - static PyObject* py_realign(PyObject* self, PyObject* args); - - // Setters that trigger realignment - void setAlignment(AlignmentType align); - AlignmentType getAlignment() const { return align_type; } - - // Python API for alignment properties - static PyObject* get_align(PyObject* self, void* closure); - static int set_align(PyObject* self, PyObject* value, void* closure); - static PyObject* get_margin(PyObject* self, void* closure); - static int set_margin(PyObject* self, PyObject* value, void* closure); - static PyObject* get_horiz_margin(PyObject* self, void* closure); - static int set_horiz_margin(PyObject* self, PyObject* value, void* closure); - static PyObject* get_vert_margin(PyObject* self, void* closure); - static int set_vert_margin(PyObject* self, PyObject* value, void* closure); - - // Validate margin settings (raises ValueError for invalid combinations) - static bool validateMargins(AlignmentType align, float margin, float horiz_margin, float vert_margin, bool set_error = true); - + // New properties for Phase 1 bool visible = true; // #87 - visibility flag float opacity = 1.0f; // #88 - opacity (0.0 = transparent, 1.0 = opaque) diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 4189ad0..34aae33 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -7,7 +7,6 @@ #include "UIGrid.h" #include "McRFPy_API.h" #include "PythonObjectCache.h" -#include "PyAlignment.h" // UIDrawable methods now in UIBase.h UIDrawable* UIFrame::click_at(sf::Vector2f point) @@ -85,15 +84,6 @@ void UIFrame::move(float dx, float dy) void UIFrame::resize(float w, float h) { box.setSize(sf::Vector2f(w, h)); - - // Notify aligned children to recalculate their positions - if (children) { - for (auto& child : *children) { - if (child->getAlignment() != AlignmentType::NONE) { - child->applyAlignment(); - } - } - } } void UIFrame::onPositionChanged() @@ -468,7 +458,6 @@ PyGetSetDef UIFrame::getsetters[] = { {"cache_subtree", (getter)UIFrame::get_cache_subtree, (setter)UIFrame::set_cache_subtree, "#144: Cache subtree rendering to texture for performance", NULL}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME), - UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIFRAME), {NULL} }; @@ -515,10 +504,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f; int clip_children = 0; int cache_subtree = 0; // #144: texture caching - PyObject* align_obj = nullptr; // Alignment enum or None - float margin = 0.0f; - float horiz_margin = -1.0f; - float vert_margin = -1.0f; // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { @@ -526,16 +511,14 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) // Keyword-only args "fill_color", "outline_color", "outline", "children", "on_click", "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children", "cache_subtree", - "align", "margin", "horiz_margin", "vert_margin", nullptr }; - + // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffiiOfff", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffii", const_cast(kwlist), &pos_obj, &size_obj, // Positional &fill_color, &outline_color, &outline, &children_arg, &click_handler, - &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree, - &align_obj, &margin, &horiz_margin, &vert_margin)) { + &visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children, &cache_subtree)) { return -1; } @@ -634,9 +617,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) if (name) { self->data->name = std::string(name); } - - // Process alignment arguments - UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin); // Handle click handler if (click_handler && click_handler != Py_None) { @@ -693,7 +673,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) Py_DECREF(grid_type); if (drawable) { - drawable->setParent(self->data); // Set parent before adding (enables alignment) self->data->children->push_back(drawable); self->data->children_need_sort = true; } diff --git a/src/UIFrame.h b/src/UIFrame.h index 772a22a..fd85939 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -115,11 +115,7 @@ namespace mcrfpydef { " w (float): Width override. Default: 0\n" " h (float): Height override. Default: 0\n" " clip_children (bool): Whether to clip children to frame bounds. Default: False\n" - " cache_subtree (bool): Cache rendering to texture for performance. Default: False\n" - " align (Alignment): Alignment relative to parent. Default: None (manual positioning)\n" - " margin (float): Margin from parent edge when aligned. Default: 0\n" - " horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n" - " vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n" + " cache_subtree (bool): Cache rendering to texture for performance. Default: False\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" " w, h (float): Size in pixels\n" @@ -133,11 +129,7 @@ namespace mcrfpydef { " z_index (int): Rendering order\n" " name (str): Element name\n" " clip_children (bool): Whether to clip children to frame bounds\n" - " cache_subtree (bool): Cache subtree rendering to texture\n" - " align (Alignment): Alignment relative to parent (or None)\n" - " margin (float): General margin for alignment\n" - " horiz_margin (float): Horizontal margin override\n" - " vert_margin (float): Vertical margin override"), + " cache_subtree (bool): Cache subtree rendering to texture"), .tp_methods = UIFrame_methods, //.tp_members = PyUIFrame_members, .tp_getset = UIFrame::getsetters, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 93e229d..2f4ae50 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -3,7 +3,6 @@ #include "GameEngine.h" #include "McRFPy_API.h" #include "PythonObjectCache.h" -#include "PyAlignment.h" #include "PyTypeCache.h" // Thread-safe cached Python types #include "UIEntity.h" #include "Profiler.h" @@ -497,15 +496,6 @@ void UIGrid::resize(float w, float h) renderTexture.create(static_cast(w), static_cast(h)); output.setTexture(renderTexture.getTexture()); } - - // Notify aligned children to recalculate their positions - if (children) { - for (auto& child : *children) { - if (child->getAlignment() != AlignmentType::NONE) { - child->applyAlignment(); - } - } - } } void UIGrid::onPositionChanged() @@ -684,10 +674,6 @@ 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_w = 2, grid_h = 2; // Default to 2x2 grid - PyObject* align_obj = nullptr; // Alignment enum or None - float margin = 0.0f; - float horiz_margin = -1.0f; - float vert_margin = -1.0f; // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { @@ -696,17 +682,15 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { "fill_color", "on_click", "center_x", "center_y", "zoom", "visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_w", "grid_h", "layers", // #150 - layers dict parameter - "align", "margin", "horiz_margin", "vert_margin", nullptr }; // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffiiOOfff", 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_w, &grid_h, - &layers_obj, - &align_obj, &margin, &horiz_margin, &vert_margin)) { + &layers_obj)) { return -1; } @@ -829,10 +813,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { if (name) { self->data->name = std::string(name); } - - // Process alignment arguments - UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin); - + // Handle fill_color if (fill_color && fill_color != Py_None) { PyColorObject* color_obj = PyColor::from_arg(fill_color); @@ -2207,7 +2188,6 @@ PyGetSetDef UIGrid::getsetters[] = { {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID), - UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIGRID), // #142 - Grid cell mouse events {"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter, "Callback when mouse enters a grid cell. Called with (cell_pos: Vector).", NULL}, diff --git a/src/UIGrid.h b/src/UIGrid.h index 9cc35ec..aa1336b 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -258,11 +258,7 @@ namespace mcrfpydef { " w (float): Width override. Default: auto-calculated\n" " h (float): Height override. Default: auto-calculated\n" " grid_w (int): Grid width override. Default: 2\n" - " grid_h (int): Grid height override. Default: 2\n" - " align (Alignment): Alignment relative to parent. Default: None\n" - " margin (float): Margin from parent edge when aligned. Default: 0\n" - " horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n" - " vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n" + " grid_h (int): Grid height override. Default: 2\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" " w, h (float): Size in pixels\n" @@ -281,11 +277,7 @@ namespace mcrfpydef { " visible (bool): Visibility state\n" " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" - " name (str): Element name\n" - " align (Alignment): Alignment relative to parent (or None)\n" - " margin (float): General margin for alignment\n" - " horiz_margin (float): Horizontal margin override\n" - " vert_margin (float): Vertical margin override"), + " name (str): Element name"), .tp_methods = UIGrid_all_methods, //.tp_members = UIGrid::members, .tp_getset = UIGrid::getsetters, diff --git a/src/UIGridPathfinding.cpp b/src/UIGridPathfinding.cpp index 2644b7f..d232262 100644 --- a/src/UIGridPathfinding.cpp +++ b/src/UIGridPathfinding.cpp @@ -3,8 +3,6 @@ #include "UIEntity.h" #include "PyVector.h" #include "McRFPy_API.h" -#include "PyHeightMap.h" -#include "PyPositionHelper.h" //============================================================================= // DijkstraMap Implementation @@ -14,8 +12,6 @@ DijkstraMap::DijkstraMap(TCODMap* map, int root_x, int root_y, float diag_cost) : tcod_map(map) , root(root_x, root_y) , diagonal_cost(diag_cost) - , map_width(map ? map->getWidth() : 0) - , map_height(map ? map->getHeight() : 0) { tcod_dijkstra = new TCODDijkstra(tcod_map, diagonal_cost); tcod_dijkstra->compute(root_x, root_y); // Compute immediately at creation @@ -33,14 +29,6 @@ float DijkstraMap::getDistance(int x, int y) const { return tcod_dijkstra->getDistance(x, y); } -int DijkstraMap::getWidth() const { - return map_width; -} - -int DijkstraMap::getHeight() const { - return map_height; -} - std::vector DijkstraMap::getPathFrom(int x, int y) const { std::vector path; if (!tcod_dijkstra) return path; @@ -136,6 +124,8 @@ bool UIGridPathfinding::ExtractPosition(PyObject* obj, int* x, int* y, *x = PyLong_AsLong(x_long); *y = PyLong_AsLong(y_long); ok = !PyErr_Occurred(); + Py_DECREF(x_long); + Py_DECREF(y_long); } Py_XDECREF(x_long); Py_XDECREF(y_long); @@ -393,83 +383,6 @@ PyObject* UIGridPathfinding::DijkstraMap_get_root(PyDijkstraMapObject* self, voi return PyVector(sf::Vector2f(static_cast(root.x), static_cast(root.y))).pyObject(); } -PyObject* UIGridPathfinding::DijkstraMap_to_heightmap(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = {"size", "unreachable", nullptr}; - PyObject* size_obj = nullptr; - float unreachable = -1.0f; // Value for cells that can't reach root (distinct from 0 = root) - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Of", const_cast(kwlist), - &size_obj, &unreachable)) { - return nullptr; - } - - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid"); - return nullptr; - } - - // Determine output size (default to dijkstra dimensions) - int width = self->data->getWidth(); - int height = self->data->getHeight(); - - if (width <= 0 || height <= 0) { - PyErr_SetString(PyExc_RuntimeError, "DijkstraMap has invalid dimensions"); - return nullptr; - } - - if (size_obj && size_obj != Py_None) { - if (!PyPosition_FromObjectInt(size_obj, &width, &height)) { - PyErr_SetString(PyExc_TypeError, "size must be (width, height) tuple, list, or Vector"); - return nullptr; - } - if (width <= 0 || height <= 0) { - PyErr_SetString(PyExc_ValueError, "size values must be positive"); - return nullptr; - } - } - - // Create HeightMap via Python API (same pattern as BSP.to_heightmap) - PyObject* hmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap"); - if (!hmap_type) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found"); - return nullptr; - } - - PyObject* size_tuple = Py_BuildValue("(ii)", width, height); - PyObject* hmap_args = PyTuple_Pack(1, size_tuple); - Py_DECREF(size_tuple); - - PyHeightMapObject* hmap = (PyHeightMapObject*)PyObject_Call(hmap_type, hmap_args, nullptr); - Py_DECREF(hmap_args); - Py_DECREF(hmap_type); - - if (!hmap) { - return nullptr; - } - - // Get the dijkstra dimensions for bounds checking - int dijkstra_w = self->data->getWidth(); - int dijkstra_h = self->data->getHeight(); - - // Fill heightmap with distance values - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float dist; - if (x < dijkstra_w && y < dijkstra_h) { - dist = self->data->getDistance(x, y); - if (dist < 0) { - dist = unreachable; // Unreachable cell - } - } else { - dist = unreachable; // Outside dijkstra bounds - } - TCOD_heightmap_set_value(hmap->heightmap, x, y, dist); - } - } - - return (PyObject*)hmap; -} - //============================================================================= // Grid Factory Methods //============================================================================= @@ -726,17 +639,6 @@ PyMethodDef PyDijkstraMap_methods[] = { "Returns:\n" " Next position as Vector, or None if at root or unreachable."}, - {"to_heightmap", (PyCFunction)UIGridPathfinding::DijkstraMap_to_heightmap, METH_VARARGS | METH_KEYWORDS, - "to_heightmap(size=None, unreachable=-1.0) -> HeightMap\n\n" - "Convert distance field to a HeightMap.\n\n" - "Each cell's height equals its pathfinding distance from the root.\n" - "Useful for visualization, procedural terrain, or influence mapping.\n\n" - "Args:\n" - " size: Optional (width, height) tuple. Defaults to dijkstra dimensions.\n" - " unreachable: Value for cells that cannot reach root (default -1.0).\n\n" - "Returns:\n" - " HeightMap with distance values as heights."}, - {NULL} }; diff --git a/src/UIGridPathfinding.h b/src/UIGridPathfinding.h index 2ca88b4..07294d0 100644 --- a/src/UIGridPathfinding.h +++ b/src/UIGridPathfinding.h @@ -44,16 +44,12 @@ public: // Accessors sf::Vector2i getRoot() const { return root; } float getDiagonalCost() const { return diagonal_cost; } - int getWidth() const; - int getHeight() const; private: TCODDijkstra* tcod_dijkstra; // Owned by this object TCODMap* tcod_map; // Borrowed from Grid sf::Vector2i root; float diagonal_cost; - int map_width; // Cached from TCODMap at construction - int map_height; }; struct PyDijkstraMapObject { @@ -110,7 +106,6 @@ namespace UIGridPathfinding { PyObject* DijkstraMap_distance(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds); PyObject* DijkstraMap_path_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds); PyObject* DijkstraMap_step_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds); - PyObject* DijkstraMap_to_heightmap(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds); // Properties PyObject* DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure); diff --git a/src/UILine.cpp b/src/UILine.cpp index fa58216..993655d 100644 --- a/src/UILine.cpp +++ b/src/UILine.cpp @@ -4,7 +4,6 @@ #include "PyVector.h" #include "PyColor.h" #include "PythonObjectCache.h" -#include "PyAlignment.h" #include UILine::UILine() @@ -466,7 +465,6 @@ PyGetSetDef UILine::getsetters[] = { (void*)PyObjectsEnum::UILINE}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UILINE), - UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UILINE), {NULL} }; @@ -499,22 +497,16 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) { float opacity = 1.0f; int z_index = 0; const char* name = nullptr; - PyObject* align_obj = nullptr; // Alignment enum or None - float margin = 0.0f; - float horiz_margin = -1.0f; - float vert_margin = -1.0f; static const char* kwlist[] = { "start", "end", "thickness", "color", "on_click", "visible", "opacity", "z_index", "name", - "align", "margin", "horiz_margin", "vert_margin", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifizOfff", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifiz", const_cast(kwlist), &start_obj, &end_obj, &thickness, &color_obj, - &click_handler, &visible, &opacity, &z_index, &name, - &align_obj, &margin, &horiz_margin, &vert_margin)) { + &click_handler, &visible, &opacity, &z_index, &name)) { return -1; } @@ -573,9 +565,6 @@ int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) { self->data->name = std::string(name); } - // Process alignment arguments - UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin); - // Handle click handler if (click_handler && click_handler != Py_None) { if (!PyCallable_Check(click_handler)) { diff --git a/src/UILine.h b/src/UILine.h index 2912f93..60a33c3 100644 --- a/src/UILine.h +++ b/src/UILine.h @@ -125,11 +125,7 @@ namespace mcrfpydef { " visible (bool): Visibility state. Default: True\n" " opacity (float): Opacity (0.0-1.0). Default: 1.0\n" " z_index (int): Rendering order. Default: 0\n" - " name (str): Element name for finding. Default: None\n" - " align (Alignment): Alignment relative to parent. Default: None\n" - " margin (float): Margin from parent edge when aligned. Default: 0\n" - " horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n" - " vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n" + " name (str): Element name for finding. Default: None\n\n" "Attributes:\n" " start (Vector): Starting point\n" " end (Vector): Ending point\n" @@ -139,10 +135,6 @@ namespace mcrfpydef { " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" " name (str): Element name\n" - " align (Alignment): Alignment relative to parent (or None)\n" - " margin (float): General margin for alignment\n" - " horiz_margin (float): Horizontal margin override\n" - " vert_margin (float): Vertical margin override\n" ), .tp_methods = UILine_methods, .tp_getset = UILine::getsetters, diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 7461017..794748b 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -3,7 +3,6 @@ #include "PyVector.h" #include "PythonObjectCache.h" #include "UIFrame.h" // #144: For snapshot= parameter -#include "PyAlignment.h" // UIDrawable methods now in UIBase.h UIDrawable* UISprite::click_at(sf::Vector2f point) @@ -356,7 +355,6 @@ PyGetSetDef UISprite::getsetters[] = { {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, "Position as a Vector", (void*)PyObjectsEnum::UISPRITE}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UISPRITE), - UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UISPRITE), {NULL} }; @@ -390,10 +388,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) const char* name = nullptr; float x = 0.0f, y = 0.0f; PyObject* snapshot = nullptr; // #144: snapshot parameter - PyObject* align_obj = nullptr; // Alignment enum or None - float margin = 0.0f; - float horiz_margin = -1.0f; - float vert_margin = -1.0f; // Keywords list matches the new spec: positional args first, then all keyword args static const char* kwlist[] = { @@ -401,16 +395,14 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) // Keyword-only args "scale", "scale_x", "scale_y", "on_click", "visible", "opacity", "z_index", "name", "x", "y", "snapshot", - "align", "margin", "horiz_margin", "vert_margin", nullptr }; // Parse arguments with | for optional positional args - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffOOfff", const_cast(kwlist), + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffO", const_cast(kwlist), &pos_obj, &texture, &sprite_index, // Positional &scale, &scale_x, &scale_y, &click_handler, - &visible, &opacity, &z_index, &name, &x, &y, &snapshot, - &align_obj, &margin, &horiz_margin, &vert_margin)) { + &visible, &opacity, &z_index, &name, &x, &y, &snapshot)) { return -1; } @@ -520,9 +512,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) self->data->name = std::string(name); } - // Process alignment arguments - UIDRAWABLE_PROCESS_ALIGNMENT(self->data, align_obj, margin, horiz_margin, vert_margin); - // Handle click handler if (click_handler && click_handler != Py_None) { if (!PyCallable_Check(click_handler)) { diff --git a/src/UISprite.h b/src/UISprite.h index d3ddb12..03128a8 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -121,11 +121,7 @@ namespace mcrfpydef { " z_index (int): Rendering order. Default: 0\n" " name (str): Element name for finding. Default: None\n" " x (float): X position override. Default: 0\n" - " y (float): Y position override. Default: 0\n" - " align (Alignment): Alignment relative to parent. Default: None\n" - " margin (float): Margin from parent edge when aligned. Default: 0\n" - " horiz_margin (float): Horizontal margin override. Default: 0 (use margin)\n" - " vert_margin (float): Vertical margin override. Default: 0 (use margin)\n\n" + " y (float): Y position override. Default: 0\n\n" "Attributes:\n" " x, y (float): Position in pixels\n" " pos (Vector): Position as a Vector object\n" @@ -138,11 +134,7 @@ namespace mcrfpydef { " opacity (float): Opacity value\n" " z_index (int): Rendering order\n" " name (str): Element name\n" - " w, h (float): Read-only computed size based on texture and scale\n" - " align (Alignment): Alignment relative to parent (or None)\n" - " margin (float): General margin for alignment\n" - " horiz_margin (float): Horizontal margin override\n" - " vert_margin (float): Vertical margin override"), + " w, h (float): Read-only computed size based on texture and scale"), .tp_methods = UISprite_methods, //.tp_members = PyUIFrame_members, .tp_getset = UISprite::getsetters, diff --git a/stubs/mcrfpy.pyi b/stubs/mcrfpy.pyi index f26ea21..258e848 100644 --- a/stubs/mcrfpy.pyi +++ b/stubs/mcrfpy.pyi @@ -210,31 +210,6 @@ class FOV(IntEnum): RESTRICTIVE = 12 SYMMETRIC_SHADOWCAST = 13 -class Alignment(IntEnum): - """Alignment positions for automatic child positioning relative to parent bounds. - - When a drawable has an alignment set and is added to a parent, its position - is automatically calculated based on the parent's bounds. The position is - updated whenever the parent is resized. - - Example: - parent = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) - child = mcrfpy.Caption(text="Centered!", align=mcrfpy.Alignment.CENTER) - parent.children.append(child) # child is auto-positioned to center - parent.w = 800 # child position updates automatically - - Set align=None to disable automatic positioning and use manual coordinates. - """ - TOP_LEFT = 0 - TOP_CENTER = 1 - TOP_RIGHT = 2 - CENTER_LEFT = 3 - CENTER = 4 - CENTER_RIGHT = 5 - BOTTOM_LEFT = 6 - BOTTOM_CENTER = 7 - BOTTOM_RIGHT = 8 - # Classes class Color: @@ -484,16 +459,6 @@ class Drawable: # Read-only hover state (#140) hovered: bool - # Alignment system - automatic positioning relative to parent - align: Optional[Alignment] - """Alignment relative to parent bounds. Set to None for manual positioning.""" - margin: float - """General margin from edge when aligned (applies to both axes unless overridden).""" - horiz_margin: float - """Horizontal margin override (0 = use general margin).""" - vert_margin: float - """Vertical margin override (0 = use general margin).""" - def get_bounds(self) -> Tuple[float, float, float, float]: """Get bounding box as (x, y, width, height).""" ... @@ -518,12 +483,7 @@ class Frame(Drawable): def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0, fill_color: Optional[Color] = None, outline_color: Optional[Color] = None, outline: float = 0, on_click: Optional[Callable] = None, - children: Optional[List[UIElement]] = None, - visible: bool = True, opacity: float = 1.0, z_index: int = 0, - name: Optional[str] = None, pos: Optional[Tuple[float, float]] = None, - size: Optional[Tuple[float, float]] = None, - align: Optional[Alignment] = None, margin: float = 0.0, - horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ... + children: Optional[List[UIElement]] = None) -> None: ... w: float h: float @@ -546,12 +506,7 @@ class Caption(Drawable): def __init__(self, text: str = '', x: float = 0, y: float = 0, font: Optional[Font] = None, fill_color: Optional[Color] = None, outline_color: Optional[Color] = None, outline: float = 0, - on_click: Optional[Callable] = None, - visible: bool = True, opacity: float = 1.0, z_index: int = 0, - name: Optional[str] = None, pos: Optional[Tuple[float, float]] = None, - size: Optional[Tuple[float, float]] = None, - align: Optional[Alignment] = None, margin: float = 0.0, - horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ... + on_click: Optional[Callable] = None) -> None: ... text: str font: Font @@ -573,12 +528,7 @@ class Sprite(Drawable): @overload def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None, sprite_index: int = 0, scale: float = 1.0, - on_click: Optional[Callable] = None, - visible: bool = True, opacity: float = 1.0, z_index: int = 0, - name: Optional[str] = None, pos: Optional[Tuple[float, float]] = None, - size: Optional[Tuple[float, float]] = None, - align: Optional[Alignment] = None, margin: float = 0.0, - horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ... + on_click: Optional[Callable] = None) -> None: ... texture: Texture sprite_index: int @@ -598,12 +548,7 @@ class Grid(Drawable): @overload def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20), texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16, - scale: float = 1.0, on_click: Optional[Callable] = None, - visible: bool = True, opacity: float = 1.0, z_index: int = 0, - name: Optional[str] = None, pos: Optional[Tuple[float, float]] = None, - size: Optional[Tuple[float, float]] = None, - align: Optional[Alignment] = None, margin: float = 0.0, - horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ... + scale: float = 1.0, on_click: Optional[Callable] = None) -> None: ... grid_size: Tuple[int, int] tile_width: int @@ -631,11 +576,7 @@ class Line(Drawable): def __init__(self, start: Optional[Tuple[float, float]] = None, end: Optional[Tuple[float, float]] = None, thickness: float = 1.0, color: Optional[Color] = None, - on_click: Optional[Callable] = None, - visible: bool = True, opacity: float = 1.0, z_index: int = 0, - name: Optional[str] = None, - align: Optional[Alignment] = None, margin: float = 0.0, - horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ... + on_click: Optional[Callable] = None) -> None: ... start: Vector end: Vector @@ -654,11 +595,7 @@ class Circle(Drawable): @overload def __init__(self, radius: float = 0, center: Optional[Tuple[float, float]] = None, fill_color: Optional[Color] = None, outline_color: Optional[Color] = None, - outline: float = 0, on_click: Optional[Callable] = None, - visible: bool = True, opacity: float = 1.0, z_index: int = 0, - name: Optional[str] = None, - align: Optional[Alignment] = None, margin: float = 0.0, - horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ... + outline: float = 0, on_click: Optional[Callable] = None) -> None: ... radius: float center: Vector @@ -679,11 +616,7 @@ class Arc(Drawable): def __init__(self, center: Optional[Tuple[float, float]] = None, radius: float = 0, start_angle: float = 0, end_angle: float = 90, color: Optional[Color] = None, thickness: float = 1.0, - on_click: Optional[Callable] = None, - visible: bool = True, opacity: float = 1.0, z_index: int = 0, - name: Optional[str] = None, - align: Optional[Alignment] = None, margin: float = 0.0, - horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ... + on_click: Optional[Callable] = None) -> None: ... center: Vector radius: float diff --git a/tests/unit/alignment_constructor_test.py b/tests/unit/alignment_constructor_test.py deleted file mode 100644 index 5f02dab..0000000 --- a/tests/unit/alignment_constructor_test.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -"""Test alignment constructor arguments work correctly.""" - -import mcrfpy -import sys - -# Test that alignment args work in constructors - -print("Test 1: Frame with align constructor arg...") -parent = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -child = mcrfpy.Frame(size=(100, 50), align=mcrfpy.Alignment.CENTER) -parent.children.append(child) -# Expected: (400-100)/2=150, (300-50)/2=125 -if abs(child.x - 150) < 0.1 and abs(child.y - 125) < 0.1: - print(" PASS: Frame align constructor arg works") -else: - print(f" FAIL: Expected (150, 125), got ({child.x}, {child.y})") - sys.exit(1) - -print("Test 2: Frame with align and margin constructor args...") -parent2 = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -child2 = mcrfpy.Frame(size=(50, 50), align=mcrfpy.Alignment.TOP_LEFT, margin=10) -parent2.children.append(child2) -if abs(child2.x - 10) < 0.1 and abs(child2.y - 10) < 0.1: - print(" PASS: Frame margin constructor arg works") -else: - print(f" FAIL: Expected (10, 10), got ({child2.x}, {child2.y})") - sys.exit(1) - -print("Test 3: Caption with align constructor arg...") -parent3 = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -cap = mcrfpy.Caption(text="Test", align=mcrfpy.Alignment.TOP_CENTER, margin=20) -parent3.children.append(cap) -# Should be centered horizontally, 20px from top -if abs(cap.y - 20) < 0.1: - print(" PASS: Caption align constructor arg works") -else: - print(f" FAIL: Expected y=20, got y={cap.y}") - sys.exit(1) - -print("Test 4: Sprite with align constructor arg...") -parent4 = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -spr = mcrfpy.Sprite(align=mcrfpy.Alignment.BOTTOM_LEFT, margin=5) -parent4.children.append(spr) -if abs(spr.x - 5) < 0.1: - print(" PASS: Sprite align constructor arg works") -else: - print(f" FAIL: Expected x=5, got x={spr.x}") - sys.exit(1) - -print("Test 5: Grid with align constructor arg...") -parent5 = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -grid = mcrfpy.Grid(grid_size=(10, 10), size=(200, 200), align=mcrfpy.Alignment.CENTER_RIGHT, margin=15) -parent5.children.append(grid) -# Expected x: 400-200-15=185 -if abs(grid.x - 185) < 0.1: - print(" PASS: Grid align constructor arg works") -else: - print(f" FAIL: Expected x=185, got x={grid.x}") - sys.exit(1) - -print("Test 6: Line with align constructor arg...") -parent6 = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -line = mcrfpy.Line(start=(0, 0), end=(50, 0), align=mcrfpy.Alignment.TOP_LEFT, margin=25) -parent6.children.append(line) -# Line's position (pos) should be at margin -if abs(line.pos.x - 25) < 0.1 and abs(line.pos.y - 25) < 0.1: - print(" PASS: Line align constructor arg works") -else: - print(f" FAIL: Expected pos at (25, 25), got ({line.pos.x}, {line.pos.y})") - sys.exit(1) - -print("Test 7: Circle with align constructor arg...") -parent7 = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -circ = mcrfpy.Circle(radius=30, align=mcrfpy.Alignment.CENTER) -parent7.children.append(circ) -# Circle is centered, center.x should be at parent center (400/2=200), center.y at (300/2=150) -if abs(circ.center.x - 200) < 0.1 and abs(circ.center.y - 150) < 0.1: - print(" PASS: Circle align constructor arg works") -else: - print(f" FAIL: Expected center at (200, 150), got ({circ.center.x}, {circ.center.y})") - sys.exit(1) - -print("Test 8: Arc with align constructor arg...") -parent8 = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -arc = mcrfpy.Arc(radius=40, align=mcrfpy.Alignment.BOTTOM_CENTER, vert_margin=10) -parent8.children.append(arc) -# Arc is BOTTOM_CENTER aligned with 10px vert_margin -# Arc bounds: width=2*radius=80, height=2*radius=80 -# center.x should be 400/2=200 (centered) -# For bottom alignment: bottom of arc = 300-10 = 290, so center.y = 290 - 40 = 250 -if abs(arc.center.x - 200) < 1.0 and abs(arc.center.y - 250) < 1.0: - print(" PASS: Arc align constructor arg works") -else: - print(f" FAIL: Expected center at (200, 250), got ({arc.center.x}, {arc.center.y})") - sys.exit(1) - -print("Test 9: Testing horiz_margin and vert_margin separately...") -parent9 = mcrfpy.Frame(pos=(0, 0), size=(400, 300)) -frame9 = mcrfpy.Frame(size=(100, 50), align=mcrfpy.Alignment.TOP_RIGHT, horiz_margin=30, vert_margin=20) -parent9.children.append(frame9) -# Expected: x = 400-100-30=270, y = 20 -if abs(frame9.x - 270) < 0.1 and abs(frame9.y - 20) < 0.1: - print(" PASS: horiz_margin and vert_margin constructor args work") -else: - print(f" FAIL: Expected (270, 20), got ({frame9.x}, {frame9.y})") - sys.exit(1) - -print("Test 10: Nested children with alignment in constructor list...") -outer = mcrfpy.Frame( - pos=(100, 100), - size=(400, 300), - children=[ - mcrfpy.Frame(size=(200, 100), align=mcrfpy.Alignment.CENTER), - mcrfpy.Caption(text="Title", align=mcrfpy.Alignment.TOP_CENTER, margin=10), - ] -) -# Check inner frame is centered -inner = outer.children[0] -# (400-200)/2=100, (300-100)/2=100 -if abs(inner.x - 100) < 0.1 and abs(inner.y - 100) < 0.1: - print(" PASS: Nested children alignment works in constructor list") -else: - print(f" FAIL: Expected inner at (100, 100), got ({inner.x}, {inner.y})") - sys.exit(1) - -print() -print("=" * 50) -print("All alignment constructor tests PASSED!") -print("=" * 50) -sys.exit(0) diff --git a/tests/unit/alignment_test.py b/tests/unit/alignment_test.py deleted file mode 100644 index 35e0f00..0000000 --- a/tests/unit/alignment_test.py +++ /dev/null @@ -1,214 +0,0 @@ -"""Test the alignment system for UIDrawable elements.""" - -import mcrfpy -import sys - -# Test 1: Check Alignment enum exists and has expected values -print("Test 1: Checking Alignment enum...") -try: - assert hasattr(mcrfpy, 'Alignment'), "Alignment enum not found" - - # Check all alignment values exist - expected_alignments = [ - 'TOP_LEFT', 'TOP_CENTER', 'TOP_RIGHT', - 'CENTER_LEFT', 'CENTER', 'CENTER_RIGHT', - 'BOTTOM_LEFT', 'BOTTOM_CENTER', 'BOTTOM_RIGHT' - ] - for name in expected_alignments: - assert hasattr(mcrfpy.Alignment, name), f"Alignment.{name} not found" - print(" PASS: Alignment enum has all expected values") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 2: Check that align property exists on Frame -print("Test 2: Checking align property on Frame...") -try: - frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200)) - - # Default alignment should be None - assert frame.align is None, f"Expected align=None, got {frame.align}" - - # Set alignment - frame.align = mcrfpy.Alignment.CENTER - assert frame.align == mcrfpy.Alignment.CENTER, f"Expected CENTER, got {frame.align}" - - # Set back to None - frame.align = None - assert frame.align is None, f"Expected None, got {frame.align}" - print(" PASS: align property works on Frame") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 3: Check margin properties exist -print("Test 3: Checking margin properties...") -try: - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - - # Check default margins are 0 - assert frame.margin == 0, f"Expected margin=0, got {frame.margin}" - assert frame.horiz_margin == 0, f"Expected horiz_margin=0, got {frame.horiz_margin}" - assert frame.vert_margin == 0, f"Expected vert_margin=0, got {frame.vert_margin}" - - # Set margins when no alignment - frame.margin = 10.0 - assert frame.margin == 10.0, f"Expected margin=10, got {frame.margin}" - print(" PASS: margin properties exist and can be set") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 4: Check alignment auto-positioning -print("Test 4: Checking alignment auto-positioning...") -try: - # Create parent frame - parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200)) - - # Create child with CENTER alignment - child = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) - child.align = mcrfpy.Alignment.CENTER - - # Add to parent - should trigger alignment - parent.children.append(child) - - # Child should be centered: (200-50)/2 = 75 - expected_x = 75.0 - expected_y = 75.0 - assert abs(child.x - expected_x) < 0.1, f"Expected x={expected_x}, got {child.x}" - assert abs(child.y - expected_y) < 0.1, f"Expected y={expected_y}, got {child.y}" - print(" PASS: CENTER alignment positions child correctly") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 5: Check TOP_LEFT with margin -print("Test 5: Checking TOP_LEFT alignment with margin...") -try: - parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200)) - child = mcrfpy.Frame(pos=(999, 999), size=(50, 50)) # Start at wrong position - child.align = mcrfpy.Alignment.TOP_LEFT - child.margin = 10.0 - - parent.children.append(child) - - # Child should be at (10, 10) - assert abs(child.x - 10.0) < 0.1, f"Expected x=10, got {child.x}" - assert abs(child.y - 10.0) < 0.1, f"Expected y=10, got {child.y}" - print(" PASS: TOP_LEFT with margin positions correctly") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 6: Check BOTTOM_RIGHT alignment -print("Test 6: Checking BOTTOM_RIGHT alignment...") -try: - parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200)) - child = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) - child.align = mcrfpy.Alignment.BOTTOM_RIGHT - child.margin = 5.0 - - parent.children.append(child) - - # Child should be at (200-50-5, 200-50-5) = (145, 145) - expected_x = 145.0 - expected_y = 145.0 - assert abs(child.x - expected_x) < 0.1, f"Expected x={expected_x}, got {child.x}" - assert abs(child.y - expected_y) < 0.1, f"Expected y={expected_y}, got {child.y}" - print(" PASS: BOTTOM_RIGHT with margin positions correctly") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 7: Check resize propagation -print("Test 7: Checking resize propagation to children...") -try: - parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200)) - child = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) - child.align = mcrfpy.Alignment.CENTER - - parent.children.append(child) - - # Initial position check - assert abs(child.x - 75.0) < 0.1, f"Initial x should be 75, got {child.x}" - - # Resize parent - parent.w = 300 - parent.h = 300 - - # Child should be re-centered: (300-50)/2 = 125 - expected_x = 125.0 - expected_y = 125.0 - assert abs(child.x - expected_x) < 0.1, f"After resize, expected x={expected_x}, got {child.x}" - assert abs(child.y - expected_y) < 0.1, f"After resize, expected y={expected_y}, got {child.y}" - print(" PASS: Resize propagates to aligned children") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 8: Check that align=None freezes position -print("Test 8: Checking that align=None freezes position...") -try: - parent = mcrfpy.Frame(pos=(0, 0), size=(200, 200)) - child = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) - child.align = mcrfpy.Alignment.CENTER - - parent.children.append(child) - centered_x = child.x - centered_y = child.y - - # Disable alignment - child.align = None - - # Resize parent - parent.w = 400 - parent.h = 400 - - # Position should NOT change - assert abs(child.x - centered_x) < 0.1, f"Position should be frozen at {centered_x}, got {child.x}" - assert abs(child.y - centered_y) < 0.1, f"Position should be frozen at {centered_y}, got {child.y}" - print(" PASS: align=None freezes position") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 9: Check CENTER alignment rejects margins -print("Test 9: Checking CENTER alignment rejects margins...") -try: - frame = mcrfpy.Frame(pos=(0, 0), size=(50, 50)) - frame.align = mcrfpy.Alignment.CENTER - - # Setting margin on CENTER should raise ValueError - try: - frame.margin = 10.0 - print(" FAIL: Expected ValueError for margin with CENTER alignment") - sys.exit(1) - except ValueError as e: - pass # Expected - - print(" PASS: CENTER alignment correctly rejects margin") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -# Test 10: Check alignment on other drawable types -print("Test 10: Checking alignment on Caption...") -try: - parent = mcrfpy.Frame(pos=(0, 0), size=(200, 100)) - caption = mcrfpy.Caption(text="Test", pos=(0, 0)) - caption.align = mcrfpy.Alignment.CENTER - - parent.children.append(caption) - - # Caption should be roughly centered (exact position depends on text size) - # Just verify it was moved from (0,0) - assert caption.x > 0 or caption.y > 0, "Caption should have been repositioned" - print(" PASS: Caption supports alignment") -except Exception as e: - print(f" FAIL: {e}") - sys.exit(1) - -print("\n" + "=" * 40) -print("All alignment tests PASSED!") -print("=" * 40) -sys.exit(0) diff --git a/tests/unit/dijkstra_to_heightmap_test.py b/tests/unit/dijkstra_to_heightmap_test.py deleted file mode 100644 index 8dc651a..0000000 --- a/tests/unit/dijkstra_to_heightmap_test.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Test DijkstraMap.to_heightmap() method.""" -import mcrfpy -import sys - -def test_basic_conversion(): - """Test basic conversion of DijkstraMap to HeightMap.""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - - # Initialize all cells as walkable - for y in range(10): - for x in range(10): - grid.at((x, y)).walkable = True - - # Get a dijkstra map from center - dijkstra = grid.get_dijkstra_map((5, 5)) - - # Convert to heightmap - hmap = dijkstra.to_heightmap() - - # Verify type - assert type(hmap).__name__ == "HeightMap", f"Expected HeightMap, got {type(hmap).__name__}" - - # Verify root cell has distance 0 - assert hmap[(5, 5)] == 0.0, f"Root cell should have height 0, got {hmap[(5, 5)]}" - - # Verify corner has non-zero distance - corner_dist = dijkstra.distance((0, 0)) - corner_height = hmap[(0, 0)] - assert abs(corner_dist - corner_height) < 0.001, f"Height {corner_height} should match distance {corner_dist}" - - print("test_basic_conversion PASSED") - -def test_unreachable_cells(): - """Test that unreachable cells use the unreachable parameter.""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - - # Initialize all cells as walkable - for y in range(10): - for x in range(10): - grid.at((x, y)).walkable = True - - # Add a wall - grid.at((3, 3)).walkable = False - - dijkstra = grid.get_dijkstra_map((5, 5)) - - # Default unreachable value is -1.0 (distinct from root which has distance 0) - hmap1 = dijkstra.to_heightmap() - assert hmap1[(3, 3)] == -1.0, f"Default unreachable should be -1.0, got {hmap1[(3, 3)]}" - - # Custom unreachable value - hmap2 = dijkstra.to_heightmap(unreachable=0.0) - assert hmap2[(3, 3)] == 0.0, f"Custom unreachable should be 0.0, got {hmap2[(3, 3)]}" - - # Large unreachable value - hmap3 = dijkstra.to_heightmap(unreachable=999.0) - assert hmap3[(3, 3)] == 999.0, f"Large unreachable should be 999.0, got {hmap3[(3, 3)]}" - - print("test_unreachable_cells PASSED") - -def test_custom_size(): - """Test custom size parameter.""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - - for y in range(10): - for x in range(10): - grid.at((x, y)).walkable = True - - dijkstra = grid.get_dijkstra_map((5, 5)) - - # Custom smaller size - hmap = dijkstra.to_heightmap(size=(5, 5)) - - # Verify dimensions via repr - repr_str = repr(hmap) - assert "5 x 5" in repr_str, f"Expected 5x5 heightmap, got {repr_str}" - - # Values within dijkstra bounds should work - assert hmap[(0, 0)] == dijkstra.distance((0, 0)), "Heights should match distances within bounds" - - print("test_custom_size PASSED") - -def test_larger_custom_size(): - """Test custom size larger than dijkstra bounds.""" - grid = mcrfpy.Grid(grid_size=(5, 5)) - - for y in range(5): - for x in range(5): - grid.at((x, y)).walkable = True - - dijkstra = grid.get_dijkstra_map((2, 2)) - - # Custom larger size - cells outside dijkstra bounds get unreachable value - hmap = dijkstra.to_heightmap(size=(10, 10), unreachable=-99.0) - - # Values within dijkstra bounds should work - assert hmap[(2, 2)] == 0.0, "Root should have height 0" - - # Values outside bounds should have unreachable value - assert hmap[(8, 8)] == -99.0, f"Outside bounds should be -99.0, got {hmap[(8, 8)]}" - - print("test_larger_custom_size PASSED") - -def test_distance_values(): - """Test that heightmap values match dijkstra distances.""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - - for y in range(10): - for x in range(10): - grid.at((x, y)).walkable = True - - dijkstra = grid.get_dijkstra_map((0, 0)) - hmap = dijkstra.to_heightmap() - - # Check various positions - for pos in [(0, 0), (1, 0), (0, 1), (5, 5), (9, 9)]: - dist = dijkstra.distance(pos) - height = hmap[pos] - assert abs(dist - height) < 0.001, f"At {pos}: height {height} != distance {dist}" - - print("test_distance_values PASSED") - -# Run all tests -test_basic_conversion() -test_unreachable_cells() -test_custom_size() -test_larger_custom_size() -test_distance_values() - -print("\nAll DijkstraMap.to_heightmap tests PASSED") -sys.exit(0)