The Container tab reuses SceneViewport against any object holder, so a container template is edited exactly like a scene. Right-click no longer changes the selection, which previously made nesting impossible: opening the menu on a container loaded it, and "Place in container" then targeted itself. Template edits (move, rename, override) now reach already-placed instances per slot, through a shared templateSync used by both the apply path and the plain object commands. Copies the user has touched keep their own value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
/**
|
||
* Prefab semantics for container templates: a placed copy that still holds the template's old value
|
||
* follows a change made to the template, a copy modified on its own keeps what it has. Used by both
|
||
* directions – an edit in the container editor (see the object commands) and "apply" from a placed
|
||
* object upwards (see `applyCommands`).
|
||
*/
|
||
import { instancesOf } from '../model/container';
|
||
import { type Axis, type PropertyValue, setBagValue } from '../model/Properties';
|
||
import { findObjectHolder, type Project } from '../model/Project';
|
||
import type { SceneObject } from '../model/SceneObject';
|
||
|
||
export const ALL_AXES: readonly Axis[] = ['x', 'y', 'z'];
|
||
|
||
/** One value that follows on its own: an override key, a single axis of the position, the name. */
|
||
export interface Slot<T> {
|
||
read(object: SceneObject): T;
|
||
write(object: SceneObject, value: T): void;
|
||
}
|
||
|
||
export interface SlotMemo<T> {
|
||
object: SceneObject;
|
||
slot: Slot<T>;
|
||
previous: T;
|
||
}
|
||
|
||
export const overrideSlot = (key: string): Slot<PropertyValue | undefined> => ({
|
||
read: (object) => object.overrides[key],
|
||
write: (object, value) => setBagValue(object.overrides, key, value),
|
||
});
|
||
|
||
export const positionSlot = (axis: Axis): Slot<number> => ({
|
||
read: (object) => object.position[axis],
|
||
write: (object, value) => {
|
||
object.position[axis] = value;
|
||
},
|
||
});
|
||
|
||
export const nameSlot: Slot<string> = {
|
||
read: (object) => object.name,
|
||
write: (object, value) => {
|
||
object.name = value;
|
||
},
|
||
};
|
||
|
||
/** Writes `after` into every copy of `template` whose slot still reads `before`. */
|
||
export function syncInstances<T>(
|
||
project: Project,
|
||
template: SceneObject,
|
||
slots: readonly Slot<T>[],
|
||
before: readonly T[],
|
||
after: readonly T[],
|
||
): SlotMemo<T>[] {
|
||
const memos: SlotMemo<T>[] = [];
|
||
for (const { object } of instancesOf(project, template.id)) {
|
||
slots.forEach((slot, i) => {
|
||
const current = slot.read(object);
|
||
if (current !== before[i]) return;
|
||
memos.push({ object, slot, previous: current });
|
||
slot.write(object, after[i]!);
|
||
});
|
||
}
|
||
return memos;
|
||
}
|
||
|
||
/**
|
||
* Carries an edit of a container template object to the placed copies; nothing to do when the
|
||
* holder is a scene, where an edit is meant for that one object. `before` has to be read off the
|
||
* object before it is written, the new values are read back from it here.
|
||
*/
|
||
export function syncTemplateEdit<T>(
|
||
project: Project,
|
||
holderId: string,
|
||
object: SceneObject,
|
||
slots: readonly Slot<T>[],
|
||
before: readonly T[],
|
||
): SlotMemo<T>[] {
|
||
if (findObjectHolder(project, holderId)?.type !== 'container') return [];
|
||
return syncInstances(
|
||
project,
|
||
object,
|
||
slots,
|
||
before,
|
||
slots.map((slot) => slot.read(object)),
|
||
);
|
||
}
|
||
|
||
export function restoreSlots<T>(memos: readonly SlotMemo<T>[]): void {
|
||
for (const memo of [...memos].reverse()) memo.slot.write(memo.object, memo.previous);
|
||
}
|