Source code for simvx.core.viewport

"""Viewport, SubViewport, and Display system for SimVX."""

from __future__ import annotations

import logging
from enum import IntEnum

import numpy as np

from .node import Node
from .descriptors import Property
from .ui.core import Control

log = logging.getLogger(__name__)


[docs] class StretchMode(IntEnum): DISABLED = 0 # Pixel-perfect, no scaling CANVAS_ITEMS = 1 # Scale 2D content, keep 3D at window resolution VIEWPORT = 2 # Scale everything (render at design res, upscale)
[docs] class StretchAspect(IntEnum): IGNORE = 0 # Stretch to fill, distort if needed KEEP = 1 # Letterbox/pillarbox to maintain aspect KEEP_WIDTH = 2 # Keep width, adjust height KEEP_HEIGHT = 3 # Keep height, adjust width EXPAND = 4 # Expand viewport, no black bars
[docs] class VSyncMode(IntEnum): DISABLED = 0 ENABLED = 1 ADAPTIVE = 2 MAILBOX = 3
[docs] class WindowMode(IntEnum): WINDOWED = 0 FULLSCREEN = 1 BORDERLESS = 2 MAXIMIZED = 3
[docs] class DisplaySettings: """Project-level display configuration. Read from project.simvx.""" def __init__(self): self.design_width: int = 1280 self.design_height: int = 720 self.stretch_mode: StretchMode = StretchMode.DISABLED self.stretch_aspect: StretchAspect = StretchAspect.KEEP self.window_mode: WindowMode = WindowMode.WINDOWED self.vsync: VSyncMode = VSyncMode.ENABLED self.max_fps: int = 0 # 0 = unlimited self.msaa: int = 0 # 0, 2, 4, 8
[docs] def compute_canvas_transform(self, window_width: int, window_height: int) -> np.ndarray: """Compute the 2D canvas transform matrix based on stretch settings. Returns 3x3 affine matrix to apply to all 2D rendering. """ if self.stretch_mode == StretchMode.DISABLED: return np.eye(3, dtype=np.float32) dw, dh = self.design_width, self.design_height ww, wh = window_width, window_height sx = ww / dw sy = wh / dh # Apply aspect ratio correction ox, oy = 0.0, 0.0 if self.stretch_aspect == StretchAspect.IGNORE: pass # Use sx, sy as-is elif self.stretch_aspect == StretchAspect.KEEP: s = min(sx, sy) ox = (ww - dw * s) / 2 oy = (wh - dh * s) / 2 sx = sy = s elif self.stretch_aspect == StretchAspect.KEEP_WIDTH: sy = sx # Match width scale elif self.stretch_aspect == StretchAspect.KEEP_HEIGHT: sx = sy # Match height scale elif self.stretch_aspect == StretchAspect.EXPAND: s = max(sx, sy) sx = sy = s return np.array([[sx, 0, ox], [0, sy, oy], [0, 0, 1]], dtype=np.float32)
[docs] def compute_viewport_rect(self, window_width: int, window_height: int) -> tuple[int, int, int, int]: """Compute the viewport rectangle (x, y, w, h) for 3D rendering.""" if self.stretch_mode == StretchMode.DISABLED: return (0, 0, window_width, window_height) dw, dh = self.design_width, self.design_height ww, wh = window_width, window_height dar = dw / dh war = ww / wh if self.stretch_aspect == StretchAspect.KEEP: if war > dar: vh = wh vw = int(vh * dar) vx = (ww - vw) // 2 vy = 0 else: vw = ww vh = int(vw / dar) vx = 0 vy = (wh - vh) // 2 return (vx, vy, vw, vh) return (0, 0, window_width, window_height)
[docs] class SubViewport(Node): """Renders its children to an offscreen texture.""" size = Property((256, 256)) transparent_bg = Property(False) render_target_update_mode = Property("always") def __init__(self, name="SubViewport", **kwargs): super().__init__(name=name, **kwargs) self._texture_id: int = -1 self._scene_tree = None @property def texture(self): """The rendered texture, usable as material input or Sprite2D texture.""" return self._texture_id
[docs] def get_texture_size(self) -> tuple[int, int]: return tuple(self.size)
[docs] class ViewportContainer(Control): """UI widget that hosts and displays a SubViewport.""" stretch = Property(True) def __init__(self, name="ViewportContainer", **kwargs): super().__init__(name=name, **kwargs) self._viewport: SubViewport | None = None
[docs] def set_viewport(self, viewport: SubViewport): self._viewport = viewport if self.stretch: viewport.size = (int(self.size.x), int(self.size.y))