Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,094 changes: 1,457 additions & 637 deletions rtxpy/engine.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions rtxpy/viewer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# rtxpy.viewer — Composed subsystem objects for InteractiveViewer.
81 changes: 81 additions & 0 deletions rtxpy/viewer/camera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Camera state and projection helpers for the interactive viewer."""

import numpy as np


class CameraState:
"""Camera position, orientation, and projection parameters.

Encapsulates all camera-related state: position, yaw/pitch, FOV,
movement/look speeds, and time-of-day presets for sun positioning.
"""

__slots__ = (
'position', 'yaw', 'pitch', 'fov',
'move_speed', 'look_speed',
'_time_presets', '_time_preset_idx',
)

def __init__(self):
self.position = None
self.yaw = 90.0 # Degrees, 0 = +X, 90 = +Y
self.pitch = -15.0 # Degrees, negative = looking down
self.fov = 60.0
self.move_speed = None # Set in run() based on terrain extent
self.look_speed = 5.0
self._time_presets = [
('Morning', 135.0, 25.0),
('Midday', 180.0, 65.0),
('Afternoon', 225.0, 35.0),
('Golden Hour', 270.0, 12.0),
('Sunset', 280.0, 3.0),
]
self._time_preset_idx = 2 # Afternoon (default)

def get_front(self):
"""Get the forward direction vector."""
yaw_rad = np.radians(self.yaw)
pitch_rad = np.radians(self.pitch)
return np.array([
np.cos(yaw_rad) * np.cos(pitch_rad),
np.sin(yaw_rad) * np.cos(pitch_rad),
np.sin(pitch_rad)
], dtype=np.float32)

def get_right(self):
"""Get the right direction vector."""
front = self.get_front()
world_up = np.array([0, 0, 1], dtype=np.float32)
right = np.cross(world_up, front)
return right / (np.linalg.norm(right) + 1e-8)

def get_look_at(self):
"""Get the current look-at point."""
return self.position + self.get_front() * 1000.0

def screen_to_ray(self, screen_x, screen_y, render_width, render_height,
display_width, display_height):
"""Convert screen pixel coordinates to a world-space ray.

Returns (origin, direction) as numpy float32 arrays of shape (3,).
"""
front = self.get_front()
world_up = np.array([0, 0, 1], dtype=np.float32)
right = np.cross(world_up, front)
rn = np.linalg.norm(right)
if rn > 1e-8:
right /= rn
else:
right = np.array([1, 0, 0], dtype=np.float32)
cam_up = np.cross(front, right)

fov_scale = np.tan(np.radians(self.fov) / 2.0)
aspect = render_width / max(1, render_height)

# Window coords -> NDC (-1..1)
nx = 2.0 * screen_x / max(1, display_width) - 1.0
ny = 1.0 - 2.0 * screen_y / max(1, display_height)

direction = front + nx * fov_scale * aspect * right + ny * fov_scale * cam_up
direction = direction / (np.linalg.norm(direction) + 1e-30)
return self.position.copy(), direction.astype(np.float32)
29 changes: 29 additions & 0 deletions rtxpy/viewer/geometry_layers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Geometry layer visibility management for the interactive viewer."""


class GeometryLayerManager:
"""Tracks GAS geometry groups and layer cycling state.

Manages which geometry groups are visible, the current cycling
index, per-layer positions for jump-to-geometry, and point-cloud
color modes.
"""

__slots__ = (
'all_geometries', 'geometry_layer_order', 'geometry_layer_idx',
'layer_positions', 'current_geom_idx',
'pc_color_modes', 'pc_color_mode_idx',
'chunk_manager', 'baked_meshes', 'geometry_colors_builder',
)

def __init__(self):
self.all_geometries = []
self.geometry_layer_order = ['none', 'all']
self.geometry_layer_idx = 1 # Start at 'all'
self.layer_positions = {} # layer_name -> [(x, y, z, geometry_id), ...]
self.current_geom_idx = 0
self.pc_color_modes = ['elevation', 'intensity', 'classification', 'rgb']
self.pc_color_mode_idx = 0
self.chunk_manager = None # set by explore() when scene_zarr provided
self.baked_meshes = {}
self.geometry_colors_builder = None
42 changes: 42 additions & 0 deletions rtxpy/viewer/hud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""HUD (heads-up display) state for the interactive viewer."""


class HUDState:
"""Holds title, subtitle, legend, help pages, and minimap state.

Groups all on-screen overlay / HUD variables into a single object.
"""

__slots__ = (
'title', 'subtitle', 'legend_config', 'info_text',
'title_overlay_rgba', 'legend_rgba',
'help_page_idx', 'help_pages',
'show_minimap',
'last_title', 'last_subtitle',
'minimap_background', 'minimap_scale_x', 'minimap_scale_y',
'minimap_has_tiles', 'minimap_rect',
'minimap_style', 'minimap_layer', 'minimap_colors',
)

def __init__(self, title='rtxpy', subtitle=None, legend=None):
self.title = title
self.subtitle = subtitle
self.legend_config = legend
self.info_text = None
self.title_overlay_rgba = None
self.legend_rgba = None
self.help_page_idx = 0 # -1 = off, 0..N-1 = page index
self.help_pages = []
self.show_minimap = True
self.last_title = None
self.last_subtitle = None

# Minimap state (initialized in run() via _compute_minimap_background)
self.minimap_background = None
self.minimap_scale_x = 1.0
self.minimap_scale_y = 1.0
self.minimap_has_tiles = False
self.minimap_rect = None
self.minimap_style = None
self.minimap_layer = None
self.minimap_colors = None
17 changes: 17 additions & 0 deletions rtxpy/viewer/input_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Input state tracking for the interactive viewer."""


class InputState:
"""Tracks keyboard and mouse input state.

Holds the set of currently-pressed movement keys and mouse drag
state, decoupled from the viewer's rendering logic.
"""

__slots__ = ('held_keys', 'mouse_dragging', 'mouse_last_x', 'mouse_last_y')

def __init__(self):
self.held_keys = set()
self.mouse_dragging = False
self.mouse_last_x = None
self.mouse_last_y = None
88 changes: 88 additions & 0 deletions rtxpy/viewer/keybindings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Declarative key-binding tables for the interactive viewer.

Three dispatch tables map key events to method names on InteractiveViewer:

- ``SHIFT_BINDINGS``: Checked first. Maps uppercase ``raw_key`` (Shift
held) to a method name. E.g. ``'O'`` → ``'_action_shift_o'``.
- ``KEY_BINDINGS``: Checked second. Maps lowercase ``key`` to a method
name. E.g. ``'t'`` → ``'_action_toggle_shadows'``.
- ``SPECIAL_BINDINGS``: Checked last. Maps ``(raw_key, key)`` tuples
for keys that need both values (e.g. ``r`` vs ``R``).

Movement keys (WASD, arrows, IJKL, Q/E, PageUp/Down) are tracked in
``MOVEMENT_KEYS`` and handled separately by adding to ``_held_keys``.

Observer slots 1-8 are handled separately in ``_handle_key_press``.
"""

# Keys that get added to _held_keys for continuous movement/look
MOVEMENT_KEYS = frozenset({
'w', 's', 'a', 'd',
'up', 'down', 'left', 'right',
'q', 'e', 'pageup', 'pagedown',
'i', 'j', 'k', 'l',
})

# Shift+<key> bindings — checked first (raw_key is uppercase)
SHIFT_BINDINGS = {
'O': '_action_shift_o', # Cycle drone mode
'V': '_action_shift_v', # Snap to observer
'K': '_action_clear_observers', # Kill all observers
'F': '_action_toggle_firms', # FIRMS fire layer
'W': '_action_toggle_wind', # Wind particles
'E': '_action_toggle_terrain_vis', # Toggle terrain visibility
'B': '_action_toggle_gtfs_rt', # GTFS-RT vehicles
'C': '_action_cycle_pc_colors', # Point cloud color mode
'D': '_action_toggle_denoiser', # Denoiser
'G': '_action_cycle_gi', # GI bounces
'H': '_action_prev_help_page', # Previous help page
'L': '_action_toggle_drone_glow', # Drone glow
'T': '_action_cycle_time', # Time-of-day
}

# Lowercase key bindings — checked after shift bindings
KEY_BINDINGS = {
't': '_action_toggle_shadows',
'c': '_action_cycle_colormap',
'g': '_cycle_terrain_layer',
'n': '_cycle_geometry_layer',
'p': '_action_jump_prev_geom',
'h': '_action_next_help_page',
'm': '_action_toggle_minimap',
'o': '_action_place_observer',
'v': '_toggle_viewshed',
'[': '_action_observer_elev_down',
']': '_action_observer_elev_up',
'f': '_save_screenshot',
'y': '_action_cycle_color_stretch',
'b': '_action_cycle_mesh_type',
'u': '_action_cycle_basemap_fwd',
',': '_action_overlay_alpha_down',
'.': '_action_overlay_alpha_up',
'0': '_action_toggle_ao',
'9': '_action_toggle_dof',
';': '_action_dof_aperture_down',
"'": '_action_dof_aperture_up',
'escape': '_action_exit',
'x': '_action_exit',
}

# Keys that need both raw_key and key for dispatch
# (raw_key, key) → method name
SPECIAL_BINDINGS = {
# Speed
('+', '+'): '_action_speed_up',
('=', '='): '_action_speed_up',
('-', '-'): '_action_speed_down',
# Resolution: r = coarser, R = finer
('r', 'r'): '_action_resolution_coarser',
('R', 'r'): '_action_resolution_finer',
# Vertical exaggeration: z = decrease, Z = increase
('z', 'z'): '_action_ve_down',
('Z', 'z'): '_action_ve_up',
# Basemap: U = reverse
('U', 'u'): '_action_cycle_basemap_rev',
# DOF focal distance: : = decrease, " = increase
(':', ':'): '_action_dof_focal_down',
('"', '"'): '_action_dof_focal_up',
}
87 changes: 87 additions & 0 deletions rtxpy/viewer/observers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Observer management for the interactive viewer."""

import threading


OBSERVER_COLORS = [
(1.0, 0.2, 0.2), # 1: red
(0.2, 0.6, 1.0), # 2: blue
(0.2, 1.0, 0.3), # 3: green
(1.0, 0.8, 0.1), # 4: yellow
(1.0, 0.4, 0.0), # 5: orange
(0.8, 0.2, 1.0), # 6: purple
(0.0, 1.0, 0.9), # 7: cyan
(1.0, 0.5, 0.7), # 8: pink
]


class Observer:
"""State for a single observer slot (1-8)."""

__slots__ = (
'slot', 'position', 'observer_elev', 'drone_mode', 'drone_placed',
'yaw', 'pitch', 'saved_camera', 'tour_thread', 'tour_stop',
'viewshed_enabled', 'viewshed_cache',
)

def __init__(self, slot, position, observer_elev=0.05):
self.slot = slot
self.position = position
self.observer_elev = observer_elev
self.drone_mode = 'off'
self.drone_placed = False
self.yaw = 0.0
self.pitch = 0.0
self.saved_camera = None
self.tour_thread = None
self.tour_stop = threading.Event()
self.viewshed_enabled = False
self.viewshed_cache = None

@property
def color(self):
return OBSERVER_COLORS[(self.slot - 1) % len(OBSERVER_COLORS)]

def geometry_id(self, part_idx):
"""Unique geometry ID for a drone sub-mesh."""
return f'_observer{self.slot}_{part_idx}'

def is_touring(self):
return self.tour_thread is not None and self.tour_thread.is_alive()

def stop_tour(self):
self.tour_stop.set()
if self.tour_thread is not None:
self.tour_thread.join(timeout=2.0)
self.tour_thread = None
self.tour_stop.clear()


class ObserverManager:
"""Manages multi-observer system (up to 8 independent observers).

Holds observer instances, viewshed settings, and drone part state.
"""

__slots__ = (
'observers', 'active_observer',
'viewshed_enabled', 'viewshed_observer_elev',
'viewshed_target_elev', 'viewshed_opacity',
'viewshed_cache', 'viewshed_coverage',
'viewshed_recalc_interval', 'last_viewshed_time',
'shared_drone_parts', 'drone_glow',
)

def __init__(self):
self.observers = {} # dict[int, Observer] — slot 1-8
self.active_observer = None # int (slot 1-8) or None
self.viewshed_enabled = False
self.viewshed_observer_elev = 0.05
self.viewshed_target_elev = 0.0
self.viewshed_opacity = 0.35
self.viewshed_cache = None
self.viewshed_coverage = 0.0
self.viewshed_recalc_interval = 0.4
self.last_viewshed_time = 0.0
self.shared_drone_parts = None
self.drone_glow = False
37 changes: 37 additions & 0 deletions rtxpy/viewer/overlays.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Overlay layer management for the interactive viewer."""


class OverlayManager:
"""Manages terrain overlay layers and basemap tile settings.

Tracks which overlay is active, the alpha blending value, basemap
cycling state, and tile service reference.
"""

__slots__ = (
'overlay_layers', 'overlay_names',
'active_color_data', 'active_overlay_data',
'overlay_alpha', 'overlay_as_water',
'terrain_layer_order', 'terrain_layer_idx',
'base_overlay_layers',
'tile_service', 'tiles_enabled',
'basemap_options', 'basemap_idx',
)

def __init__(self, overlay_layers=None, base_overlay_layers=None):
self.overlay_layers = overlay_layers or {}
self.overlay_names = list(self.overlay_layers.keys())
self.active_color_data = None
self.active_overlay_data = None
self.overlay_alpha = 0.7
self.overlay_as_water = False

self.terrain_layer_order = ['elevation'] + list(self.overlay_names)
self.terrain_layer_idx = 0

self.base_overlay_layers = base_overlay_layers or {}

self.tile_service = None
self.tiles_enabled = False
self.basemap_options = ['none', 'satellite', 'osm']
self.basemap_idx = 0
Loading
Loading