Files
feuerwerk/src/core/commands/variableCommands.ts
T
rootandClaude Opus 5 d54a620ebd Add ESLint and Prettier, and close the node test gaps
Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
  measured against the existing style); Markdown excluded because table-cell
  padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
  "this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
  tests in one go.

Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.

Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
  when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
  checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
  events now; the DOM handlers are thin adapters.

Compare fell back to == without a configured operator although the schema
default is <; aligned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:02:22 +02:00

131 lines
4.9 KiB
TypeScript

import type { PropertyValue } from '../model/Properties';
import { findUsages, 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';
/** Declares a new variable on a behavior; its default value is written to `props`. */
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; 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 { holder, object } of findUsages(project, this.assetId)) {
const value = object.overrides[this.key];
if (value === undefined) continue;
this.overrides.push({ holderId: holder.id, objectId: object.id, value });
delete object.overrides[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 usages = findUsages(project, 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;
}
}
}
/**
* Renames a variable everywhere: definition, default, instance overrides (in scenes and in
* container templates) and 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 { object } of findUsages(project, this.assetId)) {
if (object.overrides[from] === undefined) continue;
object.overrides[to] = object.overrides[from]!;
delete object.overrides[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 one of the common property keys (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);
}