Group timeline rows by object and edit values in the row (#408)
The rows of the animation timeline stood in the order they were keyed. With several objects and properties the list was hard to read, and a value could only be changed by picking a key and using the inspector. The Play button also kept saying Pause after scrubbing had paused. - Each object gets a header row with its name, indented by depth. The groups follow the scene hierarchy, the root first. Rows whose object is missing go to the end. - Under an object the rows follow the order in which it lists its properties: X, Y, Z, then the order of the inspector. Only the view is sorted, the animation itself is unchanged and there is no undo step. After a move in the hierarchy the order is right by itself. - The row head shows the same field as the inspector with the value at the playhead. Without keys it shows the value of the object, for a relative row the offset. - A change in that field sets or changes the key at the playhead, selects it and is one undo step. A running playback pauses first. - The Play/Pause button follows every change of the playing state, so scrubbing that pauses shows Play again. Tests: - Unit animation + timeline-view: 100 passed, 3 new. - e2e 25 on melektaus-atx: ALL PASSED, 34 checks, 3 new (groups, order and values at frame 4, a typed 55 becomes a selected key on frame 4, Play then a click on the ruler shows Play and pauses). - Gate on melektaus-atx: exit 0, 297 files, 4561 tests. - tsc, eslint, Prettier clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd60a795e1
commit
8679dfbe65
9 files changed
+449
-69
No files matched your search
@@ -36,6 +36,7 @@ import {
|
||||
frameCount,
|
||||
isPositionRow,
|
||||
leadingPlayer,
|
||||
lineValueAt,
|
||||
NOT_PLAYING,
|
||||
offeredDefs,
|
||||
pauseAnimation,
|
||||
@@ -49,6 +50,8 @@ import {
|
||||
stopAnimation,
|
||||
targetDefs,
|
||||
targetNamePath,
|
||||
type TimelineLine,
|
||||
timelineLines,
|
||||
trackDef,
|
||||
trackLabel,
|
||||
trackValueAt,
|
||||
@@ -422,6 +425,79 @@ describe('editing the timeline', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the timeline's header (#408)", () => {
|
||||
/** A scene with `A` and `C` at the top and `B` under `A`, placed in the order A, C, B. */
|
||||
function scene() {
|
||||
const f = makeSpriteScene();
|
||||
const a = place(f, f.spriteId, { x: 5, y: 0 });
|
||||
const c = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const b = place(f, f.spriteId, { x: 0, y: 0 }, a);
|
||||
const named = (id: string, name: string) => (f.sceneOf().objects.find((o) => o.id === id)!.name = name);
|
||||
named(a, 'A');
|
||||
named(b, 'B');
|
||||
named(c, 'C');
|
||||
return { f, a, b, c };
|
||||
}
|
||||
|
||||
const shape = (lines: TimelineLine[]) =>
|
||||
lines.map((l) => (l.kind === 'group' ? `${l.name}@${l.depth}` : ` ${l.track.property}`));
|
||||
|
||||
it('groups the rows by object in the order of the hierarchy, each in the order of its properties', () => {
|
||||
const { f, a, b, c } = scene();
|
||||
const walk = addAnimation(f, f.sceneId);
|
||||
addRow(f, walk, 'rotation', [], c);
|
||||
addRow(f, walk, 'scale', [], a);
|
||||
addRow(f, walk, 'rotation', [], b);
|
||||
addRow(f, walk, 'backgroundColor', []);
|
||||
addRow(f, walk, 'x', [], a);
|
||||
addRow(f, walk, 'enabled', [], a);
|
||||
current(f, walk).tracks.push({ id: 'lost', target: 'gone', property: 'x', keys: [] });
|
||||
expect(shape(timelineLines(f.project, current(f, walk)))).toEqual([
|
||||
`${f.sceneOf().name}@0`,
|
||||
' backgroundColor',
|
||||
'A@0',
|
||||
' x',
|
||||
' enabled',
|
||||
' scale',
|
||||
'B@1',
|
||||
' rotation',
|
||||
'C@0',
|
||||
' rotation',
|
||||
'(missing)@0',
|
||||
' x',
|
||||
]);
|
||||
// Only the view is sorted.
|
||||
expect(current(f, walk).tracks.map((t) => t.property)[0]).toBe('rotation');
|
||||
});
|
||||
|
||||
it('shows the value at the playhead, what the object holds before the first key, and an offset when relative', () => {
|
||||
const { f, a } = scene();
|
||||
const walk = addAnimation(f, f.sceneId);
|
||||
addRow(f, walk, 'x', [], a);
|
||||
addRow(
|
||||
f,
|
||||
walk,
|
||||
'scale',
|
||||
[
|
||||
[0, 1],
|
||||
[10, 3],
|
||||
],
|
||||
a,
|
||||
);
|
||||
const y = addRow(f, walk, 'y', [], a);
|
||||
f.stack.execute(new SetTrackRelativeCommand(walk.id, y, true));
|
||||
const row = (property: string) =>
|
||||
timelineLines(f.project, current(f, walk)).find(
|
||||
(l): l is TimelineLine & { kind: 'row' } => l.kind === 'row' && l.track.property === property,
|
||||
)!;
|
||||
expect(lineValueAt(row('x'), 4)).toBe(5);
|
||||
expect(lineValueAt(row('y'), 4)).toBe(0);
|
||||
expect(lineValueAt(row('scale'), 5)).toBeCloseTo(2);
|
||||
current(f, walk).tracks.push({ id: 'lost', target: 'gone', property: 'x', keys: [] });
|
||||
expect(lineValueAt(row('x'), 4)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copy, paste, duplicate and frames (#102)', () => {
|
||||
it('pastes relative to the frame, into the same rows, replacing a key there, and undoes in one step', () => {
|
||||
const f = makeSpriteScene();
|
||||
|
||||
Reference in new issue
Block a user