Source code for simvx.core.viewport

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

import logging
from enum import IntEnum

import numpy as np

from .descriptors import Property
from .node import Node
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 simvx.toml.""" 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. .. note:: The node tree, properties, and scene serialization are stable. The Vulkan render-to-texture integration that actually produces the ``_texture_id`` contents is still in progress; ``SubViewport`` is safe to place in a scene today, but the captured texture will not yet be readable by other nodes until the renderer wiring lands. """ 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
[docs] @property def texture(self): """The rendered texture, usable as material input or Sprite2D texture.""" return self._texture_id
[docs] @property def texture_size(self) -> tuple[int, int]: """Pixel dimensions of the underlying render target.""" 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))