Skip to content
D
Documentation

LinkModel

reference
6 min readUpdated

Import it from @grafloria/engine.

ts
class LinkModel extends DiagramEntity

Properties

NameTypeDefaultDescription
diagram?DiagramModel. Back-reference to the owning diagram, set by DiagramModel.installLink. Without this, read-only would have been unenforceable on exactly the edits the interaction controller performs most. NON-ENUMERABLE (defined in the constructor, mirroring NodeModel): an enumerable back-reference to the diagram would make every LinkModel a circular object and blow up deep-clone and JSON serialization.
sourcePortIdstring
targetPortIdstring
sourceNodeId?string
targetNodeId?string
pathType'direct' | 'orthogonal' | 'smooth' | 'bezier''smooth'
router?LinkRouterNameWHERE the line goes. When unset, derived from pathType — see {@link effectiveRouter}. Setting it does NOT touch pathType, so legacy consumers keep working; the explicit field simply wins.
connector?LinkConnectorNameHOW the polyline is drawn. Unset = derived from pathType.
pointsPoint[][]
segmentsPathSegment[][]
labelsLinkLabel[][]
state'default' | 'selected' | 'hovered' | 'highlighted''default'
stylePartial<LinkStyle>{}
dataRecord<string, any>{}
isSourceEndpointSelectedbooleanfalseWhether source endpoint handle is selected Used for dragging endpoint to reconnect
isTargetEndpointSelectedbooleanfalseWhether target endpoint handle is selected Used for dragging endpoint to reconnect
DEFAULT_CURVATURE (static)0.5The curve tightness of a smooth/bezier link. style.curvature is a multiplier of the endpoint distance for the control-point offset. It used to be DEAD (declared on LinkStyle, read by nobody); it is now the single knob both this model and the SVG renderer read, so a per-link value produces the same curve whichever produced the path. Default 0.5 = the historical hardcoded factor; negatives are cla

Methods

  • constructor( sourcePortId: string, targetPortId: string, pathType?: 'direct' | 'orthogonal' | 'smooth' | 'bezier' )
  • isSelfLoop(): boolean — Is this link a SELF-LOOP, i.e. do both ends live on the same node?

Reads the cached owning-node ids, which DiagramModel.installLink backfills for every link that reaches a diagram (including ones built with new LinkModel() + addLink, which carry no ids of their own). A link that has never been installed has no ids and is — correctly — not a self-loop as far as anything can tell.

A self-loop between two DIFFERENT ports of the same node counts, and so does one that starts and ends on the SAME port.

  • getNodePairKey(): string | null — The unordered node pair this link connects, as a stable key. UNORDERED on purpose: A→B and B→A are the same visual bundle and must fan out together, or a bidirectional pair would draw both links on the same centre line. Returns null when the owning nodes are unknown.
  • setSourcePort(portId: string, nodeId?: string): void — Set source port
  • setTargetPort(portId: string, nodeId?: string): void — Set target port
  • setPathType(pathType: 'direct' | 'orthogonal' | 'smooth' | 'bezier'): void — Set path type
  • setRouter(router: LinkRouterName | undefined): void — Set the routing geometry explicitly. Clears the cached route the same way setPathType does — the old polyline belongs to the old router.
  • setConnector(connector: LinkConnectorName | undefined): void — Set the polyline rendering explicitly. Pure re-render; the routed points are still valid, so the cache is NOT cleared.
  • effectiveRouter(): LinkRouterName — The router actually in force: the explicit field, else derived from pathType exactly as the renderer always derived it (direct → straight, orthogonal → orthogonal, smooth/bezier → straight-with-curved-rendering).
  • effectiveConnector(): LinkConnectorName — The connector actually in force: the explicit field, else implied by an explicitly axis-aligned ROUTER, else derived from pathType.

The router rung exists because { router: 'orthogonal' } with the default pathType used to draw a smooth SPLINE through Manhattan waypoints — the route was orthogonal, the picture was wavy (the screenshot audit caught the demo's own readout saying "orthogonal" over a curve). Asking for an axis-aligned router IS asking for axis-aligned rendering; rounded corners are that family's standard look. An explicit connector still overrides.

  • setPoints(points: Point[]): void — Set points for custom path
  • addPoint(point: Point, index?: number): void — Add point to path
  • removePoint(index: number): Point | undefined — Remove point from path
  • generatePath( sourcePoint: Point, targetPoint: Point, sourceDirection?: 'left' | 'right' | 'top' | 'bottom', targetDirection?: 'left' | 'right' | 'top' | 'bottom' ): void — Generate path based on type
  • getCurvature(): number
  • addLabel( label: Partial<LinkLabel> & { text: string } & ( | { position: number } | { slot: NonNullable<LinkLabel['slot']> } ) ): void — Add label

position is no longer required when the label names a slot — and every other LinkLabel field (html, template, slot, autoOffset, rotation…) is now carried through instead of being silently dropped. The old body hand-copied five fields, so a label created here could not be an HTML label, could not auto-rotate and could not opt into auto-placement.

  • removeLabel(labelId: string): LinkLabel | undefined — Remove label by ID
  • removeLabelAt(index: number): LinkLabel | undefined — Remove label by index
  • updateLabel(index: number, updates: Partial<LinkLabel>): void — Update label by index
  • setLabels(labels: LinkLabel[]): void — REPLACE the whole label collection — the write addLabel/updateLabel cannot express.

SetLinkLabelsCommand used to do this by assigning link.labels directly on BOTH execute and undo. A plain field write does not pass trackChange() — the one funnel collab captures from — so the command emitted ZERO ops in BOTH directions: authoring a link's labels was invisible to every other peer, and so was taking it back. (The UpdateLinkStyleCommand defect at least emitted one op on execute; this emitted none at all.) replaceStyle is the same seam for the same reason.

  • override getLabel(): string | undefined — Overloaded label read.

  • getLabel() — no argument — is the CANONICAL display-label accessor inherited from DiagramEntity (metadata.label, with the legacy data['label'] fallback). It is what the DSL generator exports and what feeds the renderer's simple edge label (svg-renderer reads link.getMetadata('label')).

  • getLabel(labelId) is the pre-existing lookup into the POSITIONED multi-label API (labels: LinkLabel[] / addLabel / updateLabel), a different feature that happens to share the verb. The overload keeps both callable without forking the canonical name.

  • override getLabel(labelId: string): LinkLabel | undefined — Overloaded label read.

  • getLabel() — no argument — is the CANONICAL display-label accessor inherited from DiagramEntity (metadata.label, with the legacy data['label'] fallback). It is what the DSL generator exports and what feeds the renderer's simple edge label (svg-renderer reads link.getMetadata('label')).

  • getLabel(labelId) is the pre-existing lookup into the POSITIONED multi-label API (labels: LinkLabel[] / addLabel / updateLabel), a different feature that happens to share the verb. The overload keeps both callable without forking the canonical name.

  • override getLabel(labelId?: string): string | LinkLabel | undefined — Overloaded label read.

  • getLabel() — no argument — is the CANONICAL display-label accessor inherited from DiagramEntity (metadata.label, with the legacy data['label'] fallback). It is what the DSL generator exports and what feeds the renderer's simple edge label (svg-renderer reads link.getMetadata('label')).

  • getLabel(labelId) is the pre-existing lookup into the POSITIONED multi-label API (labels: LinkLabel[] / addLabel / updateLabel), a different feature that happens to share the verb. The overload keeps both callable without forking the canonical name.

  • setState(state: 'default' | 'selected' | 'hovered' | 'highlighted'): void — Set state

  • updateStyle(style: Partial<LinkStyle>): void — Update style

  • replaceStyle(style: Partial<LinkStyle>): void — REPLACE the whole style object — the write updateStyle cannot express.

updateStyle merges, so it can never REMOVE a key; restoring a snapshot has to assign wholesale. UpdateLinkStyleCommand.undo() used to do that with a direct field write (link.style = restored), which never passes trackChange() — the single funnel collab captures from. Measured: execute emitted 1 op, undo emitted 0, so every peer kept the styled link forever while the author saw it correctly reverted. See collab/style-undo.spec.ts.

  • setData(key: string, value: any): void — Set data property
  • getData(key: string): any — Get data property
  • reconnectSource(newPortId: string, newNodeId?: string): void — Reconnect source endpoint to new port Used for link reconnection workflow
  • reconnectTarget(newPortId: string, newNodeId?: string): void — Reconnect target endpoint to new port Used for link reconnection workflow
  • getSourceEndpoint(): Point — Get source endpoint position Returns the first point in the path (source end)
  • getTargetEndpoint(): Point — Get target endpoint position Returns the last point in the path (target end)
  • selectSourceEndpoint(): void — Select source endpoint handle
  • selectTargetEndpoint(): void — Select target endpoint handle
  • deselectEndpoints(): void — Deselect all endpoint handles
  • hasSelectedEndpoint(): boolean — Check if any endpoint is selected
  • getPointAtPosition(t: number): Point | null — Get point at position along link (0-1)
  • getTotalLength(): number — Get total path length
  • getLength(): number — Get path length (alias for getTotalLength for API consistency)
  • getTangentAt(t: number): Point | null — Get tangent (direction vector) at position along path (0-1) Returns normalized direction vector
  • getNormalAt(t: number): Point | null — Get normal (perpendicular vector) at position along path (0-1) Returns normalized perpendicular vector (90° counter-clockwise from tangent)
  • getClosestPoint(point: Point): { point: Point; distance: number; t: number } | null — Get closest point on path to a given point Returns the closest point, distance, and normalized position (t)
  • getAngleAt(t: number): number | null — Get angle at position along path (0-1) in degrees Useful for label rotation
  • override serialize(): SerializedLink — Serialize to JSON
  • static fromJSON(data: SerializedLink): LinkModel (static) — Deserialize from JSON

Was this page helpful?