pict-section-flow 0.0.2 → 0.0.5

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.
Files changed (39) hide show
  1. package/.claude/launch.json +11 -0
  2. package/docs/README.md +51 -0
  3. package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +105 -0
  4. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +36 -0
  5. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +42 -0
  6. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +1 -1
  7. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +1 -1
  8. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +1 -1
  9. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +1 -1
  10. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +1 -1
  11. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +1 -1
  12. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +1 -1
  13. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +98 -0
  14. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +44 -0
  15. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +1 -1
  16. package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +9 -1
  17. package/package.json +7 -5
  18. package/source/Pict-Section-Flow.js +8 -1
  19. package/source/PictFlowCard.js +52 -1
  20. package/source/panels/FlowCardPropertiesPanel-Template.js +1 -1
  21. package/source/providers/PictProvider-Flow-CSS.js +1440 -0
  22. package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
  23. package/source/providers/PictProvider-Flow-Geometry.js +136 -2
  24. package/source/providers/PictProvider-Flow-Icons.js +335 -0
  25. package/source/providers/PictProvider-Flow-Layouts.js +214 -2
  26. package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
  27. package/source/providers/PictProvider-Flow-Noise.js +241 -0
  28. package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
  29. package/source/providers/PictProvider-Flow-Theme.js +755 -0
  30. package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
  31. package/source/services/PictService-Flow-PanelManager.js +188 -0
  32. package/source/services/PictService-Flow-SelectionManager.js +109 -0
  33. package/source/services/PictService-Flow-Tether.js +52 -25
  34. package/source/services/PictService-Flow-ViewportManager.js +176 -0
  35. package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
  36. package/source/views/PictView-Flow-Node.js +668 -174
  37. package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
  38. package/source/views/PictView-Flow-Toolbar.js +846 -379
  39. package/source/views/PictView-Flow.js +279 -671
@@ -32,7 +32,15 @@ class PictViewFlowNode extends libPictView
32
32
  renderNode(pNodeData, pNodesLayer, pIsSelected, pNodeTypeConfig)
33
33
  {
34
34
  let tmpGroup = this._FlowView._SVGHelperProvider.createSVGElement('g');
35
- tmpGroup.setAttribute('class', `pict-flow-node ${pIsSelected ? 'selected' : ''} pict-flow-node-${pNodeData.Type || 'default'}`);
35
+
36
+ // Build CSS class list with optional per-type modifier classes
37
+ let tmpClassList = `pict-flow-node ${pIsSelected ? 'selected' : ''} pict-flow-node-${pNodeData.Type || 'default'}`;
38
+ if (pNodeTypeConfig)
39
+ {
40
+ if (pNodeTypeConfig.PortLabelsOnHover) tmpClassList += ' pict-flow-node-port-labels-hover';
41
+ if (pNodeTypeConfig.PortLabelsVertical) tmpClassList += ' pict-flow-node-port-labels-vertical';
42
+ }
43
+ tmpGroup.setAttribute('class', tmpClassList);
36
44
  tmpGroup.setAttribute('transform', `translate(${pNodeData.X}, ${pNodeData.Y})`);
37
45
  tmpGroup.setAttribute('data-node-hash', pNodeData.Hash);
38
46
  tmpGroup.setAttribute('data-element-type', 'node');
@@ -41,167 +49,224 @@ class PictViewFlowNode extends libPictView
41
49
  let tmpHeight = pNodeData.Height || 80;
42
50
  let tmpTitleBarHeight = this.options.NodeTitleBarHeight;
43
51
 
44
- // Node body (main rectangle)
45
- let tmpBody = this._FlowView._SVGHelperProvider.createSVGElement('rect');
46
- tmpBody.setAttribute('class', 'pict-flow-node-body');
47
- tmpBody.setAttribute('x', '0');
48
- tmpBody.setAttribute('y', '0');
49
- tmpBody.setAttribute('width', String(tmpWidth));
50
- tmpBody.setAttribute('height', String(tmpHeight));
51
- tmpBody.setAttribute('data-node-hash', pNodeData.Hash);
52
- tmpBody.setAttribute('data-element-type', 'node-body');
53
-
54
- // Apply custom styles from node type
55
- if (pNodeTypeConfig && pNodeTypeConfig.BodyStyle)
52
+ // Determine node body mode from theme (bracket vs rect)
53
+ let tmpNodeBodyMode = 'rect';
54
+ if (this._FlowView._ThemeProvider)
56
55
  {
57
- for (let tmpStyleKey in pNodeTypeConfig.BodyStyle)
56
+ let tmpActiveTheme = this._FlowView._ThemeProvider.getActiveTheme();
57
+ if (tmpActiveTheme && tmpActiveTheme.NodeBodyMode)
58
58
  {
59
- tmpBody.setAttribute(tmpStyleKey, pNodeTypeConfig.BodyStyle[tmpStyleKey]);
59
+ tmpNodeBodyMode = tmpActiveTheme.NodeBodyMode;
60
60
  }
61
61
  }
62
62
 
63
- tmpGroup.appendChild(tmpBody);
64
-
65
- // Title bar background (top portion)
66
- let tmpTitleBar = this._FlowView._SVGHelperProvider.createSVGElement('rect');
67
- tmpTitleBar.setAttribute('class', 'pict-flow-node-title-bar');
68
- tmpTitleBar.setAttribute('x', '0');
69
- tmpTitleBar.setAttribute('y', '0');
70
- tmpTitleBar.setAttribute('width', String(tmpWidth));
71
- tmpTitleBar.setAttribute('height', String(tmpTitleBarHeight));
72
- tmpTitleBar.setAttribute('data-node-hash', pNodeData.Hash);
73
- tmpTitleBar.setAttribute('data-element-type', 'node-body');
74
-
75
- // Apply custom title bar color
76
- if (pNodeTypeConfig && pNodeTypeConfig.TitleBarColor)
63
+ if (tmpNodeBodyMode === 'bracket')
77
64
  {
78
- tmpTitleBar.setAttribute('fill', pNodeTypeConfig.TitleBarColor);
65
+ this._renderBracketNodeBody(tmpGroup, pNodeData, tmpWidth, tmpHeight, tmpTitleBarHeight, pNodeTypeConfig);
66
+ }
67
+ else
68
+ {
69
+ this._renderRectNodeBody(tmpGroup, pNodeData, tmpWidth, tmpHeight, tmpTitleBarHeight, pNodeTypeConfig);
79
70
  }
80
71
 
81
- tmpGroup.appendChild(tmpTitleBar);
72
+ // Determine if this node has a title-bar icon (FlowCard with CardMetadata)
73
+ let tmpHasTitleIcon = false;
74
+ let tmpTitleIconSize = 12;
75
+ let tmpTitleIconMarginLeft = 8;
76
+ let tmpTitleIconGap = 4;
82
77
 
83
- // Title bar bottom fill (to square off the rounded corners at the bottom of the title bar)
84
- let tmpTitleBarBottom = this._FlowView._SVGHelperProvider.createSVGElement('rect');
85
- tmpTitleBarBottom.setAttribute('class', 'pict-flow-node-title-bar-bottom');
86
- tmpTitleBarBottom.setAttribute('x', '0');
87
- tmpTitleBarBottom.setAttribute('y', String(tmpTitleBarHeight - 6));
88
- tmpTitleBarBottom.setAttribute('width', String(tmpWidth));
89
- tmpTitleBarBottom.setAttribute('height', '6');
90
- tmpTitleBarBottom.setAttribute('data-node-hash', pNodeData.Hash);
91
- tmpTitleBarBottom.setAttribute('data-element-type', 'node-body');
92
-
93
- if (pNodeTypeConfig && pNodeTypeConfig.TitleBarColor)
78
+ if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata)
94
79
  {
95
- tmpTitleBarBottom.setAttribute('fill', pNodeTypeConfig.TitleBarColor);
80
+ let tmpMeta = pNodeTypeConfig.CardMetadata;
81
+ let tmpIconProvider = this._FlowView._IconProvider;
82
+ if (tmpMeta.Icon || tmpIconProvider)
83
+ {
84
+ tmpHasTitleIcon = true;
85
+ }
96
86
  }
97
87
 
98
- tmpGroup.appendChild(tmpTitleBarBottom);
99
-
100
- // Title text
88
+ // Title text (position adjusts when a title-bar icon is present)
101
89
  let tmpTitle = this._FlowView._SVGHelperProvider.createSVGElement('text');
102
90
  tmpTitle.setAttribute('class', 'pict-flow-node-title');
103
- tmpTitle.setAttribute('x', String(tmpWidth / 2));
91
+ if (tmpHasTitleIcon)
92
+ {
93
+ tmpTitle.setAttribute('x', String(tmpTitleIconMarginLeft + tmpTitleIconSize + tmpTitleIconGap));
94
+ tmpTitle.setAttribute('text-anchor', 'start');
95
+ }
96
+ else
97
+ {
98
+ tmpTitle.setAttribute('x', String(tmpWidth / 2));
99
+ tmpTitle.setAttribute('text-anchor', 'middle');
100
+ }
104
101
  tmpTitle.setAttribute('y', String(tmpTitleBarHeight / 2 + 1));
105
- tmpTitle.setAttribute('text-anchor', 'middle');
106
102
  tmpTitle.setAttribute('dominant-baseline', 'central');
107
103
  tmpTitle.textContent = pNodeData.Title || 'Untitled';
108
104
  tmpGroup.appendChild(tmpTitle);
109
105
 
110
- // Type label (below title bar)
111
- if (pNodeTypeConfig && pNodeTypeConfig.Label && pNodeTypeConfig.Label !== pNodeData.Title)
112
- {
113
- let tmpTypeLabel = this._FlowView._SVGHelperProvider.createSVGElement('text');
114
- tmpTypeLabel.setAttribute('class', 'pict-flow-node-type-label');
115
- tmpTypeLabel.setAttribute('x', String(tmpWidth / 2));
116
- tmpTypeLabel.setAttribute('y', String(tmpTitleBarHeight + 18));
117
- tmpTypeLabel.setAttribute('text-anchor', 'middle');
118
- tmpTypeLabel.setAttribute('dominant-baseline', 'central');
119
- tmpTypeLabel.textContent = pNodeTypeConfig.Label;
120
- tmpGroup.appendChild(tmpTypeLabel);
121
- }
106
+ // Determine whether labels should be rendered
107
+ let tmpShowTypeLabel = (!pNodeTypeConfig || pNodeTypeConfig.ShowTypeLabel !== false);
108
+ let tmpLabelsInFront = (!pNodeTypeConfig || pNodeTypeConfig.LabelsInFront !== false);
122
109
 
123
- // FlowCard metadata: render icon and code in the node body
124
- if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata)
110
+ // Helper: render type label + code badge + tooltip (the "middle labels")
111
+ let tmpRenderTypeLabels = () =>
125
112
  {
126
- let tmpMeta = pNodeTypeConfig.CardMetadata;
127
- let tmpBodyCenterY = tmpTitleBarHeight + (tmpHeight - tmpTitleBarHeight) / 2;
113
+ // Type label (below title bar — hover-only for FlowCard nodes via CSS)
114
+ if (tmpShowTypeLabel && pNodeTypeConfig && pNodeTypeConfig.Label && pNodeTypeConfig.Label !== pNodeData.Title)
115
+ {
116
+ let tmpTypeLabel = this._FlowView._SVGHelperProvider.createSVGElement('text');
117
+ tmpTypeLabel.setAttribute('class', 'pict-flow-node-type-label');
118
+ tmpTypeLabel.setAttribute('x', String(tmpWidth / 2));
119
+ tmpTypeLabel.setAttribute('y', String(tmpTitleBarHeight + 16));
120
+ tmpTypeLabel.setAttribute('text-anchor', 'middle');
121
+ tmpTypeLabel.setAttribute('dominant-baseline', 'central');
122
+ tmpTypeLabel.textContent = pNodeTypeConfig.Label;
123
+ tmpGroup.appendChild(tmpTypeLabel);
124
+ }
128
125
 
129
- // Icon (displayed as text, left-of-center or centered if no code)
130
- if (tmpMeta.Icon)
126
+ // FlowCard metadata: icon in title bar, code badge in body (hover-only via CSS)
127
+ if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata)
131
128
  {
132
- let tmpIconText = this._FlowView._SVGHelperProvider.createSVGElement('text');
133
- tmpIconText.setAttribute('class', 'pict-flow-node-card-icon');
134
- tmpIconText.setAttribute('font-size', '16');
135
- tmpIconText.setAttribute('text-anchor', 'middle');
136
- tmpIconText.setAttribute('dominant-baseline', 'central');
137
- tmpIconText.setAttribute('pointer-events', 'none');
138
-
139
- if (tmpMeta.Code)
129
+ let tmpMeta = pNodeTypeConfig.CardMetadata;
130
+ let tmpIconProvider = this._FlowView._IconProvider;
131
+ let tmpTitleIconRendered = false;
132
+
133
+ // Icon position in title bar (vertically centered)
134
+ let tmpIconX = tmpTitleIconMarginLeft;
135
+ let tmpIconY = (tmpTitleBarHeight - tmpTitleIconSize) / 2;
136
+
137
+ if (tmpMeta.Icon && tmpIconProvider && !tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
138
+ {
139
+ // SVG icon via the icon provider — rendered into title bar
140
+ let tmpResolvedKey = tmpIconProvider.resolveIconKey(tmpMeta);
141
+ let tmpIconGroup = tmpIconProvider.renderIconIntoSVGGroup(
142
+ tmpResolvedKey, tmpGroup,
143
+ tmpIconX, tmpIconY,
144
+ tmpTitleIconSize);
145
+ if (tmpIconGroup)
146
+ {
147
+ tmpIconGroup.setAttribute('class',
148
+ (tmpIconGroup.getAttribute('class') || '') + ' pict-flow-node-title-icon');
149
+ }
150
+ tmpTitleIconRendered = true;
151
+ }
152
+ else if (tmpMeta.Icon && tmpIconProvider && tmpIconProvider.isEmojiIcon(tmpMeta.Icon))
140
153
  {
141
- // Icon on the left, code on the right
142
- tmpIconText.setAttribute('x', String(tmpWidth * 0.33));
154
+ // Emoji icon in title bar
155
+ let tmpIconText = this._FlowView._SVGHelperProvider.createSVGElement('text');
156
+ tmpIconText.setAttribute('class', 'pict-flow-node-card-icon pict-flow-node-title-icon-emoji');
157
+ tmpIconText.setAttribute('font-size', String(tmpTitleIconSize));
158
+ tmpIconText.setAttribute('text-anchor', 'middle');
159
+ tmpIconText.setAttribute('dominant-baseline', 'central');
160
+ tmpIconText.setAttribute('pointer-events', 'none');
161
+ tmpIconText.setAttribute('x', String(tmpIconX + tmpTitleIconSize / 2));
162
+ tmpIconText.setAttribute('y', String(tmpTitleBarHeight / 2));
163
+ tmpIconText.textContent = tmpMeta.Icon;
164
+ tmpGroup.appendChild(tmpIconText);
165
+ tmpTitleIconRendered = true;
143
166
  }
144
- else
167
+ else if (tmpMeta.Icon)
145
168
  {
146
- tmpIconText.setAttribute('x', String(tmpWidth / 2));
169
+ // No icon provider — text fallback in title bar
170
+ let tmpIconText = this._FlowView._SVGHelperProvider.createSVGElement('text');
171
+ tmpIconText.setAttribute('class', 'pict-flow-node-card-icon pict-flow-node-title-icon-emoji');
172
+ tmpIconText.setAttribute('font-size', String(tmpTitleIconSize));
173
+ tmpIconText.setAttribute('text-anchor', 'middle');
174
+ tmpIconText.setAttribute('dominant-baseline', 'central');
175
+ tmpIconText.setAttribute('pointer-events', 'none');
176
+ tmpIconText.setAttribute('x', String(tmpIconX + tmpTitleIconSize / 2));
177
+ tmpIconText.setAttribute('y', String(tmpTitleBarHeight / 2));
178
+ tmpIconText.textContent = tmpMeta.Icon;
179
+ tmpGroup.appendChild(tmpIconText);
180
+ tmpTitleIconRendered = true;
147
181
  }
148
- tmpIconText.setAttribute('y', String(tmpBodyCenterY));
149
- tmpIconText.textContent = tmpMeta.Icon;
150
- tmpGroup.appendChild(tmpIconText);
151
- }
152
182
 
153
- // Code badge (displayed as monospace text)
154
- if (tmpMeta.Code)
155
- {
156
- let tmpCodeText = this._FlowView._SVGHelperProvider.createSVGElement('text');
157
- tmpCodeText.setAttribute('class', 'pict-flow-node-card-code');
158
- tmpCodeText.setAttribute('font-size', '10');
159
- tmpCodeText.setAttribute('font-family', 'monospace');
160
- tmpCodeText.setAttribute('fill', '#7f8c8d');
161
- tmpCodeText.setAttribute('text-anchor', 'middle');
162
- tmpCodeText.setAttribute('dominant-baseline', 'central');
163
- tmpCodeText.setAttribute('pointer-events', 'none');
164
-
165
- if (tmpMeta.Icon)
183
+ // Default fallback icon in title bar
184
+ if (!tmpTitleIconRendered && tmpIconProvider)
166
185
  {
167
- tmpCodeText.setAttribute('x', String(tmpWidth * 0.67));
186
+ let tmpIconGroup = tmpIconProvider.renderIconIntoSVGGroup(
187
+ 'default', tmpGroup,
188
+ tmpIconX, tmpIconY,
189
+ tmpTitleIconSize);
190
+ if (tmpIconGroup)
191
+ {
192
+ tmpIconGroup.setAttribute('class',
193
+ (tmpIconGroup.getAttribute('class') || '') + ' pict-flow-node-title-icon');
194
+ }
168
195
  }
169
- else
196
+
197
+ // Code badge in body (hover-only via CSS, skipped when ShowTypeLabel is false)
198
+ let tmpBodyCenterY = tmpTitleBarHeight + (tmpHeight - tmpTitleBarHeight) / 2;
199
+ if (tmpShowTypeLabel && tmpMeta.Code)
170
200
  {
201
+ let tmpCodeText = this._FlowView._SVGHelperProvider.createSVGElement('text');
202
+ tmpCodeText.setAttribute('class', 'pict-flow-node-card-code');
203
+ tmpCodeText.setAttribute('font-size', '10');
204
+ tmpCodeText.setAttribute('font-family', 'monospace');
205
+ tmpCodeText.setAttribute('fill', '#7f8c8d');
206
+ tmpCodeText.setAttribute('text-anchor', 'middle');
207
+ tmpCodeText.setAttribute('dominant-baseline', 'central');
208
+ tmpCodeText.setAttribute('pointer-events', 'none');
171
209
  tmpCodeText.setAttribute('x', String(tmpWidth / 2));
210
+ tmpCodeText.setAttribute('y', String(tmpBodyCenterY));
211
+ tmpCodeText.textContent = tmpMeta.Code;
212
+ tmpGroup.appendChild(tmpCodeText);
172
213
  }
173
- tmpCodeText.setAttribute('y', String(tmpBodyCenterY));
174
- tmpCodeText.textContent = tmpMeta.Code;
175
- tmpGroup.appendChild(tmpCodeText);
176
- }
177
214
 
178
- // Tooltip via SVG <title> element
179
- if (tmpMeta.Tooltip || tmpMeta.Description)
180
- {
181
- let tmpSVGTitle = this._FlowView._SVGHelperProvider.createSVGElement('title');
182
- tmpSVGTitle.textContent = tmpMeta.Tooltip || tmpMeta.Description;
183
- tmpGroup.appendChild(tmpSVGTitle);
215
+ // Tooltip via SVG <title> element
216
+ if (tmpMeta.Tooltip || tmpMeta.Description)
217
+ {
218
+ let tmpSVGTitle = this._FlowView._SVGHelperProvider.createSVGElement('title');
219
+ tmpSVGTitle.textContent = tmpMeta.Tooltip || tmpMeta.Description;
220
+ tmpGroup.appendChild(tmpSVGTitle);
221
+ }
184
222
  }
185
- }
223
+ };
186
224
 
187
- // Render ports
188
- this._renderPorts(pNodeData, tmpGroup, tmpWidth, tmpHeight);
225
+ // Render order depends on LabelsInFront:
226
+ // true (default): body content first, then labels + ports (labels on top)
227
+ // false: labels + ports first, then body content (content on top)
228
+ if (tmpLabelsInFront)
229
+ {
230
+ this._renderBodyContent(pNodeData, tmpGroup, tmpWidth, tmpHeight, pNodeTypeConfig);
231
+ tmpRenderTypeLabels();
232
+ this._renderPorts(pNodeData, tmpGroup, tmpWidth, tmpHeight, pNodeTypeConfig);
233
+ }
234
+ else
235
+ {
236
+ tmpRenderTypeLabels();
237
+ this._renderPorts(pNodeData, tmpGroup, tmpWidth, tmpHeight, pNodeTypeConfig);
238
+ this._renderBodyContent(pNodeData, tmpGroup, tmpWidth, tmpHeight, pNodeTypeConfig);
239
+ }
189
240
 
190
241
  // Panel indicator icon (small rect in bottom-right corner)
191
242
  if (pNodeTypeConfig && pNodeTypeConfig.PropertiesPanel)
192
243
  {
193
244
  let tmpIndicatorSize = 10;
194
245
  let tmpIndicatorMargin = 4;
195
- let tmpIndicator = this._FlowView._SVGHelperProvider.createSVGElement('rect');
196
- tmpIndicator.setAttribute('class', 'pict-flow-node-panel-indicator');
197
- tmpIndicator.setAttribute('x', String(tmpWidth - tmpIndicatorSize - tmpIndicatorMargin));
198
- tmpIndicator.setAttribute('y', String(tmpHeight - tmpIndicatorSize - tmpIndicatorMargin));
199
- tmpIndicator.setAttribute('width', String(tmpIndicatorSize));
200
- tmpIndicator.setAttribute('height', String(tmpIndicatorSize));
201
- tmpIndicator.setAttribute('rx', '2');
202
- tmpIndicator.setAttribute('ry', '2');
203
- tmpIndicator.setAttribute('data-node-hash', pNodeData.Hash);
204
- tmpIndicator.setAttribute('data-element-type', 'panel-indicator');
246
+ let tmpIndicatorX = tmpWidth - tmpIndicatorSize - tmpIndicatorMargin;
247
+ let tmpIndicatorY = tmpHeight - tmpIndicatorSize - tmpIndicatorMargin;
248
+ let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
249
+ let tmpIndicator;
250
+
251
+ if (tmpShapeProvider)
252
+ {
253
+ tmpIndicator = tmpShapeProvider.createPanelIndicatorElement(
254
+ pNodeData.Hash, tmpIndicatorX, tmpIndicatorY,
255
+ tmpIndicatorSize, tmpIndicatorSize);
256
+ }
257
+ else
258
+ {
259
+ tmpIndicator = this._FlowView._SVGHelperProvider.createSVGElement('rect');
260
+ tmpIndicator.setAttribute('class', 'pict-flow-node-panel-indicator');
261
+ tmpIndicator.setAttribute('x', String(tmpIndicatorX));
262
+ tmpIndicator.setAttribute('y', String(tmpIndicatorY));
263
+ tmpIndicator.setAttribute('width', String(tmpIndicatorSize));
264
+ tmpIndicator.setAttribute('height', String(tmpIndicatorSize));
265
+ tmpIndicator.setAttribute('rx', '2');
266
+ tmpIndicator.setAttribute('ry', '2');
267
+ tmpIndicator.setAttribute('data-node-hash', pNodeData.Hash);
268
+ tmpIndicator.setAttribute('data-element-type', 'panel-indicator');
269
+ }
205
270
 
206
271
  let tmpIndicatorTitle = this._FlowView._SVGHelperProvider.createSVGElement('title');
207
272
  tmpIndicatorTitle.textContent = 'Double-click to open properties';
@@ -219,73 +284,135 @@ class PictViewFlowNode extends libPictView
219
284
  * @param {SVGGElement} pGroup - The node's SVG group
220
285
  * @param {number} pWidth
221
286
  * @param {number} pHeight
287
+ * @param {Object} [pNodeTypeConfig] - Node type configuration (for label display options)
222
288
  */
223
- _renderPorts(pNodeData, pGroup, pWidth, pHeight)
289
+ _renderPorts(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig)
224
290
  {
225
291
  if (!pNodeData.Ports || !Array.isArray(pNodeData.Ports)) return;
226
292
 
227
- // Group ports by side and direction for positioning
228
- let tmpPortsBySide = { left: [], right: [], top: [], bottom: [] };
293
+ let tmpPortLabelsVertical = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsVertical);
294
+ let tmpPortLabelPadding = (pNodeTypeConfig && pNodeTypeConfig.PortLabelPadding);
295
+ let tmpPortLabelsOutside = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsOutside);
296
+ let tmpGeometryProvider = this._FlowView._GeometryProvider;
297
+
298
+ // Group ports by their Side value (supports all 12 positions)
299
+ let tmpPortsBySide = {};
229
300
  for (let i = 0; i < pNodeData.Ports.length; i++)
230
301
  {
231
302
  let tmpPort = pNodeData.Ports[i];
232
303
  let tmpSide = tmpPort.Side || (tmpPort.Direction === 'input' ? 'left' : 'right');
233
- if (tmpPortsBySide[tmpSide])
304
+ if (!tmpPortsBySide[tmpSide])
234
305
  {
235
- tmpPortsBySide[tmpSide].push(tmpPort);
306
+ tmpPortsBySide[tmpSide] = [];
236
307
  }
308
+ tmpPortsBySide[tmpSide].push(tmpPort);
237
309
  }
238
310
 
239
311
  for (let tmpSide in tmpPortsBySide)
240
312
  {
241
313
  let tmpPorts = tmpPortsBySide[tmpSide];
314
+ // Determine the edge for label positioning
315
+ let tmpEdge = tmpGeometryProvider ? tmpGeometryProvider.getEdgeFromSide(tmpSide) : tmpSide;
316
+
242
317
  for (let i = 0; i < tmpPorts.length; i++)
243
318
  {
244
319
  let tmpPort = tmpPorts[i];
245
320
  let tmpPosition = this._getPortLocalPosition(tmpSide, i, tmpPorts.length, pWidth, pHeight);
246
321
 
247
322
  // Port circle
248
- let tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
249
- tmpCircle.setAttribute('class', `pict-flow-port ${tmpPort.Direction}`);
250
- tmpCircle.setAttribute('cx', String(tmpPosition.x));
251
- tmpCircle.setAttribute('cy', String(tmpPosition.y));
252
- tmpCircle.setAttribute('r', '5');
253
- tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
254
- tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
255
- tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
256
- tmpCircle.setAttribute('data-element-type', 'port');
323
+ let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
324
+ let tmpCircle;
325
+ if (tmpShapeProvider)
326
+ {
327
+ tmpCircle = tmpShapeProvider.createPortElement(tmpPort, tmpPosition, pNodeData.Hash);
328
+ }
329
+ else
330
+ {
331
+ tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
332
+ tmpCircle.setAttribute('class', `pict-flow-port ${tmpPort.Direction}`);
333
+ tmpCircle.setAttribute('cx', String(tmpPosition.x));
334
+ tmpCircle.setAttribute('cy', String(tmpPosition.y));
335
+ tmpCircle.setAttribute('r', '5');
336
+ tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
337
+ tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
338
+ tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
339
+ tmpCircle.setAttribute('data-element-type', 'port');
340
+ }
257
341
  pGroup.appendChild(tmpCircle);
258
342
 
259
- // Port label
343
+ // Port label — use the edge for alignment (all positions on the
344
+ // same edge share the same label direction)
260
345
  if (tmpPort.Label)
261
346
  {
262
347
  let tmpLabel = this._FlowView._SVGHelperProvider.createSVGElement('text');
263
348
  tmpLabel.setAttribute('class', 'pict-flow-port-label');
264
349
  tmpLabel.textContent = tmpPort.Label;
265
350
 
351
+ // Base offset from port center; PortLabelPadding adds extra space
266
352
  let tmpLabelOffset = 12;
267
- switch (tmpSide)
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)
268
361
  {
269
- case 'left':
270
- tmpLabel.setAttribute('x', String(tmpPosition.x + tmpLabelOffset));
271
- tmpLabel.setAttribute('y', String(tmpPosition.y));
272
- tmpLabel.setAttribute('text-anchor', 'start');
273
- break;
274
- case 'right':
275
- tmpLabel.setAttribute('x', String(tmpPosition.x - tmpLabelOffset));
276
- tmpLabel.setAttribute('y', String(tmpPosition.y));
277
- tmpLabel.setAttribute('text-anchor', 'end');
278
- break;
279
- case 'top':
280
- tmpLabel.setAttribute('x', String(tmpPosition.x));
281
- tmpLabel.setAttribute('y', String(tmpPosition.y + tmpLabelOffset));
282
- tmpLabel.setAttribute('text-anchor', 'middle');
283
- break;
284
- case 'bottom':
285
- tmpLabel.setAttribute('x', String(tmpPosition.x));
286
- tmpLabel.setAttribute('y', String(tmpPosition.y - tmpLabelOffset));
287
- tmpLabel.setAttribute('text-anchor', 'middle');
288
- break;
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
+ }
289
416
  }
290
417
  tmpLabel.setAttribute('dominant-baseline', 'central');
291
418
  pGroup.appendChild(tmpLabel);
@@ -309,33 +436,400 @@ class PictViewFlowNode extends libPictView
309
436
  */
310
437
  _getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight)
311
438
  {
312
- let tmpSpacing;
439
+ return this._FlowView._GeometryProvider.getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, this.options.NodeTitleBarHeight);
440
+ }
441
+
442
+ /**
443
+ * Render custom body content for a node (svg, html, or canvas).
444
+ *
445
+ * Checks for a BodyContent configuration on the node type and renders
446
+ * the appropriate content type into the node's SVG group.
447
+ *
448
+ * @param {Object} pNodeData - The node data object
449
+ * @param {SVGGElement} pGroup - The node's SVG group
450
+ * @param {number} pWidth - Node width
451
+ * @param {number} pHeight - Node height
452
+ * @param {Object} pNodeTypeConfig - The node type configuration
453
+ */
454
+ _renderBodyContent(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig)
455
+ {
456
+ if (!pNodeTypeConfig || !pNodeTypeConfig.BodyContent) return;
457
+
458
+ let tmpBodyContent = pNodeTypeConfig.BodyContent;
459
+ let tmpContentType = tmpBodyContent.ContentType;
460
+ if (!tmpContentType) return;
461
+
313
462
  let tmpTitleBarHeight = this.options.NodeTitleBarHeight;
463
+ let tmpPadding = (typeof tmpBodyContent.Padding === 'number') ? tmpBodyContent.Padding : 2;
464
+ let tmpBodyBounds =
465
+ {
466
+ x: tmpPadding,
467
+ y: tmpTitleBarHeight + tmpPadding,
468
+ width: pWidth - (tmpPadding * 2),
469
+ height: pHeight - tmpTitleBarHeight - (tmpPadding * 2)
470
+ };
471
+
472
+ let tmpPict = this._FlowView.pict || this.pict;
314
473
 
315
- switch (pSide)
474
+ // Register any templates defined in the BodyContent config (once)
475
+ if (tmpBodyContent.Templates && Array.isArray(tmpBodyContent.Templates))
316
476
  {
317
- case 'left':
477
+ if (!this._registeredBodyTemplates)
318
478
  {
319
- // Distribute ports in the body area below the title bar
320
- let tmpBodyHeight = pHeight - tmpTitleBarHeight;
321
- tmpSpacing = tmpBodyHeight / (pTotal + 1);
322
- return { x: 0, y: tmpTitleBarHeight + tmpSpacing * (pIndex + 1) };
479
+ this._registeredBodyTemplates = new Set();
323
480
  }
324
- case 'right':
481
+ for (let i = 0; i < tmpBodyContent.Templates.length; i++)
325
482
  {
326
- let tmpBodyHeight = pHeight - tmpTitleBarHeight;
327
- tmpSpacing = tmpBodyHeight / (pTotal + 1);
328
- return { x: pWidth, y: tmpTitleBarHeight + tmpSpacing * (pIndex + 1) };
483
+ let tmpTpl = tmpBodyContent.Templates[i];
484
+ if (tmpTpl.Hash && tmpTpl.Template && !this._registeredBodyTemplates.has(tmpTpl.Hash))
485
+ {
486
+ tmpPict.TemplateProvider.addTemplate(tmpTpl.Hash, tmpTpl.Template, 'PictViewFlowNode-BodyContent');
487
+ this._registeredBodyTemplates.add(tmpTpl.Hash);
488
+ }
329
489
  }
330
- case 'top':
331
- tmpSpacing = pWidth / (pTotal + 1);
332
- return { x: tmpSpacing * (pIndex + 1), y: 0 };
333
- case 'bottom':
334
- tmpSpacing = pWidth / (pTotal + 1);
335
- return { x: tmpSpacing * (pIndex + 1), y: pHeight };
490
+ }
491
+
492
+ switch (tmpContentType)
493
+ {
494
+ case 'svg':
495
+ this._renderBodyContentSVG(pNodeData, pGroup, tmpBodyContent, tmpBodyBounds, pNodeTypeConfig, tmpPict);
496
+ break;
497
+ case 'html':
498
+ this._renderBodyContentHTML(pNodeData, pGroup, tmpBodyContent, tmpBodyBounds, pNodeTypeConfig, tmpPict);
499
+ break;
500
+ case 'canvas':
501
+ this._renderBodyContentCanvas(pNodeData, pGroup, tmpBodyContent, tmpBodyBounds, pNodeTypeConfig);
502
+ break;
336
503
  default:
337
- return { x: pWidth, y: pHeight / 2 };
504
+ this.log.warn('PictViewFlowNode _renderBodyContent: unknown ContentType [' + tmpContentType + ']');
505
+ break;
506
+ }
507
+ }
508
+
509
+ /**
510
+ * Render SVG body content into a <g> group.
511
+ */
512
+ _renderBodyContentSVG(pNodeData, pGroup, pBodyContent, pBounds, pNodeTypeConfig, pPict)
513
+ {
514
+ let tmpContentGroup = this._FlowView._SVGHelperProvider.createSVGElement('g');
515
+ tmpContentGroup.setAttribute('class', 'pict-flow-node-body-content');
516
+ tmpContentGroup.setAttribute('transform', `translate(${pBounds.x}, ${pBounds.y})`);
517
+
518
+ // Render template content
519
+ let tmpRenderedContent = this._resolveBodyTemplate(pBodyContent, pNodeData, pPict);
520
+ if (tmpRenderedContent)
521
+ {
522
+ // Parse SVG markup into the group via a temporary SVG element
523
+ let tmpTempSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
524
+ tmpTempSVG.innerHTML = tmpRenderedContent;
525
+ while (tmpTempSVG.firstChild)
526
+ {
527
+ tmpContentGroup.appendChild(tmpTempSVG.firstChild);
528
+ }
529
+ }
530
+
531
+ // Invoke render callback if provided
532
+ if (typeof pBodyContent.RenderCallback === 'function')
533
+ {
534
+ pBodyContent.RenderCallback(tmpContentGroup, pNodeData, pNodeTypeConfig, pBounds);
338
535
  }
536
+
537
+ pGroup.appendChild(tmpContentGroup);
538
+ }
539
+
540
+ /**
541
+ * Render HTML body content into a foreignObject.
542
+ */
543
+ _renderBodyContentHTML(pNodeData, pGroup, pBodyContent, pBounds, pNodeTypeConfig, pPict)
544
+ {
545
+ let tmpFO = this._FlowView._SVGHelperProvider.createSVGElement('foreignObject');
546
+ tmpFO.setAttribute('class', 'pict-flow-node-body-content-fo');
547
+ tmpFO.setAttribute('x', String(pBounds.x));
548
+ tmpFO.setAttribute('y', String(pBounds.y));
549
+ tmpFO.setAttribute('width', String(pBounds.width));
550
+ tmpFO.setAttribute('height', String(pBounds.height));
551
+
552
+ let tmpDiv = document.createElement('div');
553
+ tmpDiv.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
554
+ tmpDiv.setAttribute('class', 'pict-flow-node-body-content-html');
555
+
556
+ // Pointer event isolation — prevent node drag/canvas pan
557
+ tmpDiv.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
558
+ tmpDiv.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
559
+
560
+ // Render template content
561
+ let tmpRenderedContent = this._resolveBodyTemplate(pBodyContent, pNodeData, pPict);
562
+ if (tmpRenderedContent)
563
+ {
564
+ tmpDiv.innerHTML = tmpRenderedContent;
565
+ }
566
+
567
+ // Invoke render callback if provided
568
+ if (typeof pBodyContent.RenderCallback === 'function')
569
+ {
570
+ pBodyContent.RenderCallback(tmpDiv, pNodeData, pNodeTypeConfig, pBounds);
571
+ }
572
+
573
+ tmpFO.appendChild(tmpDiv);
574
+ pGroup.appendChild(tmpFO);
575
+ }
576
+
577
+ /**
578
+ * Render canvas body content into a foreignObject.
579
+ */
580
+ _renderBodyContentCanvas(pNodeData, pGroup, pBodyContent, pBounds, pNodeTypeConfig)
581
+ {
582
+ let tmpFO = this._FlowView._SVGHelperProvider.createSVGElement('foreignObject');
583
+ tmpFO.setAttribute('class', 'pict-flow-node-body-content-fo');
584
+ tmpFO.setAttribute('x', String(pBounds.x));
585
+ tmpFO.setAttribute('y', String(pBounds.y));
586
+ tmpFO.setAttribute('width', String(pBounds.width));
587
+ tmpFO.setAttribute('height', String(pBounds.height));
588
+
589
+ let tmpCanvas = document.createElement('canvas');
590
+ tmpCanvas.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
591
+ tmpCanvas.setAttribute('class', 'pict-flow-node-body-content-canvas');
592
+ tmpCanvas.width = Math.floor(pBounds.width);
593
+ tmpCanvas.height = Math.floor(pBounds.height);
594
+ tmpCanvas.style.width = '100%';
595
+ tmpCanvas.style.height = '100%';
596
+
597
+ // Pointer event isolation
598
+ tmpCanvas.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
599
+ tmpCanvas.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
600
+
601
+ // Invoke render callback (the primary rendering path for canvas)
602
+ if (typeof pBodyContent.RenderCallback === 'function')
603
+ {
604
+ pBodyContent.RenderCallback(tmpCanvas, pNodeData, pNodeTypeConfig, pBounds);
605
+ }
606
+
607
+ tmpFO.appendChild(tmpCanvas);
608
+ pGroup.appendChild(tmpFO);
609
+ }
610
+
611
+ /**
612
+ * Resolve and render a body content template string.
613
+ * @param {Object} pBodyContent - The BodyContent config
614
+ * @param {Object} pNodeData - The node data (template record)
615
+ * @param {Object} pPict - The Pict instance
616
+ * @returns {string|null} Rendered template content, or null
617
+ */
618
+ _resolveBodyTemplate(pBodyContent, pNodeData, pPict)
619
+ {
620
+ if (pBodyContent.TemplateHash)
621
+ {
622
+ return pPict.parseTemplateByHash(pBodyContent.TemplateHash, pNodeData);
623
+ }
624
+ if (pBodyContent.Template)
625
+ {
626
+ return pPict.parseTemplate(pBodyContent.Template, pNodeData, null, [pNodeData]);
627
+ }
628
+ return null;
629
+ }
630
+
631
+ // ── Node Body Renderers ──────────────────────────────────────────────
632
+
633
+ /**
634
+ * Render the standard rect-based node body (default mode).
635
+ * @param {SVGGElement} pGroup
636
+ * @param {Object} pNodeData
637
+ * @param {number} pWidth
638
+ * @param {number} pHeight
639
+ * @param {number} pTitleBarHeight
640
+ * @param {Object} pNodeTypeConfig
641
+ */
642
+ _renderRectNodeBody(pGroup, pNodeData, pWidth, pHeight, pTitleBarHeight, pNodeTypeConfig)
643
+ {
644
+ // Node body (main rectangle)
645
+ let tmpBody = this._FlowView._SVGHelperProvider.createSVGElement('rect');
646
+ tmpBody.setAttribute('class', 'pict-flow-node-body');
647
+ tmpBody.setAttribute('x', '0');
648
+ tmpBody.setAttribute('y', '0');
649
+ tmpBody.setAttribute('width', String(pWidth));
650
+ tmpBody.setAttribute('height', String(pHeight));
651
+ tmpBody.setAttribute('data-node-hash', pNodeData.Hash);
652
+ tmpBody.setAttribute('data-element-type', 'node-body');
653
+
654
+ // Apply custom styles from node type
655
+ if (pNodeTypeConfig && pNodeTypeConfig.BodyStyle)
656
+ {
657
+ for (let tmpStyleKey in pNodeTypeConfig.BodyStyle)
658
+ {
659
+ tmpBody.setAttribute(tmpStyleKey, pNodeTypeConfig.BodyStyle[tmpStyleKey]);
660
+ }
661
+ }
662
+
663
+ // Apply per-instance style overrides (for node-specific editing)
664
+ // These must be applied as inline styles so they override CSS rules
665
+ // (CSS declarations take precedence over SVG presentation attributes).
666
+ if (pNodeData.Style)
667
+ {
668
+ let tmpInlineStyles = [];
669
+ if (pNodeData.Style.BodyFill) tmpInlineStyles.push('fill:' + pNodeData.Style.BodyFill);
670
+ if (pNodeData.Style.BodyStroke) tmpInlineStyles.push('stroke:' + pNodeData.Style.BodyStroke);
671
+ if (pNodeData.Style.BodyStrokeWidth) tmpInlineStyles.push('stroke-width:' + pNodeData.Style.BodyStrokeWidth);
672
+ if (tmpInlineStyles.length > 0)
673
+ {
674
+ tmpBody.setAttribute('style', tmpInlineStyles.join(';'));
675
+ }
676
+ }
677
+
678
+ pGroup.appendChild(tmpBody);
679
+
680
+ // Title bar background (top portion)
681
+ let tmpTitleBar = this._FlowView._SVGHelperProvider.createSVGElement('rect');
682
+ tmpTitleBar.setAttribute('class', 'pict-flow-node-title-bar');
683
+ tmpTitleBar.setAttribute('x', '0');
684
+ tmpTitleBar.setAttribute('y', '0');
685
+ tmpTitleBar.setAttribute('width', String(pWidth));
686
+ tmpTitleBar.setAttribute('height', String(pTitleBarHeight));
687
+ tmpTitleBar.setAttribute('data-node-hash', pNodeData.Hash);
688
+ tmpTitleBar.setAttribute('data-element-type', 'node-body');
689
+
690
+ // Apply custom title bar color
691
+ if (pNodeTypeConfig && pNodeTypeConfig.TitleBarColor)
692
+ {
693
+ tmpTitleBar.setAttribute('fill', pNodeTypeConfig.TitleBarColor);
694
+ }
695
+
696
+ pGroup.appendChild(tmpTitleBar);
697
+
698
+ // Title bar bottom fill (to square off the rounded corners at the bottom of the title bar)
699
+ let tmpTitleBarBottom = this._FlowView._SVGHelperProvider.createSVGElement('rect');
700
+ tmpTitleBarBottom.setAttribute('class', 'pict-flow-node-title-bar-bottom');
701
+ tmpTitleBarBottom.setAttribute('x', '0');
702
+ tmpTitleBarBottom.setAttribute('y', String(pTitleBarHeight - 8));
703
+ tmpTitleBarBottom.setAttribute('width', String(pWidth));
704
+ tmpTitleBarBottom.setAttribute('height', '8');
705
+ tmpTitleBarBottom.setAttribute('data-node-hash', pNodeData.Hash);
706
+ tmpTitleBarBottom.setAttribute('data-element-type', 'node-body');
707
+
708
+ if (pNodeTypeConfig && pNodeTypeConfig.TitleBarColor)
709
+ {
710
+ tmpTitleBarBottom.setAttribute('fill', pNodeTypeConfig.TitleBarColor);
711
+ }
712
+
713
+ // Per-instance title bar color override
714
+ // Applied as inline style to override CSS rules.
715
+ if (pNodeData.Style && pNodeData.Style.TitleBarColor)
716
+ {
717
+ tmpTitleBar.setAttribute('style', 'fill:' + pNodeData.Style.TitleBarColor);
718
+ tmpTitleBarBottom.setAttribute('style', 'fill:' + pNodeData.Style.TitleBarColor);
719
+ }
720
+
721
+ pGroup.appendChild(tmpTitleBarBottom);
722
+ }
723
+
724
+ /**
725
+ * Render a bracket-style node body (used by sketch/blueprint themes).
726
+ *
727
+ * The bracket body consists of:
728
+ * 1. A fill rect for the body background (no stroke)
729
+ * 2. A fill rect for the title bar background (no stroke)
730
+ * 3. A bracket path drawn via the noise provider (outline + title divider)
731
+ *
732
+ * @param {SVGGElement} pGroup
733
+ * @param {Object} pNodeData
734
+ * @param {number} pWidth
735
+ * @param {number} pHeight
736
+ * @param {number} pTitleBarHeight
737
+ * @param {Object} pNodeTypeConfig
738
+ */
739
+ _renderBracketNodeBody(pGroup, pNodeData, pWidth, pHeight, pTitleBarHeight, pNodeTypeConfig)
740
+ {
741
+ // 1. Body fill rect (background only, no stroke)
742
+ let tmpBodyFill = this._FlowView._SVGHelperProvider.createSVGElement('rect');
743
+ tmpBodyFill.setAttribute('class', 'pict-flow-node-body pict-flow-node-bracket-fill');
744
+ tmpBodyFill.setAttribute('x', '0');
745
+ tmpBodyFill.setAttribute('y', '0');
746
+ tmpBodyFill.setAttribute('width', String(pWidth));
747
+ tmpBodyFill.setAttribute('height', String(pHeight));
748
+ tmpBodyFill.setAttribute('data-node-hash', pNodeData.Hash);
749
+ tmpBodyFill.setAttribute('data-element-type', 'node-body');
750
+
751
+ // Per-instance style overrides
752
+ if (pNodeData.Style)
753
+ {
754
+ let tmpInlineStyles = [];
755
+ if (pNodeData.Style.BodyFill) tmpInlineStyles.push('fill:' + pNodeData.Style.BodyFill);
756
+ if (tmpInlineStyles.length > 0)
757
+ {
758
+ tmpBodyFill.setAttribute('style', tmpInlineStyles.join(';'));
759
+ }
760
+ }
761
+
762
+ pGroup.appendChild(tmpBodyFill);
763
+
764
+ // 2. Title bar fill rect (background only, no stroke)
765
+ let tmpTitleFill = this._FlowView._SVGHelperProvider.createSVGElement('rect');
766
+ tmpTitleFill.setAttribute('class', 'pict-flow-node-title-bar pict-flow-node-bracket-title-fill');
767
+ tmpTitleFill.setAttribute('x', '0');
768
+ tmpTitleFill.setAttribute('y', '0');
769
+ tmpTitleFill.setAttribute('width', String(pWidth));
770
+ tmpTitleFill.setAttribute('height', String(pTitleBarHeight));
771
+ tmpTitleFill.setAttribute('data-node-hash', pNodeData.Hash);
772
+ tmpTitleFill.setAttribute('data-element-type', 'node-body');
773
+
774
+ if (pNodeTypeConfig && pNodeTypeConfig.TitleBarColor)
775
+ {
776
+ tmpTitleFill.setAttribute('style', 'fill:' + pNodeTypeConfig.TitleBarColor);
777
+ }
778
+ if (pNodeData.Style && pNodeData.Style.TitleBarColor)
779
+ {
780
+ tmpTitleFill.setAttribute('style', 'fill:' + pNodeData.Style.TitleBarColor);
781
+ }
782
+
783
+ pGroup.appendChild(tmpTitleFill);
784
+
785
+ // 3. Bracket path (outline + title divider with optional noise)
786
+ let tmpBracketConfig = { SerifLength: 6, TitleSeparator: true };
787
+ if (this._FlowView._ThemeProvider)
788
+ {
789
+ let tmpActiveTheme = this._FlowView._ThemeProvider.getActiveTheme();
790
+ if (tmpActiveTheme && tmpActiveTheme.BracketConfig)
791
+ {
792
+ tmpBracketConfig = Object.assign(tmpBracketConfig, tmpActiveTheme.BracketConfig);
793
+ }
794
+ }
795
+
796
+ let tmpAmplitude = 0;
797
+ if (this._FlowView._ThemeProvider)
798
+ {
799
+ tmpAmplitude = this._FlowView._ThemeProvider.getNodeNoiseAmplitude();
800
+ }
801
+
802
+ let tmpBracketD = '';
803
+ if (this._FlowView._NoiseProvider)
804
+ {
805
+ tmpBracketD = this._FlowView._NoiseProvider.generateBracketPath(
806
+ pWidth, pHeight,
807
+ tmpBracketConfig.SerifLength,
808
+ tmpBracketConfig.TitleSeparator ? pTitleBarHeight : 0,
809
+ tmpAmplitude,
810
+ pNodeData.Hash
811
+ );
812
+ }
813
+
814
+ let tmpBracketPath = this._FlowView._SVGHelperProvider.createSVGElement('path');
815
+ tmpBracketPath.setAttribute('class', 'pict-flow-node-bracket');
816
+ tmpBracketPath.setAttribute('d', tmpBracketD);
817
+ tmpBracketPath.setAttribute('data-node-hash', pNodeData.Hash);
818
+ tmpBracketPath.setAttribute('data-element-type', 'node-body');
819
+
820
+ // Per-instance stroke overrides
821
+ if (pNodeData.Style)
822
+ {
823
+ let tmpInlineStyles = [];
824
+ if (pNodeData.Style.BodyStroke) tmpInlineStyles.push('stroke:' + pNodeData.Style.BodyStroke);
825
+ if (pNodeData.Style.BodyStrokeWidth) tmpInlineStyles.push('stroke-width:' + pNodeData.Style.BodyStrokeWidth);
826
+ if (tmpInlineStyles.length > 0)
827
+ {
828
+ tmpBracketPath.setAttribute('style', tmpInlineStyles.join(';'));
829
+ }
830
+ }
831
+
832
+ pGroup.appendChild(tmpBracketPath);
339
833
  }
340
834
  }
341
835