Behaviors with Blueprint-style node graph, graph editor tab and BehaviorSystem
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d4b1ffa051
commit
922774778b
36 files changed
+2969
-147
No files matched your search
@@ -0,0 +1,132 @@
|
||||
import { nodeDef } from '../graph/NodeRegistry';
|
||||
import type { BehaviorAsset } from '../model/Asset';
|
||||
import type { PropertyValue } from '../model/Properties';
|
||||
import { findAsset, findUsages, type Project } from '../model/Project';
|
||||
import { type PropertyDef, type VariableKind, variableDef } from '../properties/PropertySchema';
|
||||
import { type Command, CommandError } from './Command';
|
||||
|
||||
export function requireBehavior(project: Project, assetId: string): BehaviorAsset {
|
||||
const asset = findAsset(project, assetId);
|
||||
if (!asset || asset.type !== 'behavior') throw new CommandError(`Behavior ${assetId} not found`);
|
||||
return asset;
|
||||
}
|
||||
|
||||
/** "var1", "var2", … – the first variable name not yet used by the behavior. */
|
||||
export function nextVariableName(asset: BehaviorAsset, base = 'var'): string {
|
||||
const taken = new Set(asset.variables.map((v) => v.key));
|
||||
for (let i = 1; ; i++) {
|
||||
const candidate = `${base}${i}`;
|
||||
if (!taken.has(candidate)) return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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('Variable names must start with a letter and contain only letters, digits or _');
|
||||
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: PropertyDef; index: number; value: PropertyValue | undefined };
|
||||
private overrides: { sceneId: 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 index = asset.variables.findIndex((v) => v.key === this.key);
|
||||
const def = asset.variables[index];
|
||||
if (!def) throw new CommandError(`Variable "${this.key}" not found`);
|
||||
this.removed = { def, index, value: asset.props[this.key] };
|
||||
asset.variables.splice(index, 1);
|
||||
delete asset.props[this.key];
|
||||
this.overrides = [];
|
||||
for (const { scene, object } of findUsages(project, this.assetId)) {
|
||||
const value = object.overrides[this.key];
|
||||
if (value === undefined) continue;
|
||||
this.overrides.push({ sceneId: scene.id, objectId: object.id, value });
|
||||
delete object.overrides[this.key];
|
||||
}
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
if (!this.removed) return;
|
||||
const asset = requireBehavior(project, this.assetId);
|
||||
asset.variables.splice(this.removed.index, 0, this.removed.def);
|
||||
if (this.removed.value !== undefined) asset.props[this.key] = this.removed.value;
|
||||
for (const o of this.overrides) {
|
||||
const object = findUsages(project, this.assetId).find((u) => u.scene.id === o.sceneId && u.object.id === o.objectId)?.object;
|
||||
if (object) object.overrides[this.key] = o.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Renames a variable everywhere: definition, default, instance overrides 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('Variable names must start with a letter and contain only letters, digits or _');
|
||||
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];
|
||||
}
|
||||
for (const node of asset.graph.nodes) {
|
||||
const def = nodeDef(node.type);
|
||||
for (const param of def?.params ?? []) {
|
||||
if (param.key === 'variable' && node.params.variable === from) node.params.variable = to;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidVariableName(name: string): boolean {
|
||||
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
||||
}
|
||||
Reference in new issue
Block a user