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)); this.canvas.addEventListener('click', (e) => { 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() { 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 || !targetNode) 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) { this.connections = this.connections.filter(conn => conn.id !== connectionId); this.selectedConnection = null; 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.draggingConnection = null; this.requestUpdate(); } } }; }