diff --git a/ROADMAP.md b/ROADMAP.md index 453c125..040686a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -65,6 +65,7 @@ - Grid background colors (#50) ✅ - RenderTexture base infrastructure ✅ - UIFrame clipping support ✅ + - Viewport-based rendering (#8) ✅ ### Active Development: - **Branch**: alpha_streamline_2 @@ -244,10 +245,13 @@ Rendering Layer: ⏳ Extend to other UI classes ⏳ Effects (blur, glow, etc.) -3. #8 - Viewport-based rendering [NEXT PRIORITY] - - RenderTexture matches viewport - - Proper scaling/letterboxing - - Coordinate system transformations +3. ✅ #8 - Viewport-based rendering [COMPLETED] + - Fixed game resolution (window.game_resolution) + - Three scaling modes: "center", "stretch", "fit" + - Window to game coordinate transformation + - Mouse input properly scaled with windowToGameCoords() + - Python API fully integrated + - Tests: test_viewport_simple.py, test_viewport_visual.py, test_viewport_scaling.py 4. #106 - Shader support [STRETCH GOAL] sprite.shader = mcrfpy.Shader.load("glow.frag") @@ -267,7 +271,8 @@ Rendering Layer: - Dirty flag system crucial for performance - only re-render when properties change - Nested clipping works correctly with proper coordinate transformations - Scene transitions already use RenderTextures - good integration test -- Next: Viewport rendering (#8) will build on RenderTexture foundation +- Viewport rendering (#8) ✅ complete with three scaling modes and coordinate transformation +- Next: Extend RenderTexture support to remaining UI classes (Caption, Sprite, Grid) - Shader/Particle systems might be deferred to Phase 7 or Gamma *Rationale*: This unlocks professional visual effects but is complex. diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 836fe02..0199b37 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -32,6 +32,11 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) } visible = render_target->getDefaultView(); + + // Initialize the game view + gameView.setSize(static_cast(gameResolution.x), static_cast(gameResolution.y)); + gameView.setCenter(gameResolution.x / 2.0f, gameResolution.y / 2.0f); + updateViewport(); scene = "uitest"; scenes["uitest"] = new UITestScene(this); @@ -168,9 +173,9 @@ void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); } void GameEngine::setWindowScale(float multiplier) { if (!headless && window) { - window->setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling + window->setSize(sf::Vector2u(gameResolution.x * multiplier, gameResolution.y * multiplier)); + updateViewport(); } - //window.create(sf::VideoMode(1024 * multiplier, 768 * multiplier), window_title, sf::Style::Titlebar | sf::Style::Close); } void GameEngine::run() @@ -320,10 +325,8 @@ void GameEngine::processEvent(const sf::Event& event) if (event.type == sf::Event::Closed) { running = false; return; } // Handle window resize events else if (event.type == sf::Event::Resized) { - // Update the view to match the new window size - sf::FloatRect visibleArea(0, 0, event.size.width, event.size.height); - visible = sf::View(visibleArea); - render_target->setView(visible); + // Update the viewport to handle the new window size + updateViewport(); // Notify Python scenes about the resize McRFPy_API::triggerResize(event.size.width, event.size.height); @@ -410,3 +413,89 @@ void GameEngine::setFramerateLimit(unsigned int limit) window->setFramerateLimit(limit); } } + +void GameEngine::setGameResolution(unsigned int width, unsigned int height) { + gameResolution = sf::Vector2u(width, height); + gameView.setSize(static_cast(width), static_cast(height)); + gameView.setCenter(width / 2.0f, height / 2.0f); + updateViewport(); +} + +void GameEngine::setViewportMode(ViewportMode mode) { + viewportMode = mode; + updateViewport(); +} + +std::string GameEngine::getViewportModeString() const { + switch (viewportMode) { + case ViewportMode::Center: return "center"; + case ViewportMode::Stretch: return "stretch"; + case ViewportMode::Fit: return "fit"; + } + return "unknown"; +} + +void GameEngine::updateViewport() { + if (!render_target) return; + + auto windowSize = render_target->getSize(); + + switch (viewportMode) { + case ViewportMode::Center: { + // 1:1 pixels, centered in window + float viewportWidth = std::min(static_cast(gameResolution.x), static_cast(windowSize.x)); + float viewportHeight = std::min(static_cast(gameResolution.y), static_cast(windowSize.y)); + + float offsetX = (windowSize.x - viewportWidth) / 2.0f; + float offsetY = (windowSize.y - viewportHeight) / 2.0f; + + gameView.setViewport(sf::FloatRect( + offsetX / windowSize.x, + offsetY / windowSize.y, + viewportWidth / windowSize.x, + viewportHeight / windowSize.y + )); + break; + } + + case ViewportMode::Stretch: { + // Fill entire window, ignore aspect ratio + gameView.setViewport(sf::FloatRect(0, 0, 1, 1)); + break; + } + + case ViewportMode::Fit: { + // Maintain aspect ratio with black bars + float windowAspect = static_cast(windowSize.x) / windowSize.y; + float gameAspect = static_cast(gameResolution.x) / gameResolution.y; + + float viewportWidth, viewportHeight; + float offsetX = 0, offsetY = 0; + + if (windowAspect > gameAspect) { + // Window is wider - black bars on sides + viewportHeight = 1.0f; + viewportWidth = gameAspect / windowAspect; + offsetX = (1.0f - viewportWidth) / 2.0f; + } else { + // Window is taller - black bars on top/bottom + viewportWidth = 1.0f; + viewportHeight = windowAspect / gameAspect; + offsetY = (1.0f - viewportHeight) / 2.0f; + } + + gameView.setViewport(sf::FloatRect(offsetX, offsetY, viewportWidth, viewportHeight)); + break; + } + } + + // Apply the view + render_target->setView(gameView); +} + +sf::Vector2f GameEngine::windowToGameCoords(const sf::Vector2f& windowPos) const { + if (!render_target) return windowPos; + + // Convert window coordinates to game coordinates using the view + return render_target->mapPixelToCoords(sf::Vector2i(windowPos), gameView); +} diff --git a/src/GameEngine.h b/src/GameEngine.h index 1a0a235..e6371b5 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -13,6 +13,15 @@ class GameEngine { +public: + // Viewport modes (moved here so private section can use it) + enum class ViewportMode { + Center, // 1:1 pixels, viewport centered in window + Stretch, // viewport size = window size, doesn't respect aspect ratio + Fit // maintains original aspect ratio, leaves black bars + }; + +private: std::unique_ptr window; std::unique_ptr headless_renderer; sf::RenderTarget* render_target; @@ -37,6 +46,13 @@ class GameEngine // Scene transition state SceneTransition transition; + + // Viewport system + sf::Vector2u gameResolution{1024, 768}; // Fixed game resolution + sf::View gameView; // View for the game content + ViewportMode viewportMode = ViewportMode::Fit; + + void updateViewport(); void testTimers(); @@ -112,6 +128,14 @@ public: void setVSync(bool enabled); unsigned int getFramerateLimit() const { return framerate_limit; } void setFramerateLimit(unsigned int limit); + + // Viewport system + void setGameResolution(unsigned int width, unsigned int height); + sf::Vector2u getGameResolution() const { return gameResolution; } + void setViewportMode(ViewportMode mode); + ViewportMode getViewportMode() const { return viewportMode; } + std::string getViewportModeString() const; + sf::Vector2f windowToGameCoords(const sf::Vector2f& windowPos) const; // global textures for scripts to access std::vector textures; diff --git a/src/PyScene.cpp b/src/PyScene.cpp index 0c4919d..fb2a49e 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -28,7 +28,8 @@ void PyScene::do_mouse_input(std::string button, std::string type) } auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow()); - auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos); + // Convert window coordinates to game coordinates using the viewport + auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos)); // Create a sorted copy by z-index (highest first) std::vector> sorted_elements(*ui_elements); diff --git a/src/PyWindow.cpp b/src/PyWindow.cpp index 4500f91..c35f5c2 100644 --- a/src/PyWindow.cpp +++ b/src/PyWindow.cpp @@ -2,6 +2,7 @@ #include "GameEngine.h" #include "McRFPy_API.h" #include +#include // Singleton instance - static variable, not a class member static PyWindowObject* window_instance = nullptr; @@ -404,6 +405,82 @@ PyObject* PyWindow::screenshot(PyWindowObject* self, PyObject* args, PyObject* k return PyBytes_FromStringAndSize((const char*)pixels, size.x * size.y * 4); } +PyObject* PyWindow::get_game_resolution(PyWindowObject* self, void* closure) +{ + GameEngine* game = McRFPy_API::game; + if (!game) { + PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); + return NULL; + } + + auto resolution = game->getGameResolution(); + return Py_BuildValue("(ii)", resolution.x, resolution.y); +} + +int PyWindow::set_game_resolution(PyWindowObject* self, PyObject* value, void* closure) +{ + GameEngine* game = McRFPy_API::game; + if (!game) { + PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); + return -1; + } + + int width, height; + if (!PyArg_ParseTuple(value, "ii", &width, &height)) { + PyErr_SetString(PyExc_TypeError, "game_resolution must be a tuple of two integers (width, height)"); + return -1; + } + + if (width <= 0 || height <= 0) { + PyErr_SetString(PyExc_ValueError, "Game resolution dimensions must be positive"); + return -1; + } + + game->setGameResolution(width, height); + return 0; +} + +PyObject* PyWindow::get_scaling_mode(PyWindowObject* self, void* closure) +{ + GameEngine* game = McRFPy_API::game; + if (!game) { + PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); + return NULL; + } + + return PyUnicode_FromString(game->getViewportModeString().c_str()); +} + +int PyWindow::set_scaling_mode(PyWindowObject* self, PyObject* value, void* closure) +{ + GameEngine* game = McRFPy_API::game; + if (!game) { + PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); + return -1; + } + + const char* mode_str = PyUnicode_AsUTF8(value); + if (!mode_str) { + PyErr_SetString(PyExc_TypeError, "scaling_mode must be a string"); + return -1; + } + + GameEngine::ViewportMode mode; + if (strcmp(mode_str, "center") == 0) { + mode = GameEngine::ViewportMode::Center; + } else if (strcmp(mode_str, "stretch") == 0) { + mode = GameEngine::ViewportMode::Stretch; + } else if (strcmp(mode_str, "fit") == 0) { + mode = GameEngine::ViewportMode::Fit; + } else { + PyErr_SetString(PyExc_ValueError, "scaling_mode must be 'center', 'stretch', or 'fit'"); + return -1; + } + + game->setViewportMode(mode); + return 0; +} + // Property definitions PyGetSetDef PyWindow::getsetters[] = { {"resolution", (getter)get_resolution, (setter)set_resolution, @@ -418,6 +495,10 @@ PyGetSetDef PyWindow::getsetters[] = { "Window visibility state", NULL}, {"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit, "Frame rate limit (0 for unlimited)", NULL}, + {"game_resolution", (getter)get_game_resolution, (setter)set_game_resolution, + "Fixed game resolution as (width, height) tuple", NULL}, + {"scaling_mode", (getter)get_scaling_mode, (setter)set_scaling_mode, + "Viewport scaling mode: 'center', 'stretch', or 'fit'", NULL}, {NULL} }; diff --git a/src/PyWindow.h b/src/PyWindow.h index c1fce8f..ad69a83 100644 --- a/src/PyWindow.h +++ b/src/PyWindow.h @@ -32,6 +32,10 @@ public: static int set_visible(PyWindowObject* self, PyObject* value, void* closure); static PyObject* get_framerate_limit(PyWindowObject* self, void* closure); static int set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure); + static PyObject* get_game_resolution(PyWindowObject* self, void* closure); + static int set_game_resolution(PyWindowObject* self, PyObject* value, void* closure); + static PyObject* get_scaling_mode(PyWindowObject* self, void* closure); + static int set_scaling_mode(PyWindowObject* self, PyObject* value, void* closure); // Methods static PyObject* center(PyWindowObject* self, PyObject* args); diff --git a/tests/test_viewport_scaling.py b/tests/test_viewport_scaling.py new file mode 100644 index 0000000..1f7c433 --- /dev/null +++ b/tests/test_viewport_scaling.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Test viewport scaling modes""" + +import mcrfpy +from mcrfpy import Window, Frame, Caption, Color, Vector +import sys + +def test_viewport_modes(runtime): + """Test all three viewport scaling modes""" + mcrfpy.delTimer("test_viewport") + + print("Testing viewport scaling modes...") + + # Get window singleton + window = Window.get() + + # Test initial state + print(f"Initial game resolution: {window.game_resolution}") + print(f"Initial scaling mode: {window.scaling_mode}") + print(f"Window resolution: {window.resolution}") + + # Create test scene with visual elements + scene = mcrfpy.sceneUI("test") + + # Create a frame that fills the game resolution to show boundaries + game_res = window.game_resolution + boundary = Frame(0, 0, game_res[0], game_res[1], + fill_color=Color(50, 50, 100), + outline_color=Color(255, 255, 255), + outline=2) + boundary.name = "boundary" + scene.append(boundary) + + # Add corner markers + corner_size = 50 + corners = [ + (0, 0, "TL"), # Top-left + (game_res[0] - corner_size, 0, "TR"), # Top-right + (0, game_res[1] - corner_size, "BL"), # Bottom-left + (game_res[0] - corner_size, game_res[1] - corner_size, "BR") # Bottom-right + ] + + for x, y, label in corners: + corner = Frame(x, y, corner_size, corner_size, + fill_color=Color(255, 100, 100), + outline_color=Color(255, 255, 255), + outline=1) + scene.append(corner) + + text = Caption(x + 5, y + 5, label) + text.font_size = 20 + text.fill_color = Color(255, 255, 255) + scene.append(text) + + # Add center crosshair + center_x = game_res[0] // 2 + center_y = game_res[1] // 2 + h_line = Frame(center_x - 50, center_y - 1, 100, 2, + fill_color=Color(255, 255, 0)) + v_line = Frame(center_x - 1, center_y - 50, 2, 100, + fill_color=Color(255, 255, 0)) + scene.append(h_line) + scene.append(v_line) + + # Add mode indicator + mode_text = Caption(10, 10, f"Mode: {window.scaling_mode}") + mode_text.font_size = 24 + mode_text.fill_color = Color(255, 255, 255) + mode_text.name = "mode_text" + scene.append(mode_text) + + # Add instructions + instructions = Caption(10, 40, + "Press 1: Center mode (1:1 pixels)\n" + "Press 2: Stretch mode (fill window)\n" + "Press 3: Fit mode (maintain aspect ratio)\n" + "Press R: Change resolution\n" + "Press G: Change game resolution\n" + "Press Esc: Exit") + instructions.font_size = 14 + instructions.fill_color = Color(200, 200, 200) + scene.append(instructions) + + # Test changing modes + def test_mode_changes(runtime): + mcrfpy.delTimer("test_modes") + from mcrfpy import automation + + print("\nTesting scaling modes:") + + # Test center mode + window.scaling_mode = "center" + print(f"Set to center mode: {window.scaling_mode}") + mode_text.text = f"Mode: center (1:1 pixels)" + automation.screenshot("viewport_center_mode.png") + + # Schedule next mode test + mcrfpy.setTimer("test_stretch", test_stretch_mode, 1000) + + def test_stretch_mode(runtime): + mcrfpy.delTimer("test_stretch") + from mcrfpy import automation + + window.scaling_mode = "stretch" + print(f"Set to stretch mode: {window.scaling_mode}") + mode_text.text = f"Mode: stretch (fill window)" + automation.screenshot("viewport_stretch_mode.png") + + # Schedule next mode test + mcrfpy.setTimer("test_fit", test_fit_mode, 1000) + + def test_fit_mode(runtime): + mcrfpy.delTimer("test_fit") + from mcrfpy import automation + + window.scaling_mode = "fit" + print(f"Set to fit mode: {window.scaling_mode}") + mode_text.text = f"Mode: fit (aspect ratio maintained)" + automation.screenshot("viewport_fit_mode.png") + + # Test different window sizes + mcrfpy.setTimer("test_resize", test_window_resize, 1000) + + def test_window_resize(runtime): + mcrfpy.delTimer("test_resize") + from mcrfpy import automation + + print("\nTesting window resize with fit mode:") + + # Make window wider + window.resolution = (1280, 720) + print(f"Window resized to: {window.resolution}") + automation.screenshot("viewport_fit_wide.png") + + # Make window taller + mcrfpy.setTimer("test_tall", test_tall_window, 1000) + + def test_tall_window(runtime): + mcrfpy.delTimer("test_tall") + from mcrfpy import automation + + window.resolution = (800, 1000) + print(f"Window resized to: {window.resolution}") + automation.screenshot("viewport_fit_tall.png") + + # Test game resolution change + mcrfpy.setTimer("test_game_res", test_game_resolution, 1000) + + def test_game_resolution(runtime): + mcrfpy.delTimer("test_game_res") + + print("\nTesting game resolution change:") + window.game_resolution = (800, 600) + print(f"Game resolution changed to: {window.game_resolution}") + + # Note: UI elements won't automatically reposition, but viewport will adjust + + print("\nTest completed!") + print("Screenshots saved:") + print(" - viewport_center_mode.png") + print(" - viewport_stretch_mode.png") + print(" - viewport_fit_mode.png") + print(" - viewport_fit_wide.png") + print(" - viewport_fit_tall.png") + + # Restore original settings + window.resolution = (1024, 768) + window.game_resolution = (1024, 768) + window.scaling_mode = "fit" + + sys.exit(0) + + # Start test sequence + mcrfpy.setTimer("test_modes", test_mode_changes, 500) + +# Set up keyboard handler for manual testing +def handle_keypress(key, state): + if state != "start": + return + + window = Window.get() + scene = mcrfpy.sceneUI("test") + mode_text = None + for elem in scene: + if hasattr(elem, 'name') and elem.name == "mode_text": + mode_text = elem + break + + if key == "1": + window.scaling_mode = "center" + if mode_text: + mode_text.text = f"Mode: center (1:1 pixels)" + print(f"Switched to center mode") + elif key == "2": + window.scaling_mode = "stretch" + if mode_text: + mode_text.text = f"Mode: stretch (fill window)" + print(f"Switched to stretch mode") + elif key == "3": + window.scaling_mode = "fit" + if mode_text: + mode_text.text = f"Mode: fit (aspect ratio maintained)" + print(f"Switched to fit mode") + elif key == "r": + # Cycle through some resolutions + current = window.resolution + if current == (1024, 768): + window.resolution = (1280, 720) + elif current == (1280, 720): + window.resolution = (800, 600) + else: + window.resolution = (1024, 768) + print(f"Window resolution: {window.resolution}") + elif key == "g": + # Cycle game resolutions + current = window.game_resolution + if current == (1024, 768): + window.game_resolution = (800, 600) + elif current == (800, 600): + window.game_resolution = (640, 480) + else: + window.game_resolution = (1024, 768) + print(f"Game resolution: {window.game_resolution}") + elif key == "escape": + sys.exit(0) + +# Main execution +print("Creating viewport test scene...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") +mcrfpy.keypressScene(handle_keypress) + +# Schedule the test +mcrfpy.setTimer("test_viewport", test_viewport_modes, 100) + +print("Viewport test running...") +print("Use number keys to switch modes, R to resize window, G to change game resolution") \ No newline at end of file diff --git a/tests/test_viewport_simple.py b/tests/test_viewport_simple.py new file mode 100644 index 0000000..2df351a --- /dev/null +++ b/tests/test_viewport_simple.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Simple viewport test""" + +import mcrfpy +from mcrfpy import Window +import sys + +print("Testing viewport system...") + +# Get window singleton +window = Window.get() + +print(f"Game resolution: {window.game_resolution}") +print(f"Scaling mode: {window.scaling_mode}") +print(f"Window resolution: {window.resolution}") + +# Test changing scaling mode +window.scaling_mode = "center" +print(f"Changed to center mode: {window.scaling_mode}") + +window.scaling_mode = "stretch" +print(f"Changed to stretch mode: {window.scaling_mode}") + +window.scaling_mode = "fit" +print(f"Changed to fit mode: {window.scaling_mode}") + +# Test changing game resolution +window.game_resolution = (800, 600) +print(f"Changed game resolution to: {window.game_resolution}") + +print("Test completed!") +sys.exit(0) \ No newline at end of file diff --git a/tests/test_viewport_visual.py b/tests/test_viewport_visual.py new file mode 100644 index 0000000..926b77e --- /dev/null +++ b/tests/test_viewport_visual.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Visual viewport test with screenshots""" + +import mcrfpy +from mcrfpy import Window, Frame, Caption, Color +import sys + +def test_viewport_visual(runtime): + """Visual test of viewport modes""" + mcrfpy.delTimer("test") + + print("Creating visual viewport test...") + + # Get window singleton + window = Window.get() + + # Create test scene + scene = mcrfpy.sceneUI("test") + + # Create visual elements at game resolution boundaries + game_res = window.game_resolution + + # Full boundary frame + boundary = Frame(0, 0, game_res[0], game_res[1], + fill_color=Color(40, 40, 80), + outline_color=Color(255, 255, 0), + outline=3) + scene.append(boundary) + + # Corner markers + corner_size = 100 + colors = [ + Color(255, 100, 100), # Red TL + Color(100, 255, 100), # Green TR + Color(100, 100, 255), # Blue BL + Color(255, 255, 100), # Yellow BR + ] + positions = [ + (0, 0), # Top-left + (game_res[0] - corner_size, 0), # Top-right + (0, game_res[1] - corner_size), # Bottom-left + (game_res[0] - corner_size, game_res[1] - corner_size) # Bottom-right + ] + labels = ["TL", "TR", "BL", "BR"] + + for (x, y), color, label in zip(positions, colors, labels): + corner = Frame(x, y, corner_size, corner_size, + fill_color=color, + outline_color=Color(255, 255, 255), + outline=2) + scene.append(corner) + + text = Caption(x + 10, y + 10, label) + text.font_size = 32 + text.fill_color = Color(0, 0, 0) + scene.append(text) + + # Center crosshair + center_x = game_res[0] // 2 + center_y = game_res[1] // 2 + h_line = Frame(0, center_y - 1, game_res[0], 2, + fill_color=Color(255, 255, 255, 128)) + v_line = Frame(center_x - 1, 0, 2, game_res[1], + fill_color=Color(255, 255, 255, 128)) + scene.append(h_line) + scene.append(v_line) + + # Mode text + mode_text = Caption(center_x - 100, center_y - 50, + f"Mode: {window.scaling_mode}") + mode_text.font_size = 36 + mode_text.fill_color = Color(255, 255, 255) + scene.append(mode_text) + + # Resolution text + res_text = Caption(center_x - 150, center_y + 10, + f"Game: {game_res[0]}x{game_res[1]}") + res_text.font_size = 24 + res_text.fill_color = Color(200, 200, 200) + scene.append(res_text) + + from mcrfpy import automation + + # Test different modes and window sizes + def test_sequence(runtime): + mcrfpy.delTimer("seq") + + # Test 1: Fit mode with original size + print("Test 1: Fit mode, original window size") + automation.screenshot("viewport_01_fit_original.png") + + # Test 2: Wider window + window.resolution = (1400, 768) + print(f"Test 2: Fit mode, wider window {window.resolution}") + automation.screenshot("viewport_02_fit_wide.png") + + # Test 3: Taller window + window.resolution = (1024, 900) + print(f"Test 3: Fit mode, taller window {window.resolution}") + automation.screenshot("viewport_03_fit_tall.png") + + # Test 4: Center mode + window.scaling_mode = "center" + mode_text.text = "Mode: center" + print(f"Test 4: Center mode {window.resolution}") + automation.screenshot("viewport_04_center.png") + + # Test 5: Stretch mode + window.scaling_mode = "stretch" + mode_text.text = "Mode: stretch" + window.resolution = (1280, 720) + print(f"Test 5: Stretch mode {window.resolution}") + automation.screenshot("viewport_05_stretch.png") + + # Test 6: Small window with fit + window.scaling_mode = "fit" + mode_text.text = "Mode: fit" + window.resolution = (640, 480) + print(f"Test 6: Fit mode, small window {window.resolution}") + automation.screenshot("viewport_06_fit_small.png") + + print("\nViewport visual test completed!") + print("Screenshots saved:") + print(" - viewport_01_fit_original.png") + print(" - viewport_02_fit_wide.png") + print(" - viewport_03_fit_tall.png") + print(" - viewport_04_center.png") + print(" - viewport_05_stretch.png") + print(" - viewport_06_fit_small.png") + + sys.exit(0) + + # Start test sequence after a short delay + mcrfpy.setTimer("seq", test_sequence, 500) + +# Main execution +print("Starting visual viewport test...") +mcrfpy.createScene("test") +mcrfpy.setScene("test") +mcrfpy.setTimer("test", test_viewport_visual, 100) +print("Test scheduled...") \ No newline at end of file