Shader POC: Add shader_enabled property to UIFrame (#106)
Proof of concept for shader support on UIFrame: - Add shader and shader_enabled members to UIFrame - Add initializeTestShader() with hardcoded wave/glow fragment shader - Add shader_enabled Python property for toggling - Apply shader when drawing RenderTexture sprite - Auto-update time uniform for animated effects Also fixes position corruption when toggling RenderTexture usage: - Standard rendering path now uses `position` as source of truth - Prevents box position from staying at (0,0) after texture render Test files: - tests/shader_poc_test.py: Visual test of 6 render variants - tests/shader_toggle_test.py: Regression test for position bug Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
475fe94148
commit
41d551e6e1
4 changed files with 331 additions and 6 deletions
108
src/UIFrame.cpp
108
src/UIFrame.cpp
|
|
@ -8,6 +8,7 @@
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "PyAlignment.h"
|
#include "PyAlignment.h"
|
||||||
|
#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)
|
||||||
|
|
@ -109,10 +110,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
|
// #144: Use RenderTexture for clipping OR texture caching OR shaders
|
||||||
// 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)
|
||||||
bool use_texture = (clip_children && !children->empty()) || cache_subtree;
|
// shader_enabled: requires texture for shader post-processing
|
||||||
|
bool use_texture = (clip_children && !children->empty()) || cache_subtree || shader_enabled;
|
||||||
|
|
||||||
if (use_texture) {
|
if (use_texture) {
|
||||||
// Enable RenderTexture if not already enabled
|
// Enable RenderTexture if not already enabled
|
||||||
|
|
@ -167,13 +169,24 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
if (use_render_texture) {
|
if (use_render_texture) {
|
||||||
// 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);
|
||||||
target.draw(render_sprite);
|
|
||||||
|
// #106 POC: Apply shader if enabled
|
||||||
|
if (shader_enabled && shader) {
|
||||||
|
// Update time uniform for animated effects
|
||||||
|
static sf::Clock shader_clock;
|
||||||
|
shader->setUniform("time", shader_clock.getElapsedTime().asSeconds());
|
||||||
|
shader->setUniform("texture", sf::Shader::CurrentTexture);
|
||||||
|
target.draw(render_sprite, shader.get());
|
||||||
|
} else {
|
||||||
|
target.draw(render_sprite);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Standard rendering without caching
|
// Standard rendering without caching
|
||||||
box.move(offset);
|
// Restore box position from `position` - may have been set to (0,0) by previous texture render
|
||||||
|
box.setPosition(offset + position);
|
||||||
target.draw(box);
|
target.draw(box);
|
||||||
box.move(-offset);
|
box.setPosition(position); // Restore to canonical position
|
||||||
|
|
||||||
// Sort children by z_index if needed
|
// Sort children by z_index if needed
|
||||||
if (children_need_sort && !children->empty()) {
|
if (children_need_sort && !children->empty()) {
|
||||||
|
|
@ -185,7 +198,7 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto drawable : *children) {
|
for (auto drawable : *children) {
|
||||||
drawable->render(offset + box.getPosition(), target);
|
drawable->render(offset + position, target); // Use `position` as source of truth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -448,6 +461,88 @@ int UIFrame::set_cache_subtree(PyUIFrameObject* self, PyObject* value, void* clo
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #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;
|
||||||
|
|
||||||
|
|
@ -486,6 +581,7 @@ 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),
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ public:
|
||||||
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
|
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
|
||||||
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;
|
||||||
|
|
@ -55,6 +60,8 @@ 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);
|
||||||
|
|
|
||||||
132
tests/shader_poc_test.py
Normal file
132
tests/shader_poc_test.py
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Shader POC Test - Issue #106
|
||||||
|
|
||||||
|
Tests 6 render variants:
|
||||||
|
1. Basic frame (no special flags)
|
||||||
|
2. clip_children=True
|
||||||
|
3. cache_subtree=True
|
||||||
|
4. Basic + shader_enabled
|
||||||
|
5. clip_children + shader_enabled
|
||||||
|
6. cache_subtree + shader_enabled
|
||||||
|
|
||||||
|
The shader applies a wave distortion + glow effect.
|
||||||
|
Shader-enabled frames should show visible animation.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Create test scene
|
||||||
|
scene = mcrfpy.Scene("shader_test")
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
ui = scene.children
|
||||||
|
|
||||||
|
# Create a background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=(30, 30, 40, 255))
|
||||||
|
ui.append(bg)
|
||||||
|
|
||||||
|
# Helper to create a test frame with children
|
||||||
|
def create_test_frame(x, y, label, clip=False, cache=False, shader=False):
|
||||||
|
"""Create a frame with some child content for testing."""
|
||||||
|
frame = mcrfpy.Frame(
|
||||||
|
pos=(x, y),
|
||||||
|
size=(200, 200),
|
||||||
|
fill_color=(60, 80, 120, 255),
|
||||||
|
outline_color=(200, 200, 255, 255),
|
||||||
|
outline=2.0,
|
||||||
|
clip_children=clip,
|
||||||
|
cache_subtree=cache
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add some child content
|
||||||
|
title = mcrfpy.Caption(text=label, pos=(5, 5), font_size=12)
|
||||||
|
title.fill_color = (255, 255, 200, 255)
|
||||||
|
frame.children.append(title)
|
||||||
|
|
||||||
|
# Add a sprite or shape inside
|
||||||
|
inner = mcrfpy.Frame(
|
||||||
|
pos=(20, 35),
|
||||||
|
size=(110, 60),
|
||||||
|
fill_color=(100, 150, 200, 255),
|
||||||
|
outline_color=(255, 255, 255, 200),
|
||||||
|
outline=1.0
|
||||||
|
)
|
||||||
|
frame.children.append(inner)
|
||||||
|
|
||||||
|
# Add text inside inner frame
|
||||||
|
inner_text = mcrfpy.Caption(text="Content", pos=(25, 20), font_size=14)
|
||||||
|
inner_text.fill_color = (255, 255, 255, 255)
|
||||||
|
inner.children.append(inner_text)
|
||||||
|
|
||||||
|
# Enable shader if requested
|
||||||
|
if shader:
|
||||||
|
frame.shader_enabled = True
|
||||||
|
|
||||||
|
return frame
|
||||||
|
|
||||||
|
# Row 1: Without shader
|
||||||
|
y1 = 50
|
||||||
|
title1 = mcrfpy.Caption(text="Without Shader:", pos=(20, y1 - 30), font_size=16)
|
||||||
|
title1.fill_color = (255, 255, 100, 255)
|
||||||
|
ui.append(title1)
|
||||||
|
|
||||||
|
# 1. Basic (no flags)
|
||||||
|
basic = create_test_frame(50, y1, "Basic", clip=False, cache=False, shader=False)
|
||||||
|
ui.append(basic)
|
||||||
|
|
||||||
|
# 2. clip_children
|
||||||
|
clipped = create_test_frame(300, y1, "clip_children", clip=True, cache=False, shader=False)
|
||||||
|
ui.append(clipped)
|
||||||
|
|
||||||
|
# 3. cache_subtree
|
||||||
|
cached = create_test_frame(550, y1, "cache_subtree", clip=False, cache=True, shader=False)
|
||||||
|
ui.append(cached)
|
||||||
|
|
||||||
|
# Row 2: With shader
|
||||||
|
y2 = 300
|
||||||
|
title2 = mcrfpy.Caption(text="With Shader (should animate):", pos=(20, y2 - 30), font_size=16)
|
||||||
|
title2.fill_color = (255, 255, 100, 255)
|
||||||
|
ui.append(title2)
|
||||||
|
|
||||||
|
# 4. Basic + shader
|
||||||
|
basic_shader = create_test_frame(50, y2, "Basic+Shader", clip=False, cache=False, shader=True)
|
||||||
|
ui.append(basic_shader)
|
||||||
|
|
||||||
|
# 5. clip_children + shader
|
||||||
|
clipped_shader = create_test_frame(300, y2, "clip+Shader", clip=True, cache=False, shader=True)
|
||||||
|
ui.append(clipped_shader)
|
||||||
|
|
||||||
|
# 6. cache_subtree + shader
|
||||||
|
cached_shader = create_test_frame(550, y2, "cache+Shader", clip=False, cache=True, shader=True)
|
||||||
|
ui.append(cached_shader)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
#instructions = mcrfpy.Caption(
|
||||||
|
# text="Press Q or Escape to quit. Bottom row should show animated wave/glow effects.",
|
||||||
|
# pos=(20, 400),
|
||||||
|
# font_size=14
|
||||||
|
#)
|
||||||
|
#instructions.fill_color = (180, 180, 180, 255)
|
||||||
|
#ui.append(instructions)
|
||||||
|
|
||||||
|
# Debug info
|
||||||
|
#debug_info = mcrfpy.Caption(
|
||||||
|
# text=f"Frames created: 6 variants (3 without shader, 3 with shader)",
|
||||||
|
# pos=(20, 430),
|
||||||
|
# font_size=12
|
||||||
|
#)
|
||||||
|
#debug_info.fill_color = (120, 120, 120, 255)
|
||||||
|
#ui.append(debug_info)
|
||||||
|
|
||||||
|
# Keyboard handler
|
||||||
|
def on_key(key, state):
|
||||||
|
if state == "start" and key in ("Q", "Escape"):
|
||||||
|
print("PASS: Shader POC test complete - exiting")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
scene.on_key = on_key
|
||||||
|
|
||||||
|
print("Shader POC Test running...")
|
||||||
|
print("- Top row: No shader (static)")
|
||||||
|
print("- Bottom row: Shader enabled (should animate with wave/glow)")
|
||||||
|
print("Press Q or Escape to quit")
|
||||||
90
tests/shader_toggle_test.py
Normal file
90
tests/shader_toggle_test.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Shader Toggle Test - Regression test for position corruption bug
|
||||||
|
|
||||||
|
Tests that toggling shader_enabled on and off does not corrupt frame position.
|
||||||
|
This was a bug similar to #223 where box.setPosition(0,0) during texture
|
||||||
|
rendering was never restored when switching back to standard rendering.
|
||||||
|
"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
scene = mcrfpy.Scene("toggle_test")
|
||||||
|
mcrfpy.current_scene = scene
|
||||||
|
ui = scene.children
|
||||||
|
|
||||||
|
# Background
|
||||||
|
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600), fill_color=(20, 20, 30, 255))
|
||||||
|
ui.append(bg)
|
||||||
|
|
||||||
|
# Create test frame at a specific position
|
||||||
|
test_frame = mcrfpy.Frame(
|
||||||
|
pos=(200, 200),
|
||||||
|
size=(150, 100),
|
||||||
|
fill_color=(80, 120, 180, 255),
|
||||||
|
outline_color=(255, 255, 255, 255),
|
||||||
|
outline=3.0
|
||||||
|
)
|
||||||
|
label = mcrfpy.Caption(text="Test Frame", pos=(10, 10), font_size=14)
|
||||||
|
label.fill_color = (255, 255, 255, 255)
|
||||||
|
test_frame.children.append(label)
|
||||||
|
ui.append(test_frame)
|
||||||
|
|
||||||
|
# Status display
|
||||||
|
status = mcrfpy.Caption(text="Status: Initial", pos=(20, 20), font_size=16)
|
||||||
|
status.fill_color = (255, 255, 100, 255)
|
||||||
|
ui.append(status)
|
||||||
|
|
||||||
|
pos_display = mcrfpy.Caption(text="", pos=(20, 50), font_size=14)
|
||||||
|
pos_display.fill_color = (200, 200, 200, 255)
|
||||||
|
ui.append(pos_display)
|
||||||
|
|
||||||
|
instructions = mcrfpy.Caption(
|
||||||
|
text="Press 1: Enable shader | 2: Disable shader | Q: Quit",
|
||||||
|
pos=(20, 550), font_size=14
|
||||||
|
)
|
||||||
|
instructions.fill_color = (150, 150, 150, 255)
|
||||||
|
ui.append(instructions)
|
||||||
|
|
||||||
|
def update_display():
|
||||||
|
pos_display.text = f"Position: ({test_frame.x}, {test_frame.y}) | Shader: {test_frame.shader_enabled}"
|
||||||
|
|
||||||
|
update_display()
|
||||||
|
|
||||||
|
test_count = 0
|
||||||
|
|
||||||
|
def on_key(key, state):
|
||||||
|
global test_count
|
||||||
|
if state != "start":
|
||||||
|
return
|
||||||
|
|
||||||
|
if key == "Num1" or key == "1":
|
||||||
|
test_frame.shader_enabled = True
|
||||||
|
status.text = "Status: Shader ENABLED"
|
||||||
|
update_display()
|
||||||
|
test_count += 1
|
||||||
|
elif key == "Num2" or key == "2":
|
||||||
|
test_frame.shader_enabled = False
|
||||||
|
status.text = "Status: Shader DISABLED"
|
||||||
|
update_display()
|
||||||
|
test_count += 1
|
||||||
|
|
||||||
|
# Check if position is still correct
|
||||||
|
if test_frame.x != 200 or test_frame.y != 200:
|
||||||
|
status.text = f"BUG! Position corrupted to ({test_frame.x}, {test_frame.y})"
|
||||||
|
status.fill_color = (255, 100, 100, 255)
|
||||||
|
else:
|
||||||
|
status.text = "Status: Shader DISABLED - Position OK!"
|
||||||
|
status.fill_color = (100, 255, 100, 255)
|
||||||
|
elif key in ("Q", "Escape"):
|
||||||
|
if test_frame.x == 200 and test_frame.y == 200:
|
||||||
|
print(f"PASS: Position remained correct after {test_count} toggles")
|
||||||
|
else:
|
||||||
|
print(f"FAIL: Position corrupted to ({test_frame.x}, {test_frame.y})")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
scene.on_key = on_key
|
||||||
|
|
||||||
|
print("Shader Toggle Test")
|
||||||
|
print("Press 1 to enable shader, 2 to disable, Q to quit")
|
||||||
|
print("Frame should stay at (200, 200) regardless of shader state")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue