# Engine

Import these from `@grafloria/engine`.

## Functions

### `isValidDiagramMode`

Type guard to check if a string is a valid DiagramMode

```ts
function isValidDiagramMode(mode: string): mode is DiagramMode
```

## Classes

### `DiagramEngine`

```ts
class DiagramEngine
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `eventBus` | `EventBus` |  |  |
| `store` | `DiagramStore` |  |  |
| `commandManager` | `CommandManager` |  |  |
| `pluginManager` | `PluginManager` |  |  |
| `typeRegistry` | `TypeRegistry` |  |  |
| `validationEngine` | `ValidationEngine` |  |  |
| `serializer` | `DiagramSerializer` |  |  |
| `performanceMonitor` | `PerformanceMonitor` |  |  |
| `modeManager` | `ModeManager` |  |  |
| `clipboardManager` | `ClipboardManager` |  |  |
| `selectionManager` | `SelectionManager` |  |  |
| `routingEngine` | `RoutingEngine` |  |  |
| `templateRegistry` | `TemplateRegistry` |  |  |

**Methods**

- `constructor(config: DiagramEngineConfig = {})`
- `getDiagram(): DiagramModel | null` — Get current diagram
- `getConfig(): DiagramEngineConfig` — Get configuration
- `getInteractionConfig(): InteractionConfig` — Get interaction configuration
Returns the current interaction mode settings

A CACHED, FROZEN snapshot — not a fresh spread per call. This getter is on
the hottest paths in the product: the renderer consults it per port and per
link inside every frame, and the binder on every pointer event, so the old
`{ ...config }` allocated tens of thousands of full copies per second and
showed up as the single largest self-time in a 2,000-node drag profile
(~590ms of a 4.5s gesture — more than routing).

The spread existed to keep callers from mutating engine state; the freeze
keeps that promise the honest way. A caller that used to scribble on its
private copy now throws instead of silently diverging — which is the
correct outcome, because two callers sharing one snapshot must not see each
other's scribbles.
- `setInteractionConfig(config: Partial<InteractionConfig>): void` — Set interaction configuration
Updates interaction mode settings and emits event
- `getConnectionStateManager(): ConnectionStateManager` — Get connection state manager
Used for managing connection drag operations
- `getReconnectionPreview(): ReconnectionPreview | null` — Current endpoint-reconnection preview, or null when
no endpoint is being dragged. The renderer reads this to draw a ghost link.
- `setReconnectionPreview(preview: ReconnectionPreview | null): void` — Set (or clear, with null) the endpoint-reconnection
preview. Called by the interaction layer on start/move/end of an endpoint
drag. Does not emit — the interaction layer already triggers re-render.
- `getProximityPreview(): ProximityPreview | null` — The proximity-connect proposal the renderer draws as a live wire, or null.
- `setProximityPreview(preview: ProximityPreview | null): void` — Set (or clear, with null) the proximity-connect proposal. Does not emit —
the node drag that drives it already triggers re-renders.
- `getSnapGuides(): SnapGuideSegment[] | null` — The live snap-guide segments a node drag is showing, or null.
- `setSnapGuides(guides: SnapGuideSegment[] | null): void` — Set (or clear, with null) the live snap guides. Does not emit — the node
drag that drives them already triggers re-renders.
- `getRoutingEngine(): RoutingEngine` — Get routing engine
Used for calculating link paths with various algorithms
- `enableLiveRerouting(): void` — Enable live rerouting
Automatically updates link paths when nodes move or resize
- `disableLiveRerouting(): void` — Disable live rerouting
- `getLiveReroutingEngine(): LiveReroutingEngine | null` — Get live rerouting engine
- `setDiagram(diagram: DiagramModel | null): void` — Set diagram
- `createDiagram(name: string = 'Untitled'): DiagramModel` — Create new diagram
- `clearDiagram(): void` — Clear diagram
- `async addNode(config: { type: string; position: Point; size?: Size; data?: any; }): Promise<NodeModel>` — Add node (from config)
Add node (pre-created NodeModel)
Add node implementation
- `async addNode(node: NodeModel): Promise<NodeModel>` — Add node (from config)
Add node (pre-created NodeModel)
Add node implementation
- `async addNode(configOrNode: { type: string; position: Point; size?: Size; data?: any } | NodeModel): Promise<NodeModel>` — Add node (from config)
Add node (pre-created NodeModel)
Add node implementation
- `async removeNode(nodeId: string): Promise<void>` — Remove node

Async + awaited, mirroring removeGroup(). The execute() promise
used to float — a command failure became an unhandled rejection (fatal
under Node), and callers could not sequence on the removal completing.
- `async addLink(config: { sourcePortId: string; targetPortId: string; type?: string; data?: any; }): Promise<LinkModel>` — Add link
- `async removeLink(linkId: string): Promise<void>` — Remove link

Async + awaited, mirroring removeGroup() — see removeNode().
- `async addGroup(config: { name: string }): Promise<GroupModel>` — Add group
- `async removeGroup(groupId: string): Promise<void>` — Remove group
- `async addToGroup(groupId: string, entityId: string): Promise<void>` — Add entity to group
- `async removeFromGroup(groupId: string, entityId: string): Promise<void>` — Remove entity from group
- `async expandGroup(groupId: string): Promise<void>` — Expand group
- `async collapseGroup(groupId: string, options?: CollapseOptions): Promise<void>` — Collapse group
- `getGroup(groupId: string): GroupModel | undefined` — Get group by ID
- `getGroups(): GroupModel[]` — Get all groups
- `async setLayout( groupId: string, layoutType: 'flexbox' | 'grid', layoutConfig: LayoutConfig ): Promise<void>` — Set layout configuration on a group
- `async clearLayout(groupId: string): Promise<void>` — Clear layout configuration from a group
- `getLayout(groupId: string): { type: LayoutType; config?: LayoutConfig } | undefined` — Get layout configuration from a group
- `async setFlexItem(nodeId: string, flexConfig: FlexItemConfig): Promise<void>` — Set flex item configuration on a node
- `async setGridItem(nodeId: string, gridConfig: GridItemConfig): Promise<void>` — Set grid item configuration on a node
- `async copy(options?: { includeGroups?: boolean; includeLinks?: boolean }): Promise<void>` — Copy selected entities to clipboard
- `async paste(options?: { offset?: Point; selectPasted?: boolean }): Promise<void>` — Paste entities from clipboard
- `async duplicate(options?: { offset?: Point; selectDuplicated?: boolean }): Promise<void>` — Duplicate selected entities
- `async deleteSelection(options?: { deleteChildren?: boolean; deleteLinks?: boolean }): Promise<void>` — Delete selected entities
- `getClipboardData()` — Get clipboard data
- `hasClipboardData(): boolean` — Check if clipboard has data
- `clearClipboard(): void` — Clear clipboard
- `getClipboardStats()` — Get clipboard statistics
- `validateDiagram(options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult` — Validate the entire diagram
- `validateNode(nodeId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult` — Validate a specific node
- `validateLink(linkId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult` — Validate a specific link
- `validatePort(portId: string, nodeId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult` — Validate a specific port
- `validateLayout(groupId: string, options?: { strict?: boolean }): ValidationResult` — Validate layout configuration for a group
- `registerNodeType(definition: NodeTypeDefinition): void` — Register a node type definition
- `registerPortType(definition: PortTypeDefinition): void` — Register a port type definition
- `registerLinkType(definition: LinkTypeDefinition): void` — Register a link type definition
- `registerGroupType(definition: GroupTypeDefinition): void` — Register a group type definition
- `enableRealTimeValidation(): void` — Enable real-time validation
- `disableRealTimeValidation(): void` — Disable real-time validation
- `isRealTimeValidationEnabled(): boolean` — Check if real-time validation is enabled
- `selectNodes(nodeIds: string[]): void` — Select nodes
- `selectLinks(linkIds: string[]): void` — Select links
- `clearSelection(): void` — Clear selection
- `async undo(): Promise<void>` — Undo
- `async redo(): Promise<void>` — Redo
- `canUndo(): boolean` — Can undo
- `canRedo(): boolean` — Can redo
- `validate(): ValidationResult` — Validate diagram
- `serialize(): SerializedDiagram | null` — Serialize diagram (with mode)
- `deserialize(data: SerializedDiagram, options?: import('../models/DiagramModel').DiagramLoadOptions): DiagramModel` — Deserialize diagram (with mode)
- `loadFromJSON( json: string | SerializedDiagram, options?: import('../models/DiagramModel').DiagramLoadOptions ): DiagramModel` — Load diagram from JSON (with mode)
- `saveToJSON(): string | null` — Save diagram to JSON
- `async registerPlugin(plugin: Plugin): Promise<void>` — Register a plugin AND bring it to life.

`PluginManager.register()` only RECORDS a plugin; `install()` and
`activate()` are separate steps. Calling register alone — which both of
this engine's entry points used to do — left every plugin permanently
inert: its hooks never fired, though `getPlugin()` happily returned it.
"Register a plugin" can only sensibly mean "make it run", so this drives
the full lifecycle. A plugin that throws is reported and skipped rather
than taking the host down with it.
- `getPlugin(name: string): Plugin | undefined` — Get plugin
- `setViewport(viewport: Viewport): void` — Set viewport
- `setZoom(zoom: number): void` — Set zoom
- `getPerformanceReport(): PerformanceReport` — Get performance report
- `getMode(): DiagramMode` — Get current diagram mode
- `setMode(mode: DiagramMode): void` — Set diagram mode
- `isDesignerMode(): boolean` — Check if in designer mode
- `isRunningMode(): boolean` — Check if in running mode
- `isViewMode(): boolean` — Check if in view mode
- `isDebugMode(): boolean` — Check if in debug mode
- `isPresentationMode(): boolean` — Check if in presentation mode
- `isReadOnlyMode(): boolean` — Check if in read-only mode (any mode except designer)
- `addModeGuard(name: string, guard: ModeGuardFunction): void` — Add mode transition guard
- `removeModeGuard(name: string): void` — Remove mode transition guard
- `clearModeGuards(): void` — Clear all mode transition guards
- `configureModeViewport(mode: DiagramMode, settings: ModeViewportSettings): void` — Configure viewport settings for specific mode
- `getModeViewportSettings(mode: DiagramMode): ModeViewportSettings` — Get viewport settings for specific mode
- `getModeHistory(): ModeHistoryEntry[]` — Get mode history
- `clearModeHistory(): void` — Clear mode history
- `previousMode(): void` — Navigate to previous mode
- `nextMode(): void` — Navigate to next mode
- `pushMode(mode: DiagramMode): void` — Push mode onto stack (save current, switch to new)
- `popMode(): void` — Pop mode from stack (return to previous)
- `getModeAnalytics(): ModeAnalytics` — Get mode analytics
- `beforeModeChange(hook: ModeChangeHook): () => void` — Register before mode change hook
- `afterModeChange(hook: ModeChangeHook): () => void` — Register after mode change hook
- `getLinkBehaviorForMode(baseBehavior: Partial<{ deletable: boolean; selectable: boolean }>): { deletable: boolean; selectable: boolean; }` — Get link behavior adjusted for current mode
- `initialize(): void` — Initialize the engine
- `getStore(): DiagramStore` — Get the store
- `on(event: string, listener: (...args: any[]) => void): void` — Subscribe to events
- `off(event: string, listener: (...args: any[]) => void): void` — Unsubscribe from events
- `destroy(): void` — Destroy engine
- `refreshGroupObstacles(): void` — Reconcile the shared ObstacleMap with the
diagram's GROUP state, idempotently:

- a COLLAPSED group (with geometry) is ONE solid obstacle;
  - members hidden under a collapsed group (at any depth) are NOT obstacles
    — they are not visible, and routing around invisible things produces
    inexplicable detours;
  - expanding restores the members and removes the group block.

Runs on every group add/remove/collapse/expand. Public so the grouping
feature (which owns collapse SEMANTICS but not the ObstacleMap) can force a
reconcile after batch operations.
- `setLayoutService(service: { applyLayout(diagram: DiagramModel, config: any): Promise<any>; }): void` — Set layout service for diagram layouts
- `getLayoutRegistry(): LayoutRegistry` — The named-algorithm registry, with the built-ins already registered.

THE BUG THIS CLOSES: `applyLayout()` below requires `setLayoutService()` —
and NOTHING in the codebase ever called it (the only mention is a doc comment
in layout/index.ts). So dagre, ELK, force, spectral and community — thousands
of lines, several of them untested — were UNREACHABLE from the engine. That
is the whole "auto-layout is fragmented" finding. Layout now works out of the
box, with no setup call.
- `async layout( name: string = DEFAULT_LAYOUT_NAME, options: UnifiedLayoutOptions = {} ): Promise<UnifiedLayoutResult>` — Lay out the whole diagram.

await engine.layout('dagre', { direction: 'LR' });

DETERMINISTIC and IDEMPOTENT: the same graph and seed produce byte-identical
coordinates, and running it twice changes nothing the second time. (The seed
defaults to a fixed constant, so an author who never thinks about seeds still
gets the same picture on every reload; randomness is opt-in.)

NOT to be confused with `DiagramModel.getLayoutManager()`, which answers a
DIFFERENT question — "where should this ONE newly-added node go?" — and is a
placement strategy, not a graph layout. The audit called them "two parallel
stacks" and asked for them to be merged; they are not parallel, and merging
them would force a single-node placer to pretend it can lay out a graph.
- `async layoutIncremental( options: IncrementalOptions & { name?: string } & UnifiedLayoutOptions = {} ): Promise<UnifiedLayoutResult & { movement: MovementReport; tween: TweenPlan }>` — Mental-map-preserving incremental layout.

await engine.layoutIncremental({ changed: [newNode.id], budget: { maxPerNode: 60 } });

Mermaid re-renders the whole diagram from scratch on every edit and destroys the
user's spatial memory of their own diagram. This does the opposite:

Returns a tween PLAN rather than animating: the engine says where things go at
time t, the host drives t — which is what keeps this runnable in a worker, in
SSR and in a test.

ONE SEMANTIC, STATED PLAINLY. This runs the `layered` engine, because it is the
only one that honours anchors DURING coordinate assignment. If the diagram's
current positions came from a DIFFERENT engine, the first incremental pass
necessarily re-draws it — and that is not a bug to paper over: "move as little
as possible" is ill-posed across engines, because there is no meaningful small
move between two engines' idea of the same graph.
- `setLayoutPort(port?: LayoutPort): void` — Run layout off the main thread.

The engine does NOT construct the Worker — that would bake one bundler's URL
scheme into the engine, which is exactly what the old (never-instantiated)
`LayoutWorkerPool` did with its hardcoded `/assets/workers/layout.worker.js`. The caller builds the worker however its toolchain likes and hands it in:

const worker = new Worker(new URL('./layout.worker', import.meta.url),
                              { type: 'module' });
    engine.setLayoutPort(worker as unknown as LayoutPort);

Pass `undefined` to go back to running inline.
- `async applyLayout(config: { adapter: string | any; options?: any; animate?: boolean; animationDuration?: number; fit?: boolean; canvasDimensions?: { width: number; height: number }; }): Promise<{ nodePositions: Map<string, { x: number; y: number }>; bounds: { x: number; y: number; width: number; height: number }; metadata?: any; }>` — Apply layout to current diagram
- `async applyDagreLayout( options?: { rankdir?: 'TB' | 'BT' | 'LR' | 'RL'; align?: 'UL' | 'UR' | 'DL' | 'DR'; nodesep?: number; edgesep?: number; ranksep?: number; marginx?: number; marginy?: number; ranker?: 'network-simplex' | 'tight-tree' | 'longest-path'; }, canvasDimensions?: { width: number; height: number } ): Promise<any>` — Quick helper: Apply Dagre layout
- `async applyELKLayout( options?: { algorithm?: 'layered' | 'force' | 'stress' | 'mrtree' | 'radial' | 'disco'; 'elk.direction'?: 'RIGHT' | 'LEFT' | 'DOWN' | 'UP'; 'elk.spacing.nodeNode'?: number; [key: string]: any; }, canvasDimensions?: { width: number; height: number } ): Promise<any>` — Quick helper: Apply ELK layout
- `dispose(): void` — Cleanup and dispose of all resources
Should be called when the engine is no longer needed

## Interfaces

### `DiagramEngineConfig`

```ts
interface DiagramEngineConfig
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `plugins?` | `Plugin[]` |  |  |
| `mode?` | `DiagramMode` |  |  |
| `performance?` | `{ enableMonitoring?: boolean; enableProfiling?: boolean; warnThreshold?: number; }` |  |  |
| `validation?` | `{ realTime?: boolean; strict?: boolean; }` |  |  |
| `history?` | `{ maxCommands?: number; maxSnapshots?: number; }` |  |  |
| `interaction?` | `Partial<InteractionConfig>` |  |  |

### `ModeChangeEvent`

Mode change event payload

```ts
interface ModeChangeEvent
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `previousMode` | `DiagramMode` |  |  |
| `currentMode` | `DiagramMode` |  |  |

### `ProximityPreview`

The port pair a proximity-connect DROP would link,
while a node drag is inside the radius. The renderer reads this to draw the
proposed wire itself — highlighting only the two ports left the proposal
nearly invisible (live report: "the wire isn't showing"). Same seam shape as
{@link ReconnectionPreview}: interaction layer writes, renderer reads,
cleared on drop/cancel.

```ts
interface ProximityPreview
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `sourceNodeId` | `string` |  |  |
| `sourcePortId` | `string` |  |  |
| `targetNodeId` | `string` |  |  |
| `targetPortId` | `string` |  |  |

### `ReconnectionPreview`

Transient state for the endpoint-reconnection live
preview. Set by the interaction layer while an endpoint handle is being
dragged; read by the renderer to draw a ghost link from the stationary
endpoint to the cursor. Deliberately separate from {@link ConnectionStateManager}
(which owns NEW-link creation) so the two previews never double-render.

```ts
interface ReconnectionPreview
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `linkId` | `string` |  | Id of the link whose endpoint is being reconnected. |
| `endpoint` | `'source' \| 'target'` |  | Which endpoint the cursor is dragging (the OTHER end stays fixed). |
| `mousePoint` | `Point` |  | Current cursor position in world coordinates. |
| `isValid` | `boolean` |  | Whether the port/node currently under the cursor is a valid drop target. |

### `SnapGuideSegment`

One drawable snap-guide segment, in world coordinates. The interaction layer computes alignment / equal-spacing guides during a
node drag and publishes them here; the renderer draws them as dashed
overlay lines (spacing segments may carry a gap label). Cleared (null) when
the drag ends or nothing is within snapping distance.

```ts
interface SnapGuideSegment
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `x1` | `number` |  |  |
| `y1` | `number` |  |  |
| `x2` | `number` |  |  |
| `y2` | `number` |  |  |
| `kind` | `'alignment' \| 'spacing'` |  |  |
| `label?` | `string` |  |  |

## Enums

### `DiagramMode`

Diagram mode enum - defines the operational mode of the diagram engine

```ts
enum DiagramMode
```

**Members**

- `DESIGNER = 'designer'` — Designer mode - Full editing capabilities (default)
- All node/link operations enabled
- Create, edit, delete, move, resize nodes
- Create and delete links
- `RUNNING = 'running'` — Running mode - Execution/simulation mode
- Editing disabled
- Nodes selectable for execution flow visualization
- No structural changes allowed
- `VIEW = 'view'` — View mode - Read-only viewing
- All editing disabled
- Nodes selectable for inspection
- Pure viewing experience
- `DEBUG = 'debug'` — Debug mode - Debugging mode
- Similar to running but with debug capabilities
- Breakpoints, step-through, inspection
- No structural changes allowed
- `PRESENTATION = 'presentation'` — Presentation mode - Clean presentation view
- All editing disabled
- Nodes selectable for navigation
- Clean UI without clutter
