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;