pict-section-flow 0.0.2 → 0.0.3

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 (38) 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 +2 -2
  18. package/source/Pict-Section-Flow.js +8 -1
  19. package/source/PictFlowCard.js +49 -1
  20. package/source/providers/PictProvider-Flow-CSS.js +1440 -0
  21. package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
  22. package/source/providers/PictProvider-Flow-Geometry.js +43 -0
  23. package/source/providers/PictProvider-Flow-Icons.js +335 -0
  24. package/source/providers/PictProvider-Flow-Layouts.js +214 -2
  25. package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
  26. package/source/providers/PictProvider-Flow-Noise.js +241 -0
  27. package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
  28. package/source/providers/PictProvider-Flow-Theme.js +755 -0
  29. package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
  30. package/source/services/PictService-Flow-PanelManager.js +188 -0
  31. package/source/services/PictService-Flow-SelectionManager.js +109 -0
  32. package/source/services/PictService-Flow-Tether.js +52 -25
  33. package/source/services/PictService-Flow-ViewportManager.js +176 -0
  34. package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
  35. package/source/views/PictView-Flow-Node.js +654 -169
  36. package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
  37. package/source/views/PictView-Flow-Toolbar.js +846 -379
  38. 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);
82
-
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');
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;
92
77
 
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,11 +284,15 @@ 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
 
293
+ let tmpPortLabelsVertical = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsVertical);
294
+ let tmpPortLabelPadding = (pNodeTypeConfig && pNodeTypeConfig.PortLabelPadding);
295
+
227
296
  // Group ports by side and direction for positioning
228
297
  let tmpPortsBySide = { left: [], right: [], top: [], bottom: [] };
229
298
  for (let i = 0; i < pNodeData.Ports.length; i++)
@@ -245,15 +314,24 @@ class PictViewFlowNode extends libPictView
245
314
  let tmpPosition = this._getPortLocalPosition(tmpSide, i, tmpPorts.length, pWidth, pHeight);
246
315
 
247
316
  // 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');
317
+ let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
318
+ let tmpCircle;
319
+ if (tmpShapeProvider)
320
+ {
321
+ tmpCircle = tmpShapeProvider.createPortElement(tmpPort, tmpPosition, pNodeData.Hash);
322
+ }
323
+ else
324
+ {
325
+ tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
326
+ tmpCircle.setAttribute('class', `pict-flow-port ${tmpPort.Direction}`);
327
+ tmpCircle.setAttribute('cx', String(tmpPosition.x));
328
+ tmpCircle.setAttribute('cy', String(tmpPosition.y));
329
+ tmpCircle.setAttribute('r', '5');
330
+ tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
331
+ tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
332
+ tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
333
+ tmpCircle.setAttribute('data-element-type', 'port');
334
+ }
257
335
  pGroup.appendChild(tmpCircle);
258
336
 
259
337
  // Port label
@@ -263,29 +341,69 @@ class PictViewFlowNode extends libPictView
263
341
  tmpLabel.setAttribute('class', 'pict-flow-port-label');
264
342
  tmpLabel.textContent = tmpPort.Label;
265
343
 
344
+ // Base offset from port center; PortLabelPadding adds extra space
266
345
  let tmpLabelOffset = 12;
267
- switch (tmpSide)
346
+ let tmpPaddingExtra = tmpPortLabelPadding ? 8 : 0;
347
+
348
+ if (tmpPortLabelsVertical)
349
+ {
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)
354
+ {
355
+ case 'left':
356
+ tmpLabel.setAttribute('x', String(tmpPosition.x + tmpLabelOffset + tmpPaddingExtra));
357
+ tmpLabel.setAttribute('y', String(tmpPosition.y));
358
+ tmpLabel.setAttribute('text-anchor', 'middle');
359
+ tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x + tmpLabelOffset + tmpPaddingExtra}, ${tmpPosition.y})`);
360
+ break;
361
+ case 'right':
362
+ tmpLabel.setAttribute('x', String(tmpPosition.x - tmpLabelOffset - tmpPaddingExtra));
363
+ tmpLabel.setAttribute('y', String(tmpPosition.y));
364
+ tmpLabel.setAttribute('text-anchor', 'middle');
365
+ tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x - tmpLabelOffset - tmpPaddingExtra}, ${tmpPosition.y})`);
366
+ break;
367
+ case 'top':
368
+ tmpLabel.setAttribute('x', String(tmpPosition.x));
369
+ tmpLabel.setAttribute('y', String(tmpPosition.y + tmpLabelOffset + tmpPaddingExtra));
370
+ tmpLabel.setAttribute('text-anchor', 'middle');
371
+ tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y + tmpLabelOffset + tmpPaddingExtra})`);
372
+ break;
373
+ case 'bottom':
374
+ tmpLabel.setAttribute('x', String(tmpPosition.x));
375
+ tmpLabel.setAttribute('y', String(tmpPosition.y - tmpLabelOffset - tmpPaddingExtra));
376
+ tmpLabel.setAttribute('text-anchor', 'middle');
377
+ tmpLabel.setAttribute('transform', `rotate(-90, ${tmpPosition.x}, ${tmpPosition.y - tmpLabelOffset - tmpPaddingExtra})`);
378
+ break;
379
+ }
380
+ }
381
+ else
268
382
  {
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;
383
+ // Horizontal labels (default)
384
+ switch (tmpSide)
385
+ {
386
+ case 'left':
387
+ tmpLabel.setAttribute('x', String(tmpPosition.x + tmpLabelOffset + tmpPaddingExtra));
388
+ tmpLabel.setAttribute('y', String(tmpPosition.y));
389
+ tmpLabel.setAttribute('text-anchor', 'start');
390
+ break;
391
+ case 'right':
392
+ tmpLabel.setAttribute('x', String(tmpPosition.x - tmpLabelOffset - tmpPaddingExtra));
393
+ tmpLabel.setAttribute('y', String(tmpPosition.y));
394
+ tmpLabel.setAttribute('text-anchor', 'end');
395
+ break;
396
+ case 'top':
397
+ tmpLabel.setAttribute('x', String(tmpPosition.x));
398
+ tmpLabel.setAttribute('y', String(tmpPosition.y + tmpLabelOffset + tmpPaddingExtra));
399
+ tmpLabel.setAttribute('text-anchor', 'middle');
400
+ break;
401
+ case 'bottom':
402
+ tmpLabel.setAttribute('x', String(tmpPosition.x));
403
+ tmpLabel.setAttribute('y', String(tmpPosition.y - tmpLabelOffset - tmpPaddingExtra));
404
+ tmpLabel.setAttribute('text-anchor', 'middle');
405
+ break;
406
+ }
289
407
  }
290
408
  tmpLabel.setAttribute('dominant-baseline', 'central');
291
409
  pGroup.appendChild(tmpLabel);
@@ -309,33 +427,400 @@ class PictViewFlowNode extends libPictView
309
427
  */
310
428
  _getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight)
311
429
  {
312
- let tmpSpacing;
430
+ return this._FlowView._GeometryProvider.getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, this.options.NodeTitleBarHeight);
431
+ }
432
+
433
+ /**
434
+ * Render custom body content for a node (svg, html, or canvas).
435
+ *
436
+ * Checks for a BodyContent configuration on the node type and renders
437
+ * the appropriate content type into the node's SVG group.
438
+ *
439
+ * @param {Object} pNodeData - The node data object
440
+ * @param {SVGGElement} pGroup - The node's SVG group
441
+ * @param {number} pWidth - Node width
442
+ * @param {number} pHeight - Node height
443
+ * @param {Object} pNodeTypeConfig - The node type configuration
444
+ */
445
+ _renderBodyContent(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig)
446
+ {
447
+ if (!pNodeTypeConfig || !pNodeTypeConfig.BodyContent) return;
448
+
449
+ let tmpBodyContent = pNodeTypeConfig.BodyContent;
450
+ let tmpContentType = tmpBodyContent.ContentType;
451
+ if (!tmpContentType) return;
452
+
313
453
  let tmpTitleBarHeight = this.options.NodeTitleBarHeight;
454
+ let tmpPadding = (typeof tmpBodyContent.Padding === 'number') ? tmpBodyContent.Padding : 2;
455
+ let tmpBodyBounds =
456
+ {
457
+ x: tmpPadding,
458
+ y: tmpTitleBarHeight + tmpPadding,
459
+ width: pWidth - (tmpPadding * 2),
460
+ height: pHeight - tmpTitleBarHeight - (tmpPadding * 2)
461
+ };
314
462
 
315
- switch (pSide)
463
+ let tmpPict = this._FlowView.pict || this.pict;
464
+
465
+ // Register any templates defined in the BodyContent config (once)
466
+ if (tmpBodyContent.Templates && Array.isArray(tmpBodyContent.Templates))
316
467
  {
317
- case 'left':
468
+ if (!this._registeredBodyTemplates)
318
469
  {
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) };
470
+ this._registeredBodyTemplates = new Set();
323
471
  }
324
- case 'right':
472
+ for (let i = 0; i < tmpBodyContent.Templates.length; i++)
325
473
  {
326
- let tmpBodyHeight = pHeight - tmpTitleBarHeight;
327
- tmpSpacing = tmpBodyHeight / (pTotal + 1);
328
- return { x: pWidth, y: tmpTitleBarHeight + tmpSpacing * (pIndex + 1) };
474
+ let tmpTpl = tmpBodyContent.Templates[i];
475
+ if (tmpTpl.Hash && tmpTpl.Template && !this._registeredBodyTemplates.has(tmpTpl.Hash))
476
+ {
477
+ tmpPict.TemplateProvider.addTemplate(tmpTpl.Hash, tmpTpl.Template, 'PictViewFlowNode-BodyContent');
478
+ this._registeredBodyTemplates.add(tmpTpl.Hash);
479
+ }
329
480
  }
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 };
481
+ }
482
+
483
+ switch (tmpContentType)
484
+ {
485
+ case 'svg':
486
+ this._renderBodyContentSVG(pNodeData, pGroup, tmpBodyContent, tmpBodyBounds, pNodeTypeConfig, tmpPict);
487
+ break;
488
+ case 'html':
489
+ this._renderBodyContentHTML(pNodeData, pGroup, tmpBodyContent, tmpBodyBounds, pNodeTypeConfig, tmpPict);
490
+ break;
491
+ case 'canvas':
492
+ this._renderBodyContentCanvas(pNodeData, pGroup, tmpBodyContent, tmpBodyBounds, pNodeTypeConfig);
493
+ break;
336
494
  default:
337
- return { x: pWidth, y: pHeight / 2 };
495
+ this.log.warn('PictViewFlowNode _renderBodyContent: unknown ContentType [' + tmpContentType + ']');
496
+ break;
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Render SVG body content into a <g> group.
502
+ */
503
+ _renderBodyContentSVG(pNodeData, pGroup, pBodyContent, pBounds, pNodeTypeConfig, pPict)
504
+ {
505
+ let tmpContentGroup = this._FlowView._SVGHelperProvider.createSVGElement('g');
506
+ tmpContentGroup.setAttribute('class', 'pict-flow-node-body-content');
507
+ tmpContentGroup.setAttribute('transform', `translate(${pBounds.x}, ${pBounds.y})`);
508
+
509
+ // Render template content
510
+ let tmpRenderedContent = this._resolveBodyTemplate(pBodyContent, pNodeData, pPict);
511
+ if (tmpRenderedContent)
512
+ {
513
+ // Parse SVG markup into the group via a temporary SVG element
514
+ let tmpTempSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
515
+ tmpTempSVG.innerHTML = tmpRenderedContent;
516
+ while (tmpTempSVG.firstChild)
517
+ {
518
+ tmpContentGroup.appendChild(tmpTempSVG.firstChild);
519
+ }
338
520
  }
521
+
522
+ // Invoke render callback if provided
523
+ if (typeof pBodyContent.RenderCallback === 'function')
524
+ {
525
+ pBodyContent.RenderCallback(tmpContentGroup, pNodeData, pNodeTypeConfig, pBounds);
526
+ }
527
+
528
+ pGroup.appendChild(tmpContentGroup);
529
+ }
530
+
531
+ /**
532
+ * Render HTML body content into a foreignObject.
533
+ */
534
+ _renderBodyContentHTML(pNodeData, pGroup, pBodyContent, pBounds, pNodeTypeConfig, pPict)
535
+ {
536
+ let tmpFO = this._FlowView._SVGHelperProvider.createSVGElement('foreignObject');
537
+ tmpFO.setAttribute('class', 'pict-flow-node-body-content-fo');
538
+ tmpFO.setAttribute('x', String(pBounds.x));
539
+ tmpFO.setAttribute('y', String(pBounds.y));
540
+ tmpFO.setAttribute('width', String(pBounds.width));
541
+ tmpFO.setAttribute('height', String(pBounds.height));
542
+
543
+ let tmpDiv = document.createElement('div');
544
+ tmpDiv.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
545
+ tmpDiv.setAttribute('class', 'pict-flow-node-body-content-html');
546
+
547
+ // Pointer event isolation — prevent node drag/canvas pan
548
+ tmpDiv.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
549
+ tmpDiv.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
550
+
551
+ // Render template content
552
+ let tmpRenderedContent = this._resolveBodyTemplate(pBodyContent, pNodeData, pPict);
553
+ if (tmpRenderedContent)
554
+ {
555
+ tmpDiv.innerHTML = tmpRenderedContent;
556
+ }
557
+
558
+ // Invoke render callback if provided
559
+ if (typeof pBodyContent.RenderCallback === 'function')
560
+ {
561
+ pBodyContent.RenderCallback(tmpDiv, pNodeData, pNodeTypeConfig, pBounds);
562
+ }
563
+
564
+ tmpFO.appendChild(tmpDiv);
565
+ pGroup.appendChild(tmpFO);
566
+ }
567
+
568
+ /**
569
+ * Render canvas body content into a foreignObject.
570
+ */
571
+ _renderBodyContentCanvas(pNodeData, pGroup, pBodyContent, pBounds, pNodeTypeConfig)
572
+ {
573
+ let tmpFO = this._FlowView._SVGHelperProvider.createSVGElement('foreignObject');
574
+ tmpFO.setAttribute('class', 'pict-flow-node-body-content-fo');
575
+ tmpFO.setAttribute('x', String(pBounds.x));
576
+ tmpFO.setAttribute('y', String(pBounds.y));
577
+ tmpFO.setAttribute('width', String(pBounds.width));
578
+ tmpFO.setAttribute('height', String(pBounds.height));
579
+
580
+ let tmpCanvas = document.createElement('canvas');
581
+ tmpCanvas.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
582
+ tmpCanvas.setAttribute('class', 'pict-flow-node-body-content-canvas');
583
+ tmpCanvas.width = Math.floor(pBounds.width);
584
+ tmpCanvas.height = Math.floor(pBounds.height);
585
+ tmpCanvas.style.width = '100%';
586
+ tmpCanvas.style.height = '100%';
587
+
588
+ // Pointer event isolation
589
+ tmpCanvas.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
590
+ tmpCanvas.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
591
+
592
+ // Invoke render callback (the primary rendering path for canvas)
593
+ if (typeof pBodyContent.RenderCallback === 'function')
594
+ {
595
+ pBodyContent.RenderCallback(tmpCanvas, pNodeData, pNodeTypeConfig, pBounds);
596
+ }
597
+
598
+ tmpFO.appendChild(tmpCanvas);
599
+ pGroup.appendChild(tmpFO);
600
+ }
601
+
602
+ /**
603
+ * Resolve and render a body content template string.
604
+ * @param {Object} pBodyContent - The BodyContent config
605
+ * @param {Object} pNodeData - The node data (template record)
606
+ * @param {Object} pPict - The Pict instance
607
+ * @returns {string|null} Rendered template content, or null
608
+ */
609
+ _resolveBodyTemplate(pBodyContent, pNodeData, pPict)
610
+ {
611
+ if (pBodyContent.TemplateHash)
612
+ {
613
+ return pPict.parseTemplateByHash(pBodyContent.TemplateHash, pNodeData);
614
+ }
615
+ if (pBodyContent.Template)
616
+ {
617
+ return pPict.parseTemplate(pBodyContent.Template, pNodeData, null, [pNodeData]);
618
+ }
619
+ return null;
620
+ }
621
+
622
+ // ── Node Body Renderers ──────────────────────────────────────────────
623
+
624
+ /**
625
+ * Render the standard rect-based node body (default mode).
626
+ * @param {SVGGElement} pGroup
627
+ * @param {Object} pNodeData
628
+ * @param {number} pWidth
629
+ * @param {number} pHeight
630
+ * @param {number} pTitleBarHeight
631
+ * @param {Object} pNodeTypeConfig
632
+ */
633
+ _renderRectNodeBody(pGroup, pNodeData, pWidth, pHeight, pTitleBarHeight, pNodeTypeConfig)
634
+ {
635
+ // Node body (main rectangle)
636
+ let tmpBody = this._FlowView._SVGHelperProvider.createSVGElement('rect');
637
+ tmpBody.setAttribute('class', 'pict-flow-node-body');
638
+ tmpBody.setAttribute('x', '0');
639
+ tmpBody.setAttribute('y', '0');
640
+ tmpBody.setAttribute('width', String(pWidth));
641
+ tmpBody.setAttribute('height', String(pHeight));
642
+ tmpBody.setAttribute('data-node-hash', pNodeData.Hash);
643
+ tmpBody.setAttribute('data-element-type', 'node-body');
644
+
645
+ // Apply custom styles from node type
646
+ if (pNodeTypeConfig && pNodeTypeConfig.BodyStyle)
647
+ {
648
+ for (let tmpStyleKey in pNodeTypeConfig.BodyStyle)
649
+ {
650
+ tmpBody.setAttribute(tmpStyleKey, pNodeTypeConfig.BodyStyle[tmpStyleKey]);
651
+ }
652
+ }
653
+
654
+ // Apply per-instance style overrides (for node-specific editing)
655
+ // These must be applied as inline styles so they override CSS rules
656
+ // (CSS declarations take precedence over SVG presentation attributes).
657
+ if (pNodeData.Style)
658
+ {
659
+ let tmpInlineStyles = [];
660
+ if (pNodeData.Style.BodyFill) tmpInlineStyles.push('fill:' + pNodeData.Style.BodyFill);
661
+ if (pNodeData.Style.BodyStroke) tmpInlineStyles.push('stroke:' + pNodeData.Style.BodyStroke);
662
+ if (pNodeData.Style.BodyStrokeWidth) tmpInlineStyles.push('stroke-width:' + pNodeData.Style.BodyStrokeWidth);
663
+ if (tmpInlineStyles.length > 0)
664
+ {
665
+ tmpBody.setAttribute('style', tmpInlineStyles.join(';'));
666
+ }
667
+ }
668
+
669
+ pGroup.appendChild(tmpBody);
670
+
671
+ // Title bar background (top portion)
672
+ let tmpTitleBar = this._FlowView._SVGHelperProvider.createSVGElement('rect');
673
+ tmpTitleBar.setAttribute('class', 'pict-flow-node-title-bar');
674
+ tmpTitleBar.setAttribute('x', '0');
675
+ tmpTitleBar.setAttribute('y', '0');
676
+ tmpTitleBar.setAttribute('width', String(pWidth));
677
+ tmpTitleBar.setAttribute('height', String(pTitleBarHeight));
678
+ tmpTitleBar.setAttribute('data-node-hash', pNodeData.Hash);
679
+ tmpTitleBar.setAttribute('data-element-type', 'node-body');
680
+
681
+ // Apply custom title bar color
682
+ if (pNodeTypeConfig && pNodeTypeConfig.TitleBarColor)
683
+ {
684
+ tmpTitleBar.setAttribute('fill', pNodeTypeConfig.TitleBarColor);
685
+ }
686
+
687
+ pGroup.appendChild(tmpTitleBar);
688
+
689
+ // Title bar bottom fill (to square off the rounded corners at the bottom of the title bar)
690
+ let tmpTitleBarBottom = this._FlowView._SVGHelperProvider.createSVGElement('rect');
691
+ tmpTitleBarBottom.setAttribute('class', 'pict-flow-node-title-bar-bottom');
692
+ tmpTitleBarBottom.setAttribute('x', '0');
693
+ tmpTitleBarBottom.setAttribute('y', String(pTitleBarHeight - 8));
694
+ tmpTitleBarBottom.setAttribute('width', String(pWidth));
695
+ tmpTitleBarBottom.setAttribute('height', '8');
696
+ tmpTitleBarBottom.setAttribute('data-node-hash', pNodeData.Hash);
697
+ tmpTitleBarBottom.setAttribute('data-element-type', 'node-body');
698
+
699
+ if (pNodeTypeConfig && pNodeTypeConfig.TitleBarColor)
700
+ {
701
+ tmpTitleBarBottom.setAttribute('fill', pNodeTypeConfig.TitleBarColor);
702
+ }
703
+
704
+ // Per-instance title bar color override
705
+ // Applied as inline style to override CSS rules.
706
+ if (pNodeData.Style && pNodeData.Style.TitleBarColor)
707
+ {
708
+ tmpTitleBar.setAttribute('style', 'fill:' + pNodeData.Style.TitleBarColor);
709
+ tmpTitleBarBottom.setAttribute('style', 'fill:' + pNodeData.Style.TitleBarColor);
710
+ }
711
+
712
+ pGroup.appendChild(tmpTitleBarBottom);
713
+ }
714
+
715
+ /**
716
+ * Render a bracket-style node body (used by sketch/blueprint themes).
717
+ *
718
+ * The bracket body consists of:
719
+ * 1. A fill rect for the body background (no stroke)
720
+ * 2. A fill rect for the title bar background (no stroke)
721
+ * 3. A bracket path drawn via the noise provider (outline + title divider)
722
+ *
723
+ * @param {SVGGElement} pGroup
724
+ * @param {Object} pNodeData
725
+ * @param {number} pWidth
726
+ * @param {number} pHeight
727
+ * @param {number} pTitleBarHeight
728
+ * @param {Object} pNodeTypeConfig
729
+ */
730
+ _renderBracketNodeBody(pGroup, pNodeData, pWidth, pHeight, pTitleBarHeight, pNodeTypeConfig)
731
+ {
732
+ // 1. Body fill rect (background only, no stroke)
733
+ let tmpBodyFill = this._FlowView._SVGHelperProvider.createSVGElement('rect');
734
+ tmpBodyFill.setAttribute('class', 'pict-flow-node-body pict-flow-node-bracket-fill');
735
+ tmpBodyFill.setAttribute('x', '0');
736
+ tmpBodyFill.setAttribute('y', '0');
737
+ tmpBodyFill.setAttribute('width', String(pWidth));
738
+ tmpBodyFill.setAttribute('height', String(pHeight));
739
+ tmpBodyFill.setAttribute('data-node-hash', pNodeData.Hash);
740
+ tmpBodyFill.setAttribute('data-element-type', 'node-body');
741
+
742
+ // Per-instance style overrides
743
+ if (pNodeData.Style)
744
+ {
745
+ let tmpInlineStyles = [];
746
+ if (pNodeData.Style.BodyFill) tmpInlineStyles.push('fill:' + pNodeData.Style.BodyFill);
747
+ if (tmpInlineStyles.length > 0)
748
+ {
749
+ tmpBodyFill.setAttribute('style', tmpInlineStyles.join(';'));
750
+ }
751
+ }
752
+
753
+ pGroup.appendChild(tmpBodyFill);
754
+
755
+ // 2. Title bar fill rect (background only, no stroke)
756
+ let tmpTitleFill = this._FlowView._SVGHelperProvider.createSVGElement('rect');
757
+ tmpTitleFill.setAttribute('class', 'pict-flow-node-title-bar pict-flow-node-bracket-title-fill');
758
+ tmpTitleFill.setAttribute('x', '0');
759
+ tmpTitleFill.setAttribute('y', '0');
760
+ tmpTitleFill.setAttribute('width', String(pWidth));
761
+ tmpTitleFill.setAttribute('height', String(pTitleBarHeight));
762
+ tmpTitleFill.setAttribute('data-node-hash', pNodeData.Hash);
763
+ tmpTitleFill.setAttribute('data-element-type', 'node-body');
764
+
765
+ if (pNodeTypeConfig && pNodeTypeConfig.TitleBarColor)
766
+ {
767
+ tmpTitleFill.setAttribute('style', 'fill:' + pNodeTypeConfig.TitleBarColor);
768
+ }
769
+ if (pNodeData.Style && pNodeData.Style.TitleBarColor)
770
+ {
771
+ tmpTitleFill.setAttribute('style', 'fill:' + pNodeData.Style.TitleBarColor);
772
+ }
773
+
774
+ pGroup.appendChild(tmpTitleFill);
775
+
776
+ // 3. Bracket path (outline + title divider with optional noise)
777
+ let tmpBracketConfig = { SerifLength: 6, TitleSeparator: true };
778
+ if (this._FlowView._ThemeProvider)
779
+ {
780
+ let tmpActiveTheme = this._FlowView._ThemeProvider.getActiveTheme();
781
+ if (tmpActiveTheme && tmpActiveTheme.BracketConfig)
782
+ {
783
+ tmpBracketConfig = Object.assign(tmpBracketConfig, tmpActiveTheme.BracketConfig);
784
+ }
785
+ }
786
+
787
+ let tmpAmplitude = 0;
788
+ if (this._FlowView._ThemeProvider)
789
+ {
790
+ tmpAmplitude = this._FlowView._ThemeProvider.getNodeNoiseAmplitude();
791
+ }
792
+
793
+ let tmpBracketD = '';
794
+ if (this._FlowView._NoiseProvider)
795
+ {
796
+ tmpBracketD = this._FlowView._NoiseProvider.generateBracketPath(
797
+ pWidth, pHeight,
798
+ tmpBracketConfig.SerifLength,
799
+ tmpBracketConfig.TitleSeparator ? pTitleBarHeight : 0,
800
+ tmpAmplitude,
801
+ pNodeData.Hash
802
+ );
803
+ }
804
+
805
+ let tmpBracketPath = this._FlowView._SVGHelperProvider.createSVGElement('path');
806
+ tmpBracketPath.setAttribute('class', 'pict-flow-node-bracket');
807
+ tmpBracketPath.setAttribute('d', tmpBracketD);
808
+ tmpBracketPath.setAttribute('data-node-hash', pNodeData.Hash);
809
+ tmpBracketPath.setAttribute('data-element-type', 'node-body');
810
+
811
+ // Per-instance stroke overrides
812
+ if (pNodeData.Style)
813
+ {
814
+ let tmpInlineStyles = [];
815
+ if (pNodeData.Style.BodyStroke) tmpInlineStyles.push('stroke:' + pNodeData.Style.BodyStroke);
816
+ if (pNodeData.Style.BodyStrokeWidth) tmpInlineStyles.push('stroke-width:' + pNodeData.Style.BodyStrokeWidth);
817
+ if (tmpInlineStyles.length > 0)
818
+ {
819
+ tmpBracketPath.setAttribute('style', tmpInlineStyles.join(';'));
820
+ }
821
+ }
822
+
823
+ pGroup.appendChild(tmpBracketPath);
339
824
  }
340
825
  }
341
826