Curve editor: fix context menu, persist Bezier metadata, always-visible handles
Context menu fix: - Move RMB popup and hit-test lambdas before the early-return guard in HandleCanvasInput so BeginPopup is called every frame while the popup is open, regardless of whether the mouse has left the canvas area. Previously the early-return fired (hovered=false) the moment the mouse moved onto the menu, silently closing it before any item could register. Bezier metadata persistence: - On Apply/bake, write each channel's BezierKey array into prim.customData["curveEditor"]["channels"][attrName@component] as flat double[]/int[] arrays (times, values, inDt, inDv, outDt, outDv, broken). - FitCurveFromSamples tries LoadBezierFromMetadata first; falls back to Catmull-Rom only when no saved entry exists, so tangent shapes survive file save/reload. - Bake undo command snapshots the entire curveEditor customData value and restores it on Ctrl+Z, keeping metadata in sync with sample history. Always-visible tangent handles: - Remove keySel guard so in/out handle lines and circles render for every keyframe, not just selected ones. - hitTestHandle now iterates all visible channels/keys so any handle can be clicked directly without first selecting its parent keyframe. - Clicking a handle auto-selects the parent key as well, keeping context menu operations (Flatten, Break, Delete) consistent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+249
-87
@@ -12,8 +12,11 @@
|
||||
#include <pxr/base/gf/vec4d.h>
|
||||
#include <pxr/usd/sdf/valueTypeName.h>
|
||||
#include <pxr/usd/usd/notice.h>
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/base/tf/notice.h>
|
||||
#include <pxr/base/tf/weakPtr.h>
|
||||
#include <pxr/base/vt/dictionary.h>
|
||||
#include <pxr/base/vt/array.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
@@ -278,6 +281,140 @@ void CurveEditorPanel::RefreshFromStage()
|
||||
if (!m_channels.empty()) FrameAll();
|
||||
}
|
||||
|
||||
// ── Bezier metadata persistence ───────────────────────────────────────────────
|
||||
//
|
||||
// Stored under prim.customData["curveEditor"]["channels"][channelKey].
|
||||
// The channel key uses '@' to separate attribute name from component index
|
||||
// (avoiding the ':' path separator used by SetCustomDataByKey).
|
||||
//
|
||||
// Saved USDA example:
|
||||
// customData = {
|
||||
// dictionary curveEditor = {
|
||||
// dictionary channels = {
|
||||
// dictionary "xformOp:translate@0" = {
|
||||
// double[] times = [1, 12, 24]
|
||||
// double[] values = [0, 3, 0]
|
||||
// double[] inDt = [-4, -4, -4]
|
||||
// double[] inDv = [0, -1, 0]
|
||||
// double[] outDt = [4, 4, 4]
|
||||
// double[] outDv = [0, 1, 0]
|
||||
// int[] broken = [0, 0, 0]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/*static*/
|
||||
std::string CurveEditorPanel::ChannelMetadataKey(const CurveChannel& ch)
|
||||
{
|
||||
return ch.attr.GetName().GetString() + "@" + std::to_string(ch.component);
|
||||
}
|
||||
|
||||
bool CurveEditorPanel::SaveBezierToMetadata(const UsdPrim& prim,
|
||||
const CurveChannel& ch) const
|
||||
{
|
||||
if (!prim || ch.keys.empty()) return false;
|
||||
|
||||
// Build flat arrays from keys
|
||||
VtArray<double> times, values, inDt, inDv, outDt, outDv;
|
||||
VtArray<int> broken;
|
||||
times.reserve(ch.keys.size());
|
||||
for (const BezierKey& k : ch.keys) {
|
||||
times .push_back(k.time);
|
||||
values.push_back(k.value);
|
||||
inDt .push_back(k.inTangentDt);
|
||||
inDv .push_back(k.inTangentDv);
|
||||
outDt .push_back(k.outTangentDt);
|
||||
outDv .push_back(k.outTangentDv);
|
||||
broken.push_back(k.tangentsBroken ? 1 : 0);
|
||||
}
|
||||
|
||||
VtDictionary keyData;
|
||||
keyData["times"] = VtValue(times);
|
||||
keyData["values"] = VtValue(values);
|
||||
keyData["inDt"] = VtValue(inDt);
|
||||
keyData["inDv"] = VtValue(inDv);
|
||||
keyData["outDt"] = VtValue(outDt);
|
||||
keyData["outDv"] = VtValue(outDv);
|
||||
keyData["broken"] = VtValue(broken);
|
||||
|
||||
// Read existing curveEditor dict
|
||||
VtDictionary ceDict;
|
||||
VtValue existing = prim.GetCustomDataByKey(TfToken("curveEditor"));
|
||||
if (existing.IsHolding<VtDictionary>())
|
||||
ceDict = existing.UncheckedGet<VtDictionary>();
|
||||
|
||||
// Read / create channels sub-dict
|
||||
VtDictionary channels;
|
||||
{
|
||||
auto it = ceDict.find("channels");
|
||||
if (it != ceDict.end() && it->second.IsHolding<VtDictionary>())
|
||||
channels = it->second.UncheckedGet<VtDictionary>();
|
||||
}
|
||||
|
||||
channels[ChannelMetadataKey(ch)] = VtValue(keyData);
|
||||
ceDict["channels"] = VtValue(channels);
|
||||
prim.SetCustomDataByKey(TfToken("curveEditor"), VtValue(ceDict));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CurveEditorPanel::LoadBezierFromMetadata(const UsdPrim& prim,
|
||||
CurveChannel& ch) const
|
||||
{
|
||||
if (!prim) return false;
|
||||
|
||||
VtValue ceVal = prim.GetCustomDataByKey(TfToken("curveEditor"));
|
||||
if (!ceVal.IsHolding<VtDictionary>()) return false;
|
||||
const VtDictionary& ceDict = ceVal.UncheckedGet<VtDictionary>();
|
||||
|
||||
auto chIt = ceDict.find("channels");
|
||||
if (chIt == ceDict.end() || !chIt->second.IsHolding<VtDictionary>()) return false;
|
||||
const VtDictionary& channels = chIt->second.UncheckedGet<VtDictionary>();
|
||||
|
||||
auto kIt = channels.find(ChannelMetadataKey(ch));
|
||||
if (kIt == channels.end() || !kIt->second.IsHolding<VtDictionary>()) return false;
|
||||
const VtDictionary& kd = kIt->second.UncheckedGet<VtDictionary>();
|
||||
|
||||
auto getDoubleArr = [&](const char* name) -> VtArray<double> {
|
||||
auto it = kd.find(name);
|
||||
if (it != kd.end() && it->second.IsHolding<VtArray<double>>())
|
||||
return it->second.UncheckedGet<VtArray<double>>();
|
||||
return {};
|
||||
};
|
||||
auto getIntArr = [&](const char* name) -> VtArray<int> {
|
||||
auto it = kd.find(name);
|
||||
if (it != kd.end() && it->second.IsHolding<VtArray<int>>())
|
||||
return it->second.UncheckedGet<VtArray<int>>();
|
||||
return {};
|
||||
};
|
||||
|
||||
VtArray<double> times = getDoubleArr("times");
|
||||
VtArray<double> values = getDoubleArr("values");
|
||||
VtArray<double> inDt = getDoubleArr("inDt");
|
||||
VtArray<double> inDv = getDoubleArr("inDv");
|
||||
VtArray<double> outDt = getDoubleArr("outDt");
|
||||
VtArray<double> outDv = getDoubleArr("outDv");
|
||||
VtArray<int> broken = getIntArr("broken");
|
||||
|
||||
size_t n = times.size();
|
||||
if (n == 0 || values.size() != n) return false;
|
||||
|
||||
ch.keys.clear();
|
||||
ch.keys.reserve(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
BezierKey k;
|
||||
k.time = times[i];
|
||||
k.value = values[i];
|
||||
k.inTangentDt = (inDt.size() > i) ? inDt[i] : -1.0;
|
||||
k.inTangentDv = (inDv.size() > i) ? inDv[i] : 0.0;
|
||||
k.outTangentDt = (outDt.size() > i) ? outDt[i] : 1.0;
|
||||
k.outTangentDv = (outDv.size() > i) ? outDv[i] : 0.0;
|
||||
k.tangentsBroken = (broken.size() > i) ? broken[i] != 0 : false;
|
||||
ch.keys.push_back(k);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Catmull-Rom fit ───────────────────────────────────────────────────────────
|
||||
|
||||
void CurveEditorPanel::FitCurveFromSamples(CurveChannel& ch)
|
||||
@@ -300,6 +437,15 @@ void CurveEditorPanel::FitCurveFromSamples(CurveChannel& ch)
|
||||
ch.origTimes = times;
|
||||
ch.origValues = vals;
|
||||
|
||||
// Prefer saved Bezier tangents over Catmull-Rom re-fit
|
||||
if (m_stage && !m_selectedPrimPath.empty()) {
|
||||
UsdPrim prim = m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath));
|
||||
if (prim && LoadBezierFromMetadata(prim, ch)) {
|
||||
ch.dirty = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int n = int(times.size());
|
||||
for (int i = 0; i < n; ++i) {
|
||||
BezierKey k;
|
||||
@@ -465,6 +611,13 @@ void CurveEditorPanel::BakeChannelToUsd(CurveChannel& ch)
|
||||
UsdAttribute attr = ch.attr;
|
||||
int comp = ch.component;
|
||||
|
||||
// Snapshot current Bezier keys and old prim metadata for undo
|
||||
auto newKeys = ch.keys;
|
||||
UsdPrim prim = m_stage ? m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath))
|
||||
: UsdPrim{};
|
||||
VtValue oldCEMeta;
|
||||
if (prim) oldCEMeta = prim.GetCustomDataByKey(TfToken("curveEditor"));
|
||||
|
||||
if (!m_commandHistory) {
|
||||
ClearAllTimeSamples(attr);
|
||||
for (int i = 0; i < int(newTimes.size()); ++i)
|
||||
@@ -472,6 +625,7 @@ void CurveEditorPanel::BakeChannelToUsd(CurveChannel& ch)
|
||||
ch.origTimes = newTimes;
|
||||
ch.origValues = newValues;
|
||||
ch.dirty = false;
|
||||
if (prim) SaveBezierToMetadata(prim, ch);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -479,19 +633,27 @@ void CurveEditorPanel::BakeChannelToUsd(CurveChannel& ch)
|
||||
CurveChannel* chPtr = &ch;
|
||||
m_commandHistory->Push(std::make_unique<AttributeSetCommand>(
|
||||
"Bake F-Curve: " + ch.displayName,
|
||||
[this, attr, comp, newTimes, newValues, chPtr]() {
|
||||
[this, attr, comp, newTimes, newValues, newKeys, prim, chPtr]() {
|
||||
ClearAllTimeSamples(attr);
|
||||
for (int i = 0; i < int(newTimes.size()); ++i)
|
||||
WriteChannelValue(attr, comp, newTimes[i], newValues[i]);
|
||||
// Update channel cache — safe because bake triggers before prim switch
|
||||
chPtr->origTimes = newTimes;
|
||||
chPtr->origValues = newValues;
|
||||
chPtr->dirty = false;
|
||||
chPtr->keys = newKeys;
|
||||
chPtr->dirty = false;
|
||||
if (prim) SaveBezierToMetadata(prim, *chPtr);
|
||||
},
|
||||
[this, attr, comp, oldTimes, oldValues, chPtr]() {
|
||||
[this, attr, comp, oldTimes, oldValues, oldCEMeta, prim, chPtr]() {
|
||||
ClearAllTimeSamples(attr);
|
||||
for (int i = 0; i < int(oldTimes.size()); ++i)
|
||||
WriteChannelValue(attr, comp, oldTimes[i], oldValues[i]);
|
||||
// Restore metadata snapshot
|
||||
if (prim) {
|
||||
if (oldCEMeta.IsEmpty())
|
||||
prim.ClearCustomDataByKey(TfToken("curveEditor"));
|
||||
else
|
||||
prim.SetCustomDataByKey(TfToken("curveEditor"), oldCEMeta);
|
||||
}
|
||||
chPtr->origTimes = oldTimes;
|
||||
chPtr->origValues = oldValues;
|
||||
FitCurveFromSamples(*chPtr);
|
||||
@@ -771,29 +933,9 @@ void CurveEditorPanel::HandleCanvasInput(ImVec2 cMin, ImVec2 cMax)
|
||||
bool hovered = (mouse.x >= cMin.x && mouse.x <= cMax.x &&
|
||||
mouse.y >= cMin.y && mouse.y <= cMax.y);
|
||||
|
||||
if (!hovered && !m_isPanning && !m_isDragging && !m_isDraggingCursor
|
||||
&& !m_isBoxSelecting)
|
||||
return;
|
||||
|
||||
// ── Time cursor drag ─────────────────────────────────────────────────────
|
||||
if (!m_displayTime.IsDefault()) {
|
||||
float cx = TimeToX(m_displayTime.GetValue(), cMin, cMax);
|
||||
bool nearCursor = (std::fabs(mouse.x - cx) < 8.f && mouse.y >= cMin.y &&
|
||||
mouse.y <= cMin.y + 18.f);
|
||||
if (nearCursor && ImGui::IsMouseClicked(0))
|
||||
m_isDraggingCursor = true;
|
||||
if (m_isDraggingCursor && ImGui::IsMouseDown(0)) {
|
||||
double t = XToTime(mouse.x, cMin, cMax);
|
||||
if (m_snapToFrame) t = std::round(t);
|
||||
if (OnScrub) OnScrub(t);
|
||||
}
|
||||
if (ImGui::IsMouseReleased(0)) m_isDraggingCursor = false;
|
||||
}
|
||||
if (m_isDraggingCursor) return;
|
||||
|
||||
// ── Hit test helpers ─────────────────────────────────────────────────────
|
||||
const float kKeyRadius = 8.f;
|
||||
const float kHandleRadius = 7.f;
|
||||
// ── Hit test helpers — defined early so the RMB popup can use them ──────────
|
||||
const float kKeyRadius = 8.f;
|
||||
const float kHandleRadius = 7.f;
|
||||
|
||||
auto hitTestKey = [&](ImVec2 p) -> std::pair<int,int> {
|
||||
for (int ci = 0; ci < int(m_channels.size()); ++ci) {
|
||||
@@ -809,7 +951,6 @@ void CurveEditorPanel::HandleCanvasInput(ImVec2 cMin, ImVec2 cMax)
|
||||
};
|
||||
|
||||
auto hitTestHandle = [&](ImVec2 p) -> SelectedHandle {
|
||||
// Test all visible channels/keys — handles are always shown
|
||||
for (int ci = 0; ci < int(m_channels.size()); ++ci) {
|
||||
const CurveChannel& ch = m_channels[ci];
|
||||
if (!ch.visible) continue;
|
||||
@@ -834,6 +975,86 @@ void CurveEditorPanel::HandleCanvasInput(ImVec2 cMin, ImVec2 cMax)
|
||||
return {-1,-1,HandlePart::Key};
|
||||
};
|
||||
|
||||
// ── RMB context menu — must be processed before the early-return so the
|
||||
// popup stays alive after the mouse leaves the canvas into the menu. ────────
|
||||
if (ImGui::IsMouseClicked(1) && hovered) {
|
||||
auto [ci, ki] = hitTestKey(mouse);
|
||||
if (ci >= 0) {
|
||||
if (!IsSelected(ci, ki, HandlePart::Key))
|
||||
Select(ci, ki, HandlePart::Key, false);
|
||||
ImGui::OpenPopup("##keyCtx");
|
||||
}
|
||||
}
|
||||
if (ImGui::BeginPopup("##keyCtx")) {
|
||||
if (ImGui::MenuItem("Delete Key"))
|
||||
DeleteSelectedKeys();
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Flatten Tangents")) {
|
||||
for (const auto& s : m_selection) {
|
||||
if (s.part != HandlePart::Key) continue;
|
||||
if (s.chanIdx < 0 || s.chanIdx >= int(m_channels.size())) continue;
|
||||
CurveChannel& ch = m_channels[s.chanIdx];
|
||||
if (s.keyIdx < 0 || s.keyIdx >= int(ch.keys.size())) continue;
|
||||
BezierKey& bk = ch.keys[s.keyIdx];
|
||||
bk.inTangentDv = 0.0;
|
||||
bk.outTangentDv = 0.0;
|
||||
ch.dirty = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Break Tangents")) {
|
||||
for (const auto& s : m_selection) {
|
||||
if (s.part != HandlePart::Key) continue;
|
||||
if (s.chanIdx < 0 || s.chanIdx >= int(m_channels.size())) continue;
|
||||
CurveChannel& ch = m_channels[s.chanIdx];
|
||||
if (s.keyIdx < 0 || s.keyIdx >= int(ch.keys.size())) continue;
|
||||
ch.keys[s.keyIdx].tangentsBroken = true;
|
||||
ch.dirty = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Unify Tangents")) {
|
||||
for (const auto& s : m_selection) {
|
||||
if (s.part != HandlePart::Key) continue;
|
||||
if (s.chanIdx < 0 || s.chanIdx >= int(m_channels.size())) continue;
|
||||
CurveChannel& ch = m_channels[s.chanIdx];
|
||||
if (s.keyIdx < 0 || s.keyIdx >= int(ch.keys.size())) continue;
|
||||
BezierKey& bk = ch.keys[s.keyIdx];
|
||||
bk.tangentsBroken = false;
|
||||
bk.inTangentDv = -bk.outTangentDv *
|
||||
((-bk.inTangentDt > 1e-10) ? -bk.inTangentDt/bk.outTangentDt : 1.0);
|
||||
ch.dirty = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Auto Tangents")) {
|
||||
for (const auto& s : m_selection) {
|
||||
if (s.part != HandlePart::Key) continue;
|
||||
if (s.chanIdx < 0 || s.chanIdx >= int(m_channels.size())) continue;
|
||||
RefitNeighborTangents(m_channels[s.chanIdx], s.keyIdx);
|
||||
m_channels[s.chanIdx].dirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
if (!hovered && !m_isPanning && !m_isDragging && !m_isDraggingCursor
|
||||
&& !m_isBoxSelecting)
|
||||
return;
|
||||
|
||||
// ── Time cursor drag ─────────────────────────────────────────────────────
|
||||
if (!m_displayTime.IsDefault()) {
|
||||
float cx = TimeToX(m_displayTime.GetValue(), cMin, cMax);
|
||||
bool nearCursor = (std::fabs(mouse.x - cx) < 8.f && mouse.y >= cMin.y &&
|
||||
mouse.y <= cMin.y + 18.f);
|
||||
if (nearCursor && ImGui::IsMouseClicked(0))
|
||||
m_isDraggingCursor = true;
|
||||
if (m_isDraggingCursor && ImGui::IsMouseDown(0)) {
|
||||
double t = XToTime(mouse.x, cMin, cMax);
|
||||
if (m_snapToFrame) t = std::round(t);
|
||||
if (OnScrub) OnScrub(t);
|
||||
}
|
||||
if (ImGui::IsMouseReleased(0)) m_isDraggingCursor = false;
|
||||
}
|
||||
if (m_isDraggingCursor) return;
|
||||
|
||||
// ── Pan: MMB or Alt+LMB ──────────────────────────────────────────────────
|
||||
bool wantPan = ImGui::IsMouseDown(2) ||
|
||||
(ImGui::IsMouseDown(0) && io.KeyAlt);
|
||||
@@ -1050,65 +1271,6 @@ void CurveEditorPanel::HandleCanvasInput(ImVec2 cMin, ImVec2 cMax)
|
||||
}
|
||||
}
|
||||
|
||||
// ── RMB context menu on key ───────────────────────────────────────────────
|
||||
if (ImGui::IsMouseClicked(1) && hovered) {
|
||||
auto [ci, ki] = hitTestKey(mouse);
|
||||
if (ci >= 0) {
|
||||
if (!IsSelected(ci, ki, HandlePart::Key))
|
||||
Select(ci, ki, HandlePart::Key, false);
|
||||
ImGui::OpenPopup("##keyCtx");
|
||||
}
|
||||
}
|
||||
if (ImGui::BeginPopup("##keyCtx")) {
|
||||
if (ImGui::MenuItem("Delete Key"))
|
||||
DeleteSelectedKeys();
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Flatten Tangents")) {
|
||||
for (const auto& s : m_selection) {
|
||||
if (s.part != HandlePart::Key) continue;
|
||||
if (s.chanIdx < 0 || s.chanIdx >= int(m_channels.size())) continue;
|
||||
CurveChannel& ch = m_channels[s.chanIdx];
|
||||
if (s.keyIdx < 0 || s.keyIdx >= int(ch.keys.size())) continue;
|
||||
BezierKey& bk = ch.keys[s.keyIdx];
|
||||
bk.inTangentDv = 0.0;
|
||||
bk.outTangentDv = 0.0;
|
||||
ch.dirty = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Break Tangents")) {
|
||||
for (const auto& s : m_selection) {
|
||||
if (s.part != HandlePart::Key) continue;
|
||||
if (s.chanIdx < 0 || s.chanIdx >= int(m_channels.size())) continue;
|
||||
CurveChannel& ch = m_channels[s.chanIdx];
|
||||
if (s.keyIdx < 0 || s.keyIdx >= int(ch.keys.size())) continue;
|
||||
ch.keys[s.keyIdx].tangentsBroken = true;
|
||||
ch.dirty = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Unify Tangents")) {
|
||||
for (const auto& s : m_selection) {
|
||||
if (s.part != HandlePart::Key) continue;
|
||||
if (s.chanIdx < 0 || s.chanIdx >= int(m_channels.size())) continue;
|
||||
CurveChannel& ch = m_channels[s.chanIdx];
|
||||
if (s.keyIdx < 0 || s.keyIdx >= int(ch.keys.size())) continue;
|
||||
BezierKey& bk = ch.keys[s.keyIdx];
|
||||
bk.tangentsBroken = false;
|
||||
// Force mirror
|
||||
bk.inTangentDv = -bk.outTangentDv *
|
||||
((-bk.inTangentDt > 1e-10) ? -bk.inTangentDt/bk.outTangentDt : 1.0);
|
||||
ch.dirty = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Auto Tangents")) {
|
||||
for (const auto& s : m_selection) {
|
||||
if (s.part != HandlePart::Key) continue;
|
||||
if (s.chanIdx < 0 || s.chanIdx >= int(m_channels.size())) continue;
|
||||
RefitNeighborTangents(m_channels[s.chanIdx], s.keyIdx);
|
||||
m_channels[s.chanIdx].dirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Toolbar ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -97,9 +97,13 @@ private:
|
||||
void RevertAll();
|
||||
void DeleteSelectedKeys();
|
||||
void AddKeyAt(int chanIdx, double time, double value);
|
||||
// Re-fit Catmull-Rom tangents for neighbors of a modified key
|
||||
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 LoadBezierFromMetadata(const pxr::UsdPrim& prim, CurveChannel& ch) const;
|
||||
|
||||
// ── State ────────────────────────────────────────────────────────────────
|
||||
|
||||
pxr::UsdStageRefPtr m_stage;
|
||||
|
||||
Reference in New Issue
Block a user