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
@@ -422,6 +422,62 @@ export async function animation(t) {
|
||||
JSON.stringify({ emptied: emptied.tracks, undone: undone.tracks }),
|
||||
);
|
||||
|
||||
// 25i. The header (#408): rows grouped under their object in the order of the hierarchy, each
|
||||
// object's rows in the order of its properties, the value at the playhead in the row, and
|
||||
// a value typed there keys it at the playhead.
|
||||
await mutate(`
|
||||
__gm.state.project.assets.find((a) => a.id === 'an-walk').tracks = [
|
||||
{ id: 'an-r1', target: 'an-arm', property: 'color', keys: [] },
|
||||
{ id: 'an-r2', target: '', property: 'enabled', keys: [] },
|
||||
{ id: 'an-r3', target: 'an-arm', property: 'x', keys: [{ frame: 0, value: 0 }, { frame: 10, value: 100 }] },
|
||||
];
|
||||
`);
|
||||
await evaluate('__gm.animation.scrub(4)');
|
||||
await sleep(150);
|
||||
const lines = await json('__gm.animation.lines()');
|
||||
const shape = lines.map((l) => (l.kind === 'group' ? l.text : ` ${l.text}=${JSON.stringify(l.value)}`));
|
||||
check(
|
||||
'the header groups the rows under Dancer and Arm, X before Color, each with its value at frame 4',
|
||||
JSON.stringify(shape) ===
|
||||
JSON.stringify(['Dancer', ' Enabled=true', 'Arm', ' X=40', ` Color=${0xff0000}`]) &&
|
||||
lines[1].indent > lines[0].indent,
|
||||
JSON.stringify(lines),
|
||||
);
|
||||
await shot('20-grouped-header');
|
||||
const xField = await json("__gm.animation.valueAt('an-r3')");
|
||||
await click(xField.x, xField.y);
|
||||
await typeText('55');
|
||||
await press('Enter');
|
||||
await sleep(150);
|
||||
const typed = (await walk()).tracks.find((r) => r.id === 'an-r3').keys;
|
||||
const typedPick = await json('__gm.state.selectedKeys');
|
||||
check(
|
||||
'a value typed into the row keys it at the playhead and picks that key',
|
||||
JSON.stringify(typed) ===
|
||||
JSON.stringify([
|
||||
{ frame: 0, value: 0 },
|
||||
{ frame: 4, value: 55 },
|
||||
{ frame: 10, value: 100 },
|
||||
]) && JSON.stringify(typedPick) === JSON.stringify([{ trackId: 'an-r3', frame: 4 }]),
|
||||
JSON.stringify({ typed, typedPick }),
|
||||
);
|
||||
|
||||
// Pressing the ruler while playing pauses – and the button says Play again.
|
||||
const playButton = await json('__gm.animation.playAt()');
|
||||
await click(playButton.x, playButton.y);
|
||||
const whilePlaying = (await json('__gm.animation.playAt()')).text;
|
||||
const ruler = await json('__gm.animation.rulerAt(2)');
|
||||
await click(ruler.x, ruler.y);
|
||||
const scrubbed = {
|
||||
text: (await json('__gm.animation.playAt()')).text,
|
||||
...(await json('__gm.animation.view()')),
|
||||
};
|
||||
check(
|
||||
'scrubbing on the ruler while playing pauses, and the button turns back to Play',
|
||||
whilePlaying === 'Pause' && scrubbed.text === 'Play' && scrubbed.playing === false,
|
||||
JSON.stringify({ whilePlaying, scrubbed }),
|
||||
);
|
||||
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
p.assets = p.assets.filter((a) => !['an-walk', 'an-box', 'an-sprite'].includes(a.id));
|
||||
|
||||
Reference in new issue
Block a user