pict-section-flow 0.0.6 → 0.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-flow",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Pict Section Flow Diagram",
5
5
  "main": "source/Pict-Section-Flow.js",
6
6
  "scripts": {
@@ -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: #9b59b6;
68
+ --pf-port-value-fill: #f1c40f;
69
69
  --pf-port-error-fill: #e74c3c;
70
70
 
71
- /* Connection Type Colors */
72
- --pf-connection-event-stroke: #95a5a6;
73
- --pf-connection-data-stroke: #9b59b6;
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
- fill: #7f8c8d;
319
- font-size: 9px;
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-value {
359
- stroke: var(--pf-connection-data-stroke);
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-data-stroke);
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 circle
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 use the edge for alignment (all positions on the
353
- // same edge share the same label direction)
354
- if (tmpPort.Label)
502
+ // Port label text (on top of everything)
503
+ if (tmpLabelElement)
355
504
  {
356
- let tmpLabel = this._FlowView._SVGHelperProvider.createSVGElement('text');
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
- let tmpLocal = this._GeometryProvider.getPortLocalPosition(tmpPort.Side, tmpPortIndex, tmpPortCount, tmpNode.Width, tmpNode.Height, tmpTitleBarHeight);
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