Files
feuerwerk/src/editor/graph/folding.ts
T
rootandClaude Opus 5 e9fa592078 Add the Variable node and show the variable name in node titles
Get/Set variable nodes now title themselves "Get <name>" / "Set <name>", and
the new Variable node just "<name>"; the title is the same folded or open,
and a collapsed node shrinks to it. The redundant "Variable: <name>" row is
gone unless the variable was deleted ("<name>?" is the only hint then).

The Variable node sets its value when an exec flow arrives and exposes the
value at any time. Its output is read when a downstream node pulls it, not
frozen at the write, so count = count + 1 is one Variable node plus an Add.
Get variable keeps reading once per pulse: switching it would change the
behaviour of existing games.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 13:34:09 +02:00

115 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { GraphEdge } from '../../core/graph';
/**
* Whether a folded edge shows its curve anyway. Folding hides the line across the graph, but not
* the connection: selecting either end brings it back, and a pulse travelling the edge always
* shows – a trace that skipped an edge would lose the thread it is there to draw.
*/
export function foldedEdgeShown(edge: GraphEdge, selected: ReadonlySet<string>, hot: boolean): boolean {
return hot || selected.has(edge.from.node) || selected.has(edge.to.node);
}
/**
* One mark per port (per side on a collapsed node), so hidden links of one port don't stack on the same
* pixels. The separator is NUL: a printable one merges node "a b" + port "c" with node "a" + port "b c".
*/
export function foldMarkKey(
side: { node: string; port: string },
isOutput: boolean,
collapsed = false,
): string {
return `${side.node}\0${collapsed ? '' : side.port}\0${isOutput ? 'out' : 'in'}`;
}
/** A collapsed node is as wide as its title needs, but never wider than an open one. */
export function collapsedNodeWidth(titleWidth: number, chrome: number, openWidth: number): number {
return Math.min(openWidth, Math.ceil(titleWidth + chrome));
}
/** One name plus a count: three names do not fit, and the count is what says "there is more here". */
export function foldMarkLabel(names: readonly string[]): string {
if (names.length === 0) return '';
return names.length === 1 ? names[0]! : `${names[0]} +${names.length - 1}`;
}
/** One end of one hidden link, as the canvas measured it. */
export interface FoldMarkEnd {
edgeId: string;
key: string;
/** The node at the far end. */
other: string;
otherLabel: string;
isOutput: boolean;
portX: number;
portY: number;
color: number;
hot: boolean;
}
/** The marks to draw: one per port, however many links it hides. */
export interface FoldMarkPlan {
edgeIds: string[];
/** Far-end nodes, deduped – clicking the mark selects them all. */
others: string[];
names: string[];
isOutput: boolean;
x: number;
y: number;
portX: number;
color: number;
hot: boolean;
}
/** A bundle is drawn slightly larger, so "more than one link hides here" is visible. */
export function foldMarkRadius(linkCount: number, base: number): number {
return linkCount > 1 ? base + 2 : base;
}
/**
* Groups the ends by port, in the order they were met. `stub` is how far the mark sits out of its
* port – the direction follows `isOutput`.
*/
export function buildFoldMarks(ends: readonly FoldMarkEnd[], stub: number): FoldMarkPlan[] {
const byKey = new Map<string, FoldMarkPlan>();
for (const end of ends) {
let mark = byKey.get(end.key);
if (!mark) {
mark = {
edgeIds: [],
others: [],
names: [],
isOutput: end.isOutput,
x: end.portX + (end.isOutput ? stub : -stub),
y: end.portY,
portX: end.portX,
color: end.color,
hot: end.hot,
};
byKey.set(end.key, mark);
}
mark.edgeIds.push(end.edgeId);
mark.names.push(end.otherLabel);
if (!mark.others.includes(end.other)) mark.others.push(end.other);
// A pulse anywhere in the bundle colours the whole mark: it is the one thing worth spotting.
if (end.hot && !mark.hot) {
mark.hot = true;
mark.color = end.color;
}
}
return [...byKey.values()];
}
/** The mark under a point, or null. `slack` widens the dot so it stays clickable. */
export function foldMarkAt<T extends { edgeIds: string[]; x: number; y: number }>(
marks: readonly T[],
point: { x: number; y: number },
base: number,
slack = 4,
): T | null {
for (const mark of marks) {
const radius = foldMarkRadius(mark.edgeIds.length, base);
if (Math.hypot(point.x - mark.x, point.y - mark.y) <= radius + slack) return mark;
}
return null;
}