An animation reaches the scene's own background and objects inside placed containers (#109)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
353b348c3c
commit
f68941fa3a
7 files changed
+334
-128
No files matched your search
+59
-16
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,9 +2,8 @@ import {
|
||||
AddAnimationTrackCommand,
|
||||
RetargetAnimationTrackCommand,
|
||||
} from '../../core/commands/animationCommands';
|
||||
import { animatableDefs, animatedKind, rootObjects, targetAssetOf } from '../../core/model/animation';
|
||||
import { animatableDefs, animatedKind, animationTargets, targetAssetOf } from '../../core/model/animation';
|
||||
import type { AnimationAsset, AnimationTrack } from '../../core/model/Asset';
|
||||
import { treeOrder } from '../../core/model/hierarchy';
|
||||
import { findAsset } from '../../core/model/Project';
|
||||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||||
import { Button, Dropdown, HStack, Label, Menu, type MenuItem, Popup, Spacer, Theme } from '../../ui';
|
||||
@@ -126,12 +125,11 @@ export class RowDialog extends Popup {
|
||||
const root = findAsset(this.state.project, this.asset.root);
|
||||
if (!root) return [];
|
||||
const choices: TargetChoice[] = [{ id: '', label: `${root.name} (itself)` }];
|
||||
if (root.type === 'container' || root.type === 'scene')
|
||||
for (const { object, depth } of treeOrder(root))
|
||||
choices.push({
|
||||
id: object.id,
|
||||
label: `${' '.repeat(depth)}${object.name || findAsset(this.state.project, object.assetId)?.name || '?'}`,
|
||||
});
|
||||
for (const { id, object, depth } of animationTargets(this.state.project, root))
|
||||
choices.push({
|
||||
id,
|
||||
label: `${' '.repeat(depth)}${object.name || findAsset(this.state.project, object.assetId)?.name || '?'}`,
|
||||
});
|
||||
return choices;
|
||||
}
|
||||
|
||||
@@ -166,9 +164,7 @@ export class RowDialog extends Popup {
|
||||
this.refresh();
|
||||
},
|
||||
}));
|
||||
new Menu(items, {
|
||||
searchable: rootObjects(findAsset(this.state.project, this.asset.root)).length > 8,
|
||||
}).openBelow(this.targetButton);
|
||||
new Menu(items, { searchable: items.length > 9 }).openBelow(this.targetButton);
|
||||
}
|
||||
|
||||
private openProperties(): void {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ENTITY_HANDLE, isHandleEnabled, type PointerState, worldMatrixOf } from '../core/graph';
|
||||
import { newId } from '../core/ids';
|
||||
import type { Asset } from '../core/model/Asset';
|
||||
import type { PropertyBag } from '../core/model/Properties';
|
||||
import { isPlaceableType } from '../core/model/AssetTypes';
|
||||
import { bakeInstances, materializeInstance, objectRef, resolveObjects } from '../core/model/instance';
|
||||
import { treeOrder } from '../core/model/hierarchy';
|
||||
@@ -34,7 +35,8 @@ export interface ModelWrite {
|
||||
*/
|
||||
export class World {
|
||||
readonly entities: Entity[] = [];
|
||||
backgroundColor = 0x000000;
|
||||
/** The running scene's own properties – read from the model, and written by an animation over the scene. */
|
||||
readonly sceneProps: PropertyBag = {};
|
||||
/** Set by a behavior (Stop game node); the editor leaves play mode on the next tick. */
|
||||
stopRequested = false;
|
||||
/** Set by a behavior (Restart game node); the editor restarts the scene on the next tick. */
|
||||
@@ -83,6 +85,11 @@ export class World {
|
||||
return this.elapsed;
|
||||
}
|
||||
|
||||
get backgroundColor(): number {
|
||||
const background = this.sceneProps.backgroundColor;
|
||||
return typeof background === 'number' ? background : 0x000000;
|
||||
}
|
||||
|
||||
/** Makes the entities match the scene; props are updated in place so systems holding a bag stay current. */
|
||||
syncFromModel(): void {
|
||||
const scene = findScene(this.project, this.sceneId);
|
||||
@@ -91,8 +98,7 @@ export class World {
|
||||
// behavior destroy one of them for good. Play does it (on its throwaway copy); the editor run
|
||||
// must not, so there the content stays resolved and `objectRef` writes into the instance edits.
|
||||
if (this.bakeContainers && bakeInstances(this.project, scene)) this.structureChanged = true;
|
||||
const background = scene.props.backgroundColor;
|
||||
this.backgroundColor = typeof background === 'number' ? background : 0x000000;
|
||||
Object.assign(this.sceneProps, scene.props);
|
||||
const objects = new Map(resolveObjects(this.project, scene).map((o) => [o.id, o]));
|
||||
const keep = new Set<Entity>();
|
||||
for (const object of objects.values()) {
|
||||
|
||||
@@ -21,12 +21,14 @@ export interface AnimationFinish {
|
||||
animationId: string;
|
||||
}
|
||||
|
||||
/** A row that reaches something this tick: the entity it writes and the kind it blends as. */
|
||||
/** A row that reaches something this tick: how to read and write it, and the kind it blends as. */
|
||||
interface Binding {
|
||||
track: AnimationTrack;
|
||||
entity: Entity;
|
||||
kind: string;
|
||||
/** Where the player keeps the starting value: `<entity id> <property>`, or `scene <property>`. */
|
||||
base: string;
|
||||
read(): PropertyValue;
|
||||
write(value: PropertyValue): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,18 +55,18 @@ export class AnimationSystem implements System {
|
||||
if (!player.bases) {
|
||||
// The first tick only takes the starting values and shows frame 0; the clock runs from the next.
|
||||
player.bases = {};
|
||||
for (const b of bindings) player.bases[b.base] = read(b.entity, b.track.property);
|
||||
for (const b of bindings) player.bases[b.base] = b.read();
|
||||
} else if (player.playing && isHandleEnabled(entity)) player.time += dt;
|
||||
const head = animationPlayheadAt(asset, player.time);
|
||||
for (const b of bindings) {
|
||||
const value = trackWriteAt(b.track, b.kind, head.frame, player.bases[b.base]);
|
||||
if (value !== undefined) write(b.entity, b.track.property, value);
|
||||
if (value !== undefined) b.write(value);
|
||||
}
|
||||
if (!head.finished) continue;
|
||||
if (playbackOf(asset) === 'once-reset')
|
||||
for (const b of bindings) {
|
||||
const base = player.bases[b.base];
|
||||
if (base !== undefined) write(b.entity, b.track.property, base);
|
||||
if (base !== undefined) b.write(base);
|
||||
}
|
||||
drop(entity, player);
|
||||
this.finished.push({ entity, animationId: asset.id });
|
||||
@@ -81,32 +83,58 @@ function drop(entity: Entity, player: AnimationPlayer): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* The rows that reach something on this object. A scene's rows aim at its objects by id, and only
|
||||
* while that scene runs; everything else aims at the object itself or at what its template placed
|
||||
* inside it. A row whose target is gone, or lacks the property, is skipped.
|
||||
* The rows that reach something on this object. A scene's rows aim at the scene itself or at its
|
||||
* objects by id, and only while that scene runs; everything else aims at the object itself or at
|
||||
* what its template placed inside it, however deep. A row whose target is gone, or lacks the
|
||||
* property, is skipped.
|
||||
*/
|
||||
function bind(world: World, asset: AnimationAsset, owner: Entity): Binding[] {
|
||||
const root = world.project.assets.find((a) => a.id === asset.root);
|
||||
if (!root) return [];
|
||||
const bindings: Binding[] = [];
|
||||
for (const track of asset.tracks) {
|
||||
const key = track.property;
|
||||
if (root.type === 'scene' && track.target === '') {
|
||||
const kind = world.sceneId === root.id ? animatedKind('scene', key) : undefined;
|
||||
if (kind)
|
||||
bindings.push({
|
||||
track,
|
||||
kind,
|
||||
base: `scene ${key}`,
|
||||
read: () => world.sceneProps[key] ?? 0,
|
||||
write: (value) => {
|
||||
world.sceneProps[key] = value;
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let entity: Entity | undefined;
|
||||
if (root.type === 'scene')
|
||||
entity = world.sceneId === root.id && track.target ? world.findByObjectId(track.target) : undefined;
|
||||
entity = world.sceneId === root.id ? world.findByObjectId(track.target) : undefined;
|
||||
else if (track.target === '') entity = owner;
|
||||
else entity = world.findByObjectId(childId(owner.objectId, track.target));
|
||||
const kind = entity && animatedKind(entity.type, track.property);
|
||||
if (entity && kind) bindings.push({ track, entity, kind, base: `${entity.id} ${track.property}` });
|
||||
const kind = entity && animatedKind(entity.type, key);
|
||||
if (entity && kind) {
|
||||
const aimed = entity;
|
||||
bindings.push({
|
||||
track,
|
||||
kind,
|
||||
base: `${aimed.id} ${key}`,
|
||||
read: () => read(aimed, key),
|
||||
write: (value) => write(aimed, key, value),
|
||||
});
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/** The id a template object gets below a placed container – nested one deeper when the container is content itself. */
|
||||
function childId(ownerId: string, templateId: string): string {
|
||||
/**
|
||||
* The id a template object gets below a placed container – nested one deeper when the container is
|
||||
* content itself. `path` may run through containers placed inside the template (`a/b`).
|
||||
*/
|
||||
function childId(ownerId: string, path: string): string {
|
||||
const split = splitInstanceId(ownerId);
|
||||
return split
|
||||
? instanceChildId(split.instanceId, `${split.path}/${templateId}`)
|
||||
: instanceChildId(ownerId, templateId);
|
||||
return split ? instanceChildId(split.instanceId, `${split.path}/${path}`) : instanceChildId(ownerId, path);
|
||||
}
|
||||
|
||||
function read(entity: Entity, key: string): PropertyValue {
|
||||
|
||||
Reference in new issue
Block a user