Files
UsdLayerManager/src/ui/CurveEditorPanel.h
T
indigo ddffa5764c Curve editor: fix repeated Apply after bake
Three bugs prevented Apply from working correctly after the first bake:

1. m_needsRefresh spuriously set during bake
   ClearAllTimeSamples/WriteChannelValue fire OnObjectsChanged synchronously.
   Since the channel is dirty at that moment, it was skipped, setting
   anyMatchedChannel=false and triggering m_needsRefresh=true. This caused
   RefreshFromStage() on the next frame, clearing m_selection and rebuilding
   m_channels — dropping the user's edit context. Fixed with m_suppressNotice:
   set true during USD writes so OnObjectsChanged is a no-op during bake.

2. Prim-level metadata notice triggered rebuild
   SaveBezierToMetadata calls SetCustomDataByKey, which fires a notice with
   changed==primPath. No attribute channel matched, so m_needsRefresh=true
   again. Fixed in OnObjectsChanged: skip changed==primPath (metadata-only
   changes), and mark a path as "known" even if the channel is dirty (so
   the refresh gate only fires for genuinely new/unknown attributes).

3. Dangling chPtr in undo/redo closures
   After RefreshFromStage() cleared and rebuilt m_channels, the raw pointer
   captured in closures pointed to freed memory. Replaced with a findCh
   lambda that searches m_channels by attr path + component at call time.
   Falls back to m_needsRefresh=true if the channel no longer exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 09:28:09 +08:00

161 lines
6.5 KiB
C++

#pragma once
#include "../core/CommandHistory.h"
#include <pxr/usd/usd/stage.h>
#include <pxr/usd/usd/attribute.h>
#include <pxr/usd/usd/timeCode.h>
#include <pxr/usd/usd/notice.h>
#include <pxr/base/tf/weakBase.h>
#include <pxr/base/tf/notice.h>
#include <imgui.h>
#include <functional>
#include <string>
#include <vector>
namespace UsdLayerManager {
class CurveEditorPanel : public pxr::TfWeakBase {
public:
void SetStage(pxr::UsdStageRefPtr stage);
void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; }
void SetSelectedPrimPath(const std::string& path);
void SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime);
void Render();
// Fires when the user drags the time cursor; drives the Timeline panel.
std::function<void(double frame)> OnScrub;
private:
// ── Internal data ────────────────────────────────────────────────────────
struct BezierKey {
double time = 0.0;
double value = 0.0;
// Tangent handles as (dt, dv) offsets from the key.
// in-handle is left of the key (dt < 0), out-handle is right (dt > 0).
double inTangentDt = -1.0, inTangentDv = 0.0;
double outTangentDt = 1.0, outTangentDv = 0.0;
bool tangentsBroken = false;
};
struct CurveChannel {
std::string displayName;
pxr::UsdAttribute attr;
int component = -1; // -1=scalar, 0/1/2/3 = X/Y/Z/W
ImVec4 color = {1,1,1,1};
bool visible = true;
bool dirty = false;
std::vector<double> origTimes;
std::vector<double> origValues;
std::vector<BezierKey> keys;
};
enum class HandlePart { Key, InTangent, OutTangent };
struct SelectedHandle { int chanIdx; int keyIdx; HandlePart part; };
// ── Methods ──────────────────────────────────────────────────────────────
void RefreshFromStage();
void FitCurveFromSamples(CurveChannel& ch);
void BakeChannelToUsd(CurveChannel& ch);
// Bezier math
double EvaluateBezier(const std::vector<BezierKey>& keys, double T) const;
void DrawBezierSegment(ImDrawList* dl, const BezierKey& k0, const BezierKey& k1,
ImVec4 color, ImVec2 canvasMin, ImVec2 canvasMax) const;
// Coordinate helpers
float TimeToX(double t, ImVec2 cMin, ImVec2 cMax) const;
float ValToY (double v, ImVec2 cMin, ImVec2 cMax) const;
double XToTime(float x, ImVec2 cMin, ImVec2 cMax) const;
double YToVal (float y, ImVec2 cMin, ImVec2 cMax) const;
ImVec2 KeyToScreen(const BezierKey& k, ImVec2 cMin, ImVec2 cMax) const;
// USD channel read/write helpers
double ReadChannelValue (const pxr::UsdAttribute& attr, int comp, double time) const;
void WriteChannelValue(const pxr::UsdAttribute& attr, int comp,
double time, double val) const;
void ClearAllTimeSamples(const pxr::UsdAttribute& attr) const;
// Rendering sub-sections
void RenderToolbar();
void RenderAttrList(float listWidth);
void RenderCanvas(ImVec2 canvasMin, ImVec2 canvasMax);
void RenderGrid(ImDrawList* dl, ImVec2 cMin, ImVec2 cMax);
void RenderTimeCursor(ImDrawList* dl, ImVec2 cMin, ImVec2 cMax);
void RenderCurve(ImDrawList* dl, CurveChannel& ch, ImVec2 cMin, ImVec2 cMax);
void HandleCanvasInput(ImVec2 cMin, ImVec2 cMax);
// Selection helpers
bool IsSelected(int chanIdx, int keyIdx, HandlePart part) const;
void Select(int chanIdx, int keyIdx, HandlePart part, bool additive);
void ClearSelection();
// Interaction helpers
void FrameAll();
void ApplyAllDirty();
void RevertAll();
void DeleteSelectedKeys();
void AddKeyAt(int chanIdx, double time, double value);
void RefitNeighborTangents(CurveChannel& ch, int keyIdx);
// Bezier metadata persistence (stored in prim customData["curveEditor"])
static std::string ChannelMetadataKey(const CurveChannel& ch);
bool SaveBezierToMetadata(const pxr::UsdPrim& prim, const CurveChannel& ch) const;
bool SaveBezierToMetadata(const pxr::UsdPrim& prim, const std::string& channelKey,
const std::vector<BezierKey>& keys) const;
bool LoadBezierFromMetadata(const pxr::UsdPrim& prim, CurveChannel& ch) const;
// ── State ────────────────────────────────────────────────────────────────
pxr::UsdStageRefPtr m_stage;
CommandHistory* m_commandHistory = nullptr;
std::string m_selectedPrimPath;
pxr::UsdTimeCode m_displayTime = pxr::UsdTimeCode::Default();
pxr::UsdTimeCode m_editTime = pxr::UsdTimeCode::Default();
std::vector<CurveChannel> m_channels;
std::vector<SelectedHandle> m_selection;
int m_activeChannel = -1; // for double-click-to-add
bool m_needsRefresh = true;
bool m_suppressNotice = false; // set during bake writes to silence OnObjectsChanged
// USD change notification
pxr::TfNotice::Key m_stageChangeKey;
void OnObjectsChanged(const pxr::UsdNotice::ObjectsChanged& notice,
const pxr::UsdStageWeakPtr& sender);
// View range
double m_viewMinTime = 0.0, m_viewMaxTime = 100.0;
double m_viewMinVal = -1.0, m_viewMaxVal = 1.0;
// Pan/zoom drag state
bool m_isPanning = false;
ImVec2 m_panLastMouse = {0,0};
double m_panMinTimeStart = 0.0, m_panMaxTimeStart = 0.0;
double m_panMinValStart = 0.0, m_panMaxValStart = 0.0;
// Drag-move state
bool m_isDragging = false;
ImVec2 m_dragStartMouse= {0,0};
double m_dragStartTime = 0.0, m_dragStartValue = 0.0;
struct KeySnapshot { int chanIdx; int keyIdx; double time; double value;
double inDt, inDv, outDt, outDv; };
std::vector<KeySnapshot> m_dragSnapshot;
// Selection box drag
bool m_isBoxSelecting = false;
ImVec2 m_boxSelectStart = {0,0};
// Scrub cursor drag
bool m_isDraggingCursor = false;
// Options
bool m_simplifyOnBake = false;
float m_simplifyTol = 0.01f;
bool m_snapToFrame = true;
};
} // namespace UsdLayerManager