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>
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
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;
|
||
}
|