An animation reaches the scene's own background and objects inside placed containers (#109)
game-maker / gate (push) Successful in 1m33s
game-maker / suite (push) Successful in 5m37s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 06:48:21 +02:00
1 parent 353b348c3c
commit f68941fa3a
7 files changed
+334 -128

No files matched your search

+61 -82
View File
@@ -3,24 +3,33 @@
* the playhead written over it. Nothing here touches the document – the holder returned is built
* fresh on every call from copies, and the preview draws it the way the Scene tab draws a scene.
*/
import { animatedKind, frameCount, isPositionKey, playbackOf, rootObjects, trackWriteAt } from './animation';
import {
animatableDefs,
animatedKind,
frameCount,
isPositionKey,
PLACED_ROOT_ID,
placedRoot,
playbackOf,
trackWriteAt,
} from './animation';
import type { AnimationAsset, AnimationTrack, Asset } from './Asset';
import { type InstanceEdit, instanceChildId } from './instance';
import { instanceChildId, objectRef } from './instance';
import { findAsset, type ObjectHolder, type Project } from './Project';
import type { PropertyValue } from './Properties';
import { resolveSceneObjectProps } from '../properties/resolve';
import type { SceneObject } from './SceneObject';
/** The one object the preview places the root on – what a row aimed at the root itself writes to. */
export const PREVIEW_ROOT_ID = 'preview-root';
export const PREVIEW_ROOT_ID = PLACED_ROOT_ID;
/**
* The holder the preview draws at `frame`: the scene itself for a scene root, otherwise a holder
* The holder the preview draws at `frame`: a copy of the scene for a scene root, otherwise a holder
* of one object placing the root at the origin, its content resolved from the template like any
* placed container's. Rows reach the root through that object and its children through the
* object's instance edits – the same route a placed container's content takes. `undefined`
* without a root. Past the end of a `once-reset` pass nothing is written: the player has put the
* starting values back by then.
* placed container's. A row reaches an object stored in the holder directly and one resolved from a
* container through its instance's edits – the same route an edit in the Scene tab takes, so a row
* aimed into a placed container needs nothing of its own (#109). `undefined` without a root. Past
* the end of a `once-reset` pass nothing is written: the player has put the starting values back.
*/
export function previewHolder(
project: Project,
@@ -30,10 +39,12 @@ export function previewHolder(
const root = findAsset(project, animation.root);
if (!root) return undefined;
const reset = playbackOf(animation) === 'once-reset' && frame >= frameCount(animation);
const tracks = reset ? [] : animation.tracks;
return root.type === 'scene'
? sceneAt(project, tracks, root, frame)
: placedAt(project, tracks, root, frame);
const holder = root.type === 'scene' ? sceneCopy(root) : placedHolder(root);
for (const track of reset ? [] : animation.tracks) {
if (root.type === 'scene' && track.target === '') writeScene(holder, track, frame);
else writeObject(project, holder, previewObjectId(root, track.target), track, frame);
}
return holder;
}
/** The id the preview draws a row's target under – for a check that wants to look at it. */
@@ -42,70 +53,48 @@ export function previewObjectId(root: Asset | undefined, target: string): string
return target === '' ? PREVIEW_ROOT_ID : instanceChildId(PREVIEW_ROOT_ID, target);
}
function sceneAt(
project: Project,
tracks: readonly AnimationTrack[],
root: ObjectHolder,
frame: number,
): ObjectHolder {
const objects = root.objects.map((o) => ({
...o,
position: { ...o.position },
overrides: { ...o.overrides },
}));
const byId = new Map(objects.map((o) => [o.id, o]));
for (const track of tracks) {
const object = byId.get(track.target);
const asset = object && findAsset(project, object.assetId);
const kind = asset && animatedKind(asset.type, track.property);
if (!object || !asset || !kind) continue;
const base = isPositionKey(track.property)
? object.position[track.property]
: resolveSceneObjectProps(project, asset, object)[track.property];
const value = trackWriteAt(track, kind, frame, base);
if (value === undefined) continue;
if (isPositionKey(track.property)) object.position[track.property] = Number(value);
else object.overrides[track.property] = value;
}
return { ...root, objects };
/** Deep enough that nothing a row writes – a position, an override, an instance edit – is the document's. */
function sceneCopy(scene: ObjectHolder): ObjectHolder {
return {
...scene,
props: { ...scene.props },
objects: scene.objects.map((o) => ({
...o,
position: { ...o.position },
overrides: { ...o.overrides },
instanceEdits: structuredClone(o.instanceEdits),
})),
};
}
function placedAt(
function placedHolder(root: Asset): ObjectHolder {
return { id: PREVIEW_ROOT_ID, type: 'container', name: root.name, props: {}, objects: [placedRoot(root)] };
}
/** A row on the scene itself: its value goes into the copy's own properties. */
function writeScene(holder: ObjectHolder, track: AnimationTrack, frame: number): void {
const def = animatableDefs('scene').find((d) => d.key === track.property);
if (!def) return;
const value = trackWriteAt(track, def.kind, frame, holder.props[track.property] ?? def.default);
if (value !== undefined) holder.props[track.property] = value;
}
function writeObject(
project: Project,
tracks: readonly AnimationTrack[],
root: Asset,
holder: ObjectHolder,
id: string,
track: AnimationTrack,
frame: number,
): ObjectHolder {
const placed: SceneObject = {
id: PREVIEW_ROOT_ID,
assetId: root.id,
name: root.name,
position: { x: 0, y: 0, z: 0 },
editorOffset: { x: 0, y: 0 },
overrides: {},
parentId: '',
instanceEdits: {},
};
const templates = rootObjects(root);
for (const track of tracks) {
const template = track.target === '' ? undefined : templates.find((o) => o.id === track.target);
const aimed = track.target === '' ? root : template && findAsset(project, template.assetId);
const kind = aimed && animatedKind(aimed.type, track.property);
if (!aimed || !kind) continue;
const base = template
? baseOf(project, aimed, template, track.property)
: baseOf(project, aimed, placed, track.property);
const value = trackWriteAt(track, kind, frame, base);
if (value === undefined) continue;
if (!template) writeTo(placed, track.property, value);
else
writeEdit(
(placed.instanceEdits[template.id] ??= { position: {}, overrides: {} }),
track.property,
value,
);
}
return { id: PREVIEW_ROOT_ID, type: 'container', name: root.name, props: {}, objects: [placed] };
): void {
const ref = objectRef(project, holder, id);
const asset = ref && findAsset(project, ref.object.assetId);
const kind = asset && animatedKind(asset.type, track.property);
if (!ref || !asset || !kind) return;
const value = trackWriteAt(track, kind, frame, baseOf(project, asset, ref.object, track.property));
if (value === undefined) return;
if (isPositionKey(track.property))
ref.setPosition({ ...ref.object.position, [track.property]: Number(value) });
else ref.setOverride(track.property, value);
}
/** What the target holds before the animation writes it: the zero of a relative row. */
@@ -113,13 +102,3 @@ function baseOf(project: Project, asset: Asset, object: SceneObject, key: string
if (isPositionKey(key)) return object.position[key];
return resolveSceneObjectProps(project, asset, object)[key];
}
function writeTo(object: SceneObject, key: string, value: PropertyValue): void {
if (isPositionKey(key)) object.position[key] = Number(value);
else object.overrides[key] = value;
}
function writeEdit(edit: InstanceEdit, key: string, value: PropertyValue): void {
if (isPositionKey(key)) edit.position[key] = Number(value);
else edit.overrides[key] = value;
}