71 lines
7.6 KiB
Markdown
71 lines
7.6 KiB
Markdown
## 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<std::unique_ptr<ViewportTile>>`, 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. | |