UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

194 lines (193 loc) 6.05 kB
import { computed, signal } from "@preact/signals-core"; export const IS_PORT_TYPE = "Port"; /** * PortState - Reactive state container for a connection port * * Manages the lifecycle and state of a port, including position updates, * component ownership, and listener management for connections that use this port. * * ## Key Concepts: * * ### Lazy Creation * Ports are created on-demand when connections need them, even if the target * component doesn't exist yet. This solves initialization order problems. * * ### Lookup State * When `lookup: true`, the port is waiting for its component to provide coordinates. * When `lookup: false`, the port has valid coordinates and can be used for rendering. * * ### Listener Management * Tracks which components are listening to this port's changes. When no listeners * remain and no component owns the port, it can be safely garbage collected. */ export class PortState { /** * Get the port's unique identifier * * @returns {TPortId} The port's ID */ get id() { return this.$state.value.id; } /** * Get the port's effective X coordinate (respects delegation) * * @returns {number} The X coordinate */ get x() { return this.$point.value.x; } /** * Get the port's effective Y coordinate (respects delegation) * * @returns {number} The Y coordinate */ get y() { return this.$point.value.y; } /** * Get the component that owns this port * * @returns {Component | undefined} The owning component, if any */ get component() { return this.owner || this.$state.value.component; } /** * Get whether the port is in lookup state (waiting for coordinates) * * @returns {boolean | undefined} True if waiting for coordinates, false if resolved */ get lookup() { return this.$state.value.lookup; } /** * Get the port's metadata * * @returns {T | undefined} The metadata attached to this port */ get meta() { return this.$state.value.meta; } constructor(port) { this.$state = signal(undefined); this.$delegate = signal(undefined); /** * Set of references observing this port's changes * * Used for reference counting to determine when the port can be safely deleted. * Stores actual object references to ensure accurate counting and prevent duplicates. */ this.observers = new Set(); this.$point = computed(() => { const delegate = this.$delegate.value; if (delegate) { return delegate.$point.value; } return { x: this.$state.value.x, y: this.$state.value.y }; }); this.$state.value = { ...port }; // Initialize owner if component was provided in the constructor if (port.component) { this.owner = port.component; } } /** * Set the component that owns this port * @param owner Component that will own this port (block, anchor, etc.) * @returns void */ setOwner(owner) { this.owner = owner; this.updatePort({ component: owner, lookup: false }); } /** * Remove the current owner from this port */ removeOwner() { this.owner = undefined; this.updatePort({ component: undefined, lookup: true }); } /** * Add an observer reference to this port * Stores the actual reference for accurate counting * @param observer The object observing this port */ addObserver(observer) { this.observers.add(observer); } /** * Remove an observer reference from this port * Removes the actual reference from the set * @param observer The object to stop observing this port */ removeObserver(observer) { this.observers.delete(observer); } /** * Update the port's position coordinates. * When delegated, the position is saved but does not affect getPoint() — * the effective position comes from the delegate port. * @param x New X coordinate * @param y New Y coordinate */ setPoint(x, y) { if (this.$delegate.value) { this.savedPoint = { x, y }; return; } this.updatePort({ x, y }); } getPoint() { return this.$point.value; } /** * Delegate this port to mirror another port's position. * While delegated, getPoint() returns the target port's position. * Any setPoint() calls are saved and restored on undelegate(). * @param target The port to mirror */ delegate(target) { this.savedPoint = { x: this.$state.value.x, y: this.$state.value.y }; this.$delegate.value = target; } /** * Remove delegation and restore the last saved position. * If setPoint() was called during delegation, the last value is used. * Otherwise, the position from before delegation is restored. */ undelegate() { this.$delegate.value = undefined; if (this.savedPoint) { this.updatePort({ x: this.savedPoint.x, y: this.savedPoint.y }); this.savedPoint = undefined; } } /** * Whether this port is currently delegated to another port */ get isDelegated() { return this.$delegate.value !== undefined; } /** * Update port state with partial data * @param port Partial port data to merge with current state */ updatePort(port) { this.$state.value = { ...this.$state.value, ...port, meta: { ...this.$state.value.meta, ...port.meta, }, }; } /** * Check if this port can be safely deleted * @returns true if port has no owner and no observers */ canBeDeleted() { return !this.owner && this.observers.size === 0; } }