From 045b6256555ad7c1d06d41baa330bf47c6ebab1c Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 3 Feb 2026 12:18:21 -0500 Subject: [PATCH] opacity + animation fixes --- src/UIArc.cpp | 11 +- src/UICaption.cpp | 18 +- src/UICircle.cpp | 9 +- src/UIFrame.cpp | 30 +- src/UILine.cpp | 11 +- src/UISprite.cpp | 18 +- src/platform/EmscriptenStubs.cpp | 146 +++++- src/scripts_playground/game.py | 3 +- src/shell.html | 750 +++++++++++++++++++++++++------ 9 files changed, 845 insertions(+), 151 deletions(-) diff --git a/src/UIArc.cpp b/src/UIArc.cpp index 2cf19df..ab12bde 100644 --- a/src/UIArc.cpp +++ b/src/UIArc.cpp @@ -284,6 +284,11 @@ bool UIArc::setProperty(const std::string& name, float value) { markDirty(); return true; } + else if (name == "opacity") { + opacity = std::clamp(value, 0.0f, 1.0f); + markDirty(); + return true; + } return false; } @@ -342,6 +347,10 @@ bool UIArc::getProperty(const std::string& name, float& value) const { value = origin.y; return true; } + else if (name == "opacity") { + value = opacity; + return true; + } return false; } @@ -364,7 +373,7 @@ 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 == "opacity" || name == "rotation" || name == "origin_x" || name == "origin_y") { return true; } diff --git a/src/UICaption.cpp b/src/UICaption.cpp index df64285..f833fb5 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -66,9 +66,10 @@ void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target) // Check visibility if (!visible) return; - // Apply opacity + // Apply opacity (multiply with fill_color alpha) auto color = text.getFillColor(); - color.a = static_cast(255 * opacity); + sf::Uint8 original_alpha = color.a; + color.a = static_cast(original_alpha * opacity); text.setFillColor(color); // Apply rotation and origin @@ -114,7 +115,7 @@ void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target) } // Restore original alpha - color.a = 255; + color.a = original_alpha; text.setFillColor(color); } @@ -604,6 +605,11 @@ bool UICaption::setProperty(const std::string& name, float value) { markDirty(); // #144 - Content change return true; } + else if (name == "opacity") { + opacity = std::clamp(value, 0.0f, 1.0f); + markDirty(); // #144 - Visual change + return true; + } else if (name == "fill_color.r") { auto color = text.getFillColor(); color.r = static_cast(std::clamp(value, 0.0f, 255.0f)); @@ -730,6 +736,10 @@ bool UICaption::getProperty(const std::string& name, float& value) const { value = text.getOutlineThickness(); return true; } + else if (name == "opacity") { + value = opacity; + return true; + } else if (name == "fill_color.r") { value = text.getFillColor().r; return true; @@ -808,7 +818,7 @@ bool UICaption::getProperty(const std::string& name, std::string& value) const { bool UICaption::hasProperty(const std::string& name) const { // Float properties if (name == "x" || name == "y" || - name == "font_size" || name == "size" || name == "outline" || + name == "font_size" || name == "size" || name == "outline" || name == "opacity" || name == "fill_color.r" || name == "fill_color.g" || name == "fill_color.b" || name == "fill_color.a" || name == "outline_color.r" || name == "outline_color.g" || diff --git a/src/UICircle.cpp b/src/UICircle.cpp index 7224a6c..b7a9550 100644 --- a/src/UICircle.cpp +++ b/src/UICircle.cpp @@ -230,6 +230,10 @@ bool UICircle::setProperty(const std::string& name, float value) { shape.setOrigin(radius + origin.x, radius + origin.y); markDirty(); return true; + } else if (name == "opacity") { + opacity = std::clamp(value, 0.0f, 1.0f); + markDirty(); + return true; } return false; } @@ -278,6 +282,9 @@ bool UICircle::getProperty(const std::string& name, float& value) const { } else if (name == "origin_y") { value = origin.y; return true; + } else if (name == "opacity") { + value = opacity; + return true; } return false; } @@ -303,7 +310,7 @@ 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" || + if (name == "radius" || name == "outline" || name == "opacity" || name == "x" || name == "y" || name == "rotation" || name == "origin_x" || name == "origin_y") { return true; diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 02a8bde..ca8bb36 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -126,7 +126,15 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) // Check visibility if (!visible) return; - // TODO: Apply opacity when SFML supports it on shapes + // Apply opacity (multiply with fill/outline color alpha) + auto fill_color = box.getFillColor(); + auto outline_color = box.getOutlineColor(); + sf::Uint8 original_fill_alpha = fill_color.a; + sf::Uint8 original_outline_alpha = outline_color.a; + fill_color.a = static_cast(original_fill_alpha * opacity); + outline_color.a = static_cast(original_outline_alpha * opacity); + box.setFillColor(fill_color); + box.setOutlineColor(outline_color); // #144: Use RenderTexture for clipping OR texture caching OR shaders (#106) // clip_children: requires texture for clipping effect (only when has children) @@ -194,6 +202,11 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) render_sprite.setOrigin(origin); render_sprite.setRotation(rotation); + // Apply opacity to render_sprite + auto sprite_color = render_sprite.getColor(); + sprite_color.a = static_cast(255 * opacity); + render_sprite.setColor(sprite_color); + // #106: Apply shader if set if (shader && shader->shader) { // Apply engine uniforms (time, resolution, mouse, texture) @@ -236,6 +249,12 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) drawable->render(offset + position, target); // Use `position` as source of truth } } + + // Restore original colors + fill_color.a = original_fill_alpha; + outline_color.a = original_outline_alpha; + box.setFillColor(fill_color); + box.setOutlineColor(outline_color); } PyObject* UIFrame::get_children(PyUIFrameObject* self, void* closure) @@ -836,6 +855,10 @@ bool UIFrame::setProperty(const std::string& name, float value) { box.setOutlineThickness(value); markDirty(); return true; + } else if (name == "opacity") { + opacity = std::clamp(value, 0.0f, 1.0f); + markDirty(); + return true; } else if (name == "fill_color.r") { auto color = box.getFillColor(); color.r = std::clamp(static_cast(value), 0, 255); @@ -960,6 +983,9 @@ bool UIFrame::getProperty(const std::string& name, float& value) const { } else if (name == "outline") { value = box.getOutlineThickness(); return true; + } else if (name == "opacity") { + value = opacity; + return true; } else if (name == "fill_color.r") { value = box.getFillColor().r; return true; @@ -1029,7 +1055,7 @@ bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const { bool UIFrame::hasProperty(const std::string& name) const { // Float properties if (name == "x" || name == "y" || name == "w" || name == "h" || - name == "outline" || + name == "outline" || name == "opacity" || name == "fill_color.r" || name == "fill_color.g" || name == "fill_color.b" || name == "fill_color.a" || name == "outline_color.r" || name == "outline_color.g" || diff --git a/src/UILine.cpp b/src/UILine.cpp index b98ce2a..b42b888 100644 --- a/src/UILine.cpp +++ b/src/UILine.cpp @@ -284,6 +284,11 @@ bool UILine::setProperty(const std::string& name, float value) { markDirty(); return true; } + else if (name == "opacity") { + opacity = std::clamp(value, 0.0f, 1.0f); + markDirty(); + return true; + } return false; } @@ -354,6 +359,10 @@ bool UILine::getProperty(const std::string& name, float& value) const { value = origin.y; return true; } + else if (name == "opacity") { + value = opacity; + return true; + } return false; } @@ -379,7 +388,7 @@ bool UILine::getProperty(const std::string& name, sf::Vector2f& value) const { bool UILine::hasProperty(const std::string& name) const { // Float properties - if (name == "thickness" || name == "x" || name == "y" || + if (name == "thickness" || name == "x" || name == "y" || name == "opacity" || name == "start_x" || name == "start_y" || name == "end_x" || name == "end_y" || name == "rotation" || name == "origin_x" || name == "origin_y") { diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 4281b2c..fd1ff00 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -109,9 +109,10 @@ void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target) // Check visibility if (!visible) return; - // Apply opacity + // Apply opacity (multiply with sprite color alpha) auto color = sprite.getColor(); - color.a = static_cast(255 * opacity); + sf::Uint8 original_alpha = color.a; + color.a = static_cast(original_alpha * opacity); sprite.setColor(color); // Apply rotation and origin @@ -157,7 +158,7 @@ void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target) } // Restore original alpha - color.a = 255; + color.a = original_alpha; sprite.setColor(color); } @@ -653,6 +654,11 @@ bool UISprite::setProperty(const std::string& name, float value) { markDirty(); // #144 - Content change return true; } + else if (name == "opacity") { + opacity = std::clamp(value, 0.0f, 1.0f); + markDirty(); // #144 - Visual change + return true; + } else if (name == "z_index") { z_index = static_cast(value); markDirty(); // #144 - Z-order change affects parent @@ -718,6 +724,10 @@ bool UISprite::getProperty(const std::string& name, float& value) const { value = sprite.getScale().y; return true; } + else if (name == "opacity") { + value = opacity; + return true; + } else if (name == "z_index") { value = static_cast(z_index); return true; @@ -757,7 +767,7 @@ 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 == "opacity" || name == "z_index" || name == "rotation" || name == "origin_x" || name == "origin_y") { return true; } diff --git a/src/platform/EmscriptenStubs.cpp b/src/platform/EmscriptenStubs.cpp index 666374d..aef83b6 100644 --- a/src/platform/EmscriptenStubs.cpp +++ b/src/platform/EmscriptenStubs.cpp @@ -53,6 +53,16 @@ int run_python_string(const char* code) { // Returns a pointer to a static buffer (caller should copy immediately) static std::string python_output_buffer; +// Helper to safely delete a key from a dict without leaving error state +static void safe_dict_del(PyObject* dict, const char* key) { + PyObject* py_key = PyUnicode_FromString(key); + if (py_key && PyDict_Contains(dict, py_key)) { + PyDict_DelItem(dict, py_key); + } + Py_XDECREF(py_key); + PyErr_Clear(); // Clear any error from Contains or Del +} + EMSCRIPTEN_KEEPALIVE const char* run_python_string_with_output(const char* code) { if (!Py_IsInitialized()) { @@ -60,6 +70,12 @@ const char* run_python_string_with_output(const char* code) { return python_output_buffer.c_str(); } + // CRITICAL: Clear any lingering error state before execution + // This prevents "clogged" interpreter state from previous errors + if (PyErr_Occurred()) { + PyErr_Clear(); + } + // Redirect stdout and stderr to a StringIO, run code, capture output // Also capture repr of last expression (like Python REPL) const char* capture_code = R"( @@ -113,7 +129,37 @@ elif _mcrf_last_repr: Py_DECREF(py_code); // Run the capture code - PyRun_SimpleString(capture_code); + int result = PyRun_SimpleString(capture_code); + + // Check if capture code itself failed (e.g., internal error in REPL wrapper) + if (result != 0 || PyErr_Occurred()) { + // Capture the Python error before clearing + PyObject *ptype, *pvalue, *ptraceback; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + + python_output_buffer = "Internal REPL Error: "; + if (pvalue) { + PyObject* str = PyObject_Str(pvalue); + if (str) { + const char* err_str = PyUnicode_AsUTF8(str); + if (err_str) { + python_output_buffer += err_str; + } + Py_DECREF(str); + } + } else { + python_output_buffer += "Unknown error in capture code"; + } + + Py_XDECREF(ptype); + Py_XDECREF(pvalue); + Py_XDECREF(ptraceback); + PyErr_Clear(); // Ensure clean state for next execution + + // Still clean up what we can + safe_dict_del(main_dict, "_mcrf_user_code"); + return python_output_buffer.c_str(); + } // Get the captured output PyObject* output = PyDict_GetItemString(main_dict, "_mcrf_captured_output"); @@ -124,14 +170,22 @@ elif _mcrf_last_repr: python_output_buffer = ""; } - // Clean up temporary variables - PyDict_DelItemString(main_dict, "_mcrf_user_code"); - PyDict_DelItemString(main_dict, "_mcrf_stdout_capture"); - PyDict_DelItemString(main_dict, "_mcrf_stderr_capture"); - PyDict_DelItemString(main_dict, "_mcrf_old_stdout"); - PyDict_DelItemString(main_dict, "_mcrf_old_stderr"); - PyDict_DelItemString(main_dict, "_mcrf_exec_error"); - PyDict_DelItemString(main_dict, "_mcrf_captured_output"); + // Clean up ALL temporary variables (including previously missed ones) + safe_dict_del(main_dict, "_mcrf_user_code"); + safe_dict_del(main_dict, "_mcrf_stdout_capture"); + safe_dict_del(main_dict, "_mcrf_stderr_capture"); + safe_dict_del(main_dict, "_mcrf_old_stdout"); + safe_dict_del(main_dict, "_mcrf_old_stderr"); + safe_dict_del(main_dict, "_mcrf_exec_error"); + safe_dict_del(main_dict, "_mcrf_captured_output"); + safe_dict_del(main_dict, "_mcrf_last_repr"); // Previously leaked + safe_dict_del(main_dict, "_mcrf_result"); // Previously leaked + safe_dict_del(main_dict, "_mcrf_code_obj"); // Previously leaked + + // Final safety check - clear any residual error state + if (PyErr_Occurred()) { + PyErr_Clear(); + } return python_output_buffer.c_str(); } @@ -143,6 +197,11 @@ int reset_python_environment() { return -1; } + // Clear any lingering error state first + if (PyErr_Occurred()) { + PyErr_Clear(); + } + // Clear all scenes and reload game.py const char* reset_code = R"( import mcrfpy @@ -161,7 +220,74 @@ except Exception as e: print(f"Reset error: {e}") )"; - return PyRun_SimpleString(reset_code); + int result = PyRun_SimpleString(reset_code); + + // Clear any error state from reset + if (PyErr_Occurred()) { + PyErr_Clear(); + } + + return result; +} + +// ============================================================================= +// Interpreter health check functions (for debugging) +// ============================================================================= + +static std::string python_state_buffer; + +// Check the current state of the Python interpreter +// Returns: "OK", "NOT_INITIALIZED", or "ERROR_SET: " +EMSCRIPTEN_KEEPALIVE +const char* get_python_state() { + if (!Py_IsInitialized()) { + return "NOT_INITIALIZED"; + } + + if (PyErr_Occurred()) { + PyObject *ptype, *pvalue, *ptraceback; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + + python_state_buffer = "ERROR_SET: "; + if (pvalue) { + PyObject* str = PyObject_Str(pvalue); + if (str) { + const char* err_str = PyUnicode_AsUTF8(str); + if (err_str) { + python_state_buffer += err_str; + } + Py_DECREF(str); + } + } else { + python_state_buffer += "Unknown error"; + } + + // Restore the error so caller can decide what to do + PyErr_Restore(ptype, pvalue, ptraceback); + return python_state_buffer.c_str(); + } + + return "OK"; +} + +// Clear any pending Python error state +EMSCRIPTEN_KEEPALIVE +void clear_python_error() { + if (Py_IsInitialized() && PyErr_Occurred()) { + PyErr_Clear(); + } +} + +// Get a count of items in Python's global namespace (for debugging memory leaks) +EMSCRIPTEN_KEEPALIVE +int get_python_globals_count() { + if (!Py_IsInitialized()) { + return -1; + } + + PyObject* main_module = PyImport_AddModule("__main__"); + PyObject* main_dict = PyModule_GetDict(main_module); + return (int)PyDict_Size(main_dict); } } // extern "C" diff --git a/src/scripts_playground/game.py b/src/scripts_playground/game.py index e52dc29..14b3d7c 100644 --- a/src/scripts_playground/game.py +++ b/src/scripts_playground/game.py @@ -13,10 +13,11 @@ class PlaygroundScene(mcrfpy.Scene): for a in mcrfpy.animations: a.stop() for s in mcrfpy.scenes: + if s is self: continue s.unregister() + self.activate() while self.children: self.children.pop() - self.activate() scene = PlaygroundScene() scene.activate() diff --git a/src/shell.html b/src/shell.html index d334211..516e150 100644 --- a/src/shell.html +++ b/src/shell.html @@ -4,6 +4,13 @@ McRogueFace - WebGL + + + + + + +
-

McRogueFace

+
+

McRogueFace

+ Playground + +
Downloading...
@@ -226,23 +345,24 @@
-

Python REPL

+
+ +

Python REPL

+
+ - Ctrl+Enter to run
- -
Output
+
+ +
+
+ Output + Ctrl+Enter to run | Ctrl+Up/Down for history +
@@ -250,21 +370,312 @@ mcrfpy.setScene('test')
+ + + {{{ SCRIPT }}}