Files
feuerwerk/src/core/commands/variableCommands.ts
T
rootandClaude Opus 5 ead649eec3 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>
2026-09-08 21:09:53 +02:00

131 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PropertyValue } from '../model/Properties';
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';
import { requireBehavior } from './require';
/** The variable's default value is written to `props` as well. */
export class AddVariableCommand implements Command {
readonly label: string;
readonly def: PropertyDef;
constructor(
private readonly assetId: string,
key: string,
kind: VariableKind,
) {
this.def = variableDef(key, kind);
this.label = `Add variable ${key}`;
}
execute(project: Project): void {
const asset = requireBehavior(project, this.assetId);
if (!isValidVariableName(this.def.key)) throw new CommandError(INVALID_NAME);
if (asset.variables.some((v) => v.key === this.def.key))
throw new CommandError(`Variable "${this.def.key}" already exists`);
asset.variables.push(this.def);
asset.props[this.def.key] = this.def.default;
}
undo(project: Project): void {
const asset = requireBehavior(project, this.assetId);
asset.variables = asset.variables.filter((v) => v.key !== this.def.key);
delete asset.props[this.def.key];
}
}
/** Removes a variable together with its default and every instance override. */
export class RemoveVariableCommand implements Command {
readonly label: string;
private removed?: { def: Removed<PropertyDef>; value: PropertyValue | undefined };
private overrides: { holderId: string; objectId: string; path: string; value: PropertyValue }[] = [];
constructor(
private readonly assetId: string,
private readonly key: string,
) {
this.label = `Remove variable ${key}`;
}
execute(project: Project): void {
const asset = requireBehavior(project, this.assetId);
const def = removeFirst(asset.variables, (v) => v.key === this.key);
if (!def) throw new CommandError(`Variable "${this.key}" not found`);
this.removed = { def, value: asset.props[this.key] };
delete asset.props[this.key];
this.overrides = [];
for (const site of overrideSites(project, (id) => id === this.assetId)) {
const value = site.bag[this.key];
if (value === undefined) continue;
this.overrides.push({ holderId: site.holder.id, objectId: site.object.id, path: site.path, value });
delete site.bag[this.key];
}
}
undo(project: Project): void {
if (!this.removed) return;
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 sites = overrideSites(project, (id) => id === this.assetId);
for (const o of this.overrides) {
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;
}
}
}
/** Renames everywhere: definition, default, instance overrides and the graph nodes referring to it. */
export class RenameVariableCommand implements Command {
readonly label: string;
constructor(
private readonly assetId: string,
private readonly from: string,
private readonly to: string,
) {
this.label = `Rename variable ${from}`;
}
execute(project: Project): void {
this.rename(project, this.from, this.to);
}
undo(project: Project): void {
this.rename(project, this.to, this.from);
}
private rename(project: Project, from: string, to: string): void {
const asset = requireBehavior(project, this.assetId);
if (from === to) return;
if (!isValidVariableName(to)) throw new CommandError(INVALID_NAME);
const def = asset.variables.find((v) => v.key === from);
if (!def) throw new CommandError(`Variable "${from}" not found`);
if (asset.variables.some((v) => v.key === to)) throw new CommandError(`Variable "${to}" already exists`);
def.key = to;
def.label = to;
if (asset.props[from] !== undefined) {
asset.props[to] = asset.props[from]!;
delete asset.props[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;
}
}
const INVALID_NAME = 'Variable names must start with a letter and contain only letters, digits or _';
/** Identifier-like, and not a common property key – a variable "enabled" would shadow the switch. */
export function isValidVariableName(name: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !isCommonProp(name);
}