Import these from @grafloria/engine.
Functions
isValidDiagramMode
Type guard to check if a string is a valid DiagramMode
tsfunction isValidDiagramMode(mode: string): mode is DiagramMode
Classes
DiagramEngine
tsclass 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 diagramgetConfig(): DiagramEngineConfig— Get configurationgetInteractionConfig(): 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 eventgetConnectionStateManager(): ConnectionStateManager— Get connection state manager Used for managing connection drag operationsgetReconnectionPreview(): 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 algorithmsenableLiveRerouting(): void— Enable live rerouting Automatically updates link paths when nodes move or resizedisableLiveRerouting(): void— Disable live reroutinggetLiveReroutingEngine(): LiveReroutingEngine | null— Get live rerouting enginesetDiagram(diagram: DiagramModel | null): void— Set diagramcreateDiagram(name: string = 'Untitled'): DiagramModel— Create new diagramclearDiagram(): void— Clear diagramasync addNode(config: { type: string; position: Point; size?: Size; data?: any; }): Promise<NodeModel>— Add node (from config) Add node (pre-created NodeModel) Add node implementationasync addNode(node: NodeModel): Promise<NodeModel>— Add node (from config) Add node (pre-created NodeModel) Add node implementationasync addNode(configOrNode: { type: string; position: Point; size?: Size; data?: any } | NodeModel): Promise<NodeModel>— Add node (from config) Add node (pre-created NodeModel) Add node implementationasync 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 linkasync removeLink(linkId: string): Promise<void>— Remove link
Async + awaited, mirroring removeGroup() — see removeNode().
async addGroup(config: { name: string }): Promise<GroupModel>— Add groupasync removeGroup(groupId: string): Promise<void>— Remove groupasync addToGroup(groupId: string, entityId: string): Promise<void>— Add entity to groupasync removeFromGroup(groupId: string, entityId: string): Promise<void>— Remove entity from groupasync expandGroup(groupId: string): Promise<void>— Expand groupasync collapseGroup(groupId: string, options?: CollapseOptions): Promise<void>— Collapse groupgetGroup(groupId: string): GroupModel | undefined— Get group by IDgetGroups(): GroupModel[]— Get all groupsasync setLayout( groupId: string, layoutType: 'flexbox' | 'grid', layoutConfig: LayoutConfig ): Promise<void>— Set layout configuration on a groupasync clearLayout(groupId: string): Promise<void>— Clear layout configuration from a groupgetLayout(groupId: string): { type: LayoutType; config?: LayoutConfig } | undefined— Get layout configuration from a groupasync setFlexItem(nodeId: string, flexConfig: FlexItemConfig): Promise<void>— Set flex item configuration on a nodeasync setGridItem(nodeId: string, gridConfig: GridItemConfig): Promise<void>— Set grid item configuration on a nodeasync copy(options?: { includeGroups?: boolean; includeLinks?: boolean }): Promise<void>— Copy selected entities to clipboardasync paste(options?: { offset?: Point; selectPasted?: boolean }): Promise<void>— Paste entities from clipboardasync duplicate(options?: { offset?: Point; selectDuplicated?: boolean }): Promise<void>— Duplicate selected entitiesasync deleteSelection(options?: { deleteChildren?: boolean; deleteLinks?: boolean }): Promise<void>— Delete selected entitiesgetClipboardData()— Get clipboard datahasClipboardData(): boolean— Check if clipboard has dataclearClipboard(): void— Clear clipboardgetClipboardStats()— Get clipboard statisticsvalidateDiagram(options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult— Validate the entire diagramvalidateNode(nodeId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult— Validate a specific nodevalidateLink(linkId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult— Validate a specific linkvalidatePort(portId: string, nodeId: string, options?: { validateTypes?: boolean; validateConnections?: boolean; validatePorts?: boolean; strict?: boolean }): ValidationResult— Validate a specific portvalidateLayout(groupId: string, options?: { strict?: boolean }): ValidationResult— Validate layout configuration for a groupregisterNodeType(definition: NodeTypeDefinition): void— Register a node type definitionregisterPortType(definition: PortTypeDefinition): void— Register a port type definitionregisterLinkType(definition: LinkTypeDefinition): void— Register a link type definitionregisterGroupType(definition: GroupTypeDefinition): void— Register a group type definitionenableRealTimeValidation(): void— Enable real-time validationdisableRealTimeValidation(): void— Disable real-time validationisRealTimeValidationEnabled(): boolean— Check if real-time validation is enabledselectNodes(nodeIds: string[]): void— Select nodesselectLinks(linkIds: string[]): void— Select linksclearSelection(): void— Clear selectionasync undo(): Promise<void>— Undoasync redo(): Promise<void>— RedocanUndo(): boolean— Can undocanRedo(): boolean— Can redovalidate(): ValidationResult— Validate diagramserialize(): 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 JSONasync 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 layoutsgetLayoutRegistry(): 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 diagramasync 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 layoutasync 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 layoutdispose(): void— Cleanup and dispose of all resources Should be called when the engine is no longer needed
Interfaces
DiagramEngineConfig
tsinterface 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
tsinterface 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.
tsinterface 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.
tsinterface 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.
tsinterface 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
tsenum 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
Was this page helpful?