diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index ae63247..9ccf273 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -12,10 +12,6 @@ #include #include -// Static member definitions for shader intermediate texture (#106) -std::unique_ptr GameEngine::shaderIntermediate; -bool GameEngine::shaderIntermediateInitialized = false; - // #219 - FrameLock implementation for thread-safe UI updates void FrameLock::acquire() { @@ -722,37 +718,6 @@ sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView); } -// #106 - Shader intermediate texture: shared texture for shader rendering -void GameEngine::initShaderIntermediate(unsigned int width, unsigned int height) { - if (!sf::Shader::isAvailable()) { - std::cerr << "GameEngine: Shaders not available, skipping intermediate texture init" << std::endl; - return; - } - - if (!shaderIntermediate) { - shaderIntermediate = std::make_unique(); - } - - if (!shaderIntermediate->create(width, height)) { - std::cerr << "GameEngine: Failed to create shader intermediate texture (" - << width << "x" << height << ")" << std::endl; - shaderIntermediate.reset(); - shaderIntermediateInitialized = false; - return; - } - - shaderIntermediate->setSmooth(false); // Pixel-perfect rendering - shaderIntermediateInitialized = true; -} - -sf::RenderTexture& GameEngine::getShaderIntermediate() { - if (!shaderIntermediateInitialized) { - // Initialize with default resolution if not already done - initShaderIntermediate(1024, 768); - } - return *shaderIntermediate; -} - // #153 - Headless simulation control: step() advances simulation time float GameEngine::step(float dt) { // In windowed mode, step() is a no-op diff --git a/src/GameEngine.h b/src/GameEngine.h index 9e69a24..512726d 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -185,10 +185,6 @@ private: sf::View gameView; // View for the game content ViewportMode viewportMode = ViewportMode::Fit; - // Shader intermediate texture (#106) - shared texture for shader rendering - static std::unique_ptr shaderIntermediate; - static bool shaderIntermediateInitialized; - // Profiling overlay bool showProfilerOverlay = false; // F3 key toggles this int overlayUpdateCounter = 0; // Only update overlay every N frames @@ -261,11 +257,6 @@ public: std::string getViewportModeString() const; sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const; - // Shader system (#106) - shared intermediate texture for shader rendering - static sf::RenderTexture& getShaderIntermediate(); - static void initShaderIntermediate(unsigned int width, unsigned int height); - static bool isShaderIntermediateReady() { return shaderIntermediateInitialized; } - // #153 - Headless simulation control float step(float dt = -1.0f); // Advance simulation; dt<0 means advance to next event int getSimulationTime() const { return simulation_time; } diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 8514689..36d5aff 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -27,9 +27,6 @@ #include "PyNoiseSource.h" // Procedural generation noise (#207-208) #include "PyLock.h" // Thread synchronization (#219) #include "PyVector.h" // For bresenham Vector support (#215) -#include "PyShader.h" // Shader support (#106) -#include "PyUniformBinding.h" // Shader uniform bindings (#106) -#include "PyUniformCollection.h" // Shader uniform collection (#106) #include "McRogueFaceVersion.h" #include "GameEngine.h" #include "ImGuiConsole.h" @@ -455,11 +452,6 @@ PyObject* PyInit_mcrfpy() &mcrfpydef::PyBSPType, &mcrfpydef::PyNoiseSourceType, - /*shaders (#106)*/ - &mcrfpydef::PyShaderType, - &mcrfpydef::PyPropertyBindingType, - &mcrfpydef::PyCallableBindingType, - nullptr}; // Types that are used internally but NOT exported to module namespace (#189) @@ -481,9 +473,6 @@ PyObject* PyInit_mcrfpy() &mcrfpydef::PyBSPAdjacencyType, // #210: BSP.adjacency wrapper &mcrfpydef::PyBSPAdjacentTilesType, // #210: BSPNode.adjacent_tiles wrapper - /*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/ - &mcrfpydef::PyUniformCollectionType, - nullptr}; // Set up PyWindowType methods and getsetters before PyType_Ready @@ -508,17 +497,6 @@ PyObject* PyInit_mcrfpy() mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods; mcrfpydef::PyNoiseSourceType.tp_getset = PyNoiseSource::getsetters; - // Set up PyShaderType methods and getsetters (#106) - mcrfpydef::PyShaderType.tp_methods = PyShader::methods; - mcrfpydef::PyShaderType.tp_getset = PyShader::getsetters; - - // Set up PyPropertyBindingType and PyCallableBindingType getsetters (#106) - mcrfpydef::PyPropertyBindingType.tp_getset = PyPropertyBindingType::getsetters; - mcrfpydef::PyCallableBindingType.tp_getset = PyCallableBindingType::getsetters; - - // Set up PyUniformCollectionType methods (#106) - mcrfpydef::PyUniformCollectionType.tp_methods = ::PyUniformCollectionType::methods; - // Set up weakref support for all types that need it PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist); PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist); diff --git a/src/PyShader.cpp b/src/PyShader.cpp deleted file mode 100644 index 92ff99d..0000000 --- a/src/PyShader.cpp +++ /dev/null @@ -1,256 +0,0 @@ -#include "PyShader.h" -#include "McRFPy_API.h" -#include "McRFPy_Doc.h" -#include "GameEngine.h" -#include "Resources.h" -#include - -// Static clock for time uniform -static sf::Clock shader_engine_clock; -static sf::Clock shader_frame_clock; - -// Python method and getset definitions -PyGetSetDef PyShader::getsetters[] = { - {"dynamic", (getter)PyShader::get_dynamic, (setter)PyShader::set_dynamic, - MCRF_PROPERTY(dynamic, - "Whether this shader uses time-varying effects (bool). " - "Dynamic shaders invalidate parent caches each frame."), NULL}, - {"source", (getter)PyShader::get_source, NULL, - MCRF_PROPERTY(source, - "The GLSL fragment shader source code (str, read-only)."), NULL}, - {"is_valid", (getter)PyShader::get_is_valid, NULL, - MCRF_PROPERTY(is_valid, - "True if the shader compiled successfully (bool, read-only)."), NULL}, - {NULL} -}; - -PyMethodDef PyShader::methods[] = { - {"set_uniform", (PyCFunction)PyShader::set_uniform, METH_VARARGS | METH_KEYWORDS, - MCRF_METHOD(Shader, set_uniform, - MCRF_SIG("(name: str, value: float|tuple)", "None"), - MCRF_DESC("Set a custom uniform value on this shader."), - MCRF_ARGS_START - MCRF_ARG("name", "Uniform variable name in the shader") - MCRF_ARG("value", "Float, vec2 (2-tuple), vec3 (3-tuple), or vec4 (4-tuple)") - MCRF_RAISES("ValueError", "If uniform type cannot be determined") - MCRF_NOTE("Engine uniforms (time, resolution, etc.) are set automatically") - )}, - {NULL} -}; - -// Constructor -PyObject* PyShader::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PyShaderObject* self = (PyShaderObject*)type->tp_alloc(type, 0); - if (self) { - self->shader = nullptr; - self->dynamic = false; - self->weakreflist = NULL; - new (&self->fragment_source) std::string(); - } - return (PyObject*)self; -} - -// Destructor -void PyShader::dealloc(PyShaderObject* self) -{ - // Clear weak references - if (self->weakreflist) { - PyObject_ClearWeakRefs((PyObject*)self); - } - - // Destroy C++ objects - self->shader.reset(); - self->fragment_source.~basic_string(); - - // Free Python object - Py_TYPE(self)->tp_free((PyObject*)self); -} - -// Initializer -int PyShader::init(PyShaderObject* self, PyObject* args, PyObject* kwds) -{ - static const char* keywords[] = {"fragment_source", "dynamic", nullptr}; - const char* source = nullptr; - int dynamic = 0; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|p", const_cast(keywords), - &source, &dynamic)) { - return -1; - } - - // Check if shaders are available - if (!sf::Shader::isAvailable()) { - PyErr_SetString(PyExc_RuntimeError, - "Shaders are not available on this system (no GPU support or OpenGL too old)"); - return -1; - } - - // Store source and dynamic flag - self->fragment_source = source; - self->dynamic = (bool)dynamic; - - // Create and compile the shader - self->shader = std::make_shared(); - - // Capture sf::err() output during shader compilation - std::streambuf* oldBuf = sf::err().rdbuf(); - std::ostringstream errStream; - sf::err().rdbuf(errStream.rdbuf()); - - bool success = self->shader->loadFromMemory(source, sf::Shader::Fragment); - - // Restore sf::err() and check for errors - sf::err().rdbuf(oldBuf); - - if (!success) { - std::string error_msg = errStream.str(); - if (error_msg.empty()) { - error_msg = "Shader compilation failed (unknown error)"; - } - PyErr_Format(PyExc_ValueError, "Shader compilation failed: %s", error_msg.c_str()); - self->shader.reset(); - return -1; - } - - return 0; -} - -// Repr -PyObject* PyShader::repr(PyObject* obj) -{ - PyShaderObject* self = (PyShaderObject*)obj; - std::ostringstream ss; - ss << "shader) { - ss << " valid"; - } else { - ss << " invalid"; - } - if (self->dynamic) { - ss << " dynamic"; - } - ss << ">"; - return PyUnicode_FromString(ss.str().c_str()); -} - -// Property: dynamic -PyObject* PyShader::get_dynamic(PyShaderObject* self, void* closure) -{ - return PyBool_FromLong(self->dynamic); -} - -int PyShader::set_dynamic(PyShaderObject* self, PyObject* value, void* closure) -{ - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "dynamic must be a boolean"); - return -1; - } - self->dynamic = PyObject_IsTrue(value); - return 0; -} - -// Property: source (read-only) -PyObject* PyShader::get_source(PyShaderObject* self, void* closure) -{ - return PyUnicode_FromString(self->fragment_source.c_str()); -} - -// Property: is_valid (read-only) -PyObject* PyShader::get_is_valid(PyShaderObject* self, void* closure) -{ - return PyBool_FromLong(self->shader != nullptr); -} - -// Method: set_uniform -PyObject* PyShader::set_uniform(PyShaderObject* self, PyObject* args, PyObject* kwds) -{ - static const char* keywords[] = {"name", "value", nullptr}; - const char* name = nullptr; - PyObject* value = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sO", const_cast(keywords), - &name, &value)) { - return NULL; - } - - if (!self->shader) { - PyErr_SetString(PyExc_RuntimeError, "Shader is not valid"); - return NULL; - } - - // Determine the type and set uniform - if (PyFloat_Check(value) || PyLong_Check(value)) { - // Single float - float f = (float)PyFloat_AsDouble(value); - if (PyErr_Occurred()) return NULL; - self->shader->setUniform(name, f); - } - else if (PyTuple_Check(value)) { - Py_ssize_t size = PyTuple_Size(value); - if (size == 2) { - // vec2 - float x = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 0)); - float y = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 1)); - if (PyErr_Occurred()) return NULL; - self->shader->setUniform(name, sf::Glsl::Vec2(x, y)); - } - else if (size == 3) { - // vec3 - float x = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 0)); - float y = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 1)); - float z = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 2)); - if (PyErr_Occurred()) return NULL; - self->shader->setUniform(name, sf::Glsl::Vec3(x, y, z)); - } - else if (size == 4) { - // vec4 - float x = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 0)); - float y = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 1)); - float z = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 2)); - float w = (float)PyFloat_AsDouble(PyTuple_GetItem(value, 3)); - if (PyErr_Occurred()) return NULL; - self->shader->setUniform(name, sf::Glsl::Vec4(x, y, z, w)); - } - else { - PyErr_Format(PyExc_ValueError, - "Tuple must have 2, 3, or 4 elements for vec2/vec3/vec4, got %zd", size); - return NULL; - } - } - else { - PyErr_SetString(PyExc_TypeError, - "Uniform value must be a float or tuple of 2-4 floats"); - return NULL; - } - - Py_RETURN_NONE; -} - -// Static helper: apply engine-provided uniforms -void PyShader::applyEngineUniforms(sf::Shader& shader, sf::Vector2f resolution) -{ - // Time uniforms - shader.setUniform("time", shader_engine_clock.getElapsedTime().asSeconds()); - shader.setUniform("delta_time", shader_frame_clock.restart().asSeconds()); - - // Resolution - shader.setUniform("resolution", resolution); - - // Mouse position - get from GameEngine if available - sf::Vector2f mouse(0.f, 0.f); - if (Resources::game && !Resources::game->isHeadless()) { - sf::Vector2i mousePos = sf::Mouse::getPosition(Resources::game->getWindow()); - mouse = sf::Vector2f(static_cast(mousePos.x), static_cast(mousePos.y)); - } - shader.setUniform("mouse", mouse); - - // CurrentTexture is handled by SFML automatically when drawing - shader.setUniform("texture", sf::Shader::CurrentTexture); -} - -// Static helper: check availability -bool PyShader::isAvailable() -{ - return sf::Shader::isAvailable(); -} diff --git a/src/PyShader.h b/src/PyShader.h deleted file mode 100644 index 911dcce..0000000 --- a/src/PyShader.h +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once -#include "Common.h" -#include "Python.h" - -// Forward declarations -class UIDrawable; - -// Python object structure for Shader -typedef struct PyShaderObjectStruct { - PyObject_HEAD - std::shared_ptr shader; - bool dynamic; // Time-varying shader (affects caching) - std::string fragment_source; // Source code for recompilation - PyObject* weakreflist; // Support weak references -} PyShaderObject; - -class PyShader -{ -public: - // Python type methods - static PyObject* repr(PyObject* self); - static int init(PyShaderObject* self, PyObject* args, PyObject* kwds); - static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); - static void dealloc(PyShaderObject* self); - - // Property getters/setters - static PyObject* get_dynamic(PyShaderObject* self, void* closure); - static int set_dynamic(PyShaderObject* self, PyObject* value, void* closure); - static PyObject* get_source(PyShaderObject* self, void* closure); - static PyObject* get_is_valid(PyShaderObject* self, void* closure); - - // Methods - static PyObject* set_uniform(PyShaderObject* self, PyObject* args, PyObject* kwds); - - // Static helper: apply engine-provided uniforms (time, resolution, etc.) - static void applyEngineUniforms(sf::Shader& shader, sf::Vector2f resolution); - - // Check if shaders are available on this system - static bool isAvailable(); - - // Arrays for Python type definition - static PyGetSetDef getsetters[]; - static PyMethodDef methods[]; -}; - -namespace mcrfpydef { - // Using inline to ensure single definition across translation units (C++17) - inline PyTypeObject PyShaderType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.Shader", - .tp_basicsize = sizeof(PyShaderObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)PyShader::dealloc, - .tp_repr = PyShader::repr, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR( - "Shader(fragment_source: str, dynamic: bool = False)\n" - "\n" - "A GPU shader program for visual effects.\n" - "\n" - "Args:\n" - " fragment_source: GLSL fragment shader source code\n" - " dynamic: If True, shader uses time-varying effects and will\n" - " invalidate parent caches each frame\n" - "\n" - "Shaders enable GPU-accelerated visual effects like glow, distortion,\n" - "color manipulation, and more. Assign to drawable.shader to apply.\n" - "\n" - "Engine-provided uniforms (automatically available):\n" - " - float time: Seconds since engine start\n" - " - float delta_time: Seconds since last frame\n" - " - vec2 resolution: Texture size in pixels\n" - " - vec2 mouse: Mouse position in window coordinates\n" - "\n" - "Example:\n" - " shader = mcrfpy.Shader('''\n" - " uniform sampler2D texture;\n" - " uniform float time;\n" - " void main() {\n" - " vec2 uv = gl_TexCoord[0].xy;\n" - " vec4 color = texture2D(texture, uv);\n" - " color.rgb *= 0.5 + 0.5 * sin(time);\n" - " gl_FragColor = color;\n" - " }\n" - " ''', dynamic=True)\n" - " frame.shader = shader\n" - ), - .tp_weaklistoffset = offsetof(PyShaderObject, weakreflist), - .tp_methods = nullptr, // Set in McRFPy_API.cpp before PyType_Ready - .tp_getset = nullptr, // Set in McRFPy_API.cpp before PyType_Ready - .tp_init = (initproc)PyShader::init, - .tp_new = PyShader::pynew, - }; -} diff --git a/src/PyUniformBinding.cpp b/src/PyUniformBinding.cpp deleted file mode 100644 index 27afbcd..0000000 --- a/src/PyUniformBinding.cpp +++ /dev/null @@ -1,341 +0,0 @@ -#include "PyUniformBinding.h" -#include "UIDrawable.h" -#include "UIFrame.h" -#include "UICaption.h" -#include "UISprite.h" -#include "UIGrid.h" -#include "UILine.h" -#include "UICircle.h" -#include "UIArc.h" -#include "McRFPy_API.h" -#include "McRFPy_Doc.h" -#include -#include - -// ============================================================================ -// PropertyBinding Implementation -// ============================================================================ - -PropertyBinding::PropertyBinding(std::weak_ptr target, const std::string& property) - : target(target), property_name(property) {} - -std::optional PropertyBinding::evaluate() const { - auto ptr = target.lock(); - if (!ptr) return std::nullopt; - - float value = 0.0f; - if (ptr->getProperty(property_name, value)) { - return value; - } - return std::nullopt; -} - -bool PropertyBinding::isValid() const { - auto ptr = target.lock(); - if (!ptr) return false; - return ptr->hasProperty(property_name); -} - -// ============================================================================ -// CallableBinding Implementation -// ============================================================================ - -CallableBinding::CallableBinding(PyObject* callable) - : callable(callable) { - if (callable) { - Py_INCREF(callable); - } -} - -CallableBinding::~CallableBinding() { - if (callable) { - Py_DECREF(callable); - } -} - -CallableBinding::CallableBinding(CallableBinding&& other) noexcept - : callable(other.callable) { - other.callable = nullptr; -} - -CallableBinding& CallableBinding::operator=(CallableBinding&& other) noexcept { - if (this != &other) { - if (callable) { - Py_DECREF(callable); - } - callable = other.callable; - other.callable = nullptr; - } - return *this; -} - -std::optional CallableBinding::evaluate() const { - if (!callable || !PyCallable_Check(callable)) { - return std::nullopt; - } - - PyObject* result = PyObject_CallNoArgs(callable); - if (!result) { - // Python exception occurred - print and clear it - PyErr_Print(); - return std::nullopt; - } - - float value = 0.0f; - if (PyFloat_Check(result)) { - value = static_cast(PyFloat_AsDouble(result)); - } else if (PyLong_Check(result)) { - value = static_cast(PyLong_AsDouble(result)); - } else { - // Try to convert to float - PyObject* float_result = PyNumber_Float(result); - if (float_result) { - value = static_cast(PyFloat_AsDouble(float_result)); - Py_DECREF(float_result); - } else { - PyErr_Clear(); - Py_DECREF(result); - return std::nullopt; - } - } - - Py_DECREF(result); - return value; -} - -bool CallableBinding::isValid() const { - return callable && PyCallable_Check(callable); -} - -// ============================================================================ -// PyPropertyBindingType Python Interface -// ============================================================================ - -PyGetSetDef PyPropertyBindingType::getsetters[] = { - {"target", (getter)PyPropertyBindingType::get_target, NULL, - MCRF_PROPERTY(target, "The drawable this binding reads from (read-only)."), NULL}, - {"property", (getter)PyPropertyBindingType::get_property, NULL, - MCRF_PROPERTY(property, "The property name being read (str, read-only)."), NULL}, - {"value", (getter)PyPropertyBindingType::get_value, NULL, - MCRF_PROPERTY(value, "Current value of the binding (float, read-only). Returns None if invalid."), NULL}, - {"is_valid", (getter)PyPropertyBindingType::is_valid, NULL, - MCRF_PROPERTY(is_valid, "True if the binding target still exists and property is valid (bool, read-only)."), NULL}, - {NULL} -}; - -PyObject* PyPropertyBindingType::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { - PyPropertyBindingObject* self = (PyPropertyBindingObject*)type->tp_alloc(type, 0); - if (self) { - self->binding = nullptr; - self->weakreflist = NULL; - } - return (PyObject*)self; -} - -void PyPropertyBindingType::dealloc(PyPropertyBindingObject* self) { - if (self->weakreflist) { - PyObject_ClearWeakRefs((PyObject*)self); - } - self->binding.reset(); - Py_TYPE(self)->tp_free((PyObject*)self); -} - -int PyPropertyBindingType::init(PyPropertyBindingObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = {"target", "property", nullptr}; - PyObject* target_obj = nullptr; - const char* property = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "Os", const_cast(keywords), - &target_obj, &property)) { - return -1; - } - - // Get the shared_ptr from the target drawable by checking the type name - // We can't use PyObject_IsInstance with static type objects from other translation units - // So we check the type name string instead - std::shared_ptr target_ptr; - const char* type_name = Py_TYPE(target_obj)->tp_name; - - if (strcmp(type_name, "mcrfpy.Frame") == 0) { - target_ptr = ((PyUIFrameObject*)target_obj)->data; - } else if (strcmp(type_name, "mcrfpy.Caption") == 0) { - target_ptr = ((PyUICaptionObject*)target_obj)->data; - } else if (strcmp(type_name, "mcrfpy.Sprite") == 0) { - target_ptr = ((PyUISpriteObject*)target_obj)->data; - } else if (strcmp(type_name, "mcrfpy.Grid") == 0) { - target_ptr = ((PyUIGridObject*)target_obj)->data; - } else if (strcmp(type_name, "mcrfpy.Line") == 0) { - target_ptr = ((PyUILineObject*)target_obj)->data; - } else if (strcmp(type_name, "mcrfpy.Circle") == 0) { - target_ptr = ((PyUICircleObject*)target_obj)->data; - } else if (strcmp(type_name, "mcrfpy.Arc") == 0) { - target_ptr = ((PyUIArcObject*)target_obj)->data; - } - - if (!target_ptr) { - PyErr_SetString(PyExc_TypeError, - "PropertyBinding requires a UIDrawable (Frame, Sprite, Caption, Grid, Line, Circle, or Arc)"); - return -1; - } - - // Validate that the property exists - if (!target_ptr->hasProperty(property)) { - PyErr_Format(PyExc_ValueError, - "Property '%s' is not a valid animatable property on this drawable", property); - return -1; - } - - self->binding = std::make_shared(target_ptr, property); - return 0; -} - -PyObject* PyPropertyBindingType::repr(PyObject* obj) { - PyPropertyBindingObject* self = (PyPropertyBindingObject*)obj; - std::ostringstream ss; - ss << "binding) { - ss << " property='" << self->binding->getPropertyName() << "'"; - if (self->binding->isValid()) { - auto val = self->binding->evaluate(); - if (val) { - ss << " value=" << *val; - } - } else { - ss << " (invalid)"; - } - } - ss << ">"; - return PyUnicode_FromString(ss.str().c_str()); -} - -PyObject* PyPropertyBindingType::get_target(PyPropertyBindingObject* self, void* closure) { - if (!self->binding) { - Py_RETURN_NONE; - } - auto ptr = self->binding->getTarget().lock(); - if (!ptr) { - Py_RETURN_NONE; - } - // TODO: Return the actual Python object for the drawable - Py_RETURN_NONE; -} - -PyObject* PyPropertyBindingType::get_property(PyPropertyBindingObject* self, void* closure) { - if (!self->binding) { - Py_RETURN_NONE; - } - return PyUnicode_FromString(self->binding->getPropertyName().c_str()); -} - -PyObject* PyPropertyBindingType::get_value(PyPropertyBindingObject* self, void* closure) { - if (!self->binding) { - Py_RETURN_NONE; - } - auto val = self->binding->evaluate(); - if (!val) { - Py_RETURN_NONE; - } - return PyFloat_FromDouble(*val); -} - -PyObject* PyPropertyBindingType::is_valid(PyPropertyBindingObject* self, void* closure) { - if (!self->binding) { - Py_RETURN_FALSE; - } - return PyBool_FromLong(self->binding->isValid()); -} - -// ============================================================================ -// PyCallableBindingType Python Interface -// ============================================================================ - -PyGetSetDef PyCallableBindingType::getsetters[] = { - {"callable", (getter)PyCallableBindingType::get_callable, NULL, - MCRF_PROPERTY(callable, "The Python callable (read-only)."), NULL}, - {"value", (getter)PyCallableBindingType::get_value, NULL, - MCRF_PROPERTY(value, "Current value from calling the callable (float, read-only). Returns None on error."), NULL}, - {"is_valid", (getter)PyCallableBindingType::is_valid, NULL, - MCRF_PROPERTY(is_valid, "True if the callable is still valid (bool, read-only)."), NULL}, - {NULL} -}; - -PyObject* PyCallableBindingType::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { - PyCallableBindingObject* self = (PyCallableBindingObject*)type->tp_alloc(type, 0); - if (self) { - self->binding = nullptr; - self->weakreflist = NULL; - } - return (PyObject*)self; -} - -void PyCallableBindingType::dealloc(PyCallableBindingObject* self) { - if (self->weakreflist) { - PyObject_ClearWeakRefs((PyObject*)self); - } - self->binding.reset(); - Py_TYPE(self)->tp_free((PyObject*)self); -} - -int PyCallableBindingType::init(PyCallableBindingObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = {"callable", nullptr}; - PyObject* callable = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast(keywords), - &callable)) { - return -1; - } - - if (!PyCallable_Check(callable)) { - PyErr_SetString(PyExc_TypeError, "Argument must be callable"); - return -1; - } - - self->binding = std::make_shared(callable); - return 0; -} - -PyObject* PyCallableBindingType::repr(PyObject* obj) { - PyCallableBindingObject* self = (PyCallableBindingObject*)obj; - std::ostringstream ss; - ss << "binding && self->binding->isValid()) { - auto val = self->binding->evaluate(); - if (val) { - ss << " value=" << *val; - } - } else { - ss << " (invalid)"; - } - ss << ">"; - return PyUnicode_FromString(ss.str().c_str()); -} - -PyObject* PyCallableBindingType::get_callable(PyCallableBindingObject* self, void* closure) { - if (!self->binding) { - Py_RETURN_NONE; - } - PyObject* callable = self->binding->getCallable(); - if (!callable) { - Py_RETURN_NONE; - } - Py_INCREF(callable); - return callable; -} - -PyObject* PyCallableBindingType::get_value(PyCallableBindingObject* self, void* closure) { - if (!self->binding) { - Py_RETURN_NONE; - } - auto val = self->binding->evaluate(); - if (!val) { - Py_RETURN_NONE; - } - return PyFloat_FromDouble(*val); -} - -PyObject* PyCallableBindingType::is_valid(PyCallableBindingObject* self, void* closure) { - if (!self->binding) { - Py_RETURN_FALSE; - } - return PyBool_FromLong(self->binding->isValid()); -} diff --git a/src/PyUniformBinding.h b/src/PyUniformBinding.h deleted file mode 100644 index 97f6334..0000000 --- a/src/PyUniformBinding.h +++ /dev/null @@ -1,201 +0,0 @@ -#pragma once -#include "Common.h" -#include "Python.h" -#include -#include -#include -#include - -// Forward declarations -class UIDrawable; - -/** - * @brief Variant type for uniform values - * - * Supports float, vec2, vec3, and vec4 uniform types. - */ -using UniformValue = std::variant< - float, - sf::Glsl::Vec2, - sf::Glsl::Vec3, - sf::Glsl::Vec4 ->; - -/** - * @brief Base class for uniform bindings - * - * Bindings provide dynamic uniform values that are evaluated each frame. - */ -class UniformBinding { -public: - virtual ~UniformBinding() = default; - - /** - * @brief Evaluate the binding and return its current value - * @return The current uniform value, or std::nullopt if binding is invalid - */ - virtual std::optional evaluate() const = 0; - - /** - * @brief Check if the binding is still valid - */ - virtual bool isValid() const = 0; -}; - -/** - * @brief Binding that reads a property from a UIDrawable - * - * Uses a weak_ptr to prevent dangling references if the target is destroyed. - */ -class PropertyBinding : public UniformBinding { -public: - PropertyBinding(std::weak_ptr target, const std::string& property); - - std::optional evaluate() const override; - bool isValid() const override; - - // Accessors for Python - std::weak_ptr getTarget() const { return target; } - const std::string& getPropertyName() const { return property_name; } - -private: - std::weak_ptr target; - std::string property_name; -}; - -/** - * @brief Binding that calls a Python callable to get the value - * - * The callable should return a float value. - */ -class CallableBinding : public UniformBinding { -public: - explicit CallableBinding(PyObject* callable); - ~CallableBinding(); - - // Non-copyable due to PyObject reference management - CallableBinding(const CallableBinding&) = delete; - CallableBinding& operator=(const CallableBinding&) = delete; - - // Move semantics - CallableBinding(CallableBinding&& other) noexcept; - CallableBinding& operator=(CallableBinding&& other) noexcept; - - std::optional evaluate() const override; - bool isValid() const override; - - // Accessor for Python - PyObject* getCallable() const { return callable; } - -private: - PyObject* callable; // Owned reference -}; - -// Python object structures for bindings -typedef struct { - PyObject_HEAD - std::shared_ptr binding; - PyObject* weakreflist; -} PyPropertyBindingObject; - -typedef struct { - PyObject_HEAD - std::shared_ptr binding; - PyObject* weakreflist; -} PyCallableBindingObject; - -// Python type class for PropertyBinding -class PyPropertyBindingType { -public: - static PyObject* repr(PyObject* self); - static int init(PyPropertyBindingObject* self, PyObject* args, PyObject* kwds); - static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); - static void dealloc(PyPropertyBindingObject* self); - - static PyObject* get_target(PyPropertyBindingObject* self, void* closure); - static PyObject* get_property(PyPropertyBindingObject* self, void* closure); - static PyObject* get_value(PyPropertyBindingObject* self, void* closure); - static PyObject* is_valid(PyPropertyBindingObject* self, void* closure); - - static PyGetSetDef getsetters[]; -}; - -// Python type class for CallableBinding -class PyCallableBindingType { -public: - static PyObject* repr(PyObject* self); - static int init(PyCallableBindingObject* self, PyObject* args, PyObject* kwds); - static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); - static void dealloc(PyCallableBindingObject* self); - - static PyObject* get_callable(PyCallableBindingObject* self, void* closure); - static PyObject* get_value(PyCallableBindingObject* self, void* closure); - static PyObject* is_valid(PyCallableBindingObject* self, void* closure); - - static PyGetSetDef getsetters[]; -}; - -namespace mcrfpydef { - // Using inline to ensure single definition across translation units (C++17) - inline PyTypeObject PyPropertyBindingType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.PropertyBinding", - .tp_basicsize = sizeof(PyPropertyBindingObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)::PyPropertyBindingType::dealloc, - .tp_repr = ::PyPropertyBindingType::repr, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR( - "PropertyBinding(target: UIDrawable, property: str)\n" - "\n" - "A binding that reads a property value from a UI drawable.\n" - "\n" - "Args:\n" - " target: The drawable to read the property from\n" - " property: Name of the property to read (e.g., 'x', 'opacity')\n" - "\n" - "Use this to create dynamic shader uniforms that follow a drawable's\n" - "properties. The binding automatically handles cases where the target\n" - "is destroyed.\n" - "\n" - "Example:\n" - " other_frame = mcrfpy.Frame(pos=(100, 100))\n" - " frame.uniforms['offset_x'] = mcrfpy.PropertyBinding(other_frame, 'x')\n" - ), - .tp_weaklistoffset = offsetof(PyPropertyBindingObject, weakreflist), - .tp_getset = nullptr, // Set in McRFPy_API.cpp before PyType_Ready - .tp_init = (initproc)::PyPropertyBindingType::init, - .tp_new = ::PyPropertyBindingType::pynew, - }; - - inline PyTypeObject PyCallableBindingType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.CallableBinding", - .tp_basicsize = sizeof(PyCallableBindingObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)::PyCallableBindingType::dealloc, - .tp_repr = ::PyCallableBindingType::repr, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR( - "CallableBinding(callable: Callable[[], float])\n" - "\n" - "A binding that calls a Python function to get its value.\n" - "\n" - "Args:\n" - " callable: A function that takes no arguments and returns a float\n" - "\n" - "The callable is invoked every frame when the shader is rendered.\n" - "Keep the callable lightweight to avoid performance issues.\n" - "\n" - "Example:\n" - " player_health = 100\n" - " frame.uniforms['health_pct'] = mcrfpy.CallableBinding(\n" - " lambda: player_health / 100.0\n" - " )\n" - ), - .tp_weaklistoffset = offsetof(PyCallableBindingObject, weakreflist), - .tp_getset = nullptr, // Set in McRFPy_API.cpp before PyType_Ready - .tp_init = (initproc)::PyCallableBindingType::init, - .tp_new = ::PyCallableBindingType::pynew, - }; -} diff --git a/src/PyUniformCollection.cpp b/src/PyUniformCollection.cpp deleted file mode 100644 index 6d4abfd..0000000 --- a/src/PyUniformCollection.cpp +++ /dev/null @@ -1,405 +0,0 @@ -#include "PyUniformCollection.h" -#include "UIDrawable.h" -#include "McRFPy_API.h" -#include "McRFPy_Doc.h" -#include - -// ============================================================================ -// UniformCollection Implementation -// ============================================================================ - -void UniformCollection::setFloat(const std::string& name, float value) { - entries[name] = UniformValue(value); -} - -void UniformCollection::setVec2(const std::string& name, float x, float y) { - entries[name] = UniformValue(sf::Glsl::Vec2(x, y)); -} - -void UniformCollection::setVec3(const std::string& name, float x, float y, float z) { - entries[name] = UniformValue(sf::Glsl::Vec3(x, y, z)); -} - -void UniformCollection::setVec4(const std::string& name, float x, float y, float z, float w) { - entries[name] = UniformValue(sf::Glsl::Vec4(x, y, z, w)); -} - -void UniformCollection::setPropertyBinding(const std::string& name, std::shared_ptr binding) { - entries[name] = binding; -} - -void UniformCollection::setCallableBinding(const std::string& name, std::shared_ptr binding) { - entries[name] = binding; -} - -void UniformCollection::remove(const std::string& name) { - entries.erase(name); -} - -bool UniformCollection::contains(const std::string& name) const { - return entries.find(name) != entries.end(); -} - -std::vector UniformCollection::getNames() const { - std::vector names; - names.reserve(entries.size()); - for (const auto& [name, _] : entries) { - names.push_back(name); - } - return names; -} - -void UniformCollection::applyTo(sf::Shader& shader) const { - for (const auto& [name, entry] : entries) { - std::visit([&shader, &name](auto&& arg) { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - // Static value - std::visit([&shader, &name](auto&& val) { - using V = std::decay_t; - if constexpr (std::is_same_v) { - shader.setUniform(name, val); - } else if constexpr (std::is_same_v) { - shader.setUniform(name, val); - } else if constexpr (std::is_same_v) { - shader.setUniform(name, val); - } else if constexpr (std::is_same_v) { - shader.setUniform(name, val); - } - }, arg); - } - else if constexpr (std::is_same_v>) { - // Property binding - evaluate and apply - if (arg && arg->isValid()) { - auto val = arg->evaluate(); - if (val) { - shader.setUniform(name, *val); - } - } - } - else if constexpr (std::is_same_v>) { - // Callable binding - evaluate and apply - if (arg && arg->isValid()) { - auto val = arg->evaluate(); - if (val) { - shader.setUniform(name, *val); - } - } - } - }, entry); - } -} - -bool UniformCollection::hasDynamicBindings() const { - for (const auto& [_, entry] : entries) { - if (std::holds_alternative>(entry)) { - return true; - } - } - return false; -} - -const UniformEntry* UniformCollection::getEntry(const std::string& name) const { - auto it = entries.find(name); - if (it == entries.end()) return nullptr; - return &it->second; -} - -// ============================================================================ -// PyUniformCollectionType Python Interface -// ============================================================================ - -PyMethodDef PyUniformCollectionType::methods[] = { - {"keys", (PyCFunction)PyUniformCollectionType::keys, METH_NOARGS, - "Return list of uniform names"}, - {"values", (PyCFunction)PyUniformCollectionType::values, METH_NOARGS, - "Return list of uniform values"}, - {"items", (PyCFunction)PyUniformCollectionType::items, METH_NOARGS, - "Return list of (name, value) tuples"}, - {"clear", (PyCFunction)PyUniformCollectionType::clear, METH_NOARGS, - "Remove all uniforms"}, - {NULL} -}; - -PyMappingMethods PyUniformCollectionType::mapping_methods = { - .mp_length = PyUniformCollectionType::mp_length, - .mp_subscript = PyUniformCollectionType::mp_subscript, - .mp_ass_subscript = PyUniformCollectionType::mp_ass_subscript, -}; - -PySequenceMethods PyUniformCollectionType::sequence_methods = { - .sq_length = nullptr, - .sq_concat = nullptr, - .sq_repeat = nullptr, - .sq_item = nullptr, - .was_sq_slice = nullptr, - .sq_ass_item = nullptr, - .was_sq_ass_slice = nullptr, - .sq_contains = PyUniformCollectionType::sq_contains, -}; - -void PyUniformCollectionType::dealloc(PyUniformCollectionObject* self) { - if (self->weakreflist) { - PyObject_ClearWeakRefs((PyObject*)self); - } - // Don't delete collection - it's owned by UIDrawable - Py_TYPE(self)->tp_free((PyObject*)self); -} - -PyObject* PyUniformCollectionType::repr(PyObject* obj) { - PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj; - std::ostringstream ss; - ss << "collection) { - auto names = self->collection->getNames(); - ss << " ["; - for (size_t i = 0; i < names.size(); ++i) { - if (i > 0) ss << ", "; - ss << "'" << names[i] << "'"; - } - ss << "]"; - } - ss << ">"; - return PyUnicode_FromString(ss.str().c_str()); -} - -Py_ssize_t PyUniformCollectionType::mp_length(PyObject* obj) { - PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj; - if (!self->collection) return 0; - return static_cast(self->collection->getNames().size()); -} - -PyObject* PyUniformCollectionType::mp_subscript(PyObject* obj, PyObject* key) { - PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj; - - if (!self->collection) { - PyErr_SetString(PyExc_RuntimeError, "UniformCollection is not valid"); - return NULL; - } - - if (!PyUnicode_Check(key)) { - PyErr_SetString(PyExc_TypeError, "Uniform name must be a string"); - return NULL; - } - - const char* name = PyUnicode_AsUTF8(key); - const UniformEntry* entry = self->collection->getEntry(name); - - if (!entry) { - PyErr_Format(PyExc_KeyError, "'%s'", name); - return NULL; - } - - // Convert entry to Python object - return std::visit([](auto&& arg) -> PyObject* { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - return std::visit([](auto&& val) -> PyObject* { - using V = std::decay_t; - if constexpr (std::is_same_v) { - return PyFloat_FromDouble(val); - } else if constexpr (std::is_same_v) { - return Py_BuildValue("(ff)", val.x, val.y); - } else if constexpr (std::is_same_v) { - return Py_BuildValue("(fff)", val.x, val.y, val.z); - } else if constexpr (std::is_same_v) { - return Py_BuildValue("(ffff)", val.x, val.y, val.z, val.w); - } - Py_RETURN_NONE; - }, arg); - } - else if constexpr (std::is_same_v>) { - // Return the current value for now - // TODO: Return the actual PropertyBinding object - if (arg && arg->isValid()) { - auto val = arg->evaluate(); - if (val) { - return PyFloat_FromDouble(*val); - } - } - Py_RETURN_NONE; - } - else if constexpr (std::is_same_v>) { - // Return the current value for now - // TODO: Return the actual CallableBinding object - if (arg && arg->isValid()) { - auto val = arg->evaluate(); - if (val) { - return PyFloat_FromDouble(*val); - } - } - Py_RETURN_NONE; - } - Py_RETURN_NONE; - }, *entry); -} - -int PyUniformCollectionType::mp_ass_subscript(PyObject* obj, PyObject* key, PyObject* value) { - PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj; - - if (!self->collection) { - PyErr_SetString(PyExc_RuntimeError, "UniformCollection is not valid"); - return -1; - } - - if (!PyUnicode_Check(key)) { - PyErr_SetString(PyExc_TypeError, "Uniform name must be a string"); - return -1; - } - - const char* name = PyUnicode_AsUTF8(key); - - // Delete operation - if (value == NULL) { - self->collection->remove(name); - return 0; - } - - // Check for binding types first - // PropertyBinding - if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyPropertyBindingType)) { - PyPropertyBindingObject* binding = (PyPropertyBindingObject*)value; - if (binding->binding) { - self->collection->setPropertyBinding(name, binding->binding); - return 0; - } - PyErr_SetString(PyExc_ValueError, "PropertyBinding is not valid"); - return -1; - } - - // CallableBinding - if (PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyCallableBindingType)) { - PyCallableBindingObject* binding = (PyCallableBindingObject*)value; - if (binding->binding) { - self->collection->setCallableBinding(name, binding->binding); - return 0; - } - PyErr_SetString(PyExc_ValueError, "CallableBinding is not valid"); - return -1; - } - - // Float value - if (PyFloat_Check(value) || PyLong_Check(value)) { - float f = static_cast(PyFloat_AsDouble(value)); - if (PyErr_Occurred()) return -1; - self->collection->setFloat(name, f); - return 0; - } - - // Tuple for vec2/vec3/vec4 - if (PyTuple_Check(value)) { - Py_ssize_t size = PyTuple_Size(value); - if (size == 2) { - float x = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 0))); - float y = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 1))); - if (PyErr_Occurred()) return -1; - self->collection->setVec2(name, x, y); - return 0; - } - else if (size == 3) { - float x = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 0))); - float y = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 1))); - float z = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 2))); - if (PyErr_Occurred()) return -1; - self->collection->setVec3(name, x, y, z); - return 0; - } - else if (size == 4) { - float x = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 0))); - float y = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 1))); - float z = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 2))); - float w = static_cast(PyFloat_AsDouble(PyTuple_GetItem(value, 3))); - if (PyErr_Occurred()) return -1; - self->collection->setVec4(name, x, y, z, w); - return 0; - } - else { - PyErr_Format(PyExc_ValueError, - "Tuple must have 2, 3, or 4 elements for vec2/vec3/vec4, got %zd", size); - return -1; - } - } - - PyErr_SetString(PyExc_TypeError, - "Uniform value must be a float, tuple (vec2/vec3/vec4), PropertyBinding, or CallableBinding"); - return -1; -} - -int PyUniformCollectionType::sq_contains(PyObject* obj, PyObject* key) { - PyUniformCollectionObject* self = (PyUniformCollectionObject*)obj; - - if (!self->collection) return 0; - - if (!PyUnicode_Check(key)) return 0; - - const char* name = PyUnicode_AsUTF8(key); - return self->collection->contains(name) ? 1 : 0; -} - -PyObject* PyUniformCollectionType::keys(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)) { - if (!self->collection) { - return PyList_New(0); - } - - auto names = self->collection->getNames(); - PyObject* list = PyList_New(names.size()); - for (size_t i = 0; i < names.size(); ++i) { - PyList_SetItem(list, i, PyUnicode_FromString(names[i].c_str())); - } - return list; -} - -PyObject* PyUniformCollectionType::values(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)) { - if (!self->collection) { - return PyList_New(0); - } - - auto names = self->collection->getNames(); - PyObject* list = PyList_New(names.size()); - for (size_t i = 0; i < names.size(); ++i) { - PyObject* key = PyUnicode_FromString(names[i].c_str()); - PyObject* val = mp_subscript((PyObject*)self, key); - Py_DECREF(key); - if (!val) { - Py_DECREF(list); - return NULL; - } - PyList_SetItem(list, i, val); - } - return list; -} - -PyObject* PyUniformCollectionType::items(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)) { - if (!self->collection) { - return PyList_New(0); - } - - auto names = self->collection->getNames(); - PyObject* list = PyList_New(names.size()); - for (size_t i = 0; i < names.size(); ++i) { - PyObject* key = PyUnicode_FromString(names[i].c_str()); - PyObject* val = mp_subscript((PyObject*)self, key); - if (!val) { - Py_DECREF(key); - Py_DECREF(list); - return NULL; - } - PyObject* tuple = PyTuple_Pack(2, key, val); - Py_DECREF(key); - Py_DECREF(val); - PyList_SetItem(list, i, tuple); - } - return list; -} - -PyObject* PyUniformCollectionType::clear(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)) { - if (self->collection) { - auto names = self->collection->getNames(); - for (const auto& name : names) { - self->collection->remove(name); - } - } - Py_RETURN_NONE; -} diff --git a/src/PyUniformCollection.h b/src/PyUniformCollection.h deleted file mode 100644 index 98c8d4f..0000000 --- a/src/PyUniformCollection.h +++ /dev/null @@ -1,157 +0,0 @@ -#pragma once -#include "Common.h" -#include "Python.h" -#include "PyUniformBinding.h" -#include -#include - -// Forward declarations -class UIDrawable; - -/** - * @brief Entry in UniformCollection - can be static value or binding - */ -using UniformEntry = std::variant< - UniformValue, // Static value (float, vec2, vec3, vec4) - std::shared_ptr, // Property binding - std::shared_ptr // Callable binding ->; - -/** - * @brief Collection of shader uniforms for a UIDrawable - * - * Stores both static uniform values and dynamic bindings. When applying - * uniforms to a shader, static values are used directly while bindings - * are evaluated each frame. - */ -class UniformCollection { -public: - UniformCollection() = default; - - /** - * @brief Set a static uniform value - */ - void setFloat(const std::string& name, float value); - void setVec2(const std::string& name, float x, float y); - void setVec3(const std::string& name, float x, float y, float z); - void setVec4(const std::string& name, float x, float y, float z, float w); - - /** - * @brief Set a property binding - */ - void setPropertyBinding(const std::string& name, std::shared_ptr binding); - - /** - * @brief Set a callable binding - */ - void setCallableBinding(const std::string& name, std::shared_ptr binding); - - /** - * @brief Remove a uniform - */ - void remove(const std::string& name); - - /** - * @brief Check if a uniform exists - */ - bool contains(const std::string& name) const; - - /** - * @brief Get all uniform names - */ - std::vector getNames() const; - - /** - * @brief Apply all uniforms to a shader - */ - void applyTo(sf::Shader& shader) const; - - /** - * @brief Check if any binding is dynamic (callable) - */ - bool hasDynamicBindings() const; - - /** - * @brief Get the entry for a uniform (for Python access) - */ - const UniformEntry* getEntry(const std::string& name) const; - -private: - std::map entries; -}; - -// Python object structure for UniformCollection -typedef struct { - PyObject_HEAD - UniformCollection* collection; // Owned by UIDrawable, not by this object - std::weak_ptr owner; // For checking validity - PyObject* weakreflist; -} PyUniformCollectionObject; - -/** - * @brief Python type for UniformCollection - * - * Supports dict-like access: collection["name"] = value - */ -class PyUniformCollectionType { -public: - static PyObject* repr(PyObject* self); - static void dealloc(PyUniformCollectionObject* self); - - // Mapping protocol (dict-like access) - static Py_ssize_t mp_length(PyObject* self); - static PyObject* mp_subscript(PyObject* self, PyObject* key); - static int mp_ass_subscript(PyObject* self, PyObject* key, PyObject* value); - - // Sequence protocol for 'in' operator - static int sq_contains(PyObject* self, PyObject* key); - - // Methods - static PyObject* keys(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)); - static PyObject* values(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)); - static PyObject* items(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)); - static PyObject* clear(PyUniformCollectionObject* self, PyObject* Py_UNUSED(ignored)); - - static PyMethodDef methods[]; - static PyMappingMethods mapping_methods; - static PySequenceMethods sequence_methods; -}; - -namespace mcrfpydef { - // Using inline to ensure single definition across translation units (C++17) - inline PyTypeObject PyUniformCollectionType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.UniformCollection", - .tp_basicsize = sizeof(PyUniformCollectionObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)::PyUniformCollectionType::dealloc, - .tp_repr = ::PyUniformCollectionType::repr, - .tp_as_sequence = &::PyUniformCollectionType::sequence_methods, - .tp_as_mapping = &::PyUniformCollectionType::mapping_methods, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR( - "UniformCollection - dict-like container for shader uniforms.\n" - "\n" - "This object is accessed via drawable.uniforms and supports:\n" - "- Getting: value = uniforms['name']\n" - "- Setting: uniforms['name'] = value\n" - "- Deleting: del uniforms['name']\n" - "- Checking: 'name' in uniforms\n" - "- Iterating: for name in uniforms.keys()\n" - "\n" - "Values can be:\n" - "- float: Single value uniform\n" - "- tuple: vec2 (2-tuple), vec3 (3-tuple), or vec4 (4-tuple)\n" - "- PropertyBinding: Dynamic value from another drawable's property\n" - "- CallableBinding: Dynamic value from a Python function\n" - "\n" - "Example:\n" - " frame.uniforms['intensity'] = 0.5\n" - " frame.uniforms['color'] = (1.0, 0.5, 0.0, 1.0)\n" - " frame.uniforms['offset'] = mcrfpy.PropertyBinding(other, 'x')\n" - " del frame.uniforms['intensity']\n" - ), - .tp_weaklistoffset = offsetof(PyUniformCollectionObject, weakreflist), - .tp_methods = nullptr, // Set in McRFPy_API.cpp - }; -} diff --git a/src/UIArc.cpp b/src/UIArc.cpp index 2cf19df..2425d48 100644 --- a/src/UIArc.cpp +++ b/src/UIArc.cpp @@ -134,13 +134,9 @@ void UIArc::render(sf::Vector2f offset, sf::RenderTarget& target) { rebuildVertices(); } - // Apply offset and rotation by creating a transform + // Apply offset by creating a transformed copy sf::Transform transform; transform.translate(offset); - // Apply rotation around origin - transform.translate(origin); - transform.rotate(rotation); - transform.translate(-origin); target.draw(vertices, transform); } @@ -150,25 +146,9 @@ 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; - // 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; + // Calculate distance from center + float dx = point.x - center.x; + float dy = point.y - center.y; float dist = std::sqrt(dx * dx + dy * dy); // Check if within the arc's radial range @@ -269,21 +249,6 @@ 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; } @@ -330,18 +295,6 @@ 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; } @@ -364,8 +317,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 == "rotation" || name == "origin_x" || name == "origin_y") { + name == "thickness" || name == "x" || name == "y") { return true; } // Color properties @@ -501,7 +453,6 @@ PyGetSetDef UIArc::getsetters[] = { UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIARC), UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIARC), - UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIARC), {NULL} }; diff --git a/src/UIBase.h b/src/UIBase.h index 9e4ae3b..f02fe1c 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -282,38 +282,4 @@ 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, \ - MCRF_PROPERTY(shader, \ - "Shader for GPU visual effects (Shader or None). " \ - "When set, the drawable is rendered through the shader program. " \ - "Set to None to disable shader effects." \ - ), (void*)type_enum}, \ - {"uniforms", (getter)UIDrawable::get_uniforms, NULL, \ - MCRF_PROPERTY(uniforms, \ - "Collection of shader uniforms (read-only access to collection). " \ - "Set uniforms via dict-like syntax: drawable.uniforms['name'] = value. " \ - "Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding." \ - ), (void*)type_enum} - // UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete diff --git a/src/UICaption.cpp b/src/UICaption.cpp index df64285..9270f2e 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -5,8 +5,6 @@ #include "PyFont.h" #include "PythonObjectCache.h" #include "PyAlignment.h" -#include "PyShader.h" // #106: Shader support -#include "PyUniformCollection.h" // #106: Uniform collection support // UIDrawable methods now in UIBase.h #include @@ -25,94 +23,28 @@ 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) 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; + if (click_callable || is_python_subclass) + { + if (text.getGlobalBounds().contains(point)) return this; } - - // 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; + return NULL; } void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target) { // Check visibility if (!visible) return; - + // Apply opacity auto color = text.getFillColor(); color.a = static_cast(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 - auto bounds = text.getGlobalBounds(); - sf::Vector2f screen_pos = offset + position; - - // Get or create intermediate texture - auto& intermediate = GameEngine::getShaderIntermediate(); - intermediate.clear(sf::Color::Transparent); - - // Render text at origin in intermediate texture - sf::Text temp_text = text; - temp_text.setPosition(0, 0); // Render at origin of intermediate texture - intermediate.draw(temp_text); - intermediate.display(); - - // Create result sprite from intermediate texture - sf::Sprite result_sprite(intermediate.getTexture()); - result_sprite.setPosition(screen_pos); - - // Apply engine uniforms - sf::Vector2f resolution(bounds.width, bounds.height); - PyShader::applyEngineUniforms(*shader->shader, resolution); - - // Apply user uniforms - if (uniforms) { - uniforms->applyTo(*shader->shader); - } - - // Draw with shader - target.draw(result_sprite, shader->shader.get()); - } else { - // Standard rendering path (no shader) - text.move(offset); - target.draw(text); - text.move(-offset); - } - + + text.move(offset); + //Resources::game->getWindow().draw(text); + target.draw(text); + text.move(-offset); + // Restore original alpha color.a = 255; text.setFillColor(color); @@ -382,8 +314,6 @@ PyGetSetDef UICaption::getsetters[] = { UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION), UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICAPTION), - UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UICAPTION), - UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UICAPTION), {NULL} }; @@ -665,28 +595,6 @@ 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; - } return false; } @@ -766,22 +674,6 @@ bool UICaption::getProperty(const std::string& name, float& value) const { value = static_cast(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; - } return false; } @@ -812,8 +704,7 @@ 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 == "rotation" || name == "origin_x" || name == "origin_y") { + name == "outline_color.b" || name == "outline_color.a") { return true; } // Color properties @@ -824,14 +715,6 @@ 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; - } return false; } diff --git a/src/UICircle.cpp b/src/UICircle.cpp index 7224a6c..44f950b 100644 --- a/src/UICircle.cpp +++ b/src/UICircle.cpp @@ -115,12 +115,6 @@ 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(fill_color.a * opacity); @@ -137,30 +131,9 @@ 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 - circleCenter.x; - float dy = point.y - circleCenter.y; + float dx = point.x - position.x; + float dy = point.y - position.y; float distance = std::sqrt(dx * dx + dy * dy); float effective_radius = radius + outline_thickness; @@ -215,21 +188,6 @@ 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; } @@ -269,15 +227,6 @@ 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; } @@ -304,8 +253,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" || - name == "x" || name == "y" || - name == "rotation" || name == "origin_x" || name == "origin_y") { + name == "x" || name == "y") { return true; } // Color properties @@ -313,7 +261,7 @@ bool UICircle::hasProperty(const std::string& name) const { return true; } // Vector2f properties - if (name == "center" || name == "position" || name == "origin") { + if (name == "center" || name == "position") { return true; } return false; @@ -451,7 +399,6 @@ PyGetSetDef UICircle::getsetters[] = { UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICIRCLE), UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICIRCLE), - UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UICIRCLE), {NULL} }; diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index a1fe1cd..8ed785d 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -13,8 +13,6 @@ #include "PyAnimation.h" #include "PyEasing.h" #include "PySceneObject.h" // #183: For scene parent lookup -#include "PyShader.h" // #106: Shader support -#include "PyUniformCollection.h" // #106: Uniform collection support // Helper function to extract UIDrawable* from any Python UI object // Returns nullptr and sets Python error on failure @@ -46,9 +44,6 @@ 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 @@ -85,9 +80,6 @@ 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 @@ -134,9 +126,6 @@ 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), @@ -166,9 +155,6 @@ 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 @@ -601,132 +587,6 @@ 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(reinterpret_cast(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(reinterpret_cast(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(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(reinterpret_cast(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(reinterpret_cast(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(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(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(reinterpret_cast(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(reinterpret_cast(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(reinterpret_cast(closure)); @@ -1087,195 +947,6 @@ void UIDrawable::markDirty() { markContentDirty(); } -// #106 - Shader support -void UIDrawable::markShaderDynamic() { - shader_dynamic = true; - - // Propagate to parent to invalidate caches - auto p = parent.lock(); - if (p) { - p->markShaderDynamic(); - } -} - -// #106: Shader uniform property helpers for animation support -bool UIDrawable::setShaderProperty(const std::string& name, float value) { - // Check if name starts with "shader." - if (name.compare(0, 7, "shader.") != 0) { - return false; - } - - // Extract the uniform name after "shader." - std::string uniform_name = name.substr(7); - if (uniform_name.empty()) { - return false; - } - - // Initialize uniforms collection if needed - if (!uniforms) { - uniforms = std::make_unique(); - } - - // Set the uniform value - uniforms->setFloat(uniform_name, value); - markDirty(); - return true; -} - -bool UIDrawable::getShaderProperty(const std::string& name, float& value) const { - // Check if name starts with "shader." - if (name.compare(0, 7, "shader.") != 0) { - return false; - } - - // Extract the uniform name after "shader." - std::string uniform_name = name.substr(7); - if (uniform_name.empty() || !uniforms) { - return false; - } - - // Try to get the value from uniforms - const auto* entry = uniforms->getEntry(uniform_name); - if (!entry) { - return false; - } - - // UniformEntry is variant, shared_ptr> - // UniformValue is variant - // So we need to check for UniformValue first, then extract the float from it - - // Try to extract static UniformValue from the entry - if (const auto* uval = std::get_if(entry)) { - // Now try to extract float from UniformValue - if (const float* fval = std::get_if(uval)) { - value = *fval; - return true; - } - // Could be vec2/vec3/vec4 - not a float, return false - return false; - } - - // For bindings, evaluate and return - if (const auto* prop_binding = std::get_if>(entry)) { - auto opt_val = (*prop_binding)->evaluate(); - if (opt_val) { - value = *opt_val; - return true; - } - } else if (const auto* call_binding = std::get_if>(entry)) { - auto opt_val = (*call_binding)->evaluate(); - if (opt_val) { - value = *opt_val; - return true; - } - } - - return false; -} - -bool UIDrawable::hasShaderProperty(const std::string& name) const { - // Check if name starts with "shader." - if (name.compare(0, 7, "shader.") != 0) { - return false; - } - - // Shader uniforms are always valid property names (they'll be created on set) - return true; -} - -// Python API for shader property -PyObject* UIDrawable::get_shader(PyObject* self, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; - - if (!drawable->shader) { - Py_RETURN_NONE; - } - - // Return the shader object (increment reference) - Py_INCREF(drawable->shader.get()); - return (PyObject*)drawable->shader.get(); -} - -int UIDrawable::set_shader(PyObject* self, PyObject* value, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return -1; - - if (value == Py_None) { - // Clear shader - drawable->shader.reset(); - drawable->shader_dynamic = false; - drawable->markDirty(); - return 0; - } - - // Check if it's a Shader object - if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyShaderType)) { - PyErr_SetString(PyExc_TypeError, "shader must be a Shader object or None"); - return -1; - } - - PyShaderObject* shader_obj = (PyShaderObject*)value; - if (!shader_obj->shader) { - PyErr_SetString(PyExc_ValueError, "Shader is not valid (compilation failed?)"); - return -1; - } - - // Store the shader - drawable->shader = std::shared_ptr(shader_obj, [](PyShaderObject* p) { - // Custom deleter that doesn't delete the Python object - // The Python reference counting handles that - }); - Py_INCREF(shader_obj); // Keep the Python object alive - - // Create uniforms collection if needed - if (!drawable->uniforms) { - drawable->uniforms = std::make_unique(); - } - - // Set dynamic flag if shader is dynamic - if (shader_obj->dynamic) { - drawable->markShaderDynamic(); - } - - // Enable RenderTexture for shader rendering (if not already enabled) - auto bounds = drawable->get_bounds(); - if (bounds.width > 0 && bounds.height > 0) { - drawable->enableRenderTexture( - static_cast(bounds.width), - static_cast(bounds.height) - ); - } - - drawable->markDirty(); - return 0; -} - -PyObject* UIDrawable::get_uniforms(PyObject* self, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = extractDrawable(self, objtype); - if (!drawable) return NULL; - - // Create uniforms collection if needed - if (!drawable->uniforms) { - drawable->uniforms = std::make_unique(); - } - - // Create and return a Python wrapper for the collection - PyUniformCollectionObject* collection = (PyUniformCollectionObject*) - mcrfpydef::PyUniformCollectionType.tp_alloc(&mcrfpydef::PyUniformCollectionType, 0); - - if (!collection) return NULL; - - collection->collection = drawable->uniforms.get(); - collection->weakreflist = NULL; - // Note: owner weak_ptr could be set here if we had access to shared_ptr - - return (PyObject*)collection; -} - // Python API - get parent drawable PyObject* UIDrawable::get_parent(PyObject* self, void* closure) { PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); diff --git a/src/UIDrawable.h b/src/UIDrawable.h index 7f3be1e..cffcebd 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -16,12 +16,6 @@ #include "Resources.h" #include "UIBase.h" - -// Forward declarations for shader support (#106) -class UniformCollection; -// PyShaderObject is a typedef, forward declare as a struct with explicit typedef -typedef struct PyShaderObjectStruct PyShaderObject; - class UIFrame; class UICaption; class UISprite; class UIEntity; class UIGrid; enum PyObjectsEnum : int @@ -99,14 +93,6 @@ 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); @@ -125,16 +111,6 @@ 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 parent; @@ -229,12 +205,6 @@ public: // Check if a property name is valid for animation on this drawable type virtual bool hasProperty(const std::string& name) const { return false; } - // #106: Shader uniform property helpers for animation support - // These methods handle "shader.uniform_name" property paths - bool setShaderProperty(const std::string& name, float value); - bool getShaderProperty(const std::string& name, float& value) const; - bool hasShaderProperty(const std::string& name) const; - // Note: animate_helper is now a free function (UIDrawable_animate_impl) declared in UIBase.h // to avoid incomplete type issues with template instantiation. @@ -283,22 +253,8 @@ protected: public: void disableRenderTexture(); - // Shader support (#106) - std::shared_ptr shader; - std::unique_ptr uniforms; - bool shader_dynamic = false; // True if shader uses time-varying effects - - // Mark this drawable as having dynamic shader effects - // Propagates up to parent to invalidate caches - void markShaderDynamic(); - - // Python API for shader properties - static PyObject* get_shader(PyObject* self, void* closure); - static int set_shader(PyObject* self, PyObject* value, void* closure); - static PyObject* get_uniforms(PyObject* self, void* closure); - protected: - + public: // #144: Dirty flag system - content vs composite // content_dirty: THIS drawable's texture needs rebuild (color/text/sprite changed) diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 196b6b1..e817a7f 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -12,8 +12,6 @@ #include "PyAnimation.h" #include "PyEasing.h" #include "PyPositionHelper.h" -#include "PyShader.h" // #106: Shader support -#include "PyUniformCollection.h" // #106: Uniform collection support // UIDrawable methods now in UIBase.h #include "UIEntityPyMethods.h" @@ -1037,14 +1035,6 @@ PyGetSetDef UIEntity::getsetters[] = { {"visible", (getter)UIEntity_get_visible, (setter)UIEntity_set_visible, "Visibility flag", NULL}, {"opacity", (getter)UIEntity_get_opacity, (setter)UIEntity_set_opacity, "Opacity (0.0 = transparent, 1.0 = opaque)", NULL}, {"name", (getter)UIEntity_get_name, (setter)UIEntity_set_name, "Name for finding elements", NULL}, - {"shader", (getter)UIEntity_get_shader, (setter)UIEntity_set_shader, - "Shader for GPU visual effects (Shader or None). " - "When set, the entity is rendered through the shader program. " - "Set to None to disable shader effects.", NULL}, - {"uniforms", (getter)UIEntity_get_uniforms, NULL, - "Collection of shader uniforms (read-only access to collection). " - "Set uniforms via dict-like syntax: entity.uniforms['name'] = value. " - "Supports float, vec2/3/4 tuples, PropertyBinding, and CallableBinding.", NULL}, {NULL} /* Sentinel */ }; @@ -1083,10 +1073,6 @@ bool UIEntity::setProperty(const std::string& name, float value) { if (grid) grid->markDirty(); // #144 - Content change return true; } - // #106: Shader uniform properties - delegate to sprite - if (sprite.setShaderProperty(name, value)) { - return true; - } return false; } @@ -1112,10 +1098,6 @@ bool UIEntity::getProperty(const std::string& name, float& value) const { value = sprite.getScale().x; // Assuming uniform scale return true; } - // #106: Shader uniform properties - delegate to sprite - if (sprite.getShaderProperty(name, value)) { - return true; - } return false; } @@ -1128,10 +1110,6 @@ bool UIEntity::hasProperty(const std::string& name) const { if (name == "sprite_index" || name == "sprite_number") { return true; } - // #106: Shader uniform properties - delegate to sprite - if (sprite.hasShaderProperty(name)) { - return true; - } return false; } diff --git a/src/UIEntityPyMethods.h b/src/UIEntityPyMethods.h index 88386a2..53e5732 100644 --- a/src/UIEntityPyMethods.h +++ b/src/UIEntityPyMethods.h @@ -1,8 +1,6 @@ #pragma once #include "UIEntity.h" #include "UIBase.h" -#include "PyShader.h" // #106: Shader support -#include "PyUniformCollection.h" // #106: Uniform collection support // UIEntity-specific property implementations // These delegate to the wrapped sprite member @@ -74,73 +72,4 @@ static int UIEntity_set_name(PyUIEntityObject* self, PyObject* value, void* clos self->data->sprite.name = name_str; return 0; -} - -// #106: Shader property - delegate to sprite -static PyObject* UIEntity_get_shader(PyUIEntityObject* self, void* closure) -{ - auto& shader_ptr = self->data->sprite.shader; - if (!shader_ptr) { - Py_RETURN_NONE; - } - // Return the PyShaderObject (which is also a PyObject) - Py_INCREF((PyObject*)shader_ptr.get()); - return (PyObject*)shader_ptr.get(); -} - -static int UIEntity_set_shader(PyUIEntityObject* self, PyObject* value, void* closure) -{ - if (value == Py_None || value == NULL) { - self->data->sprite.shader.reset(); - self->data->sprite.shader_dynamic = false; - return 0; - } - - // Check if value is a Shader object - if (!PyObject_IsInstance(value, (PyObject*)&mcrfpydef::PyShaderType)) { - PyErr_SetString(PyExc_TypeError, "shader must be a Shader object or None"); - return -1; - } - - PyShaderObject* shader_obj = (PyShaderObject*)value; - - // Store a shared_ptr to the PyShaderObject - // We need to increment the refcount since we're storing a reference - Py_INCREF(value); - self->data->sprite.shader = std::shared_ptr(shader_obj, [](PyShaderObject* p) { - Py_DECREF((PyObject*)p); - }); - - // Initialize uniforms collection if needed - if (!self->data->sprite.uniforms) { - self->data->sprite.uniforms = std::make_unique(); - } - - // Propagate dynamic flag - if (shader_obj->dynamic) { - self->data->sprite.markShaderDynamic(); - } - - return 0; -} - -// #106: Uniforms property - delegate to sprite's uniforms collection -static PyObject* UIEntity_get_uniforms(PyUIEntityObject* self, void* closure) -{ - // Initialize uniforms collection if needed - if (!self->data->sprite.uniforms) { - self->data->sprite.uniforms = std::make_unique(); - } - - // Create a Python wrapper for the uniforms collection - PyUniformCollectionObject* uniforms_obj = (PyUniformCollectionObject*)mcrfpydef::PyUniformCollectionType.tp_alloc(&mcrfpydef::PyUniformCollectionType, 0); - if (!uniforms_obj) { - return NULL; - } - - // The collection is owned by the sprite, we just provide a view - uniforms_obj->collection = self->data->sprite.uniforms.get(); - uniforms_obj->weakreflist = NULL; - - return (PyObject*)uniforms_obj; } \ No newline at end of file diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 02a8bde..4779efc 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -8,38 +8,20 @@ #include "McRFPy_API.h" #include "PythonObjectCache.h" #include "PyAlignment.h" -#include "PyShader.h" // #106: Shader support -#include "PyUniformCollection.h" // #106: Uniform collection #include // #106: for shader error output // UIDrawable methods now in UIBase.h UIDrawable* UIFrame::click_at(sf::Vector2f point) { - 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; - } - - // 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) { + // 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; } + // Transform to local coordinates for children + sf::Vector2f localPoint = point - position; + // Check children in reverse order (top to bottom, highest z-index first) for (auto it = children->rbegin(); it != children->rend(); ++it) { auto& child = *it; @@ -128,11 +110,11 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) // TODO: Apply opacity when SFML supports it on shapes - // #144: Use RenderTexture for clipping OR texture caching OR shaders (#106) + // #144: Use RenderTexture for clipping OR texture caching OR shaders // clip_children: requires texture for clipping effect (only when has children) // cache_subtree: uses texture for performance (always, even without children) - // shader: requires texture for shader post-processing - bool use_texture = (clip_children && !children->empty()) || cache_subtree || (shader && shader->shader); + // shader_enabled: requires texture for shader post-processing + bool use_texture = (clip_children && !children->empty()) || cache_subtree || shader_enabled; if (use_texture) { // Enable RenderTexture if not already enabled @@ -156,10 +138,8 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) // Clear the RenderTexture render_texture->clear(sf::Color::Transparent); - // Draw the frame box to RenderTexture (without rotation - that's applied to the final sprite) + // Draw the frame box to RenderTexture 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 @@ -190,23 +170,13 @@ 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) - sf::Vector2f resolution(render_sprite.getLocalBounds().width, - render_sprite.getLocalBounds().height); - PyShader::applyEngineUniforms(*shader->shader, resolution); - - // Apply user-defined uniforms - if (uniforms) { - uniforms->applyTo(*shader->shader); - } - - target.draw(render_sprite, shader->shader.get()); + // #106 POC: Apply shader if enabled + if (shader_enabled && shader) { + // Update time uniform for animated effects + static sf::Clock shader_clock; + shader->setUniform("time", shader_clock.getElapsedTime().asSeconds()); + shader->setUniform("texture", sf::Shader::CurrentTexture); + target.draw(render_sprite, shader.get()); } else { target.draw(render_sprite); } @@ -215,8 +185,6 @@ 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 @@ -229,9 +197,6 @@ 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 } @@ -497,6 +462,87 @@ int UIFrame::set_cache_subtree(PyUIFrameObject* self, PyObject* value, void* clo } // #106 - Shader POC: shader_enabled property +PyObject* UIFrame::get_shader_enabled(PyUIFrameObject* self, void* closure) +{ + return PyBool_FromLong(self->data->shader_enabled); +} + +int UIFrame::set_shader_enabled(PyUIFrameObject* self, PyObject* value, void* closure) +{ + if (!PyBool_Check(value)) { + PyErr_SetString(PyExc_TypeError, "shader_enabled must be a boolean"); + return -1; + } + + bool new_shader = PyObject_IsTrue(value); + if (new_shader != self->data->shader_enabled) { + self->data->shader_enabled = new_shader; + + if (new_shader) { + // Initialize the test shader if not already done + if (!self->data->shader) { + self->data->initializeTestShader(); + } + // Shader requires RenderTexture - enable it + auto size = self->data->box.getSize(); + if (size.x > 0 && size.y > 0) { + self->data->enableRenderTexture(static_cast(size.x), + static_cast(size.y)); + } + } + // Note: we don't disable RenderTexture when shader disabled - + // clip_children or cache_subtree may still need it + + self->data->markDirty(); + } + + return 0; +} + +// #106 - Initialize test shader (hardcoded glow/brightness effect) +void UIFrame::initializeTestShader() +{ + // Check if shaders are available + if (!sf::Shader::isAvailable()) { + std::cerr << "Shaders are not available on this system!" << std::endl; + return; + } + + shader = std::make_unique(); + + // Simple color inversion + wave distortion shader for POC + // This makes it obvious the shader is working + const std::string fragmentShader = R"( + uniform sampler2D texture; + uniform float time; + + void main() { + vec2 uv = gl_TexCoord[0].xy; + + // Subtle wave distortion based on time + uv.x += sin(uv.y * 10.0 + time * 2.0) * 0.01; + uv.y += cos(uv.x * 10.0 + time * 2.0) * 0.01; + + vec4 color = texture2D(texture, uv); + + // Glow effect: boost brightness and add slight color shift + float glow = 0.2 + 0.1 * sin(time * 3.0); + color.rgb = color.rgb * (1.0 + glow); + + // Slight hue shift for visual interest + color.r += 0.1 * sin(time); + color.b += 0.1 * cos(time); + + gl_FragColor = color; + } + )"; + + if (!shader->loadFromMemory(fragmentShader, sf::Shader::Fragment)) { + std::cerr << "Failed to load test shader!" << std::endl; + shader.reset(); + } +} + // Define the PyObjectType alias for the macros typedef PyUIFrameObject PyObjectType; @@ -535,11 +581,10 @@ PyGetSetDef UIFrame::getsetters[] = { {"grid_size", (getter)UIDrawable::get_grid_size, (setter)UIDrawable::set_grid_size, "Size in grid tile coordinates (only when parent is Grid)", (void*)PyObjectsEnum::UIFRAME}, {"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL}, {"cache_subtree", (getter)UIFrame::get_cache_subtree, (setter)UIFrame::set_cache_subtree, "#144: Cache subtree rendering to texture for performance", NULL}, + {"shader_enabled", (getter)UIFrame::get_shader_enabled, (setter)UIFrame::set_shader_enabled, "#106 POC: Enable test shader effect", NULL}, UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME), UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIFRAME), - UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIFRAME), - UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIFRAME), {NULL} }; @@ -884,25 +929,6 @@ 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)) { - return true; } return false; } @@ -930,16 +956,11 @@ bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) { box.setSize(value); if (use_render_texture) { // Need to recreate RenderTexture with new size - enableRenderTexture(static_cast(value.x), + enableRenderTexture(static_cast(value.x), static_cast(value.y)); } markDirty(); return true; - } else if (name == "origin") { - origin = value; - box.setOrigin(origin); - markDirty(); - return true; } return false; } @@ -984,19 +1005,6 @@ 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)) { - return true; } return false; } @@ -1019,9 +1027,6 @@ 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; } @@ -1033,8 +1038,7 @@ 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 == "rotation" || name == "origin_x" || name == "origin_y") { + name == "outline_color.b" || name == "outline_color.a") { return true; } // Color properties @@ -1042,11 +1046,7 @@ bool UIFrame::hasProperty(const std::string& name) const { return true; } // Vector2f properties - if (name == "position" || name == "size" || name == "origin") { - return true; - } - // #106: Check for shader uniform properties - if (hasShaderProperty(name)) { + if (name == "position" || name == "size") { return true; } return false; diff --git a/src/UIFrame.h b/src/UIFrame.h index 75e5d48..28b72fb 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -33,6 +33,10 @@ public: bool clip_children = false; // Whether to clip children to frame bounds bool cache_subtree = false; // #144: Whether to cache subtree rendering to texture + // Shader POC (#106) + std::unique_ptr shader; + bool shader_enabled = false; + void initializeTestShader(); // Load hardcoded test shader void render(sf::Vector2f, sf::RenderTarget&) override final; void move(sf::Vector2f); PyObjectsEnum derived_type() override final; @@ -56,6 +60,8 @@ public: static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure); static PyObject* get_cache_subtree(PyUIFrameObject* self, void* closure); static int set_cache_subtree(PyUIFrameObject* self, PyObject* value, void* closure); + static PyObject* get_shader_enabled(PyUIFrameObject* self, void* closure); + static int set_shader_enabled(PyUIFrameObject* self, PyObject* value, void* closure); static PyGetSetDef getsetters[]; static PyObject* repr(PyUIFrameObject* self); static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds); diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 7aea414..683db31 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -11,8 +11,6 @@ #include "PyPositionHelper.h" // For standardized position argument parsing #include "PyVector.h" // #179, #181 - For Vector return types #include "PyHeightMap.h" // #199 - HeightMap application methods -#include "PyShader.h" // #106: Shader support -#include "PyUniformCollection.h" // #106: Uniform collection support #include #include // #142 - for std::floor, std::isnan #include // #150 - for strcmp @@ -145,59 +143,28 @@ 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; - - // 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(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; - + + // sprites that are visible according to zoom, center_x, center_y, and box width float center_x_sq = center_x / cell_width; float center_y_sq = center_y / cell_height; - float width_sq = render_w / (cell_width * zoom); - float height_sq = render_h / (cell_height * zoom); + float width_sq = box.getSize().x / (cell_width * zoom); + float height_sq = box.getSize().y / (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 - (render_w / 2.0 / zoom); - int top_spritepixels = center_y - (render_h / 2.0 / zoom); + int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom); + int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom); int x_limit = left_edge + width_sq + 2; if (x_limit > grid_w) x_limit = grid_w; @@ -210,7 +177,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(*activeTexture, left_spritepixels, top_spritepixels, + layer->render(renderTexture, left_spritepixels, top_spritepixels, left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height); } @@ -236,7 +203,9 @@ 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.render(pixel_pos, *activeTexture); + //drawent.setPosition(pixel_pos); + //renderTexture.draw(drawent); + drawent.render(pixel_pos, renderTexture); entitiesRendered++; } @@ -249,7 +218,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(*activeTexture, left_spritepixels, top_spritepixels, + layer->render(renderTexture, left_spritepixels, top_spritepixels, left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height); } @@ -281,7 +250,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) (child->position.y - top_spritepixels) * zoom ); - child->render(pixel_pos, *activeTexture); + child->render(pixel_pos, renderTexture); } } @@ -323,11 +292,11 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) if (!state.discovered) { // Never seen - black overlay.setFillColor(sf::Color(0, 0, 0, 255)); - activeTexture->draw(overlay); + renderTexture.draw(overlay); } else if (!state.visible) { // Discovered but not currently visible - dark gray overlay.setFillColor(sf::Color(32, 32, 40, 192)); - activeTexture->draw(overlay); + renderTexture.draw(overlay); } // If visible and discovered, no overlay (fully visible) } @@ -353,7 +322,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) overlay.setPosition(pixel_pos); overlay.setFillColor(sf::Color(0, 0, 0, 255)); - activeTexture->draw(overlay); + renderTexture.draw(overlay); } } } @@ -380,66 +349,11 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) renderTexture.draw(lineb, 2, sf::Lines); */ - // Finalize the active texture - activeTexture->display(); + // render to window + renderTexture.display(); + //Resources::game->getWindow().draw(output); + target.draw(output); - // 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(aabb_w), static_cast(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) { - sf::Vector2f resolution(box.getSize().x, box.getSize().y); - PyShader::applyEngineUniforms(*shader->shader, resolution); - - // Apply user uniforms - if (uniforms) { - uniforms->applyTo(*shader->shader); - } - - target.draw(output, shader->shader.get()); - } else { - target.draw(output); - } } UIGridPoint& UIGrid::at(int x, int y) @@ -1118,8 +1032,6 @@ 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"); @@ -1174,8 +1086,6 @@ 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 @@ -2282,7 +2192,6 @@ 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, @@ -2314,7 +2223,6 @@ 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}, @@ -2324,7 +2232,6 @@ PyGetSetDef UIGrid::getsetters[] = { "Callback when a grid cell is clicked. Called with (cell_pos: Vector).", NULL}, {"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL, "Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL}, - UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIGRID), {NULL} /* Sentinel */ }; @@ -2585,26 +2492,6 @@ 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(value); markDirty(); // #144 - Z-order change affects parent @@ -2630,10 +2517,6 @@ bool UIGrid::setProperty(const std::string& name, float value) { markDirty(); // #144 - Content change return true; } - // #106: Shader uniform properties - if (setShaderProperty(name, value)) { - return true; - } return false; } @@ -2657,11 +2540,6 @@ 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; } @@ -2694,22 +2572,6 @@ 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(z_index); return true; @@ -2730,10 +2592,6 @@ bool UIGrid::getProperty(const std::string& name, float& value) const { value = static_cast(fill_color.a); return true; } - // #106: Shader uniform properties - if (getShaderProperty(name, value)) { - return true; - } return false; } @@ -2750,10 +2608,6 @@ 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; } @@ -2762,18 +2616,13 @@ 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 == "camera_rotation" || name == "rotation" || - name == "origin_x" || name == "origin_y" || name == "z_index" || + 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" || name == "origin") { - return true; - } - // #106: Shader uniform properties - if (hasShaderProperty(name)) { + if (name == "position" || name == "size" || name == "center") { return true; } return false; diff --git a/src/UIGrid.h b/src/UIGrid.h index e2ab942..eb2bb04 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -79,16 +79,11 @@ 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 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 chunk_manager; // Legacy flat storage (kept for small grids or compatibility) @@ -186,8 +181,6 @@ 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); diff --git a/src/UILine.cpp b/src/UILine.cpp index b98ce2a..cf5f89d 100644 --- a/src/UILine.cpp +++ b/src/UILine.cpp @@ -134,10 +134,6 @@ 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); } @@ -145,22 +141,6 @@ 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(); @@ -169,12 +149,11 @@ UIDrawable* UILine::click_at(sf::Vector2f point) { bounds.width += thickness * 2; bounds.height += thickness * 2; - // For rotated lines, skip the bounds check (it's an optimization, not required) - if (rotation == 0.0f && !bounds.contains(localPoint)) return nullptr; + if (!bounds.contains(point)) return nullptr; // Calculate distance from point to line segment sf::Vector2f line_vec = end_pos - start_pos; - sf::Vector2f point_vec = localPoint - start_pos; + sf::Vector2f point_vec = point - start_pos; float line_len_sq = line_vec.x * line_vec.x + line_vec.y * line_vec.y; float t = 0.0f; @@ -185,7 +164,7 @@ UIDrawable* UILine::click_at(sf::Vector2f point) { } sf::Vector2f closest = start_pos + t * line_vec; - sf::Vector2f diff = localPoint - closest; + sf::Vector2f diff = point - closest; float distance = std::sqrt(diff.x * diff.x + diff.y * diff.y); // Click is valid if within thickness + some margin @@ -269,21 +248,6 @@ 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; } @@ -342,18 +306,6 @@ 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; } @@ -381,8 +333,7 @@ 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 == "rotation" || name == "origin_x" || name == "origin_y") { + name == "end_x" || name == "end_y") { return true; } // Color properties @@ -390,7 +341,7 @@ bool UILine::hasProperty(const std::string& name) const { return true; } // Vector2f properties - if (name == "start" || name == "end" || name == "origin") { + if (name == "start" || name == "end") { return true; } return false; @@ -518,7 +469,6 @@ PyGetSetDef UILine::getsetters[] = { UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UILINE), UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UILINE), - UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UILINE), {NULL} }; diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 4281b2c..27b7c3f 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -4,43 +4,16 @@ #include "PythonObjectCache.h" #include "UIFrame.h" // #144: For snapshot= parameter #include "PyAlignment.h" -#include "PyShader.h" // #106: Shader support -#include "PyUniformCollection.h" // #106: Uniform collection support // UIDrawable methods now in UIBase.h UIDrawable* UISprite::click_at(sf::Vector2f point) { // #184: Also check for Python subclass (might have on_click method) - 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; + if (click_callable || is_python_subclass) + { + if(sprite.getGlobalBounds().contains(point)) return this; } - - // 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; + return NULL; } UISprite::UISprite() @@ -108,54 +81,16 @@ void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target) { // Check visibility if (!visible) return; - + // Apply opacity auto color = sprite.getColor(); color.a = static_cast(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 - auto bounds = sprite.getGlobalBounds(); - sf::Vector2f screen_pos = offset + position; - - // Get or create intermediate texture - auto& intermediate = GameEngine::getShaderIntermediate(); - intermediate.clear(sf::Color::Transparent); - - // Render sprite at origin in intermediate texture - sf::Sprite temp_sprite = sprite; - temp_sprite.setPosition(0, 0); // Render at origin of intermediate texture - intermediate.draw(temp_sprite); - intermediate.display(); - - // Create result sprite from intermediate texture - sf::Sprite result_sprite(intermediate.getTexture()); - result_sprite.setPosition(screen_pos); - - // Apply engine uniforms - sf::Vector2f resolution(bounds.width, bounds.height); - PyShader::applyEngineUniforms(*shader->shader, resolution); - - // Apply user uniforms - if (uniforms) { - uniforms->applyTo(*shader->shader); - } - - // Draw with shader - target.draw(result_sprite, shader->shader.get()); - } else { - // Standard rendering path (no shader) - sprite.move(offset); - target.draw(sprite); - sprite.move(-offset); - } - + + sprite.move(offset); + target.draw(sprite); + sprite.move(-offset); + // Restore original alpha color.a = 255; sprite.setColor(color); @@ -424,8 +359,6 @@ PyGetSetDef UISprite::getsetters[] = { UIDRAWABLE_GETSETTERS, UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UISPRITE), UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UISPRITE), - UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UISPRITE), - UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UISPRITE), {NULL} }; @@ -658,28 +591,6 @@ 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; - } return false; } @@ -722,22 +633,6 @@ bool UISprite::getProperty(const std::string& name, float& value) const { value = static_cast(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; - } return false; } @@ -757,21 +652,12 @@ 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 == "rotation" || name == "origin_x" || name == "origin_y") { + name == "z_index") { 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; - } return false; } diff --git a/tests/unit/grid_camera_rotation_test.py b/tests/unit/grid_camera_rotation_test.py deleted file mode 100644 index 7673570..0000000 --- a/tests/unit/grid_camera_rotation_test.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/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) diff --git a/tests/unit/rotation_test.py b/tests/unit/rotation_test.py deleted file mode 100644 index c2bcc6c..0000000 --- a/tests/unit/rotation_test.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/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()) diff --git a/tests/unit/rotation_visual_test.py b/tests/unit/rotation_visual_test.py deleted file mode 100644 index 54424d6..0000000 --- a/tests/unit/rotation_visual_test.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/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) diff --git a/tests/unit/shader_test.py b/tests/unit/shader_test.py deleted file mode 100644 index 174a39e..0000000 --- a/tests/unit/shader_test.py +++ /dev/null @@ -1,422 +0,0 @@ -#!/usr/bin/env python3 -"""Unit tests for the Shader system (Issue #106) - -Tests cover: -- Shader creation and compilation -- Static uniforms (float, vec2, vec3, vec4) -- PropertyBinding for dynamic uniform values -- CallableBinding for computed uniform values -- Shader assignment to various drawable types -- Dynamic flag propagation -""" - -import mcrfpy -import sys - - -def test_shader_creation(): - """Test basic shader creation""" - print("Testing shader creation...") - - # Valid shader - shader = mcrfpy.Shader(''' - uniform sampler2D texture; - void main() { - gl_FragColor = texture2D(texture, gl_TexCoord[0].xy); - } - ''') - assert shader is not None, "Shader should be created" - assert shader.is_valid, "Shader should be valid" - assert shader.dynamic == False, "Shader should not be dynamic by default" - - # Dynamic shader - dynamic_shader = mcrfpy.Shader(''' - uniform sampler2D texture; - uniform float time; - void main() { - gl_FragColor = texture2D(texture, gl_TexCoord[0].xy); - } - ''', dynamic=True) - assert dynamic_shader.dynamic == True, "Shader should be dynamic when specified" - - print(" PASS: Basic shader creation works") - - -def test_shader_source(): - """Test that shader source is stored correctly""" - print("Testing shader source storage...") - - source = '''uniform sampler2D texture; -void main() { - gl_FragColor = texture2D(texture, gl_TexCoord[0].xy); -}''' - shader = mcrfpy.Shader(source) - assert source in shader.source, "Shader source should be stored" - - print(" PASS: Shader source is stored") - - -def test_static_uniforms(): - """Test static uniform values""" - print("Testing static uniforms...") - - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - - # Test float uniform - frame.uniforms['intensity'] = 0.5 - assert abs(frame.uniforms['intensity'] - 0.5) < 0.001, "Float uniform should match" - - # Test vec2 uniform - frame.uniforms['offset'] = (10.0, 20.0) - val = frame.uniforms['offset'] - assert len(val) == 2, "Vec2 should have 2 components" - assert abs(val[0] - 10.0) < 0.001, "Vec2.x should match" - assert abs(val[1] - 20.0) < 0.001, "Vec2.y should match" - - # Test vec3 uniform - frame.uniforms['color_rgb'] = (1.0, 0.5, 0.0) - val = frame.uniforms['color_rgb'] - assert len(val) == 3, "Vec3 should have 3 components" - - # Test vec4 uniform - frame.uniforms['color'] = (1.0, 0.5, 0.0, 1.0) - val = frame.uniforms['color'] - assert len(val) == 4, "Vec4 should have 4 components" - - print(" PASS: Static uniforms work") - - -def test_uniform_keys(): - """Test uniform collection keys/values/items""" - print("Testing uniform collection methods...") - - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - frame.uniforms['a'] = 1.0 - frame.uniforms['b'] = 2.0 - frame.uniforms['c'] = 3.0 - - keys = frame.uniforms.keys() - assert 'a' in keys, "Key 'a' should be present" - assert 'b' in keys, "Key 'b' should be present" - assert 'c' in keys, "Key 'c' should be present" - assert len(keys) == 3, "Should have 3 keys" - - # Test 'in' operator - assert 'a' in frame.uniforms, "'in' operator should work" - assert 'nonexistent' not in frame.uniforms, "'not in' should work" - - # Test deletion - del frame.uniforms['b'] - assert 'b' not in frame.uniforms, "Deleted key should be gone" - assert len(frame.uniforms.keys()) == 2, "Should have 2 keys after deletion" - - print(" PASS: Uniform collection methods work") - - -def test_property_binding(): - """Test PropertyBinding for dynamic uniform values""" - print("Testing PropertyBinding...") - - # Create source and target frames - source_frame = mcrfpy.Frame(pos=(100, 200), size=(50, 50)) - target_frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - - # Create binding to source frame's x position - binding = mcrfpy.PropertyBinding(source_frame, 'x') - assert binding is not None, "PropertyBinding should be created" - assert binding.property == 'x', "Property name should be stored" - assert abs(binding.value - 100.0) < 0.001, "Initial value should be 100" - assert binding.is_valid == True, "Binding should be valid" - - # Assign binding to uniform - target_frame.uniforms['source_x'] = binding - - # Check that value tracks changes - source_frame.x = 300 - assert abs(binding.value - 300.0) < 0.001, "Binding should track changes" - - print(" PASS: PropertyBinding works") - - -def test_callable_binding(): - """Test CallableBinding for computed uniform values""" - print("Testing CallableBinding...") - - counter = [0] # Use list for closure - - def compute_value(): - counter[0] += 1 - return counter[0] * 0.1 - - binding = mcrfpy.CallableBinding(compute_value) - assert binding is not None, "CallableBinding should be created" - assert binding.is_valid == True, "Binding should be valid" - - # Each access should call the function - v1 = binding.value - v2 = binding.value - v3 = binding.value - - assert abs(v1 - 0.1) < 0.001, "First call should return 0.1" - assert abs(v2 - 0.2) < 0.001, "Second call should return 0.2" - assert abs(v3 - 0.3) < 0.001, "Third call should return 0.3" - - # Assign to uniform - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - frame.uniforms['computed'] = binding - - print(" PASS: CallableBinding works") - - -def test_shader_on_frame(): - """Test shader assignment to Frame""" - print("Testing shader on Frame...") - - shader = mcrfpy.Shader(''' - uniform sampler2D texture; - uniform float intensity; - void main() { - vec4 color = texture2D(texture, gl_TexCoord[0].xy); - color.rgb *= intensity; - gl_FragColor = color; - } - ''') - - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - assert frame.shader is None, "Shader should be None initially" - - frame.shader = shader - assert frame.shader is not None, "Shader should be assigned" - - frame.uniforms['intensity'] = 0.8 - assert abs(frame.uniforms['intensity'] - 0.8) < 0.001, "Uniform should be set" - - # Test shader removal - frame.shader = None - assert frame.shader is None, "Shader should be removable" - - print(" PASS: Shader on Frame works") - - -def test_shader_on_sprite(): - """Test shader assignment to Sprite""" - print("Testing shader on Sprite...") - - shader = mcrfpy.Shader(''' - uniform sampler2D texture; - void main() { - gl_FragColor = texture2D(texture, gl_TexCoord[0].xy); - } - ''') - - sprite = mcrfpy.Sprite(pos=(0, 0)) - assert sprite.shader is None, "Shader should be None initially" - - sprite.shader = shader - assert sprite.shader is not None, "Shader should be assigned" - - sprite.uniforms['test'] = 1.0 - assert abs(sprite.uniforms['test'] - 1.0) < 0.001, "Uniform should be set" - - print(" PASS: Shader on Sprite works") - - -def test_shader_on_caption(): - """Test shader assignment to Caption""" - print("Testing shader on Caption...") - - shader = mcrfpy.Shader(''' - uniform sampler2D texture; - void main() { - gl_FragColor = texture2D(texture, gl_TexCoord[0].xy); - } - ''') - - caption = mcrfpy.Caption(text="Test", pos=(0, 0)) - assert caption.shader is None, "Shader should be None initially" - - caption.shader = shader - assert caption.shader is not None, "Shader should be assigned" - - caption.uniforms['test'] = 1.0 - assert abs(caption.uniforms['test'] - 1.0) < 0.001, "Uniform should be set" - - print(" PASS: Shader on Caption works") - - -def test_shader_on_grid(): - """Test shader assignment to Grid""" - print("Testing shader on Grid...") - - shader = mcrfpy.Shader(''' - uniform sampler2D texture; - void main() { - gl_FragColor = texture2D(texture, gl_TexCoord[0].xy); - } - ''') - - grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(200, 200)) - assert grid.shader is None, "Shader should be None initially" - - grid.shader = shader - assert grid.shader is not None, "Shader should be assigned" - - grid.uniforms['test'] = 1.0 - assert abs(grid.uniforms['test'] - 1.0) < 0.001, "Uniform should be set" - - print(" PASS: Shader on Grid works") - - -def test_shader_on_entity(): - """Test shader assignment to Entity""" - print("Testing shader on Entity...") - - shader = mcrfpy.Shader(''' - uniform sampler2D texture; - void main() { - gl_FragColor = texture2D(texture, gl_TexCoord[0].xy); - } - ''') - - entity = mcrfpy.Entity() - assert entity.shader is None, "Shader should be None initially" - - entity.shader = shader - assert entity.shader is not None, "Shader should be assigned" - - entity.uniforms['test'] = 1.0 - assert abs(entity.uniforms['test'] - 1.0) < 0.001, "Uniform should be set" - - print(" PASS: Shader on Entity works") - - -def test_shared_shader(): - """Test that multiple drawables can share the same shader""" - print("Testing shared shader...") - - shader = mcrfpy.Shader(''' - uniform sampler2D texture; - uniform float intensity; - void main() { - vec4 color = texture2D(texture, gl_TexCoord[0].xy); - color.rgb *= intensity; - gl_FragColor = color; - } - ''') - - frame1 = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - frame2 = mcrfpy.Frame(pos=(100, 0), size=(100, 100)) - - # Assign same shader to both - frame1.shader = shader - frame2.shader = shader - - # But different uniform values - frame1.uniforms['intensity'] = 0.5 - frame2.uniforms['intensity'] = 1.0 - - assert abs(frame1.uniforms['intensity'] - 0.5) < 0.001, "Frame1 intensity should be 0.5" - assert abs(frame2.uniforms['intensity'] - 1.0) < 0.001, "Frame2 intensity should be 1.0" - - print(" PASS: Shared shader with different uniforms works") - - -def test_shader_animation_properties(): - """Test that shader uniforms can be animated via the animation system""" - print("Testing shader animation properties...") - - frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - - # Set initial uniform value - frame.uniforms['intensity'] = 0.0 - - # Test animate() method with shader.X property syntax - # This uses hasProperty/setProperty internally - try: - frame.animate('shader.intensity', 1.0, 0.5, mcrfpy.Easing.LINEAR) - animation_works = True - except Exception as e: - animation_works = False - print(f" Animation error: {e}") - - assert animation_works, "Animating shader uniforms should work" - - # Test with different drawable types - sprite = mcrfpy.Sprite(pos=(0, 0)) - sprite.uniforms['glow'] = 0.0 - try: - sprite.animate('shader.glow', 2.0, 1.0, mcrfpy.Easing.EASE_IN) - sprite_animation_works = True - except Exception as e: - sprite_animation_works = False - print(f" Sprite animation error: {e}") - - assert sprite_animation_works, "Animating Sprite shader uniforms should work" - - # Test Caption - caption = mcrfpy.Caption(text="Test", pos=(0, 0)) - caption.uniforms['alpha'] = 1.0 - try: - caption.animate('shader.alpha', 0.0, 0.5, mcrfpy.Easing.EASE_OUT) - caption_animation_works = True - except Exception as e: - caption_animation_works = False - print(f" Caption animation error: {e}") - - assert caption_animation_works, "Animating Caption shader uniforms should work" - - # Test Grid - grid = mcrfpy.Grid(grid_size=(5, 5), pos=(0, 0), size=(100, 100)) - grid.uniforms['zoom_effect'] = 1.0 - try: - grid.animate('shader.zoom_effect', 2.0, 1.0, mcrfpy.Easing.LINEAR) - grid_animation_works = True - except Exception as e: - grid_animation_works = False - print(f" Grid animation error: {e}") - - assert grid_animation_works, "Animating Grid shader uniforms should work" - - print(" PASS: Shader animation properties work") - - -def run_all_tests(): - """Run all shader tests""" - print("=" * 50) - print("Shader System Unit Tests") - print("=" * 50) - print() - - try: - test_shader_creation() - test_shader_source() - test_static_uniforms() - test_uniform_keys() - test_property_binding() - test_callable_binding() - test_shader_on_frame() - test_shader_on_sprite() - test_shader_on_caption() - test_shader_on_grid() - test_shader_on_entity() - test_shared_shader() - test_shader_animation_properties() - - print() - print("=" * 50) - print("ALL TESTS PASSED") - print("=" * 50) - sys.exit(0) - - except AssertionError as e: - print(f" FAIL: {e}") - sys.exit(1) - except Exception as e: - print(f" ERROR: {e}") - import traceback - traceback.print_exc() - sys.exit(1) - - -if __name__ == "__main__": - run_all_tests()