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.3 s ease , stroke - width 0.3 s 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 : # 3498 db ;
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.2 s 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-18 22:11:06 +01:00
this . canvas . addEventListener ( 'click' , ( e ) => {
if ( e . target === this . canvas ) {
this . selectedConnection = null ;
this . requestUpdate ( ) ;
}
} ) ;
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 ( ) {
return html `
< svg class = "connections" style = "position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;" >
$ { this . connections . map ( conn => this . _renderConnection ( conn ) ) }
$ { this . draggingConnection ? this . _renderPreviewConnection ( ) : null }
< / s v g >
` ;
}
_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 ,
y : sourceRect . top + sourceRect . height / 2 - canvasRect . top
} ;
const end = {
x : this . mousePosition . x - canvasRect . left ,
y : this . mousePosition . y - canvasRect . top
} ;
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 ) ;
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 ,
y : sourceRect . top + sourceRect . height / 2 - canvasRect . top
} ;
const end = {
x : targetRect . left + targetRect . width / 2 - canvasRect . left ,
y : targetRect . top + targetRect . height / 2 - canvasRect . top
} ;
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 ) {
this . connections = this . connections . filter ( conn => conn . id !== connectionId ) ;
this . selectedConnection = null ;
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 ] ;
}
}
this . draggingConnection = null ;
this . requestUpdate ( ) ;
}
}
} ;
}