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>
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>
Maya-style Graph Editor panel (View > Curve Editor) that fits Catmull-Rom
Bezier curves through USD time samples, allows interactive editing with
tangent handles, and bakes the result back to dense linear samples on Apply.
Key behaviours:
- Left panel lists all animated attributes of the selected prim, decomposed
into per-component channels (translate [X/Y/Z], etc.) with colour swatches
and visibility toggles
- ImDrawList canvas: smooth Bezier polylines, always-visible tangent handle
lines/circles, diamond keyframe markers
- Pan (MMB/Alt+drag), zoom (scroll / Shift+scroll), Frame All (F)
- LMB drag moves keyframes; tangent handle drag reshapes curve with mirrored
or broken handles; box-select for multi-selection
- Double-click canvas adds a keyframe; Delete removes selected keyframes
- RMB context menu: Delete, Flatten, Break/Unify Tangents, Auto Tangents
- Simplify toggle (Ramer-Douglas-Peucker) reduces baked sample count
- Time cursor draggable to scrub the timeline
- UsdNotice::ObjectsChanged listener re-fits clean channels whenever the
stage changes externally (Property Panel edits, Auto-Key, undo/redo),
while preserving channels with unsaved Bezier edits
- Bake is a single undoable AttributeSetCommand (Ctrl+Z restores original
sparse samples); Revert discards in-editor edits without touching USD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>