From d878c8684d8b745180c63911df75a41f30587df4 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 4 Jan 2026 12:59:28 -0500 Subject: [PATCH] Easing functions as enum --- src/McRFPy_API.cpp | 8 ++ src/PyAnimation.cpp | 22 +++-- src/PyEasing.cpp | 228 ++++++++++++++++++++++++++++++++++++++++++++ src/PyEasing.h | 29 ++++++ 4 files changed, 278 insertions(+), 9 deletions(-) create mode 100644 src/PyEasing.cpp create mode 100644 src/PyEasing.h diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 0f07fbc..9550b36 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -10,6 +10,7 @@ #include "PySceneObject.h" #include "PyFOV.h" #include "PyTransition.h" +#include "PyEasing.h" #include "PySound.h" #include "PyMusic.h" #include "PyKeyboard.h" @@ -429,6 +430,13 @@ PyObject* PyInit_mcrfpy() // Note: default_transition and default_transition_duration are handled via // mcrfpy_module_getattr/setattro using PyTransition::default_transition/default_duration + // Add Easing enum class (uses Python's IntEnum) + PyObject* easing_class = PyEasing::create_enum_class(m); + if (!easing_class) { + // If enum creation fails, continue without it (non-fatal) + PyErr_Clear(); + } + // Add automation submodule PyObject* automation_module = McRFPy_Automation::init_automation_module(); if (automation_module != NULL) { diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp index 9044b9f..7208fe8 100644 --- a/src/PyAnimation.cpp +++ b/src/PyAnimation.cpp @@ -1,6 +1,7 @@ #include "PyAnimation.h" #include "McRFPy_API.h" #include "McRFPy_Doc.h" +#include "PyEasing.h" #include "UIDrawable.h" #include "UIFrame.h" #include "UICaption.h" @@ -20,16 +21,16 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) { static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr}; - + const char* property_name; PyObject* target_value; float duration; - const char* easing_name = "linear"; + PyObject* easing_arg = Py_None; int delta = 0; PyObject* callback = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast(keywords), - &property_name, &target_value, &duration, &easing_name, &delta, &callback)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpO", const_cast(keywords), + &property_name, &target_value, &duration, &easing_arg, &delta, &callback)) { return -1; } @@ -98,10 +99,13 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) { PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string"); return -1; } - - // Get easing function - EasingFunction easingFunc = EasingFunctions::getByName(easing_name); - + + // Get easing function from argument (enum, string, int, or None) + EasingFunction easingFunc; + if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) { + return -1; // Error already set by from_arg + } + // Create the Animation self->data = std::make_shared(property_name, animValue, duration, easingFunc, delta != 0, callback); diff --git a/src/PyEasing.cpp b/src/PyEasing.cpp new file mode 100644 index 0000000..95e8978 --- /dev/null +++ b/src/PyEasing.cpp @@ -0,0 +1,228 @@ +#include "PyEasing.h" +#include "McRFPy_API.h" + +// Static storage for cached enum class reference +PyObject* PyEasing::easing_enum_class = nullptr; + +// Easing function table - maps enum value to function and name +struct EasingEntry { + const char* name; + int value; + EasingFunction func; +}; + +static const EasingEntry easing_table[] = { + {"LINEAR", 0, EasingFunctions::linear}, + {"EASE_IN", 1, EasingFunctions::easeIn}, + {"EASE_OUT", 2, EasingFunctions::easeOut}, + {"EASE_IN_OUT", 3, EasingFunctions::easeInOut}, + {"EASE_IN_QUAD", 4, EasingFunctions::easeInQuad}, + {"EASE_OUT_QUAD", 5, EasingFunctions::easeOutQuad}, + {"EASE_IN_OUT_QUAD", 6, EasingFunctions::easeInOutQuad}, + {"EASE_IN_CUBIC", 7, EasingFunctions::easeInCubic}, + {"EASE_OUT_CUBIC", 8, EasingFunctions::easeOutCubic}, + {"EASE_IN_OUT_CUBIC", 9, EasingFunctions::easeInOutCubic}, + {"EASE_IN_QUART", 10, EasingFunctions::easeInQuart}, + {"EASE_OUT_QUART", 11, EasingFunctions::easeOutQuart}, + {"EASE_IN_OUT_QUART", 12, EasingFunctions::easeInOutQuart}, + {"EASE_IN_SINE", 13, EasingFunctions::easeInSine}, + {"EASE_OUT_SINE", 14, EasingFunctions::easeOutSine}, + {"EASE_IN_OUT_SINE", 15, EasingFunctions::easeInOutSine}, + {"EASE_IN_EXPO", 16, EasingFunctions::easeInExpo}, + {"EASE_OUT_EXPO", 17, EasingFunctions::easeOutExpo}, + {"EASE_IN_OUT_EXPO", 18, EasingFunctions::easeInOutExpo}, + {"EASE_IN_CIRC", 19, EasingFunctions::easeInCirc}, + {"EASE_OUT_CIRC", 20, EasingFunctions::easeOutCirc}, + {"EASE_IN_OUT_CIRC", 21, EasingFunctions::easeInOutCirc}, + {"EASE_IN_ELASTIC", 22, EasingFunctions::easeInElastic}, + {"EASE_OUT_ELASTIC", 23, EasingFunctions::easeOutElastic}, + {"EASE_IN_OUT_ELASTIC", 24, EasingFunctions::easeInOutElastic}, + {"EASE_IN_BACK", 25, EasingFunctions::easeInBack}, + {"EASE_OUT_BACK", 26, EasingFunctions::easeOutBack}, + {"EASE_IN_OUT_BACK", 27, EasingFunctions::easeInOutBack}, + {"EASE_IN_BOUNCE", 28, EasingFunctions::easeInBounce}, + {"EASE_OUT_BOUNCE", 29, EasingFunctions::easeOutBounce}, + {"EASE_IN_OUT_BOUNCE", 30, EasingFunctions::easeInOutBounce}, +}; + +// Old string names (for backwards compatibility) +static const char* legacy_names[] = { + "linear", "easeIn", "easeOut", "easeInOut", + "easeInQuad", "easeOutQuad", "easeInOutQuad", + "easeInCubic", "easeOutCubic", "easeInOutCubic", + "easeInQuart", "easeOutQuart", "easeInOutQuart", + "easeInSine", "easeOutSine", "easeInOutSine", + "easeInExpo", "easeOutExpo", "easeInOutExpo", + "easeInCirc", "easeOutCirc", "easeInOutCirc", + "easeInElastic", "easeOutElastic", "easeInOutElastic", + "easeInBack", "easeOutBack", "easeInOutBack", + "easeInBounce", "easeOutBounce", "easeInOutBounce" +}; + +static const int NUM_EASING_ENTRIES = sizeof(easing_table) / sizeof(easing_table[0]); + +const char* PyEasing::easing_name(int value) { + if (value >= 0 && value < NUM_EASING_ENTRIES) { + return easing_table[value].name; + } + return "LINEAR"; +} + +PyObject* PyEasing::create_enum_class(PyObject* module) { + // Import IntEnum from enum module + PyObject* enum_module = PyImport_ImportModule("enum"); + if (!enum_module) { + return NULL; + } + + PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum"); + Py_DECREF(enum_module); + if (!int_enum) { + return NULL; + } + + // Create dict of enum members + PyObject* members = PyDict_New(); + if (!members) { + Py_DECREF(int_enum); + return NULL; + } + + // Add all easing function members + for (int i = 0; i < NUM_EASING_ENTRIES; i++) { + PyObject* value = PyLong_FromLong(easing_table[i].value); + if (!value) { + Py_DECREF(members); + Py_DECREF(int_enum); + return NULL; + } + if (PyDict_SetItemString(members, easing_table[i].name, value) < 0) { + Py_DECREF(value); + Py_DECREF(members); + Py_DECREF(int_enum); + return NULL; + } + Py_DECREF(value); + } + + // Call IntEnum("Easing", members) to create the enum class + PyObject* name = PyUnicode_FromString("Easing"); + if (!name) { + Py_DECREF(members); + Py_DECREF(int_enum); + return NULL; + } + + // IntEnum(name, members) using functional API + PyObject* args = PyTuple_Pack(2, name, members); + Py_DECREF(name); + Py_DECREF(members); + if (!args) { + Py_DECREF(int_enum); + return NULL; + } + + PyObject* easing_class = PyObject_Call(int_enum, args, NULL); + Py_DECREF(args); + Py_DECREF(int_enum); + + if (!easing_class) { + return NULL; + } + + // Cache the reference for fast type checking + easing_enum_class = easing_class; + Py_INCREF(easing_enum_class); + + // Add to module + if (PyModule_AddObject(module, "Easing", easing_class) < 0) { + Py_DECREF(easing_class); + easing_enum_class = nullptr; + return NULL; + } + + return easing_class; +} + +int PyEasing::from_arg(PyObject* arg, EasingFunction* out_func, bool* was_none) { + if (was_none) *was_none = false; + + // Accept None -> default to linear + if (arg == Py_None) { + if (was_none) *was_none = true; + *out_func = EasingFunctions::linear; + return 1; + } + + // Accept Easing enum member (check if it's an instance of our enum) + if (easing_enum_class && PyObject_IsInstance(arg, easing_enum_class)) { + // IntEnum members have a 'value' attribute + PyObject* value = PyObject_GetAttrString(arg, "value"); + if (!value) { + return 0; + } + long val = PyLong_AsLong(value); + Py_DECREF(value); + if (val == -1 && PyErr_Occurred()) { + return 0; + } + if (val >= 0 && val < NUM_EASING_ENTRIES) { + *out_func = easing_table[val].func; + return 1; + } + PyErr_Format(PyExc_ValueError, + "Invalid Easing value: %ld. Must be 0-%d.", val, NUM_EASING_ENTRIES - 1); + return 0; + } + + // Accept int (for backwards compatibility and direct enum value access) + if (PyLong_Check(arg)) { + long val = PyLong_AsLong(arg); + if (val == -1 && PyErr_Occurred()) { + return 0; + } + if (val >= 0 && val < NUM_EASING_ENTRIES) { + *out_func = easing_table[val].func; + return 1; + } + PyErr_Format(PyExc_ValueError, + "Invalid easing value: %ld. Must be 0-%d or use mcrfpy.Easing enum.", + val, NUM_EASING_ENTRIES - 1); + return 0; + } + + // Accept string (for backwards compatibility) + if (PyUnicode_Check(arg)) { + const char* name = PyUnicode_AsUTF8(arg); + if (!name) { + return 0; + } + + // Check legacy string names first + for (int i = 0; i < NUM_EASING_ENTRIES; i++) { + if (strcmp(name, legacy_names[i]) == 0) { + *out_func = easing_table[i].func; + return 1; + } + } + + // Also check enum-style names (EASE_IN_OUT, etc.) + for (int i = 0; i < NUM_EASING_ENTRIES; i++) { + if (strcmp(name, easing_table[i].name) == 0) { + *out_func = easing_table[i].func; + return 1; + } + } + + // Build error message with available options + PyErr_Format(PyExc_ValueError, + "Unknown easing function: '%s'. Use mcrfpy.Easing enum (e.g., Easing.EASE_IN_OUT) " + "or legacy string names: 'linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInQuad', etc.", + name); + return 0; + } + + PyErr_SetString(PyExc_TypeError, + "Easing must be mcrfpy.Easing enum member, string, int, or None"); + return 0; +} diff --git a/src/PyEasing.h b/src/PyEasing.h new file mode 100644 index 0000000..43f2575 --- /dev/null +++ b/src/PyEasing.h @@ -0,0 +1,29 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include "Animation.h" + +// Module-level Easing enum class (created at runtime using Python's IntEnum) +// Stored as a module attribute: mcrfpy.Easing + +class PyEasing { +public: + // Create the Easing enum class and add to module + // Returns the enum class (new reference), or NULL on error + static PyObject* create_enum_class(PyObject* module); + + // Helper to extract easing function from Python arg + // Accepts Easing enum, string (for backwards compatibility), int, or None + // Returns 1 on success, 0 on error (with exception set) + // If arg is None, sets *out_func to linear and sets *was_none to true + static int from_arg(PyObject* arg, EasingFunction* out_func, bool* was_none = nullptr); + + // Convert easing enum value to string name + static const char* easing_name(int value); + + // Cached reference to the Easing enum class for fast type checking + static PyObject* easing_enum_class; + + // Number of easing functions + static const int NUM_EASING_FUNCTIONS = 32; +};