Various improvements
This commit is contained in:
+28
-2
@@ -124,6 +124,7 @@ export const connections = function(superClass) {
|
|||||||
|
|
||||||
// If not an input port, cancel the connection
|
// If not an input port, cancel the connection
|
||||||
if (!isInputPort) {
|
if (!isInputPort) {
|
||||||
|
this._dispatchConnectionDragEnd();
|
||||||
this.draggingConnection = null;
|
this.draggingConnection = null;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@@ -371,7 +372,7 @@ export const connections = function(superClass) {
|
|||||||
_handlePortClick(e) {
|
_handlePortClick(e) {
|
||||||
if (this.readonly) return;
|
if (this.readonly) return;
|
||||||
|
|
||||||
const { node, port } = e.detail;
|
const { node, port, dataTypes } = e.detail;
|
||||||
const nodeId = node.id;
|
const nodeId = node.id;
|
||||||
const { portType, portId, portName } = port;
|
const { portType, portId, portName } = port;
|
||||||
|
|
||||||
@@ -382,8 +383,12 @@ export const connections = function(superClass) {
|
|||||||
sourcePortId: portId,
|
sourcePortId: portId,
|
||||||
sourcePortName: portName,
|
sourcePortName: portName,
|
||||||
sourcePortType: portType,
|
sourcePortType: portType,
|
||||||
sourceNode: node
|
sourceNode: node,
|
||||||
|
sourceDataTypes: dataTypes || []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Notify all nodes that a drag has started — they evaluate their ports.
|
||||||
|
this._dispatchConnectionDragStart();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (portType === 'input' && nodeId !== this.draggingConnection.sourceNodeId) {
|
if (portType === 'input' && nodeId !== this.draggingConnection.sourceNodeId) {
|
||||||
@@ -423,11 +428,32 @@ export const connections = function(superClass) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._dispatchConnectionDragEnd();
|
||||||
this.draggingConnection = null;
|
this.draggingConnection = null;
|
||||||
this.requestUpdate();
|
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() {
|
_calculateSVGBounds() {
|
||||||
// If canvas is not ready yet, return default bounds
|
// If canvas is not ready yet, return default bounds
|
||||||
if (!this.canvas) {
|
if (!this.canvas) {
|
||||||
|
|||||||
+105
-6
@@ -16,16 +16,102 @@ class TpFlowNodePort extends LitElement {
|
|||||||
position: relative;
|
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 {
|
.connectionPoint {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background: #666;
|
background: var(--port-color, #666);
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connectionPoint:hover {
|
/* Circle (control signals) */
|
||||||
background: #888;
|
.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 {
|
.tag {
|
||||||
@@ -57,10 +143,16 @@ class TpFlowNodePort extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { showTag, errorMsg, tagContent } = this;
|
const { showTag, errorMsg, tagContent, shape } = this;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="connectionPoint" part="connection-point"></div>
|
<div class="wrapper">
|
||||||
|
<div
|
||||||
|
class="connectionPoint shape-${shape || 'circle'}"
|
||||||
|
part="connection-point"
|
||||||
|
style=${this.color ? `--port-color: ${this.color}` : ''}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
${showTag || Boolean(tagContent) ? html`
|
${showTag || Boolean(tagContent) ? html`
|
||||||
<div class="tag" ?visible=${showTag} part="tag">
|
<div class="tag" ?visible=${showTag} part="tag">
|
||||||
@@ -86,9 +178,16 @@ class TpFlowNodePort extends LitElement {
|
|||||||
tagContent: { type: String },
|
tagContent: { type: String },
|
||||||
errorMsg: { type: String },
|
errorMsg: { type: String },
|
||||||
showTag: { type: Boolean },
|
showTag: { type: Boolean },
|
||||||
|
shape: { type: String },
|
||||||
|
color: { type: String },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.shape = 'circle';
|
||||||
|
}
|
||||||
|
|
||||||
showConnectionError(msg, timeout = 0) {
|
showConnectionError(msg, timeout = 0) {
|
||||||
this.errorMsg = msg;
|
this.errorMsg = msg;
|
||||||
this.showTag = true;
|
this.showTag = true;
|
||||||
|
|||||||
+89
-8
@@ -7,7 +7,6 @@ This program is available under Apache License Version 2.0
|
|||||||
import './tp-flow-node-port.js';
|
import './tp-flow-node-port.js';
|
||||||
import { LitElement, html, css } from 'lit';
|
import { LitElement, html, css } from 'lit';
|
||||||
|
|
||||||
// tp-flow-node.js
|
|
||||||
export class TpFlowNode extends LitElement {
|
export class TpFlowNode extends LitElement {
|
||||||
static get styles() {
|
static get styles() {
|
||||||
return [
|
return [
|
||||||
@@ -73,11 +72,14 @@ export class TpFlowNode extends LitElement {
|
|||||||
<div class="node-body">
|
<div class="node-body">
|
||||||
<div class="input-ports">
|
<div class="input-ports">
|
||||||
${this.inputs.map((input, idx) => html`
|
${this.inputs.map((input, idx) => html`
|
||||||
<tp-flow-node-port class="port" exportparts="connectionPoint"
|
<tp-flow-node-port class="port" exportparts="connection-point"
|
||||||
portType="input"
|
portType="input"
|
||||||
.portId=${idx}
|
.portId=${idx}
|
||||||
.portName=${input.name}
|
.portName=${input.name}
|
||||||
.tagContent=${input.tagContent}
|
.tagContent=${input.tagContent}
|
||||||
|
.shape=${input.shape}
|
||||||
|
.color=${input.color}
|
||||||
|
.dataTypes=${input.dataTypes}
|
||||||
@mousedown="${this._handlePortClick}">
|
@mousedown="${this._handlePortClick}">
|
||||||
</tp-flow-node-port>
|
</tp-flow-node-port>
|
||||||
`)}
|
`)}
|
||||||
@@ -89,11 +91,14 @@ export class TpFlowNode extends LitElement {
|
|||||||
|
|
||||||
<div class="output-ports">
|
<div class="output-ports">
|
||||||
${this.outputs.map((output, idx) => html`
|
${this.outputs.map((output, idx) => html`
|
||||||
<tp-flow-node-port class="port" exportparts="connectionPoint"
|
<tp-flow-node-port class="port" exportparts="connection-point"
|
||||||
portType="output"
|
portType="output"
|
||||||
.portId=${idx}
|
.portId=${idx}
|
||||||
.portName=${output.name}
|
.portName=${output.name}
|
||||||
.tagContent=${output.tagContent}
|
.tagContent=${output.tagContent}
|
||||||
|
.shape=${output.shape}
|
||||||
|
.color=${output.color}
|
||||||
|
.dataTypes=${output.dataTypes}
|
||||||
@mousedown="${this._handlePortClick}">
|
@mousedown="${this._handlePortClick}">
|
||||||
</tp-flow-node-port>
|
</tp-flow-node-port>
|
||||||
`)}
|
`)}
|
||||||
@@ -135,6 +140,80 @@ export class TpFlowNode extends LitElement {
|
|||||||
this.y = 0;
|
this.y = 0;
|
||||||
this.data = {};
|
this.data = {};
|
||||||
this.readonly = false;
|
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) {
|
updated(changes) {
|
||||||
@@ -144,17 +223,21 @@ export class TpFlowNode extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_handlePortClick(e) {
|
_handlePortClick(e) {
|
||||||
e.stopPropagation(); // Prevents the event from getting caught by the panning action.
|
e.stopPropagation();
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent('port-click', {
|
this.dispatchEvent(new CustomEvent('port-click', {
|
||||||
detail: { node: this, port: e.target },
|
detail: {
|
||||||
|
node: this,
|
||||||
|
port: e.target,
|
||||||
|
dataTypes: e.target.dataTypes,
|
||||||
|
},
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
composed: true
|
composed: true
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleDelete(e) {
|
_handleDelete(e) {
|
||||||
e.stopPropagation(); // Prevent event from triggering other handlers
|
e.stopPropagation();
|
||||||
this.dispatchEvent(new CustomEvent('node-delete-requested', {
|
this.dispatchEvent(new CustomEvent('node-delete-requested', {
|
||||||
detail: { nodeId: this.id },
|
detail: { nodeId: this.id },
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -163,7 +246,6 @@ export class TpFlowNode extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exportData() {
|
exportData() {
|
||||||
// Get current transform
|
|
||||||
const transform = window.getComputedStyle(this).transform;
|
const transform = window.getComputedStyle(this).transform;
|
||||||
const matrix = new DOMMatrix(transform);
|
const matrix = new DOMMatrix(transform);
|
||||||
|
|
||||||
@@ -182,7 +264,6 @@ export class TpFlowNode extends LitElement {
|
|||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.data = data.data;
|
this.data = data.data;
|
||||||
|
|
||||||
// Apply position
|
|
||||||
if (data.position) {
|
if (data.position) {
|
||||||
this.style.transform = `translate(${data.position.x}px, ${data.position.y}px)`;
|
this.style.transform = `translate(${data.position.x}px, ${data.position.y}px)`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user