"""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_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.
The SubViewport's subtree is rendered into its own offscreen render target
each frame, using its *own* camera: a ``Camera3D`` and/or ``Camera2D``
placed among its children (a sensible default is used if none is present).
The rendered colour buffer is exposed as a bindless texture index via
:attr:`texture`, which other nodes in the main scene can sample: assign it
to a ``Material.albedo_tex_index`` for a 3D "monitor in the world", or feed
a ``Sprite2D`` for a 2D minimap / picture-in-picture.
The render-to-texture integration is driven from the graphics backend
(``simvx.graphics.renderer.sub_viewport.SubViewportManager``), not from this
node: core stays rendering-agnostic. SubViewports render *before* the main
scene pass each frame, so the main pass samples fresh content the same
frame. A SubViewport that samples *another* SubViewport sees the previous
frame's content (one-frame lag).
Properties:
size: Offscreen target dimensions in pixels.
transparent_bg: Clear to transparent instead of opaque black.
render_target_update_mode: ``"always"`` (default, render every frame),
``"once"`` (render a single frame then freeze), or ``"disabled"``
(never render; the slot stays valid but stale).
"""
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))