pict-section-flow 0.0.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pict-section-flow",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Pict Section Flow Diagram",
5
5
  "main": "source/Pict-Section-Flow.js",
6
6
  "scripts": {
@@ -133,6 +133,10 @@ class PictFlowCard extends libFableServiceProviderBase
133
133
  MinimumInputCount: (typeof tmpInput.MinimumInputCount === 'number') ? tmpInput.MinimumInputCount : 0,
134
134
  MaximumInputCount: (typeof tmpInput.MaximumInputCount === 'number') ? tmpInput.MaximumInputCount : -1
135
135
  };
136
+ if (tmpInput.PortType)
137
+ {
138
+ tmpPort.PortType = tmpInput.PortType;
139
+ }
136
140
  tmpPorts.push(tmpPort);
137
141
  }
138
142
 
@@ -140,13 +144,18 @@ class PictFlowCard extends libFableServiceProviderBase
140
144
  for (let i = 0; i < this.cardOutputs.length; i++)
141
145
  {
142
146
  let tmpOutput = this.cardOutputs[i];
143
- tmpPorts.push(
147
+ let tmpOutPort =
144
148
  {
145
149
  Hash: null,
146
150
  Direction: 'output',
147
151
  Side: tmpOutput.Side || 'right',
148
152
  Label: tmpOutput.Name || `Out ${i + 1}`
149
- });
153
+ };
154
+ if (tmpOutput.PortType)
155
+ {
156
+ tmpOutPort.PortType = tmpOutput.PortType;
157
+ }
158
+ tmpPorts.push(tmpOutPort);
150
159
  }
151
160
 
152
161
  // If no ports were defined, provide sensible defaults
@@ -50,6 +50,11 @@ class FlowCardPropertiesPanelForm extends libPictFlowCardPropertiesPanel
50
50
  let tmpContainerID = `pict-flow-panel-form-${pNodeData.Hash}`;
51
51
  pContainer.innerHTML = `<div id="${tmpContainerID}"></div>`;
52
52
 
53
+ // Bind the node data to AppData.Record so the form descriptors
54
+ // (which use addresses like Record.Data.SearchString) resolve against
55
+ // the actual node object.
56
+ this.pict.AppData.Record = pNodeData;
57
+
53
58
  try
54
59
  {
55
60
  // Look for an existing metacontroller or create one
@@ -61,6 +61,20 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
61
61
  --pf-port-stroke: #ffffff;
62
62
  --pf-port-stroke-width: 2;
63
63
 
64
+ /* Port Type Colors */
65
+ --pf-port-event-in-fill: #3498db;
66
+ --pf-port-event-out-fill: #2ecc71;
67
+ --pf-port-setting-fill: #e67e22;
68
+ --pf-port-value-fill: #f1c40f;
69
+ --pf-port-error-fill: #e74c3c;
70
+
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;
76
+ --pf-connection-error-stroke: #e74c3c;
77
+
64
78
  /* Panels */
65
79
  --pf-panel-bg: #ffffff;
66
80
  --pf-panel-border: #d0d4d8;
@@ -286,18 +300,40 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
286
300
  r: 7;
287
301
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.20));
288
302
  }
303
+ /* Port type color overrides */
304
+ .pict-flow-port.port-type-event-in {
305
+ fill: var(--pf-port-event-in-fill);
306
+ }
307
+ .pict-flow-port.port-type-event-out {
308
+ fill: var(--pf-port-event-out-fill);
309
+ }
310
+ .pict-flow-port.port-type-setting {
311
+ fill: var(--pf-port-setting-fill);
312
+ }
313
+ .pict-flow-port.port-type-value {
314
+ fill: var(--pf-port-value-fill);
315
+ }
316
+ .pict-flow-port.port-type-error {
317
+ fill: var(--pf-port-error-fill);
318
+ }
289
319
  .pict-flow-port-label {
290
- fill: #7f8c8d;
291
- font-size: 9px;
320
+ font-size: 8px;
321
+ font-weight: 600;
292
322
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
293
323
  pointer-events: none;
294
324
  }
325
+ /* Port label badge background */
326
+ .pict-flow-port-label-bg {
327
+ pointer-events: none;
328
+ }
295
329
  /* Port labels on hover: hidden by default, revealed on node hover */
296
- .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 {
297
332
  opacity: 0;
298
333
  transition: opacity 0.2s;
299
334
  }
300
- .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 {
301
337
  opacity: 1;
302
338
  }
303
339
  `;
@@ -326,6 +362,22 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
326
362
  stroke: var(--pf-connection-selected-stroke);
327
363
  stroke-width: 3;
328
364
  }
365
+ /* Connection type color overrides (based on source port type) */
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);
371
+ }
372
+ .pict-flow-connection.conn-type-setting {
373
+ stroke: var(--pf-connection-setting-stroke);
374
+ }
375
+ .pict-flow-connection.conn-type-value {
376
+ stroke: var(--pf-connection-value-stroke);
377
+ }
378
+ .pict-flow-connection.conn-type-error {
379
+ stroke: var(--pf-connection-error-stroke);
380
+ }
329
381
  .pict-flow-connection-hitarea {
330
382
  fill: none;
331
383
  stroke: transparent;
@@ -591,6 +643,22 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
591
643
  .pict-flow-info-panel-port.output {
592
644
  border-left: 3px solid var(--pf-port-output-fill);
593
645
  }
646
+ /* Info panel port type color overrides */
647
+ .pict-flow-info-panel-port.port-type-event-in {
648
+ border-left-color: var(--pf-port-event-in-fill);
649
+ }
650
+ .pict-flow-info-panel-port.port-type-event-out {
651
+ border-left-color: var(--pf-port-event-out-fill);
652
+ }
653
+ .pict-flow-info-panel-port.port-type-setting {
654
+ border-left-color: var(--pf-port-setting-fill);
655
+ }
656
+ .pict-flow-info-panel-port.port-type-value {
657
+ border-left-color: var(--pf-port-value-fill);
658
+ }
659
+ .pict-flow-info-panel-port.port-type-error {
660
+ border-left-color: var(--pf-port-error-fill);
661
+ }
594
662
  .pict-flow-info-panel-port-constraint {
595
663
  color: #8e99a4;
596
664
  font-size: 10px;
@@ -195,7 +195,12 @@ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
195
195
  {
196
196
  let tmpConfig = this._DefaultShapes['port'];
197
197
  let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
198
- tmpElement.setAttribute('class', tmpConfig.ClassName + ' ' + pPortData.Direction);
198
+ let tmpClassName = tmpConfig.ClassName + ' ' + pPortData.Direction;
199
+ if (pPortData.PortType)
200
+ {
201
+ tmpClassName += ' port-type-' + pPortData.PortType;
202
+ }
203
+ tmpElement.setAttribute('class', tmpClassName);
199
204
  tmpElement.setAttribute('cx', String(pPosition.x));
200
205
  tmpElement.setAttribute('cy', String(pPosition.y));
201
206
  // Apply config attributes (r, etc.)
@@ -206,6 +211,10 @@ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
206
211
  tmpElement.setAttribute('data-port-hash', pPortData.Hash);
207
212
  tmpElement.setAttribute('data-node-hash', pNodeHash);
208
213
  tmpElement.setAttribute('data-port-direction', pPortData.Direction);
214
+ if (pPortData.PortType)
215
+ {
216
+ tmpElement.setAttribute('data-port-type', pPortData.PortType);
217
+ }
209
218
  tmpElement.setAttribute('data-element-type', 'port');
210
219
  return tmpElement;
211
220
  }
@@ -376,7 +385,7 @@ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
376
385
 
377
386
  let tmpMarkup = '';
378
387
 
379
- // Normal connection arrowhead
388
+ // Normal connection arrowhead (default gray)
380
389
  tmpMarkup += '<marker id="flow-arrowhead-' + pViewIdentifier + '"'
381
390
  + ' markerWidth="' + tmpConnectionMarker.MarkerWidth + '"'
382
391
  + ' markerHeight="' + tmpConnectionMarker.MarkerHeight + '"'
@@ -386,6 +395,28 @@ class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
386
395
  + '<polygon points="' + tmpConnectionMarker.Points + '" fill="' + tmpConnectionMarker.Fill + '" />'
387
396
  + '</marker>';
388
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
+
389
420
  // Selected connection arrowhead
390
421
  tmpMarkup += '<marker id="flow-arrowhead-selected-' + pViewIdentifier + '"'
391
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;
@@ -24,6 +24,21 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
24
24
  let tmpSourcePos = this._FlowView.getPortPosition(pConnection.SourceNodeHash, pConnection.SourcePortHash);
25
25
  let tmpTargetPos = this._FlowView.getPortPosition(pConnection.TargetNodeHash, pConnection.TargetPortHash);
26
26
 
27
+ // Look up the source port's PortType for connection coloring
28
+ let tmpSourcePortType = null;
29
+ let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
30
+ if (tmpSourceNode && tmpSourceNode.Ports)
31
+ {
32
+ for (let i = 0; i < tmpSourceNode.Ports.length; i++)
33
+ {
34
+ if (tmpSourceNode.Ports[i].Hash === pConnection.SourcePortHash)
35
+ {
36
+ tmpSourcePortType = tmpSourceNode.Ports[i].PortType || null;
37
+ break;
38
+ }
39
+ }
40
+ }
41
+
27
42
  if (!tmpSourcePos || !tmpTargetPos) return;
28
43
 
29
44
  let tmpData = pConnection.Data || {};
@@ -69,6 +84,24 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
69
84
 
70
85
  let tmpViewIdentifier = this._FlowView.options.ViewIdentifier;
71
86
 
87
+ // Build the port-type CSS class suffix for connection coloring
88
+ let tmpConnTypeClass = tmpSourcePortType ? (' conn-type-' + tmpSourcePortType) : '';
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
+
72
105
  // Hit area (wider invisible path for easier selection)
73
106
  let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
74
107
  if (tmpShapeProvider)
@@ -78,6 +111,13 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
78
111
 
79
112
  let tmpPathElement = tmpShapeProvider.createConnectionPathElement(
80
113
  tmpPath, pConnection.Hash, pIsSelected, tmpViewIdentifier);
114
+ if (tmpConnTypeClass)
115
+ {
116
+ tmpPathElement.setAttribute('class',
117
+ (tmpPathElement.getAttribute('class') || '') + tmpConnTypeClass);
118
+ }
119
+ // Override the default arrowhead with the typed one
120
+ tmpPathElement.setAttribute('marker-end', 'url(#' + tmpArrowMarkerId + ')');
81
121
  if (tmpStrokeDashArray)
82
122
  {
83
123
  tmpPathElement.setAttribute('stroke-dasharray', tmpStrokeDashArray);
@@ -94,19 +134,11 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
94
134
  pConnectionsLayer.appendChild(tmpHitArea);
95
135
 
96
136
  let tmpPathElement = this._FlowView._SVGHelperProvider.createSVGElement('path');
97
- tmpPathElement.setAttribute('class', `pict-flow-connection ${pIsSelected ? 'selected' : ''}`);
137
+ tmpPathElement.setAttribute('class', `pict-flow-connection${tmpConnTypeClass} ${pIsSelected ? 'selected' : ''}`);
98
138
  tmpPathElement.setAttribute('d', tmpPath);
99
139
  tmpPathElement.setAttribute('data-connection-hash', pConnection.Hash);
100
140
  tmpPathElement.setAttribute('data-element-type', 'connection');
101
-
102
- if (pIsSelected)
103
- {
104
- tmpPathElement.setAttribute('marker-end', `url(#flow-arrowhead-selected-${tmpViewIdentifier})`);
105
- }
106
- else
107
- {
108
- tmpPathElement.setAttribute('marker-end', `url(#flow-arrowhead-${tmpViewIdentifier})`);
109
- }
141
+ tmpPathElement.setAttribute('marker-end', 'url(#' + tmpArrowMarkerId + ')');
110
142
 
111
143
  if (tmpStrokeDashArray)
112
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)
@@ -329,93 +479,30 @@ class PictViewFlowNode extends libPictView
329
479
  else
330
480
  {
331
481
  tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
332
- tmpCircle.setAttribute('class', `pict-flow-port ${tmpPort.Direction}`);
482
+ let tmpPortClass = `pict-flow-port ${tmpPort.Direction}`;
483
+ if (tmpPort.PortType)
484
+ {
485
+ tmpPortClass += ` port-type-${tmpPort.PortType}`;
486
+ }
487
+ tmpCircle.setAttribute('class', tmpPortClass);
333
488
  tmpCircle.setAttribute('cx', String(tmpPosition.x));
334
489
  tmpCircle.setAttribute('cy', String(tmpPosition.y));
335
490
  tmpCircle.setAttribute('r', '5');
336
491
  tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
337
492
  tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
338
493
  tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
494
+ if (tmpPort.PortType)
495
+ {
496
+ tmpCircle.setAttribute('data-port-type', tmpPort.PortType);
497
+ }
339
498
  tmpCircle.setAttribute('data-element-type', 'port');
340
499
  }
341
500
  pGroup.appendChild(tmpCircle);
342
501
 
343
- // Port label use the edge for alignment (all positions on the
344
- // same edge share the same label direction)
345
- if (tmpPort.Label)
502
+ // Port label text (on top of everything)
503
+ if (tmpLabelElement)
346
504
  {
347
- let tmpLabel = this._FlowView._SVGHelperProvider.createSVGElement('text');
348
- tmpLabel.setAttribute('class', 'pict-flow-port-label');
349
- tmpLabel.textContent = tmpPort.Label;
350
-
351
- // Base offset from port center; PortLabelPadding adds extra space
352
- let tmpLabelOffset = 12;
353
- let tmpPaddingExtra = tmpPortLabelPadding ? 8 : 0;
354
-
355
- // When PortLabelsOutside is true, labels render outside the node
356
- // boundary (away from center) instead of inside (toward center).
357
- // The direction multiplier flips the offset direction per edge.
358
- let tmpOutsideFlip = tmpPortLabelsOutside ? -1 : 1;
359
-
360
- if (tmpPortLabelsVertical)
361
- {
362
- switch (tmpEdge)
363
- {
364
- case 'left':
365
- tmpLabel.setAttribute('x', String(tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
366
- tmpLabel.setAttribute('y', String(tmpPosition.y));
367
- tmpLabel.setAttribute('text-anchor', 'middle');
368
- tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip}, ${tmpPosition.y})`);
369
- break;
370
- case 'right':
371
- tmpLabel.setAttribute('x', String(tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
372
- tmpLabel.setAttribute('y', String(tmpPosition.y));
373
- tmpLabel.setAttribute('text-anchor', 'middle');
374
- tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip}, ${tmpPosition.y})`);
375
- break;
376
- case 'top':
377
- tmpLabel.setAttribute('x', String(tmpPosition.x));
378
- tmpLabel.setAttribute('y', String(tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
379
- tmpLabel.setAttribute('text-anchor', 'middle');
380
- tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip})`);
381
- break;
382
- case 'bottom':
383
- tmpLabel.setAttribute('x', String(tmpPosition.x));
384
- tmpLabel.setAttribute('y', String(tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
385
- tmpLabel.setAttribute('text-anchor', 'middle');
386
- tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip})`);
387
- break;
388
- }
389
- }
390
- else
391
- {
392
- // Horizontal labels (default)
393
- switch (tmpEdge)
394
- {
395
- case 'left':
396
- tmpLabel.setAttribute('x', String(tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
397
- tmpLabel.setAttribute('y', String(tmpPosition.y));
398
- tmpLabel.setAttribute('text-anchor', tmpPortLabelsOutside ? 'end' : 'start');
399
- break;
400
- case 'right':
401
- tmpLabel.setAttribute('x', String(tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
402
- tmpLabel.setAttribute('y', String(tmpPosition.y));
403
- tmpLabel.setAttribute('text-anchor', tmpPortLabelsOutside ? 'start' : 'end');
404
- break;
405
- case 'top':
406
- tmpLabel.setAttribute('x', String(tmpPosition.x));
407
- tmpLabel.setAttribute('y', String(tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
408
- tmpLabel.setAttribute('text-anchor', 'middle');
409
- break;
410
- case 'bottom':
411
- tmpLabel.setAttribute('x', String(tmpPosition.x));
412
- tmpLabel.setAttribute('y', String(tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
413
- tmpLabel.setAttribute('text-anchor', 'middle');
414
- break;
415
- }
416
- }
417
- tmpLabel.setAttribute('dominant-baseline', 'central');
418
- pGroup.appendChild(tmpLabel);
505
+ pGroup.appendChild(tmpLabelElement);
419
506
  }
420
507
  }
421
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