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
commit 311dc02f1d
10 changed files with 2261 additions and 28 deletions

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