/** * rgui core — node-graph model (framework-agnostic, world coordinates). * Bitwig The Grid-style: compact modules with a colored category header, * input ports on the left edge, output ports on the right edge. */ /** * Signal kinds are OPEN: the built-ins get hand-picked colors, any other * string gets a stable derived color — rgui renders any domain's graph * (media pipelines, org charts, dependency graphs) without registration. */ export type SignalKind = "image" | "audio" | "text" | "ctl" | (string & {}); export type NodeCategory = "source" | "model" | "sink" | (string & {}); /** * Which way data runs THROUGH a node: "ltr" (default) reads inputs on the * left edge and outputs on the right, "ttb" runs top-to-bottom, and the * reversed pair mirror them. Flow is per-node, so a right-to-left sink can * sit in a left-to-right pipeline and still present its ports to the wire. */ export type Flow = "ltr" | "rtl" | "ttb" | "btt"; /** an edge of a node's rect */ export type Side = "top" | "right" | "bottom" | "left"; /** * A port carries a signal, and may DECLARE that signal's algebra: whether its * values add (`measure`), whether they may be duplicated or aliased (`ownership`), * and what this port's fan-out does with them by default (`fanout`/`grain`). All default * to the conservative choice — an unmarked port broadcasts a freely-copyable * value and refuses to be summed — so graphs written before the signal algebra * existed keep their exact behavior. See core/signal.ts. */ export interface Port { id: string; label: string; kind: SignalKind; /** is `+` meaningful across parallel sources? (default "intensive": no) */ measure?: Measure; /** * CAPABILITY (default "copy"): may this value be duplicated, and may several * consumers hold it at once? Belongs to the PRODUCING port and is not * overridable downstream — only the node emitting a value knows whether it * hands out a coordinate ("copy"), a 4K frame whose duplication costs * ("clone"), a live handle that may be aliased but never duplicated ("share"), * or a resource with a single owner ("move"). On an input port it declares what * the consumer expects of what arrives. */ ownership?: Ownership; /** * POLICY (default "broadcast"): this port's default fan-out. The real owner is * the fan-out GROUP — override per group via `Graph.fanout`, apportion within a * split via `Edge.weight`. Constrained by `ownership`: a "move" signal may never * broadcast. */ fanout?: Fanout; /** split only: where cuts are legal */ grain?: Grain; /** grain "atom" only: the boundary's name — "line" | "frame" | "row" | … */ atom?: string; /** fan-in rule when several edges converge here (default per defaultMerge) */ merge?: MergeRule; } import type { MergeRule } from "./aggregate.js"; import { type Fanout, type Grain, type Measure, type Ownership } from "./signal.js"; export interface GraphNode { id: string; title: string; category: NodeCategory; /** world coords of top-left corner */ x: number; y: number; /** * depth in position space (default 0): under viewport 3-D rotation the * node's PROJECTED position shifts with z, but the node itself always * renders as an upright 2-D card facing the user */ z?: number; w: number; /** * data-flow direction through this node (default "ltr"): decides which * edge carries the inputs and which the outputs. It is also what lets two * nodes SNAP-CONNECT — pushed flush together, an output edge meeting a * compatible input edge wires itself up (see snapConnections). */ flow?: Flow; inputs: Port[]; outputs: Port[]; /** label: value rows shown in the node body */ fields: [string, string][]; /** * how each field MERGES when this node renormalizes into a block — * declared with the data it governs, e.g. { "min score": "max", * device: "set", vad: "any" }. Takes precedence over host-level * fieldSummarize maps; unlisted keys fall back to "mode". */ fieldRules?: Record; /** * pinned: excluded from dragging (and future auto-layout) — an immovable * anchor other nodes snap around. Toggled via the header pin glyph. */ pinned?: boolean; /** * CONTAINMENT: id of the node this one lives inside. A node referenced * as someone's parent becomes a CONTAINER — it renders as an open frame * around its children and, once every child falls below readability, it * absorbs them into one block titled by the container (org-chart teams, * subgraphs). Containment is a structural relation like an edge, not a * style tag, and it is the sanctioned exception to one-cell-one-thing: * a child occupies its container's cell at a finer grid layer. */ parent?: string; /** * explicit height override (world units) — resize grows the live-body * region; ignored when below the derived minimum (rows + bodyRows) */ h?: number; /** * CONTENT SCALE (default 1): how large this node renders ITSELF — * every internal metric (header, rows, padding, ports, fonts, and the * body/draw hooks' pixels) multiplies by it. Distinct from w/h, which * only say how much world the node occupies: growing w gives the same * 11px rows more room, raising scale magnifies the node like a lens. * Shift+drag on the corner grip rescales (aspect-preserving); a plain * drag resizes. * * SETTING THIS ALONE DOES NOT RESIZE THE BOX. w/h are not derived from * scale, so a node left at its base w/h with scale=2 keeps its old width * while its type doubles: nodeMinHeight doubles, nodeHeight() silently * snaps the height up past the declared h (how far depends on the row * count — the box always drifts off the ratio the author declared), and * the body/draw hooks receive half the content width they had. Nothing * throws. To magnify a node, move all * three together, height read BEFORE scale is assigned: * * n.w *= s; n.h = nodeHeight(n) * s; n.scale = s; * * or let Rgui.rescaleNode(id, s) do it for you. */ scale?: number; /** custom block background fill (default #2b3036) */ bg?: string; /** * Host-owned visual state. `remote` marks a read-only mirror from a * federated source; `shared` marks a LOCAL node the host is publishing * outward (the inverse of remote); `busy` lets live runtimes pulse a node * without changing the graph topology. All may be functions so hosts can * reflect live state. */ remote?: boolean | (() => boolean); shared?: boolean | (() => boolean); busy?: boolean | (() => boolean); /** * annotation / sticky-card node: renders as a plain card frame (no header * band, ports, or field rows) whose rich content is the HTML `overlay` (or a * `draw` callback). It still lives in world space and drags/snaps/selects * like any node, and may be connectable if given outputs. Use annotationNode() * to build one. Beats overloading a data node with custom draw + an overlay. */ note?: boolean; /** * node-anchored HTML overlay (interactive form controls etc.) — rgui * glues the element to the node's screen rect every frame and hides it * (without destroying) when the node collapses, goes off-screen, or is * too small to read. Size is screen-fixed. */ overlay?: { el: HTMLElement; anchor?: "right" | "below" | "over"; offset?: { x: number; y: number; }; interactive?: boolean; /** "fixed" (screen-constant, default) | "zoom" (scales with view.k) | * "fit" (scales to fill the node's screen area). See NodeHtmlOverlay. */ scale?: "fixed" | "zoom" | "fit"; /** zoom/fit: hide once the applied scale drops below this (default 0.75) */ minScale?: number; /** fit: cap on the applied scale (default 1 — never upscale past natural) */ maxScale?: number; /** clip the overlay to the node rect / viewport / not at all */ clip?: "node" | "viewport" | "none"; overflow?: "hidden" | "auto"; destroy?: () => void; }; /** * Full-content draw override: when set, rgui still owns the node block * (shape, fused boundaries, border, ports, pin, selection) but the ENTIRE * content — title, fields, everything — is drawn by this hook instead of * the defaults. Same contract as `body`: SCREEN-space ctx clipped to the * node, origin at the node's top-left, rect in screen px. */ draw?: (ctx: CanvasRenderingContext2D, rect: { width: number; height: number; }, view: { k: number; }) => void; /** * Reserved live-body rows below the field rows (row height NODE_ROW_H). * The `body` hook draws inside this region. */ bodyRows?: number; /** * Live-body draw hook: called every rendered frame with a SCREEN-space * ctx clipped to the body region (origin at the region's top-left, * rect in screen px). Auto-skipped when the node is collapsed into a * pseudo-node or too small to read. Call viewer.invalidate() when the * live data changes to schedule a redraw. */ body?: (ctx: CanvasRenderingContext2D, rect: { width: number; height: number; }, view: { k: number; }) => void; } export interface Edge { from: { node: string; port: string; }; to: { node: string; port: string; }; /** dashed = event/stream-style wire */ dashed?: boolean; /** per-edge style overrides (defaults come from the signal kind) */ style?: { color?: string; /** screen px */ width?: number; /** canvas dash pattern in screen px, e.g. [6, 5] */ dash?: number[]; }; /** short label drawn at the wire midpoint */ label?: string; /** * this edge's share of a SPLIT fan-out (default 1 = an even share). Weights * are the one part of a fan-out that lives on the individual edge — the policy * itself cannot, because conservation is a property of the whole division. * Ignored unless the group's fanout is "split". */ weight?: number; /** * SNAP-CONNECTED: derived from geometry, not authored. Two nodes pushed * flush together with facing, kind-compatible ports wire themselves up; * drag them apart and the edge disappears. Temp edges are recomputed from * scratch on every move, so hosts must never persist them — filter them * out when saving, or promote one to a real edge to keep it. */ temp?: boolean; } export interface Graph { nodes: GraphNode[]; edges: Edge[]; /** * Per-fan-out-group POLICY override, keyed `"nodeId.portId"`. A fan-out group * is the set of edges leaving one output port, and the policy belongs to the * group rather than to the port or to any single edge: the same audio-segment * port broadcasts to a recorder in one graph and round-robins across a worker * pool in another. That is a topology decision, so the graph owns it. The port * only supplies the default. A "move" signal may never be overridden to * "broadcast" — see checkSignals. */ fanout?: Record; } export declare const NODE_HEADER_H = 26; export declare const NODE_ROW_H = 22; export declare const NODE_PAD = 10; export declare const PORT_R = 5; /** port pitch along a horizontal (top/bottom) edge — the ROW_H of vertical flow */ export declare const NODE_COL_W = 64; /** content scale (default 1, always positive) */ export declare function contentScale(n: GraphNode): number; /** * A node's interior metrics in WORLD units. The base constants describe a * scale-1 node; every interior length rides the node's content scale, which * is what makes a rescaled node a magnified copy of itself rather than a * bigger box holding the same small type. */ export declare function nodeMetrics(n: GraphNode): { s: number; headerH: number; rowH: number; pad: number; portR: number; }; /** number of stacked rows (fields vs. the taller port column) */ export declare function nodeRows(n: GraphNode): number; /** derived minimum height (rows + reserved body rows) */ export declare function nodeMinHeight(n: GraphNode): number; /** derived minimum width — a scale-1 node never narrows below 96 wu */ export declare const NODE_MIN_W = 96; export declare function nodeMinWidth(n: GraphNode): number; export declare function nodeHeight(n: GraphNode): number; /** * Build a first-class annotation / sticky-card node: a world-space card whose * body is rich HTML (the overlay `el`), with no ports/header/fields. It drags, * snaps, and selects like any node, and is connectable if given `outputs`. * graph.nodes.push(annotationNode({ id, x, y, w: 240, el: myCard, * title: "What is this?" })) * * `scale` controls how the HTML body tracks zoom: "fixed" (screen-constant, * default — keeps buttons/text readable) or "fit"/"zoom" to scale with the * card's world frame (with minScale/maxScale). Passing BOTH `el` and `draw` * works well: the HTML body shows up close, and the canvas `draw` acts as a * lower-detail fallback once the overlay hides (below minScale / too small). */ export declare function annotationNode(opts: { id: string; x: number; y: number; w?: number; h?: number; /** rich HTML body glued over the card (interactive) */ el?: HTMLElement; /** or a canvas-draw body (screen px within the card rect); also a good LOD * fallback drawn when the `el` overlay hides at small scales */ draw?: (ctx: CanvasRenderingContext2D, rect: { width: number; height: number; }) => void; title?: string; /** card fill (default node background) */ bg?: string; /** make the card connectable by giving it output ports */ outputs?: Port[]; /** overlay scale mode (default "fixed" — screen-constant) */ scale?: "fixed" | "zoom" | "fit"; /** zoom/fit: hide below this applied scale (default 0.75) */ minScale?: number; /** fit: cap on the applied scale (default 1) */ maxScale?: number; /** clip the HTML body to the card / viewport / not at all */ clip?: "node" | "viewport" | "none"; overflow?: "hidden" | "auto"; }): GraphNode; /** * A node's scale, per axis: the grid layer each dimension lives on * (sizeLayerStep of w and of height). Width and height may legitimately * live on DIFFERENT layers — a wide-flat node is coarse in x, fine in y — * and position snapping follows each axis's own layer. */ export declare function nodeScale(n: GraphNode, radix?: number): { x: number; y: number; }; /** direct children of a container node */ export declare function childrenOf(graph: Graph, id: string): GraphNode[]; /** every node transitively inside a container */ export declare function descendantsOf(graph: Graph, id: string): GraphNode[]; /** ids of every node that contains others (has at least one child) */ export declare function containerIds(graph: Graph): Set; /** * Containment predicate factory. `related(a, b)` is true when one node * contains the other (any depth). Containment overlap is SANCTIONED — a * child occupies its container's cell at a finer layer — so overlap * resolution, size clamping and flush-stack logic must skip related pairs. */ export declare function containmentOf(nodes: GraphNode[]): { inside: (a: string, b: string) => boolean; related: (a: string, b: string) => boolean; }; /** world-space rect of the reserved live-body region (null if none) */ export declare function bodyRect(n: GraphNode): { x: number; y: number; w: number; h: number; } | null; /** the axis data runs along: "h" for ltr/rtl, "v" for ttb/btt */ export declare function flowAxis(n: GraphNode): "h" | "v"; /** the edge this node's inputs sit on */ export declare function inSide(n: GraphNode): Side; /** the edge this node's outputs sit on (always opposite the inputs) */ export declare function outSide(n: GraphNode): Side; export declare function oppositeSide(s: Side): Side; /** true when `s` runs along the x axis, i.e. it is the top or bottom edge */ export declare const isHorizontalSide: (s: Side) => boolean; /** world y of a node's i-th port/field row center */ export declare function nodeRowY(n: GraphNode, i: number): number; /** * World center of the i-th port on a given SIDE of the node. Ports on the * left/right edges stack down in rowH rows (clearing the title band); ports on * the top/bottom edges march across in NODE_COL_W columns. Both pitches ride the * node's content scale, and both are constant for a given scale — so two nodes * with the same corner alignment and scale present their port i at the same * coordinate, which is what makes snap-connect land on whole ports instead of * guessing. */ export declare function sidePortPos(n: GraphNode, side: Side, i: number): [number, number]; /** world position of the i-th port on the node's declared in/out edge */ export declare function portPos(n: GraphNode, dir: "in" | "out", i: number): [number, number]; /** world position of an input port center */ export declare function inputPortPos(n: GraphNode, i: number): [number, number]; /** world position of an output port center */ export declare function outputPortPos(n: GraphNode, i: number): [number, number]; export declare function demoGraph(): Graph; export declare function signalGraph(ox?: number, oy?: number): Graph; export declare function orgChartGraph(): Graph; //# sourceMappingURL=graph.d.ts.map