Edit the graph during play and watch values and flow run through it
The graph was only locked by five edit-mode guards in GraphCanvas. The running game reads the graph by reference each frame, so a new node, edge, changed value, disconnect or undo takes effect from the next frame. Stop discards these edits like everything else changed in play. A new On start node does not fire for a behavior that has already started. During play every data output shows the value it last produced, in the highlight colour and refreshed ten times a second. Values are kept per graph, because node ids are only unique within one; with several instances the most recent value wins. The Variables node reads the live variable bag, so only its own outputs are kept, as a snapshot. Each time an exec edge fires, a bead runs along it in 0.3 s, at most one every 0.15 s per edge, following the same curve the edge is drawn with. Beads run on game time and freeze while paused; the game logic is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
495756b500
commit
656d61f187
17 files changed
+542
-37
No files matched your search
@@ -98,6 +98,10 @@ export class NodeView extends Widget {
|
||||
private inlineTexts = new Map<string, string>();
|
||||
/** Which sides carry a link – a collapsed node marks those with one dot each. */
|
||||
private linked = { inputs: false, outputs: false };
|
||||
private liveTexts = new Map<string, string>();
|
||||
private readonly valueTexts = new Map<string, Text>();
|
||||
/** Where an output's value may be drawn: right-aligned before its label, clear of the input text. */
|
||||
private valueSlots = new Map<string, { left: number; right: number; top: number }>();
|
||||
|
||||
/**
|
||||
* Not fixed at construction: a nested-behavior node's ports come from another graph, so the
|
||||
@@ -202,6 +206,41 @@ export class NodeView extends Widget {
|
||||
return this.def.inputs.map((input) => this.inputText(input));
|
||||
}
|
||||
|
||||
get outputLines(): string[] {
|
||||
return this.def.outputs.map((output) => {
|
||||
const value = this.valueTexts.get(output.key)?.text;
|
||||
return value === undefined ? output.label : `${value} ${output.label}`;
|
||||
});
|
||||
}
|
||||
|
||||
/** Nothing clears them when play ends: the views are rebuilt then. */
|
||||
setLiveValues(values: Record<string, Value> | null, project: Project | null): void {
|
||||
const next = new Map<string, string>();
|
||||
for (const output of this.def.outputs) {
|
||||
const value = values?.[output.key];
|
||||
if (output.type === 'exec' || value === undefined) continue;
|
||||
next.set(output.key, formatLive(output, value, project));
|
||||
}
|
||||
if (next.size === this.liveTexts.size && [...next].every(([k, v]) => this.liveTexts.get(k) === v)) return;
|
||||
this.liveTexts = next;
|
||||
this.placeValues();
|
||||
}
|
||||
|
||||
private placeValues(): void {
|
||||
for (const text of this.valueTexts.values()) text.destroy();
|
||||
this.valueTexts.clear();
|
||||
if (this.collapsed) return;
|
||||
const style = textStyle('live');
|
||||
for (const [key, value] of this.liveTexts) {
|
||||
const slot = this.valueSlots.get(key);
|
||||
if (!slot || slot.right - slot.left < 12) continue;
|
||||
const text = new Text({ text: truncate(value, style, slot.right - slot.left), style });
|
||||
text.position.set(slot.right - text.width, slot.top);
|
||||
this.addChild(text);
|
||||
this.valueTexts.set(key, text);
|
||||
}
|
||||
}
|
||||
|
||||
private inputText(input: PortDef): string {
|
||||
const inline = this.inlineTexts.get(input.key);
|
||||
return inline === undefined ? input.label : `${input.label} ${inline}`;
|
||||
@@ -247,6 +286,7 @@ export class NodeView extends Widget {
|
||||
private rebuildTexts(): void {
|
||||
for (const t of this.texts) t.destroy();
|
||||
this.texts.length = 0;
|
||||
this.valueSlots = new Map();
|
||||
const add = (
|
||||
text: string,
|
||||
style: TextStyleToken,
|
||||
@@ -259,6 +299,7 @@ export class NodeView extends Widget {
|
||||
node.position.set(align === 'left' ? x : x - node.width, y);
|
||||
this.addChild(node);
|
||||
this.texts.push(node);
|
||||
return node;
|
||||
};
|
||||
add(
|
||||
this.def.label,
|
||||
@@ -268,7 +309,10 @@ export class NodeView extends Widget {
|
||||
'left',
|
||||
this.w - TITLE_X - (this.collapsed ? COLLAPSED_TITLE_END : PAD),
|
||||
);
|
||||
if (this.collapsed) return;
|
||||
if (this.collapsed) {
|
||||
this.placeValues();
|
||||
return;
|
||||
}
|
||||
let y = TITLE_HEIGHT + PAD;
|
||||
for (const line of this.paramLines) {
|
||||
add(line, 'small', PAD + 2, y + 2);
|
||||
@@ -282,8 +326,9 @@ export class NodeView extends Widget {
|
||||
const input = this.def.inputs[i];
|
||||
const output = this.def.outputs[i];
|
||||
const rowTop = y + i * ROW_HEIGHT + 2;
|
||||
let inputEnd = PAD;
|
||||
if (input) {
|
||||
add(
|
||||
const text = add(
|
||||
this.inputText(input),
|
||||
input.type === 'exec' ? 'body' : 'muted',
|
||||
PAD + PORT_RADIUS + 4,
|
||||
@@ -291,9 +336,10 @@ export class NodeView extends Widget {
|
||||
'left',
|
||||
NODE_WIDTH / 2 + 20,
|
||||
);
|
||||
inputEnd = text.x + text.width;
|
||||
}
|
||||
if (output)
|
||||
add(
|
||||
if (output) {
|
||||
const text = add(
|
||||
output.label,
|
||||
output.type === 'exec' ? 'body' : 'muted',
|
||||
this.w - PAD - PORT_RADIUS - 4,
|
||||
@@ -301,7 +347,11 @@ export class NodeView extends Widget {
|
||||
'right',
|
||||
NODE_WIDTH / 2 - 10,
|
||||
);
|
||||
if (output.type !== 'exec')
|
||||
this.valueSlots.set(output.key, { left: inputEnd + 6, right: text.x - 6, top: rowTop + 1 });
|
||||
}
|
||||
}
|
||||
this.placeValues();
|
||||
}
|
||||
|
||||
override redraw(): void {
|
||||
@@ -388,6 +438,12 @@ function formatField(def: PropertyDef, value: Value, project: Project | null): s
|
||||
return formatValue(value);
|
||||
}
|
||||
|
||||
function formatLive(output: PortDef, value: Value, project: Project | null): string {
|
||||
if (value === null) return '—';
|
||||
if (value === '') return '""';
|
||||
return output.field ? formatField(output.field, value, project) : formatValue(value);
|
||||
}
|
||||
|
||||
function formatValue(value: Value): string {
|
||||
// Whole numbers stay bare to keep nodes narrow; fractions match the field that edits them.
|
||||
if (typeof value === 'number') return Number.isInteger(value) ? String(value) : formatNumber(value);
|
||||
|
||||
Reference in new issue
Block a user