pict-section-flow 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/source/providers/PictProvider-Flow-CSS.js +25 -11
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +23 -1
- package/source/providers/PictProvider-Flow-Geometry.js +83 -1
- package/source/services/PictService-Flow-ConnectionRenderer.js +18 -9
- package/source/views/PictView-Flow-Node.js +154 -76
- package/source/views/PictView-Flow.js +42 -1
package/package.json
CHANGED
|
@@ -65,12 +65,14 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
|
|
|
65
65
|
--pf-port-event-in-fill: #3498db;
|
|
66
66
|
--pf-port-event-out-fill: #2ecc71;
|
|
67
67
|
--pf-port-setting-fill: #e67e22;
|
|
68
|
-
--pf-port-value-fill: #
|
|
68
|
+
--pf-port-value-fill: #f1c40f;
|
|
69
69
|
--pf-port-error-fill: #e74c3c;
|
|
70
70
|
|
|
71
|
-
/* Connection Type Colors */
|
|
72
|
-
--pf-connection-event-stroke: #
|
|
73
|
-
--pf-connection-
|
|
71
|
+
/* Connection Type Colors (match source port) */
|
|
72
|
+
--pf-connection-event-in-stroke: #3498db;
|
|
73
|
+
--pf-connection-event-out-stroke: #2ecc71;
|
|
74
|
+
--pf-connection-setting-stroke: #e67e22;
|
|
75
|
+
--pf-connection-value-stroke: #f1c40f;
|
|
74
76
|
--pf-connection-error-stroke: #e74c3c;
|
|
75
77
|
|
|
76
78
|
/* Panels */
|
|
@@ -315,17 +317,23 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
|
|
|
315
317
|
fill: var(--pf-port-error-fill);
|
|
316
318
|
}
|
|
317
319
|
.pict-flow-port-label {
|
|
318
|
-
|
|
319
|
-
font-
|
|
320
|
+
font-size: 8px;
|
|
321
|
+
font-weight: 600;
|
|
320
322
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
321
323
|
pointer-events: none;
|
|
322
324
|
}
|
|
325
|
+
/* Port label badge background */
|
|
326
|
+
.pict-flow-port-label-bg {
|
|
327
|
+
pointer-events: none;
|
|
328
|
+
}
|
|
323
329
|
/* Port labels on hover: hidden by default, revealed on node hover */
|
|
324
|
-
.pict-flow-node-port-labels-hover .pict-flow-port-label
|
|
330
|
+
.pict-flow-node-port-labels-hover .pict-flow-port-label,
|
|
331
|
+
.pict-flow-node-port-labels-hover .pict-flow-port-label-bg {
|
|
325
332
|
opacity: 0;
|
|
326
333
|
transition: opacity 0.2s;
|
|
327
334
|
}
|
|
328
|
-
.pict-flow-node-port-labels-hover:hover .pict-flow-port-label
|
|
335
|
+
.pict-flow-node-port-labels-hover:hover .pict-flow-port-label,
|
|
336
|
+
.pict-flow-node-port-labels-hover:hover .pict-flow-port-label-bg {
|
|
329
337
|
opacity: 1;
|
|
330
338
|
}
|
|
331
339
|
`;
|
|
@@ -355,11 +363,17 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
|
|
|
355
363
|
stroke-width: 3;
|
|
356
364
|
}
|
|
357
365
|
/* Connection type color overrides (based on source port type) */
|
|
358
|
-
.pict-flow-connection.conn-type-
|
|
359
|
-
stroke: var(--pf-connection-
|
|
366
|
+
.pict-flow-connection.conn-type-event-in {
|
|
367
|
+
stroke: var(--pf-connection-event-in-stroke);
|
|
368
|
+
}
|
|
369
|
+
.pict-flow-connection.conn-type-event-out {
|
|
370
|
+
stroke: var(--pf-connection-event-out-stroke);
|
|
360
371
|
}
|
|
361
372
|
.pict-flow-connection.conn-type-setting {
|
|
362
|
-
stroke: var(--pf-connection-
|
|
373
|
+
stroke: var(--pf-connection-setting-stroke);
|
|
374
|
+
}
|
|
375
|
+
.pict-flow-connection.conn-type-value {
|
|
376
|
+
stroke: var(--pf-connection-value-stroke);
|
|
363
377
|
}
|
|
364
378
|
.pict-flow-connection.conn-type-error {
|
|
365
379
|
stroke: var(--pf-connection-error-stroke);
|
|
@@ -385,7 +385,7 @@ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
|
|
|
385
385
|
|
|
386
386
|
let tmpMarkup = '';
|
|
387
387
|
|
|
388
|
-
// Normal connection arrowhead
|
|
388
|
+
// Normal connection arrowhead (default gray)
|
|
389
389
|
tmpMarkup += '<marker id="flow-arrowhead-' + pViewIdentifier + '"'
|
|
390
390
|
+ ' markerWidth="' + tmpConnectionMarker.MarkerWidth + '"'
|
|
391
391
|
+ ' markerHeight="' + tmpConnectionMarker.MarkerHeight + '"'
|
|
@@ -395,6 +395,28 @@ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
|
|
|
395
395
|
+ '<polygon points="' + tmpConnectionMarker.Points + '" fill="' + tmpConnectionMarker.Fill + '" />'
|
|
396
396
|
+ '</marker>';
|
|
397
397
|
|
|
398
|
+
// Per-port-type connection arrowheads
|
|
399
|
+
let tmpPortTypeColors =
|
|
400
|
+
{
|
|
401
|
+
'event-in': '#3498db',
|
|
402
|
+
'event-out': '#2ecc71',
|
|
403
|
+
'setting': '#e67e22',
|
|
404
|
+
'value': '#f1c40f',
|
|
405
|
+
'error': '#e74c3c'
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
for (let tmpType in tmpPortTypeColors)
|
|
409
|
+
{
|
|
410
|
+
tmpMarkup += '<marker id="flow-arrowhead-' + tmpType + '-' + pViewIdentifier + '"'
|
|
411
|
+
+ ' markerWidth="' + tmpConnectionMarker.MarkerWidth + '"'
|
|
412
|
+
+ ' markerHeight="' + tmpConnectionMarker.MarkerHeight + '"'
|
|
413
|
+
+ ' refX="' + tmpConnectionMarker.RefX + '"'
|
|
414
|
+
+ ' refY="' + tmpConnectionMarker.RefY + '"'
|
|
415
|
+
+ ' orient="auto" markerUnits="strokeWidth">'
|
|
416
|
+
+ '<polygon points="' + tmpConnectionMarker.Points + '" fill="' + tmpPortTypeColors[tmpType] + '" />'
|
|
417
|
+
+ '</marker>';
|
|
418
|
+
}
|
|
419
|
+
|
|
398
420
|
// Selected connection arrowhead
|
|
399
421
|
tmpMarkup += '<marker id="flow-arrowhead-selected-' + pViewIdentifier + '"'
|
|
400
422
|
+ ' markerWidth="' + tmpSelectedMarker.MarkerWidth + '"'
|
|
@@ -134,13 +134,25 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
|
|
|
134
134
|
let tmpEdge = this.getEdgeFromSide(pSide);
|
|
135
135
|
let tmpZone = this._getZoneFromSide(pSide);
|
|
136
136
|
|
|
137
|
+
// Minimum spacing between port centers (px)
|
|
138
|
+
let tmpMinSpacing = 16;
|
|
139
|
+
|
|
140
|
+
// Reserve space at the bottom of the body so that port badges
|
|
141
|
+
// never overlap the panel-indicator icon (10×10 rect at bottom-right)
|
|
142
|
+
// and always leave a visible gap above the node bottom edge.
|
|
143
|
+
let tmpBottomPad = 16;
|
|
144
|
+
|
|
137
145
|
if (tmpEdge === 'left' || tmpEdge === 'right')
|
|
138
146
|
{
|
|
139
147
|
let tmpX = (tmpEdge === 'left') ? 0 : pWidth;
|
|
140
|
-
let tmpBodyHeight = pHeight - pTitleBarHeight;
|
|
148
|
+
let tmpBodyHeight = pHeight - pTitleBarHeight - tmpBottomPad;
|
|
141
149
|
let tmpZoneStart = pTitleBarHeight + tmpBodyHeight * tmpZone.start;
|
|
142
150
|
let tmpZoneHeight = tmpBodyHeight * (tmpZone.end - tmpZone.start);
|
|
143
151
|
let tmpSpacing = tmpZoneHeight / (pTotal + 1);
|
|
152
|
+
if (tmpSpacing < tmpMinSpacing)
|
|
153
|
+
{
|
|
154
|
+
tmpSpacing = tmpMinSpacing;
|
|
155
|
+
}
|
|
144
156
|
let tmpY = tmpZoneStart + tmpSpacing * (pIndex + 1);
|
|
145
157
|
return { x: tmpX, y: tmpY };
|
|
146
158
|
}
|
|
@@ -150,6 +162,10 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
|
|
|
150
162
|
let tmpZoneStart = pWidth * tmpZone.start;
|
|
151
163
|
let tmpZoneWidth = pWidth * (tmpZone.end - tmpZone.start);
|
|
152
164
|
let tmpSpacing = tmpZoneWidth / (pTotal + 1);
|
|
165
|
+
if (tmpSpacing < tmpMinSpacing)
|
|
166
|
+
{
|
|
167
|
+
tmpSpacing = tmpMinSpacing;
|
|
168
|
+
}
|
|
153
169
|
let tmpX = tmpZoneStart + tmpSpacing * (pIndex + 1);
|
|
154
170
|
return { x: tmpX, y: tmpY };
|
|
155
171
|
}
|
|
@@ -193,6 +209,72 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
|
|
|
193
209
|
default: return { start: 0.0, end: 1.0 };
|
|
194
210
|
}
|
|
195
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Compute the minimum node height required so that all ports
|
|
214
|
+
* (with their badges) fit within the node boundary.
|
|
215
|
+
*
|
|
216
|
+
* Uses the same zone system and minimum spacing as getPortLocalPosition.
|
|
217
|
+
* For each left/right zone, calculates where the last port would land
|
|
218
|
+
* and ensures the node is tall enough to contain it plus badge clearance.
|
|
219
|
+
*
|
|
220
|
+
* @param {Array} pPorts - Array of port objects with Side, Direction
|
|
221
|
+
* @param {number} pTitleBarHeight - Height of the title bar
|
|
222
|
+
* @returns {number} Minimum node height in pixels (0 if no ports)
|
|
223
|
+
*/
|
|
224
|
+
computeMinimumNodeHeight(pPorts, pTitleBarHeight)
|
|
225
|
+
{
|
|
226
|
+
if (!pPorts || !Array.isArray(pPorts) || pPorts.length === 0)
|
|
227
|
+
{
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let tmpMinSpacing = 16;
|
|
232
|
+
let tmpBadgeHalfHeight = 6;
|
|
233
|
+
let tmpBottomPad = 16;
|
|
234
|
+
|
|
235
|
+
// Count ports per Side value
|
|
236
|
+
let tmpCountBySide = {};
|
|
237
|
+
for (let i = 0; i < pPorts.length; i++)
|
|
238
|
+
{
|
|
239
|
+
let tmpSide = pPorts[i].Side || (pPorts[i].Direction === 'input' ? 'left' : 'right');
|
|
240
|
+
if (!tmpCountBySide[tmpSide])
|
|
241
|
+
{
|
|
242
|
+
tmpCountBySide[tmpSide] = 0;
|
|
243
|
+
}
|
|
244
|
+
tmpCountBySide[tmpSide]++;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let tmpMinHeight = 0;
|
|
248
|
+
|
|
249
|
+
for (let tmpSide in tmpCountBySide)
|
|
250
|
+
{
|
|
251
|
+
let tmpCount = tmpCountBySide[tmpSide];
|
|
252
|
+
let tmpEdge = this.getEdgeFromSide(tmpSide);
|
|
253
|
+
|
|
254
|
+
// Only left/right edge zones affect required height
|
|
255
|
+
if (tmpEdge !== 'left' && tmpEdge !== 'right')
|
|
256
|
+
{
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let tmpZone = this._getZoneFromSide(tmpSide);
|
|
261
|
+
|
|
262
|
+
// With bottomPad reserving space at the bottom:
|
|
263
|
+
// bodyHeight = H - titleBar - bottomPad
|
|
264
|
+
// lastPortY = titleBar + bodyHeight * zone.start + minSpacing * count
|
|
265
|
+
// Need: lastPortY + badgeHalfHeight <= H - bottomPad
|
|
266
|
+
// Solving for H:
|
|
267
|
+
// H >= titleBar + bottomPad + (minSpacing * count + badgeHalfHeight) / (1 - zone.start)
|
|
268
|
+
let tmpRequired = pTitleBarHeight + tmpBottomPad + (tmpMinSpacing * tmpCount + tmpBadgeHalfHeight) / (1 - tmpZone.start);
|
|
269
|
+
|
|
270
|
+
if (tmpRequired > tmpMinHeight)
|
|
271
|
+
{
|
|
272
|
+
tmpMinHeight = tmpRequired;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return Math.ceil(tmpMinHeight);
|
|
277
|
+
}
|
|
196
278
|
}
|
|
197
279
|
|
|
198
280
|
module.exports = PictProviderFlowGeometry;
|
|
@@ -87,6 +87,21 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
87
87
|
// Build the port-type CSS class suffix for connection coloring
|
|
88
88
|
let tmpConnTypeClass = tmpSourcePortType ? (' conn-type-' + tmpSourcePortType) : '';
|
|
89
89
|
|
|
90
|
+
// Determine the arrowhead marker based on port type
|
|
91
|
+
let tmpArrowMarkerId;
|
|
92
|
+
if (pIsSelected)
|
|
93
|
+
{
|
|
94
|
+
tmpArrowMarkerId = 'flow-arrowhead-selected-' + tmpViewIdentifier;
|
|
95
|
+
}
|
|
96
|
+
else if (tmpSourcePortType)
|
|
97
|
+
{
|
|
98
|
+
tmpArrowMarkerId = 'flow-arrowhead-' + tmpSourcePortType + '-' + tmpViewIdentifier;
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
{
|
|
102
|
+
tmpArrowMarkerId = 'flow-arrowhead-' + tmpViewIdentifier;
|
|
103
|
+
}
|
|
104
|
+
|
|
90
105
|
// Hit area (wider invisible path for easier selection)
|
|
91
106
|
let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
|
|
92
107
|
if (tmpShapeProvider)
|
|
@@ -101,6 +116,8 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
101
116
|
tmpPathElement.setAttribute('class',
|
|
102
117
|
(tmpPathElement.getAttribute('class') || '') + tmpConnTypeClass);
|
|
103
118
|
}
|
|
119
|
+
// Override the default arrowhead with the typed one
|
|
120
|
+
tmpPathElement.setAttribute('marker-end', 'url(#' + tmpArrowMarkerId + ')');
|
|
104
121
|
if (tmpStrokeDashArray)
|
|
105
122
|
{
|
|
106
123
|
tmpPathElement.setAttribute('stroke-dasharray', tmpStrokeDashArray);
|
|
@@ -121,15 +138,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
121
138
|
tmpPathElement.setAttribute('d', tmpPath);
|
|
122
139
|
tmpPathElement.setAttribute('data-connection-hash', pConnection.Hash);
|
|
123
140
|
tmpPathElement.setAttribute('data-element-type', 'connection');
|
|
124
|
-
|
|
125
|
-
if (pIsSelected)
|
|
126
|
-
{
|
|
127
|
-
tmpPathElement.setAttribute('marker-end', `url(#flow-arrowhead-selected-${tmpViewIdentifier})`);
|
|
128
|
-
}
|
|
129
|
-
else
|
|
130
|
-
{
|
|
131
|
-
tmpPathElement.setAttribute('marker-end', `url(#flow-arrowhead-${tmpViewIdentifier})`);
|
|
132
|
-
}
|
|
141
|
+
tmpPathElement.setAttribute('marker-end', 'url(#' + tmpArrowMarkerId + ')');
|
|
133
142
|
|
|
134
143
|
if (tmpStrokeDashArray)
|
|
135
144
|
{
|
|
@@ -49,6 +49,23 @@ class PictViewFlowNode extends libPictView
|
|
|
49
49
|
let tmpHeight = pNodeData.Height || 80;
|
|
50
50
|
let tmpTitleBarHeight = this.options.NodeTitleBarHeight;
|
|
51
51
|
|
|
52
|
+
// Ensure node is tall enough for all ports in their zones
|
|
53
|
+
let tmpGeomProvider = this._FlowView._GeometryProvider;
|
|
54
|
+
if (tmpGeomProvider && pNodeData.Ports && pNodeData.Ports.length > 0)
|
|
55
|
+
{
|
|
56
|
+
let tmpMinHeight = tmpGeomProvider.computeMinimumNodeHeight(pNodeData.Ports, tmpTitleBarHeight);
|
|
57
|
+
if (tmpMinHeight > tmpHeight)
|
|
58
|
+
{
|
|
59
|
+
tmpHeight = tmpMinHeight;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Write the adjusted dimensions back to the node data so that
|
|
64
|
+
// connection rendering (which reads pNodeData.Width/Height to
|
|
65
|
+
// compute port positions) uses the same values we render with.
|
|
66
|
+
pNodeData.Width = tmpWidth;
|
|
67
|
+
pNodeData.Height = tmpHeight;
|
|
68
|
+
|
|
52
69
|
// Determine node body mode from theme (bracket vs rect)
|
|
53
70
|
let tmpNodeBodyMode = 'rect';
|
|
54
71
|
if (this._FlowView._ThemeProvider)
|
|
@@ -319,7 +336,140 @@ class PictViewFlowNode extends libPictView
|
|
|
319
336
|
let tmpPort = tmpPorts[i];
|
|
320
337
|
let tmpPosition = this._getPortLocalPosition(tmpSide, i, tmpPorts.length, pWidth, pHeight);
|
|
321
338
|
|
|
322
|
-
// Port
|
|
339
|
+
// Port label badge — flush against the node edge with no
|
|
340
|
+
// border on the edge side; rendered before the port circle
|
|
341
|
+
// so the circle visually sits on top of the badge
|
|
342
|
+
let tmpLabelElement = null;
|
|
343
|
+
if (tmpPort.Label)
|
|
344
|
+
{
|
|
345
|
+
let tmpPortTypeColorMap =
|
|
346
|
+
{
|
|
347
|
+
'event-in': '#3498db',
|
|
348
|
+
'event-out': '#2ecc71',
|
|
349
|
+
'setting': '#e67e22',
|
|
350
|
+
'value': '#f1c40f',
|
|
351
|
+
'error': '#e74c3c'
|
|
352
|
+
};
|
|
353
|
+
let tmpBorderColor = tmpPort.PortType ? (tmpPortTypeColorMap[tmpPort.PortType] || '#95a5a6') : '#95a5a6';
|
|
354
|
+
|
|
355
|
+
let tmpBadgeHeight = 12;
|
|
356
|
+
let tmpBadgePadH = 5;
|
|
357
|
+
let tmpBadgeBorderW = 2;
|
|
358
|
+
let tmpEdgePad = 1;
|
|
359
|
+
let tmpPortRadius = 5;
|
|
360
|
+
|
|
361
|
+
let tmpTextLen = tmpPort.Label.length * 5;
|
|
362
|
+
let tmpBadgeX, tmpBadgeY, tmpBadgeWidth;
|
|
363
|
+
let tmpTextX, tmpTextAnchor;
|
|
364
|
+
let tmpStripeX, tmpStripeY, tmpStripeW, tmpStripeH;
|
|
365
|
+
let tmpBorderPath;
|
|
366
|
+
|
|
367
|
+
if (tmpEdge === 'left')
|
|
368
|
+
{
|
|
369
|
+
tmpBadgeWidth = tmpPortRadius + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpBadgeBorderW;
|
|
370
|
+
tmpBadgeX = tmpEdgePad;
|
|
371
|
+
tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
|
|
372
|
+
tmpTextX = tmpBadgeX + tmpPortRadius + tmpBadgePadH;
|
|
373
|
+
tmpTextAnchor = 'start';
|
|
374
|
+
tmpStripeX = tmpBadgeX + tmpBadgeWidth - tmpBadgeBorderW;
|
|
375
|
+
tmpStripeY = tmpBadgeY;
|
|
376
|
+
tmpStripeW = tmpBadgeBorderW;
|
|
377
|
+
tmpStripeH = tmpBadgeHeight;
|
|
378
|
+
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
379
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
380
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
381
|
+
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
382
|
+
}
|
|
383
|
+
else if (tmpEdge === 'right')
|
|
384
|
+
{
|
|
385
|
+
tmpBadgeWidth = tmpBadgeBorderW + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpPortRadius;
|
|
386
|
+
tmpBadgeX = pWidth - tmpBadgeWidth - tmpEdgePad;
|
|
387
|
+
tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
|
|
388
|
+
tmpTextX = tmpBadgeX + tmpBadgeBorderW + tmpBadgePadH;
|
|
389
|
+
tmpTextAnchor = 'start';
|
|
390
|
+
tmpStripeX = tmpBadgeX;
|
|
391
|
+
tmpStripeY = tmpBadgeY;
|
|
392
|
+
tmpStripeW = tmpBadgeBorderW;
|
|
393
|
+
tmpStripeH = tmpBadgeHeight;
|
|
394
|
+
tmpBorderPath = 'M ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
395
|
+
+ ' L ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
396
|
+
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
397
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
398
|
+
}
|
|
399
|
+
else if (tmpEdge === 'top')
|
|
400
|
+
{
|
|
401
|
+
tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
|
|
402
|
+
tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
|
|
403
|
+
tmpBadgeY = tmpEdgePad;
|
|
404
|
+
tmpTextX = tmpPosition.x;
|
|
405
|
+
tmpTextAnchor = 'middle';
|
|
406
|
+
tmpStripeX = tmpBadgeX;
|
|
407
|
+
tmpStripeY = tmpBadgeY + tmpBadgeHeight - tmpBadgeBorderW;
|
|
408
|
+
tmpStripeW = tmpBadgeWidth;
|
|
409
|
+
tmpStripeH = tmpBadgeBorderW;
|
|
410
|
+
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
411
|
+
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
412
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
413
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY;
|
|
414
|
+
}
|
|
415
|
+
else
|
|
416
|
+
{
|
|
417
|
+
tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
|
|
418
|
+
tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
|
|
419
|
+
tmpBadgeY = pHeight - tmpBadgeHeight - tmpEdgePad;
|
|
420
|
+
tmpTextX = tmpPosition.x;
|
|
421
|
+
tmpTextAnchor = 'middle';
|
|
422
|
+
tmpStripeX = tmpBadgeX;
|
|
423
|
+
tmpStripeY = tmpBadgeY;
|
|
424
|
+
tmpStripeW = tmpBadgeWidth;
|
|
425
|
+
tmpStripeH = tmpBadgeBorderW;
|
|
426
|
+
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
427
|
+
+ ' L ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
428
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
429
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Background rect (cream, no stroke — border drawn separately)
|
|
433
|
+
let tmpBgRect = this._FlowView._SVGHelperProvider.createSVGElement('rect');
|
|
434
|
+
tmpBgRect.setAttribute('class', 'pict-flow-port-label-bg');
|
|
435
|
+
tmpBgRect.setAttribute('x', String(tmpBadgeX));
|
|
436
|
+
tmpBgRect.setAttribute('y', String(tmpBadgeY));
|
|
437
|
+
tmpBgRect.setAttribute('width', String(tmpBadgeWidth));
|
|
438
|
+
tmpBgRect.setAttribute('height', String(tmpBadgeHeight));
|
|
439
|
+
tmpBgRect.setAttribute('fill', 'rgba(255, 253, 240, 0.5)');
|
|
440
|
+
pGroup.appendChild(tmpBgRect);
|
|
441
|
+
|
|
442
|
+
// 3-sided border path (open on the edge-facing side)
|
|
443
|
+
let tmpBorderPathEl = this._FlowView._SVGHelperProvider.createSVGElement('path');
|
|
444
|
+
tmpBorderPathEl.setAttribute('class', 'pict-flow-port-label-bg');
|
|
445
|
+
tmpBorderPathEl.setAttribute('d', tmpBorderPath);
|
|
446
|
+
tmpBorderPathEl.setAttribute('fill', 'none');
|
|
447
|
+
tmpBorderPathEl.setAttribute('stroke', tmpBorderColor);
|
|
448
|
+
tmpBorderPathEl.setAttribute('stroke-width', '0.75');
|
|
449
|
+
pGroup.appendChild(tmpBorderPathEl);
|
|
450
|
+
|
|
451
|
+
// Colored stripe on the inner side
|
|
452
|
+
let tmpStripe = this._FlowView._SVGHelperProvider.createSVGElement('rect');
|
|
453
|
+
tmpStripe.setAttribute('class', 'pict-flow-port-label-bg');
|
|
454
|
+
tmpStripe.setAttribute('x', String(tmpStripeX));
|
|
455
|
+
tmpStripe.setAttribute('y', String(tmpStripeY));
|
|
456
|
+
tmpStripe.setAttribute('width', String(tmpStripeW));
|
|
457
|
+
tmpStripe.setAttribute('height', String(tmpStripeH));
|
|
458
|
+
tmpStripe.setAttribute('fill', tmpBorderColor);
|
|
459
|
+
pGroup.appendChild(tmpStripe);
|
|
460
|
+
|
|
461
|
+
// Text label — appended after circle for z-order
|
|
462
|
+
tmpLabelElement = this._FlowView._SVGHelperProvider.createSVGElement('text');
|
|
463
|
+
tmpLabelElement.setAttribute('class', 'pict-flow-port-label');
|
|
464
|
+
tmpLabelElement.setAttribute('fill', '#2c3e50');
|
|
465
|
+
tmpLabelElement.textContent = tmpPort.Label;
|
|
466
|
+
tmpLabelElement.setAttribute('x', String(tmpTextX));
|
|
467
|
+
tmpLabelElement.setAttribute('y', String(tmpBadgeY + tmpBadgeHeight / 2));
|
|
468
|
+
tmpLabelElement.setAttribute('text-anchor', tmpTextAnchor);
|
|
469
|
+
tmpLabelElement.setAttribute('dominant-baseline', 'central');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Port circle (rendered on top of badge background)
|
|
323
473
|
let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
|
|
324
474
|
let tmpCircle;
|
|
325
475
|
if (tmpShapeProvider)
|
|
@@ -349,82 +499,10 @@ class PictViewFlowNode extends libPictView
|
|
|
349
499
|
}
|
|
350
500
|
pGroup.appendChild(tmpCircle);
|
|
351
501
|
|
|
352
|
-
// Port label
|
|
353
|
-
|
|
354
|
-
if (tmpPort.Label)
|
|
502
|
+
// Port label text (on top of everything)
|
|
503
|
+
if (tmpLabelElement)
|
|
355
504
|
{
|
|
356
|
-
|
|
357
|
-
tmpLabel.setAttribute('class', 'pict-flow-port-label');
|
|
358
|
-
tmpLabel.textContent = tmpPort.Label;
|
|
359
|
-
|
|
360
|
-
// Base offset from port center; PortLabelPadding adds extra space
|
|
361
|
-
let tmpLabelOffset = 12;
|
|
362
|
-
let tmpPaddingExtra = tmpPortLabelPadding ? 8 : 0;
|
|
363
|
-
|
|
364
|
-
// When PortLabelsOutside is true, labels render outside the node
|
|
365
|
-
// boundary (away from center) instead of inside (toward center).
|
|
366
|
-
// The direction multiplier flips the offset direction per edge.
|
|
367
|
-
let tmpOutsideFlip = tmpPortLabelsOutside ? -1 : 1;
|
|
368
|
-
|
|
369
|
-
if (tmpPortLabelsVertical)
|
|
370
|
-
{
|
|
371
|
-
switch (tmpEdge)
|
|
372
|
-
{
|
|
373
|
-
case 'left':
|
|
374
|
-
tmpLabel.setAttribute('x', String(tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
|
|
375
|
-
tmpLabel.setAttribute('y', String(tmpPosition.y));
|
|
376
|
-
tmpLabel.setAttribute('text-anchor', 'middle');
|
|
377
|
-
tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip}, ${tmpPosition.y})`);
|
|
378
|
-
break;
|
|
379
|
-
case 'right':
|
|
380
|
-
tmpLabel.setAttribute('x', String(tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
|
|
381
|
-
tmpLabel.setAttribute('y', String(tmpPosition.y));
|
|
382
|
-
tmpLabel.setAttribute('text-anchor', 'middle');
|
|
383
|
-
tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip}, ${tmpPosition.y})`);
|
|
384
|
-
break;
|
|
385
|
-
case 'top':
|
|
386
|
-
tmpLabel.setAttribute('x', String(tmpPosition.x));
|
|
387
|
-
tmpLabel.setAttribute('y', String(tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
|
|
388
|
-
tmpLabel.setAttribute('text-anchor', 'middle');
|
|
389
|
-
tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip})`);
|
|
390
|
-
break;
|
|
391
|
-
case 'bottom':
|
|
392
|
-
tmpLabel.setAttribute('x', String(tmpPosition.x));
|
|
393
|
-
tmpLabel.setAttribute('y', String(tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
|
|
394
|
-
tmpLabel.setAttribute('text-anchor', 'middle');
|
|
395
|
-
tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip})`);
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
else
|
|
400
|
-
{
|
|
401
|
-
// Horizontal labels (default)
|
|
402
|
-
switch (tmpEdge)
|
|
403
|
-
{
|
|
404
|
-
case 'left':
|
|
405
|
-
tmpLabel.setAttribute('x', String(tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
|
|
406
|
-
tmpLabel.setAttribute('y', String(tmpPosition.y));
|
|
407
|
-
tmpLabel.setAttribute('text-anchor', tmpPortLabelsOutside ? 'end' : 'start');
|
|
408
|
-
break;
|
|
409
|
-
case 'right':
|
|
410
|
-
tmpLabel.setAttribute('x', String(tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
|
|
411
|
-
tmpLabel.setAttribute('y', String(tmpPosition.y));
|
|
412
|
-
tmpLabel.setAttribute('text-anchor', tmpPortLabelsOutside ? 'start' : 'end');
|
|
413
|
-
break;
|
|
414
|
-
case 'top':
|
|
415
|
-
tmpLabel.setAttribute('x', String(tmpPosition.x));
|
|
416
|
-
tmpLabel.setAttribute('y', String(tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
|
|
417
|
-
tmpLabel.setAttribute('text-anchor', 'middle');
|
|
418
|
-
break;
|
|
419
|
-
case 'bottom':
|
|
420
|
-
tmpLabel.setAttribute('x', String(tmpPosition.x));
|
|
421
|
-
tmpLabel.setAttribute('y', String(tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
|
|
422
|
-
tmpLabel.setAttribute('text-anchor', 'middle');
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
tmpLabel.setAttribute('dominant-baseline', 'central');
|
|
427
|
-
pGroup.appendChild(tmpLabel);
|
|
505
|
+
pGroup.appendChild(tmpLabelElement);
|
|
428
506
|
}
|
|
429
507
|
}
|
|
430
508
|
}
|
|
@@ -1328,7 +1328,20 @@ class PictViewFlow extends libPictView
|
|
|
1328
1328
|
|
|
1329
1329
|
let tmpTitleBarHeight = (this._NodeView && this._NodeView.options.NodeTitleBarHeight) || 28;
|
|
1330
1330
|
|
|
1331
|
-
|
|
1331
|
+
// Use the adjusted node height that accounts for minimum port
|
|
1332
|
+
// spacing. Connections render before nodes, so the node renderer
|
|
1333
|
+
// may not have written back its adjusted height yet.
|
|
1334
|
+
let tmpHeight = tmpNode.Height || 80;
|
|
1335
|
+
if (this._GeometryProvider && tmpNode.Ports && tmpNode.Ports.length > 0)
|
|
1336
|
+
{
|
|
1337
|
+
let tmpMinHeight = this._GeometryProvider.computeMinimumNodeHeight(tmpNode.Ports, tmpTitleBarHeight);
|
|
1338
|
+
if (tmpMinHeight > tmpHeight)
|
|
1339
|
+
{
|
|
1340
|
+
tmpHeight = tmpMinHeight;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
let tmpLocal = this._GeometryProvider.getPortLocalPosition(tmpPort.Side, tmpPortIndex, tmpPortCount, tmpNode.Width, tmpHeight, tmpTitleBarHeight);
|
|
1332
1345
|
|
|
1333
1346
|
return { x: tmpNode.X + tmpLocal.x, y: tmpNode.Y + tmpLocal.y, side: tmpPort.Side || 'right' };
|
|
1334
1347
|
}
|
|
@@ -1381,6 +1394,34 @@ class PictViewFlow extends libPictView
|
|
|
1381
1394
|
let tmpIsSelected = (this._FlowData.ViewState.SelectedNodeHash === tmpNode.Hash);
|
|
1382
1395
|
let tmpNodeTypeConfig = this._NodeTypeProvider.getNodeType(tmpNode.Type);
|
|
1383
1396
|
|
|
1397
|
+
// Enrich saved port data with metadata from the node type's DefaultPorts.
|
|
1398
|
+
// Saved flow data may not include PortType or may have stale Side values,
|
|
1399
|
+
// so we match each port to its DefaultPort counterpart by Label and Direction,
|
|
1400
|
+
// then copy over PortType and Side from the authoritative node type definition.
|
|
1401
|
+
if (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultPorts && tmpNode.Ports)
|
|
1402
|
+
{
|
|
1403
|
+
for (let p = 0; p < tmpNode.Ports.length; p++)
|
|
1404
|
+
{
|
|
1405
|
+
let tmpPort = tmpNode.Ports[p];
|
|
1406
|
+
for (let d = 0; d < tmpNodeTypeConfig.DefaultPorts.length; d++)
|
|
1407
|
+
{
|
|
1408
|
+
let tmpDefault = tmpNodeTypeConfig.DefaultPorts[d];
|
|
1409
|
+
if (tmpDefault.Label === tmpPort.Label && tmpDefault.Direction === tmpPort.Direction)
|
|
1410
|
+
{
|
|
1411
|
+
if (tmpDefault.PortType)
|
|
1412
|
+
{
|
|
1413
|
+
tmpPort.PortType = tmpDefault.PortType;
|
|
1414
|
+
}
|
|
1415
|
+
if (tmpDefault.Side)
|
|
1416
|
+
{
|
|
1417
|
+
tmpPort.Side = tmpDefault.Side;
|
|
1418
|
+
}
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1384
1425
|
this._NodeView.renderNode(tmpNode, this._NodesLayer, tmpIsSelected, tmpNodeTypeConfig);
|
|
1385
1426
|
}
|
|
1386
1427
|
|