diff --git a/connections.js b/connections.js index 6abcfff..d3f69fb 100644 --- a/connections.js +++ b/connections.js @@ -124,6 +124,7 @@ export const connections = function(superClass) { // If not an input port, cancel the connection if (!isInputPort) { + this._dispatchConnectionDragEnd(); this.draggingConnection = null; this.requestUpdate(); } @@ -371,10 +372,10 @@ export const connections = function(superClass) { _handlePortClick(e) { if (this.readonly) return; - const { node, port } = e.detail; + const { node, port, dataTypes } = e.detail; const nodeId = node.id; const { portType, portId, portName } = port; - + if (!this.draggingConnection) { if (portType === 'output') { this.draggingConnection = { @@ -382,8 +383,12 @@ export const connections = function(superClass) { sourcePortId: portId, sourcePortName: portName, sourcePortType: portType, - sourceNode: node + sourceNode: node, + sourceDataTypes: dataTypes || [] }; + + // Notify all nodes that a drag has started — they evaluate their ports. + this._dispatchConnectionDragStart(); } } else { if (portType === 'input' && nodeId !== this.draggingConnection.sourceNodeId) { @@ -397,8 +402,8 @@ export const connections = function(superClass) { return; } - const connectionExists = this.connections.some(conn => - conn.sourceNodeId === 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 @@ -414,7 +419,7 @@ export const connections = function(superClass) { targetPortId: portId, targetPortName: portName }; - + this.connections = [...this.connections, connection]; this._dispatchChangeEvent({ @@ -423,11 +428,32 @@ export const connections = function(superClass) { }); } } + + this._dispatchConnectionDragEnd(); this.draggingConnection = null; this.requestUpdate(); } } + _dispatchConnectionDragStart() { + const detail = { + sourceNode: this.draggingConnection.sourceNode, + sourcePortId: this.draggingConnection.sourcePortId, + sourceDataTypes: this.draggingConnection.sourceDataTypes + }; + + // Dispatch on each node directly — bubbling from canvas doesn't reach children. + this.nodes.forEach(node => { + node.dispatchEvent(new CustomEvent('connection-drag-start', { detail, bubbles: false, composed: false })); + }); + } + + _dispatchConnectionDragEnd() { + this.nodes.forEach(node => { + node.dispatchEvent(new CustomEvent('connection-drag-end', { detail: {}, bubbles: false, composed: false })); + }); + } + _calculateSVGBounds() { // If canvas is not ready yet, return default bounds if (!this.canvas) { diff --git a/tp-flow-node-port.js b/tp-flow-node-port.js index 1183fdc..3a16c3c 100644 --- a/tp-flow-node-port.js +++ b/tp-flow-node-port.js @@ -16,16 +16,102 @@ class TpFlowNodePort extends LitElement { position: relative; } + .wrapper { + position: relative; + width: 12px; + height: 12px; + transition: transform 0.15s ease; + } + + /* Glow ring sits behind the shape, works for all shapes */ + .wrapper::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 22px; + height: 22px; + transform: translate(-50%, -50%); + border-radius: 50%; + background: transparent; + transition: background 0.15s ease, box-shadow 0.15s ease; + z-index: -1; + pointer-events: none; + } + .connectionPoint { width: 12px; height: 12px; - background: #666; - border-radius: 50%; + background: var(--port-color, #666); cursor: pointer; + position: relative; } - .connectionPoint:hover { - background: #888; + /* Circle (control signals) */ + .connectionPoint.shape-circle { + border-radius: 50%; + } + + /* Square (market data) */ + .connectionPoint.shape-square { + border-radius: 2px; + } + + /* Diamond (trading signals) */ + .connectionPoint.shape-diamond { + transform: rotate(45deg); + border-radius: 2px; + } + + /* Triangle (computed values) */ + .connectionPoint.shape-triangle { + width: 0; + height: 0; + background: transparent; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 12px solid var(--port-color, #666); + } + + /* Hexagon (wildcard) */ + .connectionPoint.shape-hexagon { + clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); + border-radius: 0; + } + + /* Hover: accent glow ring */ + .wrapper:hover::after { + background: color-mix(in srgb, var(--palette-accent, #3498db) 20%, transparent); + box-shadow: 0 0 0 2px var(--palette-accent, #3498db); + } + + .wrapper:hover .connectionPoint { + filter: brightness(1.2); + } + + :host([dimmed]) .wrapper { + opacity: 0.2; + pointer-events: none; + } + + /* Compatible: scale up + accent glow ring */ + :host([compatible]) .wrapper { + transform: scale(1.4); + } + + :host([compatible]) .wrapper::after { + background: color-mix(in srgb, var(--palette-accent, #3498db) 25%, transparent); + box-shadow: 0 0 0 2px var(--palette-accent, #3498db); + } + + :host([compatible]) .connectionPoint { + filter: brightness(1.15); + } + + /* Compatible + hover: strongest signal */ + :host([compatible]) .wrapper:hover::after { + background: color-mix(in srgb, var(--palette-accent, #3498db) 35%, transparent); + box-shadow: 0 0 0 3px var(--palette-accent, #3498db), 0 0 12px color-mix(in srgb, var(--palette-accent, #3498db) 50%, transparent); } .tag { @@ -57,11 +143,17 @@ class TpFlowNodePort extends LitElement { } render() { - const { showTag, errorMsg, tagContent } = this; + const { showTag, errorMsg, tagContent, shape } = this; return html` -
- +
+
+
+ ${showTag || Boolean(tagContent) ? html`
${errorMsg ? html` @@ -86,9 +178,16 @@ class TpFlowNodePort extends LitElement { tagContent: { type: String }, errorMsg: { type: String }, showTag: { type: Boolean }, + shape: { type: String }, + color: { type: String }, }; } + constructor() { + super(); + this.shape = 'circle'; + } + showConnectionError(msg, timeout = 0) { this.errorMsg = msg; this.showTag = true; @@ -106,4 +205,4 @@ class TpFlowNodePort extends LitElement { } } -window.customElements.define('tp-flow-node-port', TpFlowNodePort); \ No newline at end of file +window.customElements.define('tp-flow-node-port', TpFlowNodePort); diff --git a/tp-flow-node.js b/tp-flow-node.js index 6485b57..d99eac0 100644 --- a/tp-flow-node.js +++ b/tp-flow-node.js @@ -7,7 +7,6 @@ This program is available under Apache License Version 2.0 import './tp-flow-node-port.js'; import { LitElement, html, css } from 'lit'; -// tp-flow-node.js export class TpFlowNode extends LitElement { static get styles() { return [ @@ -73,11 +72,14 @@ export class TpFlowNode extends LitElement {
${this.inputs.map((input, idx) => html` - `)} @@ -89,11 +91,14 @@ export class TpFlowNode extends LitElement {
${this.outputs.map((output, idx) => html` - `)} @@ -135,6 +140,80 @@ export class TpFlowNode extends LitElement { this.y = 0; this.data = {}; this.readonly = false; + + this._handleConnectionDragStart = this._handleConnectionDragStart.bind(this); + this._handleConnectionDragEnd = this._handleConnectionDragEnd.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + + this.addEventListener('connection-drag-start', this._handleConnectionDragStart); + this.addEventListener('connection-drag-end', this._handleConnectionDragEnd); + } + + disconnectedCallback() { + super.disconnectedCallback(); + + this.removeEventListener('connection-drag-start', this._handleConnectionDragStart); + this.removeEventListener('connection-drag-end', this._handleConnectionDragEnd); + } + + /** + * Called when a connection drag starts somewhere on the canvas. + * The detail contains the source node, port ID, and the source port's dataTypes. + * Each node should evaluate its input ports and dim incompatible ones. + * + * Override `checkPortCompatibility(sourceDataTypes, targetDataTypes)` to implement + * type-aware dimming. Return true if compatible, false if not. + * + * @param {CustomEvent} e — detail: { sourceNode, sourcePortId, sourceDataTypes } + */ + _handleConnectionDragStart(e) { + // Don't dim ports on the source node itself. + if (e.detail.sourceNode === this) { + return; + } + + const sourceDataTypes = e.detail.sourceDataTypes || []; + + // Evaluate each input port. + const inputPorts = this.shadowRoot.querySelectorAll('.input-ports tp-flow-node-port'); + + inputPorts.forEach(port => { + const targetDataTypes = port.dataTypes || []; + + const isCompatible = this.checkPortCompatibility(sourceDataTypes, targetDataTypes); + + if (isCompatible) { + port.removeAttribute('dimmed'); + port.setAttribute('compatible', ''); + } else { + port.setAttribute('dimmed', ''); + port.removeAttribute('compatible'); + } + }); + } + + _handleConnectionDragEnd() { + const ports = this.shadowRoot.querySelectorAll('tp-flow-node-port'); + + ports.forEach(port => { + port.removeAttribute('dimmed'); + port.removeAttribute('compatible'); + }); + } + + /** + * Override this method to implement type-aware compatibility checking. + * Default: everything is compatible (no dimming). + * + * @param {string[]} sourceDataTypes — data types of the source port + * @param {string[]} targetDataTypes — data types of the target port + * @returns {boolean} true if compatible + */ + checkPortCompatibility(sourceDataTypes, targetDataTypes) { + return true; } updated(changes) { @@ -144,17 +223,21 @@ export class TpFlowNode extends LitElement { } _handlePortClick(e) { - e.stopPropagation(); // Prevents the event from getting caught by the panning action. + e.stopPropagation(); this.dispatchEvent(new CustomEvent('port-click', { - detail: { node: this, port: e.target }, + detail: { + node: this, + port: e.target, + dataTypes: e.target.dataTypes, + }, bubbles: true, composed: true })); } _handleDelete(e) { - e.stopPropagation(); // Prevent event from triggering other handlers + e.stopPropagation(); this.dispatchEvent(new CustomEvent('node-delete-requested', { detail: { nodeId: this.id }, bubbles: true, @@ -163,10 +246,9 @@ export class TpFlowNode extends LitElement { } exportData() { - // Get current transform const transform = window.getComputedStyle(this).transform; const matrix = new DOMMatrix(transform); - + return { id: this.id, type: super.tagName.toLowerCase(), @@ -177,12 +259,11 @@ export class TpFlowNode extends LitElement { data: this.data }; } - + importData(data) { this.id = data.id; this.data = data.data; - - // Apply position + if (data.position) { this.style.transform = `translate(${data.position.x}px, ${data.position.y}px)`; } @@ -198,4 +279,4 @@ export class TpFlowNode extends LitElement { composed: true })); } -} \ No newline at end of file +}