A scene now stores only the container object; its content is resolved from the template on every read, with deviations kept as sparse instanceEdits on the container object. This drops the propagation bookkeeping entirely -- templateSync.ts is gone and the three structural gaps (add, remove, reparent) cannot diverge any more. Values stay editable on an instance, structure does not; Unpack writes the content into the scene, keeping ids. Play mode bakes containers into the throwaway copy before entities are built. No migration: formatVersion is 2 and the seeders write the new format. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
3.7 KiB
TypeScript
86 lines
3.7 KiB
TypeScript
/** Lookups that throw a `CommandError` when the target does not exist. */
|
||
import { findNode, type GraphNode } from '../graph/Graph';
|
||
import { type NodeDef, nodeDef } from '../graph/NodeRegistry';
|
||
import type { Asset, BehaviorAsset, SceneAsset } from '../model/Asset';
|
||
import { findObject, type ObjectTree } from '../model/hierarchy';
|
||
import { isTemplateContent, objectRef, type ObjectRef } from '../model/instance';
|
||
import { findAsset, findObjectHolder, findScene, type ObjectHolder, type Project } from '../model/Project';
|
||
import type { SceneObject } from '../model/SceneObject';
|
||
import { propDef, type PropertyDef } from '../properties/PropertySchema';
|
||
import { CommandError } from './Command';
|
||
|
||
export function requireAsset(project: Project, assetId: string): Asset {
|
||
const asset = findAsset(project, assetId);
|
||
if (!asset) throw new CommandError(`Asset ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireScene(project: Project, sceneId: string): SceneAsset {
|
||
const scene = findScene(project, sceneId);
|
||
if (!scene) throw new CommandError(`Scene ${sceneId} not found`);
|
||
return scene;
|
||
}
|
||
|
||
export function requireHolder(project: Project, holderId: string): ObjectHolder {
|
||
const holder = findObjectHolder(project, holderId);
|
||
if (!holder) throw new CommandError(`Scene or container ${holderId} not found`);
|
||
return holder;
|
||
}
|
||
|
||
export function requireObject(project: Project, holderId: string, objectId: string): SceneObject {
|
||
const object = findObject(requireHolder(project, holderId), objectId);
|
||
if (!object) throw new CommandError(`Scene object ${objectId} not found`);
|
||
return object;
|
||
}
|
||
|
||
/**
|
||
* The object a value edit works on, whether it is stored in the holder or resolved from a
|
||
* container template – the writes then land in the instance's edits.
|
||
*/
|
||
export function requireObjectRef(project: Project, holderId: string, objectId: string): ObjectRef {
|
||
const ref = objectRef(project, requireHolder(project, holderId), objectId);
|
||
if (!ref) throw new CommandError(`Scene object ${objectId} not found`);
|
||
return ref;
|
||
}
|
||
|
||
/**
|
||
* Rejects an object that only exists because a container template says so. Its structure – what
|
||
* exists, what it is parented to – is the template's business, not the instance's.
|
||
*/
|
||
export function requireStructural(project: Project, holder: ObjectTree, objectId: string): void {
|
||
if (!isTemplateContent(holder, objectId)) return;
|
||
// Not resolvable at all is a plain "not found", which the caller's own lookup words better.
|
||
const object = objectRef(project, holder, objectId)?.object;
|
||
if (!object) return;
|
||
const name = object.name || 'That object';
|
||
throw new CommandError(`${name} comes from a container template; open the container to change it`);
|
||
}
|
||
|
||
export function requireBehavior(project: Project, assetId: string): BehaviorAsset {
|
||
const asset = findAsset(project, assetId);
|
||
if (!asset || asset.type !== 'behavior') throw new CommandError(`Behavior ${assetId} not found`);
|
||
return asset;
|
||
}
|
||
|
||
export function requireNode(project: Project, assetId: string, nodeId: string): GraphNode {
|
||
const node = findNode(requireBehavior(project, assetId).graph, nodeId);
|
||
if (!node) throw new CommandError(`Node ${nodeId} not found`);
|
||
return node;
|
||
}
|
||
|
||
/**
|
||
* `NodeRegistry.requireNodeDef` throws a plain `Error`, which the editor does not catch; a command
|
||
* needs a `CommandError` so an unknown type becomes a notice instead of a crash.
|
||
*/
|
||
export function requireNodeDef(type: string): NodeDef {
|
||
const def = nodeDef(type);
|
||
if (!def) throw new CommandError(`Unknown node type "${type}"`);
|
||
return def;
|
||
}
|
||
|
||
export function requirePropDef(asset: Asset, key: string): PropertyDef {
|
||
const def = propDef(asset, key);
|
||
if (!def) throw new CommandError(`Property "${key}" is not defined for ${asset.type}`);
|
||
return def;
|
||
}
|