@gravity-ui/graph
Version:
Modern graph editor component
174 lines (173 loc) • 6.42 kB
JavaScript
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;
}
}