.animate helper: create and start an animation directly on a target. Preferred use pattern; closes #175
This commit is contained in:
parent
d878c8684d
commit
9ab618079a
21 changed files with 738 additions and 11 deletions
|
|
@ -40,17 +40,18 @@ Animation::Animation(const std::string& targetProperty,
|
||||||
|
|
||||||
Animation::~Animation() {
|
Animation::~Animation() {
|
||||||
// Decrease reference count for Python callback if we still own it
|
// Decrease reference count for Python callback if we still own it
|
||||||
|
// Guard with Py_IsInitialized() because destructor may run during interpreter shutdown
|
||||||
PyObject* callback = pythonCallback;
|
PyObject* callback = pythonCallback;
|
||||||
if (callback) {
|
if (callback && Py_IsInitialized()) {
|
||||||
pythonCallback = nullptr;
|
pythonCallback = nullptr;
|
||||||
|
|
||||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||||
Py_DECREF(callback);
|
Py_DECREF(callback);
|
||||||
PyGILState_Release(gstate);
|
PyGILState_Release(gstate);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up cache entry
|
// Clean up cache entry (also guard - PythonObjectCache may use Python)
|
||||||
if (serial_number != 0) {
|
if (serial_number != 0 && Py_IsInitialized()) {
|
||||||
PythonObjectCache::getInstance().remove(serial_number);
|
PythonObjectCache::getInstance().remove(serial_number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,23 @@ bool UIArc::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Color properties
|
||||||
|
if (name == "color") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Vector2f properties
|
||||||
|
if (name == "center") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Python API implementation
|
// Python API implementation
|
||||||
PyObject* UIArc::get_center(PyUIArcObject* self, void* closure) {
|
PyObject* UIArc::get_center(PyUIArcObject* self, void* closure) {
|
||||||
auto center = self->data->getCenter();
|
auto center = self->data->getCenter();
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ public:
|
||||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||||
|
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
|
|
||||||
// Python API
|
// Python API
|
||||||
static PyObject* get_center(PyUIArcObject* self, void* closure);
|
static PyObject* get_center(PyUIArcObject* self, void* closure);
|
||||||
static int set_center(PyUIArcObject* self, PyObject* value, void* closure);
|
static int set_center(PyUIArcObject* self, PyObject* value, void* closure);
|
||||||
|
|
|
||||||
41
src/UIBase.h
41
src/UIBase.h
|
|
@ -71,13 +71,25 @@ static PyObject* UIDrawable_resize(T* self, PyObject* args)
|
||||||
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
|
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
self->data->resize(w, h);
|
self->data->resize(w, h);
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Macro to add common UIDrawable methods to a method array
|
// animate method implementation - shorthand for creating and starting animations
|
||||||
#define UIDRAWABLE_METHODS \
|
// This free function is implemented in UIDrawable.cpp
|
||||||
|
// We use a free function instead of UIDrawable::animate_helper to avoid incomplete type issues
|
||||||
|
class UIDrawable;
|
||||||
|
PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> target, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
return UIDrawable_animate_impl(self->data, args, kwds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Macro to add common UIDrawable methods to a method array (without animate - for base types)
|
||||||
|
#define UIDRAWABLE_METHODS_BASE \
|
||||||
{"get_bounds", (PyCFunction)UIDrawable_get_bounds<PyObjectType>, METH_NOARGS, \
|
{"get_bounds", (PyCFunction)UIDrawable_get_bounds<PyObjectType>, METH_NOARGS, \
|
||||||
MCRF_METHOD(Drawable, get_bounds, \
|
MCRF_METHOD(Drawable, get_bounds, \
|
||||||
MCRF_SIG("()", "tuple"), \
|
MCRF_SIG("()", "tuple"), \
|
||||||
|
|
@ -104,6 +116,29 @@ static PyObject* UIDrawable_resize(T* self, PyObject* args)
|
||||||
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \
|
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
// Macro to add common UIDrawable methods to a method array (includes animate for UIDrawable derivatives)
|
||||||
|
#define UIDRAWABLE_METHODS \
|
||||||
|
UIDRAWABLE_METHODS_BASE, \
|
||||||
|
{"animate", (PyCFunction)UIDrawable_animate<PyObjectType>, METH_VARARGS | METH_KEYWORDS, \
|
||||||
|
MCRF_METHOD(Drawable, animate, \
|
||||||
|
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, callback=None, conflict_mode='replace')", "Animation"), \
|
||||||
|
MCRF_DESC("Create and start an animation on this drawable's property."), \
|
||||||
|
MCRF_ARGS_START \
|
||||||
|
MCRF_ARG("property", "Name of the property to animate (e.g., 'x', 'fill_color', 'opacity')") \
|
||||||
|
MCRF_ARG("target", "Target value - type depends on property (float, tuple for color/vector, etc.)") \
|
||||||
|
MCRF_ARG("duration", "Animation duration in seconds") \
|
||||||
|
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear") \
|
||||||
|
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute") \
|
||||||
|
MCRF_ARG("callback", "Optional callable invoked when animation completes") \
|
||||||
|
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating") \
|
||||||
|
MCRF_RETURNS("Animation object for monitoring progress") \
|
||||||
|
MCRF_RAISES("ValueError", "If property name is not valid for this drawable type") \
|
||||||
|
MCRF_NOTE("This is a convenience method that creates an Animation, starts it, and adds it to the AnimationManager.") \
|
||||||
|
)}
|
||||||
|
|
||||||
|
// Legacy macro for backwards compatibility - same as UIDRAWABLE_METHODS
|
||||||
|
#define UIDRAWABLE_METHODS_FULL UIDRAWABLE_METHODS
|
||||||
|
|
||||||
// Property getters/setters for visible and opacity
|
// Property getters/setters for visible and opacity
|
||||||
template<typename T>
|
template<typename T>
|
||||||
static PyObject* UIDrawable_get_visible(T* self, void* closure)
|
static PyObject* UIDrawable_get_visible(T* self, void* closure)
|
||||||
|
|
|
||||||
|
|
@ -657,3 +657,24 @@ bool UICaption::getProperty(const std::string& name, std::string& value) const {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UICaption::hasProperty(const std::string& name) const {
|
||||||
|
// Float properties
|
||||||
|
if (name == "x" || name == "y" ||
|
||||||
|
name == "font_size" || name == "size" || name == "outline" ||
|
||||||
|
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") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Color properties
|
||||||
|
if (name == "fill_color" || name == "outline_color") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// String properties
|
||||||
|
if (name == "text") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ public:
|
||||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||||
bool getProperty(const std::string& name, std::string& value) const override;
|
bool getProperty(const std::string& name, std::string& value) const override;
|
||||||
|
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
|
|
||||||
static PyObject* get_float_member(PyUICaptionObject* self, void* closure);
|
static PyObject* get_float_member(PyUICaptionObject* self, void* closure);
|
||||||
static int set_float_member(PyUICaptionObject* self, PyObject* value, void* closure);
|
static int set_float_member(PyUICaptionObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_vec_member(PyUICaptionObject* self, void* closure);
|
static PyObject* get_vec_member(PyUICaptionObject* self, void* closure);
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,23 @@ bool UICircle::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UICircle::hasProperty(const std::string& name) const {
|
||||||
|
// Float properties
|
||||||
|
if (name == "radius" || name == "outline" ||
|
||||||
|
name == "x" || name == "y") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Color properties
|
||||||
|
if (name == "fill_color" || name == "outline_color") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Vector2f properties
|
||||||
|
if (name == "center" || name == "position") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Python API implementations
|
// Python API implementations
|
||||||
PyObject* UICircle::get_radius(PyUICircleObject* self, void* closure) {
|
PyObject* UICircle::get_radius(PyUICircleObject* self, void* closure) {
|
||||||
return PyFloat_FromDouble(self->data->getRadius());
|
return PyFloat_FromDouble(self->data->getRadius());
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@ public:
|
||||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||||
|
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
|
|
||||||
// Python API
|
// Python API
|
||||||
static PyObject* get_radius(PyUICircleObject* self, void* closure);
|
static PyObject* get_radius(PyUICircleObject* self, void* closure);
|
||||||
static int set_radius(PyUICircleObject* self, PyObject* value, void* closure);
|
static int set_radius(PyUICircleObject* self, PyObject* value, void* closure);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
|
#include "Animation.h"
|
||||||
|
#include "PyAnimation.h"
|
||||||
|
#include "PyEasing.h"
|
||||||
|
|
||||||
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
|
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
|
||||||
|
|
||||||
|
|
@ -1441,3 +1444,153 @@ int UIDrawable::set_on_move(PyObject* self, PyObject* value, void* closure) {
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Animation shorthand helper - creates and starts an animation on a UIDrawable
|
||||||
|
// This is a free function (not a member) to avoid incomplete type issues in UIBase.h template
|
||||||
|
PyObject* UIDrawable_animate_impl(std::shared_ptr<UIDrawable> self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
|
||||||
|
|
||||||
|
const char* property_name;
|
||||||
|
PyObject* target_value;
|
||||||
|
float duration;
|
||||||
|
PyObject* easing_arg = Py_None;
|
||||||
|
int delta = 0;
|
||||||
|
PyObject* callback = nullptr;
|
||||||
|
const char* conflict_mode_str = nullptr;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
|
||||||
|
&property_name, &target_value, &duration,
|
||||||
|
&easing_arg, &delta, &callback, &conflict_mode_str)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate property exists on this drawable
|
||||||
|
if (!self->hasProperty(property_name)) {
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Property '%s' is not valid for animation on this object. "
|
||||||
|
"Check spelling or use a supported property name.",
|
||||||
|
property_name);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate callback is callable if provided
|
||||||
|
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert None to nullptr for C++
|
||||||
|
if (callback == Py_None) {
|
||||||
|
callback = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Python target value to AnimationValue
|
||||||
|
AnimationValue animValue;
|
||||||
|
|
||||||
|
if (PyFloat_Check(target_value)) {
|
||||||
|
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
|
||||||
|
}
|
||||||
|
else if (PyLong_Check(target_value)) {
|
||||||
|
animValue = static_cast<int>(PyLong_AsLong(target_value));
|
||||||
|
}
|
||||||
|
else if (PyList_Check(target_value)) {
|
||||||
|
// List of integers for sprite animation
|
||||||
|
std::vector<int> indices;
|
||||||
|
Py_ssize_t size = PyList_Size(target_value);
|
||||||
|
for (Py_ssize_t i = 0; i < size; i++) {
|
||||||
|
PyObject* item = PyList_GetItem(target_value, i);
|
||||||
|
if (PyLong_Check(item)) {
|
||||||
|
indices.push_back(PyLong_AsLong(item));
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animValue = indices;
|
||||||
|
}
|
||||||
|
else if (PyTuple_Check(target_value)) {
|
||||||
|
Py_ssize_t size = PyTuple_Size(target_value);
|
||||||
|
if (size == 2) {
|
||||||
|
// Vector2f
|
||||||
|
float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0));
|
||||||
|
float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
animValue = sf::Vector2f(x, y);
|
||||||
|
}
|
||||||
|
else if (size == 3 || size == 4) {
|
||||||
|
// Color (RGB or RGBA)
|
||||||
|
int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0));
|
||||||
|
int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1));
|
||||||
|
int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2));
|
||||||
|
int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255;
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
animValue = sf::Color(r, g, b, a);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (PyUnicode_Check(target_value)) {
|
||||||
|
// String for text animation
|
||||||
|
const char* str = PyUnicode_AsUTF8(target_value);
|
||||||
|
animValue = std::string(str);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get easing function from argument
|
||||||
|
EasingFunction easingFunc;
|
||||||
|
if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) {
|
||||||
|
return NULL; // Error already set by from_arg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse conflict mode
|
||||||
|
AnimationConflictMode conflict_mode = AnimationConflictMode::REPLACE;
|
||||||
|
if (conflict_mode_str) {
|
||||||
|
if (strcmp(conflict_mode_str, "replace") == 0) {
|
||||||
|
conflict_mode = AnimationConflictMode::REPLACE;
|
||||||
|
} else if (strcmp(conflict_mode_str, "queue") == 0) {
|
||||||
|
conflict_mode = AnimationConflictMode::QUEUE;
|
||||||
|
} else if (strcmp(conflict_mode_str, "error") == 0) {
|
||||||
|
conflict_mode = AnimationConflictMode::ERROR;
|
||||||
|
} else {
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", conflict_mode_str);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Animation
|
||||||
|
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||||
|
|
||||||
|
// Start on this drawable
|
||||||
|
animation->start(self);
|
||||||
|
|
||||||
|
// Add to AnimationManager
|
||||||
|
AnimationManager::getInstance().addAnimation(animation, conflict_mode);
|
||||||
|
|
||||||
|
// Check if ERROR mode raised an exception
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and return a PyAnimation wrapper
|
||||||
|
PyTypeObject* animType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Animation");
|
||||||
|
if (!animType) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Could not find Animation type");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyAnimationObject* pyAnim = (PyAnimationObject*)animType->tp_alloc(animType, 0);
|
||||||
|
Py_DECREF(animType);
|
||||||
|
|
||||||
|
if (!pyAnim) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pyAnim->data = animation;
|
||||||
|
return (PyObject*)pyAnim;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,13 @@ public:
|
||||||
virtual bool getProperty(const std::string& name, sf::Color& value) const { return false; }
|
virtual bool getProperty(const std::string& name, sf::Color& value) const { return false; }
|
||||||
virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; }
|
virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; }
|
||||||
virtual bool getProperty(const std::string& name, std::string& value) const { return false; }
|
virtual bool getProperty(const std::string& name, std::string& value) const { return false; }
|
||||||
|
|
||||||
|
// Check if a property name is valid for animation on this drawable type
|
||||||
|
virtual bool hasProperty(const std::string& name) const { return false; }
|
||||||
|
|
||||||
|
// Note: animate_helper is now a free function (UIDrawable_animate_impl) declared in UIBase.h
|
||||||
|
// to avoid incomplete type issues with template instantiation.
|
||||||
|
|
||||||
// Python object cache support
|
// Python object cache support
|
||||||
uint64_t serial_number = 0;
|
uint64_t serial_number = 0;
|
||||||
|
|
||||||
|
|
|
||||||
143
src/UIEntity.cpp
143
src/UIEntity.cpp
|
|
@ -2,10 +2,14 @@
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
#include "PyObjectUtils.h"
|
#include "PyObjectUtils.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "PyFOV.h"
|
#include "PyFOV.h"
|
||||||
|
#include "Animation.h"
|
||||||
|
#include "PyAnimation.h"
|
||||||
|
#include "PyEasing.h"
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
#include "UIEntityPyMethods.h"
|
#include "UIEntityPyMethods.h"
|
||||||
|
|
||||||
|
|
@ -775,8 +779,26 @@ PyMethodDef UIEntity::methods[] = {
|
||||||
typedef PyUIEntityObject PyObjectType;
|
typedef PyUIEntityObject PyObjectType;
|
||||||
|
|
||||||
// Combine base methods with entity-specific methods
|
// Combine base methods with entity-specific methods
|
||||||
|
// Note: Use UIDRAWABLE_METHODS_BASE (not UIDRAWABLE_METHODS) because UIEntity is NOT a UIDrawable
|
||||||
|
// and the template-based animate helper won't work. Entity has its own animate() method.
|
||||||
PyMethodDef UIEntity_all_methods[] = {
|
PyMethodDef UIEntity_all_methods[] = {
|
||||||
UIDRAWABLE_METHODS,
|
UIDRAWABLE_METHODS_BASE,
|
||||||
|
{"animate", (PyCFunction)UIEntity::animate, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
MCRF_METHOD(Entity, animate,
|
||||||
|
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, callback=None, conflict_mode='replace')", "Animation"),
|
||||||
|
MCRF_DESC("Create and start an animation on this entity's property."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("property", "Name of the property to animate (e.g., 'x', 'y', 'sprite_index')")
|
||||||
|
MCRF_ARG("target", "Target value - float or int depending on property")
|
||||||
|
MCRF_ARG("duration", "Animation duration in seconds")
|
||||||
|
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear")
|
||||||
|
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute")
|
||||||
|
MCRF_ARG("callback", "Optional callable invoked when animation completes")
|
||||||
|
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating")
|
||||||
|
MCRF_RETURNS("Animation object for monitoring progress")
|
||||||
|
MCRF_RAISES("ValueError", "If property name is not valid for Entity (x, y, sprite_scale, sprite_index)")
|
||||||
|
MCRF_NOTE("Entity animations use grid coordinates for x/y, not pixel coordinates.")
|
||||||
|
)},
|
||||||
{"at", (PyCFunction)UIEntity::at, METH_O},
|
{"at", (PyCFunction)UIEntity::at, METH_O},
|
||||||
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
||||||
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
||||||
|
|
@ -885,3 +907,122 @@ bool UIEntity::getProperty(const std::string& name, float& value) const {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UIEntity::hasProperty(const std::string& name) const {
|
||||||
|
// Float properties
|
||||||
|
if (name == "x" || name == "y" || name == "sprite_scale") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Int properties
|
||||||
|
if (name == "sprite_index" || name == "sprite_number") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation shorthand for Entity - creates and starts an animation
|
||||||
|
PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
|
||||||
|
|
||||||
|
const char* property_name;
|
||||||
|
PyObject* target_value;
|
||||||
|
float duration;
|
||||||
|
PyObject* easing_arg = Py_None;
|
||||||
|
int delta = 0;
|
||||||
|
PyObject* callback = nullptr;
|
||||||
|
const char* conflict_mode_str = nullptr;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
|
||||||
|
&property_name, &target_value, &duration,
|
||||||
|
&easing_arg, &delta, &callback, &conflict_mode_str)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate property exists on this entity
|
||||||
|
if (!self->data->hasProperty(property_name)) {
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Property '%s' is not valid for animation on Entity. "
|
||||||
|
"Valid properties: x, y, sprite_scale, sprite_index, sprite_number",
|
||||||
|
property_name);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate callback is callable if provided
|
||||||
|
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert None to nullptr for C++
|
||||||
|
if (callback == Py_None) {
|
||||||
|
callback = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Python target value to AnimationValue
|
||||||
|
// Entity only supports float and int properties
|
||||||
|
AnimationValue animValue;
|
||||||
|
|
||||||
|
if (PyFloat_Check(target_value)) {
|
||||||
|
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
|
||||||
|
}
|
||||||
|
else if (PyLong_Check(target_value)) {
|
||||||
|
animValue = static_cast<int>(PyLong_AsLong(target_value));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Entity animations only support float or int target values");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get easing function from argument
|
||||||
|
EasingFunction easingFunc;
|
||||||
|
if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) {
|
||||||
|
return NULL; // Error already set by from_arg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse conflict mode
|
||||||
|
AnimationConflictMode conflict_mode = AnimationConflictMode::REPLACE;
|
||||||
|
if (conflict_mode_str) {
|
||||||
|
if (strcmp(conflict_mode_str, "replace") == 0) {
|
||||||
|
conflict_mode = AnimationConflictMode::REPLACE;
|
||||||
|
} else if (strcmp(conflict_mode_str, "queue") == 0) {
|
||||||
|
conflict_mode = AnimationConflictMode::QUEUE;
|
||||||
|
} else if (strcmp(conflict_mode_str, "error") == 0) {
|
||||||
|
conflict_mode = AnimationConflictMode::ERROR;
|
||||||
|
} else {
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", conflict_mode_str);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Animation
|
||||||
|
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||||
|
|
||||||
|
// Start on this entity (uses startEntity, not start)
|
||||||
|
animation->startEntity(self->data);
|
||||||
|
|
||||||
|
// Add to AnimationManager
|
||||||
|
AnimationManager::getInstance().addAnimation(animation, conflict_mode);
|
||||||
|
|
||||||
|
// Check if ERROR mode raised an exception
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and return a PyAnimation wrapper
|
||||||
|
PyTypeObject* animType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Animation");
|
||||||
|
if (!animType) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Could not find Animation type");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyAnimationObject* pyAnim = (PyAnimationObject*)animType->tp_alloc(animType, 0);
|
||||||
|
Py_DECREF(animType);
|
||||||
|
|
||||||
|
if (!pyAnim) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pyAnim->data = animation;
|
||||||
|
return (PyObject*)pyAnim;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,12 @@ public:
|
||||||
bool setProperty(const std::string& name, float value);
|
bool setProperty(const std::string& name, float value);
|
||||||
bool setProperty(const std::string& name, int value);
|
bool setProperty(const std::string& name, int value);
|
||||||
bool getProperty(const std::string& name, float& value) const;
|
bool getProperty(const std::string& name, float& value) const;
|
||||||
|
bool hasProperty(const std::string& name) const;
|
||||||
|
|
||||||
|
// Animation shorthand helper - creates and starts an animation on this entity
|
||||||
|
// Returns a PyAnimation object. Used by the .animate() method.
|
||||||
|
static PyObject* animate(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
// Methods that delegate to sprite
|
// Methods that delegate to sprite
|
||||||
sf::FloatRect get_bounds() const { return sprite.get_bounds(); }
|
sf::FloatRect get_bounds() const { return sprite.get_bounds(); }
|
||||||
void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; }
|
void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; }
|
||||||
|
|
|
||||||
|
|
@ -885,3 +885,24 @@ bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UIFrame::hasProperty(const std::string& name) const {
|
||||||
|
// Float properties
|
||||||
|
if (name == "x" || name == "y" || name == "w" || name == "h" ||
|
||||||
|
name == "outline" ||
|
||||||
|
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") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Color properties
|
||||||
|
if (name == "fill_color" || name == "outline_color") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Vector2f properties
|
||||||
|
if (name == "position" || name == "size") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ public:
|
||||||
bool getProperty(const std::string& name, float& value) const override;
|
bool getProperty(const std::string& name, float& value) const override;
|
||||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||||
|
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declaration of methods array
|
// Forward declaration of methods array
|
||||||
|
|
|
||||||
|
|
@ -3436,3 +3436,20 @@ bool UIGrid::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UIGrid::hasProperty(const std::string& name) const {
|
||||||
|
// Float properties
|
||||||
|
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 == "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") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,8 @@ public:
|
||||||
bool getProperty(const std::string& name, float& value) const override;
|
bool getProperty(const std::string& name, float& value) const override;
|
||||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||||
|
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
|
|
||||||
static int init(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static int init(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* get_grid_size(PyUIGridObject* self, void* closure);
|
static PyObject* get_grid_size(PyUIGridObject* self, void* closure);
|
||||||
static PyObject* get_grid_x(PyUIGridObject* self, void* closure);
|
static PyObject* get_grid_x(PyUIGridObject* self, void* closure);
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,24 @@ bool UILine::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Color properties
|
||||||
|
if (name == "color") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Vector2f properties
|
||||||
|
if (name == "start" || name == "end") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Python API implementation
|
// Python API implementation
|
||||||
PyObject* UILine::get_start(PyUILineObject* self, void* closure) {
|
PyObject* UILine::get_start(PyUILineObject* self, void* closure) {
|
||||||
auto vec = self->data->getStart();
|
auto vec = self->data->getStart();
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,8 @@ public:
|
||||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||||
|
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
|
|
||||||
// Python API
|
// Python API
|
||||||
static PyObject* get_start(PyUILineObject* self, void* closure);
|
static PyObject* get_start(PyUILineObject* self, void* closure);
|
||||||
static int set_start(PyUILineObject* self, PyObject* value, void* closure);
|
static int set_start(PyUILineObject* self, PyObject* value, void* closure);
|
||||||
|
|
|
||||||
|
|
@ -626,3 +626,17 @@ bool UISprite::getProperty(const std::string& name, int& value) const {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Int properties
|
||||||
|
if (name == "sprite_index" || name == "sprite_number") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ public:
|
||||||
bool getProperty(const std::string& name, float& value) const override;
|
bool getProperty(const std::string& name, float& value) const override;
|
||||||
bool getProperty(const std::string& name, int& value) const override;
|
bool getProperty(const std::string& name, int& value) const override;
|
||||||
|
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
|
|
||||||
static PyObject* get_float_member(PyUISpriteObject* self, void* closure);
|
static PyObject* get_float_member(PyUISpriteObject* self, void* closure);
|
||||||
static int set_float_member(PyUISpriteObject* self, PyObject* value, void* closure);
|
static int set_float_member(PyUISpriteObject* self, PyObject* value, void* closure);
|
||||||
|
|
|
||||||
248
tests/unit/animate_method_test.py
Normal file
248
tests/unit/animate_method_test.py
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test the new .animate() shorthand method on UIDrawable and UIEntity.
|
||||||
|
|
||||||
|
This tests issue #177 - ergonomic animation API.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_frame_animate():
|
||||||
|
"""Test animate() on Frame"""
|
||||||
|
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
|
||||||
|
|
||||||
|
# Basic float property animation
|
||||||
|
anim = frame.animate("x", 300.0, 1.0)
|
||||||
|
assert anim is not None, "animate() should return an Animation object"
|
||||||
|
|
||||||
|
# Color property animation
|
||||||
|
anim2 = frame.animate("fill_color", (255, 0, 0, 255), 0.5)
|
||||||
|
assert anim2 is not None
|
||||||
|
|
||||||
|
# Vector property animation
|
||||||
|
anim3 = frame.animate("position", (400.0, 400.0), 0.5)
|
||||||
|
assert anim3 is not None
|
||||||
|
|
||||||
|
print(" Frame animate() - PASS")
|
||||||
|
|
||||||
|
def test_caption_animate():
|
||||||
|
"""Test animate() on Caption"""
|
||||||
|
caption = mcrfpy.Caption(text="Hello", pos=(50, 50))
|
||||||
|
|
||||||
|
# Position animation
|
||||||
|
anim = caption.animate("y", 200.0, 1.0)
|
||||||
|
assert anim is not None
|
||||||
|
|
||||||
|
# Font size animation
|
||||||
|
anim2 = caption.animate("font_size", 24.0, 0.5)
|
||||||
|
assert anim2 is not None
|
||||||
|
|
||||||
|
print(" Caption animate() - PASS")
|
||||||
|
|
||||||
|
def test_sprite_animate():
|
||||||
|
"""Test animate() on Sprite"""
|
||||||
|
# Create with default texture
|
||||||
|
sprite = mcrfpy.Sprite(pos=(100, 100))
|
||||||
|
|
||||||
|
# Scale animation
|
||||||
|
anim = sprite.animate("scale", 2.0, 1.0)
|
||||||
|
assert anim is not None
|
||||||
|
|
||||||
|
# Sprite index animation
|
||||||
|
anim2 = sprite.animate("sprite_index", 5, 0.5)
|
||||||
|
assert anim2 is not None
|
||||||
|
|
||||||
|
print(" Sprite animate() - PASS")
|
||||||
|
|
||||||
|
def test_grid_animate():
|
||||||
|
"""Test animate() on Grid"""
|
||||||
|
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(50, 50), size=(300, 300))
|
||||||
|
|
||||||
|
# Zoom animation
|
||||||
|
anim = grid.animate("zoom", 2.0, 1.0)
|
||||||
|
assert anim is not None
|
||||||
|
|
||||||
|
# Center animation
|
||||||
|
anim2 = grid.animate("center", (100.0, 100.0), 0.5)
|
||||||
|
assert anim2 is not None
|
||||||
|
|
||||||
|
print(" Grid animate() - PASS")
|
||||||
|
|
||||||
|
def test_entity_animate():
|
||||||
|
"""Test animate() on Entity"""
|
||||||
|
grid = mcrfpy.Grid(grid_size=(20, 20))
|
||||||
|
entity = mcrfpy.Entity(grid_pos=(5, 5), grid=grid)
|
||||||
|
|
||||||
|
# Position animation
|
||||||
|
anim = entity.animate("x", 10.0, 1.0)
|
||||||
|
assert anim is not None
|
||||||
|
|
||||||
|
anim2 = entity.animate("y", 15.0, 1.0)
|
||||||
|
assert anim2 is not None
|
||||||
|
|
||||||
|
# Sprite index animation
|
||||||
|
anim3 = entity.animate("sprite_index", 3, 0.5)
|
||||||
|
assert anim3 is not None
|
||||||
|
|
||||||
|
print(" Entity animate() - PASS")
|
||||||
|
|
||||||
|
def test_invalid_property_raises():
|
||||||
|
"""Test that invalid property names raise ValueError"""
|
||||||
|
frame = mcrfpy.Frame()
|
||||||
|
|
||||||
|
try:
|
||||||
|
frame.animate("invalid_property", 100.0, 1.0)
|
||||||
|
print(" ERROR: Should have raised ValueError for invalid property")
|
||||||
|
return False
|
||||||
|
except ValueError as e:
|
||||||
|
# Should contain the property name in the error message
|
||||||
|
if "invalid_property" in str(e):
|
||||||
|
print(" Invalid property detection - PASS")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f" ERROR: ValueError message doesn't mention property: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: Wrong exception type: {type(e).__name__}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_entity_invalid_property():
|
||||||
|
"""Test that invalid property names raise ValueError for Entity"""
|
||||||
|
grid = mcrfpy.Grid(grid_size=(10, 10))
|
||||||
|
entity = mcrfpy.Entity(grid=grid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
entity.animate("invalid_property", 100.0, 1.0)
|
||||||
|
print(" ERROR: Should have raised ValueError for invalid Entity property")
|
||||||
|
return False
|
||||||
|
except ValueError as e:
|
||||||
|
if "invalid_property" in str(e):
|
||||||
|
print(" Entity invalid property detection - PASS")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f" ERROR: ValueError message doesn't mention property: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ERROR: Wrong exception type: {type(e).__name__}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_easing_options():
|
||||||
|
"""Test various easing parameter formats"""
|
||||||
|
frame = mcrfpy.Frame()
|
||||||
|
|
||||||
|
# String easing
|
||||||
|
anim1 = frame.animate("x", 100.0, 1.0, "easeInOut")
|
||||||
|
assert anim1 is not None, "String easing should work"
|
||||||
|
|
||||||
|
# Easing enum (if available)
|
||||||
|
try:
|
||||||
|
anim2 = frame.animate("x", 100.0, 1.0, mcrfpy.Easing.EaseIn)
|
||||||
|
assert anim2 is not None, "Enum easing should work"
|
||||||
|
except AttributeError:
|
||||||
|
pass # Easing enum might not exist
|
||||||
|
|
||||||
|
# None for linear
|
||||||
|
anim3 = frame.animate("x", 100.0, 1.0, None)
|
||||||
|
assert anim3 is not None, "None easing (linear) should work"
|
||||||
|
|
||||||
|
print(" Easing options - PASS")
|
||||||
|
|
||||||
|
def test_delta_mode():
|
||||||
|
"""Test delta=True for relative animations"""
|
||||||
|
frame = mcrfpy.Frame(pos=(100, 100))
|
||||||
|
|
||||||
|
# Absolute animation (default)
|
||||||
|
anim1 = frame.animate("x", 200.0, 1.0, delta=False)
|
||||||
|
assert anim1 is not None
|
||||||
|
|
||||||
|
# Relative animation
|
||||||
|
anim2 = frame.animate("x", 50.0, 1.0, delta=True)
|
||||||
|
assert anim2 is not None
|
||||||
|
|
||||||
|
print(" Delta mode - PASS")
|
||||||
|
|
||||||
|
def test_callback():
|
||||||
|
"""Test callback parameter"""
|
||||||
|
frame = mcrfpy.Frame()
|
||||||
|
callback_called = [False] # Use list for mutability in closure
|
||||||
|
|
||||||
|
def my_callback():
|
||||||
|
callback_called[0] = True
|
||||||
|
|
||||||
|
anim = frame.animate("x", 100.0, 0.01, callback=my_callback)
|
||||||
|
assert anim is not None, "Animation with callback should be created"
|
||||||
|
|
||||||
|
print(" Callback parameter - PASS")
|
||||||
|
|
||||||
|
def run_tests():
|
||||||
|
"""Run all tests"""
|
||||||
|
print("Testing .animate() shorthand method:")
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
|
||||||
|
# Test UIDrawable types
|
||||||
|
try:
|
||||||
|
test_frame_animate()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Frame animate() - FAIL: {e}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_caption_animate()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Caption animate() - FAIL: {e}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_sprite_animate()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Sprite animate() - FAIL: {e}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_grid_animate()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Grid animate() - FAIL: {e}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_entity_animate()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Entity animate() - FAIL: {e}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Test property validation
|
||||||
|
if not test_invalid_property_raises():
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
if not test_entity_invalid_property():
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
# Test optional parameters
|
||||||
|
try:
|
||||||
|
test_easing_options()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Easing options - FAIL: {e}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_delta_mode()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Delta mode - FAIL: {e}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
test_callback()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Callback parameter - FAIL: {e}")
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
if all_passed:
|
||||||
|
print("\nAll tests PASSED!")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("\nSome tests FAILED!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Run tests immediately (no timer needed for this test)
|
||||||
|
run_tests()
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue