feat(viewport): complete viewport-based rendering system (#8)
Implements a comprehensive viewport system that allows fixed game resolution with flexible window scaling, addressing the primary wishes for issues #34, #49, and #8. Key Features: - Fixed game resolution independent of window size (window.game_resolution property) - Three scaling modes accessible via window.scaling_mode: - "center": 1:1 pixels, viewport centered in window - "stretch": viewport fills window, ignores aspect ratio - "fit": maintains aspect ratio with black bars - Automatic window-to-game coordinate transformation for mouse input - Full Python API integration with PyWindow properties Technical Implementation: - GameEngine::ViewportMode enum with Center, Stretch, Fit modes - SFML View system for efficient GPU-based viewport scaling - updateViewport() recalculates on window resize or mode change - windowToGameCoords() transforms mouse coordinates correctly - PyScene mouse input automatically uses transformed coordinates Tests: - test_viewport_simple.py: Basic API functionality - test_viewport_visual.py: Visual verification with screenshots - test_viewport_scaling.py: Interactive mode switching and resizing This completes the viewport-based rendering task and provides the foundation for resolution-independent game development as requested for Crypt of Sokoban. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
93256b96c6
commit
5a49cb7b6d
15
ROADMAP.md
15
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.
|
||||
|
|
|
@ -32,6 +32,11 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
}
|
||||
|
||||
visible = render_target->getDefaultView();
|
||||
|
||||
// Initialize the game view
|
||||
gameView.setSize(static_cast<float>(gameResolution.x), static_cast<float>(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<float>(width), static_cast<float>(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<float>(gameResolution.x), static_cast<float>(windowSize.x));
|
||||
float viewportHeight = std::min(static_cast<float>(gameResolution.y), static_cast<float>(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<float>(windowSize.x) / windowSize.y;
|
||||
float gameAspect = static_cast<float>(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);
|
||||
}
|
||||
|
|
|
@ -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<sf::RenderWindow> window;
|
||||
std::unique_ptr<HeadlessRenderer> 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<IndexTexture> textures;
|
||||
|
|
|
@ -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<std::shared_ptr<UIDrawable>> sorted_elements(*ui_elements);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <cstring>
|
||||
|
||||
// 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}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")
|
|
@ -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)
|
|
@ -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...")
|
Loading…
Reference in New Issue