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>
131 lines
4.9 KiB
TypeScript
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);
|
|
}
|