From c4bf9673a87c28bfc3cfcaf7204256e52f9fc6b0 Mon Sep 17 00:00:00 2001 From: indigo Date: Sat, 13 Jun 2026 11:48:56 +0800 Subject: [PATCH] Timeline Prototype --- resources/icons/pause.svg | 3 + resources/icons/play.svg | 3 + resources/icons/skip-back.svg | 3 + resources/icons/skip-end.svg | 3 + resources/icons/step-back.svg | 3 + resources/icons/step-forward.svg | 3 + src/core/UsdSceneRenderer.cpp | 10 +- src/core/UsdSceneRenderer.h | 8 ++ src/core/commands/TransformCommand.cpp | 93 +++++++++++++++-- src/core/commands/TransformCommand.h | 29 ++++++ src/ui/Application.cpp | 18 +++- src/ui/Application.h | 2 + src/ui/IconManager.cpp | 7 ++ src/ui/IconManager.h | 7 ++ src/ui/PropertyPanel.cpp | 78 ++++++++++----- src/ui/PropertyPanel.h | 15 +++ src/ui/TimelinePanel.cpp | 132 +++++++++++++++++++++++++ src/ui/TimelinePanel.h | 46 +++++++++ src/ui/TransformManipulator.cpp | 23 +++-- src/ui/TransformManipulator.h | 17 ++++ src/ui/ViewportPanel.cpp | 10 ++ src/ui/ViewportPanel.h | 7 ++ src/ui/ViewportTile.cpp | 31 +++--- src/ui/ViewportTile.h | 8 ++ 24 files changed, 501 insertions(+), 58 deletions(-) create mode 100644 resources/icons/pause.svg create mode 100644 resources/icons/play.svg create mode 100644 resources/icons/skip-back.svg create mode 100644 resources/icons/skip-end.svg create mode 100644 resources/icons/step-back.svg create mode 100644 resources/icons/step-forward.svg create mode 100644 src/ui/TimelinePanel.cpp create mode 100644 src/ui/TimelinePanel.h diff --git a/resources/icons/pause.svg b/resources/icons/pause.svg new file mode 100644 index 0000000..1c6b5f2 --- /dev/null +++ b/resources/icons/pause.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/play.svg b/resources/icons/play.svg new file mode 100644 index 0000000..8b30bfd --- /dev/null +++ b/resources/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/skip-back.svg b/resources/icons/skip-back.svg new file mode 100644 index 0000000..b6e7ece --- /dev/null +++ b/resources/icons/skip-back.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/skip-end.svg b/resources/icons/skip-end.svg new file mode 100644 index 0000000..4fe9e44 --- /dev/null +++ b/resources/icons/skip-end.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/step-back.svg b/resources/icons/step-back.svg new file mode 100644 index 0000000..5aa2520 --- /dev/null +++ b/resources/icons/step-back.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/step-forward.svg b/resources/icons/step-forward.svg new file mode 100644 index 0000000..a5e1839 --- /dev/null +++ b/resources/icons/step-forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/core/UsdSceneRenderer.cpp b/src/core/UsdSceneRenderer.cpp index a33e78b..843ba03 100644 --- a/src/core/UsdSceneRenderer.cpp +++ b/src/core/UsdSceneRenderer.cpp @@ -356,7 +356,7 @@ pxr::GfRange3d UsdSceneRenderer::ComputeStageBounds() if (!m_stage) return {}; pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; - pxr::UsdGeomBBoxCache bboxCache(pxr::UsdTimeCode::Default(), purposes, true); + pxr::UsdGeomBBoxCache bboxCache(m_currentTime, purposes, true); return bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot()).ComputeAlignedRange(); } @@ -594,7 +594,7 @@ void UsdSceneRenderer::Render(int width, int height) // --- Render params (matches stageView.renderSinglePass) --- m_renderParams = pxr::UsdImagingGLRenderParams(); - m_renderParams.frame = pxr::UsdTimeCode::Default(); + m_renderParams.frame = m_currentTime; m_renderParams.complexity = 1.0f; m_renderParams.drawMode = pxr::UsdImagingGLDrawMode::DRAW_SHADED_SMOOTH; m_renderParams.showGuides = true; @@ -1101,7 +1101,7 @@ void UsdSceneRenderer::DrawBoundingBoxes( pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; pxr::UsdGeomBBoxCache bboxCache( - pxr::UsdTimeCode::Default(), purposes, /*useExtentsHint=*/true); + m_currentTime, purposes, /*useExtentsHint=*/true); // Save/restore GL state GLboolean prevDepthMask; @@ -1350,7 +1350,7 @@ void UsdSceneRenderer::DrawCameraWireframes( if (!prim) continue; pxr::UsdGeomCamera usdCam(prim); - pxr::GfCamera gfCam = usdCam.GetCamera(pxr::UsdTimeCode::Default()); + pxr::GfCamera gfCam = usdCam.GetCamera(m_currentTime); // Determine colour bool isSelected = (selectedSet.count(camPath.GetString()) > 0); @@ -1430,7 +1430,7 @@ bool UsdSceneRenderer::PickCameraAtPoint( if (!prim) continue; pxr::UsdGeomCamera usdCam(prim); - pxr::GfCamera gfCam = usdCam.GetCamera(pxr::UsdTimeCode::Default()); + pxr::GfCamera gfCam = usdCam.GetCamera(m_currentTime); std::vector verts; BuildCameraWireframeLines(gfCam, scale, verts); diff --git a/src/core/UsdSceneRenderer.h b/src/core/UsdSceneRenderer.h index 16d3e71..9d8549e 100644 --- a/src/core/UsdSceneRenderer.h +++ b/src/core/UsdSceneRenderer.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -163,6 +164,11 @@ public: if (val) m_cameraCacheDirty = true; } + /// Current evaluation time (timeline frame). Drives Hydra rendering, + /// USD camera resolution, and bounding-box computation. + pxr::UsdTimeCode GetCurrentTimeCode() const { return m_currentTime; } + void SetCurrentTimeCode(pxr::UsdTimeCode t) { m_currentTime = t; } + // ----------------------------------------------------------------------- // Bounding box display // ----------------------------------------------------------------------- @@ -267,6 +273,8 @@ private: bool m_rendererInitialized; bool m_forceRefresh; + pxr::UsdTimeCode m_currentTime = pxr::UsdTimeCode::Default(); + int m_diagFrameCount; // --- Grid GL resources (line geometry, reuses m_axisProgram / m_axisUniformMVP) --- diff --git a/src/core/commands/TransformCommand.cpp b/src/core/commands/TransformCommand.cpp index 941bfbf..ea2427e 100644 --- a/src/core/commands/TransformCommand.cpp +++ b/src/core/commands/TransformCommand.cpp @@ -1,9 +1,52 @@ #include "TransformCommand.h" #include "../../utils/Logger.h" #include +#include namespace UsdLayerManager { +// True when attr has an authored time sample exactly at time t. +static bool HasSampleAtTime(const pxr::UsdAttribute& attr, double t) +{ + if (!attr) return false; + std::vector times; + if (!attr.GetTimeSamples(×)) return false; + for (double s : times) + if (s == t) return true; + return false; +} + +// Attribute name of the rotate op XformCommonAPI authors for a rotation order +// (e.g. "xformOp:rotateXYZ"). +static pxr::TfToken RotateOpName(pxr::UsdGeomXformCommonAPI::RotationOrder ro) +{ + return pxr::UsdGeomXformOp::GetOpName( + pxr::UsdGeomXformCommonAPI::ConvertRotationOrderToOpType(ro)); +} + +TransformCommand::SamplePresence TransformCommand::QuerySamplePresence( + pxr::UsdStageRefPtr stage, + const pxr::SdfPath& primPath, + pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder, + pxr::UsdTimeCode timeCode) +{ + SamplePresence presence; + if (timeCode.IsDefault() || !stage) return presence; + pxr::UsdPrim prim = stage->GetPrimAtPath(primPath); + if (!prim) return presence; + + double t = timeCode.GetValue(); + presence.translate = HasSampleAtTime( + prim.GetAttribute(pxr::UsdGeomXformOp::GetOpName( + pxr::UsdGeomXformOp::TypeTranslate)), t); + presence.rotate = HasSampleAtTime( + prim.GetAttribute(RotateOpName(rotOrder)), t); + presence.scale = HasSampleAtTime( + prim.GetAttribute(pxr::UsdGeomXformOp::GetOpName( + pxr::UsdGeomXformOp::TypeScale)), t); + return presence; +} + TransformCommand::TransformCommand(pxr::UsdStageRefPtr stage, const pxr::SdfPath& primPath, pxr::SdfLayerHandle editLayer, @@ -14,6 +57,8 @@ TransformCommand::TransformCommand(pxr::UsdStageRefPtr stage, const pxr::GfVec3f& newRotate, const pxr::GfVec3f& newScale, pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder, + pxr::UsdTimeCode timeCode, + SamplePresence preEditSamples, std::string description) : m_stage(stage) , m_primPath(primPath) @@ -25,7 +70,9 @@ TransformCommand::TransformCommand(pxr::UsdStageRefPtr stage, , m_newRotate(newRotate) , m_newScale(newScale) , m_rotOrder(rotOrder) + , m_timeCode(timeCode) , m_description(std::move(description)) + , m_preEditSamples(preEditSamples) { } @@ -35,6 +82,7 @@ void TransformCommand::Execute() { void TransformCommand::Undo() { Apply(m_oldTranslate, m_oldRotate, m_oldScale); + ClearCreatedSamples(); } void TransformCommand::Apply(const pxr::GfVec3d& t, @@ -51,17 +99,50 @@ void TransformCommand::Apply(const pxr::GfVec3d& t, try { if (m_editLayer) { pxr::UsdEditContext ec(m_stage, m_editLayer); - api.SetTranslate(t, pxr::UsdTimeCode::Default()); - api.SetRotate(r, m_rotOrder, pxr::UsdTimeCode::Default()); - api.SetScale(s, pxr::UsdTimeCode::Default()); + api.SetTranslate(t, m_timeCode); + api.SetRotate(r, m_rotOrder, m_timeCode); + api.SetScale(s, m_timeCode); } else { - api.SetTranslate(t, pxr::UsdTimeCode::Default()); - api.SetRotate(r, m_rotOrder, pxr::UsdTimeCode::Default()); - api.SetScale(s, pxr::UsdTimeCode::Default()); + api.SetTranslate(t, m_timeCode); + api.SetRotate(r, m_rotOrder, m_timeCode); + api.SetScale(s, m_timeCode); } } catch (const std::exception& e) { LOG_ERROR(std::string("TransformCommand::Apply error: ") + e.what()); } } +void TransformCommand::ClearCreatedSamples() +{ + if (m_timeCode.IsDefault()) return; + if (!m_stage || m_primPath.IsEmpty()) return; + pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath); + if (!prim) return; + + auto clearOp = [&](const pxr::TfToken& opName, bool hadSample) { + if (hadSample) return; + pxr::UsdAttribute attr = prim.GetAttribute(opName); + if (attr) attr.ClearAtTime(m_timeCode); + }; + + auto clearAll = [&]() { + clearOp(pxr::UsdGeomXformOp::GetOpName( + pxr::UsdGeomXformOp::TypeTranslate), m_preEditSamples.translate); + clearOp(RotateOpName(m_rotOrder), m_preEditSamples.rotate); + clearOp(pxr::UsdGeomXformOp::GetOpName( + pxr::UsdGeomXformOp::TypeScale), m_preEditSamples.scale); + }; + + try { + if (m_editLayer) { + pxr::UsdEditContext ec(m_stage, m_editLayer); + clearAll(); + } else { + clearAll(); + } + } catch (const std::exception& e) { + LOG_ERROR(std::string("TransformCommand::ClearCreatedSamples error: ") + e.what()); + } +} + } // namespace UsdLayerManager diff --git a/src/core/commands/TransformCommand.h b/src/core/commands/TransformCommand.h index 9d47015..faa99fd 100644 --- a/src/core/commands/TransformCommand.h +++ b/src/core/commands/TransformCommand.h @@ -2,6 +2,7 @@ #include "../CommandHistory.h" #include +#include #include #include #include @@ -14,8 +15,26 @@ namespace UsdLayerManager { /// Reverses a complete TRS edit on a single prim (from gizmo drag or /// Property panel field commit). Stores the prim path, edit-target layer, /// and pre/post translate/rotate/scale triples. +/// +/// When timeCode is not Default() the edit is a keyframe: values are written +/// as time samples at that frame. Undo removes samples this command created +/// on ops that had no sample at that frame before the edit. class TransformCommand : public ICommand { public: + /// Whether each common op had an authored sample at the edit time. + /// Must be queried BEFORE the first live write of an interactive edit + /// (live writes author the sample, so querying later always finds one). + struct SamplePresence { + bool translate = false; + bool rotate = false; + bool scale = false; + }; + static SamplePresence QuerySamplePresence( + pxr::UsdStageRefPtr stage, + const pxr::SdfPath& primPath, + pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder, + pxr::UsdTimeCode timeCode); + TransformCommand(pxr::UsdStageRefPtr stage, const pxr::SdfPath& primPath, pxr::SdfLayerHandle editLayer, @@ -26,6 +45,8 @@ public: const pxr::GfVec3f& newRotate, const pxr::GfVec3f& newScale, pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder, + pxr::UsdTimeCode timeCode, + SamplePresence preEditSamples, std::string description = "Transform"); void Execute() override; @@ -36,6 +57,9 @@ private: void Apply(const pxr::GfVec3d& t, const pxr::GfVec3f& r, const pxr::GfVec3f& s); + /// Remove the time samples Execute created on ops that previously had + /// no sample at m_timeCode (no-op when m_timeCode is Default). + void ClearCreatedSamples(); pxr::UsdStageRefPtr m_stage; pxr::SdfPath m_primPath; @@ -50,7 +74,12 @@ private: pxr::GfVec3f m_newScale; pxr::UsdGeomXformCommonAPI::RotationOrder m_rotOrder; + pxr::UsdTimeCode m_timeCode; std::string m_description; + + // Whether each op already had a sample at m_timeCode before the edit + // (only meaningful when m_timeCode is not Default). + SamplePresence m_preEditSamples; }; } // namespace UsdLayerManager diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index 285974e..c693eb4 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -69,11 +69,19 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig m_propertyPanel->SetPropertyManager(m_propertyManager.get()); m_propertyPanel->SetCommandHistory(&m_commandHistory); + m_timelinePanel = std::make_unique(); + m_timelinePanel->OnTimeChanged = [this](pxr::UsdTimeCode displayTime, + pxr::UsdTimeCode editTime) { + m_viewportPanel->SetTimeCodes(displayTime, editTime); + m_propertyPanel->SetTimeCodes(displayTime, editTime); + }; + // Initialize IconManager — must happen after OpenGL context is ready (ImGui init above). m_iconManager = std::make_unique(); m_iconManager->Initialize(ResourcePath("resources/icons"), 24); m_sceneHierarchyPanel->SetIconManager(m_iconManager.get()); m_viewportPanel->SetIconManager(m_iconManager.get()); + m_timelinePanel->SetIconManager(m_iconManager.get()); m_sceneHierarchyPanel->SetOnPrimSelected( [this](const std::string& path) { @@ -155,19 +163,23 @@ void Application::RefreshManagers() { m_viewportPanel->SetStage(stage); m_viewportPanel->FrameScene(); m_propertyPanel->SetStage(stage); + m_timelinePanel->SetStage(stage); } else { m_layerManager->SetStage(nullptr); m_propertyManager->SetStage(nullptr); m_sceneHierarchyPanel->SetStage(nullptr); m_viewportPanel->SetStage(nullptr); m_propertyPanel->SetStage(nullptr); + m_timelinePanel->SetStage(nullptr); } } void Application::Update() { + ImGuiIO& io = ImGui::GetIO(); + m_timelinePanel->Update(io.DeltaTime); + // Process undo/redo hotkeys (Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z). // Only fire when no ImGui text-input widget has keyboard focus. - ImGuiIO& io = ImGui::GetIO(); if (!io.WantTextInput) { if (io.KeyCtrl && !io.KeyShift && ImGui::IsKeyPressed(ImGuiKey_Z, false)) { m_commandHistory.Undo(); @@ -211,6 +223,10 @@ void Application::RenderUI() { m_propertyPanel->Render(); ImGui::End(); + ImGui::Begin("Timeline", nullptr, ImGuiWindowFlags_NoCollapse); + m_timelinePanel->Render(); + ImGui::End(); + m_imguiContext->Render(); } diff --git a/src/ui/Application.h b/src/ui/Application.h index 47d4535..bb3ffed 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -6,6 +6,7 @@ #include "SceneHierarchyPanel.h" #include "ViewportPanel.h" #include "PropertyPanel.h" +#include "TimelinePanel.h" #include "../core/UsdStageManager.h" #include "../core/LayerManager.h" #include "../core/PropertyManager.h" @@ -52,6 +53,7 @@ private: std::unique_ptr m_sceneHierarchyPanel; std::unique_ptr m_viewportPanel; std::unique_ptr m_propertyPanel; + std::unique_ptr m_timelinePanel; bool m_showDemoWindow; bool m_showStageInfo; bool m_running; diff --git a/src/ui/IconManager.cpp b/src/ui/IconManager.cpp index 2f4cee9..50f4dd0 100644 --- a/src/ui/IconManager.cpp +++ b/src/ui/IconManager.cpp @@ -48,6 +48,12 @@ static const char* IconFilename(Icon icon) { case Icon::LayoutHSplit: return "layout-hsplit.svg"; case Icon::LayoutVSplit: return "layout-vsplit.svg"; case Icon::LayoutQuad: return "layout-quad.svg"; + case Icon::SkipBack: return "skip-back.svg"; + case Icon::StepBack: return "step-back.svg"; + case Icon::Play: return "play.svg"; + case Icon::Pause: return "pause.svg"; + case Icon::StepForward: return "step-forward.svg"; + case Icon::SkipEnd: return "skip-end.svg"; default: return nullptr; } } @@ -61,6 +67,7 @@ static constexpr Icon kAllIcons[] = { Icon::ToolSelect, Icon::ToolMove, Icon::ToolRotate, Icon::ToolScale, Icon::WorldSpace, Icon::LocalSpace, Icon::Grid, Icon::Antialias, Icon::LayoutSingle, Icon::LayoutHSplit, Icon::LayoutVSplit, Icon::LayoutQuad, + Icon::SkipBack, Icon::StepBack, Icon::Play, Icon::Pause, Icon::StepForward, Icon::SkipEnd, }; // --------------------------------------------------------------------------- diff --git a/src/ui/IconManager.h b/src/ui/IconManager.h index 62a33a4..1a12fc5 100644 --- a/src/ui/IconManager.h +++ b/src/ui/IconManager.h @@ -40,6 +40,13 @@ enum class Icon { LayoutHSplit, // two panels side-by-side LayoutVSplit, // two panels top/bottom LayoutQuad, // four panels 2×2 + // Timeline transport + SkipBack, // go to first frame (|◀) + StepBack, // previous frame (◀◀) + Play, // play (▶) + Pause, // pause (⏸) + StepForward, // next frame (▶▶) + SkipEnd, // go to last frame (▶|) }; /// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as diff --git a/src/ui/PropertyPanel.cpp b/src/ui/PropertyPanel.cpp index c5b6961..830a6e6 100644 --- a/src/ui/PropertyPanel.cpp +++ b/src/ui/PropertyPanel.cpp @@ -74,6 +74,13 @@ void PropertyPanel::SetSelectedPrimPath(const std::string& path) { } } +void PropertyPanel::SetTimeCodes(pxr::UsdTimeCode displayTime, + pxr::UsdTimeCode editTime) { + m_displayTime = displayTime; + m_editTime = editTime; + m_needsRead = true; +} + // --------------------------------------------------------------------------- // USD read // --------------------------------------------------------------------------- @@ -101,7 +108,7 @@ void PropertyPanel::ReadTransform() { if (xformAPI.GetXformVectors( &translation, &rotation, &scale, &pivot, &rotOrder, - UsdTimeCode::Default())) + m_displayTime)) { m_translate = GfVec3f( static_cast(translation[0]), @@ -121,7 +128,7 @@ void PropertyPanel::ReadTransform() { GfMatrix4d localMatrix; bool resetsXformStack = false; if (!xformable.GetLocalTransformation( - &localMatrix, &resetsXformStack, UsdTimeCode::Default())) + &localMatrix, &resetsXformStack, m_displayTime)) { localMatrix = GfMatrix4d(1); // identity fallback } @@ -232,6 +239,16 @@ void PropertyPanel::EnsureCommonAPILayout() { m_rotOrder = UsdGeomXformCommonAPI::RotationOrderXYZ; } +void PropertyPanel::CaptureEditStart() { + m_editStartTranslate = m_translate; + m_editStartRotate = m_rotate; + m_editStartScale = m_scale; + // Keyframe presence must be sampled before the first live write so the + // undo command knows whether this edit created the sample at m_editTime. + m_editStartSamples = TransformCommand::QuerySamplePresence( + m_stage, SdfPath(m_selectedPrimPath), m_rotOrder, m_editTime); +} + void PropertyPanel::WriteTranslate() { if (!m_stage || m_selectedPrimPath.empty()) return; UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath)); @@ -241,7 +258,7 @@ void PropertyPanel::WriteTranslate() { if (!api) return; api.SetTranslate( GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - UsdTimeCode::Default()); + m_editTime); } void PropertyPanel::WriteRotate() { @@ -251,7 +268,7 @@ void PropertyPanel::WriteRotate() { if (m_xformFallback) { EnsureCommonAPILayout(); return; } UsdGeomXformCommonAPI api(prim); if (!api) return; - api.SetRotate(m_rotate, m_rotOrder, UsdTimeCode::Default()); + api.SetRotate(m_rotate, m_rotOrder, m_editTime); } void PropertyPanel::WriteScale() { @@ -261,7 +278,7 @@ void PropertyPanel::WriteScale() { if (m_xformFallback) { EnsureCommonAPILayout(); return; } UsdGeomXformCommonAPI api(prim); if (!api) return; - api.SetScale(m_scale, UsdTimeCode::Default()); + api.SetScale(m_scale, m_editTime); } // --------------------------------------------------------------------------- @@ -323,7 +340,7 @@ void PropertyPanel::RenderTransformSection() { ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##tx", &m_translate[0], 0.01f, 0.f, 0.f, "%.3f")) WriteTranslate(); bool anyActive = ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -332,14 +349,15 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Translate")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Translate")); } } ImGui::TableSetColumnIndex(2); ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##ty", &m_translate[1], 0.01f, 0.f, 0.f, "%.3f")) WriteTranslate(); anyActive = anyActive || ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -348,14 +366,15 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Translate")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Translate")); } } ImGui::TableSetColumnIndex(3); ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##tz", &m_translate[2], 0.01f, 0.f, 0.f, "%.3f")) WriteTranslate(); anyActive = anyActive || ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -364,7 +383,8 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Translate")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Translate")); } } @@ -376,7 +396,7 @@ void PropertyPanel::RenderTransformSection() { ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##rx", &m_rotate[0], 0.1f, 0.f, 0.f, "%.3f")) WriteRotate(); anyActive = anyActive || ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -385,14 +405,15 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Rotate")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Rotate")); } } ImGui::TableSetColumnIndex(2); ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##ry", &m_rotate[1], 0.1f, 0.f, 0.f, "%.3f")) WriteRotate(); anyActive = anyActive || ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -401,14 +422,15 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Rotate")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Rotate")); } } ImGui::TableSetColumnIndex(3); ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##rz", &m_rotate[2], 0.1f, 0.f, 0.f, "%.3f")) WriteRotate(); anyActive = anyActive || ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -417,7 +439,8 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Rotate")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Rotate")); } } @@ -429,7 +452,7 @@ void PropertyPanel::RenderTransformSection() { ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##sx", &m_scale[0], 0.005f, 0.f, 0.f, "%.3f")) WriteScale(); anyActive = anyActive || ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -438,14 +461,15 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Scale")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Scale")); } } ImGui::TableSetColumnIndex(2); ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##sy", &m_scale[1], 0.005f, 0.f, 0.f, "%.3f")) WriteScale(); anyActive = anyActive || ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -454,14 +478,15 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Scale")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Scale")); } } ImGui::TableSetColumnIndex(3); ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::DragFloat("##sz", &m_scale[2], 0.005f, 0.f, 0.f, "%.3f")) WriteScale(); anyActive = anyActive || ImGui::IsItemActive(); - if (ImGui::IsItemActivated()) { m_editStartTranslate = m_translate; m_editStartRotate = m_rotate; m_editStartScale = m_scale; } + if (ImGui::IsItemActivated()) CaptureEditStart(); if (ImGui::IsItemDeactivatedAfterEdit()) { if (m_commandHistory && m_stage && !m_selectedPrimPath.empty()) { auto editLayer = m_stage->GetEditTarget().GetLayer(); @@ -470,7 +495,8 @@ void PropertyPanel::RenderTransformSection() { pxr::GfVec3d(m_editStartTranslate[0], m_editStartTranslate[1], m_editStartTranslate[2]), m_editStartRotate, m_editStartScale, pxr::GfVec3d(m_translate[0], m_translate[1], m_translate[2]), - m_rotate, m_scale, m_rotOrder, "Scale")); + m_rotate, m_scale, m_rotOrder, + m_editTime, m_editStartSamples, "Scale")); } } @@ -946,7 +972,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { attr.GetMetadata(pxr::TfToken("allowedTokens"), &allowedTokens); pxr::VtValue val; - bool hasVal = attr.Get(&val, pxr::UsdTimeCode::Default()); + bool hasVal = attr.Get(&val, m_displayTime); if (!allowedTokens.IsEmpty() && allowedTokens.IsHolding>()) @@ -957,7 +983,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { if (ImGui::BeginCombo("##tok", cur.c_str())) { for (const auto& tok : tokens) { bool sel = (tok.GetString() == cur); - if (ImGui::Selectable(tok.GetText(), sel)) attr.Set(tok); + if (ImGui::Selectable(tok.GetText(), sel)) attr.Set(tok, m_editTime); if (sel) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); @@ -966,7 +992,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { // Type-aware widget dispatch (mirrors DrawVtValue in usdtweak) pxr::VtValue modified = DrawVtValueWidget(attr, val); if (!modified.IsEmpty()) - attr.Set(modified, pxr::UsdTimeCode::Default()); + attr.Set(modified, m_editTime); } else if (attr.HasAuthoredConnections()) { SdfPathVector conns; attr.GetConnections(&conns); diff --git a/src/ui/PropertyPanel.h b/src/ui/PropertyPanel.h index 67b006c..0b1afcd 100644 --- a/src/ui/PropertyPanel.h +++ b/src/ui/PropertyPanel.h @@ -2,7 +2,9 @@ #include "../core/PropertyManager.h" #include "../core/CommandHistory.h" +#include "../core/commands/TransformCommand.h" #include +#include #include #include #include @@ -25,6 +27,10 @@ public: void SetStage(pxr::UsdStageRefPtr stage); void SetSelectedPrimPath(const std::string& path); + /// displayTime — frame used to read/display attribute values. + /// editTime — frame writes are authored at (Default unless Auto-Key). + void SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime); + void Render(); private: @@ -33,6 +39,10 @@ private: void WriteRotate(); void WriteScale(); + /// Snapshot TRS values + keyframe presence when a DragFloat gains focus; + /// both feed the undo command built on deactivation. + void CaptureEditStart(); + /// When XformCommonAPI is not applicable (e.g. referenced prim with /// xformOp:transform), author standard common-API ops in the current edit /// layer so that subsequent XformCommonAPI writes succeed. @@ -67,6 +77,11 @@ private: pxr::GfVec3f m_editStartTranslate{ 0.f, 0.f, 0.f }; pxr::GfVec3f m_editStartRotate { 0.f, 0.f, 0.f }; pxr::GfVec3f m_editStartScale { 1.f, 1.f, 1.f }; + TransformCommand::SamplePresence m_editStartSamples; + + // Timeline time codes (synced by Application) + pxr::UsdTimeCode m_displayTime = pxr::UsdTimeCode::Default(); + pxr::UsdTimeCode m_editTime = pxr::UsdTimeCode::Default(); std::string m_primType; }; diff --git a/src/ui/TimelinePanel.cpp b/src/ui/TimelinePanel.cpp new file mode 100644 index 0000000..a6d703b --- /dev/null +++ b/src/ui/TimelinePanel.cpp @@ -0,0 +1,132 @@ +#include "TimelinePanel.h" + +#include +#include + +#include +#include + +namespace UsdLayerManager { + +// --------------------------------------------------------------------------- +void TimelinePanel::SetStage(pxr::UsdStageRefPtr stage) +{ + m_stage = stage; + m_playing = false; + + m_startFrame = 1.0; + m_endFrame = 100.0; + m_fps = 24.0; + if (stage) { + if (stage->HasAuthoredTimeCodeRange()) { + m_startFrame = stage->GetStartTimeCode(); + m_endFrame = stage->GetEndTimeCode(); + } + double tps = stage->GetTimeCodesPerSecond(); + if (tps > 0.0) m_fps = tps; + } + if (m_endFrame < m_startFrame) m_endFrame = m_startFrame; + + // Always notify on stage change so consumers pick up the new range, + // even when the numeric frame happens to be unchanged. + m_currentFrame = m_startFrame; + NotifyTimeChanged(); +} + +// --------------------------------------------------------------------------- +void TimelinePanel::Update(float deltaTime) +{ + if (!m_playing) return; + double next = m_currentFrame + double(deltaTime) * m_fps; + if (next > m_endFrame) { + double span = m_endFrame - m_startFrame; + next = (span > 0.0) + ? m_startFrame + std::fmod(next - m_startFrame, span) + : m_startFrame; + } + SetCurrentFrame(next); +} + +// --------------------------------------------------------------------------- +void TimelinePanel::Render() +{ + // ---- Transport row: icon buttons | start/end range | fps | auto-key ---- + auto iconBtn = [&](const char* id, Icon icon, const char* fallback) -> bool { + if (m_iconManager) { + ImTextureID tex = m_iconManager->Get(icon); + return ImGui::ImageButton(id, ImTextureRef(tex), ImVec2(18.f, 18.f)); + } + return ImGui::Button(fallback); + }; + + if (iconBtn("##skipback", Icon::SkipBack, "|<")) { m_playing = false; SetCurrentFrame(m_startFrame); } + ImGui::SameLine(); + if (iconBtn("##stepback", Icon::StepBack, "<")) { m_playing = false; SetCurrentFrame(std::floor(m_currentFrame) - 1.0); } + ImGui::SameLine(); + if (iconBtn("##playpause", m_playing ? Icon::Pause : Icon::Play, m_playing ? "Pause" : "Play")) m_playing = !m_playing; + ImGui::SameLine(); + if (iconBtn("##stepfwd", Icon::StepForward, ">")) { m_playing = false; SetCurrentFrame(std::floor(m_currentFrame) + 1.0); } + ImGui::SameLine(); + if (iconBtn("##skipend", Icon::SkipEnd, ">|")) { m_playing = false; SetCurrentFrame(m_endFrame); } + + ImGui::SameLine(); + int start = int(std::lround(m_startFrame)); + int end = int(std::lround(m_endFrame)); + ImGui::SetNextItemWidth(90.f); + bool rangeEdited = ImGui::DragInt("##start", &start, 1.0f, 0, 0, "Start %d"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.f); + rangeEdited |= ImGui::DragInt("##end", &end, 1.0f, 0, 0, "End %d"); + if (rangeEdited) { + if (end < start) end = start; + m_startFrame = double(start); + m_endFrame = double(end); + if (m_stage) { + // Stage time metadata must live on the root (or session) layer; + // the active edit target may be a sublayer. + pxr::UsdEditContext ec(m_stage, m_stage->GetRootLayer()); + m_stage->SetStartTimeCode(m_startFrame); + m_stage->SetEndTimeCode(m_endFrame); + } + SetCurrentFrame(m_currentFrame); // re-clamp into the new range + } + + ImGui::SameLine(); + ImGui::Text("%.6g fps", m_fps); + + ImGui::SameLine(); + if (ImGui::Checkbox("Auto-Key", &m_autoKey)) NotifyTimeChanged(); + + // ---- Frame slider: full width, snaps to whole frames ---- + float frame = float(m_currentFrame); + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::SliderFloat("##frame", &frame, + float(m_startFrame), float(m_endFrame), "%.0f")) { + m_playing = false; + SetCurrentFrame(std::round(double(frame))); + } +} + +// --------------------------------------------------------------------------- +void TimelinePanel::SetCurrentFrame(double frame) +{ + frame = std::min(std::max(frame, m_startFrame), m_endFrame); + if (frame != m_currentFrame) { + m_currentFrame = frame; + NotifyTimeChanged(); + } +} + +void TimelinePanel::NotifyTimeChanged() +{ + if (OnTimeChanged) + OnTimeChanged(pxr::UsdTimeCode(m_currentFrame), EditTime()); +} + +pxr::UsdTimeCode TimelinePanel::EditTime() const +{ + return m_autoKey ? pxr::UsdTimeCode(m_currentFrame) + : pxr::UsdTimeCode::Default(); +} + +} // namespace UsdLayerManager diff --git a/src/ui/TimelinePanel.h b/src/ui/TimelinePanel.h new file mode 100644 index 0000000..2a8c6b2 --- /dev/null +++ b/src/ui/TimelinePanel.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include "IconManager.h" +#include + +namespace UsdLayerManager { + +/// Bottom-docked playback timeline. +/// +/// Owns the current frame, playback state, and the Auto-Key toggle, and fires +/// OnTimeChanged whenever either changes: +/// displayTime — the current frame; used for all reads/evaluation +/// editTime — the current frame when Auto-Key is ON, otherwise Default() +/// (writes at editTime author time samples = keyframes) +class TimelinePanel { +public: + void SetStage(pxr::UsdStageRefPtr stage); + void SetIconManager(IconManager* icons) { m_iconManager = icons; } + + /// Advance playback by deltaTime seconds; called once per app frame. + void Update(float deltaTime); + void Render(); + + std::function OnTimeChanged; + +private: + /// Clamp to [start, end]; fires OnTimeChanged when the frame changes. + void SetCurrentFrame(double frame); + void NotifyTimeChanged(); + pxr::UsdTimeCode EditTime() const; + + IconManager* m_iconManager = nullptr; + pxr::UsdStageRefPtr m_stage; + + double m_startFrame = 1.0; + double m_endFrame = 100.0; + double m_fps = 24.0; + double m_currentFrame = 1.0; + bool m_playing = false; + bool m_autoKey = false; +}; + +} // namespace UsdLayerManager diff --git a/src/ui/TransformManipulator.cpp b/src/ui/TransformManipulator.cpp index 5ead935..9bab30f 100644 --- a/src/ui/TransformManipulator.cpp +++ b/src/ui/TransformManipulator.cpp @@ -63,6 +63,7 @@ static constexpr float kCenterCircleRadius = 5.0f; static bool SetupCompatibleXform( const pxr::UsdStageRefPtr& stage, const pxr::SdfPath& primPath, + pxr::UsdTimeCode time, pxr::GfVec3d& outT, pxr::GfVec3f& outR, pxr::GfVec3f& outS, @@ -75,7 +76,7 @@ static bool SetupCompatibleXform( pxr::GfVec3f pivot3f; if (api.GetXformVectors(&outT, &outR, &outS, &pivot3f, - &outRotOrder, pxr::UsdTimeCode::Default())) + &outRotOrder, time)) return true; // Fallback ----------------------------------------------------------------- @@ -83,7 +84,7 @@ static bool SetupCompatibleXform( " has an incompatible xform stack. Creating a compatible " "override in the edit layer."); - pxr::UsdGeomXformCache xc(pxr::UsdTimeCode::Default()); + pxr::UsdGeomXformCache xc(time); bool reset = false; pxr::GfMatrix4d localMat = xc.GetLocalTransformation(prim, &reset); @@ -143,7 +144,7 @@ void TransformManipulator::GetGizmoAxes(pxr::GfVec3d axes[3]) const if (!m_stage || m_primPath.IsEmpty()) return; pxr::UsdPrim prim = m_stage->GetPrimAtPath(m_primPath); if (!prim) return; - pxr::UsdGeomXformCache xc(pxr::UsdTimeCode::Default()); + pxr::UsdGeomXformCache xc(m_displayTime); pxr::GfMatrix4d l2w = xc.GetLocalToWorldTransform(prim); for (int i = 0; i < 3; ++i) { pxr::GfVec3d row(l2w[i][0], l2w[i][1], l2w[i][2]); @@ -454,13 +455,17 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp, pxr::GfVec3d trans; pxr::GfVec3f rot,scale; pxr::UsdGeomXformCommonAPI::RotationOrder rotOrder; - if (!SetupCompatibleXform(m_stage,m_primPath,trans,rot,scale,rotOrder)) { + if (!SetupCompatibleXform(m_stage,m_primPath,m_displayTime, + trans,rot,scale,rotOrder)) { m_isDragging=false; return false; } m_dragOriginalTranslate=trans; m_dragOriginalRotate=rot; m_dragOriginalScale=scale; m_dragOriginalRotOrder=rotOrder; m_dragStartTranslate=trans; m_dragStartRotate=rot; m_dragStartScale=scale; + // Snapshot keyframe presence before live writes author samples. + m_dragPreEditSamples=TransformCommand::QuerySamplePresence( + m_stage,m_primPath,rotOrder,m_editTime); pxr::GfRay ray=ComputeMouseRay(vp,mouse,imgPos,vW,vH); @@ -546,7 +551,7 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp, pxr::GfVec3d parentDelta=worldDelta; pxr::UsdPrim prim=m_stage->GetPrimAtPath(m_primPath); if (prim) { - pxr::UsdGeomXformCache xc(pxr::UsdTimeCode::Default()); + pxr::UsdGeomXformCache xc(m_displayTime); pxr::UsdPrim parent=prim.GetParent(); if (parent && !parent.IsPseudoRoot()) { pxr::GfMatrix4d p2w=xc.GetLocalToWorldTransform(parent); @@ -643,7 +648,7 @@ bool TransformManipulator::HandleInput(const pxr::GfMatrix4d& vp, m_stage->GetEditTarget().GetLayer(), m_dragOriginalTranslate,m_dragOriginalRotate,m_dragOriginalScale, m_dragStartTranslate, m_dragStartRotate, m_dragStartScale, - m_dragOriginalRotOrder, + m_dragOriginalRotOrder, m_editTime, m_dragPreEditSamples, "Transform "+m_primPath.GetName()); m_commandHistory->Push(std::move(cmd)); } @@ -661,7 +666,7 @@ void TransformManipulator::ApplyTranslate(const pxr::GfVec3d& t) if (!m_stage||m_primPath.IsEmpty()) return; pxr::UsdPrim p=m_stage->GetPrimAtPath(m_primPath); if (!p) return; pxr::UsdEditContext ec(m_stage,m_stage->GetEditTarget()); - pxr::UsdGeomXformCommonAPI(p).SetTranslate(t,pxr::UsdTimeCode::Default()); + pxr::UsdGeomXformCommonAPI(p).SetTranslate(t,m_editTime); m_dragStartTranslate=t; } @@ -671,7 +676,7 @@ void TransformManipulator::ApplyRotate(const pxr::GfVec3f& r, if (!m_stage||m_primPath.IsEmpty()) return; pxr::UsdPrim p=m_stage->GetPrimAtPath(m_primPath); if (!p) return; pxr::UsdEditContext ec(m_stage,m_stage->GetEditTarget()); - pxr::UsdGeomXformCommonAPI(p).SetRotate(r,ro,pxr::UsdTimeCode::Default()); + pxr::UsdGeomXformCommonAPI(p).SetRotate(r,ro,m_editTime); m_dragStartRotate=r; } @@ -680,7 +685,7 @@ void TransformManipulator::ApplyScale(const pxr::GfVec3f& s) if (!m_stage||m_primPath.IsEmpty()) return; pxr::UsdPrim p=m_stage->GetPrimAtPath(m_primPath); if (!p) return; pxr::UsdEditContext ec(m_stage,m_stage->GetEditTarget()); - pxr::UsdGeomXformCommonAPI(p).SetScale(s,pxr::UsdTimeCode::Default()); + pxr::UsdGeomXformCommonAPI(p).SetScale(s,m_editTime); m_dragStartScale=s; } diff --git a/src/ui/TransformManipulator.h b/src/ui/TransformManipulator.h index 18cd695..8ccdcd6 100644 --- a/src/ui/TransformManipulator.h +++ b/src/ui/TransformManipulator.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -12,6 +13,8 @@ #include #include +#include "../core/commands/TransformCommand.h" + namespace UsdLayerManager { class CommandHistory; @@ -33,6 +36,13 @@ public: void SetSelectedPrim(const pxr::SdfPath& path); void SetCommandHistory(CommandHistory* h) { m_commandHistory = h; } + /// displayTime — frame used to read/evaluate transforms (timeline frame). + /// editTime — frame writes are authored at (Default unless Auto-Key). + void SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime) { + m_displayTime = displayTime; + m_editTime = editTime; + } + void Render(ImDrawList* dl, const pxr::GfMatrix4d& viewProj, const pxr::GfVec3d& pivot, const pxr::GfVec3d& cameraEye, const ImVec2& imagePos, int viewW, int viewH); @@ -83,6 +93,8 @@ private: pxr::UsdStageRefPtr m_stage; pxr::SdfPath m_primPath; CommandHistory* m_commandHistory = nullptr; + pxr::UsdTimeCode m_displayTime = pxr::UsdTimeCode::Default(); + pxr::UsdTimeCode m_editTime = pxr::UsdTimeCode::Default(); bool m_isDragging = false; int m_dragAxis = -1; @@ -127,6 +139,11 @@ private: pxr::GfVec3d m_dragStartTranslate = {0,0,0}; pxr::GfVec3f m_dragStartRotate = {0,0,0}; pxr::GfVec3f m_dragStartScale = {1,1,1}; + + // Sample presence at m_editTime, captured at drag start (before the + // first live write) so the end-of-drag TransformCommand can undo + // keyframe creation correctly. + TransformCommand::SamplePresence m_dragPreEditSamples; }; } // namespace UsdLayerManager diff --git a/src/ui/ViewportPanel.cpp b/src/ui/ViewportPanel.cpp index d9a6215..1b1ca5d 100644 --- a/src/ui/ViewportPanel.cpp +++ b/src/ui/ViewportPanel.cpp @@ -28,6 +28,7 @@ void ViewportPanel::EnsureTileCount(int count) if (m_stage) m_tiles.back()->SetStage(m_stage); if (m_iconManager) m_tiles.back()->SetIconManager(m_iconManager); if (m_commandHistory) m_tiles.back()->SetCommandHistory(m_commandHistory); + m_tiles.back()->SetTimeCodes(m_displayTime, m_editTime); m_tiles.back()->SetSelectedPaths(m_selectedSdfPaths, m_selectedPrimPath); WireCallbacks(idx); } @@ -116,6 +117,15 @@ void ViewportPanel::SetIconManager(IconManager* icons) for (auto& t : m_tiles) t->SetIconManager(icons); } +void ViewportPanel::SetTimeCodes(pxr::UsdTimeCode displayTime, + pxr::UsdTimeCode editTime) +{ + m_displayTime = displayTime; + m_editTime = editTime; + for (auto& t : m_tiles) t->SetTimeCodes(displayTime, editTime); + m_manipulator.SetTimeCodes(displayTime, editTime); +} + void ViewportPanel::SetSelectedPrimPath(const std::string& path) { m_selectedPrimPath = path; diff --git a/src/ui/ViewportPanel.h b/src/ui/ViewportPanel.h index f9ec3b7..69b33b3 100644 --- a/src/ui/ViewportPanel.h +++ b/src/ui/ViewportPanel.h @@ -40,6 +40,11 @@ public: void SetCommandHistory(CommandHistory* history); void SetIconManager(IconManager* icons); + /// Push timeline time codes to every tile and the shared manipulator. + /// displayTime — frame used for all evaluation (render, cameras, bboxes). + /// editTime — frame writes are authored at (Default unless Auto-Key). + void SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime); + // ── Selection (called by SceneHierarchyPanel) ──────────────────────────── /// Set a single selected prim (clears any multi-selection). void SetSelectedPrimPath(const std::string& path); @@ -124,6 +129,8 @@ private: pxr::UsdStageRefPtr m_stage; IconManager* m_iconManager = nullptr; CommandHistory* m_commandHistory = nullptr; + pxr::UsdTimeCode m_displayTime = pxr::UsdTimeCode::Default(); + pxr::UsdTimeCode m_editTime = pxr::UsdTimeCode::Default(); }; } // namespace UsdLayerManager diff --git a/src/ui/ViewportTile.cpp b/src/ui/ViewportTile.cpp index f05fa4d..f240ba1 100644 --- a/src/ui/ViewportTile.cpp +++ b/src/ui/ViewportTile.cpp @@ -32,6 +32,15 @@ void ViewportTile::SetCommandHistory(CommandHistory* h) // (ViewportPanel), not by individual tiles. } +void ViewportTile::SetTimeCodes(pxr::UsdTimeCode displayTime, + pxr::UsdTimeCode editTime) +{ + m_displayTime = displayTime; + m_editTime = editTime; + m_renderer.SetCurrentTimeCode(displayTime); + m_renderer.SetForceRefresh(true); +} + void ViewportTile::SetStage(pxr::UsdStageRefPtr stage) { m_stage = stage; @@ -48,7 +57,7 @@ void ViewportTile::SetStage(pxr::UsdStageRefPtr stage) if (stage) { pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; - pxr::UsdGeomBBoxCache bboxCache(pxr::UsdTimeCode::Default(), purposes, true); + pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true); pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(stage->GetPseudoRoot()); if (!stageBBox.GetRange().IsEmpty()) m_camera.FrameSelection(stageBBox, 1.1); @@ -70,7 +79,7 @@ void ViewportTile::FrameScene() if (!m_stage) return; pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; - pxr::UsdGeomBBoxCache bboxCache(pxr::UsdTimeCode::Default(), purposes, true); + pxr::UsdGeomBBoxCache bboxCache(m_displayTime, purposes, true); pxr::GfBBox3d stageBBox = bboxCache.ComputeWorldBound(m_stage->GetPseudoRoot()); if (!stageBBox.GetRange().IsEmpty()) { InitCameraNavigation(); @@ -109,7 +118,7 @@ void ViewportTile::InitCameraNavigation() pxr::UsdPrim prim = m_stage->GetPrimAtPath(camPath); if (!prim || !prim.IsA()) { TrySwitchToFreeCamera(); return; } pxr::UsdGeomCamera usdCam(prim); - pxr::GfCamera gfCam = usdCam.GetCamera(pxr::UsdTimeCode::Default()); + pxr::GfCamera gfCam = usdCam.GetCamera(m_displayTime); m_camera.SwitchToFreeCamera(&gfCam); m_isDrivingUsdCamPrim = true; m_drivenUsdCamPath = camPath; @@ -124,13 +133,13 @@ pxr::GfVec3d ViewportTile::ComputeGizmoPivot() const pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; pxr::UsdGeomBBoxCache bboxCache( - pxr::UsdTimeCode::Default(), purposes, true); + m_displayTime, purposes, true); pxr::GfBBox3d bbox = bboxCache.ComputeWorldBound(prim); pxr::GfRange3d range = bbox.ComputeAlignedRange(); if (!range.IsEmpty()) return (range.GetMin() + range.GetMax()) * 0.5; - pxr::UsdGeomXformCache xformCache(pxr::UsdTimeCode::Default()); + pxr::UsdGeomXformCache xformCache(m_displayTime); pxr::GfMatrix4d worldXform = xformCache.GetLocalToWorldTransform(prim); return worldXform.ExtractTranslation(); } @@ -261,7 +270,7 @@ pxr::GfCamera ViewportTile::ResolveCamera() pxr::GfVec3d t; pxr::GfVec3f r, s, pv; pxr::UsdGeomXformCommonAPI::RotationOrder ro; if (!xformAPI.GetXformVectors( - &t, &r, &s, &pv, &ro, pxr::UsdTimeCode::Default())) + &t, &r, &s, &pv, &ro, m_displayTime)) pxr::UsdGeomXformable(prim).ClearXformOpOrder(); } pxr::GfMatrix4d camToWorld = gfCamera.GetTransform(); @@ -271,13 +280,13 @@ pxr::GfCamera ViewportTile::ResolveCamera() pxr::GfVec3d::XAxis(), pxr::GfVec3d::YAxis(), pxr::GfVec3d::ZAxis()); - xformAPI.SetTranslate(translate, pxr::UsdTimeCode::Default()); + xformAPI.SetTranslate(translate, m_editTime); xformAPI.SetRotate( pxr::GfVec3f(float(eulerDeg[0]), float(eulerDeg[1]), float(eulerDeg[2])), pxr::UsdGeomXformCommonAPI::RotationOrderXYZ, - pxr::UsdTimeCode::Default()); + m_editTime); } if (!m_isOrbiting && !m_isPanning && !m_isDollying) { m_isDrivingUsdCamPrim = false; @@ -292,7 +301,7 @@ pxr::GfCamera ViewportTile::ResolveCamera() pxr::UsdPrim camPrim = m_stage->GetPrimAtPath(camPath); if (camPrim && camPrim.IsA()) { pxr::UsdGeomCamera usdCam(camPrim); - gfCamera = usdCam.GetCamera(pxr::UsdTimeCode::Default()); + gfCamera = usdCam.GetCamera(m_displayTime); pxr::CameraUtilConformWindow( &gfCamera, pxr::CameraUtilMatchVertically, aspect); m_lastComputedGfCamera = gfCamera; @@ -576,7 +585,7 @@ void ViewportTile::HandleInput(bool isFocused, TransformManipulator& manipulator pxr::TfTokenVector purposes = { pxr::UsdGeomTokens->default_, pxr::UsdGeomTokens->proxy }; pxr::UsdGeomBBoxCache bboxCache( - pxr::UsdTimeCode::Default(), purposes, true); + m_displayTime, purposes, true); pxr::GfRange3d combined; for (const auto& path : m_selectedSdfPaths) { pxr::UsdPrim prim = m_stage->GetPrimAtPath(path); @@ -585,7 +594,7 @@ void ViewportTile::HandleInput(bool isFocused, TransformManipulator& manipulator combined.UnionWith(bbox.ComputeAlignedRange()); } if (combined.IsEmpty()) { - pxr::UsdGeomXformCache xformCache(pxr::UsdTimeCode::Default()); + pxr::UsdGeomXformCache xformCache(m_displayTime); for (const auto& path : m_selectedSdfPaths) { pxr::UsdPrim prim = m_stage->GetPrimAtPath(path); if (!prim) continue; diff --git a/src/ui/ViewportTile.h b/src/ui/ViewportTile.h index 8a686bb..5486700 100644 --- a/src/ui/ViewportTile.h +++ b/src/ui/ViewportTile.h @@ -47,6 +47,10 @@ public: void SetIconManager(IconManager* icons) { m_iconManager = icons; } void SetCommandHistory(CommandHistory* h); + /// displayTime — frame used for all evaluation (render, cameras, bboxes). + /// editTime — frame writes are authored at (Default unless Auto-Key). + void SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime); + // ── Selection sync ─────────────────────────────────────────────────────── /// Called by the container to broadcast the authoritative selection. /// Updates the local shadow copy and pushes it into the renderer highlight. @@ -107,6 +111,10 @@ private: IconManager* m_iconManager = nullptr; CommandHistory* m_commandHistory = nullptr; + // ── Timeline time codes (synced by container) ──────────────────────────── + pxr::UsdTimeCode m_displayTime = pxr::UsdTimeCode::Default(); + pxr::UsdTimeCode m_editTime = pxr::UsdTimeCode::Default(); + // ── Stage + camera list ────────────────────────────────────────────────── pxr::UsdStageRefPtr m_stage; std::vector m_cameraPaths;