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:
rootandClaude Opus 5 committed 2026-09-11 21:19:07 +02:00
1 parent 495756b500
commit 656d61f187
17 files changed
+542 -37

No files matched your search

+24 -3
View File
@@ -10,12 +10,14 @@ import {
foldMarkLabel,
foldMarkRadius,
} from './folding';
import { curvePoint, curveReach } from './beads';
import { type NodeView, portColor } from './NodeView';
/** How far a folded edge's stub reaches out of its port. */
const FOLD_STUB = 16;
const FOLD_MARK_RADIUS = 5;
const FOLD_LABEL_WIDTH = 96;
const BEAD_RADIUS = 4.5;
/** One port's hidden links as drawn: what they stand in for, and where they can be clicked. */
export interface FoldMark {
@@ -31,6 +33,7 @@ export interface FoldMark {
/** The curves between nodes and the marks of the hidden ones, in graph coordinates. */
export class EdgeLayer extends Container {
private readonly curves = new Graphics();
private readonly beads = new Graphics();
private readonly chips = new Graphics();
/** Texts naming the far node of each folded edge; pooled, since a redraw rebuilds them all. */
private readonly labelLayer = new Container();
@@ -40,10 +43,11 @@ export class EdgeLayer extends Container {
private marks: FoldMark[] = [];
/** Edges the last draw really stroked as a curve – a hidden one is missing from it. */
private readonly stroked = new Set<string>();
private beadsDrawn: { edgeId: string; progress: number; x: number; y: number }[] = [];
constructor() {
super();
this.addChild(this.curves, this.chips, this.labelLayer);
this.addChild(this.curves, this.beads, this.chips, this.labelLayer);
}
draw(
@@ -51,14 +55,17 @@ export class EdgeLayer extends Container {
views: ReadonlyMap<string, NodeView>,
isHot: (edgeId: string) => boolean,
selected: Set<string>,
beads: (edgeId: string) => readonly number[] = () => [],
): void {
this.curves.clear();
this.beads.clear();
this.beadsDrawn = [];
this.chips.clear();
this.marks = [];
this.ends = [];
this.labelCount = 0;
this.stroked.clear();
for (const edge of edges) this.drawEdge(edge, views, isHot(edge.id), selected);
for (const edge of edges) this.drawEdge(edge, views, isHot(edge.id), selected, beads(edge.id));
// After the curves, so no line drawn later crosses a mark that stands for a hidden one.
this.drawFoldMarks();
for (let i = this.labelCount; i < this.labels.length; i++) this.labels[i]!.visible = false;
@@ -76,11 +83,16 @@ export class EdgeLayer extends Container {
return this.stroked.has(edgeId);
}
drawnBeads(): readonly { edgeId: string; progress: number; x: number; y: number }[] {
return this.beadsDrawn;
}
private drawEdge(
edge: GraphEdge,
views: ReadonlyMap<string, NodeView>,
hot: boolean,
selected: Set<string>,
beads: readonly number[],
): void {
const from = views.get(edge.from.node);
const to = views.get(edge.to.node);
@@ -106,6 +118,15 @@ export class EdgeLayer extends Container {
alpha: (type === 'exec' || hot ? 1 : 0.85) * (edge.folded ? 0.6 : 1),
});
this.stroked.add(edge.id);
for (const progress of beads) {
const { x, y } = curvePoint(start.x, start.y, end.x, end.y, progress);
// Light with a dark rim: a bead in the highlight colour would vanish on the edge it lights up.
this.beads
.circle(x, y, BEAD_RADIUS)
.fill(Theme.colors.text)
.stroke({ width: 1.5, color: Theme.colors.graphBackground });
this.beadsDrawn.push({ edgeId: edge.id, progress, x, y });
}
}
private addFoldEnd(
@@ -167,6 +188,6 @@ export class EdgeLayer extends Container {
/** Horizontal S-curve between two points. */
export function bezier(g: Graphics, x1: number, y1: number, x2: number, y2: number): void {
const dx = Math.max(40, Math.abs(x2 - x1) * 0.5);
const dx = curveReach(x1, x2);
g.moveTo(x1, y1).bezierCurveTo(x1 + dx, y1, x2 - dx, y2, x2, y2);
}