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>
This commit is contained in:
+60
-37
@@ -168,6 +168,7 @@ void CurveEditorPanel::OnObjectsChanged(
|
|||||||
const UsdNotice::ObjectsChanged& notice,
|
const UsdNotice::ObjectsChanged& notice,
|
||||||
const UsdStageWeakPtr& /*sender*/)
|
const UsdStageWeakPtr& /*sender*/)
|
||||||
{
|
{
|
||||||
|
if (m_suppressNotice) return;
|
||||||
if (m_selectedPrimPath.empty()) return;
|
if (m_selectedPrimPath.empty()) return;
|
||||||
SdfPath primPath(m_selectedPrimPath);
|
SdfPath primPath(m_selectedPrimPath);
|
||||||
|
|
||||||
@@ -179,26 +180,24 @@ void CurveEditorPanel::OnObjectsChanged(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Value-only change → re-fit individual channels that were affected and are clean
|
// Value-only change → re-fit clean channels; ignore prim-level metadata changes
|
||||||
bool anyMatchedChannel = false;
|
|
||||||
for (const SdfPath& changed : notice.GetChangedInfoOnlyPaths()) {
|
for (const SdfPath& changed : notice.GetChangedInfoOnlyPaths()) {
|
||||||
// changed is an attribute path like /Prim.attrName
|
// Prim-level path means a metadata change (e.g. customData) — not an attr write
|
||||||
// or a prim path /Prim when multiple attrs changed at once
|
if (changed == primPath) continue;
|
||||||
bool belongsToSelectedPrim =
|
|
||||||
(changed.GetPrimPath() == primPath) || (changed == primPath);
|
|
||||||
if (!belongsToSelectedPrim) continue;
|
|
||||||
|
|
||||||
|
if (changed.GetPrimPath() != primPath) continue;
|
||||||
|
|
||||||
|
bool knownChannel = false;
|
||||||
for (auto& ch : m_channels) {
|
for (auto& ch : m_channels) {
|
||||||
if (ch.dirty) continue; // preserve unsaved edits
|
if (changed == ch.attr.GetPath()) {
|
||||||
if (changed == ch.attr.GetPath() || changed == primPath) {
|
knownChannel = true;
|
||||||
FitCurveFromSamples(ch);
|
if (!ch.dirty) FitCurveFromSamples(ch); // preserve unsaved edits
|
||||||
anyMatchedChannel = true;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A change on the selected prim that didn't match any known channel
|
// Changed attribute not in our list → may be a newly animated attr → full refresh
|
||||||
// may mean a new animated attribute was added → full refresh
|
if (!knownChannel)
|
||||||
if (!anyMatchedChannel)
|
|
||||||
m_needsRefresh = true;
|
m_needsRefresh = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,15 +310,15 @@ std::string CurveEditorPanel::ChannelMetadataKey(const CurveChannel& ch)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool CurveEditorPanel::SaveBezierToMetadata(const UsdPrim& prim,
|
bool CurveEditorPanel::SaveBezierToMetadata(const UsdPrim& prim,
|
||||||
const CurveChannel& ch) const
|
const std::string& channelKey,
|
||||||
|
const std::vector<BezierKey>& keys) const
|
||||||
{
|
{
|
||||||
if (!prim || ch.keys.empty()) return false;
|
if (!prim || keys.empty()) return false;
|
||||||
|
|
||||||
// Build flat arrays from keys
|
|
||||||
VtArray<double> times, values, inDt, inDv, outDt, outDv;
|
VtArray<double> times, values, inDt, inDv, outDt, outDv;
|
||||||
VtArray<int> broken;
|
VtArray<int> broken;
|
||||||
times.reserve(ch.keys.size());
|
times.reserve(keys.size());
|
||||||
for (const BezierKey& k : ch.keys) {
|
for (const BezierKey& k : keys) {
|
||||||
times .push_back(k.time);
|
times .push_back(k.time);
|
||||||
values.push_back(k.value);
|
values.push_back(k.value);
|
||||||
inDt .push_back(k.inTangentDt);
|
inDt .push_back(k.inTangentDt);
|
||||||
@@ -338,13 +337,11 @@ bool CurveEditorPanel::SaveBezierToMetadata(const UsdPrim& prim,
|
|||||||
keyData["outDv"] = VtValue(outDv);
|
keyData["outDv"] = VtValue(outDv);
|
||||||
keyData["broken"] = VtValue(broken);
|
keyData["broken"] = VtValue(broken);
|
||||||
|
|
||||||
// Read existing curveEditor dict
|
|
||||||
VtDictionary ceDict;
|
VtDictionary ceDict;
|
||||||
VtValue existing = prim.GetCustomDataByKey(TfToken("curveEditor"));
|
VtValue existing = prim.GetCustomDataByKey(TfToken("curveEditor"));
|
||||||
if (existing.IsHolding<VtDictionary>())
|
if (existing.IsHolding<VtDictionary>())
|
||||||
ceDict = existing.UncheckedGet<VtDictionary>();
|
ceDict = existing.UncheckedGet<VtDictionary>();
|
||||||
|
|
||||||
// Read / create channels sub-dict
|
|
||||||
VtDictionary channels;
|
VtDictionary channels;
|
||||||
{
|
{
|
||||||
auto it = ceDict.find("channels");
|
auto it = ceDict.find("channels");
|
||||||
@@ -352,12 +349,18 @@ bool CurveEditorPanel::SaveBezierToMetadata(const UsdPrim& prim,
|
|||||||
channels = it->second.UncheckedGet<VtDictionary>();
|
channels = it->second.UncheckedGet<VtDictionary>();
|
||||||
}
|
}
|
||||||
|
|
||||||
channels[ChannelMetadataKey(ch)] = VtValue(keyData);
|
channels[channelKey] = VtValue(keyData);
|
||||||
ceDict["channels"] = VtValue(channels);
|
ceDict["channels"] = VtValue(channels);
|
||||||
prim.SetCustomDataByKey(TfToken("curveEditor"), VtValue(ceDict));
|
prim.SetCustomDataByKey(TfToken("curveEditor"), VtValue(ceDict));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CurveEditorPanel::SaveBezierToMetadata(const UsdPrim& prim,
|
||||||
|
const CurveChannel& ch) const
|
||||||
|
{
|
||||||
|
return SaveBezierToMetadata(prim, ChannelMetadataKey(ch), ch.keys);
|
||||||
|
}
|
||||||
|
|
||||||
bool CurveEditorPanel::LoadBezierFromMetadata(const UsdPrim& prim,
|
bool CurveEditorPanel::LoadBezierFromMetadata(const UsdPrim& prim,
|
||||||
CurveChannel& ch) const
|
CurveChannel& ch) const
|
||||||
{
|
{
|
||||||
@@ -611,52 +614,72 @@ void CurveEditorPanel::BakeChannelToUsd(CurveChannel& ch)
|
|||||||
UsdAttribute attr = ch.attr;
|
UsdAttribute attr = ch.attr;
|
||||||
int comp = ch.component;
|
int comp = ch.component;
|
||||||
|
|
||||||
// Snapshot current Bezier keys and old prim metadata for undo
|
|
||||||
auto newKeys = ch.keys;
|
auto newKeys = ch.keys;
|
||||||
|
std::string channelKey = ChannelMetadataKey(ch);
|
||||||
UsdPrim prim = m_stage ? m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath))
|
UsdPrim prim = m_stage ? m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath))
|
||||||
: UsdPrim{};
|
: UsdPrim{};
|
||||||
VtValue oldCEMeta;
|
VtValue oldCEMeta;
|
||||||
if (prim) oldCEMeta = prim.GetCustomDataByKey(TfToken("curveEditor"));
|
if (prim) oldCEMeta = prim.GetCustomDataByKey(TfToken("curveEditor"));
|
||||||
|
|
||||||
|
// Safe channel finder: looks up by attr path + component at closure call time,
|
||||||
|
// so it remains valid even if m_channels is rebuilt between bake and undo.
|
||||||
|
SdfPath chanAttrPath = attr.GetPath();
|
||||||
|
auto findCh = [this, chanAttrPath, comp]() -> CurveChannel* {
|
||||||
|
for (auto& c : m_channels)
|
||||||
|
if (c.attr.GetPath() == chanAttrPath && c.component == comp) return &c;
|
||||||
|
return nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
if (!m_commandHistory) {
|
if (!m_commandHistory) {
|
||||||
|
m_suppressNotice = true;
|
||||||
ClearAllTimeSamples(attr);
|
ClearAllTimeSamples(attr);
|
||||||
for (int i = 0; i < int(newTimes.size()); ++i)
|
for (int i = 0; i < int(newTimes.size()); ++i)
|
||||||
WriteChannelValue(attr, comp, newTimes[i], newValues[i]);
|
WriteChannelValue(attr, comp, newTimes[i], newValues[i]);
|
||||||
|
m_suppressNotice = false;
|
||||||
ch.origTimes = newTimes;
|
ch.origTimes = newTimes;
|
||||||
ch.origValues = newValues;
|
ch.origValues = newValues;
|
||||||
ch.dirty = false;
|
ch.dirty = false;
|
||||||
if (prim) SaveBezierToMetadata(prim, ch);
|
if (prim) SaveBezierToMetadata(prim, channelKey, newKeys);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture for closure
|
|
||||||
CurveChannel* chPtr = &ch;
|
|
||||||
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||||
"Bake F-Curve: " + ch.displayName,
|
"Bake F-Curve: " + ch.displayName,
|
||||||
[this, attr, comp, newTimes, newValues, newKeys, prim, chPtr]() {
|
[this, attr, comp, newTimes, newValues, newKeys, channelKey, prim, findCh]() {
|
||||||
|
m_suppressNotice = true;
|
||||||
ClearAllTimeSamples(attr);
|
ClearAllTimeSamples(attr);
|
||||||
for (int i = 0; i < int(newTimes.size()); ++i)
|
for (int i = 0; i < int(newTimes.size()); ++i)
|
||||||
WriteChannelValue(attr, comp, newTimes[i], newValues[i]);
|
WriteChannelValue(attr, comp, newTimes[i], newValues[i]);
|
||||||
chPtr->origTimes = newTimes;
|
m_suppressNotice = false;
|
||||||
chPtr->origValues = newValues;
|
if (prim) SaveBezierToMetadata(prim, channelKey, newKeys);
|
||||||
chPtr->keys = newKeys;
|
if (auto* c = findCh()) {
|
||||||
chPtr->dirty = false;
|
c->origTimes = newTimes;
|
||||||
if (prim) SaveBezierToMetadata(prim, *chPtr);
|
c->origValues = newValues;
|
||||||
|
c->keys = newKeys;
|
||||||
|
c->dirty = false;
|
||||||
|
} else {
|
||||||
|
m_needsRefresh = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[this, attr, comp, oldTimes, oldValues, oldCEMeta, prim, chPtr]() {
|
[this, attr, comp, oldTimes, oldValues, oldCEMeta, prim, findCh]() {
|
||||||
|
m_suppressNotice = true;
|
||||||
ClearAllTimeSamples(attr);
|
ClearAllTimeSamples(attr);
|
||||||
for (int i = 0; i < int(oldTimes.size()); ++i)
|
for (int i = 0; i < int(oldTimes.size()); ++i)
|
||||||
WriteChannelValue(attr, comp, oldTimes[i], oldValues[i]);
|
WriteChannelValue(attr, comp, oldTimes[i], oldValues[i]);
|
||||||
// Restore metadata snapshot
|
m_suppressNotice = false;
|
||||||
if (prim) {
|
if (prim) {
|
||||||
if (oldCEMeta.IsEmpty())
|
if (oldCEMeta.IsEmpty())
|
||||||
prim.ClearCustomDataByKey(TfToken("curveEditor"));
|
prim.ClearCustomDataByKey(TfToken("curveEditor"));
|
||||||
else
|
else
|
||||||
prim.SetCustomDataByKey(TfToken("curveEditor"), oldCEMeta);
|
prim.SetCustomDataByKey(TfToken("curveEditor"), oldCEMeta);
|
||||||
}
|
}
|
||||||
chPtr->origTimes = oldTimes;
|
if (auto* c = findCh()) {
|
||||||
chPtr->origValues = oldValues;
|
c->origTimes = oldTimes;
|
||||||
FitCurveFromSamples(*chPtr);
|
c->origValues = oldValues;
|
||||||
|
FitCurveFromSamples(*c);
|
||||||
|
} else {
|
||||||
|
m_needsRefresh = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ private:
|
|||||||
// Bezier metadata persistence (stored in prim customData["curveEditor"])
|
// Bezier metadata persistence (stored in prim customData["curveEditor"])
|
||||||
static std::string ChannelMetadataKey(const CurveChannel& ch);
|
static std::string ChannelMetadataKey(const CurveChannel& ch);
|
||||||
bool SaveBezierToMetadata(const pxr::UsdPrim& prim, const CurveChannel& ch) const;
|
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;
|
bool LoadBezierFromMetadata(const pxr::UsdPrim& prim, CurveChannel& ch) const;
|
||||||
|
|
||||||
// ── State ────────────────────────────────────────────────────────────────
|
// ── State ────────────────────────────────────────────────────────────────
|
||||||
@@ -117,6 +119,7 @@ private:
|
|||||||
int m_activeChannel = -1; // for double-click-to-add
|
int m_activeChannel = -1; // for double-click-to-add
|
||||||
|
|
||||||
bool m_needsRefresh = true;
|
bool m_needsRefresh = true;
|
||||||
|
bool m_suppressNotice = false; // set during bake writes to silence OnObjectsChanged
|
||||||
|
|
||||||
// USD change notification
|
// USD change notification
|
||||||
pxr::TfNotice::Key m_stageChangeKey;
|
pxr::TfNotice::Key m_stageChangeKey;
|
||||||
|
|||||||
Reference in New Issue
Block a user