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

+59 -16
View File
@@ -2,6 +2,8 @@ import { isKindWith } from '../properties/kinds';
import { type PropertyDef, schemaFor } from '../properties/PropertySchema';
import type { AnimationAsset, AnimationKey, AnimationTrack, Asset } from './Asset';
import type { AssetType } from './AssetTypes';
import { treeOrder } from './hierarchy';
import { instanceChildId, resolveObjects } from './instance';
import { findAsset, type Project } from './Project';
import { colorOf, type PropertyValue, textOf } from './Properties';
import type { SceneObject } from './SceneObject';
@@ -151,29 +153,71 @@ export function trackWriteAt(
/** The kind a row's property has on an asset of that type; undefined for one the timeline cannot animate. */
export function animatedKind(type: AssetType, key: string): string | undefined {
if (isPositionKey(key)) return 'number';
return animatableDefs(type).find((d) => d.key === key)?.kind;
}
/**
* The rows the timeline offers for an asset type: position, then the schema's flat properties. A
* list or struct has no single value to key; a shader's own parameters come later (#97).
* list or struct has no single value to key; a shader's own parameters come later (#97). A scene is
* placed nowhere, so it offers only its own properties.
*/
export function animatableDefs(type: AssetType): PropertyDef[] {
if (type === 'scene') return [];
const own = schemaFor(type).filter((d) => d.kind !== 'list' && d.kind !== 'struct');
if (type === 'scene') return own;
const position: PropertyDef[] = POSITION_KEYS.map((key) => ({
key,
label: key.toUpperCase(),
kind: 'number',
default: 0,
}));
const own = schemaFor(type).filter((d) => d.kind !== 'list' && d.kind !== 'struct');
return [...position, ...own];
}
/** The objects a row may aim at below the root: a container's or scene's whole tree, nothing for the rest. */
export function rootObjects(root: Asset | undefined): readonly SceneObject[] {
return root && (root.type === 'container' || root.type === 'scene') ? root.objects : [];
/**
* The id of the one object a placed root stands on – the preview's holder, and the instance the
* targets inside a container are resolved below.
*/
export const PLACED_ROOT_ID = 'preview-root';
/** The root placed at the origin, its content following the template like any placed container's. */
export function placedRoot(root: Asset): SceneObject {
return {
id: PLACED_ROOT_ID,
assetId: root.id,
name: root.name,
position: { x: 0, y: 0, z: 0 },
editorOffset: { x: 0, y: 0 },
overrides: {},
parentId: '',
instanceEdits: {},
};
}
/** One object a row may aim at below the root, under the id the row stores. */
export interface AnimationTarget {
id: string;
object: SceneObject;
depth: number;
}
/**
* Everything a row may aim at below the root, in tree order – into placed containers, too (#109).
* A scene's rows name its objects by the ids the running scene gives them, which a baked instance
* keeps. A container's rows name a path of template ids, what the runtime appends to whichever
* object the animation plays on. A sprite or text has nothing below it.
*/
export function animationTargets(project: Project, root: Asset | undefined): AnimationTarget[] {
if (root?.type === 'scene')
return treeOrder({ objects: resolveObjects(project, root) }).map(({ object, depth }) => ({
id: object.id,
object,
depth,
}));
if (root?.type !== 'container') return [];
const prefix = instanceChildId(PLACED_ROOT_ID, '');
return treeOrder({ objects: resolveObjects(project, { objects: [placedRoot(root)] }) })
.filter(({ object }) => object.id.startsWith(prefix))
.map(({ object, depth }) => ({ id: object.id.slice(prefix.length), object, depth: depth - 1 }));
}
/** The asset a row aims at: the root itself, or what the object inside it places. Undefined once gone. */
@@ -184,21 +228,20 @@ export function targetAssetOf(
): Asset | undefined {
const root = findAsset(project, animation.root);
if (target === '') return root;
const object = rootObjects(root).find((o) => o.id === target);
return object ? findAsset(project, object.assetId) : undefined;
const aimed = animationTargets(project, root).find((t) => t.id === target);
return aimed ? findAsset(project, aimed.object.assetId) : undefined;
}
/** How the timeline names what a row aims at: the object (or the root itself) and the property. */
export function trackLabel(project: Project, animation: AnimationAsset, track: AnimationTrack): string {
const root = findAsset(project, animation.root);
const object = track.target === '' ? undefined : rootObjects(root).find((o) => o.id === track.target);
const aimed = targetAssetOf(project, animation, track.target);
const who =
const object =
track.target === ''
? (root?.name ?? '?')
: object
? object.name || findAsset(project, object.assetId)?.name || '?'
: '(missing)';
? undefined
: animationTargets(project, root).find((t) => t.id === track.target)?.object;
const aimed = object ? findAsset(project, object.assetId) : track.target === '' ? root : undefined;
const who =
track.target === '' ? (root?.name ?? '?') : object ? object.name || aimed?.name || '?' : '(missing)';
const def = aimed && animatableDefs(aimed.type).find((d) => d.key === track.property);
return `${who} · ${def?.label ?? track.property}`;
}