--- title: GUI Architecture & Interaction last_updated: 2025-11-05 MingHan --- # GUI Architecture & Interaction This document details the GUI subsystem implemented in `_gui.py`. It covers layout, widgets, data flow, commands emitted to the backend, and the update loop. --- ## 1. High-Level Layout The main window (`MainWindow`) arranges three vertical zones: ``` +------------------------------ MainWindow ------------------------------+ | [Left: Control Panel ~250px] | [Center: Vispy Canvas] | [Right: Plot ~400px] | | Grid of image views + 3D (optional) | Tip-point X/Y/Z traces | - Capture Toolbar | - L/R/B images, warp, render, tracking | (pyqtgraph) | - Dataset Selector | - VisualTree binds & updates | | - EV / Contrast Controls | | | - Robotic Control Panel | | | - Logger (QPlainTextEdit) | | +-----------------------------------------------------------------------+ ``` - **Control Panel (left, fixed width ~250)**: capture/record, dataset switch, exposure/contrast, robotic control, and a live log console. - **Canvas (center)**: a Vispy `SceneCanvas` hosting a grid of views. Each view shows an image (e.g., `L.image`, `R.image`, `B.render`, etc.) or 3D visuals (frusta, meshes). - **Plot Panel (right, fixed width ~400)**: three pyqtgraph plots (X, Y, Z vs time) visualizing solver tip-point traces. --- ## 2. Modes & Initialization `MainWindow(replay: bool, enable_plots: bool=False, command_queue: Queue=None, dataset_path: str="None")`: - **Replay mode (`replay=True`)** - Shows **PlaybackControlWidget** (transport + seek bar). - Hides live-only controls (e.g., snapshot/record toolbar and robotic panel). - **Live mode (`replay=False`)** - Shows **CaptureToolbar** (snapshot/record with overlay/flash). - Shows **RoboticControlPanel**. - **Common** - **DatasetSelectorWidget** to set dataset folder or show `Network` if none. - **EvControlWidget** / **ContrastControlWidget** for exposure/contrast tuning. - **Log terminal** wired via a `QTextEditLogger` handler. --- ## 3. Canvas & Visual Binding ```python self.visual_tree = VisualTree() self.canvas = scene.SceneCanvas(keys="interactive", size=(1920, 1080)) self.grid = self.canvas.central_widget.add_grid() self.cameras = load_cameras() # used if enabling 3D viewport bindings ``` ### 3.1 Views The grid places multiple `PanZoomCamera` image views (exact rows/cols configured in `_setup_views()`): - **Left/Right (L/R)**: raw BGR and processed images - bindings like `["L.bgr_image", "L.image"]`, `["R.bgr_image", "R.image"]` - **Back (B)**: render and tracking views - bindings like `"B.render"`, `"B.tracking_image"` - **Warp previews**: `"L.warp.image"`, `"R.warp.image"` Each image view is created via `image(view, binding, resolution)`, which: - sets `view.bgcolor = "#150000"` - binds an `Image` visual to `binding` through `VisualTree.bind(...)` - uses `scene.PanZoomCamera(aspect=1)` and sets range to the provided `resolution` > Note: A 3D viewport (`viewport(view)`) is wired with frustum and mesh visuals but is currently commented out by default. It binds: > - `Frustum3D` for L/R warp cameras and loaded cameras. > - `ExternalMeshVisual` (`"assets/CatAR v2 Assembly v23.obj"`) and a `Mesh3D("assets/test.obj")` aligned to solver markers. > - `Marker3D` for `"solver.tippoint"`. --- ## 4. Update Loop & Context Contract A Vispy `app.Timer(interval=0.005)` drives `_update(event)`: ```python if self.q is not None and not self.q.empty(): context = self.q.get() self.visual_tree.update(context) # pushes data into bound visuals self.canvas.update() ``` - When first context arrives, the total frame count (`context["frame_length"]`) initializes the replay seek bar. - The current frame index (`context["frame_serial"]`) is used to keep UI in sync. - If `solver.tippoint_b` is present, tip traces are recorded and plotted (micrometers vs time): - X → red, Y → green, Z → blue - time base uses `context["timestamp"]` (fallback to sample count) **Expected context keys (partial, inferred from bindings):** - `frame_length: int`, `frame_serial: int`, `timestamp: float` - `L.image`, `L.bgr_image`, `R.image`, `R.bgr_image` - `L.warp.image`, `R.warp.image`, `L.warp.camera`, `R.warp.camera` - `B.render`, `B.tracking_image` - `solver.tippoint` / `solver.tippoint_b`, `solver.instrument_1`, `solver.marker_points.*` > The exact schema is governed by pipeline/stage outputs; the GUI consumes whatever is present and bound. --- ## 5. Control Panel Widgets ### 5.1 PlaybackControlWidget (Replay mode) - Toolbar with actions: **prev / play / pause / stop / next** - **Seek slider** (pageStep=30) and **index label** `"{k+1} / {total}"`. - Emits to the backend via `command_queue`: - `PLAYBACKCONTROL, PlaybackMode.PLAY|PAUSE|STOP|BACKWARD|FORWARD` - `GOTOFRAME, k` (when seek released) - Debounced scrubbing: UI updates label during drag; command sent on release. ### 5.2 CaptureToolbar (Live mode) - Buttons: **Take Snapshot** and **Start/Stop Recording** (native icons). - On snapshot: - UI flash via overlay effect - `TAKESNAPSHOT` command sent - On record toggle: - Starts a blinking border overlay around the target widget (`canvas.native` by default) - `RECORD, True|False` command sent - Overlay implementation uses: - `QGraphicsOpacityEffect` + `QPropertyAnimation` for flash/blink - An always-on-top child overlay (`QFrame`) following the target’s geometry (updated on `Resize` / `Move` via `eventFilter`) ### 5.3 DatasetSelectorWidget - Shows current dataset path (read-only line edit) and a folder button. - On folder pick → updates path and emits: `CHANGEDATASET, "