Files
orisys-sdk/examples/gui/layout.py
T
ykcheungab 2e5c047d1e 1. tuned arrow mesh drawing
2. centralixzed arrow config in layout.py
3. seperate mesh render and display resolution to speed up (~40 fps)
2026-06-19 14:28:28 +08:00

272 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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")