Overview
Final chunk of slice D from #260. Adds a curve editor view alongside the dope sheet: per-channel curves with Bezier handles, interpolation-mode-per-keyframe, multi-channel display with color coding.
This is the largest D-slice — landing it as one PR may be impractical. Plan to split into D3a (data model + read-only view) and D3b (handle-drag + interp modes + resample) if implementation grows too big.
Why a side-table?
Ogre's TransformKeyFrame stores translate, rotation, and scale at a time — but no Bezier tangent handles, no interpolation mode flag. We need to keep that data on the side and re-emit linear samples into the underlying TransformKeyFrame whenever the curve changes, so Ogre's existing playback path keeps working.
Scope
Data model (C++)
- New
CurveEditModel class. Keyed by (skeleton, animationName, boneName, channelId, time) with channelId ∈ {tx, ty, tz, rw, rx, ry, rz, sx, sy, sz}.
- Per-keyframe entry:
{ inTangent: float, outTangent: float, mode: enum }.
- Modes:
Bezier, Linear, Stepped, Auto (Catmull-Rom auto-tangents from neighbors).
- API:
tangentsAt(skel, anim, bone, channel, time) → {in, out, mode}
setTangent(skel, anim, bone, channel, time, in, out)
setMode(skel, anim, bone, channel, time, mode)
evaluate(skel, anim, bone, channel, time) → float — sample the curve
- Side-table is in-memory only in this slice. Persistence (sidecar JSON) is a follow-up issue.
Resampler
When a tangent or mode changes, resample N points per second across the affected segment(s) into the underlying TransformKeyFrame. N is adaptive to local curvature (more samples where the second derivative is large) to keep file size reasonable.
- Default N = 30 samples per second; bumped to 60 around high-curvature regions.
- Limit to ±200 keyframes per segment to bound worst-case insertion.
QML — AnimationCurveEditor.qml
- New QML view, hosted in the same dock area as the Dope Sheet (toggle between them, or a tab widget).
- X axis = time (seconds), Y axis = channel value.
- Renders the selected channels as colored curves (red/green/blue T, magenta+RGB R, orange-tinted RGB S — matches D2 sub-row colors).
- Each keyframe shows as a square; drag to retime + change value.
- Each keyframe has two handles (in / out tangent); drag to adjust curve shape.
- Per-keyframe interp-mode dropdown shown on hover.
- Wheel = zoom around cursor (X + Y independently); middle-drag = pan.
Selection integration
- Reuses Dope Sheet's selection state — selected keyframe diamonds in the Dope Sheet appear as selected squares in the Curve Editor.
- Multi-select / bulk-drag still operates time-only (consistent with D1).
Tests
- Pure-data: tangent storage round-trip; auto-tangent computation matches Catmull-Rom math; evaluate(t) at a keyframe equals stored value.
- Ogre-fixture: setting a Bezier handle resamples into TransformKeyFrames; the resampled animation matches the curve at sample points within tolerance.
Out of scope
- Sidecar persistence (separate issue).
- Multi-channel batch editing (e.g. set all selected to "Linear" at once) — natural follow-up.
- Per-channel marquee selection (mostly redundant with Dope Sheet's selection).
Acceptance criteria
Follows: #373 (slice C), #375 (D1), #377 (D2). Last animation slice in Phase 5.
Overview
Final chunk of slice D from #260. Adds a curve editor view alongside the dope sheet: per-channel curves with Bezier handles, interpolation-mode-per-keyframe, multi-channel display with color coding.
This is the largest D-slice — landing it as one PR may be impractical. Plan to split into D3a (data model + read-only view) and D3b (handle-drag + interp modes + resample) if implementation grows too big.
Why a side-table?
Ogre's
TransformKeyFramestores translate, rotation, and scale at a time — but no Bezier tangent handles, no interpolation mode flag. We need to keep that data on the side and re-emit linear samples into the underlying TransformKeyFrame whenever the curve changes, so Ogre's existing playback path keeps working.Scope
Data model (C++)
CurveEditModelclass. Keyed by(skeleton, animationName, boneName, channelId, time)with channelId ∈ {tx, ty, tz, rw, rx, ry, rz, sx, sy, sz}.{ inTangent: float, outTangent: float, mode: enum }.Bezier,Linear,Stepped,Auto(Catmull-Rom auto-tangents from neighbors).tangentsAt(skel, anim, bone, channel, time) → {in, out, mode}setTangent(skel, anim, bone, channel, time, in, out)setMode(skel, anim, bone, channel, time, mode)evaluate(skel, anim, bone, channel, time) → float— sample the curveResampler
When a tangent or mode changes, resample N points per second across the affected segment(s) into the underlying TransformKeyFrame. N is adaptive to local curvature (more samples where the second derivative is large) to keep file size reasonable.
QML —
AnimationCurveEditor.qmlSelection integration
Tests
Out of scope
Acceptance criteria
Plan (#260)
Follows: #373 (slice C), #375 (D1), #377 (D2). Last animation slice in Phase 5.