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
@@ -577,6 +577,77 @@ export function trackLabel(project: Project, animation: AnimationAsset, track: A
|
||||
return `${who} · ${def ? def.label : aimed ? `${track.property} (missing)` : track.property}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A line of the timeline's header column (#408): the head of an object's group, or a row under it,
|
||||
* named by its property alone. `depth` is the object's in the hierarchy. A row carries its property
|
||||
* (none once that is gone) and `held`, what it shows before it has keys: it writes nothing then, so
|
||||
* that is what the object holds – or no offset at all for a relative row.
|
||||
*/
|
||||
export type TimelineLine =
|
||||
| { kind: 'group'; target: string; name: string; depth: number }
|
||||
| {
|
||||
kind: 'row';
|
||||
track: AnimationTrack;
|
||||
label: string;
|
||||
def: PropertyDef | undefined;
|
||||
held: PropertyValue;
|
||||
depth: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The rows grouped by the object they aim at, in the order the objects stand in the hierarchy – the
|
||||
* root first – and under each object in the order it lists its properties. Rows aiming at something
|
||||
* gone come last, as they were added. Only the view is sorted: the asset keeps its own order.
|
||||
*/
|
||||
export function timelineLines(project: Project, animation: AnimationAsset): TimelineLine[] {
|
||||
const placed = new Map<string, { at: number; depth: number }>([['', { at: 0, depth: 0 }]]);
|
||||
animationTargets(project, findAsset(project, animation.root)).forEach((t, i) =>
|
||||
placed.set(t.id, { at: i + 1, depth: t.depth }),
|
||||
);
|
||||
const targets = [...new Set(animation.tracks.map((t) => t.target))];
|
||||
const rank = (target: string) => placed.get(target)?.at ?? placed.size + targets.indexOf(target);
|
||||
const lines: TimelineLine[] = [];
|
||||
for (const target of targets.sort((a, b) => rank(a) - rank(b))) {
|
||||
const aimed = aimedAt(project, animation, target);
|
||||
const defs = aimed ? animatableDefsOf(project, aimed.asset, aimed.bag) : [];
|
||||
const depth = placed.get(target)?.depth ?? 0;
|
||||
const name = !aimed
|
||||
? '(missing)'
|
||||
: aimed.object
|
||||
? objectName(aimed.object, aimed.asset)
|
||||
: aimed.asset.name;
|
||||
lines.push({ kind: 'group', target, name, depth });
|
||||
const order = (track: AnimationTrack) => {
|
||||
const i = defs.findIndex((d) => d.key === track.property);
|
||||
return i < 0 ? defs.length : i;
|
||||
};
|
||||
const rows = animation.tracks.filter((t) => t.target === target).sort((a, b) => order(a) - order(b));
|
||||
for (const track of rows) {
|
||||
const def = defs.find((d) => d.key === track.property);
|
||||
const label = def ? def.label : `${track.property} (missing)`;
|
||||
const held = !aimed || !def ? 0 : track.relative ? 0 : heldBy(aimed, def);
|
||||
lines.push({ kind: 'row', track, label, def, held, depth });
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function heldBy(aimed: Aimed, def: PropertyDef): PropertyValue {
|
||||
if (isPositionRow(aimed.asset.type, def.key)) return aimed.object?.position[def.key] ?? 0;
|
||||
return aimed.bag[def.key] ?? def.default;
|
||||
}
|
||||
|
||||
/** What a row's header shows at `frame` (#408): the row's value there, else what it holds without keys. */
|
||||
export function lineValueAt(
|
||||
line: TimelineLine & { kind: 'row' },
|
||||
frame: number,
|
||||
reverse = false,
|
||||
curves: CurveLookup = NO_CURVES,
|
||||
): PropertyValue | undefined {
|
||||
if (!line.def) return undefined;
|
||||
return trackValueAt(line.track.keys, line.def.kind, frame, reverse, curves) ?? line.held;
|
||||
}
|
||||
|
||||
/** Whether a value names something in the project – what deleting that something has to count. */
|
||||
export function referencesAny(
|
||||
track: AnimationTrack,
|
||||
|
||||
Reference in new issue
Block a user