From ae4a5ab96de821e5cd1e1d62dff39bdcc32fd17f Mon Sep 17 00:00:00 2001 From: indigo Date: Tue, 9 Jun 2026 08:47:44 +0800 Subject: [PATCH] Refract Manipulator --- README.md | 41 +++++- src/ui/TransformManipulator.h | 243 ++++++++++++---------------------- 2 files changed, 121 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 9c5f3d7..a7f8be8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A C++17 / Windows desktop application providing a **Maya Render Layers-style** USD scene editor using **OpenUSD v25.05**, **ImGui 1.92.7** (docking branch), and **OpenGL 3.3** (via GLAD). Users can open, edit, and save USD files, manage layered stage overrides, inspect and edit prim properties, and interact with 3D scenes through a Hydra viewport. -!["SceenShot"](docs/icons/screenshot_01.png) +![Screenshot](docs/icons/screenshot_01.png) ## Features @@ -96,4 +96,41 @@ src/ | `cmake/modules/FindImgui.cmake` | ImGui source integration | | `cmake/modules/FindGlad.cmake` | GLAD loader setup | | `CMakePresets.json` | Build presets (VS 17 2022, x64) | -| `AGENTS.md` | AI agent build/architecture guide | \ No newline at end of file +| `AGENTS.md` | AI agent build/architecture guide | +| `.kilo/` | Kilo AI configuration (commands, agents, skills) | +| `openspec/changes/` | Feature proposals and implementation tracking | + +## Development Workflow + +This project uses **OpenSpec** for feature development: + +1. **Propose** — Create a new change under `openspec/changes//` with `proposal.md`, `design.md`, `specs/`, and `tasks.md` +2. **Implement** — Work through checkbox-tracked tasks in `tasks.md` +3. **Archive** — Move completed changes to `openspec/archive/` when done + +Use Kilo's `openspec-*` skills to streamline this workflow. + +## Runtime Requirements + +- **Python 3.12** — OpenUSD requires `python312.dll` at runtime. CMake copies it to the output directory via `POST_BUILD` commands. +- **USD Plugin Path** — The app scans `/usd/` for `plugInfo.json` and registers plugins via `pxr::PlugRegistry`. + +## Contributing + +Before writing any OpenUSD API call, verify the API exists in the actual SDK: + +```powershell +findstr /r /s "FunctionName" third_party\OpenUSD_v25.05\include\ +``` + +Always build and install before running tests: + +```powershell +cmake --build build --config Release +cmake --install build --config Release +ctest --test-dir build -C Release +``` + +## License + +See [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/src/ui/TransformManipulator.h b/src/ui/TransformManipulator.h index 3776dc2..18cd695 100644 --- a/src/ui/TransformManipulator.h +++ b/src/ui/TransformManipulator.h @@ -7,205 +7,126 @@ #include #include #include +#include +#include +#include #include namespace UsdLayerManager { class CommandHistory; -/// Active transform tool mode — mirrors Maya Q/W/E/R convention. -enum class ManipulatorMode { - Select, ///< Q — no gizmo, normal click-to-select - Move, ///< W — translate along axis arrows - Rotate, ///< E — rotate around axis rings - Scale ///< R — scale along axis handles -}; +enum class ManipulatorMode { Select, Move, Rotate, Scale }; +enum class TransformSpace { Object, World }; -/// Coordinate space in which the gizmo axes are expressed. -enum class TransformSpace { - Object, ///< Gizmo axes align with the selected prim's local axes (default) - World, ///< Gizmo axes are fixed world-space X/Y/Z -}; - -/// Maya-style interactive transform gizmo. -/// -/// Rendering is done with ImGui DrawList (2-D screen-space overlay), drawn -/// AFTER ImGui::Image() for the viewport — exactly the same approach used by -/// ImGuizmo. No OpenGL resources are needed; the FBO bind/unbind dance is -/// entirely eliminated. -/// -/// Gizmo world-space size is computed each frame using ImGuizmo's screen- -/// factor formula: project a camera-aligned unit vector to clip space and -/// derive the world size that spans a fixed fraction of the screen. This -/// gives constant apparent size regardless of camera distance or FOV. class TransformManipulator { public: TransformManipulator() = default; ~TransformManipulator() = default; - // ----------------------------------------------------------------------- - // Mode - // ----------------------------------------------------------------------- - void SetMode(ManipulatorMode mode) { m_mode = mode; } - ManipulatorMode GetMode() const { return m_mode; } + void SetMode(ManipulatorMode m) { m_mode = m; } + ManipulatorMode GetMode() const { return m_mode; } + void SetTransformSpace(TransformSpace s) { m_transformSpace = s; } + TransformSpace GetTransformSpace() const { return m_transformSpace; } - // ----------------------------------------------------------------------- - // Transform space - // ----------------------------------------------------------------------- - void SetTransformSpace(TransformSpace space) { m_transformSpace = space; } - TransformSpace GetTransformSpace() const { return m_transformSpace; } - - // ----------------------------------------------------------------------- - // Stage / selection - // ----------------------------------------------------------------------- void SetStage(pxr::UsdStageRefPtr stage); void SetSelectedPrim(const pxr::SdfPath& path); - void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } + void SetCommandHistory(CommandHistory* h) { m_commandHistory = h; } - // ----------------------------------------------------------------------- - // Per-frame API (called from ViewportPanel::Render) - // ----------------------------------------------------------------------- + void Render(ImDrawList* dl, const pxr::GfMatrix4d& viewProj, + const pxr::GfVec3d& pivot, const pxr::GfVec3d& cameraEye, + const ImVec2& imagePos, int viewW, int viewH); - /// Draw the gizmo as a 2-D overlay onto @p dl. - /// Call AFTER ImGui::Image() so the overlay appears on top of the scene. - /// @param dl ImGui::GetWindowDrawList() of the Viewport window. - /// @param viewProj Combined view × projection matrix (USD row-major). - /// @param pivot World-space pivot (bounding-box centre of selection). - /// @param cameraEye World-space camera eye position (for half-arc orientation). - /// @param imagePos Screen-space top-left corner of the rendered image. - /// @param viewW/H Viewport pixel dimensions. - void Render(ImDrawList* dl, - const pxr::GfMatrix4d& viewProj, - const pxr::GfVec3d& pivot, - const pxr::GfVec3d& cameraEye, - const ImVec2& imagePos, - int viewW, int viewH); - - /// Process mouse input. Must be called BEFORE camera-drag / prim-pick - /// logic in ViewportPanel so the gizmo can consume LMB clicks first. - /// @return true if the gizmo consumed the event. bool HandleInput(const pxr::GfMatrix4d& viewProj, - const pxr::GfVec3d& pivot, - const pxr::GfVec3d& cameraEye, - const ImVec2& imagePos, - int viewW, int viewH, + const pxr::GfVec3d& pivot, const pxr::GfVec3d& cameraEye, + const ImVec2& imagePos, int viewW, int viewH, bool viewportHovered); bool IsDragging() const { return m_isDragging; } private: - // ----------------------------------------------------------------------- - // ImGuizmo-style screen-factor computation - // ----------------------------------------------------------------------- - - /// Compute the world-space gizmo size so that the gizmo spans - /// @p desiredFraction of the smaller viewport dimension in NDC. - /// - /// Algorithm (from ImGuizmo): - /// 1. Project @p pivot to clip space. - /// 2. Project @p pivot + each world axis unit vector to clip space. - /// 3. Measure clip-space length (aspect-ratio corrected). - /// 4. screenFactor = desiredFraction / maxClipLen. - static float ComputeScreenFactor(const pxr::GfMatrix4d& viewProj, - const pxr::GfVec3d& pivot, - int viewW, int viewH, - float desiredFraction = 0.15f); - - // ----------------------------------------------------------------------- - // Screen-space helpers - // ----------------------------------------------------------------------- - - /// Project a world-space point to absolute screen coordinates. - /// Returns false if the point is behind the camera (w ≤ 0). - static bool WorldToScreen(const pxr::GfVec3d& world, - const pxr::GfMatrix4d& viewProj, - int viewW, int viewH, - const ImVec2& imagePos, - ImVec2& outScreen); - - /// Distance from point @p p to line segment @p a – @p b (2-D). + static float ComputeScreenFactor(const pxr::GfMatrix4d& vp, + const pxr::GfVec3d& pivot, + int vW, int vH, float frac = 0.15f); + static bool WorldToScreen(const pxr::GfVec3d& world, + const pxr::GfMatrix4d& vp, + int vW, int vH, + const ImVec2& imgPos, ImVec2& out); static float PointToSegmentDist(ImVec2 p, ImVec2 a, ImVec2 b); + static pxr::GfRay ComputeMouseRay(const pxr::GfMatrix4d& vp, + const ImVec2& mouse, + const ImVec2& imgPos, int vW, int vH); + + void DrawMoveGizmo (ImDrawList*, const pxr::GfMatrix4d&, const pxr::GfVec3d&, + float, const ImVec2&, int, int, const pxr::GfVec3d[3]); + void DrawRotateGizmo(ImDrawList*, const pxr::GfMatrix4d&, const pxr::GfVec3d&, + float, const pxr::GfVec3d&, const ImVec2&, int, int, + const pxr::GfVec3d[3]); + void DrawScaleGizmo (ImDrawList*, const pxr::GfMatrix4d&, const pxr::GfVec3d&, + float, const ImVec2&, int, int, const pxr::GfVec3d[3]); + + int HitTestAxes(const pxr::GfMatrix4d&, const pxr::GfVec3d&, float, + const ImVec2&, int, int, const ImVec2&, + const pxr::GfVec3d[3]) const; + int HitTestRotateRings(const pxr::GfMatrix4d&, const pxr::GfVec3d&, float, + const pxr::GfVec3d&, const ImVec2&, int, int, + const ImVec2&, const pxr::GfVec3d[3]) const; + + void ApplyTranslate(const pxr::GfVec3d&); + void ApplyRotate(const pxr::GfVec3f&, pxr::UsdGeomXformCommonAPI::RotationOrder); + void ApplyScale(const pxr::GfVec3f&); + void GetGizmoAxes(pxr::GfVec3d[3]) const; // ----------------------------------------------------------------------- - // Per-mode drawing (all ImGui DrawList, screen-space) - // ----------------------------------------------------------------------- - void DrawMoveGizmo (ImDrawList* dl, const pxr::GfMatrix4d& vp, - const pxr::GfVec3d& pivot, float sf, - const ImVec2& imgPos, int vW, int vH, - const pxr::GfVec3d axes[3]); - void DrawRotateGizmo(ImDrawList* dl, const pxr::GfMatrix4d& vp, - const pxr::GfVec3d& pivot, float sf, - const pxr::GfVec3d& cameraEye, - const ImVec2& imgPos, int vW, int vH, - const pxr::GfVec3d axes[3]); - void DrawScaleGizmo (ImDrawList* dl, const pxr::GfMatrix4d& vp, - const pxr::GfVec3d& pivot, float sf, - const ImVec2& imgPos, int vW, int vH, - const pxr::GfVec3d axes[3]); - - // ----------------------------------------------------------------------- - // Hit-testing - // ----------------------------------------------------------------------- - - /// Returns axis index 0=X 1=Y 2=Z, or -1 if nothing hit (move / scale). - int HitTestAxes(const pxr::GfMatrix4d& vp, - const pxr::GfVec3d& pivot, float sf, - const ImVec2& imgPos, int vW, int vH, - const ImVec2& mousePosAbsolute, - const pxr::GfVec3d axes[3]) const; - - /// Returns axis index 0=X 1=Y 2=Z, or -1 if nothing hit (rotate rings). - /// Uses proximity to the VISIBLE front-facing half-arc only. - int HitTestRotateRings(const pxr::GfMatrix4d& vp, - const pxr::GfVec3d& pivot, float sf, - const pxr::GfVec3d& cameraEye, - const ImVec2& imgPos, int vW, int vH, - const ImVec2& mousePosAbsolute, - const pxr::GfVec3d axes[3]) const; - - // ----------------------------------------------------------------------- - // USD transform write helpers - // ----------------------------------------------------------------------- - void ApplyMoveDelta (const pxr::GfVec3d& worldDelta); - void ApplyRotateDelta(int axisIndex, float angleDeg); - void ApplyScaleDelta (int axisIndex, float factor); - - /// Fills @p outAxes[3] with the gizmo X/Y/Z axis directions in world space. - /// In World space: fixed unit vectors. - /// In Object space: the prim's local axes extracted from its local-to-world matrix. - void GetGizmoAxes(pxr::GfVec3d outAxes[3]) const; - - // ----------------------------------------------------------------------- - // State - // ----------------------------------------------------------------------- - ManipulatorMode m_mode = ManipulatorMode::Select; + ManipulatorMode m_mode = ManipulatorMode::Select; TransformSpace m_transformSpace = TransformSpace::Object; pxr::UsdStageRefPtr m_stage; pxr::SdfPath m_primPath; CommandHistory* m_commandHistory = nullptr; - // Drag state bool m_isDragging = false; int m_dragAxis = -1; - ImVec2 m_dragLastPos = {0.f, 0.f}; + int m_hoveredAxis = -1; - // For rotation drag: screen-angle around projected pivot center - float m_dragRotateLastAngle = 0.f; ///< atan2 angle of mouse around pivot (radians) + // ---- Translate drag --------------------------------------------------- + pxr::GfLine m_dragMoveAxisLine; + pxr::GfVec3d m_dragMoveOriginOnAxis; - // Saved xform at drag START (never mutated during drag — used for undo) - pxr::GfVec3d m_dragOriginalTranslate = {0.0, 0.0, 0.0}; - pxr::GfVec3f m_dragOriginalRotate = {0.f, 0.f, 0.f}; - pxr::GfVec3f m_dragOriginalScale = {1.f, 1.f, 1.f}; + // ---- Rotate drag (usdtweak clock-hand, extended with space-switch) ---- + // + // Ring plane: world-space ring normal (for mouse-ray intersection + sign). + // + // Delta-rotation axis differs by TransformSpace: + // Object => initRot.GetRow3(axis) -- local axis in PARENT space + // (exact usdtweak localPlaneNormal convention) + // World => world unit vector + // (correct for root-level / simple hierarchies) + // + // resultingRotation formula: + // Object => GfMatrix4d(1).SetRotate(delta) * initRot + // (usdtweak: delta applied in local frame before initRot) + // World => initRot * GfMatrix4d(1).SetRotate(delta) + // (USD row-vector: initRot maps local->parent, then world delta) + pxr::GfVec3d m_dragRotatePlaneNormal; // world-space + pxr::GfVec3d m_dragRotateDeltaAxis; // space-dependent (see above) + pxr::GfVec3d m_dragRotateFrom; // clock-hand at drag start + pxr::GfMatrix4d m_dragRotateInitialRotMat; // local->parent rot at drag start + bool m_dragRotateObjectSpace = true; + + // ---- Scale drag ------------------------------------------------------- + pxr::GfLine m_dragScaleAxisLine; + pxr::GfVec3d m_dragScaleOriginOnAxis; + + // ---- Snapshot --------------------------------------------------------- + pxr::GfVec3d m_dragOriginalTranslate = {0,0,0}; + pxr::GfVec3f m_dragOriginalRotate = {0,0,0}; + pxr::GfVec3f m_dragOriginalScale = {1,1,1}; pxr::UsdGeomXformCommonAPI::RotationOrder m_dragOriginalRotOrder = pxr::UsdGeomXformCommonAPI::RotationOrderXYZ; - // Working accumulator for the current drag (updated each frame) - pxr::GfVec3d m_dragStartTranslate = {0.0, 0.0, 0.0}; - pxr::GfVec3f m_dragStartRotate = {0.f, 0.f, 0.f}; - pxr::GfVec3f m_dragStartScale = {1.f, 1.f, 1.f}; - - // Hover highlight - int m_hoveredAxis = -1; + pxr::GfVec3d m_dragStartTranslate = {0,0,0}; + pxr::GfVec3f m_dragStartRotate = {0,0,0}; + pxr::GfVec3f m_dragStartScale = {1,1,1}; }; } // namespace UsdLayerManager