Overview
Second chunk of slice D from #260. Builds on D1 (#375) by adding per-channel expansion to each bone row in the dope sheet — TX / TY / TZ / RW / RX / RY / RZ / SX / SY / SZ shown as sub-rows under the bone.
D3 (the curve editor + Bezier handles) is the remaining chunk.
Scope (this slice)
Controller
Q_INVOKABLE QVariantList allBoneRows() already returns { bone, keyTimes }. Extend each row with a channels map: for each animated bone, indicate which TRS channels actually deviate from the bind pose at any keyframe. Avoids painting empty sub-rows for channels the clip never touches.
- Specifically: a channel
tx is "active" if any keyframe on the track has translate.x non-zero (within epsilon). Same for the rest.
- New optional sub-row keyframe API for D3 — not implemented in this slice; sub-row diamonds reuse the existing per-track keyframes.
QML
- Bone row gains a chevron (▶/▼) on the left of the bone name that toggles per-channel expansion. Persists per-bone in QML state.
- When expanded, render up to 9 sub-rows (one per active channel, in TRS order). Each sub-row has a single horizontal track with diamonds at the same key times as the parent bone, colored per channel:
- T.X = red, T.Y = green, T.Z = blue
- R.W = magenta, R.X = red, R.Y = green, R.Z = blue
- S.X = orange-red, S.Y = orange-green, S.Z = orange-blue
- Sub-row diamonds participate in selection / drag / copy-paste as the parent keyframe in this slice — moving a sub-row diamond moves the entire keyframe time. Per-channel-only edits + Bezier handles arrive in slice D3.
- Sub-row Y-coordinates inherit the same row-height (22 px) as the parent. The dope sheet expands vertically when a bone is expanded.
Marquee + bulk move
- Marquee continues to operate on the parent bone keyframes; sub-rows are visual decoration in this slice.
- Bulk move on a parent diamond moves all channels (no change from D1).
Out of scope
- Per-channel keyframe values (slice D3 introduces a side-table or extended TransformKeyFrame).
- Bezier handles (slice D3).
- Per-channel marquee selection (would only matter once D3 lands).
Tests
Pure-data:
allBoneRows() shape: each row now has a channels field, populated only with channels whose values actually differ from bind pose somewhere on the track.
Ogre fixture:
- TestAnim's Child track has only translate motion (0.5 → 0.5 on x at t=0.5). Verify
tx is active, ty/tz/r*/s* are not.
Acceptance criteria
Follows: #373 (D / dope sheet view), #375 (D1 / multi-select + copy/paste).
Overview
Second chunk of slice D from #260. Builds on D1 (#375) by adding per-channel expansion to each bone row in the dope sheet — TX / TY / TZ / RW / RX / RY / RZ / SX / SY / SZ shown as sub-rows under the bone.
D3 (the curve editor + Bezier handles) is the remaining chunk.
Scope (this slice)
Controller
Q_INVOKABLE QVariantList allBoneRows()already returns{ bone, keyTimes }. Extend each row with achannelsmap: for each animated bone, indicate which TRS channels actually deviate from the bind pose at any keyframe. Avoids painting empty sub-rows for channels the clip never touches.txis "active" if any keyframe on the track hastranslate.xnon-zero (within epsilon). Same for the rest.QML
Marquee + bulk move
Out of scope
Tests
Pure-data:
allBoneRows()shape: each row now has achannelsfield, populated only with channels whose values actually differ from bind pose somewhere on the track.Ogre fixture:
txis active,ty/tz/r*/s*are not.Acceptance criteria
Plan (#260)
Follows: #373 (D / dope sheet view), #375 (D1 / multi-select + copy/paste).