""" Finger dashboard layout — edit this file to define the panel hierarchy and sizes. Structure: ┌─ toolbar ─────────────────────────────────────────────┐ ├─ TOP (stretch: top_stretch) ──────────────────────────┤ │ ┌ mesh ─┬─ arrows ─┬─ charts ─┐ (left → right) │ │ └───────┴───────────┴─────────┘ │ ├─ LOG (stretch: log_stretch) ──────────────────────────┤ └───────────────────────────────────────────────────────┘ Each panel uses explicit width_px and height_px. top_stretch / log_stretch control how extra vertical space is shared on resize. MeshLighting (below) controls the 3D mesh panel sun light: azimuth_deg — horizontal angle in degrees (0° = +X, 90° = +Y) elevation_deg — vertical angle in degrees (positive = from above) intensity — Filament sun strength (typical 30000–120000) ambient/diffuse — Windows viewer shading weights (0–1) MESH_CONTACT_ARROW (below) controls the yellow 3D contact arrow on the mesh panel: reference_normal_force — Fn at which arrow reaches full size (log/power/linear curve) force_strength_mode — "log" | "power" | "linear" min/max_length_fraction, min/max_thickness_scale, color Mesh panel has two sizes (edit MESH_RENDER_* and TOP_ROW mesh slot): display (TOP_ROW mesh width_px/height_px) — Qt widget size on screen render (MESH_RENDER_WIDTH/HEIGHT) — Open3D offscreen resolution (upscaled for display) Performance (3D dashboard; edit DashboardLayout defaults): timer_interval_ms — Qt timer period (16 ≈ 60 Hz tick rate) mesh_render_stride — render 3D mesh every N frames (2 ≈ half mesh cost) chart_update_stride — update force charts every N frames mesh_async_render — background mesh render (Linux only; Windows uses sync) Edit TOP_ROW.slots to reorder or resize panels. Edit PANEL_SETUP to change top vs log proportions. Edit LOG_CONFIG for live status line and log panel appearance. """ from __future__ import annotations import math from dataclasses import dataclass from typing import Literal from viz.contact_arrow import ContactArrowStyle, DEFAULT_CONTACT_ARROW_STYLE PanelId = Literal["mesh", "arrows", "charts", "log"] @dataclass(frozen=True) class PanelSlot: """One panel in the dashboard.""" id: PanelId title: str width_px: int height_px: int stretch: int = 0 enabled: bool = True @dataclass(frozen=True) class PanelRow: """Horizontal row of panels (the top visual strip).""" slots: tuple[PanelSlot, ...] collapsible: bool = False @dataclass(frozen=True) class PanelSetup: """Full panel hierarchy: top row + bottom log.""" top_row: PanelRow log_panel: PanelSlot top_stretch: int = 5 log_stretch: int = 1 # --- Top strip: mesh | arrows | charts (edit order and sizes here) --- TOP_ROW = PanelRow( slots=( PanelSlot("mesh", "3D mesh", width_px=500, height_px=500), PanelSlot("arrows", "UV Unrolled", width_px=500, height_px=500), PanelSlot("charts", "Force charts", width_px=500, height_px=500), ), ) LOG_PANEL = PanelSlot("log", "Log", width_px=0, height_px=100) PANEL_SETUP = PanelSetup( top_row=TOP_ROW, log_panel=LOG_PANEL, top_stretch=5, log_stretch=1, ) # Open3D offscreen resolution for the mesh panel (independent of on-screen widget size). MESH_RENDER_WIDTH_PX = 256 MESH_RENDER_HEIGHT_PX = 256 @dataclass(frozen=True) class MeshLighting: """3D mesh panel light — edit MESH_LIGHTING or DashboardLayout.mesh_lighting.""" azimuth_deg: float = -45.0 # finger outline elevation_deg: float = -35.0 # tip to root intensity: float = 75000.0 ambient: float = 0.35 diffuse: float = 0.65 @property def direction(self) -> tuple[float, float, float]: """Unit light direction derived from azimuth / elevation (degrees).""" az = math.radians(self.azimuth_deg) el = math.radians(self.elevation_deg) cos_el = math.cos(el) return (cos_el * math.cos(az), cos_el * math.sin(az), math.sin(el)) MESH_LIGHTING = MeshLighting( azimuth_deg=-45.0, elevation_deg=0, intensity=75000.0, ambient=0.5, diffuse=0.5, ) # 3D mesh contact arrow — edit here for dashboard tuning (see ContactArrowStyle in viz/contact_arrow.py). MESH_CONTACT_ARROW = ContactArrowStyle( reference_normal_force=10000.0, force_strength_mode="log", force_strength_gamma=0.5, min_length_fraction=0.02, max_length_fraction=0.30, min_thickness_scale=0.20, max_thickness_scale=1.0, normal_force_scale=1.0, color=(1.0, 0.92, 0.10), ) @dataclass(frozen=True) class LogConfig: """Log panel — edit LOG_CONFIG or DashboardLayout.log_config.""" status_enabled: bool = True max_lines: int = 2000 font_family: str = "Consolas" font_pt: int = 12 show_flow: bool = True show_mask: bool = True def format_status( self, *, fps: float, fnormal: float, fshearx: float, fsheary: float, flow_rms: float = 0.0, mask_coverage: float = 0.0, wall_fps: float | None = None, ) -> str: shear_mag = float((fshearx**2 + fsheary**2) ** 0.5) if wall_fps is not None: parts = [f"FPS={wall_fps:.1f}", f"sdk={fps:.1f}"] else: parts = [f"FPS={fps:.1f}"] parts.extend( [ f"Fn={fnormal:.1f}", f"|Fs|={shear_mag:.1f}", f"Fs=({fshearx:.1f}, {fsheary:.1f})", ] ) return " ".join(parts) LOG_CONFIG = LogConfig() @dataclass(frozen=True) class DashboardLayout: """Window chrome, chart styling, and panel hierarchy.""" panel_setup: PanelSetup = PANEL_SETUP window_title: str = "Orisys Finger Dashboard" window_width: int = 1500 window_height: int = 600 root_margin_px: int = 6 root_spacing_px: int = 6 toolbar_spacing_px: int = 8 plot_buffer_points: int = 120 plot_sample_rate_hz: float = 30.0 chart_title_font_pt: int = 11 chart_axis_label_font_pt: int = 9 chart_tick_font_pt: int = 9 y_axis_min_span: float = 20000.0 y_range_smooth_alpha: float = 0.5 curve_color: str = "#00E5FF" # UV arrow panel: min flow magnitude to draw an arrow (display only; not dd02-ov.json contact.flow_threshold) arrow_flow_threshold: float = 1.0 mesh_lighting: MeshLighting = MESH_LIGHTING mesh_contact_arrow: ContactArrowStyle = MESH_CONTACT_ARROW log_config: LogConfig = LOG_CONFIG mesh_render_width_px: int = MESH_RENDER_WIDTH_PX mesh_render_height_px: int = MESH_RENDER_HEIGHT_PX timer_interval_ms: int = 16 mesh_async_render: bool = True mesh_render_stride: int = 2 chart_update_stride: int = 2 def mesh_display_size(self) -> tuple[int, int]: """On-screen mesh panel size from TOP_ROW mesh slot.""" slot = self.top_slot("mesh") return slot.width_px, slot.height_px def mesh_render_size(self) -> tuple[int, int]: """Open3D offscreen render target size (may differ from display size).""" return self.mesh_render_width_px, self.mesh_render_height_px def slot(self, panel_id: PanelId) -> PanelSlot | None: if self.panel_setup.log_panel.id == panel_id: return self.panel_setup.log_panel for slot in self.panel_setup.top_row.slots: if slot.id == panel_id: return slot return None def top_slot(self, panel_id: PanelId) -> PanelSlot: for slot in self.panel_setup.top_row.slots: if slot.id == panel_id: return slot raise KeyError(f"panel {panel_id!r} not in top row") @property def mesh_panel_title(self) -> str: return self.top_slot("mesh").title @property def arrows_panel_title(self) -> str: return self.top_slot("arrows").title LAYOUT = DashboardLayout() def top_row_splitter_sizes(layout: DashboardLayout = LAYOUT) -> tuple[int, ...]: """Horizontal splitter widths from panel setup.""" return tuple(slot.width_px for slot in layout.panel_setup.top_row.slots if slot.enabled) def top_row_slot_index(panel_id: PanelId, layout: DashboardLayout = LAYOUT) -> int: """Index of a panel in the horizontal top splitter.""" for i, slot in enumerate(layout.panel_setup.top_row.slots): if slot.id == panel_id: return i raise KeyError(f"panel {panel_id!r} not in top row")