From 311dc02f1dc49eb02328b29544c097f080646a27 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 25 Nov 2025 21:42:33 -0500 Subject: [PATCH] feat: Add UILine, UICircle, and UIArc drawing primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement new UIDrawable-derived classes for vector graphics: - UILine: Thick line segments using sf::ConvexShape for proper thickness - Properties: start, end, color, thickness - Supports click detection along the line - UICircle: Filled and outlined circles using sf::CircleShape - Properties: radius, center, fill_color, outline_color, outline - Full property system for animations - UIArc: Arc segments for orbital paths and partial circles - Properties: center, radius, start_angle, end_angle, color, thickness - Uses sf::VertexArray with TriangleStrip for smooth rendering - Supports arbitrary angle spans including negative (reverse) arcs All primitives integrate with the Python API through mcrfpy module: - Added to PyObjectsEnum for type identification - Full getter/setter support for all properties - Added to UICollection for scene management - Support for visibility, opacity, z_index, name, and click handling closes #128, closes #129 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/McRFPy_API.cpp | 17 +- src/UIArc.cpp | 520 +++++++++++++++++++++++++++++++++++++++ src/UIArc.h | 167 +++++++++++++ src/UICircle.cpp | 490 +++++++++++++++++++++++++++++++++++++ src/UICircle.h | 155 ++++++++++++ src/UICollection.cpp | 92 ++++++- src/UIDrawable.cpp | 132 ++++++++-- src/UIDrawable.h | 5 +- src/UILine.cpp | 561 +++++++++++++++++++++++++++++++++++++++++++ src/UILine.h | 150 ++++++++++++ 10 files changed, 2261 insertions(+), 28 deletions(-) create mode 100644 src/UIArc.cpp create mode 100644 src/UIArc.h create mode 100644 src/UICircle.cpp create mode 100644 src/UICircle.h create mode 100644 src/UILine.cpp create mode 100644 src/UILine.h diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 3947163..b095e52 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -10,6 +10,9 @@ #include "PySceneObject.h" #include "GameEngine.h" #include "UI.h" +#include "UILine.h" +#include "UICircle.h" +#include "UIArc.h" #include "Resources.h" #include "PyScene.h" #include @@ -251,6 +254,7 @@ PyObject* PyInit_mcrfpy() /*UI widgets*/ &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType, + &PyUILineType, &PyUICircleType, &PyUIArcType, /*game map & perspective data*/ &PyUIGridPointType, &PyUIGridPointStateType, @@ -258,19 +262,19 @@ PyObject* PyInit_mcrfpy() /*collections & iterators*/ &PyUICollectionType, &PyUICollectionIterType, &PyUIEntityCollectionType, &PyUIEntityCollectionIterType, - + /*animation*/ &PyAnimationType, - + /*timer*/ &PyTimerType, - + /*window singleton*/ &PyWindowType, - + /*scene class*/ &PySceneType, - + nullptr}; // Set up PyWindowType methods and getsetters before PyType_Ready @@ -288,6 +292,9 @@ PyObject* PyInit_mcrfpy() PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist); PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist); PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist); + PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist); + PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist); + PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist); int i = 0; auto t = pytypes[i]; diff --git a/src/UIArc.cpp b/src/UIArc.cpp new file mode 100644 index 0000000..71031ec --- /dev/null +++ b/src/UIArc.cpp @@ -0,0 +1,520 @@ +#include "UIArc.h" +#include "McRFPy_API.h" +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +UIArc::UIArc() + : center(0.0f, 0.0f), radius(0.0f), start_angle(0.0f), end_angle(90.0f), + color(sf::Color::White), thickness(1.0f), vertices_dirty(true) +{ + position = center; +} + +UIArc::UIArc(sf::Vector2f center, float radius, float startAngle, float endAngle, + sf::Color color, float thickness) + : center(center), radius(radius), start_angle(startAngle), end_angle(endAngle), + color(color), thickness(thickness), vertices_dirty(true) +{ + position = center; +} + +UIArc::UIArc(const UIArc& other) + : UIDrawable(other), + center(other.center), + radius(other.radius), + start_angle(other.start_angle), + end_angle(other.end_angle), + color(other.color), + thickness(other.thickness), + vertices_dirty(true) +{ +} + +UIArc& UIArc::operator=(const UIArc& other) { + if (this != &other) { + UIDrawable::operator=(other); + center = other.center; + radius = other.radius; + start_angle = other.start_angle; + end_angle = other.end_angle; + color = other.color; + thickness = other.thickness; + vertices_dirty = true; + } + return *this; +} + +UIArc::UIArc(UIArc&& other) noexcept + : UIDrawable(std::move(other)), + center(other.center), + radius(other.radius), + start_angle(other.start_angle), + end_angle(other.end_angle), + color(other.color), + thickness(other.thickness), + vertices(std::move(other.vertices)), + vertices_dirty(other.vertices_dirty) +{ +} + +UIArc& UIArc::operator=(UIArc&& other) noexcept { + if (this != &other) { + UIDrawable::operator=(std::move(other)); + center = other.center; + radius = other.radius; + start_angle = other.start_angle; + end_angle = other.end_angle; + color = other.color; + thickness = other.thickness; + vertices = std::move(other.vertices); + vertices_dirty = other.vertices_dirty; + } + return *this; +} + +void UIArc::rebuildVertices() { + vertices.clear(); + vertices.setPrimitiveType(sf::TriangleStrip); + + // Calculate the arc parameters + float inner_radius = radius - thickness / 2.0f; + float outer_radius = radius + thickness / 2.0f; + + if (inner_radius < 0) inner_radius = 0; + + // Normalize angles + float start_rad = start_angle * M_PI / 180.0f; + float end_rad = end_angle * M_PI / 180.0f; + + // Calculate number of segments based on arc length + float angle_span = end_rad - start_rad; + int num_segments = std::max(3, static_cast(std::abs(angle_span * radius) / 5.0f)); + num_segments = std::min(num_segments, 100); // Cap at 100 segments + + float angle_step = angle_span / num_segments; + + // Apply opacity to color + sf::Color render_color = color; + render_color.a = static_cast(render_color.a * opacity); + + // Build the triangle strip + for (int i = 0; i <= num_segments; ++i) { + float angle = start_rad + i * angle_step; + float cos_a = std::cos(angle); + float sin_a = std::sin(angle); + + // Inner vertex + sf::Vector2f inner_pos( + center.x + inner_radius * cos_a, + center.y + inner_radius * sin_a + ); + vertices.append(sf::Vertex(inner_pos, render_color)); + + // Outer vertex + sf::Vector2f outer_pos( + center.x + outer_radius * cos_a, + center.y + outer_radius * sin_a + ); + vertices.append(sf::Vertex(outer_pos, render_color)); + } + + vertices_dirty = false; +} + +void UIArc::render(sf::Vector2f offset, sf::RenderTarget& target) { + if (!visible) return; + + if (vertices_dirty) { + rebuildVertices(); + } + + // Apply offset by creating a transformed copy + sf::Transform transform; + transform.translate(offset); + target.draw(vertices, transform); +} + +UIDrawable* UIArc::click_at(sf::Vector2f point) { + if (!visible) return nullptr; + + // Calculate distance from center + float dx = point.x - center.x; + float dy = point.y - center.y; + float dist = std::sqrt(dx * dx + dy * dy); + + // Check if within the arc's radial range + float inner_radius = radius - thickness / 2.0f; + float outer_radius = radius + thickness / 2.0f; + if (inner_radius < 0) inner_radius = 0; + + if (dist < inner_radius || dist > outer_radius) { + return nullptr; + } + + // Check if within the angle range + float angle = std::atan2(dy, dx) * 180.0f / M_PI; + + // Normalize angle to match start/end angle range + float start = start_angle; + float end = end_angle; + + // Handle angle wrapping + while (angle < start - 180.0f) angle += 360.0f; + while (angle > start + 180.0f) angle -= 360.0f; + + if ((start <= end && angle >= start && angle <= end) || + (start > end && (angle >= start || angle <= end))) { + return this; + } + + return nullptr; +} + +PyObjectsEnum UIArc::derived_type() { + return PyObjectsEnum::UIARC; +} + +sf::FloatRect UIArc::get_bounds() const { + float outer_radius = radius + thickness / 2.0f; + return sf::FloatRect( + center.x - outer_radius, + center.y - outer_radius, + outer_radius * 2, + outer_radius * 2 + ); +} + +void UIArc::move(float dx, float dy) { + center.x += dx; + center.y += dy; + position = center; + vertices_dirty = true; +} + +void UIArc::resize(float w, float h) { + // Resize by adjusting radius to fit in the given dimensions + radius = std::min(w, h) / 2.0f - thickness / 2.0f; + if (radius < 0) radius = 0; + vertices_dirty = true; +} + +// Property setters +bool UIArc::setProperty(const std::string& name, float value) { + if (name == "radius") { + setRadius(value); + return true; + } + else if (name == "start_angle") { + setStartAngle(value); + return true; + } + else if (name == "end_angle") { + setEndAngle(value); + return true; + } + else if (name == "thickness") { + setThickness(value); + return true; + } + else if (name == "x") { + center.x = value; + position = center; + vertices_dirty = true; + return true; + } + else if (name == "y") { + center.y = value; + position = center; + vertices_dirty = true; + return true; + } + return false; +} + +bool UIArc::setProperty(const std::string& name, const sf::Color& value) { + if (name == "color") { + setColor(value); + return true; + } + return false; +} + +bool UIArc::setProperty(const std::string& name, const sf::Vector2f& value) { + if (name == "center") { + setCenter(value); + return true; + } + return false; +} + +bool UIArc::getProperty(const std::string& name, float& value) const { + if (name == "radius") { + value = radius; + return true; + } + else if (name == "start_angle") { + value = start_angle; + return true; + } + else if (name == "end_angle") { + value = end_angle; + return true; + } + else if (name == "thickness") { + value = thickness; + return true; + } + else if (name == "x") { + value = center.x; + return true; + } + else if (name == "y") { + value = center.y; + return true; + } + return false; +} + +bool UIArc::getProperty(const std::string& name, sf::Color& value) const { + if (name == "color") { + value = color; + return true; + } + return false; +} + +bool UIArc::getProperty(const std::string& name, sf::Vector2f& value) const { + if (name == "center") { + value = center; + return true; + } + return false; +} + +// Python API implementation +PyObject* UIArc::get_center(PyUIArcObject* self, void* closure) { + auto center = self->data->getCenter(); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (!type) return NULL; + PyObject* result = PyObject_CallFunction((PyObject*)type, "ff", center.x, center.y); + Py_DECREF(type); + return result; +} + +int UIArc::set_center(PyUIArcObject* self, PyObject* value, void* closure) { + PyVectorObject* vec = PyVector::from_arg(value); + if (!vec) { + PyErr_Clear(); + PyErr_SetString(PyExc_TypeError, "center must be a Vector or tuple (x, y)"); + return -1; + } + self->data->setCenter(vec->data); + return 0; +} + +PyObject* UIArc::get_radius(PyUIArcObject* self, void* closure) { + return PyFloat_FromDouble(self->data->getRadius()); +} + +int UIArc::set_radius(PyUIArcObject* self, PyObject* value, void* closure) { + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "radius must be a number"); + return -1; + } + self->data->setRadius(static_cast(PyFloat_AsDouble(value))); + return 0; +} + +PyObject* UIArc::get_start_angle(PyUIArcObject* self, void* closure) { + return PyFloat_FromDouble(self->data->getStartAngle()); +} + +int UIArc::set_start_angle(PyUIArcObject* self, PyObject* value, void* closure) { + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "start_angle must be a number"); + return -1; + } + self->data->setStartAngle(static_cast(PyFloat_AsDouble(value))); + return 0; +} + +PyObject* UIArc::get_end_angle(PyUIArcObject* self, void* closure) { + return PyFloat_FromDouble(self->data->getEndAngle()); +} + +int UIArc::set_end_angle(PyUIArcObject* self, PyObject* value, void* closure) { + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "end_angle must be a number"); + return -1; + } + self->data->setEndAngle(static_cast(PyFloat_AsDouble(value))); + return 0; +} + +PyObject* UIArc::get_color(PyUIArcObject* self, void* closure) { + auto color = self->data->getColor(); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); + PyObject* args = Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a); + PyObject* obj = PyObject_CallObject((PyObject*)type, args); + Py_DECREF(args); + Py_DECREF(type); + return obj; +} + +int UIArc::set_color(PyUIArcObject* self, PyObject* value, void* closure) { + auto color = PyColor::from_arg(value); + if (!color) { + PyErr_SetString(PyExc_TypeError, "color must be a Color or tuple (r, g, b) or (r, g, b, a)"); + return -1; + } + self->data->setColor(color->data); + return 0; +} + +PyObject* UIArc::get_thickness(PyUIArcObject* self, void* closure) { + return PyFloat_FromDouble(self->data->getThickness()); +} + +int UIArc::set_thickness(PyUIArcObject* self, PyObject* value, void* closure) { + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "thickness must be a number"); + return -1; + } + self->data->setThickness(static_cast(PyFloat_AsDouble(value))); + return 0; +} + +// Required typedef for UIDRAWABLE_GETSETTERS and UIDRAWABLE_METHODS macro templates +typedef PyUIArcObject PyObjectType; + +PyGetSetDef UIArc::getsetters[] = { + {"center", (getter)UIArc::get_center, (setter)UIArc::set_center, + "Center position of the arc", NULL}, + {"radius", (getter)UIArc::get_radius, (setter)UIArc::set_radius, + "Arc radius in pixels", NULL}, + {"start_angle", (getter)UIArc::get_start_angle, (setter)UIArc::set_start_angle, + "Starting angle in degrees", NULL}, + {"end_angle", (getter)UIArc::get_end_angle, (setter)UIArc::set_end_angle, + "Ending angle in degrees", NULL}, + {"color", (getter)UIArc::get_color, (setter)UIArc::set_color, + "Arc color", NULL}, + {"thickness", (getter)UIArc::get_thickness, (setter)UIArc::set_thickness, + "Line thickness", NULL}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, + "Callable executed when arc is clicked.", (void*)PyObjectsEnum::UIARC}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, + "Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UIARC}, + {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, + "Name for finding this element.", (void*)PyObjectsEnum::UIARC}, + {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, + "Position as a Vector (same as center).", (void*)PyObjectsEnum::UIARC}, + UIDRAWABLE_GETSETTERS, + {NULL} +}; + +PyMethodDef UIArc_methods[] = { + UIDRAWABLE_METHODS, + {NULL} +}; + +PyObject* UIArc::repr(PyUIArcObject* self) { + std::ostringstream oss; + if (!self->data) { + oss << ""; + } else { + auto center = self->data->getCenter(); + auto color = self->data->getColor(); + oss << "data->getStartAngle() << ", " << self->data->getEndAngle() << ") " + << "color=(" << (int)color.r << ", " << (int)color.g << ", " + << (int)color.b << ", " << (int)color.a << ")>"; + } + std::string repr_str = oss.str(); + return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); +} + +int UIArc::init(PyUIArcObject* self, PyObject* args, PyObject* kwds) { + // Arguments + PyObject* center_obj = nullptr; + float radius = 0.0f; + float start_angle = 0.0f; + float end_angle = 90.0f; + PyObject* color_obj = nullptr; + float thickness = 1.0f; + PyObject* click_handler = nullptr; + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + + static const char* kwlist[] = { + "center", "radius", "start_angle", "end_angle", "color", "thickness", + "click", "visible", "opacity", "z_index", "name", + nullptr + }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfffOfOifiz", const_cast(kwlist), + ¢er_obj, &radius, &start_angle, &end_angle, + &color_obj, &thickness, + &click_handler, &visible, &opacity, &z_index, &name)) { + return -1; + } + + // Parse center position + sf::Vector2f center(0.0f, 0.0f); + if (center_obj) { + PyVectorObject* vec = PyVector::from_arg(center_obj); + if (vec) { + center = vec->data; + } else { + PyErr_Clear(); + PyErr_SetString(PyExc_TypeError, "center must be a Vector or tuple (x, y)"); + return -1; + } + } + + // Parse color + sf::Color color = sf::Color::White; + if (color_obj) { + auto pycolor = PyColor::from_arg(color_obj); + if (pycolor) { + color = pycolor->data; + } else { + PyErr_Clear(); + PyErr_SetString(PyExc_TypeError, "color must be a Color or tuple (r, g, b) or (r, g, b, a)"); + return -1; + } + } + + // Set values + self->data->setCenter(center); + self->data->setRadius(radius); + self->data->setStartAngle(start_angle); + self->data->setEndAngle(end_angle); + self->data->setColor(color); + self->data->setThickness(thickness); + + // Handle common UIDrawable properties + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } + + self->data->visible = (visible != 0); + self->data->opacity = opacity; + self->data->z_index = z_index; + + if (name) { + self->data->name = name; + } + + return 0; +} diff --git a/src/UIArc.h b/src/UIArc.h new file mode 100644 index 0000000..ce9dcf9 --- /dev/null +++ b/src/UIArc.h @@ -0,0 +1,167 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include "structmember.h" +#include "UIDrawable.h" +#include "UIBase.h" +#include "PyDrawable.h" +#include "PyColor.h" +#include "PyVector.h" +#include "McRFPy_Doc.h" + +// Forward declaration +class UIArc; + +// Python object structure +typedef struct { + PyObject_HEAD + std::shared_ptr data; + PyObject* weakreflist; +} PyUIArcObject; + +class UIArc : public UIDrawable +{ +private: + sf::Vector2f center; + float radius; + float start_angle; // in degrees + float end_angle; // in degrees + sf::Color color; + float thickness; + + // Cached vertex array for rendering + sf::VertexArray vertices; + bool vertices_dirty; + + void rebuildVertices(); + +public: + UIArc(); + UIArc(sf::Vector2f center, float radius, float startAngle, float endAngle, + sf::Color color = sf::Color::White, float thickness = 1.0f); + + // Copy constructor and assignment + UIArc(const UIArc& other); + UIArc& operator=(const UIArc& other); + + // Move constructor and assignment + UIArc(UIArc&& other) noexcept; + UIArc& operator=(UIArc&& other) noexcept; + + // UIDrawable interface + void render(sf::Vector2f offset, sf::RenderTarget& target) override; + UIDrawable* click_at(sf::Vector2f point) override; + PyObjectsEnum derived_type() override; + + // Getters and setters + sf::Vector2f getCenter() const { return center; } + void setCenter(sf::Vector2f c) { center = c; position = c; vertices_dirty = true; } + + float getRadius() const { return radius; } + void setRadius(float r) { radius = r; vertices_dirty = true; } + + float getStartAngle() const { return start_angle; } + void setStartAngle(float a) { start_angle = a; vertices_dirty = true; } + + float getEndAngle() const { return end_angle; } + void setEndAngle(float a) { end_angle = a; vertices_dirty = true; } + + sf::Color getColor() const { return color; } + void setColor(sf::Color c) { color = c; vertices_dirty = true; } + + float getThickness() const { return thickness; } + void setThickness(float t) { thickness = t; vertices_dirty = true; } + + // Phase 1 virtual method implementations + sf::FloatRect get_bounds() const override; + void move(float dx, float dy) override; + void resize(float w, float h) override; + + // Property system for animations + bool setProperty(const std::string& name, float value) override; + bool setProperty(const std::string& name, const sf::Color& value) override; + bool setProperty(const std::string& name, const sf::Vector2f& value) override; + bool getProperty(const std::string& name, float& value) const override; + bool getProperty(const std::string& name, sf::Color& value) const override; + bool getProperty(const std::string& name, sf::Vector2f& value) const override; + + // Python API + static PyObject* get_center(PyUIArcObject* self, void* closure); + static int set_center(PyUIArcObject* self, PyObject* value, void* closure); + static PyObject* get_radius(PyUIArcObject* self, void* closure); + static int set_radius(PyUIArcObject* self, PyObject* value, void* closure); + static PyObject* get_start_angle(PyUIArcObject* self, void* closure); + static int set_start_angle(PyUIArcObject* self, PyObject* value, void* closure); + static PyObject* get_end_angle(PyUIArcObject* self, void* closure); + static int set_end_angle(PyUIArcObject* self, PyObject* value, void* closure); + static PyObject* get_color(PyUIArcObject* self, void* closure); + static int set_color(PyUIArcObject* self, PyObject* value, void* closure); + static PyObject* get_thickness(PyUIArcObject* self, void* closure); + static int set_thickness(PyUIArcObject* self, PyObject* value, void* closure); + + static PyGetSetDef getsetters[]; + static PyObject* repr(PyUIArcObject* self); + static int init(PyUIArcObject* self, PyObject* args, PyObject* kwds); +}; + +// Method definitions +extern PyMethodDef UIArc_methods[]; + +namespace mcrfpydef { + static PyTypeObject PyUIArcType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.Arc", + .tp_basicsize = sizeof(PyUIArcObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) { + PyUIArcObject* obj = (PyUIArcObject*)self; + if (obj->weakreflist != NULL) { + PyObject_ClearWeakRefs(self); + } + obj->data.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_repr = (reprfunc)UIArc::repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = PyDoc_STR( + "Arc(center=None, radius=0, start_angle=0, end_angle=90, color=None, thickness=1, **kwargs)\n\n" + "An arc UI element for drawing curved line segments.\n\n" + "Args:\n" + " center (tuple, optional): Center position as (x, y). Default: (0, 0)\n" + " radius (float, optional): Arc radius in pixels. Default: 0\n" + " start_angle (float, optional): Starting angle in degrees. Default: 0\n" + " end_angle (float, optional): Ending angle in degrees. Default: 90\n" + " color (Color, optional): Arc color. Default: White\n" + " thickness (float, optional): Line thickness. Default: 1.0\n\n" + "Keyword Args:\n" + " click (callable): Click handler. Default: None\n" + " 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\n" + "Attributes:\n" + " center (Vector): Center position\n" + " radius (float): Arc radius\n" + " start_angle (float): Starting angle in degrees\n" + " end_angle (float): Ending angle in degrees\n" + " color (Color): Arc color\n" + " thickness (float): Line thickness\n" + " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" + " z_index (int): Rendering order\n" + " name (str): Element name\n" + ), + .tp_methods = UIArc_methods, + .tp_getset = UIArc::getsetters, + .tp_base = &mcrfpydef::PyDrawableType, + .tp_init = (initproc)UIArc::init, + .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { + PyUIArcObject* self = (PyUIArcObject*)type->tp_alloc(type, 0); + if (self) { + self->data = std::make_shared(); + self->weakreflist = nullptr; + } + return (PyObject*)self; + } + }; +} diff --git a/src/UICircle.cpp b/src/UICircle.cpp new file mode 100644 index 0000000..a8affb1 --- /dev/null +++ b/src/UICircle.cpp @@ -0,0 +1,490 @@ +#include "UICircle.h" +#include "GameEngine.h" +#include "McRFPy_API.h" +#include "PyVector.h" +#include "PyColor.h" +#include "PythonObjectCache.h" +#include + +UICircle::UICircle() + : radius(10.0f), + fill_color(sf::Color::White), + outline_color(sf::Color::Transparent), + outline_thickness(0.0f) +{ + position = sf::Vector2f(0.0f, 0.0f); + shape.setRadius(radius); + shape.setFillColor(fill_color); + shape.setOutlineColor(outline_color); + shape.setOutlineThickness(outline_thickness); + shape.setOrigin(radius, radius); // Center the origin +} + +UICircle::UICircle(float radius, sf::Vector2f center, sf::Color fillColor, + sf::Color outlineColor, float outlineThickness) + : radius(radius), + fill_color(fillColor), + outline_color(outlineColor), + outline_thickness(outlineThickness) +{ + position = center; + shape.setRadius(radius); + shape.setFillColor(fill_color); + shape.setOutlineColor(outline_color); + shape.setOutlineThickness(outline_thickness); + shape.setOrigin(radius, radius); // Center the origin +} + +UICircle::UICircle(const UICircle& other) + : UIDrawable(other), + radius(other.radius), + fill_color(other.fill_color), + outline_color(other.outline_color), + outline_thickness(other.outline_thickness) +{ + shape.setRadius(radius); + shape.setFillColor(fill_color); + shape.setOutlineColor(outline_color); + shape.setOutlineThickness(outline_thickness); + shape.setOrigin(radius, radius); +} + +UICircle& UICircle::operator=(const UICircle& other) { + if (this != &other) { + UIDrawable::operator=(other); + radius = other.radius; + fill_color = other.fill_color; + outline_color = other.outline_color; + outline_thickness = other.outline_thickness; + shape.setRadius(radius); + shape.setFillColor(fill_color); + shape.setOutlineColor(outline_color); + shape.setOutlineThickness(outline_thickness); + shape.setOrigin(radius, radius); + } + return *this; +} + +UICircle::UICircle(UICircle&& other) noexcept + : UIDrawable(std::move(other)), + shape(std::move(other.shape)), + radius(other.radius), + fill_color(other.fill_color), + outline_color(other.outline_color), + outline_thickness(other.outline_thickness) +{ +} + +UICircle& UICircle::operator=(UICircle&& other) noexcept { + if (this != &other) { + UIDrawable::operator=(std::move(other)); + shape = std::move(other.shape); + radius = other.radius; + fill_color = other.fill_color; + outline_color = other.outline_color; + outline_thickness = other.outline_thickness; + } + return *this; +} + +void UICircle::setRadius(float r) { + radius = r; + shape.setRadius(r); + shape.setOrigin(r, r); // Keep origin at center +} + +void UICircle::setFillColor(sf::Color c) { + fill_color = c; + shape.setFillColor(c); +} + +void UICircle::setOutlineColor(sf::Color c) { + outline_color = c; + shape.setOutlineColor(c); +} + +void UICircle::setOutline(float t) { + outline_thickness = t; + shape.setOutlineThickness(t); +} + +void UICircle::render(sf::Vector2f offset, sf::RenderTarget& target) { + if (!visible) return; + + // Apply position and offset + shape.setPosition(position + offset); + + // Apply opacity to colors + sf::Color render_fill = fill_color; + render_fill.a = static_cast(fill_color.a * opacity); + shape.setFillColor(render_fill); + + sf::Color render_outline = outline_color; + render_outline.a = static_cast(outline_color.a * opacity); + shape.setOutlineColor(render_outline); + + target.draw(shape); +} + +UIDrawable* UICircle::click_at(sf::Vector2f point) { + if (!click_callable) return nullptr; + + // Check if point is within the circle (including outline) + float dx = point.x - position.x; + float dy = point.y - position.y; + float distance = std::sqrt(dx * dx + dy * dy); + + float effective_radius = radius + outline_thickness; + if (distance <= effective_radius) { + return this; + } + + return nullptr; +} + +PyObjectsEnum UICircle::derived_type() { + return PyObjectsEnum::UICIRCLE; +} + +sf::FloatRect UICircle::get_bounds() const { + float effective_radius = radius + outline_thickness; + return sf::FloatRect( + position.x - effective_radius, + position.y - effective_radius, + effective_radius * 2, + effective_radius * 2 + ); +} + +void UICircle::move(float dx, float dy) { + position.x += dx; + position.y += dy; +} + +void UICircle::resize(float w, float h) { + // For circles, use the average of w and h as diameter + radius = (w + h) / 4.0f; // Average of w and h, then divide by 2 for radius + shape.setRadius(radius); + shape.setOrigin(radius, radius); +} + +// Property system for animations +bool UICircle::setProperty(const std::string& name, float value) { + if (name == "radius") { + setRadius(value); + return true; + } else if (name == "outline") { + setOutline(value); + return true; + } else if (name == "x") { + position.x = value; + return true; + } else if (name == "y") { + position.y = value; + return true; + } + return false; +} + +bool UICircle::setProperty(const std::string& name, const sf::Color& value) { + if (name == "fill_color") { + setFillColor(value); + return true; + } else if (name == "outline_color") { + setOutlineColor(value); + return true; + } + return false; +} + +bool UICircle::setProperty(const std::string& name, const sf::Vector2f& value) { + if (name == "center" || name == "position") { + position = value; + return true; + } + return false; +} + +bool UICircle::getProperty(const std::string& name, float& value) const { + if (name == "radius") { + value = radius; + return true; + } else if (name == "outline") { + value = outline_thickness; + return true; + } else if (name == "x") { + value = position.x; + return true; + } else if (name == "y") { + value = position.y; + return true; + } + return false; +} + +bool UICircle::getProperty(const std::string& name, sf::Color& value) const { + if (name == "fill_color") { + value = fill_color; + return true; + } else if (name == "outline_color") { + value = outline_color; + return true; + } + return false; +} + +bool UICircle::getProperty(const std::string& name, sf::Vector2f& value) const { + if (name == "center" || name == "position") { + value = position; + return true; + } + return false; +} + +// Python API implementations +PyObject* UICircle::get_radius(PyUICircleObject* self, void* closure) { + return PyFloat_FromDouble(self->data->getRadius()); +} + +int UICircle::set_radius(PyUICircleObject* self, PyObject* value, void* closure) { + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "radius must be a number"); + return -1; + } + self->data->setRadius(static_cast(PyFloat_AsDouble(value))); + return 0; +} + +PyObject* UICircle::get_center(PyUICircleObject* self, void* closure) { + sf::Vector2f center = self->data->getCenter(); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (!type) return NULL; + PyObject* result = PyObject_CallFunction((PyObject*)type, "ff", center.x, center.y); + Py_DECREF(type); + return result; +} + +int UICircle::set_center(PyUICircleObject* self, PyObject* value, void* closure) { + PyVectorObject* vec = PyVector::from_arg(value); + if (!vec) { + PyErr_Clear(); + PyErr_SetString(PyExc_TypeError, "center must be a Vector or tuple (x, y)"); + return -1; + } + self->data->setCenter(vec->data); + return 0; +} + +PyObject* UICircle::get_fill_color(PyUICircleObject* self, void* closure) { + sf::Color c = self->data->getFillColor(); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); + if (!type) return NULL; + PyObject* result = PyObject_CallFunction((PyObject*)type, "iiii", c.r, c.g, c.b, c.a); + Py_DECREF(type); + return result; +} + +int UICircle::set_fill_color(PyUICircleObject* self, PyObject* value, void* closure) { + sf::Color color; + if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) { + auto pyColor = (PyColorObject*)value; + color = pyColor->data; + } else if (PyTuple_Check(value)) { + int r, g, b, a = 255; + if (!PyArg_ParseTuple(value, "iii|i", &r, &g, &b, &a)) { + PyErr_SetString(PyExc_TypeError, "color tuple must be (r, g, b) or (r, g, b, a)"); + return -1; + } + color = sf::Color(r, g, b, a); + } else { + PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or tuple"); + return -1; + } + self->data->setFillColor(color); + return 0; +} + +PyObject* UICircle::get_outline_color(PyUICircleObject* self, void* closure) { + sf::Color c = self->data->getOutlineColor(); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); + if (!type) return NULL; + PyObject* result = PyObject_CallFunction((PyObject*)type, "iiii", c.r, c.g, c.b, c.a); + Py_DECREF(type); + return result; +} + +int UICircle::set_outline_color(PyUICircleObject* self, PyObject* value, void* closure) { + sf::Color color; + if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) { + auto pyColor = (PyColorObject*)value; + color = pyColor->data; + } else if (PyTuple_Check(value)) { + int r, g, b, a = 255; + if (!PyArg_ParseTuple(value, "iii|i", &r, &g, &b, &a)) { + PyErr_SetString(PyExc_TypeError, "color tuple must be (r, g, b) or (r, g, b, a)"); + return -1; + } + color = sf::Color(r, g, b, a); + } else { + PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or tuple"); + return -1; + } + self->data->setOutlineColor(color); + return 0; +} + +PyObject* UICircle::get_outline(PyUICircleObject* self, void* closure) { + return PyFloat_FromDouble(self->data->getOutline()); +} + +int UICircle::set_outline(PyUICircleObject* self, PyObject* value, void* closure) { + if (!PyNumber_Check(value)) { + PyErr_SetString(PyExc_TypeError, "outline must be a number"); + return -1; + } + self->data->setOutline(static_cast(PyFloat_AsDouble(value))); + return 0; +} + +// Required typedef for UIDRAWABLE_GETSETTERS and UIDRAWABLE_METHODS macro templates +typedef PyUICircleObject PyObjectType; + +PyGetSetDef UICircle::getsetters[] = { + {"radius", (getter)UICircle::get_radius, (setter)UICircle::set_radius, + "Circle radius in pixels", NULL}, + {"center", (getter)UICircle::get_center, (setter)UICircle::set_center, + "Center position of the circle", NULL}, + {"fill_color", (getter)UICircle::get_fill_color, (setter)UICircle::set_fill_color, + "Fill color of the circle", NULL}, + {"outline_color", (getter)UICircle::get_outline_color, (setter)UICircle::set_outline_color, + "Outline color of the circle", NULL}, + {"outline", (getter)UICircle::get_outline, (setter)UICircle::set_outline, + "Outline thickness (0 for no outline)", NULL}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, + "Callable executed when circle is clicked.", (void*)PyObjectsEnum::UICIRCLE}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, + "Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UICIRCLE}, + {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, + "Name for finding this element.", (void*)PyObjectsEnum::UICIRCLE}, + {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, + "Position as a Vector (same as center).", (void*)PyObjectsEnum::UICIRCLE}, + UIDRAWABLE_GETSETTERS, + {NULL} +}; + +PyMethodDef UICircle_methods[] = { + UIDRAWABLE_METHODS, + {NULL} +}; + +PyObject* UICircle::repr(PyUICircleObject* self) { + std::ostringstream oss; + auto& circle = self->data; + sf::Vector2f center = circle->getCenter(); + sf::Color fc = circle->getFillColor(); + oss << ""; + return PyUnicode_FromString(oss.str().c_str()); +} + +int UICircle::init(PyUICircleObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = { + "radius", "center", "fill_color", "outline_color", "outline", + "click", "visible", "opacity", "z_index", "name", NULL + }; + + float radius = 10.0f; + PyObject* center_obj = NULL; + PyObject* fill_color_obj = NULL; + PyObject* outline_color_obj = NULL; + float outline = 0.0f; + + // Common UIDrawable kwargs + PyObject* click_obj = NULL; + int visible = 1; + float opacity_val = 1.0f; + int z_index = 0; + const char* name = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|fOOOfOpfis", (char**)kwlist, + &radius, ¢er_obj, &fill_color_obj, &outline_color_obj, &outline, + &click_obj, &visible, &opacity_val, &z_index, &name)) { + return -1; + } + + // Set radius + self->data->setRadius(radius); + + // Set center if provided + if (center_obj && center_obj != Py_None) { + PyVectorObject* vec = PyVector::from_arg(center_obj); + if (!vec) { + PyErr_Clear(); + PyErr_SetString(PyExc_TypeError, "center must be a Vector or tuple (x, y)"); + return -1; + } + self->data->setCenter(vec->data); + } + + // Set fill color if provided + if (fill_color_obj && fill_color_obj != Py_None) { + sf::Color color; + if (PyObject_IsInstance(fill_color_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) { + color = ((PyColorObject*)fill_color_obj)->data; + } else if (PyTuple_Check(fill_color_obj)) { + int r, g, b, a = 255; + if (!PyArg_ParseTuple(fill_color_obj, "iii|i", &r, &g, &b, &a)) { + PyErr_SetString(PyExc_TypeError, "fill_color tuple must be (r, g, b) or (r, g, b, a)"); + return -1; + } + color = sf::Color(r, g, b, a); + } else { + PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or tuple"); + return -1; + } + self->data->setFillColor(color); + } + + // Set outline color if provided + if (outline_color_obj && outline_color_obj != Py_None) { + sf::Color color; + if (PyObject_IsInstance(outline_color_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) { + color = ((PyColorObject*)outline_color_obj)->data; + } else if (PyTuple_Check(outline_color_obj)) { + int r, g, b, a = 255; + if (!PyArg_ParseTuple(outline_color_obj, "iii|i", &r, &g, &b, &a)) { + PyErr_SetString(PyExc_TypeError, "outline_color tuple must be (r, g, b) or (r, g, b, a)"); + return -1; + } + color = sf::Color(r, g, b, a); + } else { + PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or tuple"); + return -1; + } + self->data->setOutlineColor(color); + } + + // Set outline thickness + self->data->setOutline(outline); + + // Handle common UIDrawable properties + if (click_obj && click_obj != Py_None) { + if (!PyCallable_Check(click_obj)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_obj); + } + + self->data->visible = (visible != 0); + self->data->opacity = opacity_val; + self->data->z_index = z_index; + + if (name) { + self->data->name = name; + } + + return 0; +} diff --git a/src/UICircle.h b/src/UICircle.h new file mode 100644 index 0000000..c84c1a5 --- /dev/null +++ b/src/UICircle.h @@ -0,0 +1,155 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include "structmember.h" +#include "UIDrawable.h" +#include "UIBase.h" +#include "PyDrawable.h" +#include "PyColor.h" +#include "PyVector.h" +#include "McRFPy_Doc.h" + +// Forward declaration +class UICircle; + +// Python object structure +typedef struct { + PyObject_HEAD + std::shared_ptr data; + PyObject* weakreflist; +} PyUICircleObject; + +class UICircle : public UIDrawable +{ +private: + sf::CircleShape shape; + float radius; + sf::Color fill_color; + sf::Color outline_color; + float outline_thickness; + +public: + UICircle(); + UICircle(float radius, sf::Vector2f center = sf::Vector2f(0, 0), + sf::Color fillColor = sf::Color::White, + sf::Color outlineColor = sf::Color::Transparent, + float outlineThickness = 0.0f); + + // Copy constructor and assignment + UICircle(const UICircle& other); + UICircle& operator=(const UICircle& other); + + // Move constructor and assignment + UICircle(UICircle&& other) noexcept; + UICircle& operator=(UICircle&& other) noexcept; + + // UIDrawable interface + void render(sf::Vector2f offset, sf::RenderTarget& target) override; + UIDrawable* click_at(sf::Vector2f point) override; + PyObjectsEnum derived_type() override; + + // Getters and setters + float getRadius() const { return radius; } + void setRadius(float r); + + sf::Vector2f getCenter() const { return position; } + void setCenter(sf::Vector2f c) { position = c; } + + sf::Color getFillColor() const { return fill_color; } + void setFillColor(sf::Color c); + + sf::Color getOutlineColor() const { return outline_color; } + void setOutlineColor(sf::Color c); + + float getOutline() const { return outline_thickness; } + void setOutline(float t); + + // Phase 1 virtual method implementations + sf::FloatRect get_bounds() const override; + void move(float dx, float dy) override; + void resize(float w, float h) override; + + // Property system for animations + bool setProperty(const std::string& name, float value) override; + bool setProperty(const std::string& name, const sf::Color& value) override; + bool setProperty(const std::string& name, const sf::Vector2f& value) override; + bool getProperty(const std::string& name, float& value) const override; + bool getProperty(const std::string& name, sf::Color& value) const override; + bool getProperty(const std::string& name, sf::Vector2f& value) const override; + + // Python API + static PyObject* get_radius(PyUICircleObject* self, void* closure); + static int set_radius(PyUICircleObject* self, PyObject* value, void* closure); + static PyObject* get_center(PyUICircleObject* self, void* closure); + static int set_center(PyUICircleObject* self, PyObject* value, void* closure); + static PyObject* get_fill_color(PyUICircleObject* self, void* closure); + static int set_fill_color(PyUICircleObject* self, PyObject* value, void* closure); + static PyObject* get_outline_color(PyUICircleObject* self, void* closure); + static int set_outline_color(PyUICircleObject* self, PyObject* value, void* closure); + static PyObject* get_outline(PyUICircleObject* self, void* closure); + static int set_outline(PyUICircleObject* self, PyObject* value, void* closure); + + static PyGetSetDef getsetters[]; + static PyObject* repr(PyUICircleObject* self); + static int init(PyUICircleObject* self, PyObject* args, PyObject* kwds); +}; + +// Method definitions +extern PyMethodDef UICircle_methods[]; + +namespace mcrfpydef { + static PyTypeObject PyUICircleType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.Circle", + .tp_basicsize = sizeof(PyUICircleObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) { + PyUICircleObject* obj = (PyUICircleObject*)self; + if (obj->weakreflist != NULL) { + PyObject_ClearWeakRefs(self); + } + obj->data.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_repr = (reprfunc)UICircle::repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = PyDoc_STR( + "Circle(radius=0, center=None, fill_color=None, outline_color=None, outline=0, **kwargs)\n\n" + "A circle UI element for drawing filled or outlined circles.\n\n" + "Args:\n" + " radius (float, optional): Circle radius in pixels. Default: 0\n" + " center (tuple, optional): Center position as (x, y). Default: (0, 0)\n" + " fill_color (Color, optional): Fill color. Default: White\n" + " outline_color (Color, optional): Outline color. Default: Transparent\n" + " outline (float, optional): Outline thickness. Default: 0 (no outline)\n\n" + "Keyword Args:\n" + " click (callable): Click handler. Default: None\n" + " 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\n" + "Attributes:\n" + " radius (float): Circle radius\n" + " center (Vector): Center position\n" + " fill_color (Color): Fill color\n" + " outline_color (Color): Outline color\n" + " outline (float): Outline thickness\n" + " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" + " z_index (int): Rendering order\n" + " name (str): Element name\n" + ), + .tp_methods = UICircle_methods, + .tp_getset = UICircle::getsetters, + .tp_base = &mcrfpydef::PyDrawableType, + .tp_init = (initproc)UICircle::init, + .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { + PyUICircleObject* self = (PyUICircleObject*)type->tp_alloc(type, 0); + if (self) { + self->data = std::make_shared(); + self->weakreflist = nullptr; + } + return (PyObject*)self; + } + }; +} diff --git a/src/UICollection.cpp b/src/UICollection.cpp index 5e749cb..7e91e41 100644 --- a/src/UICollection.cpp +++ b/src/UICollection.cpp @@ -4,6 +4,9 @@ #include "UICaption.h" #include "UISprite.h" #include "UIGrid.h" +#include "UILine.h" +#include "UICircle.h" +#include "UIArc.h" #include "McRFPy_API.h" #include "PyObjectUtils.h" #include "PythonObjectCache.h" @@ -79,6 +82,42 @@ static PyObject* convertDrawableToPython(std::shared_ptr drawable) { obj = (PyObject*)pyObj; break; } + case PyObjectsEnum::UILINE: + { + type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"); + if (!type) return nullptr; + auto pyObj = (PyUILineObject*)type->tp_alloc(type, 0); + if (pyObj) { + pyObj->data = std::static_pointer_cast(drawable); + pyObj->weakreflist = NULL; + } + obj = (PyObject*)pyObj; + break; + } + case PyObjectsEnum::UICIRCLE: + { + type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"); + if (!type) return nullptr; + auto pyObj = (PyUICircleObject*)type->tp_alloc(type, 0); + if (pyObj) { + pyObj->data = std::static_pointer_cast(drawable); + pyObj->weakreflist = NULL; + } + obj = (PyObject*)pyObj; + break; + } + case PyObjectsEnum::UIARC: + { + type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"); + if (!type) return nullptr; + auto pyObj = (PyUIArcObject*)type->tp_alloc(type, 0); + if (pyObj) { + pyObj->data = std::static_pointer_cast(drawable); + pyObj->weakreflist = NULL; + } + obj = (PyObject*)pyObj; + break; + } default: PyErr_SetString(PyExc_TypeError, "Unknown UIDrawable derived type"); return nullptr; @@ -577,10 +616,13 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o) if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && - !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) + !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) && + !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) && + !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) && + !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")) ) { - PyErr_SetString(PyExc_TypeError, "Only Frame, Caption, Sprite, and Grid objects can be added to UICollection"); + PyErr_SetString(PyExc_TypeError, "Only Frame, Caption, Sprite, Grid, Line, Circle, and Arc objects can be added to UICollection"); return NULL; } @@ -620,7 +662,25 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o) grid->data->z_index = new_z_index; self->data->push_back(grid->data); } - + if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"))) + { + PyUILineObject* line = (PyUILineObject*)o; + line->data->z_index = new_z_index; + self->data->push_back(line->data); + } + if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"))) + { + PyUICircleObject* circle = (PyUICircleObject*)o; + circle->data->z_index = new_z_index; + self->data->push_back(circle->data); + } + if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) + { + PyUIArcObject* arc = (PyUIArcObject*)o; + arc->data->z_index = new_z_index; + self->data->push_back(arc->data); + } + // Mark scene as needing resort after adding element McRFPy_API::markSceneNeedsSort(); @@ -656,11 +716,14 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable) if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && - !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) + !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) && + !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) && + !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) && + !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) { Py_DECREF(item); Py_DECREF(iterator); - PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, or Grid objects"); + PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, Grid, Line, Circle, or Arc objects"); return NULL; } @@ -692,10 +755,25 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable) grid->data->z_index = current_z_index; self->data->push_back(grid->data); } - + else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line"))) { + PyUILineObject* line = (PyUILineObject*)item; + line->data->z_index = current_z_index; + self->data->push_back(line->data); + } + else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle"))) { + PyUICircleObject* circle = (PyUICircleObject*)item; + circle->data->z_index = current_z_index; + self->data->push_back(circle->data); + } + else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) { + PyUIArcObject* arc = (PyUIArcObject*)item; + arc->data->z_index = current_z_index; + self->data->push_back(arc->data); + } + Py_DECREF(item); } - + Py_DECREF(iterator); // Check if iteration ended due to an error diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index 882b3c3..44bcfaf 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -3,6 +3,9 @@ #include "UICaption.h" #include "UISprite.h" #include "UIGrid.h" +#include "UILine.h" +#include "UICircle.h" +#include "UIArc.h" #include "GameEngine.h" #include "McRFPy_API.h" #include "PythonObjectCache.h" @@ -152,6 +155,24 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) { else ptr = NULL; break; + case PyObjectsEnum::UILINE: + if (((PyUILineObject*)self)->data->click_callable) + ptr = ((PyUILineObject*)self)->data->click_callable->borrow(); + else + ptr = NULL; + break; + case PyObjectsEnum::UICIRCLE: + if (((PyUICircleObject*)self)->data->click_callable) + ptr = ((PyUICircleObject*)self)->data->click_callable->borrow(); + else + ptr = NULL; + break; + case PyObjectsEnum::UIARC: + if (((PyUIArcObject*)self)->data->click_callable) + ptr = ((PyUIArcObject*)self)->data->click_callable->borrow(); + else + ptr = NULL; + break; default: PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click"); return NULL; @@ -179,6 +200,15 @@ int UIDrawable::set_click(PyObject* self, PyObject* value, void* closure) { case PyObjectsEnum::UIGRID: target = (((PyUIGridObject*)self)->data.get()); break; + case PyObjectsEnum::UILINE: + target = (((PyUILineObject*)self)->data.get()); + break; + case PyObjectsEnum::UICIRCLE: + target = (((PyUICircleObject*)self)->data.get()); + break; + case PyObjectsEnum::UIARC: + target = (((PyUIArcObject*)self)->data.get()); + break; default: PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _set_click"); return -1; @@ -215,18 +245,27 @@ PyObject* UIDrawable::get_int(PyObject* self, void* closure) { case PyObjectsEnum::UIGRID: drawable = ((PyUIGridObject*)self)->data.get(); break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); return NULL; } - + return PyLong_FromLong(drawable->z_index); } int UIDrawable::set_int(PyObject* self, PyObject* value, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); UIDrawable* drawable = nullptr; - + switch (objtype) { case PyObjectsEnum::UIFRAME: drawable = ((PyUIFrameObject*)self)->data.get(); @@ -240,11 +279,20 @@ int UIDrawable::set_int(PyObject* self, PyObject* value, void* closure) { 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"); return -1; @@ -283,7 +331,7 @@ void UIDrawable::notifyZIndexChanged() { PyObject* UIDrawable::get_name(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); UIDrawable* drawable = nullptr; - + switch (objtype) { case PyObjectsEnum::UIFRAME: drawable = ((PyUIFrameObject*)self)->data.get(); @@ -297,18 +345,27 @@ PyObject* UIDrawable::get_name(PyObject* self, void* closure) { case PyObjectsEnum::UIGRID: drawable = ((PyUIGridObject*)self)->data.get(); break; + case PyObjectsEnum::UILINE: + drawable = ((PyUILineObject*)self)->data.get(); + break; + case PyObjectsEnum::UICIRCLE: + drawable = ((PyUICircleObject*)self)->data.get(); + break; + case PyObjectsEnum::UIARC: + drawable = ((PyUIArcObject*)self)->data.get(); + break; default: PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); return NULL; } - + return PyUnicode_FromString(drawable->name.c_str()); } int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); UIDrawable* drawable = nullptr; - + switch (objtype) { case PyObjectsEnum::UIFRAME: drawable = ((PyUIFrameObject*)self)->data.get(); @@ -322,11 +379,20 @@ int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) { 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 = ""; return 0; @@ -383,7 +449,7 @@ PyObject* UIDrawable::get_float_member(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure) >> 8); int member = reinterpret_cast(closure) & 0xFF; UIDrawable* drawable = nullptr; - + switch (objtype) { case PyObjectsEnum::UIFRAME: drawable = ((PyUIFrameObject*)self)->data.get(); @@ -397,11 +463,20 @@ PyObject* UIDrawable::get_float_member(PyObject* self, void* closure) { 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 return PyFloat_FromDouble(drawable->position.x); @@ -421,7 +496,7 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure) PyObjectsEnum objtype = static_cast(reinterpret_cast(closure) >> 8); int member = reinterpret_cast(closure) & 0xFF; UIDrawable* drawable = nullptr; - + switch (objtype) { case PyObjectsEnum::UIFRAME: drawable = ((PyUIFrameObject*)self)->data.get(); @@ -435,11 +510,20 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure) 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)) { val = PyFloat_AsDouble(value); @@ -481,7 +565,7 @@ int UIDrawable::set_float_member(PyObject* self, PyObject* value, void* closure) PyObject* UIDrawable::get_pos(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); UIDrawable* drawable = nullptr; - + switch (objtype) { case PyObjectsEnum::UIFRAME: drawable = ((PyUIFrameObject*)self)->data.get(); @@ -495,11 +579,20 @@ PyObject* UIDrawable::get_pos(PyObject* self, void* closure) { 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"); if (!module) return NULL; @@ -519,7 +612,7 @@ PyObject* UIDrawable::get_pos(PyObject* self, void* closure) { int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); UIDrawable* drawable = nullptr; - + switch (objtype) { case PyObjectsEnum::UIFRAME: drawable = ((PyUIFrameObject*)self)->data.get(); @@ -533,11 +626,20 @@ int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) { 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; if (PyTuple_Check(value) && PyTuple_Size(value) == 2) { diff --git a/src/UIDrawable.h b/src/UIDrawable.h index 12a3ed0..12ddeee 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -21,7 +21,10 @@ enum PyObjectsEnum : int UIFRAME = 1, UICAPTION, UISPRITE, - UIGRID + UIGRID, + UILINE, + UICIRCLE, + UIARC }; class UIDrawable diff --git a/src/UILine.cpp b/src/UILine.cpp new file mode 100644 index 0000000..6b5718b --- /dev/null +++ b/src/UILine.cpp @@ -0,0 +1,561 @@ +#include "UILine.h" +#include "GameEngine.h" +#include "McRFPy_API.h" +#include "PyVector.h" +#include "PyColor.h" +#include "PythonObjectCache.h" +#include + +UILine::UILine() + : start_pos(0.0f, 0.0f), + end_pos(0.0f, 0.0f), + color(sf::Color::White), + thickness(1.0f), + vertices(sf::TriangleStrip, 4), + vertices_dirty(true) +{ + position = sf::Vector2f(0.0f, 0.0f); +} + +UILine::UILine(sf::Vector2f start, sf::Vector2f end, float thickness, sf::Color color) + : start_pos(start), + end_pos(end), + color(color), + thickness(thickness), + vertices(sf::TriangleStrip, 4), + vertices_dirty(true) +{ + // Set position to the midpoint for consistency with other UIDrawables + position = (start + end) / 2.0f; +} + +UILine::UILine(const UILine& other) + : UIDrawable(other), + start_pos(other.start_pos), + end_pos(other.end_pos), + color(other.color), + thickness(other.thickness), + vertices(sf::TriangleStrip, 4), + vertices_dirty(true) +{ +} + +UILine& UILine::operator=(const UILine& other) { + if (this != &other) { + UIDrawable::operator=(other); + start_pos = other.start_pos; + end_pos = other.end_pos; + color = other.color; + thickness = other.thickness; + vertices_dirty = true; + } + return *this; +} + +UILine::UILine(UILine&& other) noexcept + : UIDrawable(std::move(other)), + start_pos(other.start_pos), + end_pos(other.end_pos), + color(other.color), + thickness(other.thickness), + vertices(std::move(other.vertices)), + vertices_dirty(other.vertices_dirty) +{ +} + +UILine& UILine::operator=(UILine&& other) noexcept { + if (this != &other) { + UIDrawable::operator=(std::move(other)); + start_pos = other.start_pos; + end_pos = other.end_pos; + color = other.color; + thickness = other.thickness; + vertices = std::move(other.vertices); + vertices_dirty = other.vertices_dirty; + } + return *this; +} + +void UILine::updateVertices() const { + if (!vertices_dirty) return; + + // Calculate direction and perpendicular + sf::Vector2f direction = end_pos - start_pos; + float length = std::sqrt(direction.x * direction.x + direction.y * direction.y); + + if (length < 0.0001f) { + // Zero-length line - make a small dot + float half = thickness / 2.0f; + vertices[0].position = start_pos + sf::Vector2f(-half, -half); + vertices[1].position = start_pos + sf::Vector2f(half, -half); + vertices[2].position = start_pos + sf::Vector2f(-half, half); + vertices[3].position = start_pos + sf::Vector2f(half, half); + } else { + // Normalize direction + direction /= length; + + // Perpendicular vector + sf::Vector2f perpendicular(-direction.y, direction.x); + perpendicular *= thickness / 2.0f; + + // Create a quad (triangle strip) for the thick line + vertices[0].position = start_pos + perpendicular; + vertices[1].position = start_pos - perpendicular; + vertices[2].position = end_pos + perpendicular; + vertices[3].position = end_pos - perpendicular; + } + + // Set colors + for (int i = 0; i < 4; ++i) { + vertices[i].color = color; + } + + vertices_dirty = false; +} + +void UILine::render(sf::Vector2f offset, sf::RenderTarget& target) { + if (!visible) return; + + updateVertices(); + + // Apply opacity to color + sf::Color render_color = color; + render_color.a = static_cast(color.a * opacity); + + // Use ConvexShape to draw the line as a quad + sf::ConvexShape line_shape(4); + // Vertices are: 0=start+perp, 1=start-perp, 2=end+perp, 3=end-perp + // ConvexShape needs points in clockwise/counter-clockwise order + line_shape.setPoint(0, vertices[0].position + offset); // start + perp + line_shape.setPoint(1, vertices[2].position + offset); // end + perp + line_shape.setPoint(2, vertices[3].position + offset); // end - perp + line_shape.setPoint(3, vertices[1].position + offset); // start - perp + line_shape.setFillColor(render_color); + line_shape.setOutlineThickness(0); + + target.draw(line_shape); +} + +UIDrawable* UILine::click_at(sf::Vector2f point) { + if (!click_callable) return nullptr; + + // Check if point is close enough to the line + // Using a simple bounding box check plus distance-to-line calculation + sf::FloatRect bounds = get_bounds(); + bounds.left -= thickness; + bounds.top -= thickness; + bounds.width += thickness * 2; + bounds.height += thickness * 2; + + if (!bounds.contains(point)) return nullptr; + + // Calculate distance from point to line segment + sf::Vector2f line_vec = end_pos - start_pos; + sf::Vector2f point_vec = point - start_pos; + + float line_len_sq = line_vec.x * line_vec.x + line_vec.y * line_vec.y; + float t = 0.0f; + + if (line_len_sq > 0.0001f) { + t = std::max(0.0f, std::min(1.0f, + (point_vec.x * line_vec.x + point_vec.y * line_vec.y) / line_len_sq)); + } + + sf::Vector2f closest = start_pos + t * line_vec; + sf::Vector2f diff = point - closest; + float distance = std::sqrt(diff.x * diff.x + diff.y * diff.y); + + // Click is valid if within thickness + some margin + if (distance <= thickness / 2.0f + 2.0f) { + return this; + } + + return nullptr; +} + +PyObjectsEnum UILine::derived_type() { + return PyObjectsEnum::UILINE; +} + +sf::FloatRect UILine::get_bounds() const { + float min_x = std::min(start_pos.x, end_pos.x); + float min_y = std::min(start_pos.y, end_pos.y); + float max_x = std::max(start_pos.x, end_pos.x); + float max_y = std::max(start_pos.y, end_pos.y); + + return sf::FloatRect(min_x, min_y, max_x - min_x, max_y - min_y); +} + +void UILine::move(float dx, float dy) { + start_pos.x += dx; + start_pos.y += dy; + end_pos.x += dx; + end_pos.y += dy; + position.x += dx; + position.y += dy; + vertices_dirty = true; +} + +void UILine::resize(float w, float h) { + // For a line, resize adjusts the end point relative to start + end_pos = start_pos + sf::Vector2f(w, h); + vertices_dirty = true; +} + +// Animation property system +bool UILine::setProperty(const std::string& name, float value) { + if (name == "thickness") { + thickness = value; + vertices_dirty = true; + return true; + } + else if (name == "x") { + float dx = value - position.x; + move(dx, 0); + return true; + } + else if (name == "y") { + float dy = value - position.y; + move(0, dy); + return true; + } + else if (name == "start_x") { + start_pos.x = value; + vertices_dirty = true; + return true; + } + else if (name == "start_y") { + start_pos.y = value; + vertices_dirty = true; + return true; + } + else if (name == "end_x") { + end_pos.x = value; + vertices_dirty = true; + return true; + } + else if (name == "end_y") { + end_pos.y = value; + vertices_dirty = true; + return true; + } + return false; +} + +bool UILine::setProperty(const std::string& name, const sf::Color& value) { + if (name == "color") { + color = value; + vertices_dirty = true; + return true; + } + return false; +} + +bool UILine::setProperty(const std::string& name, const sf::Vector2f& value) { + if (name == "start") { + start_pos = value; + vertices_dirty = true; + return true; + } + else if (name == "end") { + end_pos = value; + vertices_dirty = true; + return true; + } + return false; +} + +bool UILine::getProperty(const std::string& name, float& value) const { + if (name == "thickness") { + value = thickness; + return true; + } + else if (name == "x") { + value = position.x; + return true; + } + else if (name == "y") { + value = position.y; + return true; + } + else if (name == "start_x") { + value = start_pos.x; + return true; + } + else if (name == "start_y") { + value = start_pos.y; + return true; + } + else if (name == "end_x") { + value = end_pos.x; + return true; + } + else if (name == "end_y") { + value = end_pos.y; + return true; + } + return false; +} + +bool UILine::getProperty(const std::string& name, sf::Color& value) const { + if (name == "color") { + value = color; + return true; + } + return false; +} + +bool UILine::getProperty(const std::string& name, sf::Vector2f& value) const { + if (name == "start") { + value = start_pos; + return true; + } + else if (name == "end") { + value = end_pos; + return true; + } + return false; +} + +// Python API implementation +PyObject* UILine::get_start(PyUILineObject* self, void* closure) { + auto vec = self->data->getStart(); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + auto obj = (PyVectorObject*)type->tp_alloc(type, 0); + Py_DECREF(type); + if (obj) { + obj->data = vec; + } + return (PyObject*)obj; +} + +int UILine::set_start(PyUILineObject* self, PyObject* value, void* closure) { + PyVectorObject* vec = PyVector::from_arg(value); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "start must be a Vector or tuple (x, y)"); + return -1; + } + self->data->setStart(vec->data); + return 0; +} + +PyObject* UILine::get_end(PyUILineObject* self, void* closure) { + auto vec = self->data->getEnd(); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + auto obj = (PyVectorObject*)type->tp_alloc(type, 0); + Py_DECREF(type); + if (obj) { + obj->data = vec; + } + return (PyObject*)obj; +} + +int UILine::set_end(PyUILineObject* self, PyObject* value, void* closure) { + PyVectorObject* vec = PyVector::from_arg(value); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "end must be a Vector or tuple (x, y)"); + return -1; + } + self->data->setEnd(vec->data); + return 0; +} + +PyObject* UILine::get_color(PyUILineObject* self, void* closure) { + auto color = self->data->getColor(); + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); + PyObject* args = Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a); + PyObject* obj = PyObject_CallObject((PyObject*)type, args); + Py_DECREF(args); + Py_DECREF(type); + return obj; +} + +int UILine::set_color(PyUILineObject* self, PyObject* value, void* closure) { + auto color = PyColor::from_arg(value); + if (!color) { + PyErr_SetString(PyExc_TypeError, "color must be a Color or tuple (r, g, b) or (r, g, b, a)"); + return -1; + } + self->data->setColor(color->data); + return 0; +} + +PyObject* UILine::get_thickness(PyUILineObject* self, void* closure) { + return PyFloat_FromDouble(self->data->getThickness()); +} + +int UILine::set_thickness(PyUILineObject* self, PyObject* value, void* closure) { + float thickness; + if (PyFloat_Check(value)) { + thickness = PyFloat_AsDouble(value); + } else if (PyLong_Check(value)) { + thickness = PyLong_AsLong(value); + } else { + PyErr_SetString(PyExc_TypeError, "thickness must be a number"); + return -1; + } + + if (thickness < 0.0f) { + PyErr_SetString(PyExc_ValueError, "thickness must be non-negative"); + return -1; + } + + self->data->setThickness(thickness); + return 0; +} + +// Define the Python type alias for macros +typedef PyUILineObject PyObjectType; + +// Method definitions +PyMethodDef UILine_methods[] = { + UIDRAWABLE_METHODS, + {NULL} // Sentinel +}; + +PyGetSetDef UILine::getsetters[] = { + {"start", (getter)UILine::get_start, (setter)UILine::set_start, + MCRF_PROPERTY(start, "Starting point of the line as a Vector."), NULL}, + {"end", (getter)UILine::get_end, (setter)UILine::set_end, + MCRF_PROPERTY(end, "Ending point of the line as a Vector."), NULL}, + {"color", (getter)UILine::get_color, (setter)UILine::set_color, + MCRF_PROPERTY(color, "Line color as a Color object."), NULL}, + {"thickness", (getter)UILine::get_thickness, (setter)UILine::set_thickness, + MCRF_PROPERTY(thickness, "Line thickness in pixels."), NULL}, + {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, + MCRF_PROPERTY(click, "Callable executed when line is clicked."), + (void*)PyObjectsEnum::UILINE}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, + MCRF_PROPERTY(z_index, "Z-order for rendering (lower values rendered first)."), + (void*)PyObjectsEnum::UILINE}, + {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, + MCRF_PROPERTY(name, "Name for finding this element."), + (void*)PyObjectsEnum::UILINE}, + {"pos", (getter)UIDrawable::get_pos, (setter)UIDrawable::set_pos, + MCRF_PROPERTY(pos, "Position as a Vector (midpoint of line)."), + (void*)PyObjectsEnum::UILINE}, + UIDRAWABLE_GETSETTERS, + {NULL} +}; + +PyObject* UILine::repr(PyUILineObject* self) { + std::ostringstream ss; + if (!self->data) { + ss << ""; + } else { + auto start = self->data->getStart(); + auto end = self->data->getEnd(); + auto color = self->data->getColor(); + ss << ""; + } + std::string repr_str = ss.str(); + return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); +} + +int UILine::init(PyUILineObject* self, PyObject* args, PyObject* kwds) { + // Arguments + PyObject* start_obj = nullptr; + PyObject* end_obj = nullptr; + float thickness = 1.0f; + PyObject* color_obj = nullptr; + PyObject* click_handler = nullptr; + int visible = 1; + float opacity = 1.0f; + int z_index = 0; + const char* name = nullptr; + + static const char* kwlist[] = { + "start", "end", "thickness", "color", + "click", "visible", "opacity", "z_index", "name", + nullptr + }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOfOOifiz", const_cast(kwlist), + &start_obj, &end_obj, &thickness, &color_obj, + &click_handler, &visible, &opacity, &z_index, &name)) { + return -1; + } + + // Parse start position + sf::Vector2f start(0.0f, 0.0f); + if (start_obj) { + PyVectorObject* vec = PyVector::from_arg(start_obj); + if (vec) { + start = vec->data; + } else { + PyErr_Clear(); + PyErr_SetString(PyExc_TypeError, "start must be a Vector or tuple (x, y)"); + return -1; + } + } + + // Parse end position + sf::Vector2f end(0.0f, 0.0f); + if (end_obj) { + PyVectorObject* vec = PyVector::from_arg(end_obj); + if (vec) { + end = vec->data; + } else { + PyErr_Clear(); + PyErr_SetString(PyExc_TypeError, "end must be a Vector or tuple (x, y)"); + return -1; + } + } + + // Parse color + sf::Color color = sf::Color::White; + if (color_obj && color_obj != Py_None) { + auto pycolor = PyColor::from_arg(color_obj); + if (pycolor) { + color = pycolor->data; + } else { + PyErr_Clear(); + PyErr_SetString(PyExc_TypeError, "color must be a Color or tuple"); + return -1; + } + } + + // Validate thickness + if (thickness < 0.0f) { + PyErr_SetString(PyExc_ValueError, "thickness must be non-negative"); + return -1; + } + + // Create the line + self->data = std::make_shared(start, end, thickness, color); + self->data->visible = visible; + self->data->opacity = opacity; + self->data->z_index = z_index; + + if (name) { + self->data->name = std::string(name); + } + + // Handle click handler + if (click_handler && click_handler != Py_None) { + if (!PyCallable_Check(click_handler)) { + PyErr_SetString(PyExc_TypeError, "click must be callable"); + return -1; + } + self->data->click_register(click_handler); + } + + // Initialize weak reference list + self->weakreflist = NULL; + + // Register in Python object cache + if (self->data->serial_number == 0) { + self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); + PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL); + if (weakref) { + PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref); + Py_DECREF(weakref); + } + } + + return 0; +} diff --git a/src/UILine.h b/src/UILine.h new file mode 100644 index 0000000..3b56847 --- /dev/null +++ b/src/UILine.h @@ -0,0 +1,150 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include "structmember.h" +#include "UIDrawable.h" +#include "UIBase.h" +#include "PyDrawable.h" +#include "PyColor.h" +#include "PyVector.h" +#include "McRFPy_Doc.h" + +// Forward declaration +class UILine; + +// Python object structure +typedef struct { + PyObject_HEAD + std::shared_ptr data; + PyObject* weakreflist; +} PyUILineObject; + +class UILine : public UIDrawable +{ +private: + sf::Vector2f start_pos; // Starting point + sf::Vector2f end_pos; // Ending point + sf::Color color; // Line color + float thickness; // Line thickness in pixels + + // Cached vertex array for rendering + mutable sf::VertexArray vertices; + mutable bool vertices_dirty; + + void updateVertices() const; + +public: + UILine(); + UILine(sf::Vector2f start, sf::Vector2f end, float thickness = 1.0f, sf::Color color = sf::Color::White); + + // Copy constructor and assignment + UILine(const UILine& other); + UILine& operator=(const UILine& other); + + // Move constructor and assignment + UILine(UILine&& other) noexcept; + UILine& operator=(UILine&& other) noexcept; + + // UIDrawable interface + void render(sf::Vector2f offset, sf::RenderTarget& target) override; + UIDrawable* click_at(sf::Vector2f point) override; + PyObjectsEnum derived_type() override; + + // Getters and setters + sf::Vector2f getStart() const { return start_pos; } + void setStart(sf::Vector2f pos) { start_pos = pos; vertices_dirty = true; } + + sf::Vector2f getEnd() const { return end_pos; } + void setEnd(sf::Vector2f pos) { end_pos = pos; vertices_dirty = true; } + + sf::Color getColor() const { return color; } + void setColor(sf::Color c) { color = c; vertices_dirty = true; } + + float getThickness() const { return thickness; } + void setThickness(float t) { thickness = t; vertices_dirty = true; } + + // Phase 1 virtual method implementations + sf::FloatRect get_bounds() const override; + void move(float dx, float dy) override; + void resize(float w, float h) override; + + // Property system for animations + bool setProperty(const std::string& name, float value) override; + bool setProperty(const std::string& name, const sf::Color& value) override; + bool setProperty(const std::string& name, const sf::Vector2f& value) override; + bool getProperty(const std::string& name, float& value) const override; + bool getProperty(const std::string& name, sf::Color& value) const override; + bool getProperty(const std::string& name, sf::Vector2f& value) const override; + + // Python API + static PyObject* get_start(PyUILineObject* self, void* closure); + static int set_start(PyUILineObject* self, PyObject* value, void* closure); + static PyObject* get_end(PyUILineObject* self, void* closure); + static int set_end(PyUILineObject* self, PyObject* value, void* closure); + static PyObject* get_color(PyUILineObject* self, void* closure); + static int set_color(PyUILineObject* self, PyObject* value, void* closure); + static PyObject* get_thickness(PyUILineObject* self, void* closure); + static int set_thickness(PyUILineObject* self, PyObject* value, void* closure); + + static PyGetSetDef getsetters[]; + static PyObject* repr(PyUILineObject* self); + static int init(PyUILineObject* self, PyObject* args, PyObject* kwds); +}; + +// Method definitions (extern to be defined in .cpp) +extern PyMethodDef UILine_methods[]; + +namespace mcrfpydef { + static PyTypeObject PyUILineType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.Line", + .tp_basicsize = sizeof(PyUILineObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) { + PyUILineObject* obj = (PyUILineObject*)self; + if (obj->weakreflist != NULL) { + PyObject_ClearWeakRefs(self); + } + obj->data.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_repr = (reprfunc)UILine::repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = PyDoc_STR( + "Line(start=None, end=None, thickness=1.0, color=None, **kwargs)\n\n" + "A line UI element for drawing straight lines between two points.\n\n" + "Args:\n" + " start (tuple, optional): Starting point as (x, y). Default: (0, 0)\n" + " end (tuple, optional): Ending point as (x, y). Default: (0, 0)\n" + " thickness (float, optional): Line thickness in pixels. Default: 1.0\n" + " color (Color, optional): Line color. Default: White\n\n" + "Keyword Args:\n" + " click (callable): Click handler. Default: None\n" + " 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\n" + "Attributes:\n" + " start (Vector): Starting point\n" + " end (Vector): Ending point\n" + " thickness (float): Line thickness\n" + " color (Color): Line color\n" + " visible (bool): Visibility state\n" + " opacity (float): Opacity value\n" + " z_index (int): Rendering order\n" + " name (str): Element name\n" + ), + .tp_methods = UILine_methods, + .tp_getset = UILine::getsetters, + .tp_base = &mcrfpydef::PyDrawableType, + .tp_init = (initproc)UILine::init, + .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { + PyUILineObject* self = (PyUILineObject*)type->tp_alloc(type, 0); + if (self) { + self->data = std::make_shared(); + self->weakreflist = nullptr; + } + return (PyObject*)self; + } + }; +}