feat: Add UILine, UICircle, and UIArc drawing primitives

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 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-11-25 21:42:33 -05:00
parent acef21593b
commit 311dc02f1d
10 changed files with 2261 additions and 28 deletions

View File

@ -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 <filesystem>
@ -251,6 +254,7 @@ PyObject* PyInit_mcrfpy()
/*UI widgets*/
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
&PyUILineType, &PyUICircleType, &PyUIArcType,
/*game map & perspective data*/
&PyUIGridPointType, &PyUIGridPointStateType,
@ -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];

520
src/UIArc.cpp Normal file
View File

@ -0,0 +1,520 @@
#include "UIArc.h"
#include "McRFPy_API.h"
#include <cmath>
#include <sstream>
#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<int>(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<sf::Uint8>(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<float>(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<float>(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<float>(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<float>(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 << "<Arc (invalid internal object)>";
} else {
auto center = self->data->getCenter();
auto color = self->data->getColor();
oss << "<Arc center=(" << center.x << ", " << center.y << ") "
<< "radius=" << self->data->getRadius() << " "
<< "angles=(" << self->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<char**>(kwlist),
&center_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;
}

167
src/UIArc.h Normal file
View File

@ -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<UIArc> 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<UIArc>();
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};
}

490
src/UICircle.cpp Normal file
View File

@ -0,0 +1,490 @@
#include "UICircle.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "PyVector.h"
#include "PyColor.h"
#include "PythonObjectCache.h"
#include <cmath>
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<sf::Uint8>(fill_color.a * opacity);
shape.setFillColor(render_fill);
sf::Color render_outline = outline_color;
render_outline.a = static_cast<sf::Uint8>(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<float>(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<float>(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 << "<Circle center=(" << center.x << ", " << center.y << ") "
<< "radius=" << circle->getRadius() << " "
<< "fill_color=(" << (int)fc.r << ", " << (int)fc.g << ", "
<< (int)fc.b << ", " << (int)fc.a << ")>";
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, &center_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;
}

155
src/UICircle.h Normal file
View File

@ -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<UICircle> 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<UICircle>();
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};
}

View File

@ -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<UIDrawable> 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<UILine>(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<UICircle>(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<UIArc>(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,6 +662,24 @@ 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,6 +755,21 @@ 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);
}

View File

@ -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,6 +245,15 @@ 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;
@ -240,6 +279,15 @@ 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;
@ -297,6 +345,15 @@ 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;
@ -322,6 +379,15 @@ 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;
@ -397,6 +463,15 @@ 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;
@ -435,6 +510,15 @@ 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;
@ -495,6 +579,15 @@ 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;
@ -533,6 +626,15 @@ 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;

View File

@ -21,7 +21,10 @@ enum PyObjectsEnum : int
UIFRAME = 1,
UICAPTION,
UISPRITE,
UIGRID
UIGRID,
UILINE,
UICIRCLE,
UIARC
};
class UIDrawable

561
src/UILine.cpp Normal file
View File

@ -0,0 +1,561 @@
#include "UILine.h"
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "PyVector.h"
#include "PyColor.h"
#include "PythonObjectCache.h"
#include <cmath>
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<sf::Uint8>(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 << "<Line (invalid internal object)>";
} else {
auto start = self->data->getStart();
auto end = self->data->getEnd();
auto color = self->data->getColor();
ss << "<Line start=(" << start.x << ", " << start.y << ") "
<< "end=(" << end.x << ", " << end.y << ") "
<< "thickness=" << self->data->getThickness() << " "
<< "color=(" << (int)color.r << ", " << (int)color.g << ", "
<< (int)color.b << ", " << (int)color.a << ")>";
}
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<char**>(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<UILine>(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;
}

150
src/UILine.h Normal file
View File

@ -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<UILine> 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<UILine>();
self->weakreflist = nullptr;
}
return (PyObject*)self;
}
};
}