# Functions A–P

Import these from `@grafloria/renderer`.

## Functions

### `applySpread`

Slide an attachment point along the port's EDGE (its tangent) by one lane.

Along the tangent, deliberately — sliding along the NORMAL would push the
endpoint off the node and leave a visible gap between the link and the port
it is supposed to be touching.

```ts
function applySpread(
  point: { x: number; y: number },
  side: Side,
  laneOffset: number
): { x: number; y: number }
```

### `assignSpreadLanes`

The lane assignment for every link on one port: `linkId → offset`.

Lanes are ordered by a STABLE key (the id of the link's other endpoint, then
the link's own id) rather than by insertion order, so the fan doesn't reshuffle
itself every time an unrelated link is added or the diagram is reloaded.

```ts
function assignSpreadLanes(
  entries: Array<{ linkId: string; sortKey: string }>,
  spec: PortSpreadSpec | undefined
): Map<string, number>
```

### `autoSizeDiagram`

Auto-size every opted-in node in a diagram. Returns the count that changed —
a host can skip a re-route when it is 0. Intended to run just before a frame
so routing consumes the settled bounds.

```ts
function autoSizeDiagram(
  nodes: Iterable<NodeModel>,
  opts: AutoSizeOptions = {}
): number
```

### `autoSizeNode`

Auto-size ONE node in place. No-op (returns false) when the node hasn't opted
in, or is already within half a pixel of its desired size — the idempotence
that keeps this safe to call every frame. Mutates strictly via `setSize`, so
the spatial index + routing observe the new bounds.

```ts
function autoSizeNode(node: NodeModel, opts: AutoSizeOptions = {}): boolean
```

### `buildHtmlForeignObject`

Build the `<foreignObject>` body VNode for an HTML node, sized to the node. Returns null when the node has no HTML content. The wrapper div is
`pointer-events: none` unless the content opts into interactivity, so the
shape background beneath keeps receiving selection / drag hits.

```ts
function buildHtmlForeignObject(
  node: NodeModel,
  width: number,
  height: number
): VNode | null
```

### `buildPathShapeDefinition`

BUILD a path shape's definition without storing it anywhere.

Extracted from {@link registerPathShape} so a PER-DIAGRAM registry can offer
`registerPathShape` too. Everything expensive and everything subtle — path
parsing, the one-slot outline sample memo, the derived boundary and port
anchors — lives here, so the global and scoped paths cannot drift into
producing different geometry for the same path.

```ts
function buildPathShapeDefinition(
  type: string,
  path: PathGeometry,
  opts: PathShapeOptions = {}
): ShapeDefinition
```

### `buildSelfLoopPoints`

Route a self-loop as a polyline: out of the source port, around, back into the
target port. EVERY segment is axis-aligned, which is what lets the three path
emitters each do the right thing with the same points — rounded rectangle for
`orthogonal`, a smooth closed-looking curve for `smooth`/`bezier`, a hard
polygon for `direct`.

Three cases, and they cover every port pairing:

A. SAME side (incl. the same port twice — the common case). The loop bulges
    straight out and spans laterally. When the two attachment points coincide
    (or sit closer together than `width`) they are SPREAD apart along the side
    by `width`, clamped to the node's own span: a loop whose feet are the same
    point has zero area and cannot be drawn at all.

B. PERPENDICULAR sides (right → top, …). Out of the source, one corner, into
    the target. An L that wraps the node's corner.

C. OPPOSITE sides (left → right, …). Out of the source, over/around the node
    body in a lane `size` clear of it, and back in the far side.

```ts
function buildSelfLoopPoints(spec: SelfLoopSpec): FanoutPoint[]
```

### `buildShapeBody`

```ts
function buildShapeBody(
  def: ShapeDefinition,
  width: number,
  height: number,
  cornerRadius: number | undefined,
  // `any` (not Record<string, any>) so the inline `style` string below is
  // accepted — matches the original render helpers' loosely-typed styles arg.
  styles: any
): VNode
```

### `buildShapeSelection`

Selection-highlight VNode: the outline grown by `padding`, plus baseProps. `radius` is the corner of the GROWN outline — the renderer passes the node's
own corner + padding, so the ring stays concentric with a rounded card; 6 is
the corner a node that declares none has always had.

```ts
function buildShapeSelection(
  def: ShapeDefinition,
  width: number,
  height: number,
  padding: number,
  baseProps: Record<string, any>,
  radius = 6
): VNode
```

### `buildShapeShadow`

Drop-shadow VNode: the outline offset by (offset, offset), plus baseProps.

```ts
function buildShapeShadow(
  def: ShapeDefinition,
  width: number,
  height: number,
  offset: number,
  borderRadius: number,
  baseProps: Record<string, any>
): VNode
```

### `bundleNormal`

The unit LEFT normal of a → b, i.e. the axis a parallel bundle fans along.

The caller must derive `a`/`b` from the bundle's CANONICAL node order (e.g.
lower node id first), not from each link's own source → target. Otherwise the
two halves of a bidirectional pair compute opposite normals, their opposite
lane offsets cancel out, and both links land back on top of each other — the
exact bug the card exists to fix.

Degenerate input (coincident points) falls back to "up", so a bundle between
two concentric nodes still fans instead of collapsing to NaN.

```ts
function bundleNormal(a: FanoutPoint, b: FanoutPoint): FanoutPoint
```

### `clampSizeToConstraints`

Clamp a candidate width/height to a node's sizing constraints. The per-node
min/max win; a global floor (the resizer's `minWidth`/`minHeight`) fills in
when the node sets none. Used identically by the resizer and the auto-sizer,
so both cannot disagree about the legal range.

```ts
function clampSizeToConstraints(
  width: number,
  height: number,
  sizing: NodeSizing,
  opts: ClampOptions = {}
): { width: number; height: number }
```

### `clampValue`

Clamp a scalar into `[min, max]`, tolerating an inverted or absent bound.

```ts
function clampValue(value: number, min?: number, max?: number): number
```

### `clearEdgeTemplates`

Drop every registration (tests, hosts tearing a document down).

```ts
function clearEdgeTemplates(): void
```

### `coalesce`

Merge dirty rects that overlap or nearly touch, so a drag of one node does not
fire N separate spatial queries. Cheap and approximate: one pass, absorbing
into the first box that already covers the candidate's neighbourhood. The
result is a SUPERSET of the input regions, which is the safe direction — it
can only invalidate more links, never fewer.

```ts
function coalesce(rects: Rect[], maxBoxes = 16): Rect[]
```

### `compensateForRotation`

Rotate `point` about the node's centre by `-degrees`, so a port on a rotated
node keeps its WORLD-space arrangement (a `compensateRotation` group on a node
spun 90° still shows its inputs on the left of the screen).

Node-local in, node-local out: the node's own transform then rotates it BACK,
and the two cancel.

```ts
function compensateForRotation(
  point: { x: number; y: number },
  width: number,
  height: number,
  degrees: number
): { x: number; y: number }
```

### `defaultInnerRect`

Padded-bbox label box: inset by up to 8px, clamped so it never inverts.

```ts
function defaultInnerRect(width: number, height: number): InnerRect
```

### `desiredNodeSize`

Compute the desired OUTER size of an auto-sized node from its label + reserved
panel content, clamped to the node's sizing constraints and any aspect lock. Pure — returns the size, mutates nothing.

```ts
function desiredNodeSize(
  node: NodeModel,
  opts: AutoSizeOptions = {}
): { width: number; height: number }
```

### `estimateTextWidth`

Average-glyph width estimate. See the module header for the rationale.

```ts
function estimateTextWidth(text: string, fontSize: number): number
```

### `fitCmdsToBox`

Rescale a parsed static path from its `viewBox` reference frame into a
`width × height` box anchored at the origin. Degenerate viewBox dimensions
fall back to 1 so a zero-width authoring box can't divide by zero.

```ts
function fitCmdsToBox(
  cmds: PathCmd[],
  viewBox: PathViewBox,
  width: number,
  height: number
): PathCmd[]
```

### `fitFontSize`

```ts
function fitFontSize(text: string, maxWidth: number, base: number): number
```

### `getEdgeTemplateVersion`

Bumped on every mutation — renderers key cache invalidation off this.

```ts
function getEdgeTemplateVersion(): number
```

### `getHtmlContent`

Read a node's HTML body spec, or null when it has none.

```ts
function getHtmlContent(node: NodeModel): HtmlNodeContent | null
```

### `getInnerRect`

The label box for a shape in local coords. Falls back to a padded bounding
box when the shape doesn't override `innerRect`.

```ts
function getInnerRect(def: ShapeDefinition, width: number, height: number): InnerRect
```

### `getLabelTemplate`

```ts
function getLabelTemplate(name: string): LabelTemplate | undefined
```

### `getLinkTemplate`

```ts
function getLinkTemplate(name: string): LinkTemplate | undefined
```

### `getMarker`

```ts
function getMarker(name: string): MarkerDefinition | undefined
```

### `getNodePanel`

Read a node's panel spec, or null when it has none.

```ts
function getNodePanel(node: NodeModel): PanelSpec | null
```

### `getNodeSizing`

Read a node's sizing config (never null — an absent config is `{}`).

```ts
function getNodeSizing(node: NodeModel): NodeSizing
```

### `getNodeToolbar`

Read a node's own toolbar config from metadata, or undefined.

```ts
function getNodeToolbar(node: NodeModel): NodeToolbarConfig | undefined
```

### `getPortLayout`

```ts
function getPortLayout(name: string | undefined): PortLayoutStrategy
```

### `getPortPositionForShape`

Calculate port position based on node shape
Returns position relative to node's local coordinate system (0,0 = top-left)

Shape-aware port positioning
- Rectangle: ports spread evenly along the edge (single port = midpoint)
- Circle/Ellipse: ports on the perimeter, fanned symmetrically per side
- Diamond: ports at vertices
- Hexagon: ports spread along the flat edges

Multiple ports on the same side are distributed by their rank among that
side's ports (ordered by `index`, then declaration order) so they never
stack on the same point.

```ts
function getPortPositionForShape(
  port: PortModel,
  node: NodeModel
): { x: number; y: number }
```

### `getShape`

The rect shape is the default fallback for unknown / unset shape types.

Resolution order is DIAGRAM-FIRST, then process-global. A diagram that
contributed its own `badge` sees its own; every other diagram — and this one,
for every name it did not claim — still sees the global registry. See
`ext/registry-scope.ts` for why the lookup is ambient rather than threaded.

```ts
function getShape(type: string | undefined): ShapeDefinition
```

### `getShapeDefinition`

Snapshot the current catalogue, so a caller can restore it later. Used by the
ExtensionHost to give a scoped extension an exact "undo" even when it OVERWROTE
an existing shape rather than adding a new one.

```ts
function getShapeDefinition(type: string): ShapeDefinition | undefined
```

### `getShapeRegistryVersion`

Monotonic counter, incremented on every add/remove. Cheap cache key. NOTE: `registerShape` itself bumps it — see the wrapper installed below.

```ts
function getShapeRegistryVersion(): number
```

### `glyphHalfExtents`

Half-extents of the glyph box, honouring size/width/height with `radius` fallback.

```ts
function glyphHalfExtents(
  shape: PortShapeSpec | undefined,
  radius: number
): { hw: number; hh: number }
```

### `haloAllows`

Whether a Halo action is enabled: `undefined` → `def`, `false` → never,
`true` → always, an array → membership.

```ts
function haloAllows(
  config: NodeToolbarConfig,
  action: ToolbarHaloAction,
  def = true
): boolean
```

### `hashString`

FNV-1a — small, fast, stable across runs. Only used to key VNodes.

```ts
function hashString(value: string): string
```

### `hasHtmlContent`

True when the node renders an HTML body.

```ts
function hasHtmlContent(node: NodeModel): boolean
```

### `hasMarker`

```ts
function hasMarker(name: string): boolean
```

### `hasPanel`

True when the node carries a composite panel.

```ts
function hasPanel(node: NodeModel): boolean
```

### `hasPortLayout`

```ts
function hasPortLayout(name: string): boolean
```

### `hasShape`

Whether a shape type is registered (excludes the implicit rect fallback).

```ts
function hasShape(type: string): boolean
```

### `hitTestLink`

Part-aware link hit-test.

Precedence (highest first): endpoint / arrow handles (small grab radius) >
labels (bounding box) > body (near the path). Within the handle tier the
nearest handle wins, tie-broken by declaration order
(source-endpoint, target-endpoint, source-arrow, target-arrow).

```ts
function hitTestLink(
  options: LinkHitTestOptions,
  query: Point,
  tolerance: number
): LinkHitResult | null
```

**Parameters**

- `options`: the link geometry to test against
- `query`: the world-space point to test
- `tolerance`: grab distance for the body (and label box padding)

**Returns** the hit part with local info, or `null` when nothing is within reach

### `htmlLabelVNode`

A `foreignObject` VNode carrying arbitrary HTML, positioned so `anchor` is its
CENTRE (labels are centred on the path, unlike nodes which are top-left).

THE KEY CARRIES A CONTENT HASH, and that is load-bearing. The VNode patcher
treats `foreignObject` subtrees as OPAQUE: it patches their props but NEVER
diffs into their children, so that whatever a framework mounted inside stays
alive. That is exactly right for host-mounted content — and exactly wrong for
content we own, because an edited label would keep rendering its old HTML
forever. Folding the content into the key means changed HTML is a DIFFERENT
VNode identity, which the patcher replaces wholesale. Unchanged HTML keeps the
same key and the same live DOM.

```ts
function htmlLabelVNode(options: HtmlLabelOptions): VNode
```

### `inflate`

Grow a rect by `pad` on every side.

```ts
function inflate(r: Rect, pad: number): Rect
```

### `isAutoSized`

True when the node opts into content-aware auto-sizing.

```ts
function isAutoSized(node: NodeModel): boolean
```

### `linkBodyHitTolerance`

The grab distance for a link BODY — the one number the painted invitation
and the accepted press must both derive from.

The SVG renderer paints a transparent hit-area stroke `linkHitAreaWidth`
wide, but the interaction layer used to accept only the flat 5px floor: the
ring between them was DEAD — the DOM caught the pointer (cursor, native
focus — the "rectangle around the line" report) while the press selected
nothing. Every consumer of "how close is close enough to a link" now calls
this: the interaction controller (SVG mode), the canvas pick-buffer stroke,
and the hit-area painter (at exactly `2 x` this, minus slop).

`literalStrokeWidth` must be the resolved numeric width (fall back to 2 for
theme-bound `var(--…)` widths, matching the renderer's own literal
fallback).

```ts
function linkBodyHitTolerance(
  literalStrokeWidth: number,
  configWidth: number = DEFAULT_LINK_HIT_AREA_WIDTH
): number
```

### `linkHitAreaWidth`

Width of the invisible "interaction stroke" painted along a link.

Sized from the LITERAL stroke width, never a `var(--…)` expression — the
renderer resolves its literals before calling this. The `+ 8` keeps a grab
margin around fat strokes (a 10px casing would otherwise be pixel-hunting).

```ts
function linkHitAreaWidth(
  literalStrokeWidth: number,
  configWidth: number = DEFAULT_LINK_HIT_AREA_WIDTH
): number
```

### `listLabelTemplates`

```ts
function listLabelTemplates(): string[]
```

### `listLinkTemplates`

```ts
function listLinkTemplates(): string[]
```

### `listMarkers`

```ts
function listMarkers(): string[]
```

### `listShapes`

All registered shape type names (built-ins + extended library + aliases).

```ts
function listShapes(): string[]
```

### `mapPathCmds`

Map every coordinate in a command list through `fn` (structure preserved).

```ts
function mapPathCmds(cmds: PathCmd[], fn: (x: number, y: number) => Point): PathCmd[]
```

### `markerTipOffset`

Resolve a registered marker's tip offset for a concrete style.

```ts
function markerTipOffset(definition: MarkerDefinition, style: ArrowStyle): number
```

### `measureLabelContent`

The content box a label occupies: the widest wrapped line × the line count. Uses the same `estimateTextWidth` heuristic the label renderer wraps with, so
the measured box and the rendered text agree about where lines break.

```ts
function measureLabelContent(text: string, opts: MeasureLabelOptions = {}): ContentSize
```

### `measurePanelReserve`

Extra content space a panel reserves so content-aware auto-sizing
grows the shape to fit the WHOLE body. `top` = header + image stacked above
the label; `bottom` = the stacked rows below it; `width` = the widest text the
panel must show.

```ts
function measurePanelReserve(
  node: NodeModel
): { top?: number; bottom?: number; width?: number } | undefined
```

### `notifyEdgeTemplatesChanged`

Internal: let a PER-DIAGRAM registry participate in the version/notify
protocol. A scoped registration must invalidate cached VNodes for exactly the
reason a global one must — the definition is baked into the cache. Notifying
every renderer (not just the contributing one) is deliberate: over-invalidation
costs a repaint, under-invalidation shows a stale picture.

```ts
function notifyEdgeTemplatesChanged(): void
```

### `notifyShapeRegistered`

Internal: let `registerShape` participate in the version/notify protocol.

```ts
function notifyShapeRegistered(): void
```

### `nudgePortLabels`

Collision-aware nudging: when several port labels crowd, push them apart along
the axis they stack on.

Deliberately a ONE-AXIS resolver over labels that share a side. That is the
shape the crowding actually has — a column of inputs down the left edge whose
labels overlap vertically — and a general 2-D label-placement solver would be
both slower and less predictable (labels would visibly hop sideways as you
dragged a node). Returns a per-label nudge, so the caller can decide whether
to apply it.

`heights` are the labels' rendered heights, `centres` their unnudged centre
coordinates on the stacking axis, both in the SAME order.

```ts
function nudgePortLabels(centres: number[], heights: number[], gap = 2): number[]
```

### `onEdgeTemplateChange`

Subscribe to registry changes. Renderers use this to drop cached link VNodes
when a template is (re)defined — the template's OUTPUT is baked into the
cached VNode, so a redefinition that did not invalidate would never show up.

```ts
function onEdgeTemplateChange(listener: () => void): () => void
```

### `onShapeRegistryChange`

Subscribe to catalogue changes. Returns a disposer.

```ts
function onShapeRegistryChange(listener: () => void): () => void
```

### `outerSizeForInner`

Invert a shape's `innerRect`: the OUTER width/height whose label box is at
least `contentW × contentH`. `innerRect` is a monotone function of the outer
size (fractional insets for curved shapes, additive padding for the rect
default), so a few fixed-point steps converge — one step for the fractional
shapes, a handful for additive padding.

```ts
function outerSizeForInner(
  def: ShapeDefinition,
  contentW: number,
  contentH: number,
  seed?: { width: number; height: number }
): { width: number; height: number }
```

### `overlapArea`

```ts
function overlapArea(a: OptimizerRect, b: OptimizerRect): number
```

### `panelAdjustedInnerRect`

The body inner rect available to the node LABEL once the panel's header/image
(top) and rows (bottom) have taken their space. Keeps the label from
overlapping the panel bands. Given the shape's own inner rect.

```ts
function panelAdjustedInnerRect(
  node: NodeModel,
  inner: { x: number; y: number; w: number; h: number },
  width: number,
  height: number
): { x: number; y: number; w: number; h: number }
```

### `parallelOffsets`

The signed lane offsets for a bundle of `count` parallel links, spaced
`spacing` apart and centred on the un-separated route.

count 1 → [0]                     (a lone link NEVER moves — this is what
                                     keeps every existing diagram pixel-identical)
  count 2 → [-s/2, +s/2]
  count 3 → [-s, 0, +s]

Centred rather than one-sided so a bundle stays visually anchored on the line
the single link used to occupy: adding a second relationship between two
entities nudges both apart instead of shunting the diagram sideways.

```ts
function parallelOffsets(count: number, spacing = DEFAULT_PARALLEL_SPACING): number[]
```
