Compare commits
2 commits
41d551e6e1
...
da434dcc64
| Author | SHA1 | Date | |
|---|---|---|---|
| da434dcc64 | |||
| 486087b9cb |
27 changed files with 3514 additions and 174 deletions
|
|
@ -12,6 +12,10 @@
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <Python.h>
|
#include <Python.h>
|
||||||
|
|
||||||
|
// Static member definitions for shader intermediate texture (#106)
|
||||||
|
std::unique_ptr<sf::RenderTexture> GameEngine::shaderIntermediate;
|
||||||
|
bool GameEngine::shaderIntermediateInitialized = false;
|
||||||
|
|
||||||
// #219 - FrameLock implementation for thread-safe UI updates
|
// #219 - FrameLock implementation for thread-safe UI updates
|
||||||
|
|
||||||
void FrameLock::acquire() {
|
void FrameLock::acquire() {
|
||||||
|
|
@ -718,6 +722,37 @@ sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const
|
||||||
return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView);
|
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<sf::RenderTexture>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// #153 - Headless simulation control: step() advances simulation time
|
||||||
float GameEngine::step(float dt) {
|
float GameEngine::step(float dt) {
|
||||||
// In windowed mode, step() is a no-op
|
// In windowed mode, step() is a no-op
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,10 @@ private:
|
||||||
sf::View gameView; // View for the game content
|
sf::View gameView; // View for the game content
|
||||||
ViewportMode viewportMode = ViewportMode::Fit;
|
ViewportMode viewportMode = ViewportMode::Fit;
|
||||||
|
|
||||||
|
// Shader intermediate texture (#106) - shared texture for shader rendering
|
||||||
|
static std::unique_ptr<sf::RenderTexture> shaderIntermediate;
|
||||||
|
static bool shaderIntermediateInitialized;
|
||||||
|
|
||||||
// Profiling overlay
|
// Profiling overlay
|
||||||
bool showProfilerOverlay = false; // F3 key toggles this
|
bool showProfilerOverlay = false; // F3 key toggles this
|
||||||
int overlayUpdateCounter = 0; // Only update overlay every N frames
|
int overlayUpdateCounter = 0; // Only update overlay every N frames
|
||||||
|
|
@ -257,6 +261,11 @@ public:
|
||||||
std::string getViewportModeString() const;
|
std::string getViewportModeString() const;
|
||||||
sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) 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
|
// #153 - Headless simulation control
|
||||||
float step(float dt = -1.0f); // Advance simulation; dt<0 means advance to next event
|
float step(float dt = -1.0f); // Advance simulation; dt<0 means advance to next event
|
||||||
int getSimulationTime() const { return simulation_time; }
|
int getSimulationTime() const { return simulation_time; }
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@
|
||||||
#include "PyNoiseSource.h" // Procedural generation noise (#207-208)
|
#include "PyNoiseSource.h" // Procedural generation noise (#207-208)
|
||||||
#include "PyLock.h" // Thread synchronization (#219)
|
#include "PyLock.h" // Thread synchronization (#219)
|
||||||
#include "PyVector.h" // For bresenham Vector support (#215)
|
#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 "McRogueFaceVersion.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "ImGuiConsole.h"
|
#include "ImGuiConsole.h"
|
||||||
|
|
@ -452,6 +455,11 @@ PyObject* PyInit_mcrfpy()
|
||||||
&mcrfpydef::PyBSPType,
|
&mcrfpydef::PyBSPType,
|
||||||
&mcrfpydef::PyNoiseSourceType,
|
&mcrfpydef::PyNoiseSourceType,
|
||||||
|
|
||||||
|
/*shaders (#106)*/
|
||||||
|
&mcrfpydef::PyShaderType,
|
||||||
|
&mcrfpydef::PyPropertyBindingType,
|
||||||
|
&mcrfpydef::PyCallableBindingType,
|
||||||
|
|
||||||
nullptr};
|
nullptr};
|
||||||
|
|
||||||
// Types that are used internally but NOT exported to module namespace (#189)
|
// Types that are used internally but NOT exported to module namespace (#189)
|
||||||
|
|
@ -473,6 +481,9 @@ PyObject* PyInit_mcrfpy()
|
||||||
&mcrfpydef::PyBSPAdjacencyType, // #210: BSP.adjacency wrapper
|
&mcrfpydef::PyBSPAdjacencyType, // #210: BSP.adjacency wrapper
|
||||||
&mcrfpydef::PyBSPAdjacentTilesType, // #210: BSPNode.adjacent_tiles wrapper
|
&mcrfpydef::PyBSPAdjacentTilesType, // #210: BSPNode.adjacent_tiles wrapper
|
||||||
|
|
||||||
|
/*shader uniform collection - returned by drawable.uniforms but not directly instantiable (#106)*/
|
||||||
|
&mcrfpydef::PyUniformCollectionType,
|
||||||
|
|
||||||
nullptr};
|
nullptr};
|
||||||
|
|
||||||
// Set up PyWindowType methods and getsetters before PyType_Ready
|
// Set up PyWindowType methods and getsetters before PyType_Ready
|
||||||
|
|
@ -497,6 +508,17 @@ PyObject* PyInit_mcrfpy()
|
||||||
mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods;
|
mcrfpydef::PyNoiseSourceType.tp_methods = PyNoiseSource::methods;
|
||||||
mcrfpydef::PyNoiseSourceType.tp_getset = PyNoiseSource::getsetters;
|
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
|
// Set up weakref support for all types that need it
|
||||||
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
|
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
|
||||||
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
||||||
|
|
|
||||||
256
src/PyShader.cpp
Normal file
256
src/PyShader.cpp
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
#include "PyShader.h"
|
||||||
|
#include "McRFPy_API.h"
|
||||||
|
#include "McRFPy_Doc.h"
|
||||||
|
#include "GameEngine.h"
|
||||||
|
#include "Resources.h"
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
// 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<char**>(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<sf::Shader>();
|
||||||
|
|
||||||
|
// 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";
|
||||||
|
if (self->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<char**>(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<float>(mousePos.x), static_cast<float>(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();
|
||||||
|
}
|
||||||
94
src/PyShader.h
Normal file
94
src/PyShader.h
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
#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<sf::Shader> 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
341
src/PyUniformBinding.cpp
Normal file
341
src/PyUniformBinding.cpp
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
#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 <sstream>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PropertyBinding Implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
PropertyBinding::PropertyBinding(std::weak_ptr<UIDrawable> target, const std::string& property)
|
||||||
|
: target(target), property_name(property) {}
|
||||||
|
|
||||||
|
std::optional<float> 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<float> 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<float>(PyFloat_AsDouble(result));
|
||||||
|
} else if (PyLong_Check(result)) {
|
||||||
|
value = static_cast<float>(PyLong_AsDouble(result));
|
||||||
|
} else {
|
||||||
|
// Try to convert to float
|
||||||
|
PyObject* float_result = PyNumber_Float(result);
|
||||||
|
if (float_result) {
|
||||||
|
value = static_cast<float>(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<char**>(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<UIDrawable> 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<PropertyBinding>(target_ptr, property);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyPropertyBindingType::repr(PyObject* obj) {
|
||||||
|
PyPropertyBindingObject* self = (PyPropertyBindingObject*)obj;
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << "<PropertyBinding";
|
||||||
|
if (self->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<char**>(keywords),
|
||||||
|
&callable)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PyCallable_Check(callable)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "Argument must be callable");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->binding = std::make_shared<CallableBinding>(callable);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyCallableBindingType::repr(PyObject* obj) {
|
||||||
|
PyCallableBindingObject* self = (PyCallableBindingObject*)obj;
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << "<CallableBinding";
|
||||||
|
if (self->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());
|
||||||
|
}
|
||||||
201
src/PyUniformBinding.h
Normal file
201
src/PyUniformBinding.h
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include <variant>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
// 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<float> 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<UIDrawable> target, const std::string& property);
|
||||||
|
|
||||||
|
std::optional<float> evaluate() const override;
|
||||||
|
bool isValid() const override;
|
||||||
|
|
||||||
|
// Accessors for Python
|
||||||
|
std::weak_ptr<UIDrawable> getTarget() const { return target; }
|
||||||
|
const std::string& getPropertyName() const { return property_name; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::weak_ptr<UIDrawable> 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<float> 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<PropertyBinding> binding;
|
||||||
|
PyObject* weakreflist;
|
||||||
|
} PyPropertyBindingObject;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD
|
||||||
|
std::shared_ptr<CallableBinding> 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
405
src/PyUniformCollection.cpp
Normal file
405
src/PyUniformCollection.cpp
Normal file
|
|
@ -0,0 +1,405 @@
|
||||||
|
#include "PyUniformCollection.h"
|
||||||
|
#include "UIDrawable.h"
|
||||||
|
#include "McRFPy_API.h"
|
||||||
|
#include "McRFPy_Doc.h"
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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<PropertyBinding> binding) {
|
||||||
|
entries[name] = binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UniformCollection::setCallableBinding(const std::string& name, std::shared_ptr<CallableBinding> 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<std::string> UniformCollection::getNames() const {
|
||||||
|
std::vector<std::string> 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<decltype(arg)>;
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<T, UniformValue>) {
|
||||||
|
// Static value
|
||||||
|
std::visit([&shader, &name](auto&& val) {
|
||||||
|
using V = std::decay_t<decltype(val)>;
|
||||||
|
if constexpr (std::is_same_v<V, float>) {
|
||||||
|
shader.setUniform(name, val);
|
||||||
|
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec2>) {
|
||||||
|
shader.setUniform(name, val);
|
||||||
|
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec3>) {
|
||||||
|
shader.setUniform(name, val);
|
||||||
|
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec4>) {
|
||||||
|
shader.setUniform(name, val);
|
||||||
|
}
|
||||||
|
}, arg);
|
||||||
|
}
|
||||||
|
else if constexpr (std::is_same_v<T, std::shared_ptr<PropertyBinding>>) {
|
||||||
|
// 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<T, std::shared_ptr<CallableBinding>>) {
|
||||||
|
// 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<std::shared_ptr<CallableBinding>>(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 << "<UniformCollection";
|
||||||
|
if (self->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<Py_ssize_t>(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<decltype(arg)>;
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<T, UniformValue>) {
|
||||||
|
return std::visit([](auto&& val) -> PyObject* {
|
||||||
|
using V = std::decay_t<decltype(val)>;
|
||||||
|
if constexpr (std::is_same_v<V, float>) {
|
||||||
|
return PyFloat_FromDouble(val);
|
||||||
|
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec2>) {
|
||||||
|
return Py_BuildValue("(ff)", val.x, val.y);
|
||||||
|
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec3>) {
|
||||||
|
return Py_BuildValue("(fff)", val.x, val.y, val.z);
|
||||||
|
} else if constexpr (std::is_same_v<V, sf::Glsl::Vec4>) {
|
||||||
|
return Py_BuildValue("(ffff)", val.x, val.y, val.z, val.w);
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}, arg);
|
||||||
|
}
|
||||||
|
else if constexpr (std::is_same_v<T, std::shared_ptr<PropertyBinding>>) {
|
||||||
|
// 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<T, std::shared_ptr<CallableBinding>>) {
|
||||||
|
// 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<float>(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<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 0)));
|
||||||
|
float y = static_cast<float>(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<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 0)));
|
||||||
|
float y = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 1)));
|
||||||
|
float z = static_cast<float>(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<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 0)));
|
||||||
|
float y = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 1)));
|
||||||
|
float z = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(value, 2)));
|
||||||
|
float w = static_cast<float>(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;
|
||||||
|
}
|
||||||
157
src/PyUniformCollection.h
Normal file
157
src/PyUniformCollection.h
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include "PyUniformBinding.h"
|
||||||
|
#include <map>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
// 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<PropertyBinding>, // Property binding
|
||||||
|
std::shared_ptr<CallableBinding> // 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<PropertyBinding> binding);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set a callable binding
|
||||||
|
*/
|
||||||
|
void setCallableBinding(const std::string& name, std::shared_ptr<CallableBinding> 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<std::string> 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<std::string, UniformEntry> entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Python object structure for UniformCollection
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD
|
||||||
|
UniformCollection* collection; // Owned by UIDrawable, not by this object
|
||||||
|
std::weak_ptr<UIDrawable> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -134,9 +134,13 @@ void UIArc::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
||||||
rebuildVertices();
|
rebuildVertices();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply offset by creating a transformed copy
|
// Apply offset and rotation by creating a transform
|
||||||
sf::Transform transform;
|
sf::Transform transform;
|
||||||
transform.translate(offset);
|
transform.translate(offset);
|
||||||
|
// Apply rotation around origin
|
||||||
|
transform.translate(origin);
|
||||||
|
transform.rotate(rotation);
|
||||||
|
transform.translate(-origin);
|
||||||
target.draw(vertices, transform);
|
target.draw(vertices, transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,9 +150,25 @@ UIDrawable* UIArc::click_at(sf::Vector2f point) {
|
||||||
// #184: Also check for Python subclass (might have on_click method)
|
// #184: Also check for Python subclass (might have on_click method)
|
||||||
if (!click_callable && !is_python_subclass) return nullptr;
|
if (!click_callable && !is_python_subclass) return nullptr;
|
||||||
|
|
||||||
// Calculate distance from center
|
// Transform click point to local coordinates accounting for rotation
|
||||||
float dx = point.x - center.x;
|
sf::Vector2f localPoint;
|
||||||
float dy = point.y - center.y;
|
if (rotation != 0.0f) {
|
||||||
|
// Build transform: rotate around origin (matches render transform)
|
||||||
|
sf::Transform transform;
|
||||||
|
transform.translate(origin);
|
||||||
|
transform.rotate(rotation);
|
||||||
|
transform.translate(-origin);
|
||||||
|
|
||||||
|
// Apply inverse transform to get local coordinates
|
||||||
|
sf::Transform inverse = transform.getInverse();
|
||||||
|
localPoint = inverse.transformPoint(point);
|
||||||
|
} else {
|
||||||
|
localPoint = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate distance from center in local (unrotated) space
|
||||||
|
float dx = localPoint.x - center.x;
|
||||||
|
float dy = localPoint.y - center.y;
|
||||||
float dist = std::sqrt(dx * dx + dy * dy);
|
float dist = std::sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
// Check if within the arc's radial range
|
// Check if within the arc's radial range
|
||||||
|
|
@ -249,6 +269,21 @@ bool UIArc::setProperty(const std::string& name, float value) {
|
||||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -295,6 +330,18 @@ bool UIArc::getProperty(const std::string& name, float& value) const {
|
||||||
value = center.y;
|
value = center.y;
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,7 +364,8 @@ bool UIArc::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
bool UIArc::hasProperty(const std::string& name) const {
|
bool UIArc::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "radius" || name == "start_angle" || name == "end_angle" ||
|
if (name == "radius" || name == "start_angle" || name == "end_angle" ||
|
||||||
name == "thickness" || name == "x" || name == "y") {
|
name == "thickness" || name == "x" || name == "y" ||
|
||||||
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Color properties
|
// Color properties
|
||||||
|
|
@ -453,6 +501,7 @@ PyGetSetDef UIArc::getsetters[] = {
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIARC),
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIARC),
|
||||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIARC),
|
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIARC),
|
||||||
|
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIARC),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
34
src/UIBase.h
34
src/UIBase.h
|
|
@ -282,4 +282,38 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
|
||||||
"Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER)." \
|
"Invalid for horizontally-centered alignments (CENTER_LEFT, CENTER_RIGHT, CENTER)." \
|
||||||
), (void*)type_enum}
|
), (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
|
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
#include "PyFont.h"
|
#include "PyFont.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "PyAlignment.h"
|
#include "PyAlignment.h"
|
||||||
|
#include "PyShader.h" // #106: Shader support
|
||||||
|
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
@ -23,11 +25,40 @@ UICaption::UICaption()
|
||||||
UIDrawable* UICaption::click_at(sf::Vector2f point)
|
UIDrawable* UICaption::click_at(sf::Vector2f point)
|
||||||
{
|
{
|
||||||
// #184: Also check for Python subclass (might have on_click method)
|
// #184: Also check for Python subclass (might have on_click method)
|
||||||
if (click_callable || is_python_subclass)
|
if (!click_callable && !is_python_subclass) return nullptr;
|
||||||
{
|
|
||||||
if (text.getGlobalBounds().contains(point)) return this;
|
// Get text dimensions from local bounds
|
||||||
|
sf::FloatRect localBounds = text.getLocalBounds();
|
||||||
|
float w = localBounds.width;
|
||||||
|
float h = localBounds.height;
|
||||||
|
// Account for text origin offset (SFML text has non-zero left/top in local bounds)
|
||||||
|
float textOffsetX = localBounds.left;
|
||||||
|
float textOffsetY = localBounds.top;
|
||||||
|
|
||||||
|
// Transform click point to local coordinates accounting for rotation
|
||||||
|
sf::Vector2f localPoint;
|
||||||
|
if (rotation != 0.0f) {
|
||||||
|
// Build transform: translate to position, then rotate around origin
|
||||||
|
sf::Transform transform;
|
||||||
|
transform.translate(position);
|
||||||
|
transform.translate(origin);
|
||||||
|
transform.rotate(rotation);
|
||||||
|
transform.translate(-origin);
|
||||||
|
|
||||||
|
// Apply inverse transform to get local coordinates
|
||||||
|
sf::Transform inverse = transform.getInverse();
|
||||||
|
localPoint = inverse.transformPoint(point);
|
||||||
|
} else {
|
||||||
|
// No rotation - simple subtraction
|
||||||
|
localPoint = point - position;
|
||||||
}
|
}
|
||||||
return NULL;
|
|
||||||
|
// Check if local point is within bounds (accounting for text offset)
|
||||||
|
if (localPoint.x >= textOffsetX && localPoint.y >= textOffsetY &&
|
||||||
|
localPoint.x < textOffsetX + w && localPoint.y < textOffsetY + h) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
|
|
@ -40,10 +71,47 @@ void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
color.a = static_cast<sf::Uint8>(255 * opacity);
|
color.a = static_cast<sf::Uint8>(255 * opacity);
|
||||||
text.setFillColor(color);
|
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);
|
text.move(offset);
|
||||||
//Resources::game->getWindow().draw(text);
|
|
||||||
target.draw(text);
|
target.draw(text);
|
||||||
text.move(-offset);
|
text.move(-offset);
|
||||||
|
}
|
||||||
|
|
||||||
// Restore original alpha
|
// Restore original alpha
|
||||||
color.a = 255;
|
color.a = 255;
|
||||||
|
|
@ -314,6 +382,8 @@ PyGetSetDef UICaption::getsetters[] = {
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||||
|
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||||
|
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UICAPTION),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -595,6 +665,28 @@ bool UICaption::setProperty(const std::string& name, float value) {
|
||||||
markDirty(); // #144 - Z-order change affects parent
|
markDirty(); // #144 - Z-order change affects parent
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -674,6 +766,22 @@ bool UICaption::getProperty(const std::string& name, float& value) const {
|
||||||
value = static_cast<float>(z_index);
|
value = static_cast<float>(z_index);
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -704,7 +812,8 @@ bool UICaption::hasProperty(const std::string& name) const {
|
||||||
name == "fill_color.r" || name == "fill_color.g" ||
|
name == "fill_color.r" || name == "fill_color.g" ||
|
||||||
name == "fill_color.b" || name == "fill_color.a" ||
|
name == "fill_color.b" || name == "fill_color.a" ||
|
||||||
name == "outline_color.r" || name == "outline_color.g" ||
|
name == "outline_color.r" || name == "outline_color.g" ||
|
||||||
name == "outline_color.b" || name == "outline_color.a") {
|
name == "outline_color.b" || name == "outline_color.a" ||
|
||||||
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Color properties
|
// Color properties
|
||||||
|
|
@ -715,6 +824,14 @@ bool UICaption::hasProperty(const std::string& name) const {
|
||||||
if (name == "text") {
|
if (name == "text") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Vector2f properties
|
||||||
|
if (name == "origin") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// #106: Check for shader uniform properties
|
||||||
|
if (hasShaderProperty(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,12 @@ void UICircle::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
||||||
// Apply position and offset
|
// Apply position and offset
|
||||||
shape.setPosition(position + 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
|
// Apply opacity to colors
|
||||||
sf::Color render_fill = fill_color;
|
sf::Color render_fill = fill_color;
|
||||||
render_fill.a = static_cast<sf::Uint8>(fill_color.a * opacity);
|
render_fill.a = static_cast<sf::Uint8>(fill_color.a * opacity);
|
||||||
|
|
@ -131,9 +137,30 @@ UIDrawable* UICircle::click_at(sf::Vector2f point) {
|
||||||
// #184: Also check for Python subclass (might have on_click method)
|
// #184: Also check for Python subclass (might have on_click method)
|
||||||
if (!click_callable && !is_python_subclass) return nullptr;
|
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)
|
// Check if point is within the circle (including outline)
|
||||||
float dx = point.x - position.x;
|
float dx = point.x - circleCenter.x;
|
||||||
float dy = point.y - position.y;
|
float dy = point.y - circleCenter.y;
|
||||||
float distance = std::sqrt(dx * dx + dy * dy);
|
float distance = std::sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
float effective_radius = radius + outline_thickness;
|
float effective_radius = radius + outline_thickness;
|
||||||
|
|
@ -188,6 +215,21 @@ bool UICircle::setProperty(const std::string& name, float value) {
|
||||||
position.y = value;
|
position.y = value;
|
||||||
markCompositeDirty(); // #144 - Position change, texture still valid
|
markCompositeDirty(); // #144 - Position change, texture still valid
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -227,6 +269,15 @@ bool UICircle::getProperty(const std::string& name, float& value) const {
|
||||||
} else if (name == "y") {
|
} else if (name == "y") {
|
||||||
value = position.y;
|
value = position.y;
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +304,8 @@ bool UICircle::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
bool UICircle::hasProperty(const std::string& name) const {
|
bool UICircle::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "radius" || name == "outline" ||
|
if (name == "radius" || name == "outline" ||
|
||||||
name == "x" || name == "y") {
|
name == "x" || name == "y" ||
|
||||||
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Color properties
|
// Color properties
|
||||||
|
|
@ -261,7 +313,7 @@ bool UICircle::hasProperty(const std::string& name) const {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Vector2f properties
|
// Vector2f properties
|
||||||
if (name == "center" || name == "position") {
|
if (name == "center" || name == "position" || name == "origin") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -399,6 +451,7 @@ PyGetSetDef UICircle::getsetters[] = {
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||||
|
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UICIRCLE),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
#include "PyAnimation.h"
|
#include "PyAnimation.h"
|
||||||
#include "PyEasing.h"
|
#include "PyEasing.h"
|
||||||
#include "PySceneObject.h" // #183: For scene parent lookup
|
#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
|
// Helper function to extract UIDrawable* from any Python UI object
|
||||||
// Returns nullptr and sets Python error on failure
|
// Returns nullptr and sets Python error on failure
|
||||||
|
|
@ -44,6 +46,9 @@ UIDrawable::UIDrawable(const UIDrawable& other)
|
||||||
: z_index(other.z_index),
|
: z_index(other.z_index),
|
||||||
name(other.name),
|
name(other.name),
|
||||||
position(other.position),
|
position(other.position),
|
||||||
|
rotation(other.rotation),
|
||||||
|
origin(other.origin),
|
||||||
|
rotate_with_camera(other.rotate_with_camera),
|
||||||
visible(other.visible),
|
visible(other.visible),
|
||||||
opacity(other.opacity),
|
opacity(other.opacity),
|
||||||
hovered(false), // Don't copy hover state
|
hovered(false), // Don't copy hover state
|
||||||
|
|
@ -80,6 +85,9 @@ UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
|
||||||
z_index = other.z_index;
|
z_index = other.z_index;
|
||||||
name = other.name;
|
name = other.name;
|
||||||
position = other.position;
|
position = other.position;
|
||||||
|
rotation = other.rotation;
|
||||||
|
origin = other.origin;
|
||||||
|
rotate_with_camera = other.rotate_with_camera;
|
||||||
visible = other.visible;
|
visible = other.visible;
|
||||||
opacity = other.opacity;
|
opacity = other.opacity;
|
||||||
hovered = false; // Don't copy hover state
|
hovered = false; // Don't copy hover state
|
||||||
|
|
@ -126,6 +134,9 @@ UIDrawable::UIDrawable(UIDrawable&& other) noexcept
|
||||||
: z_index(other.z_index),
|
: z_index(other.z_index),
|
||||||
name(std::move(other.name)),
|
name(std::move(other.name)),
|
||||||
position(other.position),
|
position(other.position),
|
||||||
|
rotation(other.rotation),
|
||||||
|
origin(other.origin),
|
||||||
|
rotate_with_camera(other.rotate_with_camera),
|
||||||
visible(other.visible),
|
visible(other.visible),
|
||||||
opacity(other.opacity),
|
opacity(other.opacity),
|
||||||
hovered(other.hovered),
|
hovered(other.hovered),
|
||||||
|
|
@ -155,6 +166,9 @@ UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept {
|
||||||
z_index = other.z_index;
|
z_index = other.z_index;
|
||||||
name = std::move(other.name);
|
name = std::move(other.name);
|
||||||
position = other.position;
|
position = other.position;
|
||||||
|
rotation = other.rotation;
|
||||||
|
origin = other.origin;
|
||||||
|
rotate_with_camera = other.rotate_with_camera;
|
||||||
visible = other.visible;
|
visible = other.visible;
|
||||||
opacity = other.opacity;
|
opacity = other.opacity;
|
||||||
hovered = other.hovered; // #140
|
hovered = other.hovered; // #140
|
||||||
|
|
@ -587,6 +601,132 @@ int UIDrawable::set_pos(PyObject* self, PyObject* value, void* closure) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotation property getter/setter
|
||||||
|
PyObject* UIDrawable::get_rotation(PyObject* self, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||||
|
if (!drawable) return NULL;
|
||||||
|
|
||||||
|
return PyFloat_FromDouble(drawable->rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIDrawable::set_rotation(PyObject* self, PyObject* value, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||||
|
if (!drawable) return -1;
|
||||||
|
|
||||||
|
float val = 0.0f;
|
||||||
|
if (PyFloat_Check(value)) {
|
||||||
|
val = PyFloat_AsDouble(value);
|
||||||
|
} else if (PyLong_Check(value)) {
|
||||||
|
val = static_cast<float>(PyLong_AsLong(value));
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "rotation must be a number (int or float)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawable->rotation = val;
|
||||||
|
drawable->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Origin property getter/setter
|
||||||
|
PyObject* UIDrawable::get_origin(PyObject* self, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||||
|
if (!drawable) return NULL;
|
||||||
|
|
||||||
|
// Create a Python Vector object from origin
|
||||||
|
PyObject* module = PyImport_ImportModule("mcrfpy");
|
||||||
|
if (!module) return NULL;
|
||||||
|
|
||||||
|
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
|
||||||
|
Py_DECREF(module);
|
||||||
|
if (!vector_type) return NULL;
|
||||||
|
|
||||||
|
PyObject* args = Py_BuildValue("(ff)", drawable->origin.x, drawable->origin.y);
|
||||||
|
PyObject* result = PyObject_CallObject(vector_type, args);
|
||||||
|
Py_DECREF(vector_type);
|
||||||
|
Py_DECREF(args);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIDrawable::set_origin(PyObject* self, PyObject* value, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||||
|
if (!drawable) return -1;
|
||||||
|
|
||||||
|
// Accept tuple or Vector
|
||||||
|
float x, y;
|
||||||
|
if (PyTuple_Check(value) && PyTuple_Size(value) == 2) {
|
||||||
|
PyObject* x_obj = PyTuple_GetItem(value, 0);
|
||||||
|
PyObject* y_obj = PyTuple_GetItem(value, 1);
|
||||||
|
|
||||||
|
if (PyFloat_Check(x_obj) || PyLong_Check(x_obj)) {
|
||||||
|
x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : static_cast<float>(PyLong_AsLong(x_obj));
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "origin x must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PyFloat_Check(y_obj) || PyLong_Check(y_obj)) {
|
||||||
|
y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : static_cast<float>(PyLong_AsLong(y_obj));
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "origin y must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try to get as Vector
|
||||||
|
PyObject* module = PyImport_ImportModule("mcrfpy");
|
||||||
|
if (!module) return -1;
|
||||||
|
|
||||||
|
PyObject* vector_type = PyObject_GetAttrString(module, "Vector");
|
||||||
|
Py_DECREF(module);
|
||||||
|
if (!vector_type) return -1;
|
||||||
|
|
||||||
|
int is_vector = PyObject_IsInstance(value, vector_type);
|
||||||
|
Py_DECREF(vector_type);
|
||||||
|
|
||||||
|
if (is_vector) {
|
||||||
|
PyVectorObject* vec = (PyVectorObject*)value;
|
||||||
|
x = vec->data.x;
|
||||||
|
y = vec->data.y;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "origin must be a tuple (x, y) or Vector");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawable->origin = sf::Vector2f(x, y);
|
||||||
|
drawable->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate_with_camera property getter/setter
|
||||||
|
PyObject* UIDrawable::get_rotate_with_camera(PyObject* self, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||||
|
if (!drawable) return NULL;
|
||||||
|
|
||||||
|
return PyBool_FromLong(drawable->rotate_with_camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIDrawable::set_rotate_with_camera(PyObject* self, PyObject* value, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||||
|
if (!drawable) return -1;
|
||||||
|
|
||||||
|
if (!PyBool_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "rotate_with_camera must be a boolean");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawable->rotate_with_camera = PyObject_IsTrue(value);
|
||||||
|
drawable->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// #221 - Grid coordinate properties (only valid when parent is UIGrid)
|
// #221 - Grid coordinate properties (only valid when parent is UIGrid)
|
||||||
PyObject* UIDrawable::get_grid_pos(PyObject* self, void* closure) {
|
PyObject* UIDrawable::get_grid_pos(PyObject* self, void* closure) {
|
||||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
|
@ -947,6 +1087,195 @@ void UIDrawable::markDirty() {
|
||||||
markContentDirty();
|
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<UniformCollection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<UniformValue, shared_ptr<PropertyBinding>, shared_ptr<CallableBinding>>
|
||||||
|
// UniformValue is variant<float, vec2, vec3, vec4>
|
||||||
|
// 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<UniformValue>(entry)) {
|
||||||
|
// Now try to extract float from UniformValue
|
||||||
|
if (const float* fval = std::get_if<float>(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<std::shared_ptr<PropertyBinding>>(entry)) {
|
||||||
|
auto opt_val = (*prop_binding)->evaluate();
|
||||||
|
if (opt_val) {
|
||||||
|
value = *opt_val;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (const auto* call_binding = std::get_if<std::shared_ptr<CallableBinding>>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyObjectsEnum>(reinterpret_cast<intptr_t>(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<PyShaderObject>(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<UniformCollection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<unsigned int>(bounds.width),
|
||||||
|
static_cast<unsigned int>(bounds.height)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawable->markDirty();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIDrawable::get_uniforms(PyObject* self, void* closure) {
|
||||||
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
UIDrawable* drawable = extractDrawable(self, objtype);
|
||||||
|
if (!drawable) return NULL;
|
||||||
|
|
||||||
|
// Create uniforms collection if needed
|
||||||
|
if (!drawable->uniforms) {
|
||||||
|
drawable->uniforms = std::make_unique<UniformCollection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Python API - get parent drawable
|
||||||
PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
|
PyObject* UIDrawable::get_parent(PyObject* self, void* closure) {
|
||||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<intptr_t>(closure));
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@
|
||||||
|
|
||||||
#include "Resources.h"
|
#include "Resources.h"
|
||||||
#include "UIBase.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;
|
class UIFrame; class UICaption; class UISprite; class UIEntity; class UIGrid;
|
||||||
|
|
||||||
enum PyObjectsEnum : int
|
enum PyObjectsEnum : int
|
||||||
|
|
@ -93,6 +99,14 @@ public:
|
||||||
static PyObject* get_pos(PyObject* self, void* closure);
|
static PyObject* get_pos(PyObject* self, void* closure);
|
||||||
static int set_pos(PyObject* self, PyObject* value, 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)
|
// #221 - Grid coordinate properties (only valid when parent is UIGrid)
|
||||||
static PyObject* get_grid_pos(PyObject* self, void* closure);
|
static PyObject* get_grid_pos(PyObject* self, void* closure);
|
||||||
static int set_grid_pos(PyObject* self, PyObject* value, void* closure);
|
static int set_grid_pos(PyObject* self, PyObject* value, void* closure);
|
||||||
|
|
@ -111,6 +125,16 @@ public:
|
||||||
// Position in pixel coordinates (moved from derived classes)
|
// Position in pixel coordinates (moved from derived classes)
|
||||||
sf::Vector2f position;
|
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)
|
// Parent-child hierarchy (#122)
|
||||||
std::weak_ptr<UIDrawable> parent;
|
std::weak_ptr<UIDrawable> parent;
|
||||||
|
|
||||||
|
|
@ -205,6 +229,12 @@ public:
|
||||||
// Check if a property name is valid for animation on this drawable type
|
// Check if a property name is valid for animation on this drawable type
|
||||||
virtual bool hasProperty(const std::string& name) const { return false; }
|
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
|
// Note: animate_helper is now a free function (UIDrawable_animate_impl) declared in UIBase.h
|
||||||
// to avoid incomplete type issues with template instantiation.
|
// to avoid incomplete type issues with template instantiation.
|
||||||
|
|
||||||
|
|
@ -253,6 +283,20 @@ protected:
|
||||||
public:
|
public:
|
||||||
void disableRenderTexture();
|
void disableRenderTexture();
|
||||||
|
|
||||||
|
// Shader support (#106)
|
||||||
|
std::shared_ptr<PyShaderObject> shader;
|
||||||
|
std::unique_ptr<UniformCollection> 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:
|
protected:
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
#include "PyAnimation.h"
|
#include "PyAnimation.h"
|
||||||
#include "PyEasing.h"
|
#include "PyEasing.h"
|
||||||
#include "PyPositionHelper.h"
|
#include "PyPositionHelper.h"
|
||||||
|
#include "PyShader.h" // #106: Shader support
|
||||||
|
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
#include "UIEntityPyMethods.h"
|
#include "UIEntityPyMethods.h"
|
||||||
|
|
||||||
|
|
@ -1035,6 +1037,14 @@ PyGetSetDef UIEntity::getsetters[] = {
|
||||||
{"visible", (getter)UIEntity_get_visible, (setter)UIEntity_set_visible, "Visibility flag", NULL},
|
{"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},
|
{"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},
|
{"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 */
|
{NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1073,6 +1083,10 @@ bool UIEntity::setProperty(const std::string& name, float value) {
|
||||||
if (grid) grid->markDirty(); // #144 - Content change
|
if (grid) grid->markDirty(); // #144 - Content change
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// #106: Shader uniform properties - delegate to sprite
|
||||||
|
if (sprite.setShaderProperty(name, value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1098,6 +1112,10 @@ bool UIEntity::getProperty(const std::string& name, float& value) const {
|
||||||
value = sprite.getScale().x; // Assuming uniform scale
|
value = sprite.getScale().x; // Assuming uniform scale
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// #106: Shader uniform properties - delegate to sprite
|
||||||
|
if (sprite.getShaderProperty(name, value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1110,6 +1128,10 @@ bool UIEntity::hasProperty(const std::string& name) const {
|
||||||
if (name == "sprite_index" || name == "sprite_number") {
|
if (name == "sprite_index" || name == "sprite_number") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// #106: Shader uniform properties - delegate to sprite
|
||||||
|
if (sprite.hasShaderProperty(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
#include "UIBase.h"
|
#include "UIBase.h"
|
||||||
|
#include "PyShader.h" // #106: Shader support
|
||||||
|
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||||
|
|
||||||
// UIEntity-specific property implementations
|
// UIEntity-specific property implementations
|
||||||
// These delegate to the wrapped sprite member
|
// These delegate to the wrapped sprite member
|
||||||
|
|
@ -73,3 +75,72 @@ static int UIEntity_set_name(PyUIEntityObject* self, PyObject* value, void* clos
|
||||||
self->data->sprite.name = name_str;
|
self->data->sprite.name = name_str;
|
||||||
return 0;
|
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<PyShaderObject>(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<UniformCollection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<UniformCollection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
202
src/UIFrame.cpp
202
src/UIFrame.cpp
|
|
@ -8,19 +8,37 @@
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "PyAlignment.h"
|
#include "PyAlignment.h"
|
||||||
|
#include "PyShader.h" // #106: Shader support
|
||||||
|
#include "PyUniformCollection.h" // #106: Uniform collection
|
||||||
#include <iostream> // #106: for shader error output
|
#include <iostream> // #106: for shader error output
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
|
|
||||||
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||||
{
|
{
|
||||||
// Check bounds first (optimization)
|
float w = box.getSize().x, h = box.getSize().y;
|
||||||
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) {
|
// Transform click point to local coordinates accounting for rotation
|
||||||
return nullptr;
|
sf::Vector2f localPoint;
|
||||||
|
if (rotation != 0.0f) {
|
||||||
|
// Build transform: translate to position, then rotate around origin
|
||||||
|
sf::Transform transform;
|
||||||
|
transform.translate(position);
|
||||||
|
transform.translate(origin);
|
||||||
|
transform.rotate(rotation);
|
||||||
|
transform.translate(-origin);
|
||||||
|
|
||||||
|
// Apply inverse transform to get local coordinates
|
||||||
|
sf::Transform inverse = transform.getInverse();
|
||||||
|
localPoint = inverse.transformPoint(point);
|
||||||
|
} else {
|
||||||
|
// No rotation - simple subtraction
|
||||||
|
localPoint = point - position;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform to local coordinates for children
|
// Check if local point is within bounds (0,0 to w,h in local space)
|
||||||
sf::Vector2f localPoint = point - position;
|
if (localPoint.x < 0 || localPoint.y < 0 || localPoint.x >= w || localPoint.y >= h) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Check children in reverse order (top to bottom, highest z-index first)
|
// Check children in reverse order (top to bottom, highest z-index first)
|
||||||
for (auto it = children->rbegin(); it != children->rend(); ++it) {
|
for (auto it = children->rbegin(); it != children->rend(); ++it) {
|
||||||
|
|
@ -110,11 +128,11 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
|
|
||||||
// TODO: Apply opacity when SFML supports it on shapes
|
// TODO: Apply opacity when SFML supports it on shapes
|
||||||
|
|
||||||
// #144: Use RenderTexture for clipping OR texture caching OR shaders
|
// #144: Use RenderTexture for clipping OR texture caching OR shaders (#106)
|
||||||
// clip_children: requires texture for clipping effect (only when has children)
|
// clip_children: requires texture for clipping effect (only when has children)
|
||||||
// cache_subtree: uses texture for performance (always, even without children)
|
// cache_subtree: uses texture for performance (always, even without children)
|
||||||
// shader_enabled: requires texture for shader post-processing
|
// shader: requires texture for shader post-processing
|
||||||
bool use_texture = (clip_children && !children->empty()) || cache_subtree || shader_enabled;
|
bool use_texture = (clip_children && !children->empty()) || cache_subtree || (shader && shader->shader);
|
||||||
|
|
||||||
if (use_texture) {
|
if (use_texture) {
|
||||||
// Enable RenderTexture if not already enabled
|
// Enable RenderTexture if not already enabled
|
||||||
|
|
@ -138,8 +156,10 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// Clear the RenderTexture
|
// Clear the RenderTexture
|
||||||
render_texture->clear(sf::Color::Transparent);
|
render_texture->clear(sf::Color::Transparent);
|
||||||
|
|
||||||
// Draw the frame box to RenderTexture
|
// Draw the frame box to RenderTexture (without rotation - that's applied to the final sprite)
|
||||||
box.setPosition(0, 0); // Render at origin in texture
|
box.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);
|
render_texture->draw(box);
|
||||||
|
|
||||||
// Sort children by z_index if needed
|
// Sort children by z_index if needed
|
||||||
|
|
@ -170,13 +190,23 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// Use `position` instead of box.getPosition() - box was set to (0,0) for texture rendering
|
// Use `position` instead of box.getPosition() - box was set to (0,0) for texture rendering
|
||||||
render_sprite.setPosition(offset + position);
|
render_sprite.setPosition(offset + position);
|
||||||
|
|
||||||
// #106 POC: Apply shader if enabled
|
// Apply rotation to the rendered sprite (children rotate with parent)
|
||||||
if (shader_enabled && shader) {
|
render_sprite.setOrigin(origin);
|
||||||
// Update time uniform for animated effects
|
render_sprite.setRotation(rotation);
|
||||||
static sf::Clock shader_clock;
|
|
||||||
shader->setUniform("time", shader_clock.getElapsedTime().asSeconds());
|
// #106: Apply shader if set
|
||||||
shader->setUniform("texture", sf::Shader::CurrentTexture);
|
if (shader && shader->shader) {
|
||||||
target.draw(render_sprite, shader.get());
|
// 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());
|
||||||
} else {
|
} else {
|
||||||
target.draw(render_sprite);
|
target.draw(render_sprite);
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +215,8 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// Standard rendering without caching
|
// Standard rendering without caching
|
||||||
// Restore box position from `position` - may have been set to (0,0) by previous texture render
|
// Restore box position from `position` - may have been set to (0,0) by previous texture render
|
||||||
box.setPosition(offset + position);
|
box.setPosition(offset + position);
|
||||||
|
box.setOrigin(origin);
|
||||||
|
box.setRotation(rotation);
|
||||||
target.draw(box);
|
target.draw(box);
|
||||||
box.setPosition(position); // Restore to canonical position
|
box.setPosition(position); // Restore to canonical position
|
||||||
|
|
||||||
|
|
@ -197,6 +229,9 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
children_need_sort = false;
|
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) {
|
for (auto drawable : *children) {
|
||||||
drawable->render(offset + position, target); // Use `position` as source of truth
|
drawable->render(offset + position, target); // Use `position` as source of truth
|
||||||
}
|
}
|
||||||
|
|
@ -462,87 +497,6 @@ int UIFrame::set_cache_subtree(PyUIFrameObject* self, PyObject* value, void* clo
|
||||||
}
|
}
|
||||||
|
|
||||||
// #106 - Shader POC: shader_enabled property
|
// #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<unsigned int>(size.x),
|
|
||||||
static_cast<unsigned int>(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<sf::Shader>();
|
|
||||||
|
|
||||||
// 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
|
// Define the PyObjectType alias for the macros
|
||||||
typedef PyUIFrameObject PyObjectType;
|
typedef PyUIFrameObject PyObjectType;
|
||||||
|
|
||||||
|
|
@ -581,10 +535,11 @@ 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},
|
{"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},
|
{"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},
|
{"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_GETSETTERS,
|
||||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||||
|
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||||
|
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIFRAME),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -929,6 +884,25 @@ bool UIFrame::setProperty(const std::string& name, float value) {
|
||||||
box.setOutlineColor(color);
|
box.setOutlineColor(color);
|
||||||
markDirty();
|
markDirty();
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -961,6 +935,11 @@ bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
|
||||||
}
|
}
|
||||||
markDirty();
|
markDirty();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (name == "origin") {
|
||||||
|
origin = value;
|
||||||
|
box.setOrigin(origin);
|
||||||
|
markDirty();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1005,6 +984,19 @@ bool UIFrame::getProperty(const std::string& name, float& value) const {
|
||||||
} else if (name == "outline_color.a") {
|
} else if (name == "outline_color.a") {
|
||||||
value = box.getOutlineColor().a;
|
value = box.getOutlineColor().a;
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1027,6 +1019,9 @@ bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
} else if (name == "size") {
|
} else if (name == "size") {
|
||||||
value = box.getSize();
|
value = box.getSize();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (name == "origin") {
|
||||||
|
value = origin;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1038,7 +1033,8 @@ bool UIFrame::hasProperty(const std::string& name) const {
|
||||||
name == "fill_color.r" || name == "fill_color.g" ||
|
name == "fill_color.r" || name == "fill_color.g" ||
|
||||||
name == "fill_color.b" || name == "fill_color.a" ||
|
name == "fill_color.b" || name == "fill_color.a" ||
|
||||||
name == "outline_color.r" || name == "outline_color.g" ||
|
name == "outline_color.r" || name == "outline_color.g" ||
|
||||||
name == "outline_color.b" || name == "outline_color.a") {
|
name == "outline_color.b" || name == "outline_color.a" ||
|
||||||
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Color properties
|
// Color properties
|
||||||
|
|
@ -1046,7 +1042,11 @@ bool UIFrame::hasProperty(const std::string& name) const {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Vector2f properties
|
// Vector2f properties
|
||||||
if (name == "position" || name == "size") {
|
if (name == "position" || name == "size" || name == "origin") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// #106: Check for shader uniform properties
|
||||||
|
if (hasShaderProperty(name)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,6 @@ public:
|
||||||
bool clip_children = false; // Whether to clip children to frame bounds
|
bool clip_children = false; // Whether to clip children to frame bounds
|
||||||
bool cache_subtree = false; // #144: Whether to cache subtree rendering to texture
|
bool cache_subtree = false; // #144: Whether to cache subtree rendering to texture
|
||||||
|
|
||||||
// Shader POC (#106)
|
|
||||||
std::unique_ptr<sf::Shader> shader;
|
|
||||||
bool shader_enabled = false;
|
|
||||||
void initializeTestShader(); // Load hardcoded test shader
|
|
||||||
void render(sf::Vector2f, sf::RenderTarget&) override final;
|
void render(sf::Vector2f, sf::RenderTarget&) override final;
|
||||||
void move(sf::Vector2f);
|
void move(sf::Vector2f);
|
||||||
PyObjectsEnum derived_type() override final;
|
PyObjectsEnum derived_type() override final;
|
||||||
|
|
@ -60,8 +56,6 @@ public:
|
||||||
static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure);
|
static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_cache_subtree(PyUIFrameObject* self, void* closure);
|
static PyObject* get_cache_subtree(PyUIFrameObject* self, void* closure);
|
||||||
static int set_cache_subtree(PyUIFrameObject* self, PyObject* value, 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 PyGetSetDef getsetters[];
|
||||||
static PyObject* repr(PyUIFrameObject* self);
|
static PyObject* repr(PyUIFrameObject* self);
|
||||||
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);
|
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
|
||||||
205
src/UIGrid.cpp
205
src/UIGrid.cpp
|
|
@ -11,6 +11,8 @@
|
||||||
#include "PyPositionHelper.h" // For standardized position argument parsing
|
#include "PyPositionHelper.h" // For standardized position argument parsing
|
||||||
#include "PyVector.h" // #179, #181 - For Vector return types
|
#include "PyVector.h" // #179, #181 - For Vector return types
|
||||||
#include "PyHeightMap.h" // #199 - HeightMap application methods
|
#include "PyHeightMap.h" // #199 - HeightMap application methods
|
||||||
|
#include "PyShader.h" // #106: Shader support
|
||||||
|
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath> // #142 - for std::floor, std::isnan
|
#include <cmath> // #142 - for std::floor, std::isnan
|
||||||
#include <cstring> // #150 - for strcmp
|
#include <cstring> // #150 - for strcmp
|
||||||
|
|
@ -143,28 +145,59 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
|
|
||||||
// TODO: Apply opacity to output sprite
|
// 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
|
// Get cell dimensions - use texture if available, otherwise defaults
|
||||||
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||||
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||||
|
|
||||||
// sprites that are visible according to zoom, center_x, center_y, and box width
|
// Determine if we need camera rotation handling
|
||||||
|
bool has_camera_rotation = (camera_rotation != 0.0f);
|
||||||
|
float grid_w_px = box.getSize().x;
|
||||||
|
float grid_h_px = box.getSize().y;
|
||||||
|
|
||||||
|
// Calculate AABB for rotated view (if camera rotation is active)
|
||||||
|
float rad = camera_rotation * (M_PI / 180.0f);
|
||||||
|
float cos_r = std::cos(rad);
|
||||||
|
float sin_r = std::sin(rad);
|
||||||
|
float abs_cos = std::abs(cos_r);
|
||||||
|
float abs_sin = std::abs(sin_r);
|
||||||
|
|
||||||
|
// AABB dimensions of the rotated viewport
|
||||||
|
float aabb_w = grid_w_px * abs_cos + grid_h_px * abs_sin;
|
||||||
|
float aabb_h = grid_w_px * abs_sin + grid_h_px * abs_cos;
|
||||||
|
|
||||||
|
// Choose which texture to render to
|
||||||
|
sf::RenderTexture* activeTexture = &renderTexture;
|
||||||
|
|
||||||
|
if (has_camera_rotation) {
|
||||||
|
// Ensure rotation texture is large enough
|
||||||
|
unsigned int needed_size = static_cast<unsigned int>(std::max(aabb_w, aabb_h) + 1);
|
||||||
|
if (rotationTextureSize < needed_size) {
|
||||||
|
rotationTexture.create(needed_size, needed_size);
|
||||||
|
rotationTextureSize = needed_size;
|
||||||
|
}
|
||||||
|
activeTexture = &rotationTexture;
|
||||||
|
activeTexture->clear(fill_color);
|
||||||
|
} else {
|
||||||
|
output.setPosition(box.getPosition() + offset);
|
||||||
|
output.setTextureRect(sf::IntRect(0, 0, grid_w_px, grid_h_px));
|
||||||
|
renderTexture.clear(fill_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate visible tile range
|
||||||
|
// For camera rotation, use AABB dimensions; otherwise use grid dimensions
|
||||||
|
float render_w = has_camera_rotation ? aabb_w : grid_w_px;
|
||||||
|
float render_h = has_camera_rotation ? aabb_h : grid_h_px;
|
||||||
|
|
||||||
float center_x_sq = center_x / cell_width;
|
float center_x_sq = center_x / cell_width;
|
||||||
float center_y_sq = center_y / cell_height;
|
float center_y_sq = center_y / cell_height;
|
||||||
|
|
||||||
float width_sq = box.getSize().x / (cell_width * zoom);
|
float width_sq = render_w / (cell_width * zoom);
|
||||||
float height_sq = box.getSize().y / (cell_height * zoom);
|
float height_sq = render_h / (cell_height * zoom);
|
||||||
float left_edge = center_x_sq - (width_sq / 2.0);
|
float left_edge = center_x_sq - (width_sq / 2.0);
|
||||||
float top_edge = center_y_sq - (height_sq / 2.0);
|
float top_edge = center_y_sq - (height_sq / 2.0);
|
||||||
|
|
||||||
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
|
int left_spritepixels = center_x - (render_w / 2.0 / zoom);
|
||||||
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
|
int top_spritepixels = center_y - (render_h / 2.0 / zoom);
|
||||||
|
|
||||||
int x_limit = left_edge + width_sq + 2;
|
int x_limit = left_edge + width_sq + 2;
|
||||||
if (x_limit > grid_w) x_limit = grid_w;
|
if (x_limit > grid_w) x_limit = grid_w;
|
||||||
|
|
@ -177,7 +210,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
sortLayers();
|
sortLayers();
|
||||||
for (auto& layer : layers) {
|
for (auto& layer : layers) {
|
||||||
if (layer->z_index >= 0) break; // Stop at layers that go above entities
|
if (layer->z_index >= 0) break; // Stop at layers that go above entities
|
||||||
layer->render(renderTexture, left_spritepixels, top_spritepixels,
|
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||||
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,9 +236,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
auto pixel_pos = sf::Vector2f(
|
auto pixel_pos = sf::Vector2f(
|
||||||
(e->position.x*cell_width - left_spritepixels) * zoom,
|
(e->position.x*cell_width - left_spritepixels) * zoom,
|
||||||
(e->position.y*cell_height - top_spritepixels) * zoom );
|
(e->position.y*cell_height - top_spritepixels) * zoom );
|
||||||
//drawent.setPosition(pixel_pos);
|
drawent.render(pixel_pos, *activeTexture);
|
||||||
//renderTexture.draw(drawent);
|
|
||||||
drawent.render(pixel_pos, renderTexture);
|
|
||||||
|
|
||||||
entitiesRendered++;
|
entitiesRendered++;
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +249,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
// #147 - Render dynamic layers with z_index >= 0 (above entities)
|
// #147 - Render dynamic layers with z_index >= 0 (above entities)
|
||||||
for (auto& layer : layers) {
|
for (auto& layer : layers) {
|
||||||
if (layer->z_index < 0) continue; // Skip layers below entities
|
if (layer->z_index < 0) continue; // Skip layers below entities
|
||||||
layer->render(renderTexture, left_spritepixels, top_spritepixels,
|
layer->render(*activeTexture, left_spritepixels, top_spritepixels,
|
||||||
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,7 +281,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
(child->position.y - top_spritepixels) * zoom
|
(child->position.y - top_spritepixels) * zoom
|
||||||
);
|
);
|
||||||
|
|
||||||
child->render(pixel_pos, renderTexture);
|
child->render(pixel_pos, *activeTexture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,11 +323,11 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
if (!state.discovered) {
|
if (!state.discovered) {
|
||||||
// Never seen - black
|
// Never seen - black
|
||||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||||
renderTexture.draw(overlay);
|
activeTexture->draw(overlay);
|
||||||
} else if (!state.visible) {
|
} else if (!state.visible) {
|
||||||
// Discovered but not currently visible - dark gray
|
// Discovered but not currently visible - dark gray
|
||||||
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
overlay.setFillColor(sf::Color(32, 32, 40, 192));
|
||||||
renderTexture.draw(overlay);
|
activeTexture->draw(overlay);
|
||||||
}
|
}
|
||||||
// If visible and discovered, no overlay (fully visible)
|
// If visible and discovered, no overlay (fully visible)
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +353,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
|
|
||||||
overlay.setPosition(pixel_pos);
|
overlay.setPosition(pixel_pos);
|
||||||
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
overlay.setFillColor(sf::Color(0, 0, 0, 255));
|
||||||
renderTexture.draw(overlay);
|
activeTexture->draw(overlay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -349,11 +380,66 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
renderTexture.draw(lineb, 2, sf::Lines);
|
renderTexture.draw(lineb, 2, sf::Lines);
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// render to window
|
// Finalize the active texture
|
||||||
renderTexture.display();
|
activeTexture->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<int>(aabb_w), static_cast<int>(aabb_h)));
|
||||||
|
|
||||||
|
// Draw to the grid's renderTexture (which clips to grid bounds)
|
||||||
|
renderTexture.draw(rotatedSprite);
|
||||||
|
renderTexture.display();
|
||||||
|
|
||||||
|
// Set up output sprite
|
||||||
|
output.setPosition(box.getPosition() + offset);
|
||||||
|
output.setTextureRect(sf::IntRect(0, 0, grid_w_px, grid_h_px));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply viewport rotation (UIDrawable::rotation) to the entire grid widget
|
||||||
|
if (rotation != 0.0f) {
|
||||||
|
output.setOrigin(origin);
|
||||||
|
output.setRotation(rotation);
|
||||||
|
// Adjust position to account for origin offset
|
||||||
|
output.setPosition(box.getPosition() + offset + origin);
|
||||||
|
} else {
|
||||||
|
output.setOrigin(0, 0);
|
||||||
|
output.setRotation(0);
|
||||||
|
// Position already set above
|
||||||
|
}
|
||||||
|
|
||||||
|
// #106: Apply shader if set
|
||||||
|
if (shader && shader->shader) {
|
||||||
|
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)
|
UIGridPoint& UIGrid::at(int x, int y)
|
||||||
|
|
@ -1032,6 +1118,8 @@ PyObject* UIGrid::get_float_member(PyUIGridObject* self, void* closure)
|
||||||
return PyFloat_FromDouble(self->data->center_y);
|
return PyFloat_FromDouble(self->data->center_y);
|
||||||
else if (member_ptr == 6) // zoom
|
else if (member_ptr == 6) // zoom
|
||||||
return PyFloat_FromDouble(self->data->zoom);
|
return PyFloat_FromDouble(self->data->zoom);
|
||||||
|
else if (member_ptr == 7) // camera_rotation
|
||||||
|
return PyFloat_FromDouble(self->data->camera_rotation);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
|
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
|
||||||
|
|
@ -1086,6 +1174,8 @@ int UIGrid::set_float_member(PyUIGridObject* self, PyObject* value, void* closur
|
||||||
self->data->center_y = val;
|
self->data->center_y = val;
|
||||||
else if (member_ptr == 6) // zoom
|
else if (member_ptr == 6) // zoom
|
||||||
self->data->zoom = val;
|
self->data->zoom = val;
|
||||||
|
else if (member_ptr == 7) // camera_rotation
|
||||||
|
self->data->camera_rotation = val;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
// TODO (7DRL Day 2, item 5.) return Texture object
|
// TODO (7DRL Day 2, item 5.) return Texture object
|
||||||
|
|
@ -2192,6 +2282,7 @@ PyGetSetDef UIGrid::getsetters[] = {
|
||||||
{"center_x", (getter)UIGrid::get_float_member, (setter)UIGrid::set_float_member, "center of the view X-coordinate", (void*)4},
|
{"center_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},
|
{"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},
|
{"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,
|
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
|
||||||
MCRF_PROPERTY(on_click,
|
MCRF_PROPERTY(on_click,
|
||||||
|
|
@ -2223,6 +2314,7 @@ PyGetSetDef UIGrid::getsetters[] = {
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
|
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
// #142 - Grid cell mouse events
|
// #142 - Grid cell mouse events
|
||||||
{"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter,
|
{"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},
|
"Callback when mouse enters a grid cell. Called with (cell_pos: Vector).", NULL},
|
||||||
|
|
@ -2232,6 +2324,7 @@ PyGetSetDef UIGrid::getsetters[] = {
|
||||||
"Callback when a grid cell is clicked. Called with (cell_pos: Vector).", NULL},
|
"Callback when a grid cell is clicked. Called with (cell_pos: Vector).", NULL},
|
||||||
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
|
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
|
||||||
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
|
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
|
||||||
|
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UIGRID),
|
||||||
{NULL} /* Sentinel */
|
{NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2492,6 +2585,26 @@ bool UIGrid::setProperty(const std::string& name, float value) {
|
||||||
markDirty(); // #144 - View change affects content
|
markDirty(); // #144 - View change affects content
|
||||||
return true;
|
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") {
|
else if (name == "z_index") {
|
||||||
z_index = static_cast<int>(value);
|
z_index = static_cast<int>(value);
|
||||||
markDirty(); // #144 - Z-order change affects parent
|
markDirty(); // #144 - Z-order change affects parent
|
||||||
|
|
@ -2517,6 +2630,10 @@ bool UIGrid::setProperty(const std::string& name, float value) {
|
||||||
markDirty(); // #144 - Content change
|
markDirty(); // #144 - Content change
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// #106: Shader uniform properties
|
||||||
|
if (setShaderProperty(name, value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2540,6 +2657,11 @@ bool UIGrid::setProperty(const std::string& name, const sf::Vector2f& value) {
|
||||||
markDirty(); // #144 - View change affects content
|
markDirty(); // #144 - View change affects content
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "origin") {
|
||||||
|
origin = value;
|
||||||
|
markCompositeDirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2572,6 +2694,22 @@ bool UIGrid::getProperty(const std::string& name, float& value) const {
|
||||||
value = zoom;
|
value = zoom;
|
||||||
return true;
|
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") {
|
else if (name == "z_index") {
|
||||||
value = static_cast<float>(z_index);
|
value = static_cast<float>(z_index);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -2592,6 +2730,10 @@ bool UIGrid::getProperty(const std::string& name, float& value) const {
|
||||||
value = static_cast<float>(fill_color.a);
|
value = static_cast<float>(fill_color.a);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// #106: Shader uniform properties
|
||||||
|
if (getShaderProperty(name, value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2608,6 +2750,10 @@ bool UIGrid::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
value = sf::Vector2f(center_x, center_y);
|
value = sf::Vector2f(center_x, center_y);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
else if (name == "origin") {
|
||||||
|
value = origin;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2616,13 +2762,18 @@ bool UIGrid::hasProperty(const std::string& name) const {
|
||||||
if (name == "x" || name == "y" ||
|
if (name == "x" || name == "y" ||
|
||||||
name == "w" || name == "h" || name == "width" || name == "height" ||
|
name == "w" || name == "h" || name == "width" || name == "height" ||
|
||||||
name == "center_x" || name == "center_y" || name == "zoom" ||
|
name == "center_x" || name == "center_y" || name == "zoom" ||
|
||||||
name == "z_index" ||
|
name == "camera_rotation" || name == "rotation" ||
|
||||||
|
name == "origin_x" || name == "origin_y" || name == "z_index" ||
|
||||||
name == "fill_color.r" || name == "fill_color.g" ||
|
name == "fill_color.r" || name == "fill_color.g" ||
|
||||||
name == "fill_color.b" || name == "fill_color.a") {
|
name == "fill_color.b" || name == "fill_color.a") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Vector2f properties
|
// Vector2f properties
|
||||||
if (name == "position" || name == "size" || name == "center") {
|
if (name == "position" || name == "size" || name == "center" || name == "origin") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// #106: Shader uniform properties
|
||||||
|
if (hasShaderProperty(name)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,16 @@ public:
|
||||||
//int grid_size; // grid sizes are implied by IndexTexture now
|
//int grid_size; // grid sizes are implied by IndexTexture now
|
||||||
sf::RectangleShape box;
|
sf::RectangleShape box;
|
||||||
float center_x, center_y, zoom;
|
float center_x, center_y, zoom;
|
||||||
|
float camera_rotation = 0.0f; // Rotation of grid contents around camera center (degrees)
|
||||||
//IndexTexture* itex;
|
//IndexTexture* itex;
|
||||||
std::shared_ptr<PyTexture> getTexture();
|
std::shared_ptr<PyTexture> getTexture();
|
||||||
sf::Sprite sprite, output;
|
sf::Sprite sprite, output;
|
||||||
sf::RenderTexture renderTexture;
|
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
|
// #123 - Chunk-based storage for large grid support
|
||||||
std::unique_ptr<ChunkManager> chunk_manager;
|
std::unique_ptr<ChunkManager> chunk_manager;
|
||||||
// Legacy flat storage (kept for small grids or compatibility)
|
// Legacy flat storage (kept for small grids or compatibility)
|
||||||
|
|
@ -181,6 +186,8 @@ public:
|
||||||
// py_clear_dijkstra_maps -> UIGridPathfinding::Grid_clear_dijkstra_maps
|
// 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_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
|
||||||
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
|
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
|
// #199 - HeightMap application methods
|
||||||
static PyObject* py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,10 @@ void UILine::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
||||||
line_shape.setFillColor(render_color);
|
line_shape.setFillColor(render_color);
|
||||||
line_shape.setOutlineThickness(0);
|
line_shape.setOutlineThickness(0);
|
||||||
|
|
||||||
|
// Apply rotation around origin
|
||||||
|
line_shape.setOrigin(origin);
|
||||||
|
line_shape.setRotation(rotation);
|
||||||
|
|
||||||
target.draw(line_shape);
|
target.draw(line_shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,6 +145,22 @@ UIDrawable* UILine::click_at(sf::Vector2f point) {
|
||||||
// #184: Also check for Python subclass (might have on_click method)
|
// #184: Also check for Python subclass (might have on_click method)
|
||||||
if (!click_callable && !is_python_subclass) return nullptr;
|
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
|
// Check if point is close enough to the line
|
||||||
// Using a simple bounding box check plus distance-to-line calculation
|
// Using a simple bounding box check plus distance-to-line calculation
|
||||||
sf::FloatRect bounds = get_bounds();
|
sf::FloatRect bounds = get_bounds();
|
||||||
|
|
@ -149,11 +169,12 @@ UIDrawable* UILine::click_at(sf::Vector2f point) {
|
||||||
bounds.width += thickness * 2;
|
bounds.width += thickness * 2;
|
||||||
bounds.height += thickness * 2;
|
bounds.height += thickness * 2;
|
||||||
|
|
||||||
if (!bounds.contains(point)) return nullptr;
|
// For rotated lines, skip the bounds check (it's an optimization, not required)
|
||||||
|
if (rotation == 0.0f && !bounds.contains(localPoint)) return nullptr;
|
||||||
|
|
||||||
// Calculate distance from point to line segment
|
// Calculate distance from point to line segment
|
||||||
sf::Vector2f line_vec = end_pos - start_pos;
|
sf::Vector2f line_vec = end_pos - start_pos;
|
||||||
sf::Vector2f point_vec = point - start_pos;
|
sf::Vector2f point_vec = localPoint - start_pos;
|
||||||
|
|
||||||
float line_len_sq = line_vec.x * line_vec.x + line_vec.y * line_vec.y;
|
float line_len_sq = line_vec.x * line_vec.x + line_vec.y * line_vec.y;
|
||||||
float t = 0.0f;
|
float t = 0.0f;
|
||||||
|
|
@ -164,7 +185,7 @@ UIDrawable* UILine::click_at(sf::Vector2f point) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sf::Vector2f closest = start_pos + t * line_vec;
|
sf::Vector2f closest = start_pos + t * line_vec;
|
||||||
sf::Vector2f diff = point - closest;
|
sf::Vector2f diff = localPoint - closest;
|
||||||
float distance = std::sqrt(diff.x * diff.x + diff.y * diff.y);
|
float distance = std::sqrt(diff.x * diff.x + diff.y * diff.y);
|
||||||
|
|
||||||
// Click is valid if within thickness + some margin
|
// Click is valid if within thickness + some margin
|
||||||
|
|
@ -248,6 +269,21 @@ bool UILine::setProperty(const std::string& name, float value) {
|
||||||
markDirty(); // #144 - Content change
|
markDirty(); // #144 - Content change
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -306,6 +342,18 @@ bool UILine::getProperty(const std::string& name, float& value) const {
|
||||||
value = end_pos.y;
|
value = end_pos.y;
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,7 +381,8 @@ bool UILine::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "thickness" || name == "x" || name == "y" ||
|
if (name == "thickness" || name == "x" || name == "y" ||
|
||||||
name == "start_x" || name == "start_y" ||
|
name == "start_x" || name == "start_y" ||
|
||||||
name == "end_x" || name == "end_y") {
|
name == "end_x" || name == "end_y" ||
|
||||||
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Color properties
|
// Color properties
|
||||||
|
|
@ -341,7 +390,7 @@ bool UILine::hasProperty(const std::string& name) const {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Vector2f properties
|
// Vector2f properties
|
||||||
if (name == "start" || name == "end") {
|
if (name == "start" || name == "end" || name == "origin") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -469,6 +518,7 @@ PyGetSetDef UILine::getsetters[] = {
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UILINE),
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UILINE),
|
||||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UILINE),
|
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UILINE),
|
||||||
|
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UILINE),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
124
src/UISprite.cpp
124
src/UISprite.cpp
|
|
@ -4,16 +4,43 @@
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "UIFrame.h" // #144: For snapshot= parameter
|
#include "UIFrame.h" // #144: For snapshot= parameter
|
||||||
#include "PyAlignment.h"
|
#include "PyAlignment.h"
|
||||||
|
#include "PyShader.h" // #106: Shader support
|
||||||
|
#include "PyUniformCollection.h" // #106: Uniform collection support
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
|
|
||||||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||||
{
|
{
|
||||||
// #184: Also check for Python subclass (might have on_click method)
|
// #184: Also check for Python subclass (might have on_click method)
|
||||||
if (click_callable || is_python_subclass)
|
if (!click_callable && !is_python_subclass) return nullptr;
|
||||||
{
|
|
||||||
if(sprite.getGlobalBounds().contains(point)) return this;
|
// Get sprite dimensions from local bounds
|
||||||
|
sf::FloatRect localBounds = sprite.getLocalBounds();
|
||||||
|
float w = localBounds.width * sprite.getScale().x;
|
||||||
|
float h = localBounds.height * sprite.getScale().y;
|
||||||
|
|
||||||
|
// Transform click point to local coordinates accounting for rotation
|
||||||
|
sf::Vector2f localPoint;
|
||||||
|
if (rotation != 0.0f) {
|
||||||
|
// Build transform: translate to position, then rotate around origin
|
||||||
|
sf::Transform transform;
|
||||||
|
transform.translate(position);
|
||||||
|
transform.translate(origin);
|
||||||
|
transform.rotate(rotation);
|
||||||
|
transform.translate(-origin);
|
||||||
|
|
||||||
|
// Apply inverse transform to get local coordinates
|
||||||
|
sf::Transform inverse = transform.getInverse();
|
||||||
|
localPoint = inverse.transformPoint(point);
|
||||||
|
} else {
|
||||||
|
// No rotation - simple subtraction
|
||||||
|
localPoint = point - position;
|
||||||
}
|
}
|
||||||
return NULL;
|
|
||||||
|
// Check if local point is within bounds (0,0 to w,h in local space)
|
||||||
|
if (localPoint.x >= 0 && localPoint.y >= 0 && localPoint.x < w && localPoint.y < h) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
UISprite::UISprite()
|
UISprite::UISprite()
|
||||||
|
|
@ -87,9 +114,47 @@ void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
color.a = static_cast<sf::Uint8>(255 * opacity);
|
color.a = static_cast<sf::Uint8>(255 * opacity);
|
||||||
sprite.setColor(color);
|
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);
|
sprite.move(offset);
|
||||||
target.draw(sprite);
|
target.draw(sprite);
|
||||||
sprite.move(-offset);
|
sprite.move(-offset);
|
||||||
|
}
|
||||||
|
|
||||||
// Restore original alpha
|
// Restore original alpha
|
||||||
color.a = 255;
|
color.a = 255;
|
||||||
|
|
@ -359,6 +424,8 @@ PyGetSetDef UISprite::getsetters[] = {
|
||||||
UIDRAWABLE_GETSETTERS,
|
UIDRAWABLE_GETSETTERS,
|
||||||
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||||
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
UIDRAWABLE_ALIGNMENT_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||||
|
UIDRAWABLE_SHADER_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||||
|
UIDRAWABLE_ROTATION_GETSETTERS(PyObjectsEnum::UISPRITE),
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -591,6 +658,28 @@ bool UISprite::setProperty(const std::string& name, float value) {
|
||||||
markDirty(); // #144 - Z-order change affects parent
|
markDirty(); // #144 - Z-order change affects parent
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -633,6 +722,22 @@ bool UISprite::getProperty(const std::string& name, float& value) const {
|
||||||
value = static_cast<float>(z_index);
|
value = static_cast<float>(z_index);
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -652,12 +757,21 @@ bool UISprite::hasProperty(const std::string& name) const {
|
||||||
// Float properties
|
// Float properties
|
||||||
if (name == "x" || name == "y" ||
|
if (name == "x" || name == "y" ||
|
||||||
name == "scale" || name == "scale_x" || name == "scale_y" ||
|
name == "scale" || name == "scale_x" || name == "scale_y" ||
|
||||||
name == "z_index") {
|
name == "z_index" ||
|
||||||
|
name == "rotation" || name == "origin_x" || name == "origin_y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Int properties
|
// Int properties
|
||||||
if (name == "sprite_index" || name == "sprite_number") {
|
if (name == "sprite_index" || name == "sprite_number") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Vector2f properties
|
||||||
|
if (name == "origin") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// #106: Check for shader uniform properties
|
||||||
|
if (hasShaderProperty(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
86
tests/unit/grid_camera_rotation_test.py
Normal file
86
tests/unit/grid_camera_rotation_test.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test UIGrid camera_rotation functionality"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Create test scene
|
||||||
|
test_scene = mcrfpy.Scene("grid_rotation_test")
|
||||||
|
ui = test_scene.children
|
||||||
|
|
||||||
|
# Create background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600), fill_color=mcrfpy.Color(30, 30, 40))
|
||||||
|
ui.append(bg)
|
||||||
|
|
||||||
|
# Create a grid with entities to visualize rotation
|
||||||
|
grid = mcrfpy.Grid(grid_size=(8, 8), pos=(50, 50), size=(300, 300))
|
||||||
|
grid.fill_color = mcrfpy.Color(60, 60, 80)
|
||||||
|
|
||||||
|
# Add some entities to visualize the rotation
|
||||||
|
for i in range(8):
|
||||||
|
entity = mcrfpy.Entity((i, 0)) # Top row
|
||||||
|
grid.entities.append(entity)
|
||||||
|
|
||||||
|
for i in range(1, 8):
|
||||||
|
entity = mcrfpy.Entity((0, i)) # Left column
|
||||||
|
grid.entities.append(entity)
|
||||||
|
|
||||||
|
# Apply camera rotation
|
||||||
|
grid.camera_rotation = 30.0 # 30 degree rotation
|
||||||
|
grid.center_camera((4, 4)) # Center on middle of grid
|
||||||
|
|
||||||
|
ui.append(grid)
|
||||||
|
|
||||||
|
# Create a second grid without rotation for comparison
|
||||||
|
grid2 = mcrfpy.Grid(grid_size=(8, 8), pos=(400, 50), size=(300, 300))
|
||||||
|
grid2.fill_color = mcrfpy.Color(60, 60, 80)
|
||||||
|
|
||||||
|
# Add same entities pattern
|
||||||
|
for i in range(8):
|
||||||
|
entity = mcrfpy.Entity((i, 0))
|
||||||
|
grid2.entities.append(entity)
|
||||||
|
|
||||||
|
for i in range(1, 8):
|
||||||
|
entity = mcrfpy.Entity((0, i))
|
||||||
|
grid2.entities.append(entity)
|
||||||
|
|
||||||
|
grid2.camera_rotation = 0.0 # No rotation
|
||||||
|
grid2.center_camera((4, 4))
|
||||||
|
|
||||||
|
ui.append(grid2)
|
||||||
|
|
||||||
|
# Labels
|
||||||
|
label1 = mcrfpy.Caption(text="Grid with camera_rotation=30", pos=(50, 20))
|
||||||
|
ui.append(label1)
|
||||||
|
|
||||||
|
label2 = mcrfpy.Caption(text="Grid with camera_rotation=0", pos=(400, 20))
|
||||||
|
ui.append(label2)
|
||||||
|
|
||||||
|
# Create a third grid with viewport rotation (different from camera rotation)
|
||||||
|
grid3 = mcrfpy.Grid(grid_size=(6, 6), pos=(175, 400), size=(200, 150))
|
||||||
|
grid3.fill_color = mcrfpy.Color(80, 60, 60)
|
||||||
|
|
||||||
|
# Add entities
|
||||||
|
for i in range(6):
|
||||||
|
entity = mcrfpy.Entity((i, 0))
|
||||||
|
grid3.entities.append(entity)
|
||||||
|
|
||||||
|
# Apply viewport rotation (entire grid rotates)
|
||||||
|
grid3.rotation = 15.0
|
||||||
|
grid3.origin = (100, 75) # Center origin for rotation
|
||||||
|
grid3.center_camera((3, 3))
|
||||||
|
|
||||||
|
ui.append(grid3)
|
||||||
|
|
||||||
|
label3 = mcrfpy.Caption(text="Grid with viewport rotation=15 (rotates entire widget)", pos=(100, 560))
|
||||||
|
ui.append(label3)
|
||||||
|
|
||||||
|
# Activate scene
|
||||||
|
mcrfpy.current_scene = test_scene
|
||||||
|
|
||||||
|
# Advance the game loop to render, then take screenshot
|
||||||
|
mcrfpy.step(0.1)
|
||||||
|
automation.screenshot("grid_camera_rotation_test.png")
|
||||||
|
print("Screenshot saved as grid_camera_rotation_test.png")
|
||||||
|
print("PASS")
|
||||||
|
sys.exit(0)
|
||||||
163
tests/unit/rotation_test.py
Normal file
163
tests/unit/rotation_test.py
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test rotation support for UIDrawable subclasses"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_rotation_properties():
|
||||||
|
"""Test rotation, origin, rotate_with_camera properties on all UIDrawable types"""
|
||||||
|
print("Testing rotation properties on all UIDrawable types...")
|
||||||
|
|
||||||
|
# Test UIFrame
|
||||||
|
frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50))
|
||||||
|
assert frame.rotation == 0.0, f"Frame default rotation should be 0, got {frame.rotation}"
|
||||||
|
frame.rotation = 45.0
|
||||||
|
assert frame.rotation == 45.0, f"Frame rotation should be 45, got {frame.rotation}"
|
||||||
|
|
||||||
|
# Test origin as Vector
|
||||||
|
frame.origin = (25, 25)
|
||||||
|
assert frame.origin.x == 25.0, f"Frame origin.x should be 25, got {frame.origin.x}"
|
||||||
|
assert frame.origin.y == 25.0, f"Frame origin.y should be 25, got {frame.origin.y}"
|
||||||
|
|
||||||
|
# Test rotate_with_camera
|
||||||
|
assert frame.rotate_with_camera == False, "Default rotate_with_camera should be False"
|
||||||
|
frame.rotate_with_camera = True
|
||||||
|
assert frame.rotate_with_camera == True, "rotate_with_camera should be True after setting"
|
||||||
|
print(" Frame: PASS")
|
||||||
|
|
||||||
|
# Test UISprite
|
||||||
|
sprite = mcrfpy.Sprite(pos=(100, 100))
|
||||||
|
assert sprite.rotation == 0.0, f"Sprite default rotation should be 0, got {sprite.rotation}"
|
||||||
|
sprite.rotation = 90.0
|
||||||
|
assert sprite.rotation == 90.0, f"Sprite rotation should be 90, got {sprite.rotation}"
|
||||||
|
sprite.origin = (8, 8)
|
||||||
|
assert sprite.origin.x == 8.0, f"Sprite origin.x should be 8, got {sprite.origin.x}"
|
||||||
|
print(" Sprite: PASS")
|
||||||
|
|
||||||
|
# Test UICaption
|
||||||
|
caption = mcrfpy.Caption(text="Test", pos=(100, 100))
|
||||||
|
assert caption.rotation == 0.0, f"Caption default rotation should be 0, got {caption.rotation}"
|
||||||
|
caption.rotation = -30.0
|
||||||
|
assert caption.rotation == -30.0, f"Caption rotation should be -30, got {caption.rotation}"
|
||||||
|
caption.origin = (0, 0)
|
||||||
|
assert caption.origin.x == 0.0, f"Caption origin.x should be 0, got {caption.origin.x}"
|
||||||
|
print(" Caption: PASS")
|
||||||
|
|
||||||
|
# Test UICircle
|
||||||
|
circle = mcrfpy.Circle(center=(100, 100), radius=25)
|
||||||
|
assert circle.rotation == 0.0, f"Circle default rotation should be 0, got {circle.rotation}"
|
||||||
|
circle.rotation = 180.0
|
||||||
|
assert circle.rotation == 180.0, f"Circle rotation should be 180, got {circle.rotation}"
|
||||||
|
print(" Circle: PASS")
|
||||||
|
|
||||||
|
# Test UILine
|
||||||
|
line = mcrfpy.Line(start=(0, 0), end=(100, 100))
|
||||||
|
assert line.rotation == 0.0, f"Line default rotation should be 0, got {line.rotation}"
|
||||||
|
line.rotation = 45.0
|
||||||
|
assert line.rotation == 45.0, f"Line rotation should be 45, got {line.rotation}"
|
||||||
|
print(" Line: PASS")
|
||||||
|
|
||||||
|
# Test UIArc
|
||||||
|
arc = mcrfpy.Arc(center=(100, 100), radius=50, start_angle=0, end_angle=90)
|
||||||
|
assert arc.rotation == 0.0, f"Arc default rotation should be 0, got {arc.rotation}"
|
||||||
|
arc.rotation = 270.0
|
||||||
|
assert arc.rotation == 270.0, f"Arc rotation should be 270, got {arc.rotation}"
|
||||||
|
print(" Arc: PASS")
|
||||||
|
|
||||||
|
print("All rotation property tests passed!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_rotation_animation():
|
||||||
|
"""Test that rotation can be animated"""
|
||||||
|
print("\nTesting rotation animation...")
|
||||||
|
|
||||||
|
frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50))
|
||||||
|
frame.rotation = 0.0
|
||||||
|
|
||||||
|
# Test that animate method exists and accepts rotation
|
||||||
|
try:
|
||||||
|
frame.animate("rotation", 360.0, 1.0, mcrfpy.Easing.LINEAR)
|
||||||
|
print(" Animation started successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Animation failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Test origin animation
|
||||||
|
try:
|
||||||
|
frame.animate("origin_x", 25.0, 0.5, mcrfpy.Easing.LINEAR)
|
||||||
|
frame.animate("origin_y", 25.0, 0.5, mcrfpy.Easing.LINEAR)
|
||||||
|
print(" Origin animation started successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Origin animation failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("Rotation animation tests passed!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_grid_camera_rotation():
|
||||||
|
"""Test UIGrid camera_rotation property"""
|
||||||
|
print("\nTesting Grid camera_rotation...")
|
||||||
|
|
||||||
|
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(50, 50), size=(200, 200))
|
||||||
|
|
||||||
|
# Test default camera_rotation
|
||||||
|
assert grid.camera_rotation == 0.0, f"Grid default camera_rotation should be 0, got {grid.camera_rotation}"
|
||||||
|
|
||||||
|
# Test setting camera_rotation
|
||||||
|
grid.camera_rotation = 45.0
|
||||||
|
assert grid.camera_rotation == 45.0, f"Grid camera_rotation should be 45, got {grid.camera_rotation}"
|
||||||
|
|
||||||
|
# Test negative rotation
|
||||||
|
grid.camera_rotation = -90.0
|
||||||
|
assert grid.camera_rotation == -90.0, f"Grid camera_rotation should be -90, got {grid.camera_rotation}"
|
||||||
|
|
||||||
|
# Test full rotation
|
||||||
|
grid.camera_rotation = 360.0
|
||||||
|
assert grid.camera_rotation == 360.0, f"Grid camera_rotation should be 360, got {grid.camera_rotation}"
|
||||||
|
|
||||||
|
# Grid also has regular rotation (viewport rotation)
|
||||||
|
assert grid.rotation == 0.0, f"Grid viewport rotation should default to 0, got {grid.rotation}"
|
||||||
|
grid.rotation = 15.0
|
||||||
|
assert grid.rotation == 15.0, f"Grid viewport rotation should be 15, got {grid.rotation}"
|
||||||
|
|
||||||
|
# Test camera_rotation animation
|
||||||
|
try:
|
||||||
|
grid.animate("camera_rotation", 90.0, 1.0, mcrfpy.Easing.EASE_IN_OUT)
|
||||||
|
print(" Camera rotation animation started successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Camera rotation animation failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("Grid camera_rotation tests passed!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""Run all rotation tests"""
|
||||||
|
print("=" * 50)
|
||||||
|
print("UIDrawable Rotation Tests")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
results.append(("Rotation Properties", test_rotation_properties()))
|
||||||
|
results.append(("Rotation Animation", test_rotation_animation()))
|
||||||
|
results.append(("Grid Camera Rotation", test_grid_camera_rotation()))
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("Test Results Summary")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
all_passed = True
|
||||||
|
for name, passed in results:
|
||||||
|
status = "PASS" if passed else "FAIL"
|
||||||
|
print(f" {name}: {status}")
|
||||||
|
if not passed:
|
||||||
|
all_passed = False
|
||||||
|
|
||||||
|
if all_passed:
|
||||||
|
print("\nAll tests PASSED!")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("\nSome tests FAILED!")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(run_all_tests())
|
||||||
114
tests/unit/rotation_visual_test.py
Normal file
114
tests/unit/rotation_visual_test.py
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Visual test for rotation support - uses direct screenshot"""
|
||||||
|
import mcrfpy
|
||||||
|
from mcrfpy import automation
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Create test scene
|
||||||
|
test_scene = mcrfpy.Scene("rotation_test")
|
||||||
|
ui = test_scene.children
|
||||||
|
|
||||||
|
# Create background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600), fill_color=mcrfpy.Color(40, 40, 50))
|
||||||
|
ui.append(bg)
|
||||||
|
|
||||||
|
# Row 1: Frames with different rotations
|
||||||
|
# Frame at 0 degrees
|
||||||
|
frame1 = mcrfpy.Frame(pos=(100, 100), size=(60, 60), fill_color=mcrfpy.Color(200, 50, 50))
|
||||||
|
frame1.rotation = 0.0
|
||||||
|
frame1.origin = (30, 30) # Center origin
|
||||||
|
ui.append(frame1)
|
||||||
|
|
||||||
|
# Frame at 45 degrees
|
||||||
|
frame2 = mcrfpy.Frame(pos=(250, 100), size=(60, 60), fill_color=mcrfpy.Color(50, 200, 50))
|
||||||
|
frame2.rotation = 45.0
|
||||||
|
frame2.origin = (30, 30)
|
||||||
|
ui.append(frame2)
|
||||||
|
|
||||||
|
# Frame at 90 degrees
|
||||||
|
frame3 = mcrfpy.Frame(pos=(400, 100), size=(60, 60), fill_color=mcrfpy.Color(50, 50, 200))
|
||||||
|
frame3.rotation = 90.0
|
||||||
|
frame3.origin = (30, 30)
|
||||||
|
ui.append(frame3)
|
||||||
|
|
||||||
|
# Label for row 1
|
||||||
|
label1 = mcrfpy.Caption(text="Frames: 0, 45, 90 degrees", pos=(100, 50))
|
||||||
|
ui.append(label1)
|
||||||
|
|
||||||
|
# Row 2: Captions with rotation
|
||||||
|
caption1 = mcrfpy.Caption(text="Rotated Text", pos=(100, 250))
|
||||||
|
caption1.rotation = 0.0
|
||||||
|
ui.append(caption1)
|
||||||
|
|
||||||
|
caption2 = mcrfpy.Caption(text="Rotated Text", pos=(300, 250))
|
||||||
|
caption2.rotation = -15.0
|
||||||
|
ui.append(caption2)
|
||||||
|
|
||||||
|
caption3 = mcrfpy.Caption(text="Rotated Text", pos=(500, 250))
|
||||||
|
caption3.rotation = 30.0
|
||||||
|
ui.append(caption3)
|
||||||
|
|
||||||
|
# Label for row 2
|
||||||
|
label2 = mcrfpy.Caption(text="Captions: 0, -15, 30 degrees", pos=(100, 200))
|
||||||
|
ui.append(label2)
|
||||||
|
|
||||||
|
# Row 3: Circles (rotation with offset origin causes orbiting)
|
||||||
|
circle1 = mcrfpy.Circle(center=(100, 400), radius=25, fill_color=mcrfpy.Color(200, 200, 50))
|
||||||
|
circle1.rotation = 0.0
|
||||||
|
ui.append(circle1)
|
||||||
|
|
||||||
|
circle2 = mcrfpy.Circle(center=(250, 400), radius=25, fill_color=mcrfpy.Color(200, 50, 200))
|
||||||
|
circle2.rotation = 45.0
|
||||||
|
circle2.origin = (20, 0) # Offset origin to show orbiting effect
|
||||||
|
ui.append(circle2)
|
||||||
|
|
||||||
|
circle3 = mcrfpy.Circle(center=(400, 400), radius=25, fill_color=mcrfpy.Color(50, 200, 200))
|
||||||
|
circle3.rotation = 90.0
|
||||||
|
circle3.origin = (20, 0) # Same offset
|
||||||
|
ui.append(circle3)
|
||||||
|
|
||||||
|
# Label for row 3
|
||||||
|
label3 = mcrfpy.Caption(text="Circles with offset origin: 0, 45, 90 degrees", pos=(100, 350))
|
||||||
|
ui.append(label3)
|
||||||
|
|
||||||
|
# Row 4: Lines with rotation
|
||||||
|
line1 = mcrfpy.Line(start=(100, 500), end=(150, 500), thickness=3, color=mcrfpy.Color(255, 255, 255))
|
||||||
|
line1.rotation = 0.0
|
||||||
|
ui.append(line1)
|
||||||
|
|
||||||
|
line2 = mcrfpy.Line(start=(250, 500), end=(300, 500), thickness=3, color=mcrfpy.Color(255, 200, 200))
|
||||||
|
line2.rotation = 45.0
|
||||||
|
line2.origin = (125, 500) # Rotate around line center
|
||||||
|
ui.append(line2)
|
||||||
|
|
||||||
|
line3 = mcrfpy.Line(start=(400, 500), end=(450, 500), thickness=3, color=mcrfpy.Color(200, 255, 200))
|
||||||
|
line3.rotation = -45.0
|
||||||
|
line3.origin = (200, 500)
|
||||||
|
ui.append(line3)
|
||||||
|
|
||||||
|
# Label for row 4
|
||||||
|
label4 = mcrfpy.Caption(text="Lines: 0, 45, -45 degrees", pos=(100, 470))
|
||||||
|
ui.append(label4)
|
||||||
|
|
||||||
|
# Arcs with rotation
|
||||||
|
arc1 = mcrfpy.Arc(center=(600, 100), radius=40, start_angle=0, end_angle=90, thickness=5)
|
||||||
|
arc1.rotation = 0.0
|
||||||
|
ui.append(arc1)
|
||||||
|
|
||||||
|
arc2 = mcrfpy.Arc(center=(700, 100), radius=40, start_angle=0, end_angle=90, thickness=5)
|
||||||
|
arc2.rotation = 45.0
|
||||||
|
ui.append(arc2)
|
||||||
|
|
||||||
|
# Label for arcs
|
||||||
|
label5 = mcrfpy.Caption(text="Arcs: 0, 45 degrees", pos=(550, 50))
|
||||||
|
ui.append(label5)
|
||||||
|
|
||||||
|
# Activate scene
|
||||||
|
mcrfpy.current_scene = test_scene
|
||||||
|
|
||||||
|
# Advance the game loop to render, then take screenshot
|
||||||
|
mcrfpy.step(0.1)
|
||||||
|
automation.screenshot("rotation_visual_test.png")
|
||||||
|
print("Screenshot saved as rotation_visual_test.png")
|
||||||
|
print("PASS")
|
||||||
|
sys.exit(0)
|
||||||
422
tests/unit/shader_test.py
Normal file
422
tests/unit/shader_test.py
Normal file
|
|
@ -0,0 +1,422 @@
|
||||||
|
#!/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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue