Skip to content
D
Documentation

NodeModel

reference
8 min readUpdated

Import it from @grafloria/engine.

ts
class NodeModel extends DiagramEntity

Properties

NameTypeDefaultDescription
diagram?DiagramModel
positionPoint
sizeSize
rotationnumber0
scalePoint
positionModePositioningMode'absolute'
transformOriginPoint
zIndex?numberC — model-level stacking order (lower renders further back). DELIBERATELY OPTIONAL, unlike the group's zIndex = 0. undefined means "this node never expressed an opinion", which is what lets {@link getEffectiveZIndex} fall through to the legacy style.zIndex the renderer already honoured, and what keeps serialize() byte-identical for every node that doesn't use it.
flexConfig?FlexItemConfig
gridConfig?GridItemConfig
portRenderingConfig?any
dragHandlerConfig?any
connectionGroup?string
typestring
systemType?string
definitionId?string
parentId?string
childrenSet<string>
depthnumber0
portsMap<string, PortModel>
stateNodeState
behaviorNodeBehavior
stylePartial<NodeStyle>{}
classesSet<string>
dataRecord<string, any>{}
computedMap<string, any>

Methods

  • constructor(config: { id?: string; type: string; position: Point; size?: Size; systemType?: string; definitionId?: string; })
  • setPosition(x: number, y: number, z?: number): void — Set position
  • move(dx: number, dy: number, dz?: number): void — Move by delta
  • setSize(width: number, height: number, depth?: number): void — Set size.

A node growing inside a flex/grid container is a layout-invalidating event: its siblings have to move.

It notifies its LAYOUT CONTAINERS, and deliberately NOT the transform chain setPosition uses: a parent's size does not move a relative child (the child's offset is measured from the parent ORIGIN), so emitting transform-propagated here would be noise that says something untrue.

  • resize(dw: number, dh: number, dd?: number): void — Resize by delta
  • setRotation(degrees: number): void — Set rotation
  • rotate(degrees: number): void — Rotate by delta
  • setScale(x: number, y: number): void — Set scale
  • addPort(port: PortModel): void — Add port
  • removePort(portId: string): PortModel | undefined — Remove port
  • getPort(portId: string): PortModel | undefined — Get port by ID
  • getPorts(): PortModel[] — Get all ports
  • getIncomingLinks(): import('./LinkModel').LinkModel[] — Links arriving at this node (resolved through the owning diagram; empty when the node isn't attached to a diagram yet)
  • getOutgoingLinks(): import('./LinkModel').LinkModel[] — Links leaving this node (resolved through the owning diagram)
  • getPortsByType(type: 'input' | 'output' | 'bi'): PortModel[] — Get ports by type
  • getPortBySide(side: 'top' | 'right' | 'bottom' | 'left'): PortModel | undefined — Get port by side Returns the first port found on the specified side
  • getPortsBySide(side: 'top' | 'right' | 'bottom' | 'left'): PortModel[] — Get all ports on a specific side Useful for nodes with multiple ports per side
  • getAvailablePorts(type?: 'input' | 'output' | 'bi'): PortModel[] — Get available ports that can accept connections
  • getConnectedPorts(): PortModel[] — Get ports that have active connections
  • setState(state: Partial<NodeState>): void — Set state property
  • replaceState(state: Partial<NodeState>): void — REPLACE the DOCUMENT half of state, keeping THIS viewer's own view half.

The write setState cannot express, and the collab reducer's write path.

Why a merge was wrong

state is a value register: the op carries the whole (projected) object the author now holds. Applying it with the merging setState meant a peer could GAIN a key and never LOSE one — NodeState.error, warning, status and animateStatus are all optional, so the author clears an error badge and every other peer keeps it FOREVER, with no later edit able to correct it. Node style had exactly this defect and was fixed with replaceStyle; this is the same fix for the register next to it.

Why it is not a plain wholesale replace either

selected / hovered / highlighted / focused are facts about a VIEWER, not about the document. Capture strips them (see collab/capture.ts — syncing them meant your cursor lit up my node and your click deselected it), so an incoming register value never carries them. Replacing wholesale would therefore BLANK the receiving user's own selection on every remote state edit — reintroducing the very bug through the back door. So: durable keys replaced wholesale, view keys taken from what this replica already had.

The read-only posture, deliberately UNCHANGED

This refuses outright while the document is locked, exactly like setPosition, setStyle and replaceStyle.

• That filter exists so a LOCAL user can still select, hover and keyboard-navigate a presentation-mode diagram. It is about input, not about the wire. • It would be a no-op here anyway: capture strips the view keys, so an incoming state value contains none of the keys the filter admits — a locked replica already dropped remote state ops entirely, before this method existed. Behaviour is therefore identical, and setState is left untouched. • Making state the one register that DID reach a locked replica would be incoherent: a read-only replica currently applies no remote document write at all (verified — a locked peer ignores remote position and style too). That gap is real and systemic, and it belongs to the lock, not to this register.

  • setBehavior(behavior: Partial<NodeBehavior>): void — Set behavior property
  • isSelected(): boolean — Check if node is selected
  • setSelected(selected: boolean): void — Set selection state
  • isHighlighted(): boolean — Check if node is highlighted (attention state, independent of selection)
  • setHighlighted(highlighted: boolean): void — Set highlight state (attention emphasis without selecting the node)
  • isSelectable(): boolean — Check if node is selectable (based on behavior) Note: Locked nodes are still selectable so users can unlock them
  • isDraggable(): boolean — Check if node is draggable (based on behavior and state) Note: Locked nodes cannot be dragged
  • setStyle(style: Partial<NodeStyle>): void — Set style property
  • replaceStyle(style: Partial<NodeStyle>): void — REPLACE the whole style object — the write setStyle cannot express.

setStyle merges, so it can add a key and overwrite a key but can never REMOVE one. Restoring a snapshot therefore has to assign wholesale, and the obvious way to do that — node.style = snapshot — is a plain field write that never passes trackChange(). That funnel is what collab captures from, so the direct assignment reaches the renderer (via markDirty) and reaches no other peer at all. That is not hypothetical: it is exactly how an undone LINK style stayed applied on every peer but the one that pressed Ctrl+Z. See collab/style-undo.spec.ts.

Undo paths must use this, not the field.

  • addClass(className: string): void — Add CSS class
  • removeClass(className: string): void — Remove CSS class
  • setClasses(classNames: string[]): void — REPLACE the whole class collection.

classes is a Set, so it needs the same treatment members and ports needed: a register whose in-memory form is a collection and whose wire form is an array must be REBUILT on the receiving side, never assigned. The collab reducer writes it through here.

A non-array is REFUSED rather than read as "no classes" — logs persisted before the funnel fix carry a bare class name (the old addClass) or a bare null (the old removeClass), and assigning either is what corrupted the Set in the first place.

  • setData(key: string, value: any): void — Set data property
  • getData(key: string): any — Get data property
  • setComputed(key: string, value: any): void — Set computed property
  • getComputed(key: string): any — Get computed property
  • getWorldPosition(): Point — Get world position (absolute coordinates accounting for parent chain) For nodes without parents, this is the same as position For child nodes, this walks up the parent chain and accumulates offsets
  • getBoundingBox(): BoundingBox — Get bounding box in world coordinates For child nodes, this accounts for parent position
  • getCenter(): Point — Get center point in world coordinates
  • containsPoint(point: Point): boolean — Check if point is inside node
  • intersectsBounds(bounds: BoundingBox): boolean — Check if intersects with bounding box
  • setParent(parentId: string | undefined): void — Set parent
  • addChild(childId: string): void — Add child
  • removeChild(childId: string): void — Remove child
  • setChildren(childIds: string[]): void — REPLACE the whole child collection. The collab reducer's write path.

Same contract as {@link setClasses}: a Set in memory, an array on the wire, rebuilt rather than assigned, and a non-array refused so a pre-fix log degrades instead of destroying the collection.

This maintains only its own half of the hierarchy, exactly as addChild/removeChild do — the child's parentId is its own register with its own op.

  • setTransformOrigin(x: number, y: number): void — Set transform origin
  • getAbsoluteTransformOrigin(): Point — Get absolute transform origin in pixels
  • getLocalPosition(): Point — Get local position Returns the position property as-is
  • getGlobalPosition(): Point — Get global position In absolute mode: returns position as-is In relative mode: transforms position by parent's hierarchy transform
  • setLocalPosition(x: number, y: number, z?: number): void — Set local position Sets position directly and switches to relative mode
  • setGlobalPosition(x: number, y: number, z?: number): void — Set global position Converts global coordinates to local if parent exists
  • getLocalTransformMatrix(): TransformMatrix — Get local transform matrix Composes translation, rotation, and scale relative to transform origin
  • getGlobalTransformMatrix(): TransformMatrix — Get global transform matrix In absolute mode: returns local matrix In relative mode: composes parent's global matrix with local matrix
  • getGlobalBounds(): BoundingBox — Get global bounding box Calculates bounds by transforming all 4 corners through global matrix
  • getChildren(): NodeModel[] — Get direct children nodes
  • getParent(): NodeModel | undefined — Get parent node Public version of getParentNode
  • getAncestors(): NodeModel[] — Get all ancestor nodes up to root Returns array with direct parent first, then grandparent, etc.
  • getDescendants(): NodeModel[] — Get all descendant nodes recursively
  • getRoot(): NodeModel — Get root node of hierarchy Returns self if this is the root
  • getSiblings(): NodeModel[] — Get sibling nodes (same parent, excluding self)
  • isAncestorOf(nodeId: string): boolean — Check if this node is an ancestor of another node
  • getDepth(): number — Get depth in hierarchy Root nodes have depth 0, their children have depth 1, etc.
  • validateHierarchy(): boolean — Validate hierarchy for circular references
  • updateHierarchyDepth(): void — Update depth for this node and all descendants Recalculates depth values based on current hierarchy
  • getAffectedByTransform(): NodeModel[] — Get all nodes affected by transform changes Returns this node plus all descendants in relative positioning mode
  • getEffectiveZIndex(): number — The stacking index a renderer should paint by.

Precedence is explicit-model-field → legacy style.zIndex → 0. That ordering is the whole compatibility story: diagrams that restacked via style keep working untouched, and the moment a node states a model z-index it wins — so setZIndex(1) on a node styled zIndex: 4 does what it says instead of silently losing to a stylesheet.

  • setZIndex(z: number | undefined): void — Set the stacking index (lower renders further back). Tracked for undo/diff.
  • bringToFront(diagram?: DiagramModel): void — Bring this node in front of every other node in the diagram. Falls back to a relative bump when the node is detached, exactly as GroupModel.bringToFront does.
  • sendToBack(diagram?: DiagramModel): void — Send this node behind every other node in the diagram.
  • setFlexItem(config: FlexItemConfig): void — Set flexbox item configuration
  • clearFlexItem(): void — Clear flexbox item configuration
  • getFlexItem(): FlexItemConfig | undefined — Get flexbox item configuration
  • hasFlexItem(): boolean — Check if node has flex item configuration
  • setGridItem(config: GridItemConfig): void — Set grid item configuration
  • clearGridItem(): void — Clear grid item configuration
  • getGridItem(): GridItemConfig | undefined — Get grid item configuration
  • hasGridItem(): boolean — Check if node has grid item configuration
  • setPortRenderingConfig(config: any): void — Set port rendering configuration
  • getPortRenderingConfig(): any | undefined — Get port rendering configuration
  • getPortRenderingMode(): 'svg' | 'html' | 'auto' — Get port rendering mode Auto-detects based on configuration and metadata
  • setDragHandlerConfig(config: any): void — Set drag handler configuration
  • getDragHandlerConfig(): any | undefined — Get drag handler configuration
  • isDragHandler(): boolean — Check if this node is a drag handler
  • setConnectionGroup(group: string): void — Set connection group
  • getConnectionGroup(): string | undefined — Get connection group
  • serialize(): SerializedNode — Serialize to JSON
  • static fromJSON(data: SerializedNode): NodeModel (static) — Deserialize from JSON

Was this page helpful?