tp-flow-nodes/connections.js

388 lines
12 KiB
JavaScript
Raw Normal View History

2024-12-18 21:27:29 +01:00
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;
2024-12-18 22:11:06 +01:00
pointer-events: all;
cursor: pointer;
2024-12-18 21:27:29 +01:00
}
.connections path:hover {
stroke: #999;
stroke-width: 3;
}
2024-12-18 22:11:06 +01:00
.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;
}
2024-12-18 21:27:29 +01:00
`;
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));
2024-12-20 10:29:39 +01:00
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
2024-12-18 22:11:06 +01:00
if (e.target === this.canvas) {
this.selectedConnection = null;
this.requestUpdate();
}
});
2024-12-20 10:29:39 +01:00
2024-12-18 21:27:29 +01:00
super.firstUpdated();
}
_updatePreviewConnection(e) {
if (this.draggingConnection) {
this.mousePosition = {
x: e.clientX,
y: e.clientY
};
this.requestUpdate();
}
}
_renderConnections() {
2024-12-20 10:29:39 +01:00
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;
`;
2024-12-18 21:27:29 +01:00
return html`
2024-12-20 10:29:39 +01:00
<svg
class="connections"
style="${style}"
viewBox="${this.canvas ? `${bounds.minX} ${bounds.minY} ${bounds.width} ${bounds.height}` : '0 0 100 100'}"
>
2024-12-18 21:27:29 +01:00
${this.connections.map(conn => this._renderConnection(conn))}
${this.draggingConnection ? this._renderPreviewConnection() : null}
</svg>
`;
}
_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 = {
2024-12-18 22:27:27 +01:00
x: (sourceRect.left + sourceRect.width/2 - canvasRect.left) / this.scale,
y: (sourceRect.top + sourceRect.height/2 - canvasRect.top) / this.scale
2024-12-18 21:27:29 +01:00
};
const end = {
2024-12-18 22:27:27 +01:00
x: (this.mousePosition.x - canvasRect.left) / this.scale,
y: (this.mousePosition.y - canvasRect.top) / this.scale
2024-12-18 21:27:29 +01:00
};
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`
<path
d="${path}"
fill="none"
stroke="#666"
stroke-width="2"
stroke-dasharray="5,5"
id="preview-connection"
/>
`;
}
_renderConnection(conn) {
const sourceNode = this.nodes.find(node => node.id === conn.sourceNodeId);
const targetNode = this.nodes.find(node => node.id === conn.targetNodeId);
2024-12-20 10:29:39 +01:00
if (!sourceNode?.shadowRoot || !targetNode?.shadowRoot) return '';
2024-12-18 21:27:29 +01:00
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 = {
2024-12-18 22:27:27 +01:00
x: (sourceRect.left + sourceRect.width/2 - canvasRect.left) / this.scale,
y: (sourceRect.top + sourceRect.height/2 - canvasRect.top) / this.scale
2024-12-18 21:27:29 +01:00
};
const end = {
2024-12-18 22:27:27 +01:00
x: (targetRect.left + targetRect.width/2 - canvasRect.left) / this.scale,
y: (targetRect.top + targetRect.height/2 - canvasRect.top) / this.scale
2024-12-18 21:27:29 +01:00
};
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
};
2024-12-18 22:11:06 +01:00
// 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);
2024-12-18 21:27:29 +01:00
const path = `M${start.x},${start.y} C${controlPoint1.x},${controlPoint1.y} ${controlPoint2.x},${controlPoint2.y} ${end.x},${end.y}`;
2024-12-18 22:11:06 +01:00
const isSelected = this.selectedConnection === conn.id;
2024-12-18 21:27:29 +01:00
return svg`
2024-12-18 22:11:06 +01:00
<g>
<path
d="${path}"
fill="none"
stroke="#666"
stroke-width="2"
id="${conn.id}"
class="${isSelected ? 'selected' : ''}"
@click="${() => this._handleConnectionClick(conn.id)}"
/>
${isSelected ? svg`
<g>
<!-- Source delete button -->
<g class="delete-button-group" @click="${() => this._deleteConnection(conn.id)}">
<circle
class="delete-button-circle"
cx="${startButton.x}"
cy="${startButton.y}"
r="8"
/>
<path
class="delete-button-x"
d="M ${startButton.x-4},${startButton.y-4} L ${startButton.x+4},${startButton.y+4} M ${startButton.x-4},${startButton.y+4} L ${startButton.x+4},${startButton.y-4}"
/>
</g>
<!-- Target delete button -->
<g class="delete-button-group" @click="${() => this._deleteConnection(conn.id)}">
<circle
class="delete-button-circle"
cx="${endButton.x}"
cy="${endButton.y}"
r="8"
/>
<path
class="delete-button-x"
d="M ${endButton.x-4},${endButton.y-4} L ${endButton.x+4},${endButton.y+4} M ${endButton.x-4},${endButton.y+4} L ${endButton.x+4},${endButton.y-4}"
/>
</g>
</g>
` : ''}
</g>
2024-12-18 21:27:29 +01:00
`;
}
2024-12-18 22:11:06 +01:00
_handleConnectionClick(connectionId) {
// Deselect if clicking the same connection
if (this.selectedConnection === connectionId) {
this.selectedConnection = null;
} else {
this.selectedConnection = connectionId;
}
this.requestUpdate();
}
_deleteConnection(connectionId) {
2024-12-20 10:29:39 +01:00
const connection = this.connections.find(conn => conn.id === connectionId);
2024-12-18 22:11:06 +01:00
this.connections = this.connections.filter(conn => conn.id !== connectionId);
this.selectedConnection = null;
2024-12-20 10:29:39 +01:00
if (connection) {
this._dispatchChangeEvent({
type: 'connection-removed',
data: { connectionId }
});
}
2024-12-18 22:11:06 +01:00
this.requestUpdate();
}
2024-12-18 21:27:29 +01:00
_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];
2024-12-20 10:29:39 +01:00
this._dispatchChangeEvent({
type: 'connection-added',
data: connection
});
2024-12-18 21:27:29 +01:00
}
}
this.draggingConnection = null;
this.requestUpdate();
}
}
2024-12-20 10:29:39 +01:00
_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)
};
}
2024-12-18 21:27:29 +01:00
};
}