## 1. UsdSceneRenderer — DrawCameraWireframes - [ ] 1.1 Add to `UsdSceneRenderer.h`: `void DrawCameraWireframes(pxr::UsdStageRefPtr stage, const pxr::SdfPathVector& selectedPaths, const pxr::SdfPath& activeCameraPath, const pxr::GfMatrix4d& viewProjMatrix, double viewportCameraDist)` and private helpers `BuildCameraWireframeLines(const pxr::GfCamera& gfCam, double scale, std::vector& outVerts)` and `pxr::SdfPathVector m_cachedCameraPaths` + `bool m_cameraCacheDirty = true` - [ ] 1.2 In `UsdSceneRenderer.cpp` implement `DrawCameraWireframes`: - If `m_cameraCacheDirty`, traverse stage for `UsdGeomCamera` prims and cache paths; clear dirty flag - For each cached camera path, resolve world transform + lens params via `UsdGeomCamera::GetCamera(UsdTimeCode::Default())` → `GfCamera` - Compute `frustumScale = std::min(viewportCameraDist * 0.12, 50.0)` - Call `BuildCameraWireframeLines` to produce a `std::vector` of XYZ vertex pairs (GL_LINES) - Determine colour: selected → `(1.0,0.75,0.1,1.0)`, active → `(0.2,0.9,1.0,1.0)`, inactive → `(0.55,0.55,0.55,0.85)` - `BindDrawTarget()`, draw all segments using the existing axis GLSL program + a dynamic VBO (same pattern as `DrawBoundingBoxes`), `UnbindDrawTarget()` - [ ] 1.3 Implement `BuildCameraWireframeLines`: given `GfCamera` and scale, produce: - **Body box** — 12 edges of a small box (width × height × depth = scale×0.15 each) centred at camera origin in local space, transformed to world space by the camera's transform matrix - **Frustum pyramid** — 4 lines from camera origin to each corner of a near-display quad at depth `max(nearClip, 0.01)`, scaled so the quad width/height match `horizontalAperture/focalLength * nearDepth` (perspective divide) - **Up arrow** — one line from body-box top-centre upward by `scale * 0.2` along camera local Y - [ ] 1.4 Set `m_cameraCacheDirty = true` inside `SetStage` (already exists) and inside the `SetForceRefresh(true)` path ## 2. UsdSceneRenderer — PickCameraAtPoint - [ ] 2.1 Add to `UsdSceneRenderer.h`: `bool PickCameraAtPoint(pxr::UsdStageRefPtr stage, const ImVec2& mousePosAbsolute, const pxr::GfMatrix4d& viewProjMatrix, const ImVec2& imagePos, int viewW, int viewH, double viewportCameraDist, pxr::SdfPath* outCameraPath)` - [ ] 2.2 In `UsdSceneRenderer.cpp` implement `PickCameraAtPoint`: - Iterate `m_cachedCameraPaths` (build cache if needed) - For each camera: build wireframe vertex list via `BuildCameraWireframeLines` - Project each vertex pair to screen space using the same `WorldToScreen` math as `TransformManipulator` - Compute `PointToSegmentDist` for each segment against `mousePosAbsolute` - Track minimum distance and corresponding path; if `< kCameraPickRadius (10 px)` return true with that path - If tie between multiple cameras, return the one with the smallest minimum distance ## 3. ViewportPanel — Integration - [ ] 3.1 In `ViewportPanel.h`: add `pxr::SdfPath GetActiveCameraPath() const` (returns `m_camera.GetUsdCameraPath()` when in UsdCamera mode, else `SdfPath()`) — check `ViewportCamera` API for the getter name - [ ] 3.2 In `ViewportPanel.cpp` `Render()`: after the `DrawBoundingBoxes` call, add: ```cpp m_renderer.DrawCameraWireframes(m_stage, m_selectedSdfPaths, m_camera.GetUsdCameraPath(), viewProj, m_camera.GetDist()); ``` (Use the local `viewProj` already computed in that scope) - [ ] 3.3 In `ViewportPanel.cpp` `HandleInput()`, single-click path (before the existing `PickObject` call): add camera pick: ```cpp pxr::SdfPath camPath; if (m_renderer.PickCameraAtPoint(m_stage, mouse, viewProj, m_imageScreenPos, m_viewWidth, m_viewHeight, m_camera.GetDist(), &camPath)) { // fire selection callbacks with camPath } ``` Only fall through to `PickObject` if `PickCameraAtPoint` returns false ## 4. Build and Verification - [ ] 4.1 Run `cmake --preset default` (only needed if new `.cpp` files were added; all changes here are in existing files so this may be skipped) then `cmake --build build --config Release` — resolve any compilation errors - [ ] 4.2 Install and launch `install/bin/App.exe`; create a `Camera` prim via Stage menu, verify its frustum wireframe appears in the viewport at the world origin - [ ] 4.3 Verify clicking the camera wireframe selects the prim in the Scene Hierarchy panel - [ ] 4.4 Verify the Move (W) gizmo appears on the selected camera and dragging repositions it; the wireframe moves to match - [ ] 4.5 Switch the viewport to that camera via the camera toolbar; verify the wireframe turns cyan - [ ] 4.6 With the camera active in the toolbar, click it in the viewport; verify it turns yellow-orange (selected overrides active colour)