# PortModel

Import it from `@grafloria/engine`.

Also has every member of `DiagramEntity`, listed on its own entry.

```ts
class PortModel extends DiagramEntity
```

**Properties**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `nodeId` | `string` | `''` |  |
| `type` | `'input' \| 'output' \| 'bi'` |  |  |
| `systemType?` | `string` |  |  |
| `position` | `PortPosition` |  |  |
| `alignment` | `PortAlignment` |  |  |
| `offset` | `Point` |  |  |
| `index` | `number` | `0` |  |
| `maxConnections` | `number` |  |  |
| `currentConnections` | `Set<string>` |  |  |
| `allowedTypes` | `Set<string>` |  |  |
| `linkRoles` | `Map<string, 'source' \| 'target'>` |  | Which END of each attached link this port is — `linkId → 'source'\|'target'`. |
| `visible` | `boolean` | `true` |  |
| `style` | `Record<string, any>` | `{}` |  |
| `data` | `Record<string, any>` | `{}` |  |
| `group?` | `string` |  | Named port group. Its config is inherited; these fields override it. |
| `explicitSide` | `boolean` | `false` | Was the side actually declared by the author? A port that only says `group: 'in'` must inherit the GROUP's side — without this flag the constructor's `alignment.side = 'right'` default would silently win. |
| `shape?` | `PortShapeSpec` |  | Non-circle glyph: square / diamond / triangle / custom SVG path. |
| `label?` | `PortLabelSpec` |  | Port label + its layout mode. |
| `layout?` | `PortLayoutSpec` |  | Layout strategy override. Unset → the shape registry's anchor. |
| `fromSpot?` | `PortSpot` |  | Where a link leaves / lands on the glyph. |
| `toSpot?` | `PortSpot` |  |  |
| `spread?` | `PortSpreadSpec` |  | Spread multiple links along this port's edge instead of piling them. |
| `dataType?` | `string` |  | Declarative data type: drives link validity AND glyph colour. |
| `isConnectableStart?` | `boolean` |  | May a link START at this port? Unset → true. |
| `isConnectableEnd?` | `boolean` |  | May a link END at this port? Unset → true. |
| `fromMaxLinks?` | `number \| null` |  | Cap on OUTGOING links. null/unset → unlimited. |
| `toMaxLinks?` | `number \| null` |  | Cap on INCOMING links. null/unset → unlimited. |
| `allowSelfLink?` | `boolean` |  | Allow a link whose source node IS its target node. Unset → false. |
| `allowDuplicateLinks?` | `boolean` |  | Allow a second link between the same ordered port pair. Unset → true. |
| `dynamic?` | `boolean` |  | Spawned by the dynamic auto-port allocator rather than authored. |
| `renderingConfig?` | `any` |  |  |
| `isHovered` | `boolean` | `false` | Whether mouse is currently hovering over this port Used for visual feedback (scaling, highlighting) |
| `isHighlighted` | `boolean` | `false` | Whether this port is highlighted as a valid connection target Set during connection drag operations |
| `isValidTarget` | `boolean` | `false` | Whether this port is a valid target for current connection Set by ConnectionStateManager during drag |

**Methods**

- `constructor(config: { id?: string; type: 'input' | 'output' | 'bi'; systemType?: string; position?: PortPosition; alignment?: PortAlignment; side?: 'left' | 'right' | 'top' | 'bottom'; index?: number; maxConnections?: number; group?: string; shape?: PortShapeSpec; label?: PortLabelSpec; layout?: PortLayoutSpec; fromSpot?: PortSpot; toSpot?: PortSpot; spread?: PortSpreadSpec; style?: Record<string, any>; visible?: boolean; dataType?: string; isConnectableStart?: boolean; isConnectableEnd?: boolean; fromMaxLinks?: number | null; toMaxLinks?: number | null; allowSelfLink?: boolean; allowDuplicateLinks?: boolean; allowedTypes?: string[]; dynamic?: boolean; })`
- `get side(): 'left' | 'right' | 'top' | 'bottom'` — Get the side this port is on (convenience getter)
Set the side this port is on (convenience setter)
- `set side(value: 'left' | 'right' | 'top' | 'bottom')` — Get the side this port is on (convenience getter)
Set the side this port is on (convenience setter)
- `setPosition(position: PortPosition): void` — Set position
- `setAlignment(alignment: PortAlignment): void` — Set alignment
- `setOffset(offset: Point): void` — Set offset
- `addAllowedType(type: string): void` — Add allowed type
- `removeAllowedType(type: string): void` — Remove allowed type
- `isTypeAllowed(type: string): boolean` — Check if type is allowed
- `addConnection(linkId: string, role?: 'source' | 'target'): void` — Add connection
- `restoreConnection(linkId: string, role?: 'source' | 'target'): void` — Load-time reconcile: register a connection WITHOUT the maxConnections
guard or change tracking. The connection registry is derived state that
is rebuilt deterministically from the diagram's links on load — a
persisted graph may legitimately exceed a since-tightened maxConnections,
so enforcement applies to NEW interactive connections (addConnection),
never to reloading saved state.
- `removeConnection(linkId: string): void` — Remove connection
- `canConnect(): boolean` — Check if can accept more connections
- `getConnectionCount(): number` — Get connection count
- `getFromLinkCount(): number` — How many links LEAVE this port / how many ARRIVE at it.

A self-loop that both starts and ends here registers once in
`currentConnections` (the Set dedupes) but `linkRoles` can only hold one
role for it, so it is counted on whichever end was registered last. That is
acceptable — a port that allows self-links and also caps its directional
fan-out is pathological, and the TOTAL `maxConnections` cap still holds.
- `getToLinkCount(): number`
- `canConnectTo(targetPort: PortModel): boolean` — Check if this port (as the SOURCE) can connect to `targetPort` (as the
TARGET). Direction matters — this is not a symmetric predicate.

Rules:
- input can connect to output or bi
- output can connect to input or bi
- bi can connect to any

`isConnectableStart` on
the source, `isConnectableEnd` on the target, the per-direction
`fromMaxLinks`/`toMaxLinks` caps, the `allowedTypes` whitelist (which was
dead config — `isTypeAllowed` had no caller anywhere in the tree) and
`dataType` compatibility.

NOTE: this is the PORT-LOCAL rule. Rules that need the graph (self-links,
duplicate links, connection groups) live in `evaluatePortConnection()`,
which folds this in.
- `typeIdentity(): string` — The name this port answers to when another port's `allowedTypes` whitelist
is checked: its declared data type, else its systemType, else its direction.
- `getAbsolutePosition(nodeBounds: BoundingBox): Point` — Get absolute position relative to node
- `getEdgePosition(nodeBounds: BoundingBox): Point` — Get port position at node edge (for smart mode)
Returns the position at the edge midpoint based on alignment
- `static findNearestPort( point: Point, ports: Map<string, PortModel>, nodeBounds: BoundingBox ): PortModel | null` (static) — Find nearest port on a node to a given point
Used in smart mode for auto-connect to nearest port
- `getDistanceFromPoint(point: Point, nodeBounds: BoundingBox): number` — Calculate distance from point to this port
- `resetInteractionState(): void` — Reset interaction state
Called when connection drag ends or is cancelled
- `setRenderingConfig(config: any): void` — Set port rendering configuration from template
- `getRenderingConfig(): any | undefined` — Get port rendering configuration
- `getEffectiveVisibility( node: any, globalDefault?: 'always' | 'on-hover' | 'never' | 'hidden' ): 'always' | 'on-hover' | 'never' | 'hidden'` — Get effective visibility considering port and node configuration
Priority: port config > node metadata > default ('on-hover')
- `serialize(): SerializedPort` — Serialize to JSON
- `static fromJSON(data: SerializedPort): PortModel` (static) — Deserialize from JSON
