"""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_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))