@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
346 lines (321 loc) • 10.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
/**
* ConnectionService - Handles arrow spawning and attachment mechanisms
* Provides draw.io-style connection experience with visual feedback and smart attachment
*/
class ConnectionService {
constructor(options = {}) {
this.connectionStartPoint = null;
this.connectionEndPoint = null;
this.isConnecting = false;
this.sourceNode = null;
this.targetNode = null;
this.connectionPreview = null;
this.connectionPointRadius = options.connectionPointRadius || 6;
this.connectionPointMargin = options.connectionPointMargin || 10;
this.visualFeedbackEnabled = options.visualFeedbackEnabled !== false;
this.smartAttachmentEnabled = options.smartAttachmentEnabled !== false;
this.connectionValidationEnabled = options.connectionValidationEnabled !== false;
}
/**
* Start connection process from a source node
*/
startConnection(sourceNode, sourceHandle, position) {
this.isConnecting = true;
this.sourceNode = sourceNode;
this.connectionStartPoint = this.calculateConnectionPoint(sourceNode, sourceHandle, position);
this.connectionPreview = {
source: this.connectionStartPoint,
target: position,
isValid: false
};
return {
isConnecting: true,
startPoint: this.connectionStartPoint,
preview: this.connectionPreview
};
}
/**
* Update connection preview during dragging
*/
updateConnectionPreview(targetPosition, targetNode = null, targetHandle = null) {
if (!this.isConnecting) return null;
this.connectionEndPoint = targetPosition;
this.targetNode = targetNode;
// Calculate optimal target connection point
const targetConnectionPoint = targetNode ? this.calculateConnectionPoint(targetNode, targetHandle, targetPosition) : targetPosition;
// Validate connection
const isValid = this.validateConnection(this.sourceNode, this.targetNode);
this.connectionPreview = {
source: this.connectionStartPoint,
target: targetConnectionPoint,
isValid,
targetNode: this.targetNode
};
return this.connectionPreview;
}
/**
* Complete connection process
*/
completeConnection(targetNode, targetHandle) {
if (!this.isConnecting || !this.sourceNode) {
return null;
}
const targetConnectionPoint = this.calculateConnectionPoint(targetNode, targetHandle);
const isValid = this.validateConnection(this.sourceNode, targetNode);
const connection = {
source: this.sourceNode.id,
target: targetNode.id,
sourceHandle: this.getSourceHandle(),
targetHandle: targetHandle,
sourcePoint: this.connectionStartPoint,
targetPoint: targetConnectionPoint,
isValid,
connectionType: 'orthogonal'
};
// Reset connection state
this.resetConnection();
return connection;
}
/**
* Cancel connection process
*/
cancelConnection() {
this.resetConnection();
return {
isConnecting: false
};
}
/**
* Reset connection state
*/
resetConnection() {
this.isConnecting = false;
this.connectionStartPoint = null;
this.connectionEndPoint = null;
this.sourceNode = null;
this.targetNode = null;
this.connectionPreview = null;
}
/**
* Calculate optimal connection point on a node
*/
calculateConnectionPoint(node, handle = null, position = null) {
const {
x,
y,
width = 100,
height = 100
} = node.position;
const nodeWidth = node.width || width;
const nodeHeight = node.height || height;
if (handle) {
// Use specific handle position
switch (handle) {
case 'top-source':
case 'top-target':
return {
x: x + nodeWidth / 2,
y: y - this.connectionPointMargin
};
case 'right-source':
case 'right-target':
return {
x: x + nodeWidth + this.connectionPointMargin,
y: y + nodeHeight / 2
};
case 'bottom-source':
case 'bottom-target':
return {
x: x + nodeWidth / 2,
y: y + nodeHeight + this.connectionPointMargin
};
case 'left-source':
case 'left-target':
return {
x: x - this.connectionPointMargin,
y: y + nodeHeight / 2
};
default:
return {
x: x + nodeWidth / 2,
y: y + nodeHeight / 2
};
}
}
if (position) {
// Calculate closest connection point to position
const centerX = x + nodeWidth / 2;
const centerY = y + nodeHeight / 2;
const dx = position.x - centerX;
const dy = position.y - centerY;
// Determine which side is closest
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
const halfWidth = nodeWidth / 2;
const halfHeight = nodeHeight / 2;
if (absDx / halfWidth > absDy / halfHeight) {
// Closer to left or right side
return {
x: dx > 0 ? x + nodeWidth + this.connectionPointMargin : x - this.connectionPointMargin,
y: centerY
};
} else {
// Closer to top or bottom side
return {
x: centerX,
y: dy > 0 ? y + nodeHeight + this.connectionPointMargin : y - this.connectionPointMargin
};
}
}
// Default to center
return {
x: x + nodeWidth / 2,
y: y + nodeHeight / 2
};
}
/**
* Get source handle from current connection
*/
getSourceHandle() {
if (!this.sourceNode || !this.connectionStartPoint) return null;
const {
x,
y,
width = 100,
height = 100
} = this.sourceNode.position;
const nodeWidth = this.sourceNode.width || width;
const nodeHeight = this.sourceNode.height || height;
const centerX = x + nodeWidth / 2;
const centerY = y + nodeHeight / 2;
const startX = this.connectionStartPoint.x;
const startY = this.connectionStartPoint.y;
// Determine which handle was used
if (Math.abs(startX - centerX) < 5) {
// Vertical alignment
return startY < centerY ? 'top-source' : 'bottom-source';
} else {
// Horizontal alignment
return startX < centerX ? 'left-source' : 'right-source';
}
}
/**
* Validate if connection is allowed
*/
validateConnection(sourceNode, targetNode) {
if (!sourceNode || !targetNode) return false;
// Prevent self-connection
if (sourceNode.id === targetNode.id) return false;
// Check if target node is a valid connection target
if (targetNode.type === 'container') {
// Containers can receive connections
return true;
}
// Check for existing connections between these nodes
// This would need to be implemented with access to current edges
return true;
}
/**
* Find the best connection point on target node
*/
findBestConnectionPoint(sourceNode, targetNode, mousePosition) {
const sourcePoint = this.calculateConnectionPoint(sourceNode);
const targetPoint = this.calculateConnectionPoint(targetNode);
// Calculate direction from source to target
const dx = targetPoint.x - sourcePoint.x;
const dy = targetPoint.y - sourcePoint.y;
// Determine optimal connection side based on direction
const absDx = Math.abs(dx);
const absDy = Math.abs(dy);
if (absDx > absDy) {
// Horizontal connection preferred
return dx > 0 ? 'left-target' : 'right-target';
} else {
// Vertical connection preferred
return dy > 0 ? 'top-target' : 'bottom-target';
}
}
/**
* Get visual feedback for connection preview
*/
getConnectionPreviewStyle(isValid) {
return {
stroke: isValid ? '#10b981' : '#ef4444',
strokeWidth: 2,
strokeDasharray: '5,5',
opacity: 0.8,
pointerEvents: 'none'
};
}
/**
* Check if a point is near a connection handle
*/
isNearConnectionHandle(point, node, handle) {
const handlePoint = this.calculateConnectionPoint(node, handle);
const distance = Math.sqrt(Math.pow(point.x - handlePoint.x, 2) + Math.pow(point.y - handlePoint.y, 2));
return distance <= this.connectionPointRadius * 2;
}
/**
* Get connection handles for a node
*/
getConnectionHandles(node) {
const {
x,
y,
width = 100,
height = 100
} = node.position;
const nodeWidth = node.width || width;
const nodeHeight = node.height || height;
return {
'top-source': {
x: x + nodeWidth / 2,
y: y - this.connectionPointMargin
},
'right-source': {
x: x + nodeWidth + this.connectionPointMargin,
y: y + nodeHeight / 2
},
'bottom-source': {
x: x + nodeWidth / 2,
y: y + nodeHeight + this.connectionPointMargin
},
'left-source': {
x: x - this.connectionPointMargin,
y: y + nodeHeight / 2
},
'top-target': {
x: x + nodeWidth / 2,
y: y - this.connectionPointMargin
},
'right-target': {
x: x + nodeWidth + this.connectionPointMargin,
y: y + nodeHeight / 2
},
'bottom-target': {
x: x + nodeWidth / 2,
y: y + nodeHeight + this.connectionPointMargin
},
'left-target': {
x: x - this.connectionPointMargin,
y: y + nodeHeight / 2
}
};
}
/**
* Update configuration
*/
updateConfiguration(config) {
this.connectionPointRadius = config.connectionPointRadius || this.connectionPointRadius;
this.connectionPointMargin = config.connectionPointMargin || this.connectionPointMargin;
this.visualFeedbackEnabled = config.visualFeedbackEnabled !== false;
this.smartAttachmentEnabled = config.smartAttachmentEnabled !== false;
this.connectionValidationEnabled = config.connectionValidationEnabled !== false;
}
}
var _default = exports.default = ConnectionService;