Add Get/Set properties nodes and a breadcrumb for opened sub-behaviors

Get properties / Set properties work like their variable counterparts:
one port per chosen property, an unconnected input writes its inline
value. A port chosen from the built-in list keeps that property's field
and default (height starts at 100, color is a colour field). "object" is
reserved for the fixed input, checked by the command and on load.

The property picker names the owning type ("Sprite width"), and text
fields can now carry a suggestion list; Get/Set property use it.

Double-clicking a call node shows the path of opened behaviors under the
graph header. Clicking an entry jumps back and trims the path; choosing
a behavior any other way hides it. Reopening a behavior already on the
path steps back to it instead of repeating it. The bar shows before the
graph switches, otherwise it clips the graph that was just fitted.

The "Add node..." header button is gone; nodes come from the context menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-11 15:07:07 +02:00
1 parent e9fa592078
commit 854ae2e756
35 files changed
+1044 -101

No files matched your search

+39 -6
View File
@@ -1,25 +1,59 @@
import type { Asset, BehaviorAsset } from '../../core/model/Asset';
import { Button, Checkbox, Label } from '../../ui';
import { findAsset, findAssetOf } from '../../core/model/Project';
import { Checkbox, Label } from '../../ui';
import { AssetTabEditor } from '../AssetTabEditor';
import type { EditorState } from '../state';
import { Breadcrumb } from './Breadcrumb';
import { GraphCanvas } from './GraphCanvas';
import { backTo, descend, follow, type Trail } from './trail';
/** Room kept for the "N nodes · M links" readout, wide enough for a three-digit graph. */
const INFO_WIDTH = 150;
export class GraphEditor extends AssetTabEditor<BehaviorAsset, GraphCanvas> {
private readonly addButton: Button;
private readonly snapBox = new Checkbox();
readonly breadcrumb = new Breadcrumb();
private trail: Trail = [];
constructor(state: EditorState) {
super(state, { noun: 'Behavior', infoWidth: INFO_WIDTH });
this.addButton = new Button('Add node…', { onClick: () => this.canvas.addNodeFrom(this.addButton) });
this.snapBox.onChange = (on) => this.canvas.setSnapping(on);
this.mount(new GraphCanvas(state), [this.addButton, new Label('Snap', { style: 'muted' }), this.snapBox]);
// Adding nodes is an edit, and edits made while playing would be thrown away with the run.
this.breadcrumb.onPick = (index) => this.open(this.trail[index]!, backTo(this.trail, index));
this.mount(
new GraphCanvas(state),
[new Label('Snap', { style: 'muted' }), this.snapBox],
this.breadcrumb,
);
this.canvas.onOpenBehavior = (id) => this.open(id, descend(this.trail, this.currentAssetId, id));
// Edits made while playing would be thrown away with the run.
state.on('mode', () => this.refresh());
}
/**
* The bar goes up before the graph switches: switching fits the view to the canvas, and a bar
* appearing afterwards would cut the bottom of the fitted graph off.
*/
private open(assetId: string, trail: Trail): void {
this.trail = trail;
this.showTrail();
this.state.select({ kind: 'asset', assetId });
this.refresh();
}
protected override refresh(): void {
super.refresh();
const project = this.state.project;
this.trail = follow(this.trail, this.currentAssetId, (id) =>
Boolean(findAssetOf(project, id, 'behavior')),
);
this.showTrail();
}
private showTrail(): void {
const names = this.trail.map((id) => findAsset(this.state.project, id)?.name ?? '');
if (this.breadcrumb.show(names)) this.relayout();
}
protected override selectedId(): string {
return this.state.selectedBehaviorId ?? '';
}
@@ -36,7 +70,6 @@ export class GraphEditor extends AssetTabEditor<BehaviorAsset, GraphCanvas> {
protected override syncExtras(asset: BehaviorAsset | null): void {
const editable = Boolean(asset) && this.state.mode === 'edit';
this.addButton.setEnabled(editable);
this.snapBox.setEnabled(editable);
}
}