Compare commits

..

No commits in common. "65b5ecc5c73bfa74de49eef9a42b61027f566a39" and "73230989ad0b60471ee1648126a59345c1205127" have entirely different histories.

27 changed files with 406 additions and 1660 deletions

View file

@ -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) {

View file

@ -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<int>(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;
}

View file

@ -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;
};

View file

@ -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}
};

View file

@ -1,7 +1,6 @@
#include "UIArc.h"
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include <cmath>
#include <sstream>
@ -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<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifiz", const_cast<char**>(kwlist),
&center_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();

View file

@ -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,

View file

@ -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<typename T>
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

View file

@ -4,7 +4,6 @@
#include "PyVector.h"
#include "PyFont.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
// UIDrawable methods now in UIBase.h
#include <algorithm>
@ -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<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast<char**>(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;

View file

@ -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,

View file

@ -4,7 +4,6 @@
#include "PyVector.h"
#include "PyColor.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include <cmath>
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, &center_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();

View file

@ -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,

View file

@ -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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure) >> 8);
int member = reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure) >> 8);
int member = reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<UIDrawable> 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> 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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<float>(resolution.x);
ph = static_cast<float>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<int>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
float margin = 0.0f;
if (PyFloat_Check(value)) {
margin = static_cast<float>(PyFloat_AsDouble(value));
} else if (PyLong_Check(value)) {
margin = static_cast<float>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
float horiz_margin = 0.0f;
if (PyFloat_Check(value)) {
horiz_margin = static_cast<float>(PyFloat_AsDouble(value));
} else if (PyLong_Check(value)) {
horiz_margin = static_cast<float>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
UIDrawable* drawable = extractDrawable(self, objtype);
if (!drawable) return -1;
float vert_margin = 0.0f;
if (PyFloat_Check(value)) {
vert_margin = static_cast<float>(PyFloat_AsDouble(value));
} else if (PyLong_Check(value)) {
vert_margin = static_cast<float>(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;
}

View file

@ -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)

View file

@ -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<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffii", const_cast<char**>(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;
}

View file

@ -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,

View file

@ -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<unsigned int>(w), static_cast<unsigned int>(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<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffiiO", const_cast<char**>(kwlist),
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
&fill_color, &click_handler, &center_x, &center_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},

View file

@ -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,

View file

@ -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<sf::Vector2i> DijkstraMap::getPathFrom(int x, int y) const {
std::vector<sf::Vector2i> 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<float>(root.x), static_cast<float>(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<char**>(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}
};

View file

@ -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);

View file

@ -4,7 +4,6 @@
#include "PyVector.h"
#include "PyColor.h"
#include "PythonObjectCache.h"
#include "PyAlignment.h"
#include <cmath>
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<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifiz", const_cast<char**>(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)) {

View file

@ -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,

View file

@ -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<char**>(kwlist),
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizffO", const_cast<char**>(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)) {

View file

@ -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,

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)