pict-section-flow 0.0.3 → 0.0.6

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.3",
3
+ "version": "0.0.6",
4
4
  "description": "Pict Section Flow Diagram",
5
5
  "main": "source/Pict-Section-Flow.js",
6
6
  "scripts": {
@@ -11,12 +11,14 @@
11
11
  "author": "steven velozo <steven@velozo.com>",
12
12
  "license": "MIT",
13
13
  "dependencies": {
14
- "pict-view": "^1.0.67",
14
+ "fable-serviceproviderbase": "^3.0.19",
15
15
  "pict-provider": "^1.0.12",
16
- "fable-serviceproviderbase": "^3.0.19"
16
+ "pict-section-form": "^1.0.194",
17
+ "pict-view": "^1.0.67"
17
18
  },
18
19
  "devDependencies": {
19
- "pict": "^1.0.354",
20
- "quackage": "^1.0.59"
20
+ "pict": "^1.0.357",
21
+ "pict-router": "^1.0.6",
22
+ "quackage": "^1.0.64"
21
23
  }
22
24
  }
@@ -105,6 +105,7 @@ class PictFlowCard extends libFableServiceProviderBase
105
105
  this.cardPortLabelsOnHover = (typeof tmpOptions.PortLabelsOnHover === 'boolean') ? tmpOptions.PortLabelsOnHover : false;
106
106
  this.cardPortLabelsVertical = (typeof tmpOptions.PortLabelsVertical === 'boolean') ? tmpOptions.PortLabelsVertical : false;
107
107
  this.cardPortLabelPadding = (typeof tmpOptions.PortLabelPadding === 'boolean') ? tmpOptions.PortLabelPadding : false;
108
+ this.cardPortLabelsOutside = (typeof tmpOptions.PortLabelsOutside === 'boolean') ? tmpOptions.PortLabelsOutside : false;
108
109
  this.cardLabelsInFront = (typeof tmpOptions.LabelsInFront === 'boolean') ? tmpOptions.LabelsInFront : true;
109
110
  }
110
111
 
@@ -132,6 +133,10 @@ class PictFlowCard extends libFableServiceProviderBase
132
133
  MinimumInputCount: (typeof tmpInput.MinimumInputCount === 'number') ? tmpInput.MinimumInputCount : 0,
133
134
  MaximumInputCount: (typeof tmpInput.MaximumInputCount === 'number') ? tmpInput.MaximumInputCount : -1
134
135
  };
136
+ if (tmpInput.PortType)
137
+ {
138
+ tmpPort.PortType = tmpInput.PortType;
139
+ }
135
140
  tmpPorts.push(tmpPort);
136
141
  }
137
142
 
@@ -139,13 +144,18 @@ class PictFlowCard extends libFableServiceProviderBase
139
144
  for (let i = 0; i < this.cardOutputs.length; i++)
140
145
  {
141
146
  let tmpOutput = this.cardOutputs[i];
142
- tmpPorts.push(
147
+ let tmpOutPort =
143
148
  {
144
149
  Hash: null,
145
150
  Direction: 'output',
146
151
  Side: tmpOutput.Side || 'right',
147
152
  Label: tmpOutput.Name || `Out ${i + 1}`
148
- });
153
+ };
154
+ if (tmpOutput.PortType)
155
+ {
156
+ tmpOutPort.PortType = tmpOutput.PortType;
157
+ }
158
+ tmpPorts.push(tmpOutPort);
149
159
  }
150
160
 
151
161
  // If no ports were defined, provide sensible defaults
@@ -184,6 +194,7 @@ class PictFlowCard extends libFableServiceProviderBase
184
194
  tmpResult.PortLabelsOnHover = this.cardPortLabelsOnHover;
185
195
  tmpResult.PortLabelsVertical = this.cardPortLabelsVertical;
186
196
  tmpResult.PortLabelPadding = this.cardPortLabelPadding;
197
+ tmpResult.PortLabelsOutside = this.cardPortLabelsOutside;
187
198
  tmpResult.LabelsInFront = this.cardLabelsInFront;
188
199
 
189
200
  // Include properties panel config if defined
@@ -251,5 +262,6 @@ module.exports.default_configuration =
251
262
  PortLabelsOnHover: false,
252
263
  PortLabelsVertical: false,
253
264
  PortLabelPadding: false,
265
+ PortLabelsOutside: false,
254
266
  LabelsInFront: true
255
267
  };
@@ -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
@@ -67,7 +67,7 @@ class FlowCardPropertiesPanelTemplate extends libPictFlowCardPropertiesPanel
67
67
  if (!tmpTemplateHash) return;
68
68
 
69
69
  let tmpRecord = this._NodeData;
70
- let tmpHTML = this.fable.parseTemplate(tmpTemplateHash, tmpRecord, null, [tmpRecord]);
70
+ let tmpHTML = this.fable.parseTemplateByHash(tmpTemplateHash, tmpRecord, null, [tmpRecord]);
71
71
  this._ContentContainer.innerHTML = tmpHTML;
72
72
  }
73
73
  }
@@ -61,6 +61,18 @@ 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: #9b59b6;
69
+ --pf-port-error-fill: #e74c3c;
70
+
71
+ /* Connection Type Colors */
72
+ --pf-connection-event-stroke: #95a5a6;
73
+ --pf-connection-data-stroke: #9b59b6;
74
+ --pf-connection-error-stroke: #e74c3c;
75
+
64
76
  /* Panels */
65
77
  --pf-panel-bg: #ffffff;
66
78
  --pf-panel-border: #d0d4d8;
@@ -286,6 +298,22 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
286
298
  r: 7;
287
299
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.20));
288
300
  }
301
+ /* Port type color overrides */
302
+ .pict-flow-port.port-type-event-in {
303
+ fill: var(--pf-port-event-in-fill);
304
+ }
305
+ .pict-flow-port.port-type-event-out {
306
+ fill: var(--pf-port-event-out-fill);
307
+ }
308
+ .pict-flow-port.port-type-setting {
309
+ fill: var(--pf-port-setting-fill);
310
+ }
311
+ .pict-flow-port.port-type-value {
312
+ fill: var(--pf-port-value-fill);
313
+ }
314
+ .pict-flow-port.port-type-error {
315
+ fill: var(--pf-port-error-fill);
316
+ }
289
317
  .pict-flow-port-label {
290
318
  fill: #7f8c8d;
291
319
  font-size: 9px;
@@ -326,6 +354,16 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
326
354
  stroke: var(--pf-connection-selected-stroke);
327
355
  stroke-width: 3;
328
356
  }
357
+ /* Connection type color overrides (based on source port type) */
358
+ .pict-flow-connection.conn-type-value {
359
+ stroke: var(--pf-connection-data-stroke);
360
+ }
361
+ .pict-flow-connection.conn-type-setting {
362
+ stroke: var(--pf-connection-data-stroke);
363
+ }
364
+ .pict-flow-connection.conn-type-error {
365
+ stroke: var(--pf-connection-error-stroke);
366
+ }
329
367
  .pict-flow-connection-hitarea {
330
368
  fill: none;
331
369
  stroke: transparent;
@@ -591,6 +629,22 @@ class PictProviderFlowCSS extends libFableServiceProviderBase
591
629
  .pict-flow-info-panel-port.output {
592
630
  border-left: 3px solid var(--pf-port-output-fill);
593
631
  }
632
+ /* Info panel port type color overrides */
633
+ .pict-flow-info-panel-port.port-type-event-in {
634
+ border-left-color: var(--pf-port-event-in-fill);
635
+ }
636
+ .pict-flow-info-panel-port.port-type-event-out {
637
+ border-left-color: var(--pf-port-event-out-fill);
638
+ }
639
+ .pict-flow-info-panel-port.port-type-setting {
640
+ border-left-color: var(--pf-port-setting-fill);
641
+ }
642
+ .pict-flow-info-panel-port.port-type-value {
643
+ border-left-color: var(--pf-port-value-fill);
644
+ }
645
+ .pict-flow-info-panel-port.port-type-error {
646
+ border-left-color: var(--pf-port-error-fill);
647
+ }
594
648
  .pict-flow-info-panel-port-constraint {
595
649
  color: #8e99a4;
596
650
  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
  }
@@ -6,6 +6,16 @@ const libFableServiceProviderBase = require('fable-serviceproviderbase');
6
6
  * Shared geometry utilities for the flow diagram.
7
7
  * Provides direction vectors and edge center calculations used by
8
8
  * connections, tethers, and other flow components.
9
+ *
10
+ * Port Side values (12 positions):
11
+ *
12
+ * Top edge: 'top-left' 'top' 'top-right'
13
+ * Left edge: 'left-top' 'left' 'left-bottom'
14
+ * Right edge: 'right-top' 'right' 'right-bottom'
15
+ * Bottom edge: 'bottom-left' 'bottom' 'bottom-right'
16
+ *
17
+ * The old 4-value sides ('left', 'right', 'top', 'bottom') map to
18
+ * the middle position on each edge for backward compatibility.
9
19
  */
10
20
  class PictProviderFlowGeometry extends libFableServiceProviderBase
11
21
  {
@@ -16,15 +26,55 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
16
26
  this.serviceType = 'PictProviderFlowGeometry';
17
27
  }
18
28
 
29
+ /**
30
+ * Extract the edge name from a Side value.
31
+ *
32
+ * Maps all 12 positions (and the 4 legacy values) back to
33
+ * the edge they sit on: 'left', 'right', 'top', or 'bottom'.
34
+ *
35
+ * @param {string} pSide - Any valid Side value
36
+ * @returns {string} The edge: 'left', 'right', 'top', or 'bottom'
37
+ */
38
+ getEdgeFromSide(pSide)
39
+ {
40
+ switch (pSide)
41
+ {
42
+ case 'left-top':
43
+ case 'left':
44
+ case 'left-bottom':
45
+ return 'left';
46
+
47
+ case 'right-top':
48
+ case 'right':
49
+ case 'right-bottom':
50
+ return 'right';
51
+
52
+ case 'top-left':
53
+ case 'top':
54
+ case 'top-right':
55
+ return 'top';
56
+
57
+ case 'bottom-left':
58
+ case 'bottom':
59
+ case 'bottom-right':
60
+ return 'bottom';
61
+
62
+ default:
63
+ return 'right';
64
+ }
65
+ }
66
+
19
67
  /**
20
68
  * Get the outward unit direction vector for a given side.
21
69
  *
22
- * @param {string} pSide - 'left', 'right', 'top', or 'bottom'
70
+ * All positions on the same edge share the same direction vector.
71
+ *
72
+ * @param {string} pSide - Any valid Side value (12 positions or 4 legacy)
23
73
  * @returns {{dx: number, dy: number}}
24
74
  */
25
75
  sideDirection(pSide)
26
76
  {
27
- switch (pSide)
77
+ switch (this.getEdgeFromSide(pSide))
28
78
  {
29
79
  case 'left': return { dx: -1, dy: 0 };
30
80
  case 'right': return { dx: 1, dy: 0 };
@@ -63,12 +113,17 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
63
113
  /**
64
114
  * Calculate a port's local position relative to node origin.
65
115
  *
66
- * For left and right side ports, positioning is offset below the title bar
67
- * so that ports never overlap the header area.
116
+ * Supports 12 positions (3 zones per edge). For left/right edges,
117
+ * the body area below the title bar is divided into three vertical zones
118
+ * (start/middle/end). For top/bottom edges, the full width is divided
119
+ * into three horizontal zones.
68
120
  *
69
- * @param {string} pSide - 'left', 'right', 'top', 'bottom'
70
- * @param {number} pIndex - Index of this port on its side
71
- * @param {number} pTotal - Total ports on this side
121
+ * Multiple ports sharing the same Side value distribute evenly within
122
+ * their zone.
123
+ *
124
+ * @param {string} pSide - Side value (any of 12 positions or 4 legacy)
125
+ * @param {number} pIndex - Index of this port within its Side group
126
+ * @param {number} pTotal - Total ports with this Side value
72
127
  * @param {number} pWidth - Node width
73
128
  * @param {number} pHeight - Node height
74
129
  * @param {number} pTitleBarHeight - Height of the node title bar
@@ -76,30 +131,66 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
76
131
  */
77
132
  getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pTitleBarHeight)
78
133
  {
79
- let tmpSpacing;
134
+ let tmpEdge = this.getEdgeFromSide(pSide);
135
+ let tmpZone = this._getZoneFromSide(pSide);
136
+
137
+ if (tmpEdge === 'left' || tmpEdge === 'right')
138
+ {
139
+ let tmpX = (tmpEdge === 'left') ? 0 : pWidth;
140
+ let tmpBodyHeight = pHeight - pTitleBarHeight;
141
+ let tmpZoneStart = pTitleBarHeight + tmpBodyHeight * tmpZone.start;
142
+ let tmpZoneHeight = tmpBodyHeight * (tmpZone.end - tmpZone.start);
143
+ let tmpSpacing = tmpZoneHeight / (pTotal + 1);
144
+ let tmpY = tmpZoneStart + tmpSpacing * (pIndex + 1);
145
+ return { x: tmpX, y: tmpY };
146
+ }
80
147
 
148
+ // top or bottom
149
+ let tmpY = (tmpEdge === 'top') ? 0 : pHeight;
150
+ let tmpZoneStart = pWidth * tmpZone.start;
151
+ let tmpZoneWidth = pWidth * (tmpZone.end - tmpZone.start);
152
+ let tmpSpacing = tmpZoneWidth / (pTotal + 1);
153
+ let tmpX = tmpZoneStart + tmpSpacing * (pIndex + 1);
154
+ return { x: tmpX, y: tmpY };
155
+ }
156
+
157
+ /**
158
+ * Get the zone fraction (start, end) for a Side value.
159
+ *
160
+ * Each edge is divided into three zones of equal size:
161
+ * start: 0.0 — 0.333
162
+ * middle: 0.333 — 0.667
163
+ * end: 0.667 — 1.0
164
+ *
165
+ * @param {string} pSide
166
+ * @returns {{start: number, end: number}}
167
+ */
168
+ _getZoneFromSide(pSide)
169
+ {
81
170
  switch (pSide)
82
171
  {
83
- case 'left':
84
- {
85
- let tmpBodyHeight = pHeight - pTitleBarHeight;
86
- tmpSpacing = tmpBodyHeight / (pTotal + 1);
87
- return { x: 0, y: pTitleBarHeight + tmpSpacing * (pIndex + 1) };
88
- }
89
- case 'right':
90
- {
91
- let tmpBodyHeight = pHeight - pTitleBarHeight;
92
- tmpSpacing = tmpBodyHeight / (pTotal + 1);
93
- return { x: pWidth, y: pTitleBarHeight + tmpSpacing * (pIndex + 1) };
94
- }
95
- case 'top':
96
- tmpSpacing = pWidth / (pTotal + 1);
97
- return { x: tmpSpacing * (pIndex + 1), y: 0 };
98
- case 'bottom':
99
- tmpSpacing = pWidth / (pTotal + 1);
100
- return { x: tmpSpacing * (pIndex + 1), y: pHeight };
101
- default:
102
- return { x: pWidth, y: pHeight / 2 };
172
+ // Left edge: top, middle, bottom
173
+ case 'left-top': return { start: 0.0, end: 0.333 };
174
+ case 'left': return { start: 0.333, end: 0.667 };
175
+ case 'left-bottom': return { start: 0.667, end: 1.0 };
176
+
177
+ // Right edge: top, middle, bottom
178
+ case 'right-top': return { start: 0.0, end: 0.333 };
179
+ case 'right': return { start: 0.333, end: 0.667 };
180
+ case 'right-bottom': return { start: 0.667, end: 1.0 };
181
+
182
+ // Top edge: left, middle, right
183
+ case 'top-left': return { start: 0.0, end: 0.333 };
184
+ case 'top': return { start: 0.333, end: 0.667 };
185
+ case 'top-right': return { start: 0.667, end: 1.0 };
186
+
187
+ // Bottom edge: left, middle, right
188
+ case 'bottom-left': return { start: 0.0, end: 0.333 };
189
+ case 'bottom': return { start: 0.333, end: 0.667 };
190
+ case 'bottom-right': return { start: 0.667, end: 1.0 };
191
+
192
+ // Fallback: full range (legacy behavior)
193
+ default: return { start: 0.0, end: 1.0 };
103
194
  }
104
195
  }
105
196
  }
@@ -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,9 @@ 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
+
72
90
  // Hit area (wider invisible path for easier selection)
73
91
  let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
74
92
  if (tmpShapeProvider)
@@ -78,6 +96,11 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
78
96
 
79
97
  let tmpPathElement = tmpShapeProvider.createConnectionPathElement(
80
98
  tmpPath, pConnection.Hash, pIsSelected, tmpViewIdentifier);
99
+ if (tmpConnTypeClass)
100
+ {
101
+ tmpPathElement.setAttribute('class',
102
+ (tmpPathElement.getAttribute('class') || '') + tmpConnTypeClass);
103
+ }
81
104
  if (tmpStrokeDashArray)
82
105
  {
83
106
  tmpPathElement.setAttribute('stroke-dasharray', tmpStrokeDashArray);
@@ -94,7 +117,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
94
117
  pConnectionsLayer.appendChild(tmpHitArea);
95
118
 
96
119
  let tmpPathElement = this._FlowView._SVGHelperProvider.createSVGElement('path');
97
- tmpPathElement.setAttribute('class', `pict-flow-connection ${pIsSelected ? 'selected' : ''}`);
120
+ tmpPathElement.setAttribute('class', `pict-flow-connection${tmpConnTypeClass} ${pIsSelected ? 'selected' : ''}`);
98
121
  tmpPathElement.setAttribute('d', tmpPath);
99
122
  tmpPathElement.setAttribute('data-connection-hash', pConnection.Hash);
100
123
  tmpPathElement.setAttribute('data-element-type', 'connection');
@@ -292,22 +292,28 @@ class PictViewFlowNode extends libPictView
292
292
 
293
293
  let tmpPortLabelsVertical = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsVertical);
294
294
  let tmpPortLabelPadding = (pNodeTypeConfig && pNodeTypeConfig.PortLabelPadding);
295
+ let tmpPortLabelsOutside = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsOutside);
296
+ let tmpGeometryProvider = this._FlowView._GeometryProvider;
295
297
 
296
- // Group ports by side and direction for positioning
297
- let tmpPortsBySide = { left: [], right: [], top: [], bottom: [] };
298
+ // Group ports by their Side value (supports all 12 positions)
299
+ let tmpPortsBySide = {};
298
300
  for (let i = 0; i < pNodeData.Ports.length; i++)
299
301
  {
300
302
  let tmpPort = pNodeData.Ports[i];
301
303
  let tmpSide = tmpPort.Side || (tmpPort.Direction === 'input' ? 'left' : 'right');
302
- if (tmpPortsBySide[tmpSide])
304
+ if (!tmpPortsBySide[tmpSide])
303
305
  {
304
- tmpPortsBySide[tmpSide].push(tmpPort);
306
+ tmpPortsBySide[tmpSide] = [];
305
307
  }
308
+ tmpPortsBySide[tmpSide].push(tmpPort);
306
309
  }
307
310
 
308
311
  for (let tmpSide in tmpPortsBySide)
309
312
  {
310
313
  let tmpPorts = tmpPortsBySide[tmpSide];
314
+ // Determine the edge for label positioning
315
+ let tmpEdge = tmpGeometryProvider ? tmpGeometryProvider.getEdgeFromSide(tmpSide) : tmpSide;
316
+
311
317
  for (let i = 0; i < tmpPorts.length; i++)
312
318
  {
313
319
  let tmpPort = tmpPorts[i];
@@ -323,18 +329,28 @@ class PictViewFlowNode extends libPictView
323
329
  else
324
330
  {
325
331
  tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
326
- tmpCircle.setAttribute('class', `pict-flow-port ${tmpPort.Direction}`);
332
+ let tmpPortClass = `pict-flow-port ${tmpPort.Direction}`;
333
+ if (tmpPort.PortType)
334
+ {
335
+ tmpPortClass += ` port-type-${tmpPort.PortType}`;
336
+ }
337
+ tmpCircle.setAttribute('class', tmpPortClass);
327
338
  tmpCircle.setAttribute('cx', String(tmpPosition.x));
328
339
  tmpCircle.setAttribute('cy', String(tmpPosition.y));
329
340
  tmpCircle.setAttribute('r', '5');
330
341
  tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
331
342
  tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
332
343
  tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
344
+ if (tmpPort.PortType)
345
+ {
346
+ tmpCircle.setAttribute('data-port-type', tmpPort.PortType);
347
+ }
333
348
  tmpCircle.setAttribute('data-element-type', 'port');
334
349
  }
335
350
  pGroup.appendChild(tmpCircle);
336
351
 
337
- // Port label
352
+ // Port label — use the edge for alignment (all positions on the
353
+ // same edge share the same label direction)
338
354
  if (tmpPort.Label)
339
355
  {
340
356
  let tmpLabel = this._FlowView._SVGHelperProvider.createSVGElement('text');
@@ -345,62 +361,64 @@ class PictViewFlowNode extends libPictView
345
361
  let tmpLabelOffset = 12;
346
362
  let tmpPaddingExtra = tmpPortLabelPadding ? 8 : 0;
347
363
 
348
- if (tmpPortLabelsVertical)
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)
349
370
  {
350
- // Vertical labels: rotated -90° and centered on the port position.
351
- // After rotation, text-anchor controls vertical centering, so 'middle'
352
- // ensures the label is centered next to its port circle.
353
- switch (tmpSide)
371
+ switch (tmpEdge)
354
372
  {
355
373
  case 'left':
356
- tmpLabel.setAttribute('x', String(tmpPosition.x + tmpLabelOffset + tmpPaddingExtra));
374
+ tmpLabel.setAttribute('x', String(tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
357
375
  tmpLabel.setAttribute('y', String(tmpPosition.y));
358
376
  tmpLabel.setAttribute('text-anchor', 'middle');
359
- tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x + tmpLabelOffset + tmpPaddingExtra}, ${tmpPosition.y})`);
377
+ tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip}, ${tmpPosition.y})`);
360
378
  break;
361
379
  case 'right':
362
- tmpLabel.setAttribute('x', String(tmpPosition.x - tmpLabelOffset - tmpPaddingExtra));
380
+ tmpLabel.setAttribute('x', String(tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
363
381
  tmpLabel.setAttribute('y', String(tmpPosition.y));
364
382
  tmpLabel.setAttribute('text-anchor', 'middle');
365
- tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x - tmpLabelOffset - tmpPaddingExtra}, ${tmpPosition.y})`);
383
+ tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip}, ${tmpPosition.y})`);
366
384
  break;
367
385
  case 'top':
368
386
  tmpLabel.setAttribute('x', String(tmpPosition.x));
369
- tmpLabel.setAttribute('y', String(tmpPosition.y + tmpLabelOffset + tmpPaddingExtra));
387
+ tmpLabel.setAttribute('y', String(tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
370
388
  tmpLabel.setAttribute('text-anchor', 'middle');
371
- tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y + tmpLabelOffset + tmpPaddingExtra})`);
389
+ tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip})`);
372
390
  break;
373
391
  case 'bottom':
374
392
  tmpLabel.setAttribute('x', String(tmpPosition.x));
375
- tmpLabel.setAttribute('y', String(tmpPosition.y - tmpLabelOffset - tmpPaddingExtra));
393
+ tmpLabel.setAttribute('y', String(tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
376
394
  tmpLabel.setAttribute('text-anchor', 'middle');
377
- tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y - tmpLabelOffset - tmpPaddingExtra})`);
395
+ tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip})`);
378
396
  break;
379
397
  }
380
398
  }
381
399
  else
382
400
  {
383
401
  // Horizontal labels (default)
384
- switch (tmpSide)
402
+ switch (tmpEdge)
385
403
  {
386
404
  case 'left':
387
- tmpLabel.setAttribute('x', String(tmpPosition.x + tmpLabelOffset + tmpPaddingExtra));
405
+ tmpLabel.setAttribute('x', String(tmpPosition.x + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
388
406
  tmpLabel.setAttribute('y', String(tmpPosition.y));
389
- tmpLabel.setAttribute('text-anchor', 'start');
407
+ tmpLabel.setAttribute('text-anchor', tmpPortLabelsOutside ? 'end' : 'start');
390
408
  break;
391
409
  case 'right':
392
- tmpLabel.setAttribute('x', String(tmpPosition.x - tmpLabelOffset - tmpPaddingExtra));
410
+ tmpLabel.setAttribute('x', String(tmpPosition.x - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
393
411
  tmpLabel.setAttribute('y', String(tmpPosition.y));
394
- tmpLabel.setAttribute('text-anchor', 'end');
412
+ tmpLabel.setAttribute('text-anchor', tmpPortLabelsOutside ? 'start' : 'end');
395
413
  break;
396
414
  case 'top':
397
415
  tmpLabel.setAttribute('x', String(tmpPosition.x));
398
- tmpLabel.setAttribute('y', String(tmpPosition.y + tmpLabelOffset + tmpPaddingExtra));
416
+ tmpLabel.setAttribute('y', String(tmpPosition.y + (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
399
417
  tmpLabel.setAttribute('text-anchor', 'middle');
400
418
  break;
401
419
  case 'bottom':
402
420
  tmpLabel.setAttribute('x', String(tmpPosition.x));
403
- tmpLabel.setAttribute('y', String(tmpPosition.y - tmpLabelOffset - tmpPaddingExtra));
421
+ tmpLabel.setAttribute('y', String(tmpPosition.y - (tmpLabelOffset + tmpPaddingExtra) * tmpOutsideFlip));
404
422
  tmpLabel.setAttribute('text-anchor', 'middle');
405
423
  break;
406
424
  }