Various improvements

This commit is contained in:
pk
2026-09-27 18:58:32 +02:00
parent 88fa815bf5
commit ec8e895d48
3 changed files with 232 additions and 26 deletions
+28 -2
View File
@@ -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,7 +372,7 @@ 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;
@@ -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) {
@@ -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) {
+105 -6
View File
@@ -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,10 +143,16 @@ class TpFlowNodePort extends LitElement {
}
render() {
const { showTag, errorMsg, tagContent } = this;
const { showTag, errorMsg, tagContent, shape } = this;
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`
<div class="tag" ?visible=${showTag} part="tag">
@@ -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;
+89 -8
View File
@@ -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 {
<div class="node-body">
<div class="input-ports">
${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"
.portId=${idx}
.portName=${input.name}
.tagContent=${input.tagContent}
.shape=${input.shape}
.color=${input.color}
.dataTypes=${input.dataTypes}
@mousedown="${this._handlePortClick}">
</tp-flow-node-port>
`)}
@@ -89,11 +91,14 @@ export class TpFlowNode extends LitElement {
<div class="output-ports">
${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"
.portId=${idx}
.portName=${output.name}
.tagContent=${output.tagContent}
.shape=${output.shape}
.color=${output.color}
.dataTypes=${output.dataTypes}
@mousedown="${this._handlePortClick}">
</tp-flow-node-port>
`)}
@@ -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,7 +246,6 @@ export class TpFlowNode extends LitElement {
}
exportData() {
// Get current transform
const transform = window.getComputedStyle(this).transform;
const matrix = new DOMMatrix(transform);
@@ -182,7 +264,6 @@ export class TpFlowNode extends LitElement {
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)`;
}