## Context The app currently has a single `ViewportPanel` class that owns one `ViewportCamera`, one `UsdSceneRenderer`, and draws a full-window render with toolbars + overlays. Selection propagates through `OnPrimPicked` / `OnPrimsPickedRect` callbacks to `SceneHierarchyPanel` and `PropertyPanel`. The `UsdSceneRenderer` owns a `GlfDrawTarget` FBO; the render loop calls `m_renderer.Render()` then renders overlays (axis, bbox, gizmo, manipulator toolbar) into the same FBO or directly to the screen via ImGui draw lists. The `ViewportCamera` and `UsdSceneRenderer` classes are already designed as independent units — they can be instantiated per-viewport without modification. The key architectural challenge is sharing the selection state across N viewport tiles while ensuring only the focused tile renders the transform gizmo. ## Goals / Non-Goals **Goals:** - Support 1, 2 (horizontal/vertical split), and 4-quadrant viewport layouts - Each viewport tile has its own camera, renderer, and per-viewport settings (grid, AA, bbox mode, background color, render delegate) - Single shared selection: picking in any tile updates all tiles and propagates to panels - Transform manipulator (gizmo) renders only in the focused tile - Drag-based dividers to resize tiles within a layout - Backward-compatible `Application` API: `m_viewportPanel` stays as the entry point - Space-key maximize/restore: pressing Space over the hovered tile temporarily fills the entire viewport area; pressing Space again restores the previous layout (Maya-style) **Non-Goals:** - Not adding tear-off/undockable viewport windows (ImGui docking handles that separately) - Not adding per-viewport layer visibility overrides (future feature) - Not supporting more than 4 tiles (can be extended later) - Not adding viewport sync (playblasting) or timeline scrubbing across tiles ## Decisions 1. **Extract `ViewportTile` from `ViewportPanel` rather than subclassing.** - The existing 1100-line `ViewportPanel::Render()` method contains everything: toolbar rendering, camera resolution, scene rendering, overlay drawing, context menu, and manipulator. Extracting a `ViewportTile` class that owns a camera + renderer + local per-viewport settings + the subset of overlay logic that is per-tile keeps the container clean. - `ViewportPanel` becomes the container: it owns `std::vector>`, manages layout geometry, and delegates `Render()` calls to each tile. - Alternative considered: having `ViewportPanel` contain N inline structs. This was rejected because tiles need their own camera/resolution/overlay lifecycle, which calls for a proper class. 2. **Selection lives on the container (`ViewportPanel`), not on any tile.** - Each tile still calls `m_renderer.SetSelectedPaths()` and `m_renderer.AddSelected()` so its `UsdImagingGLEngine` highlights the correct prims, but the authoritative `m_selectedSdfPaths` / `m_selectedPrimPath` vector lives on `ViewportPanel`. - When a tile receives a pick hit, it calls back into the container to update the shared selection, and the container broadcasts it to all tiles and to the `OnPrimPicked`/`OnPrimsPickedRect` callbacks. - Alternative considered: each tile owns its selection and syncs via signals. Rejected because it adds complexity for a single-authority selection model. 3. **Manipulator ownership moves to `ViewportPanel` (container), not per tile.** - The `TransformManipulator` is a singleton — there should never be two gizmos active. The container owns it and only passes it to the focused tile for rendering. - The focused tile is the one that last received a `ImGui::IsWindowHovered()` click. Tile tracks `m_isFocused` set by container during `HandleInput()`. 4. **Layout state stored as simple enum + divider positions, no external dependency.** - Layout enum values: `Single`, `HSplit` (2 tiles side-by-side), `VSplit` (2 tiles top-bottom), `Quad` (4 tiles). - Divider positions stored as normalized floats (`0.0–1.0`). Dragging a divider updates the normalized position, then `ViewportPanel::Render()` recomputes tile rectangles. - No XML/JSON config for layout — state is ephemeral (could be persisted via future settings system). - Alternative considered: `ImGui::Splitter()` based approach from ImGui demos. We'll implement a lightweight version directly since we need precise control over tile rectangle subdivision. 5. **Render delegate can differ per tile.** - Each tile's `UsdSceneRenderer` independently calls `SetRendererPlugin()`. This means each tile FBO can use e.g. Storm vs HdEmbree independently. - This is useful for comparing renderers side-by-side and is already supported by `UsdSceneRenderer`'s per-instance design. 6. **Space-key maximize stored as a simple toggle on the container, not on individual tiles.** - `ViewportPanel` stores `m_maximizedTileIndex` (-1 = not maximized) and `m_layoutBeforeMaximize` (the `LayoutMode` enum value before Space was pressed). - When Space is pressed (with tile hovered and `m_maximizedTileIndex == -1`): save `m_layoutBeforeMaximize`, set `m_maximizedTileIndex` to the hovered tile, and render only that tile filling the entire viewport area (no dividers, no layout menu). - When Space is pressed again (or Escape): restore `m_maximizedTileIndex` to -1 and re-render the saved layout. - Keyboard shortcuts (F, A, Q/W/E/R) continue to target the maximized tile as the de facto focused tile. - Alternative considered: pushing a separate layout mode `Maximized`. Rejected because it conflates layout state with a transient display mode; storing the pre-maximize layout explicitly is cleaner and avoids state machine complexity. 7. **Per-tile settings stored on `ViewportTile`, not on a settings struct singleton.** - Grid/AA/bg-color/bbox-mode/render-delegate are all fields on `ViewportTile`. The compact toolbar in each tile reads/writes these directly. - For layout persistence (future), tiles could serialize their settings. ## Risks / Trade-offs | Risk | Mitigation | |---|---| | N renderers means N FBO draw targets, each at full viewport resolution. For a 4K monitor with Quad layout, each tile is ~960×540 → 4× FBO allocations at that size. GPU memory may spike. | Cap layout to 4 tiles. Add a `m_renderScale` per tile (default 1.0) if perf issues arise. OpenUSD's `UsdImagingGLEngine` already has `renderParams` for resolution scaling. | | Input routing: multiple tiles competing for Alt+LMB, Alt+RMB, scroll, etc. | The container dispatches `HandleInput()` only to the **hovered** tile each frame. The focused tile's manipulator is the only one that can consume input. | | Right-click context menu: which tile owns it? | The context menu appears on the hovered tile. The popup (`ViewportContextMenuN`) is opened per-tile using unique IDs. | | Divider drag area is thin — hard to click on high-DPI. | Dividers are 6px wide with a 2px visual line in the centre. Cursor changes to resize cursor on hover. | | Keyboard shortcuts (F=frame, A=frame all, Q/W/E/R=manipulator) need to target the focused tile. | Shortcuts in the container delegate to the focused tile's camera/manipulator. If no tile is focused (e.g. user just clicked a panel outside the viewport), shortcuts are ignored. | | Space key might conflict with ImGui docking or text input. | Guard with `!io.WantTextInput` (same as existing F/A shortcuts). Space is not a default ImGui docking shortcut, so no conflict with docking. | | User might accidentally press Space when no tile is hovered (e.g. focus is on a panel). | Space is a no-op when no tile is hovered and viewport is not maximized. If already maximized, Space always restores regardless of hover state. |