## 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. |