Rotation
This commit is contained in:
parent
486087b9cb
commit
da434dcc64
14 changed files with 1076 additions and 60 deletions
|
|
@ -134,9 +134,13 @@ void UIArc::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
|||
rebuildVertices();
|
||||
}
|
||||
|
||||
// Apply offset by creating a transformed copy
|
||||
// Apply offset and rotation by creating a transform
|
||||
sf::Transform transform;
|
||||
transform.translate(offset);
|
||||
// Apply rotation around origin
|
||||
transform.translate(origin);
|
||||
transform.rotate(rotation);
|
||||
transform.translate(-origin);
|
||||
target.draw(vertices, transform);
|
||||
}
|
||||
|
||||
|
|
@ -146,9 +150,25 @@ UIDrawable* UIArc::click_at(sf::Vector2f point) {
|
|||
// #184: Also check for Python subclass (might have on_click method)
|
||||
if (!click_callable && !is_python_subclass) return nullptr;
|
||||
|
||||
// Calculate distance from center
|
||||
float dx = point.x - center.x;
|
||||
float dy = point.y - center.y;
|
||||
// Transform click point to local coordinates accounting for rotation
|
||||
sf::Vector2f localPoint;
|
||||
if (rotation != 0.0f) {
|
||||
// Build transform: rotate around origin (matches render transform)
|
||||
sf::Transform transform;
|
||||
transform.translate(origin);
|
||||
transform.rotate(rotation);
|
||||
transform.translate(-origin);
|
||||
|
||||
// Apply inverse transform to get local coordinates
|
||||
sf::Transform inverse = transform.getInverse();
|
||||
localPoint = inverse.transformPoint(point);
|
||||
} else {
|
||||
localPoint = point;
|
||||
}
|
||||
|
||||
// Calculate distance from center in local (unrotated) space
|
||||
float dx = localPoint.x - center.x;
|
||||
float dy = localPoint.y - center.y;
|
||||
float dist = std::sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Check if within the arc's radial range
|
||||
|
|
@ -249,6 +269,21 @@ bool UIArc::setProperty(const std::string& name, float value) {
|
|||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
rotation = value;
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
origin.x = value;
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
origin.y = value;
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -295,6 +330,18 @@ bool UIArc::getProperty(const std::string& name, float& value) const {
|
|||
value = center.y;
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
value = rotation;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
value = origin.x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
value = origin.y;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +364,8 @@ bool UIArc::getProperty(const std::string& name, sf::Vector2f& value) const {
|
|||
bool UIArc::hasProperty(const std::string& name) const {
|
||||
// Float properties
|
||||
if (name == "radius" || name == "start_angle" || name == "end_angle" ||
|
||||
name == "thickness" || name == "x" || name == "y") {
|
||||
name == "thickness" || name == "x" || name == "y" ||
|
||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||
return true;
|
||||
}
|
||||
// Color properties
|
||||
|
|
@ -453,6 +501,7 @@ PyGetSetDef UIArc::getsetters[] = {
|
|||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIARC),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIARC),
|
||||
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIARC),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
19
src/UIBase.h
19
src/UIBase.h
|
|
@ -282,6 +282,25 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
|
|||
"Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER)." \
|
||||
), (void*)type_enum}
|
||||
|
||||
// Rotation support - rotation angle and transform origin
|
||||
#define UIDRAWABLE_ROTATION_GETSETTERS(type_enum) \
|
||||
{"rotation", (getter)UIDrawable::get_rotation, (setter)UIDrawable::set_rotation, \
|
||||
MCRF_PROPERTY(rotation, \
|
||||
"Rotation angle in degrees (clockwise around origin). " \
|
||||
"Animatable property." \
|
||||
), (void*)type_enum}, \
|
||||
{"origin", (getter)UIDrawable::get_origin, (setter)UIDrawable::set_origin, \
|
||||
MCRF_PROPERTY(origin, \
|
||||
"Transform origin as Vector (pivot point for rotation). " \
|
||||
"Default (0,0) is top-left; set to (w/2, h/2) to rotate around center." \
|
||||
), (void*)type_enum}, \
|
||||
{"rotate_with_camera", (getter)UIDrawable::get_rotate_with_camera, (setter)UIDrawable::set_rotate_with_camera, \
|
||||
MCRF_PROPERTY(rotate_with_camera, \
|
||||
"Whether to rotate visually with parent Grid's camera_rotation (bool). " \
|
||||
"False (default): stay screen-aligned. True: tilt with camera. " \
|
||||
"Only affects children of UIGrid; ignored for other parents." \
|
||||
), (void*)type_enum}
|
||||
|
||||
// #106: Shader support - GPU-accelerated visual effects
|
||||
#define UIDRAWABLE_SHADER_GETSETTERS(type_enum) \
|
||||
{"shader", (getter)UIDrawable::get_shader, (setter)UIDrawable::set_shader, \
|
||||
|
|
|
|||
|
|
@ -25,11 +25,40 @@ UICaption::UICaption()
|
|||
UIDrawable* UICaption::click_at(sf::Vector2f point)
|
||||
{
|
||||
// #184: Also check for Python subclass (might have on_click method)
|
||||
if (click_callable || is_python_subclass)
|
||||
{
|
||||
if (text.getGlobalBounds().contains(point)) return this;
|
||||
if (!click_callable && !is_python_subclass) return nullptr;
|
||||
|
||||
// Get text dimensions from local bounds
|
||||
sf::FloatRect localBounds = text.getLocalBounds();
|
||||
float w = localBounds.width;
|
||||
float h = localBounds.height;
|
||||
// Account for text origin offset (SFML text has non-zero left/top in local bounds)
|
||||
float textOffsetX = localBounds.left;
|
||||
float textOffsetY = localBounds.top;
|
||||
|
||||
// Transform click point to local coordinates accounting for rotation
|
||||
sf::Vector2f localPoint;
|
||||
if (rotation != 0.0f) {
|
||||
// Build transform: translate to position, then rotate around origin
|
||||
sf::Transform transform;
|
||||
transform.translate(position);
|
||||
transform.translate(origin);
|
||||
transform.rotate(rotation);
|
||||
transform.translate(-origin);
|
||||
|
||||
// Apply inverse transform to get local coordinates
|
||||
sf::Transform inverse = transform.getInverse();
|
||||
localPoint = inverse.transformPoint(point);
|
||||
} else {
|
||||
// No rotation - simple subtraction
|
||||
localPoint = point - position;
|
||||
}
|
||||
return NULL;
|
||||
|
||||
// Check if local point is within bounds (accounting for text offset)
|
||||
if (localPoint.x >= textOffsetX && localPoint.y >= textOffsetY &&
|
||||
localPoint.x < textOffsetX + w && localPoint.y < textOffsetY + h) {
|
||||
return this;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||
|
|
@ -42,6 +71,10 @@ void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
color.a = static_cast<sf::Uint8>(255 * opacity);
|
||||
text.setFillColor(color);
|
||||
|
||||
// Apply rotation and origin
|
||||
text.setOrigin(origin);
|
||||
text.setRotation(rotation);
|
||||
|
||||
// #106: Shader rendering path
|
||||
if (shader && shader->shader) {
|
||||
// Get the text bounds for rendering
|
||||
|
|
@ -350,6 +383,7 @@ PyGetSetDef UICaption::getsetters[] = {
|
|||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -631,6 +665,24 @@ bool UICaption::setProperty(const std::string& name, float value) {
|
|||
markDirty(); // #144 - Z-order change affects parent
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
rotation = value;
|
||||
text.setRotation(rotation);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
origin.x = value;
|
||||
text.setOrigin(origin);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
origin.y = value;
|
||||
text.setOrigin(origin);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
if (setShaderProperty(name, value)) {
|
||||
return true;
|
||||
|
|
@ -714,6 +766,18 @@ bool UICaption::getProperty(const std::string& name, float& value) const {
|
|||
value = static_cast<float>(z_index);
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
value = rotation;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
value = origin.x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
value = origin.y;
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
if (getShaderProperty(name, value)) {
|
||||
return true;
|
||||
|
|
@ -748,7 +812,8 @@ bool UICaption::hasProperty(const std::string& name) const {
|
|||
name == "fill_color.r" || name == "fill_color.g" ||
|
||||
name == "fill_color.b" || name == "fill_color.a" ||
|
||||
name == "outline_color.r" || name == "outline_color.g" ||
|
||||
name == "outline_color.b" || name == "outline_color.a") {
|
||||
name == "outline_color.b" || name == "outline_color.a" ||
|
||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||
return true;
|
||||
}
|
||||
// Color properties
|
||||
|
|
@ -759,6 +824,10 @@ bool UICaption::hasProperty(const std::string& name) const {
|
|||
if (name == "text") {
|
||||
return true;
|
||||
}
|
||||
// Vector2f properties
|
||||
if (name == "origin") {
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
if (hasShaderProperty(name)) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -115,6 +115,12 @@ void UICircle::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
|||
// Apply position and offset
|
||||
shape.setPosition(position + offset);
|
||||
|
||||
// Apply rotation (using UIDrawable::origin as offset from circle center)
|
||||
// The shape already has its origin at center (radius, radius)
|
||||
// UIDrawable::origin provides additional offset from that center
|
||||
shape.setOrigin(radius + origin.x, radius + origin.y);
|
||||
shape.setRotation(rotation);
|
||||
|
||||
// Apply opacity to colors
|
||||
sf::Color render_fill = fill_color;
|
||||
render_fill.a = static_cast<sf::Uint8>(fill_color.a * opacity);
|
||||
|
|
@ -131,9 +137,30 @@ UIDrawable* UICircle::click_at(sf::Vector2f point) {
|
|||
// #184: Also check for Python subclass (might have on_click method)
|
||||
if (!click_callable && !is_python_subclass) return nullptr;
|
||||
|
||||
// Calculate the actual circle center accounting for rotation around origin
|
||||
// In render(), the circle is drawn at position with origin offset (radius + origin.x/y)
|
||||
// So the visual center moves when rotated around a non-default origin
|
||||
sf::Vector2f circleCenter = position;
|
||||
|
||||
if (rotation != 0.0f && (origin.x != 0.0f || origin.y != 0.0f)) {
|
||||
// The circle center in local space (relative to position) is at (0, 0)
|
||||
// With rotation around (origin.x, origin.y), the center moves
|
||||
float rad = rotation * 3.14159265f / 180.0f;
|
||||
float cos_r = std::cos(rad);
|
||||
float sin_r = std::sin(rad);
|
||||
|
||||
// Rotate (0,0) around origin
|
||||
float dx = -origin.x;
|
||||
float dy = -origin.y;
|
||||
float rotatedX = dx * cos_r - dy * sin_r + origin.x;
|
||||
float rotatedY = dx * sin_r + dy * cos_r + origin.y;
|
||||
|
||||
circleCenter = position + sf::Vector2f(rotatedX, rotatedY);
|
||||
}
|
||||
|
||||
// Check if point is within the circle (including outline)
|
||||
float dx = point.x - position.x;
|
||||
float dy = point.y - position.y;
|
||||
float dx = point.x - circleCenter.x;
|
||||
float dy = point.y - circleCenter.y;
|
||||
float distance = std::sqrt(dx * dx + dy * dy);
|
||||
|
||||
float effective_radius = radius + outline_thickness;
|
||||
|
|
@ -188,6 +215,21 @@ bool UICircle::setProperty(const std::string& name, float value) {
|
|||
position.y = value;
|
||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||
return true;
|
||||
} else if (name == "rotation") {
|
||||
rotation = value;
|
||||
shape.setRotation(rotation);
|
||||
markDirty();
|
||||
return true;
|
||||
} else if (name == "origin_x") {
|
||||
origin.x = value;
|
||||
shape.setOrigin(radius + origin.x, radius + origin.y);
|
||||
markDirty();
|
||||
return true;
|
||||
} else if (name == "origin_y") {
|
||||
origin.y = value;
|
||||
shape.setOrigin(radius + origin.x, radius + origin.y);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -227,6 +269,15 @@ bool UICircle::getProperty(const std::string& name, float& value) const {
|
|||
} else if (name == "y") {
|
||||
value = position.y;
|
||||
return true;
|
||||
} else if (name == "rotation") {
|
||||
value = rotation;
|
||||
return true;
|
||||
} else if (name == "origin_x") {
|
||||
value = origin.x;
|
||||
return true;
|
||||
} else if (name == "origin_y") {
|
||||
value = origin.y;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -253,7 +304,8 @@ bool UICircle::getProperty(const std::string& name, sf::Vector2f& value) const {
|
|||
bool UICircle::hasProperty(const std::string& name) const {
|
||||
// Float properties
|
||||
if (name == "radius" || name == "outline" ||
|
||||
name == "x" || name == "y") {
|
||||
name == "x" || name == "y" ||
|
||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||
return true;
|
||||
}
|
||||
// Color properties
|
||||
|
|
@ -261,7 +313,7 @@ bool UICircle::hasProperty(const std::string& name) const {
|
|||
return true;
|
||||
}
|
||||
// Vector2f properties
|
||||
if (name == "center" || name == "position") {
|
||||
if (name == "center" || name == "position" || name == "origin") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -399,6 +451,7 @@ PyGetSetDef UICircle::getsetters[] = {
|
|||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ UIDrawable::UIDrawable(const UIDrawable& other)
|
|||
: z_index(other.z_index),
|
||||
name(other.name),
|
||||
position(other.position),
|
||||
rotation(other.rotation),
|
||||
origin(other.origin),
|
||||
rotate_with_camera(other.rotate_with_camera),
|
||||
visible(other.visible),
|
||||
opacity(other.opacity),
|
||||
hovered(false), // Don't copy hover state
|
||||
|
|
@ -82,6 +85,9 @@ UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
|
|||
z_index = other.z_index;
|
||||
name = other.name;
|
||||
position = other.position;
|
||||
rotation = other.rotation;
|
||||
origin = other.origin;
|
||||
rotate_with_camera = other.rotate_with_camera;
|
||||
visible = other.visible;
|
||||
opacity = other.opacity;
|
||||
hovered = false; // Don't copy hover state
|
||||
|
|
@ -128,6 +134,9 @@ UIDrawable::UIDrawable(UIDrawable&& other) noexcept
|
|||
: z_index(other.z_index),
|
||||
name(std::move(other.name)),
|
||||
position(other.position),
|
||||
rotation(other.rotation),
|
||||
origin(other.origin),
|
||||
rotate_with_camera(other.rotate_with_camera),
|
||||
visible(other.visible),
|
||||
opacity(other.opacity),
|
||||
hovered(other.hovered),
|
||||
|
|
@ -157,6 +166,9 @@ UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept {
|
|||
z_index = other.z_index;
|
||||
name = std::move(other.name);
|
||||
position = other.position;
|
||||
rotation = other.rotation;
|
||||
origin = other.origin;
|
||||
rotate_with_camera = other.rotate_with_camera;
|
||||
visible = other.visible;
|
||||
opacity = other.opacity;
|
||||
hovered = other.hovered; // #140
|
||||
|
|
@ -589,6 +601,132 @@ int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Rotation property getter/setter
|
||||
PyObject* UIDrawable::get_rotation(PyObject* self, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
return PyFloat_FromDouble(drawable->rotation);
|
||||
}
|
||||
|
||||
int UIDrawable::set_rotation(PyObject* self, PyObject* value, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
float val = 0.0f;
|
||||
if (PyFloat_Check(value)) {
|
||||
val = PyFloat_AsDouble(value);
|
||||
} else if (PyLong_Check(value)) {
|
||||
val = static_cast<float>(PyLong_AsLong(value));
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "rotation must be a number (int or float)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
drawable->rotation = val;
|
||||
drawable->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Origin property getter/setter
|
||||
PyObject* UIDrawable::get_origin(PyObject* self, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
// Create a Python Vector object from origin
|
||||
PyObject* module = PyImport_ImportModule("mcrfpy");
|
||||
if (!module) return NULL;
|
||||
|
||||
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
|
||||
Py_DECREF(module);
|
||||
if (!vector_type) return NULL;
|
||||
|
||||
PyObject* args = Py_BuildValue("(ff)", drawable->origin.x, drawable->origin.y);
|
||||
PyObject* result = PyObject_CallObject(vector_type, args);
|
||||
Py_DECREF(vector_type);
|
||||
Py_DECREF(args);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int UIDrawable::set_origin(PyObject* self, PyObject* value, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
// Accept tuple or Vector
|
||||
float x, y;
|
||||
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(value, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(value, 1);
|
||||
|
||||
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
|
||||
x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "origin x must be a number");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
|
||||
y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "origin y must be a number");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
// Try to get as Vector
|
||||
PyObject* module = PyImport_ImportModule("mcrfpy");
|
||||
if (!module) return -1;
|
||||
|
||||
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
|
||||
Py_DECREF(module);
|
||||
if (!vector_type) return -1;
|
||||
|
||||
int is_vector = PyObject_IsInstance(value, vector_type);
|
||||
Py_DECREF(vector_type);
|
||||
|
||||
if (is_vector) {
|
||||
PyVectorObject* vec = (PyVectorObject*)value;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "origin must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
drawable->origin = sf::Vector2f(x, y);
|
||||
drawable->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// rotate_with_camera property getter/setter
|
||||
PyObject* UIDrawable::get_rotate_with_camera(PyObject* self, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return NULL;
|
||||
|
||||
return PyBool_FromLong(drawable->rotate_with_camera);
|
||||
}
|
||||
|
||||
int UIDrawable::set_rotate_with_camera(PyObject* self, PyObject* value, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||
if (!drawable) return -1;
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "rotate_with_camera must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
drawable->rotate_with_camera = PyObject_IsTrue(value);
|
||||
drawable->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// #221 - Grid coordinate properties (only valid when parent is UIGrid)
|
||||
PyObject* UIDrawable::get_grid_pos(PyObject* self, void* closure) {
|
||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||
|
|
|
|||
|
|
@ -99,6 +99,14 @@ public:
|
|||
static PyObject* get_pos(PyObject* self, void* closure);
|
||||
static int set_pos(PyObject* self, PyObject* value, void* closure);
|
||||
|
||||
// Rotation getters/setters for Python API
|
||||
static PyObject* get_rotation(PyObject* self, void* closure);
|
||||
static int set_rotation(PyObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_origin(PyObject* self, void* closure);
|
||||
static int set_origin(PyObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_rotate_with_camera(PyObject* self, void* closure);
|
||||
static int set_rotate_with_camera(PyObject* self, PyObject* value, void* closure);
|
||||
|
||||
// #221 - Grid coordinate properties (only valid when parent is UIGrid)
|
||||
static PyObject* get_grid_pos(PyObject* self, void* closure);
|
||||
static int set_grid_pos(PyObject* self, PyObject* value, void* closure);
|
||||
|
|
@ -117,6 +125,16 @@ public:
|
|||
// Position in pixel coordinates (moved from derived classes)
|
||||
sf::Vector2f position;
|
||||
|
||||
// Rotation in degrees (clockwise around origin)
|
||||
float rotation = 0.0f;
|
||||
|
||||
// Transform origin point (relative to position, pivot for rotation/scale)
|
||||
sf::Vector2f origin;
|
||||
|
||||
// Whether to rotate visually with parent Grid's camera_rotation
|
||||
// Only affects children of UIGrid; ignored for other parents
|
||||
bool rotate_with_camera = false;
|
||||
|
||||
// Parent-child hierarchy (#122)
|
||||
std::weak_ptr<UIDrawable> parent;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,30 @@
|
|||
|
||||
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||
{
|
||||
// Check bounds first (optimization)
|
||||
float x = position.x, y = position.y, w = box.getSize().x, h = box.getSize().y;
|
||||
if (point.x < x || point.y < y || point.x >= x+w || point.y >= y+h) {
|
||||
return nullptr;
|
||||
float w = box.getSize().x, h = box.getSize().y;
|
||||
|
||||
// Transform click point to local coordinates accounting for rotation
|
||||
sf::Vector2f localPoint;
|
||||
if (rotation != 0.0f) {
|
||||
// Build transform: translate to position, then rotate around origin
|
||||
sf::Transform transform;
|
||||
transform.translate(position);
|
||||
transform.translate(origin);
|
||||
transform.rotate(rotation);
|
||||
transform.translate(-origin);
|
||||
|
||||
// Apply inverse transform to get local coordinates
|
||||
sf::Transform inverse = transform.getInverse();
|
||||
localPoint = inverse.transformPoint(point);
|
||||
} else {
|
||||
// No rotation - simple subtraction
|
||||
localPoint = point - position;
|
||||
}
|
||||
|
||||
// Transform to local coordinates for children
|
||||
sf::Vector2f localPoint = point - position;
|
||||
// Check if local point is within bounds (0,0 to w,h in local space)
|
||||
if (localPoint.x < 0 || localPoint.y < 0 || localPoint.x >= w || localPoint.y >= h) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check children in reverse order (top to bottom, highest z-index first)
|
||||
for (auto it = children->rbegin(); it != children->rend(); ++it) {
|
||||
|
|
@ -140,8 +156,10 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
// Clear the RenderTexture
|
||||
render_texture->clear(sf::Color::Transparent);
|
||||
|
||||
// Draw the frame box to RenderTexture
|
||||
// Draw the frame box to RenderTexture (without rotation - that's applied to the final sprite)
|
||||
box.setPosition(0, 0); // Render at origin in texture
|
||||
box.setOrigin(0, 0); // No origin offset in texture
|
||||
box.setRotation(0); // No rotation in texture
|
||||
render_texture->draw(box);
|
||||
|
||||
// Sort children by z_index if needed
|
||||
|
|
@ -172,6 +190,10 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
// Use `position` instead of box.getPosition() - box was set to (0,0) for texture rendering
|
||||
render_sprite.setPosition(offset + position);
|
||||
|
||||
// Apply rotation to the rendered sprite (children rotate with parent)
|
||||
render_sprite.setOrigin(origin);
|
||||
render_sprite.setRotation(rotation);
|
||||
|
||||
// #106: Apply shader if set
|
||||
if (shader && shader->shader) {
|
||||
// Apply engine uniforms (time, resolution, mouse, texture)
|
||||
|
|
@ -193,6 +215,8 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
// Standard rendering without caching
|
||||
// Restore box position from `position` - may have been set to (0,0) by previous texture render
|
||||
box.setPosition(offset + position);
|
||||
box.setOrigin(origin);
|
||||
box.setRotation(rotation);
|
||||
target.draw(box);
|
||||
box.setPosition(position); // Restore to canonical position
|
||||
|
||||
|
|
@ -205,6 +229,9 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
children_need_sort = false;
|
||||
}
|
||||
|
||||
// Render children - note: in non-texture mode, children don't automatically
|
||||
// rotate with parent. Use clip_children=True or cache_subtree=True if you need
|
||||
// children to rotate with the frame.
|
||||
for (auto drawable : *children) {
|
||||
drawable->render(offset + position, target); // Use `position` as source of truth
|
||||
}
|
||||
|
|
@ -512,6 +539,7 @@ PyGetSetDef UIFrame::getsetters[] = {
|
|||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -856,6 +884,21 @@ bool UIFrame::setProperty(const std::string& name, float value) {
|
|||
box.setOutlineColor(color);
|
||||
markDirty();
|
||||
return true;
|
||||
} else if (name == "rotation") {
|
||||
rotation = value;
|
||||
box.setRotation(rotation);
|
||||
markDirty();
|
||||
return true;
|
||||
} else if (name == "origin_x") {
|
||||
origin.x = value;
|
||||
box.setOrigin(origin);
|
||||
markDirty();
|
||||
return true;
|
||||
} else if (name == "origin_y") {
|
||||
origin.y = value;
|
||||
box.setOrigin(origin);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
if (setShaderProperty(name, value)) {
|
||||
|
|
@ -892,6 +935,11 @@ bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
|
|||
}
|
||||
markDirty();
|
||||
return true;
|
||||
} else if (name == "origin") {
|
||||
origin = value;
|
||||
box.setOrigin(origin);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -936,6 +984,15 @@ bool UIFrame::getProperty(const std::string& name, float& value) const {
|
|||
} else if (name == "outline_color.a") {
|
||||
value = box.getOutlineColor().a;
|
||||
return true;
|
||||
} else if (name == "rotation") {
|
||||
value = rotation;
|
||||
return true;
|
||||
} else if (name == "origin_x") {
|
||||
value = origin.x;
|
||||
return true;
|
||||
} else if (name == "origin_y") {
|
||||
value = origin.y;
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
if (getShaderProperty(name, value)) {
|
||||
|
|
@ -962,6 +1019,9 @@ bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const {
|
|||
} else if (name == "size") {
|
||||
value = box.getSize();
|
||||
return true;
|
||||
} else if (name == "origin") {
|
||||
value = origin;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -973,7 +1033,8 @@ bool UIFrame::hasProperty(const std::string& name) const {
|
|||
name == "fill_color.r" || name == "fill_color.g" ||
|
||||
name == "fill_color.b" || name == "fill_color.a" ||
|
||||
name == "outline_color.r" || name == "outline_color.g" ||
|
||||
name == "outline_color.b" || name == "outline_color.a") {
|
||||
name == "outline_color.b" || name == "outline_color.a" ||
|
||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||
return true;
|
||||
}
|
||||
// Color properties
|
||||
|
|
@ -981,7 +1042,7 @@ bool UIFrame::hasProperty(const std::string& name) const {
|
|||
return true;
|
||||
}
|
||||
// Vector2f properties
|
||||
if (name == "position" || name == "size") {
|
||||
if (name == "position" || name == "size" || name == "origin") {
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
|
|
|
|||
174
src/UIGrid.cpp
174
src/UIGrid.cpp
|
|
@ -145,28 +145,59 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
|
||||
// TODO: Apply opacity to output sprite
|
||||
|
||||
output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing
|
||||
// output size can change; update size when drawing
|
||||
output.setTextureRect(
|
||||
sf::IntRect(0, 0,
|
||||
box.getSize().x, box.getSize().y));
|
||||
renderTexture.clear(fill_color);
|
||||
|
||||
// Get cell dimensions - use texture if available, otherwise defaults
|
||||
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||
|
||||
// sprites that are visible according to zoom, center_x, center_y, and box width
|
||||
// Determine if we need camera rotation handling
|
||||
bool has_camera_rotation = (camera_rotation != 0.0f);
|
||||
float grid_w_px = box.getSize().x;
|
||||
float grid_h_px = box.getSize().y;
|
||||
|
||||
// Calculate AABB for rotated view (if camera rotation is active)
|
||||
float rad = camera_rotation * (M_PI / 180.0f);
|
||||
float cos_r = std::cos(rad);
|
||||
float sin_r = std::sin(rad);
|
||||
float abs_cos = std::abs(cos_r);
|
||||
float abs_sin = std::abs(sin_r);
|
||||
|
||||
// AABB dimensions of the rotated viewport
|
||||
float aabb_w = grid_w_px * abs_cos + grid_h_px * abs_sin;
|
||||
float aabb_h = grid_w_px * abs_sin + grid_h_px * abs_cos;
|
||||
|
||||
// Choose which texture to render to
|
||||
sf::RenderTexture* activeTexture = &renderTexture;
|
||||
|
||||
if (has_camera_rotation) {
|
||||
// Ensure rotation texture is large enough
|
||||
unsigned int needed_size = static_cast<unsigned int>(std::max(aabb_w, aabb_h) + 1);
|
||||
if (rotationTextureSize < needed_size) {
|
||||
rotationTexture.create(needed_size, needed_size);
|
||||
rotationTextureSize = needed_size;
|
||||
}
|
||||
activeTexture = &rotationTexture;
|
||||
activeTexture->clear(fill_color);
|
||||
} else {
|
||||
output.setPosition(box.getPosition() + offset);
|
||||
output.setTextureRect(sf::IntRect(0, 0, grid_w_px, grid_h_px));
|
||||
renderTexture.clear(fill_color);
|
||||
}
|
||||
|
||||
// Calculate visible tile range
|
||||
// For camera rotation, use AABB dimensions; otherwise use grid dimensions
|
||||
float render_w = has_camera_rotation ? aabb_w : grid_w_px;
|
||||
float render_h = has_camera_rotation ? aabb_h : grid_h_px;
|
||||
|
||||
float center_x_sq = center_x / cell_width;
|
||||
float center_y_sq = center_y / cell_height;
|
||||
|
||||
float width_sq = box.getSize().x / (cell_width * zoom);
|
||||
float height_sq = box.getSize().y / (cell_height * zoom);
|
||||
float width_sq = render_w / (cell_width * zoom);
|
||||
float height_sq = render_h / (cell_height * zoom);
|
||||
float left_edge = center_x_sq - (width_sq / 2.0);
|
||||
float top_edge = center_y_sq - (height_sq / 2.0);
|
||||
|
||||
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
|
||||
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
|
||||
int left_spritepixels = center_x - (render_w / 2.0 / zoom);
|
||||
int top_spritepixels = center_y - (render_h / 2.0 / zoom);
|
||||
|
||||
int x_limit = left_edge + width_sq + 2;
|
||||
if (x_limit > grid_w) x_limit = grid_w;
|
||||
|
|
@ -179,7 +210,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
sortLayers();
|
||||
for (auto& layer : layers) {
|
||||
if (layer->z_index >= 0) break; // Stop at layers that go above entities
|
||||
layer->render(renderTexture, left_spritepixels, top_spritepixels,
|
||||
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||
}
|
||||
|
||||
|
|
@ -205,9 +236,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
auto pixel_pos = sf::Vector2f(
|
||||
(e->position.x*cell_width - left_spritepixels) * zoom,
|
||||
(e->position.y*cell_height - top_spritepixels) * zoom );
|
||||
//drawent.setPosition(pixel_pos);
|
||||
//renderTexture.draw(drawent);
|
||||
drawent.render(pixel_pos, renderTexture);
|
||||
drawent.render(pixel_pos, *activeTexture);
|
||||
|
||||
entitiesRendered++;
|
||||
}
|
||||
|
|
@ -220,7 +249,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
// #147 - Render dynamic layers with z_index >= 0 (above entities)
|
||||
for (auto& layer : layers) {
|
||||
if (layer->z_index < 0) continue; // Skip layers below entities
|
||||
layer->render(renderTexture, left_spritepixels, top_spritepixels,
|
||||
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +281,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
(child->position.y - top_spritepixels) * zoom
|
||||
);
|
||||
|
||||
child->render(pixel_pos, renderTexture);
|
||||
child->render(pixel_pos, *activeTexture);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -294,11 +323,11 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
if (!state.discovered) {
|
||||
// Never seen - black
|
||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||
renderTexture.draw(overlay);
|
||||
activeTexture->draw(overlay);
|
||||
} else if (!state.visible) {
|
||||
// Discovered but not currently visible - dark gray
|
||||
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
||||
renderTexture.draw(overlay);
|
||||
activeTexture->draw(overlay);
|
||||
}
|
||||
// If visible and discovered, no overlay (fully visible)
|
||||
}
|
||||
|
|
@ -324,7 +353,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
|
||||
overlay.setPosition(pixel_pos);
|
||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||
renderTexture.draw(overlay);
|
||||
activeTexture->draw(overlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -351,8 +380,51 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
renderTexture.draw(lineb, 2, sf::Lines);
|
||||
*/
|
||||
|
||||
// render to window
|
||||
renderTexture.display();
|
||||
// Finalize the active texture
|
||||
activeTexture->display();
|
||||
|
||||
// If camera rotation was used, rotate and blit to the grid's renderTexture
|
||||
if (has_camera_rotation) {
|
||||
// Clear the final renderTexture with fill color
|
||||
renderTexture.clear(fill_color);
|
||||
|
||||
// Create sprite from the larger rotated texture
|
||||
sf::Sprite rotatedSprite(rotationTexture.getTexture());
|
||||
|
||||
// Set origin to center of the rendered content
|
||||
float tex_center_x = aabb_w / 2.0f;
|
||||
float tex_center_y = aabb_h / 2.0f;
|
||||
rotatedSprite.setOrigin(tex_center_x, tex_center_y);
|
||||
|
||||
// Apply rotation
|
||||
rotatedSprite.setRotation(camera_rotation);
|
||||
|
||||
// Position so the rotated center lands at the viewport center
|
||||
rotatedSprite.setPosition(grid_w_px / 2.0f, grid_h_px / 2.0f);
|
||||
|
||||
// Set texture rect to only use the AABB portion (texture may be larger)
|
||||
rotatedSprite.setTextureRect(sf::IntRect(0, 0, static_cast<int>(aabb_w), static_cast<int>(aabb_h)));
|
||||
|
||||
// Draw to the grid's renderTexture (which clips to grid bounds)
|
||||
renderTexture.draw(rotatedSprite);
|
||||
renderTexture.display();
|
||||
|
||||
// Set up output sprite
|
||||
output.setPosition(box.getPosition() + offset);
|
||||
output.setTextureRect(sf::IntRect(0, 0, grid_w_px, grid_h_px));
|
||||
}
|
||||
|
||||
// Apply viewport rotation (UIDrawable::rotation) to the entire grid widget
|
||||
if (rotation != 0.0f) {
|
||||
output.setOrigin(origin);
|
||||
output.setRotation(rotation);
|
||||
// Adjust position to account for origin offset
|
||||
output.setPosition(box.getPosition() + offset + origin);
|
||||
} else {
|
||||
output.setOrigin(0, 0);
|
||||
output.setRotation(0);
|
||||
// Position already set above
|
||||
}
|
||||
|
||||
// #106: Apply shader if set
|
||||
if (shader && shader->shader) {
|
||||
|
|
@ -1046,6 +1118,8 @@ PyObject* UIGrid::get_float_member(PyUIGridObject* self, void* closure)
|
|||
return PyFloat_FromDouble(self->data->center_y);
|
||||
else if (member_ptr == 6) // zoom
|
||||
return PyFloat_FromDouble(self->data->zoom);
|
||||
else if (member_ptr == 7) // camera_rotation
|
||||
return PyFloat_FromDouble(self->data->camera_rotation);
|
||||
else
|
||||
{
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
|
||||
|
|
@ -1100,6 +1174,8 @@ int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closur
|
|||
self->data->center_y = val;
|
||||
else if (member_ptr == 6) // zoom
|
||||
self->data->zoom = val;
|
||||
else if (member_ptr == 7) // camera_rotation
|
||||
self->data->camera_rotation = val;
|
||||
return 0;
|
||||
}
|
||||
// TODO (7DRL Day 2, item 5.) return Texture object
|
||||
|
|
@ -2206,6 +2282,7 @@ PyGetSetDef UIGrid::getsetters[] = {
|
|||
{"center_x", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view X-coordinate", (void*)4},
|
||||
{"center_y", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view Y-coordinate", (void*)5},
|
||||
{"zoom", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "zoom factor for displaying the Grid", (void*)6},
|
||||
{"camera_rotation", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "Rotation of grid contents around camera center (degrees). The grid widget stays axis-aligned; only the view into the world rotates.", (void*)7},
|
||||
|
||||
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||
MCRF_PROPERTY(on_click,
|
||||
|
|
@ -2237,6 +2314,7 @@ PyGetSetDef UIGrid::getsetters[] = {
|
|||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||
// #142 - Grid cell mouse events
|
||||
{"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter,
|
||||
"Callback when mouse enters a grid cell. Called with (cell_pos: Vector).", NULL},
|
||||
|
|
@ -2507,6 +2585,26 @@ bool UIGrid::setProperty(const std::string& name, float value) {
|
|||
markDirty(); // #144 - View change affects content
|
||||
return true;
|
||||
}
|
||||
else if (name == "camera_rotation") {
|
||||
camera_rotation = value;
|
||||
markDirty(); // View rotation affects content
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
rotation = value;
|
||||
markCompositeDirty(); // Viewport rotation doesn't affect internal content
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
origin.x = value;
|
||||
markCompositeDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
origin.y = value;
|
||||
markCompositeDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
z_index = static_cast<int>(value);
|
||||
markDirty(); // #144 - Z-order change affects parent
|
||||
|
|
@ -2559,6 +2657,11 @@ bool UIGrid::setProperty(const std::string& name, const sf::Vector2f& value) {
|
|||
markDirty(); // #144 - View change affects content
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin") {
|
||||
origin = value;
|
||||
markCompositeDirty();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -2591,6 +2694,22 @@ bool UIGrid::getProperty(const std::string& name, float& value) const {
|
|||
value = zoom;
|
||||
return true;
|
||||
}
|
||||
else if (name == "camera_rotation") {
|
||||
value = camera_rotation;
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
value = rotation;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
value = origin.x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
value = origin.y;
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
value = static_cast<float>(z_index);
|
||||
return true;
|
||||
|
|
@ -2631,6 +2750,10 @@ bool UIGrid::getProperty(const std::string& name, sf::Vector2f& value) const {
|
|||
value = sf::Vector2f(center_x, center_y);
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin") {
|
||||
value = origin;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -2639,13 +2762,14 @@ bool UIGrid::hasProperty(const std::string& name) const {
|
|||
if (name == "x" || name == "y" ||
|
||||
name == "w" || name == "h" || name == "width" || name == "height" ||
|
||||
name == "center_x" || name == "center_y" || name == "zoom" ||
|
||||
name == "z_index" ||
|
||||
name == "camera_rotation" || name == "rotation" ||
|
||||
name == "origin_x" || name == "origin_y" || name == "z_index" ||
|
||||
name == "fill_color.r" || name == "fill_color.g" ||
|
||||
name == "fill_color.b" || name == "fill_color.a") {
|
||||
return true;
|
||||
}
|
||||
// Vector2f properties
|
||||
if (name == "position" || name == "size" || name == "center") {
|
||||
if (name == "position" || name == "size" || name == "center" || name == "origin") {
|
||||
return true;
|
||||
}
|
||||
// #106: Shader uniform properties
|
||||
|
|
|
|||
|
|
@ -79,11 +79,16 @@ public:
|
|||
//int grid_size; // grid sizes are implied by IndexTexture now
|
||||
sf::RectangleShape box;
|
||||
float center_x, center_y, zoom;
|
||||
float camera_rotation = 0.0f; // Rotation of grid contents around camera center (degrees)
|
||||
//IndexTexture* itex;
|
||||
std::shared_ptr<PyTexture> getTexture();
|
||||
sf::Sprite sprite, output;
|
||||
sf::RenderTexture renderTexture;
|
||||
|
||||
// Intermediate texture for camera_rotation (larger than viewport to hold rotated content)
|
||||
sf::RenderTexture rotationTexture;
|
||||
unsigned int rotationTextureSize = 0; // Track current allocation size
|
||||
|
||||
// #123 - Chunk-based storage for large grid support
|
||||
std::unique_ptr<ChunkManager> chunk_manager;
|
||||
// Legacy flat storage (kept for small grids or compatibility)
|
||||
|
|
@ -181,6 +186,8 @@ public:
|
|||
// py_clear_dijkstra_maps -> UIGridPathfinding::Grid_clear_dijkstra_maps
|
||||
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
|
||||
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
|
||||
static PyObject* get_camera_rotation(PyUIGridObject* self, void* closure);
|
||||
static int set_camera_rotation(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
|
||||
// #199 - HeightMap application methods
|
||||
static PyObject* py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@ void UILine::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
|||
line_shape.setFillColor(render_color);
|
||||
line_shape.setOutlineThickness(0);
|
||||
|
||||
// Apply rotation around origin
|
||||
line_shape.setOrigin(origin);
|
||||
line_shape.setRotation(rotation);
|
||||
|
||||
target.draw(line_shape);
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +145,22 @@ UIDrawable* UILine::click_at(sf::Vector2f point) {
|
|||
// #184: Also check for Python subclass (might have on_click method)
|
||||
if (!click_callable && !is_python_subclass) return nullptr;
|
||||
|
||||
// Transform click point to local coordinates accounting for rotation
|
||||
sf::Vector2f localPoint;
|
||||
if (rotation != 0.0f) {
|
||||
// Build transform: rotate around origin
|
||||
sf::Transform transform;
|
||||
transform.translate(origin);
|
||||
transform.rotate(rotation);
|
||||
transform.translate(-origin);
|
||||
|
||||
// Apply inverse transform to get local coordinates
|
||||
sf::Transform inverse = transform.getInverse();
|
||||
localPoint = inverse.transformPoint(point);
|
||||
} else {
|
||||
localPoint = point;
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
|
@ -149,11 +169,12 @@ UIDrawable* UILine::click_at(sf::Vector2f point) {
|
|||
bounds.width += thickness * 2;
|
||||
bounds.height += thickness * 2;
|
||||
|
||||
if (!bounds.contains(point)) return nullptr;
|
||||
// For rotated lines, skip the bounds check (it's an optimization, not required)
|
||||
if (rotation == 0.0f && !bounds.contains(localPoint)) return nullptr;
|
||||
|
||||
// Calculate distance from point to line segment
|
||||
sf::Vector2f line_vec = end_pos - start_pos;
|
||||
sf::Vector2f point_vec = point - start_pos;
|
||||
sf::Vector2f point_vec = localPoint - start_pos;
|
||||
|
||||
float line_len_sq = line_vec.x * line_vec.x + line_vec.y * line_vec.y;
|
||||
float t = 0.0f;
|
||||
|
|
@ -164,7 +185,7 @@ UIDrawable* UILine::click_at(sf::Vector2f point) {
|
|||
}
|
||||
|
||||
sf::Vector2f closest = start_pos + t * line_vec;
|
||||
sf::Vector2f diff = point - closest;
|
||||
sf::Vector2f diff = localPoint - closest;
|
||||
float distance = std::sqrt(diff.x * diff.x + diff.y * diff.y);
|
||||
|
||||
// Click is valid if within thickness + some margin
|
||||
|
|
@ -248,6 +269,21 @@ bool UILine::setProperty(const std::string& name, float value) {
|
|||
markDirty(); // #144 - Content change
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
rotation = value;
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
origin.x = value;
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
origin.y = value;
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -306,6 +342,18 @@ bool UILine::getProperty(const std::string& name, float& value) const {
|
|||
value = end_pos.y;
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
value = rotation;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
value = origin.x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
value = origin.y;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -333,7 +381,8 @@ bool UILine::hasProperty(const std::string& name) const {
|
|||
// Float properties
|
||||
if (name == "thickness" || name == "x" || name == "y" ||
|
||||
name == "start_x" || name == "start_y" ||
|
||||
name == "end_x" || name == "end_y") {
|
||||
name == "end_x" || name == "end_y" ||
|
||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||
return true;
|
||||
}
|
||||
// Color properties
|
||||
|
|
@ -341,7 +390,7 @@ bool UILine::hasProperty(const std::string& name) const {
|
|||
return true;
|
||||
}
|
||||
// Vector2f properties
|
||||
if (name == "start" || name == "end") {
|
||||
if (name == "start" || name == "end" || name == "origin") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -469,6 +518,7 @@ PyGetSetDef UILine::getsetters[] = {
|
|||
UIDRAWABLE_GETSETTERS,
|
||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UILINE),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UILINE),
|
||||
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UILINE),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,11 +11,36 @@
|
|||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||
{
|
||||
// #184: Also check for Python subclass (might have on_click method)
|
||||
if (click_callable || is_python_subclass)
|
||||
{
|
||||
if(sprite.getGlobalBounds().contains(point)) return this;
|
||||
if (!click_callable && !is_python_subclass) return nullptr;
|
||||
|
||||
// Get sprite dimensions from local bounds
|
||||
sf::FloatRect localBounds = sprite.getLocalBounds();
|
||||
float w = localBounds.width * sprite.getScale().x;
|
||||
float h = localBounds.height * sprite.getScale().y;
|
||||
|
||||
// Transform click point to local coordinates accounting for rotation
|
||||
sf::Vector2f localPoint;
|
||||
if (rotation != 0.0f) {
|
||||
// Build transform: translate to position, then rotate around origin
|
||||
sf::Transform transform;
|
||||
transform.translate(position);
|
||||
transform.translate(origin);
|
||||
transform.rotate(rotation);
|
||||
transform.translate(-origin);
|
||||
|
||||
// Apply inverse transform to get local coordinates
|
||||
sf::Transform inverse = transform.getInverse();
|
||||
localPoint = inverse.transformPoint(point);
|
||||
} else {
|
||||
// No rotation - simple subtraction
|
||||
localPoint = point - position;
|
||||
}
|
||||
return NULL;
|
||||
|
||||
// Check if local point is within bounds (0,0 to w,h in local space)
|
||||
if (localPoint.x >= 0 && localPoint.y >= 0 && localPoint.x < w && localPoint.y < h) {
|
||||
return this;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UISprite::UISprite()
|
||||
|
|
@ -89,6 +114,10 @@ void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
color.a = static_cast<sf::Uint8>(255 * opacity);
|
||||
sprite.setColor(color);
|
||||
|
||||
// Apply rotation and origin
|
||||
sprite.setOrigin(origin);
|
||||
sprite.setRotation(rotation);
|
||||
|
||||
// #106: Shader rendering path
|
||||
if (shader && shader->shader) {
|
||||
// Get the sprite bounds for rendering
|
||||
|
|
@ -396,6 +425,7 @@ PyGetSetDef UISprite::getsetters[] = {
|
|||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
@ -628,6 +658,24 @@ bool UISprite::setProperty(const std::string& name, float value) {
|
|||
markDirty(); // #144 - Z-order change affects parent
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
rotation = value;
|
||||
sprite.setRotation(rotation);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
origin.x = value;
|
||||
sprite.setOrigin(origin);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
origin.y = value;
|
||||
sprite.setOrigin(origin);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
if (setShaderProperty(name, value)) {
|
||||
return true;
|
||||
|
|
@ -674,6 +722,18 @@ bool UISprite::getProperty(const std::string& name, float& value) const {
|
|||
value = static_cast<float>(z_index);
|
||||
return true;
|
||||
}
|
||||
else if (name == "rotation") {
|
||||
value = rotation;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_x") {
|
||||
value = origin.x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "origin_y") {
|
||||
value = origin.y;
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
if (getShaderProperty(name, value)) {
|
||||
return true;
|
||||
|
|
@ -697,13 +757,18 @@ bool UISprite::hasProperty(const std::string& name) const {
|
|||
// Float properties
|
||||
if (name == "x" || name == "y" ||
|
||||
name == "scale" || name == "scale_x" || name == "scale_y" ||
|
||||
name == "z_index") {
|
||||
name == "z_index" ||
|
||||
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||
return true;
|
||||
}
|
||||
// Int properties
|
||||
if (name == "sprite_index" || name == "sprite_number") {
|
||||
return true;
|
||||
}
|
||||
// Vector2f properties
|
||||
if (name == "origin") {
|
||||
return true;
|
||||
}
|
||||
// #106: Check for shader uniform properties
|
||||
if (hasShaderProperty(name)) {
|
||||
return true;
|
||||
|
|
|
|||
86
tests/unit/grid_camera_rotation_test.py
Normal file
86
tests/unit/grid_camera_rotation_test.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test UIGrid camera_rotation functionality"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
# Create test scene
|
||||
test_scene = mcrfpy.Scene("grid_rotation_test")
|
||||
ui = test_scene.children
|
||||
|
||||
# Create background
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600), fill_color=mcrfpy.Color(30, 30, 40))
|
||||
ui.append(bg)
|
||||
|
||||
# Create a grid with entities to visualize rotation
|
||||
grid = mcrfpy.Grid(grid_size=(8, 8), pos=(50, 50), size=(300, 300))
|
||||
grid.fill_color = mcrfpy.Color(60, 60, 80)
|
||||
|
||||
# Add some entities to visualize the rotation
|
||||
for i in range(8):
|
||||
entity = mcrfpy.Entity((i, 0)) # Top row
|
||||
grid.entities.append(entity)
|
||||
|
||||
for i in range(1, 8):
|
||||
entity = mcrfpy.Entity((0, i)) # Left column
|
||||
grid.entities.append(entity)
|
||||
|
||||
# Apply camera rotation
|
||||
grid.camera_rotation = 30.0 # 30 degree rotation
|
||||
grid.center_camera((4, 4)) # Center on middle of grid
|
||||
|
||||
ui.append(grid)
|
||||
|
||||
# Create a second grid without rotation for comparison
|
||||
grid2 = mcrfpy.Grid(grid_size=(8, 8), pos=(400, 50), size=(300, 300))
|
||||
grid2.fill_color = mcrfpy.Color(60, 60, 80)
|
||||
|
||||
# Add same entities pattern
|
||||
for i in range(8):
|
||||
entity = mcrfpy.Entity((i, 0))
|
||||
grid2.entities.append(entity)
|
||||
|
||||
for i in range(1, 8):
|
||||
entity = mcrfpy.Entity((0, i))
|
||||
grid2.entities.append(entity)
|
||||
|
||||
grid2.camera_rotation = 0.0 # No rotation
|
||||
grid2.center_camera((4, 4))
|
||||
|
||||
ui.append(grid2)
|
||||
|
||||
# Labels
|
||||
label1 = mcrfpy.Caption(text="Grid with camera_rotation=30", pos=(50, 20))
|
||||
ui.append(label1)
|
||||
|
||||
label2 = mcrfpy.Caption(text="Grid with camera_rotation=0", pos=(400, 20))
|
||||
ui.append(label2)
|
||||
|
||||
# Create a third grid with viewport rotation (different from camera rotation)
|
||||
grid3 = mcrfpy.Grid(grid_size=(6, 6), pos=(175, 400), size=(200, 150))
|
||||
grid3.fill_color = mcrfpy.Color(80, 60, 60)
|
||||
|
||||
# Add entities
|
||||
for i in range(6):
|
||||
entity = mcrfpy.Entity((i, 0))
|
||||
grid3.entities.append(entity)
|
||||
|
||||
# Apply viewport rotation (entire grid rotates)
|
||||
grid3.rotation = 15.0
|
||||
grid3.origin = (100, 75) # Center origin for rotation
|
||||
grid3.center_camera((3, 3))
|
||||
|
||||
ui.append(grid3)
|
||||
|
||||
label3 = mcrfpy.Caption(text="Grid with viewport rotation=15 (rotates entire widget)", pos=(100, 560))
|
||||
ui.append(label3)
|
||||
|
||||
# Activate scene
|
||||
mcrfpy.current_scene = test_scene
|
||||
|
||||
# Advance the game loop to render, then take screenshot
|
||||
mcrfpy.step(0.1)
|
||||
automation.screenshot("grid_camera_rotation_test.png")
|
||||
print("Screenshot saved as grid_camera_rotation_test.png")
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
163
tests/unit/rotation_test.py
Normal file
163
tests/unit/rotation_test.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test rotation support for UIDrawable subclasses"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_rotation_properties():
|
||||
"""Test rotation, origin, rotate_with_camera properties on all UIDrawable types"""
|
||||
print("Testing rotation properties on all UIDrawable types...")
|
||||
|
||||
# Test UIFrame
|
||||
frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50))
|
||||
assert frame.rotation == 0.0, f"Frame default rotation should be 0, got {frame.rotation}"
|
||||
frame.rotation = 45.0
|
||||
assert frame.rotation == 45.0, f"Frame rotation should be 45, got {frame.rotation}"
|
||||
|
||||
# Test origin as Vector
|
||||
frame.origin = (25, 25)
|
||||
assert frame.origin.x == 25.0, f"Frame origin.x should be 25, got {frame.origin.x}"
|
||||
assert frame.origin.y == 25.0, f"Frame origin.y should be 25, got {frame.origin.y}"
|
||||
|
||||
# Test rotate_with_camera
|
||||
assert frame.rotate_with_camera == False, "Default rotate_with_camera should be False"
|
||||
frame.rotate_with_camera = True
|
||||
assert frame.rotate_with_camera == True, "rotate_with_camera should be True after setting"
|
||||
print(" Frame: PASS")
|
||||
|
||||
# Test UISprite
|
||||
sprite = mcrfpy.Sprite(pos=(100, 100))
|
||||
assert sprite.rotation == 0.0, f"Sprite default rotation should be 0, got {sprite.rotation}"
|
||||
sprite.rotation = 90.0
|
||||
assert sprite.rotation == 90.0, f"Sprite rotation should be 90, got {sprite.rotation}"
|
||||
sprite.origin = (8, 8)
|
||||
assert sprite.origin.x == 8.0, f"Sprite origin.x should be 8, got {sprite.origin.x}"
|
||||
print(" Sprite: PASS")
|
||||
|
||||
# Test UICaption
|
||||
caption = mcrfpy.Caption(text="Test", pos=(100, 100))
|
||||
assert caption.rotation == 0.0, f"Caption default rotation should be 0, got {caption.rotation}"
|
||||
caption.rotation = -30.0
|
||||
assert caption.rotation == -30.0, f"Caption rotation should be -30, got {caption.rotation}"
|
||||
caption.origin = (0, 0)
|
||||
assert caption.origin.x == 0.0, f"Caption origin.x should be 0, got {caption.origin.x}"
|
||||
print(" Caption: PASS")
|
||||
|
||||
# Test UICircle
|
||||
circle = mcrfpy.Circle(center=(100, 100), radius=25)
|
||||
assert circle.rotation == 0.0, f"Circle default rotation should be 0, got {circle.rotation}"
|
||||
circle.rotation = 180.0
|
||||
assert circle.rotation == 180.0, f"Circle rotation should be 180, got {circle.rotation}"
|
||||
print(" Circle: PASS")
|
||||
|
||||
# Test UILine
|
||||
line = mcrfpy.Line(start=(0, 0), end=(100, 100))
|
||||
assert line.rotation == 0.0, f"Line default rotation should be 0, got {line.rotation}"
|
||||
line.rotation = 45.0
|
||||
assert line.rotation == 45.0, f"Line rotation should be 45, got {line.rotation}"
|
||||
print(" Line: PASS")
|
||||
|
||||
# Test UIArc
|
||||
arc = mcrfpy.Arc(center=(100, 100), radius=50, start_angle=0, end_angle=90)
|
||||
assert arc.rotation == 0.0, f"Arc default rotation should be 0, got {arc.rotation}"
|
||||
arc.rotation = 270.0
|
||||
assert arc.rotation == 270.0, f"Arc rotation should be 270, got {arc.rotation}"
|
||||
print(" Arc: PASS")
|
||||
|
||||
print("All rotation property tests passed!")
|
||||
return True
|
||||
|
||||
def test_rotation_animation():
|
||||
"""Test that rotation can be animated"""
|
||||
print("\nTesting rotation animation...")
|
||||
|
||||
frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50))
|
||||
frame.rotation = 0.0
|
||||
|
||||
# Test that animate method exists and accepts rotation
|
||||
try:
|
||||
frame.animate("rotation", 360.0, 1.0, mcrfpy.Easing.LINEAR)
|
||||
print(" Animation started successfully")
|
||||
except Exception as e:
|
||||
print(f" Animation failed: {e}")
|
||||
return False
|
||||
|
||||
# Test origin animation
|
||||
try:
|
||||
frame.animate("origin_x", 25.0, 0.5, mcrfpy.Easing.LINEAR)
|
||||
frame.animate("origin_y", 25.0, 0.5, mcrfpy.Easing.LINEAR)
|
||||
print(" Origin animation started successfully")
|
||||
except Exception as e:
|
||||
print(f" Origin animation failed: {e}")
|
||||
return False
|
||||
|
||||
print("Rotation animation tests passed!")
|
||||
return True
|
||||
|
||||
def test_grid_camera_rotation():
|
||||
"""Test UIGrid camera_rotation property"""
|
||||
print("\nTesting Grid camera_rotation...")
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(50, 50), size=(200, 200))
|
||||
|
||||
# Test default camera_rotation
|
||||
assert grid.camera_rotation == 0.0, f"Grid default camera_rotation should be 0, got {grid.camera_rotation}"
|
||||
|
||||
# Test setting camera_rotation
|
||||
grid.camera_rotation = 45.0
|
||||
assert grid.camera_rotation == 45.0, f"Grid camera_rotation should be 45, got {grid.camera_rotation}"
|
||||
|
||||
# Test negative rotation
|
||||
grid.camera_rotation = -90.0
|
||||
assert grid.camera_rotation == -90.0, f"Grid camera_rotation should be -90, got {grid.camera_rotation}"
|
||||
|
||||
# Test full rotation
|
||||
grid.camera_rotation = 360.0
|
||||
assert grid.camera_rotation == 360.0, f"Grid camera_rotation should be 360, got {grid.camera_rotation}"
|
||||
|
||||
# Grid also has regular rotation (viewport rotation)
|
||||
assert grid.rotation == 0.0, f"Grid viewport rotation should default to 0, got {grid.rotation}"
|
||||
grid.rotation = 15.0
|
||||
assert grid.rotation == 15.0, f"Grid viewport rotation should be 15, got {grid.rotation}"
|
||||
|
||||
# Test camera_rotation animation
|
||||
try:
|
||||
grid.animate("camera_rotation", 90.0, 1.0, mcrfpy.Easing.EASE_IN_OUT)
|
||||
print(" Camera rotation animation started successfully")
|
||||
except Exception as e:
|
||||
print(f" Camera rotation animation failed: {e}")
|
||||
return False
|
||||
|
||||
print("Grid camera_rotation tests passed!")
|
||||
return True
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all rotation tests"""
|
||||
print("=" * 50)
|
||||
print("UIDrawable Rotation Tests")
|
||||
print("=" * 50)
|
||||
|
||||
results = []
|
||||
results.append(("Rotation Properties", test_rotation_properties()))
|
||||
results.append(("Rotation Animation", test_rotation_animation()))
|
||||
results.append(("Grid Camera Rotation", test_grid_camera_rotation()))
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Test Results Summary")
|
||||
print("=" * 50)
|
||||
|
||||
all_passed = True
|
||||
for name, passed in results:
|
||||
status = "PASS" if passed else "FAIL"
|
||||
print(f" {name}: {status}")
|
||||
if not passed:
|
||||
all_passed = False
|
||||
|
||||
if all_passed:
|
||||
print("\nAll tests PASSED!")
|
||||
return 0
|
||||
else:
|
||||
print("\nSome tests FAILED!")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_all_tests())
|
||||
114
tests/unit/rotation_visual_test.py
Normal file
114
tests/unit/rotation_visual_test.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Visual test for rotation support - uses direct screenshot"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
# Create test scene
|
||||
test_scene = mcrfpy.Scene("rotation_test")
|
||||
ui = test_scene.children
|
||||
|
||||
# Create background
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600), fill_color=mcrfpy.Color(40, 40, 50))
|
||||
ui.append(bg)
|
||||
|
||||
# Row 1: Frames with different rotations
|
||||
# Frame at 0 degrees
|
||||
frame1 = mcrfpy.Frame(pos=(100, 100), size=(60, 60), fill_color=mcrfpy.Color(200, 50, 50))
|
||||
frame1.rotation = 0.0
|
||||
frame1.origin = (30, 30) # Center origin
|
||||
ui.append(frame1)
|
||||
|
||||
# Frame at 45 degrees
|
||||
frame2 = mcrfpy.Frame(pos=(250, 100), size=(60, 60), fill_color=mcrfpy.Color(50, 200, 50))
|
||||
frame2.rotation = 45.0
|
||||
frame2.origin = (30, 30)
|
||||
ui.append(frame2)
|
||||
|
||||
# Frame at 90 degrees
|
||||
frame3 = mcrfpy.Frame(pos=(400, 100), size=(60, 60), fill_color=mcrfpy.Color(50, 50, 200))
|
||||
frame3.rotation = 90.0
|
||||
frame3.origin = (30, 30)
|
||||
ui.append(frame3)
|
||||
|
||||
# Label for row 1
|
||||
label1 = mcrfpy.Caption(text="Frames: 0, 45, 90 degrees", pos=(100, 50))
|
||||
ui.append(label1)
|
||||
|
||||
# Row 2: Captions with rotation
|
||||
caption1 = mcrfpy.Caption(text="Rotated Text", pos=(100, 250))
|
||||
caption1.rotation = 0.0
|
||||
ui.append(caption1)
|
||||
|
||||
caption2 = mcrfpy.Caption(text="Rotated Text", pos=(300, 250))
|
||||
caption2.rotation = -15.0
|
||||
ui.append(caption2)
|
||||
|
||||
caption3 = mcrfpy.Caption(text="Rotated Text", pos=(500, 250))
|
||||
caption3.rotation = 30.0
|
||||
ui.append(caption3)
|
||||
|
||||
# Label for row 2
|
||||
label2 = mcrfpy.Caption(text="Captions: 0, -15, 30 degrees", pos=(100, 200))
|
||||
ui.append(label2)
|
||||
|
||||
# Row 3: Circles (rotation with offset origin causes orbiting)
|
||||
circle1 = mcrfpy.Circle(center=(100, 400), radius=25, fill_color=mcrfpy.Color(200, 200, 50))
|
||||
circle1.rotation = 0.0
|
||||
ui.append(circle1)
|
||||
|
||||
circle2 = mcrfpy.Circle(center=(250, 400), radius=25, fill_color=mcrfpy.Color(200, 50, 200))
|
||||
circle2.rotation = 45.0
|
||||
circle2.origin = (20, 0) # Offset origin to show orbiting effect
|
||||
ui.append(circle2)
|
||||
|
||||
circle3 = mcrfpy.Circle(center=(400, 400), radius=25, fill_color=mcrfpy.Color(50, 200, 200))
|
||||
circle3.rotation = 90.0
|
||||
circle3.origin = (20, 0) # Same offset
|
||||
ui.append(circle3)
|
||||
|
||||
# Label for row 3
|
||||
label3 = mcrfpy.Caption(text="Circles with offset origin: 0, 45, 90 degrees", pos=(100, 350))
|
||||
ui.append(label3)
|
||||
|
||||
# Row 4: Lines with rotation
|
||||
line1 = mcrfpy.Line(start=(100, 500), end=(150, 500), thickness=3, color=mcrfpy.Color(255, 255, 255))
|
||||
line1.rotation = 0.0
|
||||
ui.append(line1)
|
||||
|
||||
line2 = mcrfpy.Line(start=(250, 500), end=(300, 500), thickness=3, color=mcrfpy.Color(255, 200, 200))
|
||||
line2.rotation = 45.0
|
||||
line2.origin = (125, 500) # Rotate around line center
|
||||
ui.append(line2)
|
||||
|
||||
line3 = mcrfpy.Line(start=(400, 500), end=(450, 500), thickness=3, color=mcrfpy.Color(200, 255, 200))
|
||||
line3.rotation = -45.0
|
||||
line3.origin = (200, 500)
|
||||
ui.append(line3)
|
||||
|
||||
# Label for row 4
|
||||
label4 = mcrfpy.Caption(text="Lines: 0, 45, -45 degrees", pos=(100, 470))
|
||||
ui.append(label4)
|
||||
|
||||
# Arcs with rotation
|
||||
arc1 = mcrfpy.Arc(center=(600, 100), radius=40, start_angle=0, end_angle=90, thickness=5)
|
||||
arc1.rotation = 0.0
|
||||
ui.append(arc1)
|
||||
|
||||
arc2 = mcrfpy.Arc(center=(700, 100), radius=40, start_angle=0, end_angle=90, thickness=5)
|
||||
arc2.rotation = 45.0
|
||||
ui.append(arc2)
|
||||
|
||||
# Label for arcs
|
||||
label5 = mcrfpy.Caption(text="Arcs: 0, 45 degrees", pos=(550, 50))
|
||||
ui.append(label5)
|
||||
|
||||
# Activate scene
|
||||
mcrfpy.current_scene = test_scene
|
||||
|
||||
# Advance the game loop to render, then take screenshot
|
||||
mcrfpy.step(0.1)
|
||||
automation.screenshot("rotation_visual_test.png")
|
||||
print("Screenshot saved as rotation_visual_test.png")
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue