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:
1 parent
9d93020bd8
commit
ead649eec3
37 files changed
+1878
-1006
No files matched your search
@@ -1,41 +1,31 @@
|
||||
import { newId } from '../ids';
|
||||
import { isPlaceableType } from '../model/AssetTypes';
|
||||
import { containerReaches, instantiateContainer } from '../model/container';
|
||||
import { containerReaches } from '../model/container';
|
||||
import { isSelfOrAncestor, localPositionFor, removeSubtrees, worldPosition } from '../model/hierarchy';
|
||||
import {
|
||||
type Position,
|
||||
type PropertyBag,
|
||||
type PropertyValue,
|
||||
toPosition,
|
||||
type Vec2,
|
||||
} from '../model/Properties';
|
||||
import { captureObject, isLiveInstance, type ObjectMemo, restoreObject } from '../model/instance';
|
||||
import { type Position, type PropertyValue, toPosition, type Vec2 } from '../model/Properties';
|
||||
import type { Project } from '../model/Project';
|
||||
import { createSceneObject, type SceneObject } from '../model/SceneObject';
|
||||
import { coerceValue } from '../properties/PropertySchema';
|
||||
import { type Removed, restoreAt } from '../util/list';
|
||||
import { type Command, CommandError } from './Command';
|
||||
import { requireAsset, requireObject, requirePropDef, requireHolder } from './require';
|
||||
import { SetBagValueCommand } from './SetBagValueCommand';
|
||||
import {
|
||||
ALL_AXES,
|
||||
nameSlot,
|
||||
overrideSlot,
|
||||
positionSlot,
|
||||
restoreSlots,
|
||||
type SlotMemo,
|
||||
syncTemplateEdit,
|
||||
} from './templateSync';
|
||||
requireAsset,
|
||||
requireObject,
|
||||
requireObjectRef,
|
||||
requirePropDef,
|
||||
requireHolder,
|
||||
requireStructural,
|
||||
} from './require';
|
||||
|
||||
/**
|
||||
* The position is a world position; with a parent it is converted to the parent's local space.
|
||||
* Placing a container into a scene also creates its content (see `instantiateContainer`); into
|
||||
* a container template it does not, because the template spells out one object per placed copy
|
||||
* and `instantiateContainer` expands the nested one when the copy is made.
|
||||
* Placing a container adds the container object alone – its content is resolved from the template
|
||||
* (see `model/instance.ts`), so it is never copied into the scene.
|
||||
*/
|
||||
export class PlaceAssetCommand implements Command {
|
||||
readonly label = 'Place asset';
|
||||
readonly objectId = newId();
|
||||
private createdIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly holderId: string,
|
||||
@@ -48,25 +38,19 @@ export class PlaceAssetCommand implements Command {
|
||||
const holder = requireHolder(project, this.holderId);
|
||||
const asset = requireAsset(project, this.assetId);
|
||||
if (!isPlaceableType(asset.type)) throw new CommandError('A scene cannot be placed into a scene');
|
||||
if (
|
||||
holder.type === 'container' &&
|
||||
asset.type === 'container' &&
|
||||
containerReaches(project, asset, holder.id)
|
||||
)
|
||||
if (asset.type === 'container' && containerReaches(project, asset, holder.id))
|
||||
throw new CommandError(`${asset.name} cannot contain ${holder.name}: that nests forever`);
|
||||
if (this.parentId) requireObject(project, this.holderId, this.parentId);
|
||||
if (this.parentId) {
|
||||
requireObject(project, this.holderId, this.parentId);
|
||||
requireStructural(project, holder, this.parentId);
|
||||
}
|
||||
const local = localPositionFor(holder, this.parentId, toPosition(this.position));
|
||||
const created = [createSceneObject(this.objectId, this.assetId, local, this.parentId)];
|
||||
if (asset.type === 'container' && holder.type === 'scene')
|
||||
created.push(...instantiateContainer(project, asset, this.objectId));
|
||||
this.createdIds = created.map((o) => o.id);
|
||||
holder.objects.push(...created);
|
||||
holder.objects.push(createSceneObject(this.objectId, this.assetId, local, this.parentId));
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
const holder = requireHolder(project, this.holderId);
|
||||
const ids = new Set(this.createdIds);
|
||||
holder.objects = holder.objects.filter((o) => !ids.has(o.id));
|
||||
holder.objects = holder.objects.filter((o) => o.id !== this.objectId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +67,12 @@ export class RemoveSceneObjectsCommand implements Command {
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
for (const id of this.objectIds) requireObject(project, this.holderId, id);
|
||||
this.removed = removeSubtrees(requireHolder(project, this.holderId), this.objectIds);
|
||||
const holder = requireHolder(project, this.holderId);
|
||||
for (const id of this.objectIds) {
|
||||
requireStructural(project, holder, id);
|
||||
requireObject(project, this.holderId, id);
|
||||
}
|
||||
this.removed = removeSubtrees(holder, this.objectIds);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
@@ -104,8 +92,7 @@ export class RemoveSceneObjectCommand extends RemoveSceneObjectsCommand {
|
||||
*/
|
||||
export class MoveSceneObjectCommand implements Command {
|
||||
readonly label = 'Move object';
|
||||
private previous: Position = { x: 0, y: 0, z: 0 };
|
||||
private memos: SlotMemo<number>[] = [];
|
||||
private memo: ObjectMemo | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly holderId: string,
|
||||
@@ -114,28 +101,20 @@ export class MoveSceneObjectCommand implements Command {
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.holderId, this.objectId);
|
||||
this.previous = { ...object.position };
|
||||
object.position = { ...this.position };
|
||||
this.memos = syncTemplateEdit(
|
||||
project,
|
||||
this.holderId,
|
||||
object,
|
||||
ALL_AXES.map(positionSlot),
|
||||
ALL_AXES.map((axis) => this.previous[axis]),
|
||||
);
|
||||
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
||||
this.memo = captureObject(ref);
|
||||
ref.setPosition(this.position);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
restoreSlots(this.memos);
|
||||
requireObject(project, this.holderId, this.objectId).position = { ...this.previous };
|
||||
undo(): void {
|
||||
if (this.memo) restoreObject(this.memo);
|
||||
}
|
||||
}
|
||||
|
||||
/** Editor-only: undoable like any other edit, but it changes nothing the game can see. */
|
||||
export class SetEditorOffsetCommand implements Command {
|
||||
readonly label = 'Editor offset';
|
||||
private previous: Vec2 = { x: 0, y: 0 };
|
||||
private memo: ObjectMemo | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly holderId: string,
|
||||
@@ -144,20 +123,19 @@ export class SetEditorOffsetCommand implements Command {
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.holderId, this.objectId);
|
||||
this.previous = { ...object.editorOffset };
|
||||
object.editorOffset = { x: this.offset.x, y: this.offset.y };
|
||||
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
||||
this.memo = captureObject(ref);
|
||||
ref.setEditorOffset(this.offset);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
requireObject(project, this.holderId, this.objectId).editorOffset = { ...this.previous };
|
||||
undo(): void {
|
||||
if (this.memo) restoreObject(this.memo);
|
||||
}
|
||||
}
|
||||
|
||||
export class RenameSceneObjectCommand implements Command {
|
||||
readonly label = 'Rename object';
|
||||
private previous = '';
|
||||
private memos: SlotMemo<string>[] = [];
|
||||
private memo: ObjectMemo | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly holderId: string,
|
||||
@@ -166,53 +144,36 @@ export class RenameSceneObjectCommand implements Command {
|
||||
) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.holderId, this.objectId);
|
||||
this.previous = object.name;
|
||||
object.name = this.name;
|
||||
this.memos = syncTemplateEdit(project, this.holderId, object, [nameSlot], [this.previous]);
|
||||
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
||||
this.memo = captureObject(ref);
|
||||
ref.setName(this.name);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
restoreSlots(this.memos);
|
||||
requireObject(project, this.holderId, this.objectId).name = this.previous;
|
||||
undo(): void {
|
||||
if (this.memo) restoreObject(this.memo);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OverrideCommand extends SetBagValueCommand<SceneObject> {
|
||||
private memos: SlotMemo<PropertyValue | undefined>[] = [];
|
||||
abstract class OverrideCommand implements Command {
|
||||
private memo: ObjectMemo | undefined;
|
||||
|
||||
protected constructor(
|
||||
private readonly holderId: string,
|
||||
private readonly objectId: string,
|
||||
key: string,
|
||||
label: string,
|
||||
) {
|
||||
super(key, label);
|
||||
protected readonly key: string,
|
||||
readonly label: string,
|
||||
) {}
|
||||
|
||||
protected abstract nextValue(object: SceneObject, project: Project): PropertyValue | undefined;
|
||||
|
||||
execute(project: Project): void {
|
||||
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
||||
this.memo = captureObject(ref);
|
||||
ref.setOverride(this.key, this.nextValue(ref.object, project));
|
||||
}
|
||||
|
||||
protected override resolveTarget(project: Project): SceneObject {
|
||||
return requireObject(project, this.holderId, this.objectId);
|
||||
}
|
||||
|
||||
protected override bagOf(object: SceneObject): PropertyBag {
|
||||
return object.overrides;
|
||||
}
|
||||
|
||||
override execute(project: Project): void {
|
||||
const before = this.resolveTarget(project).overrides[this.key];
|
||||
super.execute(project);
|
||||
this.memos = syncTemplateEdit(
|
||||
project,
|
||||
this.holderId,
|
||||
this.resolveTarget(project),
|
||||
[overrideSlot(this.key)],
|
||||
[before],
|
||||
);
|
||||
}
|
||||
|
||||
override undo(project: Project): void {
|
||||
restoreSlots(this.memos);
|
||||
super.undo(project);
|
||||
undo(): void {
|
||||
if (this.memo) restoreObject(this.memo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +187,7 @@ export class SetOverrideCommand extends OverrideCommand {
|
||||
super(holderId, objectId, key, `Override ${key}`);
|
||||
}
|
||||
|
||||
protected override computeNext(object: SceneObject, project: Project): PropertyValue {
|
||||
protected override nextValue(object: SceneObject, project: Project): PropertyValue {
|
||||
return coerceValue(requirePropDef(requireAsset(project, object.assetId), this.key), this.value);
|
||||
}
|
||||
}
|
||||
@@ -238,7 +199,7 @@ export class ClearOverrideCommand extends OverrideCommand {
|
||||
|
||||
// No schema check, unlike setting one: a key the asset no longer declares is exactly what one
|
||||
// wants to be able to clear.
|
||||
protected override computeNext(): undefined {
|
||||
protected override nextValue(): undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -262,10 +223,13 @@ export class SetParentCommand implements Command {
|
||||
|
||||
execute(project: Project): void {
|
||||
const holder = requireHolder(project, this.holderId);
|
||||
requireStructural(project, holder, this.objectId);
|
||||
const object = requireObject(project, this.holderId, this.objectId);
|
||||
if (this.parentId) {
|
||||
if (this.parentId === this.objectId) throw new CommandError('An object cannot be its own parent');
|
||||
requireObject(project, this.holderId, this.parentId);
|
||||
requireStructural(project, holder, this.parentId);
|
||||
if (isLiveInstance(project, holder, requireObject(project, this.holderId, this.parentId)))
|
||||
throw new CommandError('A placed container shows its template; unpack it to put objects inside');
|
||||
if (isSelfOrAncestor(holder, this.objectId, this.parentId))
|
||||
throw new CommandError('An object cannot be parented to one of its own children');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user