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:
+246
-84
@@ -12,8 +12,11 @@
|
|||||||
#include <pxr/base/gf/vec4d.h>
|
#include <pxr/base/gf/vec4d.h>
|
||||||
#include <pxr/usd/sdf/valueTypeName.h>
|
#include <pxr/usd/sdf/valueTypeName.h>
|
||||||
#include <pxr/usd/usd/notice.h>
|
#include <pxr/usd/usd/notice.h>
|
||||||
|
#include <pxr/usd/usd/prim.h>
|
||||||
#include <pxr/base/tf/notice.h>
|
#include <pxr/base/tf/notice.h>
|
||||||
#include <pxr/base/tf/weakPtr.h>
|
#include <pxr/base/tf/weakPtr.h>
|
||||||
|
#include <pxr/base/vt/dictionary.h>
|
||||||
|
#include <pxr/base/vt/array.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <imgui_internal.h>
|
#include <imgui_internal.h>
|
||||||
@@ -278,6 +281,140 @@ void CurveEditorPanel::RefreshFromStage()
|
|||||||
if (!m_channels.empty()) FrameAll();
|
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 ───────────────────────────────────────────────────────────
|
// ── Catmull-Rom fit ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
void CurveEditorPanel::FitCurveFromSamples(CurveChannel& ch)
|
void CurveEditorPanel::FitCurveFromSamples(CurveChannel& ch)
|
||||||
@@ -300,6 +437,15 @@ void CurveEditorPanel::FitCurveFromSamples(CurveChannel& ch)
|
|||||||
ch.origTimes = times;
|
ch.origTimes = times;
|
||||||
ch.origValues = vals;
|
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());
|
int n = int(times.size());
|
||||||
for (int i = 0; i < n; ++i) {
|
for (int i = 0; i < n; ++i) {
|
||||||
BezierKey k;
|
BezierKey k;
|
||||||
@@ -465,6 +611,13 @@ 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;
|
||||||
|
UsdPrim prim = m_stage ? m_stage->GetPrimAtPath(SdfPath(m_selectedPrimPath))
|
||||||
|
: UsdPrim{};
|
||||||
|
VtValue oldCEMeta;
|
||||||
|
if (prim) oldCEMeta = prim.GetCustomDataByKey(TfToken("curveEditor"));
|
||||||
|
|
||||||
if (!m_commandHistory) {
|
if (!m_commandHistory) {
|
||||||
ClearAllTimeSamples(attr);
|
ClearAllTimeSamples(attr);
|
||||||
for (int i = 0; i < int(newTimes.size()); ++i)
|
for (int i = 0; i < int(newTimes.size()); ++i)
|
||||||
@@ -472,6 +625,7 @@ void CurveEditorPanel::BakeChannelToUsd(CurveChannel& ch)
|
|||||||
ch.origTimes = newTimes;
|
ch.origTimes = newTimes;
|
||||||
ch.origValues = newValues;
|
ch.origValues = newValues;
|
||||||
ch.dirty = false;
|
ch.dirty = false;
|
||||||
|
if (prim) SaveBezierToMetadata(prim, ch);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,19 +633,27 @@ void CurveEditorPanel::BakeChannelToUsd(CurveChannel& ch)
|
|||||||
CurveChannel* chPtr = &ch;
|
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, chPtr]() {
|
[this, attr, comp, newTimes, newValues, newKeys, prim, chPtr]() {
|
||||||
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]);
|
||||||
// Update channel cache — safe because bake triggers before prim switch
|
|
||||||
chPtr->origTimes = newTimes;
|
chPtr->origTimes = newTimes;
|
||||||
chPtr->origValues = newValues;
|
chPtr->origValues = newValues;
|
||||||
|
chPtr->keys = newKeys;
|
||||||
chPtr->dirty = false;
|
chPtr->dirty = false;
|
||||||
|
if (prim) SaveBezierToMetadata(prim, *chPtr);
|
||||||
},
|
},
|
||||||
[this, attr, comp, oldTimes, oldValues, chPtr]() {
|
[this, attr, comp, oldTimes, oldValues, oldCEMeta, prim, chPtr]() {
|
||||||
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
|
||||||
|
if (prim) {
|
||||||
|
if (oldCEMeta.IsEmpty())
|
||||||
|
prim.ClearCustomDataByKey(TfToken("curveEditor"));
|
||||||
|
else
|
||||||
|
prim.SetCustomDataByKey(TfToken("curveEditor"), oldCEMeta);
|
||||||
|
}
|
||||||
chPtr->origTimes = oldTimes;
|
chPtr->origTimes = oldTimes;
|
||||||
chPtr->origValues = oldValues;
|
chPtr->origValues = oldValues;
|
||||||
FitCurveFromSamples(*chPtr);
|
FitCurveFromSamples(*chPtr);
|
||||||
@@ -771,27 +933,7 @@ void CurveEditorPanel::HandleCanvasInput(ImVec2 cMin, ImVec2 cMax)
|
|||||||
bool hovered = (mouse.x >= cMin.x && mouse.x <= cMax.x &&
|
bool hovered = (mouse.x >= cMin.x && mouse.x <= cMax.x &&
|
||||||
mouse.y >= cMin.y && mouse.y <= cMax.y);
|
mouse.y >= cMin.y && mouse.y <= cMax.y);
|
||||||
|
|
||||||
if (!hovered && !m_isPanning && !m_isDragging && !m_isDraggingCursor
|
// ── Hit test helpers — defined early so the RMB popup can use them ──────────
|
||||||
&& !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 kKeyRadius = 8.f;
|
||||||
const float kHandleRadius = 7.f;
|
const float kHandleRadius = 7.f;
|
||||||
|
|
||||||
@@ -809,7 +951,6 @@ void CurveEditorPanel::HandleCanvasInput(ImVec2 cMin, ImVec2 cMax)
|
|||||||
};
|
};
|
||||||
|
|
||||||
auto hitTestHandle = [&](ImVec2 p) -> SelectedHandle {
|
auto hitTestHandle = [&](ImVec2 p) -> SelectedHandle {
|
||||||
// Test all visible channels/keys — handles are always shown
|
|
||||||
for (int ci = 0; ci < int(m_channels.size()); ++ci) {
|
for (int ci = 0; ci < int(m_channels.size()); ++ci) {
|
||||||
const CurveChannel& ch = m_channels[ci];
|
const CurveChannel& ch = m_channels[ci];
|
||||||
if (!ch.visible) continue;
|
if (!ch.visible) continue;
|
||||||
@@ -834,6 +975,86 @@ void CurveEditorPanel::HandleCanvasInput(ImVec2 cMin, ImVec2 cMax)
|
|||||||
return {-1,-1,HandlePart::Key};
|
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 ──────────────────────────────────────────────────
|
// ── Pan: MMB or Alt+LMB ──────────────────────────────────────────────────
|
||||||
bool wantPan = ImGui::IsMouseDown(2) ||
|
bool wantPan = ImGui::IsMouseDown(2) ||
|
||||||
(ImGui::IsMouseDown(0) && io.KeyAlt);
|
(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 ───────────────────────────────────────────────────────────────────
|
// ── Toolbar ───────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -97,9 +97,13 @@ private:
|
|||||||
void RevertAll();
|
void RevertAll();
|
||||||
void DeleteSelectedKeys();
|
void DeleteSelectedKeys();
|
||||||
void AddKeyAt(int chanIdx, double time, double value);
|
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);
|
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 ────────────────────────────────────────────────────────────────
|
// ── State ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pxr::UsdStageRefPtr m_stage;
|
pxr::UsdStageRefPtr m_stage;
|
||||||
|
|||||||
Reference in New Issue
Block a user