Resolve container content from the template instead of copying it

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>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 21:09:53 +02:00
1 parent 9d93020bd8
commit ead649eec3
37 files changed
+1878 -1006

No files matched your search

+16 -13
View File
@@ -1,5 +1,6 @@
import type { PropertyValue } from '../model/Properties';
import { findUsages, type Project } from '../model/Project';
import { overrideSites } from '../model/instance';
import type { Project } from '../model/Project';
import { isCommonProp, type PropertyDef, type VariableKind, variableDef } from '../properties/PropertySchema';
import { type Removed, removeFirst, restoreAt } from '../util/list';
import { type Command, CommandError } from './Command';
@@ -39,7 +40,7 @@ export class AddVariableCommand implements Command {
export class RemoveVariableCommand implements Command {
readonly label: string;
private removed?: { def: Removed<PropertyDef>; value: PropertyValue | undefined };
private overrides: { holderId: string; objectId: string; value: PropertyValue }[] = [];
private overrides: { holderId: string; objectId: string; path: string; value: PropertyValue }[] = [];
constructor(
private readonly assetId: string,
@@ -55,11 +56,11 @@ export class RemoveVariableCommand implements Command {
this.removed = { def, value: asset.props[this.key] };
delete asset.props[this.key];
this.overrides = [];
for (const { holder, object } of findUsages(project, this.assetId)) {
const value = object.overrides[this.key];
for (const site of overrideSites(project, (id) => id === this.assetId)) {
const value = site.bag[this.key];
if (value === undefined) continue;
this.overrides.push({ holderId: holder.id, objectId: object.id, value });
delete object.overrides[this.key];
this.overrides.push({ holderId: site.holder.id, objectId: site.object.id, path: site.path, value });
delete site.bag[this.key];
}
}
@@ -68,10 +69,12 @@ export class RemoveVariableCommand implements Command {
const asset = requireBehavior(project, this.assetId);
restoreAt(asset.variables, [this.removed.def]);
if (this.removed.value !== undefined) asset.props[this.key] = this.removed.value;
const usages = findUsages(project, this.assetId);
const sites = overrideSites(project, (id) => id === this.assetId);
for (const o of this.overrides) {
const object = usages.find((u) => u.holder.id === o.holderId && u.object.id === o.objectId)?.object;
if (object) object.overrides[this.key] = o.value;
const site = sites.find(
(s) => s.holder.id === o.holderId && s.object.id === o.objectId && s.path === o.path,
);
if (site) site.bag[this.key] = o.value;
}
}
}
@@ -109,10 +112,10 @@ export class RenameVariableCommand implements Command {
asset.props[to] = asset.props[from]!;
delete asset.props[from];
}
for (const { object } of findUsages(project, this.assetId)) {
if (object.overrides[from] === undefined) continue;
object.overrides[to] = object.overrides[from]!;
delete object.overrides[from];
for (const { bag } of overrideSites(project, (id) => id === this.assetId)) {
if (bag[from] === undefined) continue;
bag[to] = bag[from]!;
delete bag[from];
}
// Every node that picks a variable stores it under the param key `variable`.
for (const node of asset.graph.nodes) if (node.params.variable === from) node.params.variable = to;