Import it from @grafloria/engine.
tsclass DiagramModel extends DiagramEntity
Properties
| Name | Type | Default | Description |
|---|---|---|---|
name | string | 'Untitled Diagram' | |
nodes | Map<string, NodeModel> | ||
links | Map<string, LinkModel> | ||
groups | Map<string, GroupModel> | ||
strokes | Map<string, StrokeModel> | Freehand ink strokes. See StrokeModel for why these are not nodes. | |
linkIntegrityOwner | 'model' | 'external' | 'model' | Who owns the invariant "a link whose node is gone is not a link"? 'model' (the default): {@link removeNode} CASCADES — it removes the links attached to the node it removes. This is the right answer for an ordinary single-user document, and its absence was a real bug: deleting a node left its edges in getLinks() and on the screen, through every removal path there is. 'external': something wit |
viewport |
Methods
get comments(): CommentRegisterTree— The comment register tree. Read-only in spirit: write through writeCommentRegister. The landing pad forapplyOp's whole-tree write (remote ops, replay, load).
Coarse ON PURPOSE at this level: applyOp hands us the rebuilt tree, and all we owe the world is "the comments changed". The FINE-grained registers — the thing that decides whether two concurrent replies both survive — are cut in the OPS, not here (see writeCommentRegister and comments/types.ts).
set comments(next: CommentRegisterTree)— The comment register tree. Read-only in spirit: write through writeCommentRegister. The landing pad forapplyOp's whole-tree write (remote ops, replay, load).
Coarse ON PURPOSE at this level: applyOp hands us the rebuilt tree, and all we owe the world is "the comments changed". The FINE-grained registers — the thing that decides whether two concurrent replies both survive — are cut in the OPS, not here (see writeCommentRegister and comments/types.ts).
writeCommentRegister(path: string, value: unknown): boolean— Write ONE comment register —t1.status,t1.messages.m3— and nothing else.
This is the LOCAL authoring path, and the granularity here becomes the granularity
of the emitted op (OpCapture turns trackChange('comments.t1.messages.m3') into
set(diagram, path='comments.t1.messages.m3')), which becomes the granularity of
the LWW register, which is what decides whether your colleague's simultaneous reply
survives. The dotted path is not a convenience. It is the concurrency semantics.
Returns false when the write changes nothing — so a redundant resolve is not an op, not a broadcast, and not a frame.
readCommentRegister(path: string): unknown— Read one register. Returns undefined for any missing segment — never throws.constructor( name?: string, options?: { lodConfig?: LODConfig; id?: string; uuid?: string } )refreshLinkBounds(link: LinkModel): void— Re-index a link after its geometry changed WITHOUT achange:pointsevent.
Renderers route links per frame and assign link.points directly (using
setPoints() would emit change → link:changed → another render, i.e. a
render loop). The spatial index therefore never hears about the routed path,
and its grid cells would keep pointing at the geometry the link had when it
was added. Call this after writing points directly.
isReadonly(): boolean— Is this document locked against edits?setReadonly(value: boolean): void— Lock / unlock the document. Normally driven byDiagramEngine.setMode()— VIEW and PRESENTATION lock, DESIGNER unlocks — soDiagramModefinally means something. Can also be set directly for a host that has no mode concept.blocksDocumentWrite(): boolean— True when a document mutation must be refused right now.inSystemWrite(): boolean— True while a SYSTEM write is in flight. Read by the PER-NODE geometry lock (NodeState.locked) so it exempts measured writes exactly as the document lock does — see readonly-lock.ts.runSystemWrite<T>(fn: () => T): T— Run a SYSTEM write — a derived/measured value (auto-size, portal placement) the engine needs in order to render the document as it already is. Permitted even while locked. NOT reachable from user input; see readonly-lock.ts.addNode(node: NodeModel): voidremoveNode(nodeId: string): NodeModel | undefined— Remove node from diagram — AND every link attached to it.
It matters because EVERY removal path funnels through here:
deleteSelected()— what the Delete key calls;applyNodes()— whatsetNodes()calls, so dropping a node from a React-shaped spec left the dangling links behind;RemoveNodeCommand.
A link's endpoints are the invariant that makes it a link; a link to nowhere is not a link. So the cascade belongs at the choke point, not at each of the three call sites that would each have to remember it.
UNLESS someone better owns that invariant — see {@link linkIntegrityOwner}.
getLinksForNode(nodeId: string): LinkModel[]— Every link with an endpoint onnodeId(either end, including a self-loop).
Resolved through the port index rather than the cached sourceNodeId/targetNodeId,
because those are a backfill that can be absent on a link built by hand.
restoreNode(data: any): NodeModel | undefined— Restore node from serialized datagetNode(nodeId: string): NodeModel | undefined— Get node by IDgetNodes(): NodeModel[]— Get all nodesgetNodeByPortId(portId: string): NodeModel | undefined— Get node that owns a specific port Used for connection group validation and other port-based queries. O(1) via the portIndex (was an O(nodes×ports) linear scan).getPortById(portId: string): PortModel | undefined— Get the port model for a port id, O(1) via the portIndex. Companion to getNodeByPortId for callers that need the port itself.getDetachedAnchor(nodeId: string): DetachedParentAnchor | undefined— The last-known anchor of a REMOVED node, or undefined if the id is live, was never here, or the anchor was wholesale-cleared. The tolerant readers in NodeModel (getWorldPosition / getGlobalPosition / getGlobalTransformMatrix / setGlobalPosition) resolve an unresolvable parent through this so orphaned relative children freeze in place instead of jumping to their raw offsets. See {@link detachedAnchors} and the design argument on {@link removeNode}.clearNodes(): void— Clear all nodesaddLink(link: LinkModel): void— Add link to diagramremoveLink(linkId: string): LinkModel | undefined— Remove link from diagramrestoreLink(data: any): LinkModel | undefined— Restore link from serialized datagetLink(linkId: string): LinkModel | undefined— Get link by IDgetLinks(): LinkModel[]— Get all linksgetLinksForPort(portId: string): LinkModel[]— Get all links connected to a specific portclearLinks(): void— Clear all linkscreateSmartLink( sourceNode: NodeModel, targetNode: NodeModel, pathType: 'direct' | 'orthogonal' | 'smooth' | 'bezier' = 'smooth' ): LinkModel | undefined— Create a smart link with automatic port selection
This high-level API simplifies link creation by:
- Automatically selecting optimal ports based on node geometry
- Creating the link with proper port connections
- Registering connections in port models
- Generating the initial path
connectNodes( sourceNode: NodeModel, targetNode: NodeModel, pathType: 'direct' | 'orthogonal' | 'smooth' | 'bezier' = 'smooth' ): boolean— High-level API to connect two nodes
Convenience method that creates a smart link and returns success status. This is the simplest way to connect nodes.
getNodeConnections(node: NodeModel): { incoming: LinkModel[]; outgoing: LinkModel[]; all: LinkModel[]; }— Get all connections for a node
Returns all links where the node is either source or target. Useful for querying node connectivity.
disconnectNodes(sourceNode: NodeModel, targetNode: NodeModel): number— Disconnect two nodes
Removes all links between the specified nodes. Handles cleanup of port connections.
addGroup(group: GroupModel): void— Add groupremoveGroup(groupId: string): GroupModel | undefined— Remove grouprestoreGroup(data: any): GroupModel | undefined— Restore group from serialized datagetGroup(groupId: string): GroupModel | undefined— Get group by IDgetGroups(): GroupModel[]— Get all groupsgetGroupsInRenderOrder(): GroupModel[]— Groups in deterministic back-to-front stacking order — ascendingzIndex, ties broken by Map insertion order (a STABLE sort keeps it). This is the model-level z-order story that replaces "stacking == Map insertion order" as the only determinant; a renderer paints groups in this order (behind their members) instead of relying on iteration order.getProxyNodeForGroup(groupId: string): NodeModel | undefined— The placeholder "group-as-node" for a collapsed group, if present. Placeholder nodes are ordinary NodeModels tagged with the group id so callers can filter them out of exports / counts.isProxyNode(node: NodeModel): boolean— Is this node a collapsed-group placeholder?clearGroups(): void— Clear all groupsaddStroke(stroke: StrokeModel): voidremoveStroke(strokeId: string): StrokeModel | undefinedrestoreStroke(data: SerializedStroke): StrokeModel | undefinedgetStroke(strokeId: string): StrokeModel | undefinedgetStrokes(): StrokeModel[]clearStrokes(): voidgetVisibleStrokes(viewport: Rectangle): StrokeModel[]— Strokes whose bounds overlapviewport. The ink layer's culling query.getStrokesAlongSegment(a: Point, b: Point, tolerance = 0): StrokeModel[]— Every stroke the pointer swept across travellinga→b. THE ERASER'S QUERY.
Segment-based, not point-based: a fast flick puts 80px between two pointermove samples, and an eraser that only tested the samples would jump clean over a stroke it visibly wiped through. Ask what the pointer TRAVELLED THROUGH, not where it happened to land.
getAncestors(groupId: string): GroupModel[]— Get a group's ancestor chain (nearest parent first), walking parentGroupId upward. Robust against malformed self/looping pointers.getDescendants(groupId: string): GroupModel[]— Get every group nested (directly or transitively) insidegroupId. Breadth-first over the parentGroupId back-pointers.getDepth(groupId: string): number— Nesting depth of a group: number of ancestors (0 for a top-level group).getSelectedNodes(): NodeModel[]— Get all selected nodesselectNode(node: NodeModel): void— Select a single node (clears previous selection)addToSelection(node: NodeModel): void— Add node to selection (multi-select)removeFromSelection(node: NodeModel): void— Remove node from selectiontoggleNodeSelection(node: NodeModel): void— Toggle node selection (add if not selected, remove if selected)clearSelection(): void— Clear all selectionsselectAll(): void— Select all nodesdeleteSelected(): number— Delete all selected nodes and their connected linksgetNodeAtPosition(x: number, y: number): NodeModel | undefined— Get node at position (for click detection) Returns the topmost node at the given positionisPointCoveredAbove(x: number, y: number, nodeId: string): boolean— Is (x, y) inside any node ABOVEnodeIdin z-order?
Same z contract as {@link getNodeAtPosition} (array order, topmost last), same shape-aware containment. This is the occlusion oracle for PORTS: a port whose anchor a higher node covers must neither paint nor accept input — pre-fix, an overlapped node's port glyphs floated on top of the covering node's body, and its hidden ports still won the hover/press race through it (live report from stacked pasted nodes).
lockSelected(): number— Option 3: Lock/pin selected nodes Locked nodes will not move during layout operationsunlockSelected(): number— Option 3: Unlock selected nodesgetLockedNodes(): NodeModel[]— Option 3: Get locked nodesunlockAll(): number— Option 3: Unlock all nodessetViewport(x: number, y: number, width: number, height: number, zoom?: number): void— Set viewportgetViewport(): { x: number; y: number; width: number; height: number; zoom: number }— Get current viewportpan(dx: number, dy: number): void— Pan viewportzoom(delta: number, center?: Point): void— Zoom viewport (relative adjustment)setZoom(level: number, center?: Point): void— Set absolute zoom level Pan/Zoom controlsfitToView(padding: number = 100): void— Fit viewport to show all nodes (without changing zoom level) Pan/Zoom controlszoomToFit(targetWidth: number, targetHeight: number, padding: number = 100): void— Fit viewport to show all nodes AND adjust zoom to fit screen Pan/Zoom controlsclear(): void— Clear all nodes, links, and groupsgetVisibleNodes(viewport: Rectangle): NodeModel[]— Get nodes visible in viewport This enables viewport virtualization - only render visible nodesgetVisibleLinks(viewport: Rectangle): LinkModel[]— Get links visible in viewport This enables viewport virtualization - only render visible linksfindNearestPort( point: Point, options?: NearestPortOptions ): NearestPortHit | null— The nearest port to a world point, served BY THE SPATIAL INDEX.
. This is the query a link drag makes on every
pointermove, so it is the one query that must never be a scan: the existing
answer (PortModel.findNearestPort) could only search ONE node — the one the
pointer happened to be over — because searching more would have meant walking
every node in the diagram. The index turns "which port am I near" into a
bounded region query, so a drag can snap to a port it is merely NEAR, not one
it is already on top of, without paying O(nodes) sixty times a second.
portPosition is injectable because THE ENGINE DOES NOT KNOW WHERE PORTS ARE. Its default (getAbsolutePosition) walks the bounding box — edge midpoints,
blind to the silhouette and to how many ports share a side — while the
renderer draws them shape-aware (portWorldPosition).
getVisibleBounds(viewport: Rectangle): Rectangle | null— Get bounding box of all visible entities Useful for "fit to viewport" operationsgetDirtyNodes(): NodeModel[]— Get all dirty nodes Returns nodes that need re-renderinggetDirtyLinks(): LinkModel[]— Get all dirty links Returns links that need re-renderinggetDirtyGroups(): GroupModel[]— Get all dirty groups Returns groups that need re-renderingmarkAllClean(): void— Mark all entities as clean Call this after rendering to reset dirty flagsgetDirtyCount(): number— Get total count of dirty entities Useful for monitoring render performancegetVisibleDirtyNodes(viewport: Rectangle): NodeModel[]— Get visible dirty nodes Combines viewport virtualization with dirty marking Only returns nodes that are both visible AND need re-renderinggetVisibleDirtyLinks(viewport: Rectangle): LinkModel[]— Get visible dirty links Combines viewport virtualization with dirty marking Only returns links that are both visible AND need re-renderinggetLODLevel(zoom: number): LODLevel— Get LOD level based on zoom
Driven by the declarative {@link LODConfig}. Picks the
tier whose minZoom the zoom crosses — tiers are pre-sorted highest-first,
so the first match wins. With the default config this is exactly:
zoom >= 1.0 -> 'high'
0.5 <= zoom < 1.0 -> 'medium'
zoom < 0.5 -> 'low'
shouldRender(feature: LODFeature, lod: LODLevel): boolean— Single feature gate that reads the active LOD tier's feature set. Renderers call this instead of hardcodinglod === 'high'checks, so custom tiers work automatically.getLODConfig(): LODConfig— Get the current Level-of-Detail policy.setLODConfig(config: LODConfig): void— Replace the Level-of-Detail policy wholesale. Apps use this to define their own tiers (names, breakpoints and feature sets).registerLODTier(tier: LODTier): void— Register (or replace, by name) a single LOD tier. Lets apps extend the default policy with an extra tier without rebuilding the whole config.getNodesWithLOD(viewport: Rectangle, zoom: number): EntityWithLOD<NodeModel>[]— Get visible nodes with LOD information Combines viewport virtualization with Level of DetailgetLinksWithLOD(viewport: Rectangle, zoom: number): EntityWithLOD<LinkModel>[]— Get visible links with LOD information Combines viewport virtualization with Level of DetailshouldRenderLabels(lod: LODLevel): boolean— Check if labels should be rendered at this LOD level Now reads the LOD tier's feature set.shouldRenderIcons(lod: LODLevel): boolean— Check if icons should be rendered at this LOD level Now reads the LOD tier's feature set.shouldRenderBorders(lod: LODLevel): boolean— Check if borders should be rendered at this LOD level Now reads the LOD tier's feature set.shouldRenderShadows(lod: LODLevel): boolean— Check if shadows should be rendered at this LOD level Now reads the LOD tier's feature set.getLayoutManager(): LayoutManager— Get the layout manager for this diagramsetLayoutAlgorithm(type: LayoutAlgorithmType, config?: LayoutConfiguration): void— Set layout algorithmgetLayoutAlgorithm(): LayoutAlgorithmType— Get current layout algorithm typeconfigureLayout(config: LayoutConfiguration): void— Configure current layout algorithmgetLayoutConfiguration(): LayoutConfiguration— Get layout configurationsetAutoLayout(enabled: boolean): void— Enable or disable automatic layout for new nodes When enabled, newly added nodes will be automatically positioned using the current layout algorithmisAutoLayoutEnabled(): boolean— Check if auto-layout is enabledasync reLayout(config?: LayoutConfiguration): Promise<void>— Re-layout all nodes using current algorithm This will recalculate positions for all nodes in the diagramoverride endBatch(): void— Override: End batch update mode Fires accumulated events when all batches completeserialize(): SerializedDiagram— Serialize to JSONreconcilePortConnections(): Array<{ linkId: string; portId: string; end: 'source' | 'target' }>— Rebuild the derived port-connection registries from the diagram's links.
PortModel.currentConnections is DERIVED state (which links touch this
port). It is never serialized; instead it is reconstructed
deterministically here so canConnect() / maxConnections enforcement
survive save/load. Runs inside fromJSON, and is safe to re-run at any
time — existing registries are reset first, so the result is always
exactly the current links.
A self-loop (source === target port) registers once — the Set dedupes — so it counts as ONE connection on that port.
applyIncremental(patch: DiagramIncremental): void— Apply an incremental patch (see serialization/Incremental.ts) — the receive side of toIncremental/apply. Added entities are installed through the SAME unified restore path as document load (fully wired); modified entities are updated IN PLACE so object identity is preserved for renderers holding references; normal change events fire (an applied patch IS a mutation, unlike a document load).static fromJSON(data: SerializedDiagram, options?: DiagramLoadOptions): DiagramModel(static) — Deserialize from JSON — THE document load path.
Contract: a loaded diagram behaves identically to an authored one. Every entity is installed through the same install* wiring as
interactive creation (diagram back-refs, spatial indices, port index,
change-forwarding listeners), port registries are reconciled, and the
document is migrated to the current schema first. The load itself is
NOT a user mutation: per-entity events queued during the restore are
dropped, the change log ends empty, and version reports the SAVED
counter — then a single 'diagram
override dispose(): void— Dispose diagram and all child entities Prevents memory leaks by:- Disposing all nodes, links, and groups
- Breaking circular references
- Clearing spatial indices
- Calling parent dispose()
Was this page helpful?