Key what an empty timeline row shows on a double-click (#412)

A double-click into a timeline row without keys set the property's
default, while the field in the row header showed the value the target
actually holds. A sprite standing on x 40 got a key with 0, so the
first key moved it. The double-click now keys the value the row shows.

- trackHeld in src/core/model/animation.ts says what a row without keys
  holds. The field in the row header and SetAnimationKeyCommand both
  ask it, so they cannot drift apart again.
- A relative row still gets 0, a row with keys still takes the value of
  its curve at that frame, and a row whose target is gone still only
  gives a notice and no key.
- README and the wiki page Development name 2047 checks.

Tests:
- Unit animation: 87 green, 1 new. A sprite on x 40 with the override
  scale 2 gets keys with 40 and 2. Red with the old def.default.
- e2e 25 on melektaus-atx: ALL PASSED, 35 checks, 25i-ii new. The arm
  stands on x 40 and a double-click on frame 3 keys { frame: 3,
  value: 40 }.
- Gate on melektaus-atx: green, 304 files, 4703 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-12 18:09:02 +02:00
1 parent d4fc425233
commit c425eea004
5 files changed
+51 -6

No files matched your search

+3 -2
View File
@@ -10,6 +10,7 @@ import {
targetAssetOf,
targetDefs,
trackDef,
trackHeld,
trackValueAt,
} from '../model/animation';
import type { AnimationAsset, AnimationKey, AnimationTrack, KeyInterpolation } from '../model/Asset';
@@ -215,7 +216,7 @@ export class SetTrackRelativeCommand extends TimelineCommand {
/**
* Sets the key at `frame`, replacing the value of one already there but keeping its interpolation.
* Without a value it takes the value the row has at that frame – what a double click in the timeline
* means – and on an empty row the property's default, which for a relative row is no offset at all.
* means – and on an empty row what the row's header shows: the object's value, or no offset when relative.
*/
export class SetAnimationKeyCommand extends TimelineCommand {
readonly label = 'Set key';
@@ -235,7 +236,7 @@ export class SetAnimationKeyCommand extends TimelineCommand {
const def = trackDef(project, asset, track);
if (!def) throw new CommandError('The row aims at something that is gone');
const current = trackValueAt(track.keys, def.kind, this.frame, false, curveLookup(project));
const raw = this.value ?? current ?? (track.relative ? 0 : def.default);
const raw = this.value ?? current ?? trackHeld(project, asset, track);
const key: AnimationKey = { frame: this.frame, value: coerceValue(def, raw) };
const interpolation = track.keys.find((k) => k.frame === this.frame)?.interpolation;
if (interpolation) key.interpolation = interpolation;
+12 -3
View File
@@ -625,18 +625,27 @@ export function timelineLines(project: Project, animation: AnimationAsset): Time
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 });
lines.push({ kind: 'row', track, label, def, held: heldBy(aimed, def, track), depth });
}
}
return lines;
}
function heldBy(aimed: Aimed, def: PropertyDef): PropertyValue {
function heldBy(
aimed: Aimed | undefined,
def: PropertyDef | undefined,
track: AnimationTrack,
): PropertyValue {
if (!aimed || !def || track.relative) return 0;
if (isPositionRow(aimed.asset.type, def.key)) return aimed.object?.position[def.key] ?? 0;
return aimed.bag[def.key] ?? def.default;
}
/** What a row holds without keys (#412): the value its object has, no offset for a relative row. */
export function trackHeld(project: Project, animation: AnimationAsset, track: AnimationTrack): PropertyValue {
return heldBy(aimedAt(project, animation, track.target), trackDef(project, animation, track), track);
}
/** 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' },