UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

174 lines (173 loc) 6.42 kB
import { selectConnectionById } from "../../../store/connection/selectors"; import { GraphComponent } from "../GraphComponent"; /** * BaseConnection - Foundation class for all connection types in @gravity-ui/graph * * Provides core functionality for connection components including: * - Integration with the Port System for reliable connection points * - Automatic state synchronization with ConnectionState * - Reactive geometry updates when ports change * - HitBox management for user interaction * * ## Key Features: * * ### Port System Integration * Uses the Port System to resolve connection endpoints, which solves the initialization * order problem where connections can be created before blocks/anchors are ready. * * ### Reactive Updates * Automatically subscribes to port changes and updates connection geometry when * source or target positions change. * * ### Event Handling * Provides hover state management and can be extended for custom interaction handling. * * ## Usage: * * BaseConnection is typically used as a base class for more specific connection types. * For most use cases, prefer BlockConnection which extends this with optimized rendering. * * @example * ```typescript * class SimpleConnection extends BaseConnection { * protected render() { * if (!this.connectionPoints) return; * * const [source, target] = this.connectionPoints; * const ctx = this.context.ctx; * * ctx.beginPath(); * ctx.moveTo(source.x, source.y); * ctx.lineTo(target.x, target.y); * ctx.stroke(); * } * } * ``` * * @see {@link BlockConnection} For production-ready connection implementation * @see {@link ConnectionState} For connection data management * @see {@link PortState} For port system details */ export class BaseConnection extends GraphComponent { /** * @deprecated use port system instead */ get sourceBlock() { return this.connectedState.$sourcePortState.value.component; } /** * @deprecated use port system instead */ get targetBlock() { return this.connectedState.$targetPortState.value.component; } /** * @deprecated use port system instead */ get sourceAnchor() { return this.sourceBlock.connectedState.getAnchorById(this.connectedState.sourceAnchorId)?.asTAnchor(); } /** * @deprecated use port system instead */ get targetAnchor() { return this.targetBlock.connectedState.getAnchorById(this.connectedState.targetAnchorId)?.asTAnchor(); } constructor(props, parent) { super(props, parent); /** * Updates the hit box for user interaction * Adds threshold padding around the connection line to make it easier to click * * @returns {void} */ this.updateHitBox = () => { const [x1, y1, x2, y2] = this.getBBox(); const threshold = this.context.constants.connection.THRESHOLD_LINE_HIT; this.setHitBox(Math.min(x1, x2) - threshold, Math.min(y1, y2) - threshold, Math.max(x1, x2) + threshold, Math.max(y1, y2) + threshold); }; // Get reactive connection state from the store this.connectedState = selectConnectionById(this.context.graph, this.props.id); this.connectedState.setViewComponent(this); // Subscribe to port changes for automatic geometry updates this.connectedState.$sourcePortState.value.addObserver(this); this.connectedState.$targetPortState.value.addObserver(this); // Initialize component state with connection data this.setState({ ...this.connectedState.$state.value, hovered: false }); } getEntityId() { return this.props.id; } willMount() { // Subscribe to connection state changes for automatic updates this.subscribeSignal(this.connectedState.$selected, (selected) => { this.setState({ selected }); }); this.subscribeSignal(this.connectedState.$state, (state) => { this.setState({ ...state }); }); // Subscribe to geometry changes to update connection points this.subscribeSignal(this.connectedState.$geometry, () => { this.updatePoints(); }); // Enable hover interaction this.listenEvents(["mouseenter", "mouseleave"]); } handleEvent(event) { event.stopPropagation(); super.handleEvent(event); switch (event.type) { case "mouseenter": this.setState({ hovered: true }); break; case "mouseleave": this.setState({ hovered: false }); break; } } unmount() { this.connectedState.$sourcePortState.value.removeObserver(this); this.connectedState.$targetPortState.value.removeObserver(this); super.unmount(); } /** * Updates connection points based on current port positions * Called automatically when port geometry changes * * This method: * 1. Retrieves current port positions from the Port System * 2. Updates connectionPoints for rendering * 3. Recalculates bounding box for optimization * 4. Updates hit box for interaction * * @returns {void} */ updatePoints() { // Initialize with default points this.connectionPoints = [ { x: 0, y: 0 }, { x: 0, y: 0 }, ]; // Update with actual port positions if available if (this.connectedState.$geometry.value) { const [source, target] = this.connectedState.$geometry.value; this.connectionPoints = [ { x: source.x, y: source.y }, { x: target.x, y: target.y }, ]; } // Calculate bounding box from valid coordinates const x = [this.connectionPoints[0].x, this.connectionPoints[1].x].filter(Number.isFinite); const y = [this.connectionPoints[0].y, this.connectionPoints[1].y].filter(Number.isFinite); this.bBox = [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)]; // Update interaction area this.updateHitBox(); } /** * Get the current bounding box of the connection * @returns Readonly tuple of [sourceX, sourceY, targetX, targetY] */ getBBox() { return this.bBox; } }