# Instance

Import these from `@grafloria/renderer`.

## On their own pages

- [`CreateDiagramOptions`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance-creatediagramoptions)
- [`DiagramInstance`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance-diagraminstance)
- [`EdgeSpec`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance-edgespec): An edge, as a host hands it in. Node-to-node, like React Flow.
- [`NodeSpec`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance-nodespec)
- [`PortSpec`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance-portspec): A port on a node. Omit `id` to get the deterministic `<nodeId>__<side>` name.

## Functions

### `applyEdges`

Reconcile the diagram's links against `specs`. See {@link applyNodes}.

```ts
function applyEdges(diagram: DiagramModel, specs: Array<EdgeSpec | LinkModel>): boolean
```

### `applyEdgeSpec`

Apply the mutable parts of an edge spec onto an existing link.

```ts
function applyEdgeSpec(link: LinkModel, spec: EdgeSpec): void
```

### `applyGroups`

Reconcile the diagram's groups against `specs` — add, update, remove — the
way {@link applyNodes} does for nodes. A live `GroupModel` passes through (a
Mermaid subgraph arrives that way). Removing a group never removes its boxes.

```ts
function applyGroups(diagram: DiagramModel, specs: Array<GroupSpec | GroupModel>): boolean
```

**Returns** whether anything changed.

### `applyNodes`

Reconcile the diagram's nodes against `specs`: add what is new, update what
moved, remove what disappeared. Live `NodeModel`s pass through untouched, so a
host can mix "give me the data" with "here is my own model".

```ts
function applyNodes(diagram: DiagramModel, specs: Array<NodeSpec | NodeModel>): boolean
```

**Returns** whether anything changed (i.e. whether a repaint is warranted).

### `applyNodeSpec`

Apply the mutable parts of a spec onto an existing node (the update path).

```ts
function applyNodeSpec(node: NodeModel, spec: NodeSpec): void
```

### `buildEdge`

Build a fresh `LinkModel` from a spec. Returns null when an endpoint is unresolvable.

```ts
function buildEdge(
  diagram: DiagramModel,
  spec: EdgeSpec,
  index: number
): LinkModel | null
```

### `buildNode`

Build a fresh `NodeModel` from a spec, with deterministic ports.

```ts
function buildNode(spec: NodeSpec, index: number): NodeModel
```

### `buildPort`

The gating spec is flattened onto the model's individual fields because that
is the shape `resolvePortConfig()` reads; `PortSpec.gating` is only the
ergonomic grouping of them.

A port with no explicit `side` and no `group` still lands on `right` (the
`PortModel` default) — but a port that names a group and no side is now built
WITHOUT `side`, so `explicitSide` stays false and the group's side is
inherited, which is the entire reason that flag exists.

```ts
function buildPort(nodeId: string, spec: PortSpec, index: number): PortModel
```

### `contentBounds`

World bounding box of every visible node, or null when there is nothing to fit.

```ts
function contentBounds(model: DiagramModel): Rectangle | null
```

### `createDiagram`

```ts
function createDiagram(
  container: HTMLElement,
  options: CreateDiagramOptions = {}
): DiagramInstance
```

### `defaultPortId`

The deterministic id of a node's default port on `side`.

```ts
function defaultPortId(nodeId: string, side: (typeof PORT_SIDES)[number]): string
```

### `edgeSpecId`

Stable id for the nth edge of a spec list.

```ts
function edgeSpecId(spec: EdgeSpec, index: number): string
```

### `htmlLayerStyle`

The HTML layer's style for a given camera transform (see ViewportController).

```ts
function htmlLayerStyle(transform: string): string
```

### `isGroupModel`

True for a live `GroupModel`.

```ts
function isGroupModel(value: unknown): value is GroupModel
```

### `isLinkModel`

True for a live `LinkModel`.

```ts
function isLinkModel(value: unknown): value is LinkModel
```

### `isNodeModel`

True for a live `NodeModel` (vs a plain spec object).

```ts
function isNodeModel(value: unknown): value is NodeModel
```

### `nodeHostStyle`

The style of one custom node's host element inside the HTML layer.

```ts
function nodeHostStyle(
  x: number,
  y: number,
  width: number,
  height: number
): string
```

### `nodeSpecId`

Stable id for the nth node of a spec list.

```ts
function nodeSpecId(spec: NodeSpec, index: number): string
```

### `resolvePortId`

Resolve an edge endpoint to a PORT id. Accepts (in order): an explicit port id, a side name, or the node's default
port for `fallbackSide`. Returns undefined when the node does not exist.

```ts
function resolvePortId(
  diagram: DiagramModel,
  nodeOrPortId: string,
  handle: string | undefined,
  fallbackSide: (typeof PORT_SIDES)[number]
): string | undefined
```

### `toEdgeSpec`

Model → spec for a link. See {@link toNodeSpec}.

```ts
function toEdgeSpec(link: LinkModel): EdgeSpec
```

### `toNodeSpec`

Model → spec: the projection a host needs to write model changes BACK into its
own state (a React `useState`, a Vue `ref`, a web-component property). Without
it a wrapper would have to reach into engine models, which is exactly the
coupling these specs exist to avoid.

```ts
function toNodeSpec(node: NodeModel): NodeSpec
```

## Classes

### `DomEventBinder`

```ts
class DomEventBinder
```

**Methods**

- `getDraggingNodeIds(): string[]` — . The nodes currently being DRAGGED (past the movement
threshold — an armed-but-uncommitted press is a click, not a drag).

Node-drag state lives here, not on the InteractionController, so a custom
node component had no way to know it was being dragged — and `dragging` is
one of the props the component contract promises. This is the read-only
window onto it.
- `hasActiveGesture(): boolean` — True while a resize / rotate / vertex gesture owns the pointer.

The companion to {@link getDraggingNodeIds} for the gestures that are NOT node drags. Custom-node culling needs it: unmounting a host element mid-resize would take the
pointer capture and the handles with it, and `SelectionToolsController` keeps the
gesture's own node private, so "is a gesture live" plus the current selection is the
answer available from out here.
- `constructor( private readonly container: HTMLElement, private readonly host: DomEventBinderHost, options: DomEventBinderOptions = {} )`
- `attach(): void` — Bind DOM listeners. No-op on the server and no-op if already attached.
- `detach(): void` — Remove EXACTLY the listeners we added, and drop all gesture state.
- `get isAttached(): boolean`
- `onWheel(event: WheelEvent): void`
- `onMouseDown(event: MouseEvent): void`
- `onMouseMove(event: MouseEvent): void`
- `onMouseUp(event: MouseEvent): void`
- `onMouseLeave(): void` — Pointer left the canvas — abort every in-flight gesture so nothing sticks.
- `onDoubleClick(event: MouseEvent): void` — Double-click: node → in-place rename; link label → rename; link body → waypoint.
- `onKeyDown(event: KeyboardEvent): void`
- `onKeyUp(event: KeyboardEvent): void`
- `beginLabelEdit(target: TextEditTarget, options?: { seed?: string }): boolean` — Open the in-place label editor programmatically — the seam behind F2 and a
host's context-menu Rename. Unlike the double-click path this is NOT gated
on `enableInPlaceTextEdit`: an explicit call IS the host's opt-in. Returns false when the target does not exist / is not editable / readonly.

### `RenderScheduler`

```ts
class RenderScheduler
```

**Methods**

- `constructor(options: RenderSchedulerOptions)`
- `get stats(): Readonly<RenderSchedulerStats>`
- `get pending(): boolean` — True while a frame is queued but has not run yet.
- `schedule(): void` — Mark dirty and queue a frame. Idempotent within a tick — the second and
later calls before the frame runs are counted as `coalesced`, not queued.
- `flush(): void` — Paint NOW, bypassing rAF and the idle-skip check, and cancel any queued
frame. This is the mount paint (and the "give me a correct DOM before I
measure it" escape hatch).
- `cancel(): void` — Drop a queued frame without painting.
- `dispose(): void`

## Constants

### `HTML_LAYER_CLASS`

```ts
const HTML_LAYER_CLASS: "grafloria-html-layer"
```

### `INSTANCE_ATTR`

`data-grafloria-instance` — the renderer's CSS scope, mirrored onto the root.

```ts
const INSTANCE_ATTR: "data-grafloria-instance"
```

### `PORT_SIDES`

```ts
const PORT_SIDES: readonly ["top", "right", "bottom", "left"]
```

### `ROOT_CLASS`

The DOM skeleton of a mounted diagram — ONE definition, used by both halves.

The server (`renderToStaticSVG`) emits this markup as a string; the client
(`createDiagram`) builds the identical structure with `createElement`, or
ADOPTS the server's when hydrating. Any divergence between the two — a class
name, a style declaration, an attribute — is a hydration mismatch, so both
paths read the constants from here rather than each spelling them out.

<div class="grafloria-diagram-root" data-grafloria-instance="grafloria-1">
    <div class="grafloria-svg-layer">   <svg …/>  </div>   ← the deterministic part
    <div class="grafloria-html-layer">  …custom nodes…  </div>  ← client-only
  </div>

The HTML layer holds nodes that render as framework components (React
portals, slotted templates). It carries the camera as a CSS transform so it
stays registered with the SVG layer, and is `pointer-events: none` so it does
not eat clicks meant for the SVG underneath — each mounted node host turns
pointer events back on for itself.

```ts
const ROOT_CLASS: "grafloria-diagram-root"
```

### `ROOT_STYLE`

```ts
const ROOT_STYLE: "position:relative;width:100%;height:100%;overflow:hidden"
```

### `SVG_LAYER_CLASS`

```ts
const SVG_LAYER_CLASS: "grafloria-svg-layer"
```

### `SVG_LAYER_STYLE`

```ts
const SVG_LAYER_STYLE: "position:absolute;top:0;left:0;width:100%;height:100%"
```

## Interfaces

### `DiagramEventMap`

```ts
interface DiagramEventMap
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `'nodes:change'` | `{ nodes: NodeModel[] }` |  |  |
| `'edges:change'` | `{ edges: LinkModel[] }` |  |  |
| `'selection:change'` | `{ nodes: NodeModel[]; edges: LinkModel[] }` |  |  |
| `connect` | `{ link: LinkModel }` |  |  |
| `reconnect` | `{ link: LinkModel; endpoint: 'source' \| 'target' }` |  |  |
| `'node:click'` | `{ node: NodeModel; world: { x: number; y: number } }` |  |  |
| `'node:doubleclick'` | `{ node: NodeModel; world: { x: number; y: number } }` |  |  |
| `'edge:click'` | `{ edge: LinkModel; world: { x: number; y: number } }` |  |  |
| `'viewport:change'` | `{ viewport: Rectangle; zoom: number }` |  |  |
| `ready` | `void` |  |  |
| `'nodes:change'` | `{ nodes: NodeModel[] }` |  |  |
| `'edges:change'` | `{ edges: LinkModel[] }` |  |  |
| `'selection:change'` | `{ nodes: NodeModel[]; edges: LinkModel[] }` |  |  |
| `'node:click'` | `{ node: NodeModel; world: { x: number; y: number } }` |  |  |
| `'node:doubleclick'` | `{ node: NodeModel; world: { x: number; y: number } }` |  |  |
| `'edge:click'` | `{ edge: LinkModel; world: { x: number; y: number } }` |  |  |
| `'viewport:change'` | `{ viewport: Rectangle; zoom: number }` |  |  |

### `DomEventBinderHost`

Everything the binder needs from its host. Keeps this class DI-free.

```ts
interface DomEventBinderHost
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `viewport` | `ViewportController` |  |  |
| `interaction` | `InteractionController` |  |  |

**Members**

- `getEngine(): DiagramEngine | null` — The engine, or null before a diagram is attached.
- `getRect(): CanvasRect` — The canvas' client rect (for screen→world).
- `requestRender(): void` — "Something visible changed" — the host coalesces this into a frame.
- `emit(event: string, payload: unknown): void` — Emit a public diagram event (`node:click`, `connect`, …).

### `DomEventBinderOptions`

```ts
interface DomEventBinderOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `enablePan?` | `boolean` |  | Middle-drag / space-drag / wheel-scroll panning. Default true. |
| `enableZoom?` | `boolean` |  | Ctrl/⌘ + wheel zoom. Default true. |
| `zoomSensitivity?` | `number` |  | Relative zoom step per wheel notch. Default 0.1 (a notch is ×1.1). |
| `dragThreshold?` | `number` |  | CSS px the pointer must travel before a node drag commits. Default 4. |
| `readonly?` | `boolean` |  | Ignore every mutation-causing gesture (still pans/zooms). Default false. |

### `GroupFrameStyle`

A zone's own frame — what makes a group look like the tinted, captioned
regions of the diagrams AI tools draw instead of the theme's titled box. Declaring any of it replaces the theme frame (no title band).

```ts
interface GroupFrameStyle
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `fill?` | `string` |  |  |
| `stroke?` | `string` |  |  |
| `strokeWidth?` | `number` |  |  |
| `strokeDasharray?` | `string` |  |  |
| `borderRadius?` | `number` |  |  |
| `color?` | `string` |  | Caption colour. |
| `fontSize?` | `number` |  | Caption size in px. Default 11. |
| `fontWeight?` | `string \| number` |  |  |
| `fontFamily?` | `string` |  |  |
| `letterSpacing?` | `number` |  | Caption letter spacing in px. |
| `textTransform?` | `'none' \| 'uppercase' \| 'lowercase' \| 'capitalize'` |  |  |

### `GroupSpec`

A GROUP in the spec — a zone around some boxes. `bounds` pins its frame;
without it the frame is fitted around `children` with `padding`. The children
become the group's members (they travel with it). Stored as a GroupModel whose
`metadata.frameStyle` carries `style` + `labelPlacement`, so it serializes.

```ts
interface GroupSpec
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` |  |  |
| `label?` | `string` |  |  |
| `children?` | `string[]` |  |  |
| `bounds?` | `{ x: number; y: number; width: number; height: number }` |  |  |
| `padding?` | `number` |  | Space between the children and the fitted frame. Default 20. |
| `style?` | `GroupFrameStyle` |  |  |
| `labelPlacement?` | `GroupLabelPlacement` |  | Default 'top-left'. |
| `direction?` | `'LR' \| 'RL' \| 'TB' \| 'TD' \| 'BT'` |  | How the zone lays its boxes out under a composing layout: `'LR'` a row, `'TB'` a column. |

### `NodeSublabel`

A node's second line, when it needs its own font or colour. See `NodeSpec.sublabel`.

```ts
interface NodeSublabel
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `string` |  |  |
| `fontFamily?` | `string` |  | A CSS font stack, or `'mono'` for a monospace one. |
| `fontSize?` | `number` |  | px. Default: 0.85 of the label's size. |
| `color?` | `string` |  | Default: the theme's secondary text colour. |
| `fontWeight?` | `string \| number` |  |  |

### `RenderSchedulerOptions`

RenderScheduler — framework-agnostic rAF coalescing + idle-skip.

Blocker #3 of the headless-instance contract (see ./diagram-instance.ts): the
only render loop in the codebase was `DiagramCanvasComponent.scheduleRender()`,
a private Angular method. This is that logic, lifted verbatim in behaviour and
with no framework or DOM imports, so React / the web component / a plain
`<script>` host all inherit the same frame discipline:

- **Coalescing.** Any number of `schedule()` calls in one tick collapse into
    exactly ONE painted frame. A burst of engine events (`node:changed` ×N, a
    drag's mousemoves, several prop changes in one React commit) paints once.
  - **Idle-skip.** A queued frame is DROPPED when `shouldSkip()` says nothing
    visible can have changed — cheaper than a no-op render of a big diagram.
  - **Synchronous escape.** `flush()` paints right now and cancels the queued
    frame; used for the mount paint (so the first frame is not one rAF late)
    and by tests.

`requestFrame`/`cancelFrame` are injectable: pass fakes in tests, and note the
default falls back to `setTimeout` where rAF is missing (Node), so a scheduler
constructed during SSR never throws — it simply never gets a chance to fire
because nothing calls `schedule()` on the server.

```ts
interface RenderSchedulerOptions
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `onFrame` | `() => void` |  | The paint. Called at most once per frame. |
| `shouldSkip?` | `() => boolean` |  | Idle-skip predicate, evaluated INSIDE the frame (not at schedule time, so it sees the final state of the tick). Return true to drop the frame. |
| `requestFrame?` | `(cb: (time: number) => void) => number` |  | Injectable rAF (defaults to the platform one, with a setTimeout fallback). |
| `cancelFrame?` | `(handle: number) => void` |  | Injectable cancel, must pair with `requestFrame`. |

### `RenderSchedulerStats`

Cheap counters — a steady-state idle canvas should paint 0 frames.

```ts
interface RenderSchedulerStats
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `scheduled` | `number` |  | `schedule()` calls. |
| `painted` | `number` |  | Frames actually painted (`onFrame` ran). |
| `skipped` | `number` |  | Queued frames dropped by `shouldSkip()`. |
| `coalesced` | `number` |  | `schedule()` calls that folded into an already-queued frame. |
| `lastFrameMs` | `number` |  | Duration (ms) of the most recent paint. |

## Types

### `CreateDiagram`

The factory's own signature, for hosts that store it.

```ts
type CreateDiagram = typeof import('./create-diagram').createDiagram;
```

### `DiagramEventHandler`

```ts
type DiagramEventHandler<K extends DiagramEventName> = (
  payload: DiagramEventMap[K]
) => void;
```

### `DiagramEventName`

Also has every member of `String`, listed on its own entry.

```ts
type DiagramEventName = keyof DiagramEventMap;
```

### `EdgeInput`

Also has every member of `EdgeSpec`, listed on its own entry.

```ts
type EdgeInput = EdgeSpec | LinkModel;
```

### `GroupLabelPlacement`

Also has every member of `String`, listed on its own entry.

Where a zone's caption sits.

```ts
type GroupLabelPlacement = 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right';
```

### `NodeInput`

Also has every member of `NodeSpec`, listed on its own entry.

Nodes/edges may be handed in as plain specs or as live engine models.

```ts
type NodeInput = NodeSpec | NodeModel;
```
