import { html, css, svg } from 'lit'; export const connectionStyles = css` .connections { pointer-events: none; } .connections path { transition: stroke 0.3s ease, stroke-width 0.3s ease; pointer-events: all; cursor: pointer; } .connections path:hover { stroke: #999; stroke-width: 3; } .connections path.selected { stroke: #3498db; stroke-width: 3; } .delete-button-group { pointer-events: all; cursor: pointer; } .delete-button-group:hover .delete-button-circle { fill: #c0392b; } .delete-button-circle { fill: #e74c3c; transition: fill 0.2s ease; } .delete-button-x { stroke: white; stroke-width: 2; } .delete-button-group { pointer-events: all; cursor: pointer; /* Ensure buttons are always on top of nodes */ z-index: 1000; } `; export const connections = function(superClass) { return class extends superClass { constructor() { super(); this.connections = []; this.draggingConnection = null; this.mousePosition = { x: 0, y: 0 }; } firstUpdated() { this.addEventListener('port-click', this._handlePortClick); document.addEventListener('mousemove', this._updatePreviewConnection.bind(this)); document.addEventListener('click', (e) => { // Check if clicking on an output port const path = e.composedPath(); const isOutputPort = path.some(el => el instanceof HTMLElement && el.classList && el.classList.contains('output-ports') ); // Only cancel connection if we're dragging AND it's not an output port if (this.draggingConnection && !isOutputPort) { // Check if we clicked on an input port const isInputPort = path.some(el => el instanceof HTMLElement && el.classList && el.classList.contains('input-ports') ); // If not an input port, cancel the connection if (!isInputPort) { this.draggingConnection = null; this.requestUpdate(); } } // Handle existing connection deselection if (e.target === this.canvas) { this.selectedConnection = null; this.requestUpdate(); } }); super.firstUpdated(); } _updatePreviewConnection(e) { if (this.draggingConnection) { this.mousePosition = { x: e.clientX, y: e.clientY }; this.requestUpdate(); } } _renderConnections() { const bounds = this._calculateSVGBounds(); // Handle initial render when canvas is not ready const style = this.canvas ? ` position: absolute; top: ${bounds.minY}px; left: ${bounds.minX}px; width: ${bounds.width}px; height: ${bounds.height}px; pointer-events: none; ` : ` position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; `; return html` ${this.connections.map(conn => this._renderConnection(conn))} ${this.draggingConnection ? this._renderPreviewConnection() : null} `; } _renderPreviewConnection() { const sourceNode = this.nodes.find(node => node.id === this.draggingConnection.sourceNodeId); if (!sourceNode) return ''; const sourcePort = sourceNode.shadowRoot.querySelector( `.output-ports [data-port-id="${this.draggingConnection.sourcePortId}"]` ); if (!sourcePort) return ''; const sourceRect = sourcePort.getBoundingClientRect(); const canvasRect = this.canvas.getBoundingClientRect(); const start = { x: (sourceRect.left + sourceRect.width/2 - canvasRect.left) / this.scale, y: (sourceRect.top + sourceRect.height/2 - canvasRect.top) / this.scale }; const end = { x: (this.mousePosition.x - canvasRect.left) / this.scale, y: (this.mousePosition.y - canvasRect.top) / this.scale }; const controlPoint1 = { x: start.x + Math.min(100, Math.abs(end.x - start.x) / 2), y: start.y }; const controlPoint2 = { x: end.x - Math.min(100, Math.abs(end.x - start.x) / 2), y: end.y }; const path = `M${start.x},${start.y} C${controlPoint1.x},${controlPoint1.y} ${controlPoint2.x},${controlPoint2.y} ${end.x},${end.y}`; return svg` `; } _renderConnection(conn) { const sourceNode = this.nodes.find(node => node.id === conn.sourceNodeId); const targetNode = this.nodes.find(node => node.id === conn.targetNodeId); if (!sourceNode?.shadowRoot || !targetNode?.shadowRoot) return ''; const sourcePort = sourceNode.shadowRoot.querySelector(`.output-ports [data-port-id="${conn.sourcePortId}"]`); const targetPort = targetNode.shadowRoot.querySelector(`.input-ports [data-port-id="${conn.targetPortId}"]`); if (!sourcePort || !targetPort) return ''; const sourceRect = sourcePort.getBoundingClientRect(); const targetRect = targetPort.getBoundingClientRect(); const canvasRect = this.canvas.getBoundingClientRect(); const start = { x: (sourceRect.left + sourceRect.width/2 - canvasRect.left) / this.scale, y: (sourceRect.top + sourceRect.height/2 - canvasRect.top) / this.scale }; const end = { x: (targetRect.left + targetRect.width/2 - canvasRect.left) / this.scale, y: (targetRect.top + targetRect.height/2 - canvasRect.top) / this.scale }; const controlPoint1 = { x: start.x + Math.min(100, Math.abs(end.x - start.x) / 2), y: start.y }; const controlPoint2 = { x: end.x - Math.min(100, Math.abs(end.x - start.x) / 2), y: end.y }; // Helper function to get point on cubic bezier curve at t (0-1) const getPointOnCurve = (t) => { const t1 = 1 - t; return { x: Math.pow(t1, 3) * start.x + 3 * Math.pow(t1, 2) * t * controlPoint1.x + 3 * t1 * Math.pow(t, 2) * controlPoint2.x + Math.pow(t, 3) * end.x, y: Math.pow(t1, 3) * start.y + 3 * Math.pow(t1, 2) * t * controlPoint1.y + 3 * t1 * Math.pow(t, 2) * controlPoint2.y + Math.pow(t, 3) * end.y }; }; // Get points at 15% and 85% along the curve const startButton = getPointOnCurve(0.15); const endButton = getPointOnCurve(0.85); const path = `M${start.x},${start.y} C${controlPoint1.x},${controlPoint1.y} ${controlPoint2.x},${controlPoint2.y} ${end.x},${end.y}`; const isSelected = this.selectedConnection === conn.id; return svg` ${isSelected ? svg` ` : ''} `; } _handleConnectionClick(connectionId) { // Deselect if clicking the same connection if (this.selectedConnection === connectionId) { this.selectedConnection = null; } else { this.selectedConnection = connectionId; } this.requestUpdate(); } _deleteConnection(connectionId) { const connection = this.connections.find(conn => conn.id === connectionId); this.connections = this.connections.filter(conn => conn.id !== connectionId); this.selectedConnection = null; if (connection) { this._dispatchChangeEvent({ type: 'connection-removed', data: { connectionId } }); } this.requestUpdate(); } _handlePortClick(e) { const { nodeId, portType, portId, portName } = e.detail; if (!this.draggingConnection) { if (portType === 'output') { this.draggingConnection = { sourceNodeId: nodeId, sourcePortId: portId, sourcePortType: portType }; } } else { if (portType === 'input' && nodeId !== this.draggingConnection.sourceNodeId) { const connectionExists = this.connections.some(conn => conn.sourceNodeId === this.draggingConnection.sourceNodeId && conn.sourcePortId === this.draggingConnection.sourcePortId && conn.targetNodeId === nodeId && conn.targetPortId === portId ); if (!connectionExists) { const connection = { id: `conn_${Date.now()}`, sourceNodeId: this.draggingConnection.sourceNodeId, sourcePortId: this.draggingConnection.sourcePortId, targetNodeId: nodeId, targetPortId: portId }; this.connections = [...this.connections, connection]; this._dispatchChangeEvent({ type: 'connection-added', data: connection }); } } this.draggingConnection = null; this.requestUpdate(); } } _calculateSVGBounds() { // If canvas is not ready yet, return default bounds if (!this.canvas) { return { minX: 0, minY: 0, width: '100%', height: '100%' }; } let minX = 0, minY = 0, maxX = 0, maxY = 0; const canvasRect = this.canvas.getBoundingClientRect(); // Include all node positions this.nodes.forEach(node => { const rect = node.getBoundingClientRect(); minX = Math.min(minX, (rect.left - canvasRect.left) / this.scale); minY = Math.min(minY, (rect.top - canvasRect.top) / this.scale); maxX = Math.max(maxX, (rect.right - canvasRect.left) / this.scale); maxY = Math.max(maxY, (rect.bottom - canvasRect.top) / this.scale); }); const padding = 1000; return { minX: minX - padding, minY: minY - padding, width: maxX - minX + (padding * 2), height: maxY - minY + (padding * 2) }; } }; }