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:
parent
acef21593b
commit
311dc02f1d
10 changed files with 2261 additions and 28 deletions
561
src/UILine.cpp
Normal file
561
src/UILine.cpp
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue