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.
- package/.claude/launch.json +11 -0
- package/docs/README.md +51 -0
- package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +105 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +36 -0
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +42 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +98 -0
- package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +44 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +1 -1
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +9 -1
- package/package.json +2 -2
- package/source/Pict-Section-Flow.js +8 -1
- package/source/PictFlowCard.js +49 -1
- package/source/providers/PictProvider-Flow-CSS.js +1440 -0
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
- package/source/providers/PictProvider-Flow-Geometry.js +43 -0
- package/source/providers/PictProvider-Flow-Icons.js +335 -0
- package/source/providers/PictProvider-Flow-Layouts.js +214 -2
- package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
- package/source/providers/PictProvider-Flow-Noise.js +241 -0
- package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
- package/source/providers/PictProvider-Flow-Theme.js +755 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
- package/source/services/PictService-Flow-PanelManager.js +188 -0
- package/source/services/PictService-Flow-SelectionManager.js +109 -0
- package/source/services/PictService-Flow-Tether.js +52 -25
- package/source/services/PictService-Flow-ViewportManager.js +176 -0
- package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
- package/source/views/PictView-Flow-Node.js +654 -169
- package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
- package/source/views/PictView-Flow-Toolbar.js +846 -379
- 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
|
-
|
|
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
|
-
//
|
|
45
|
-
let
|
|
46
|
-
|
|
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
|
-
|
|
56
|
+
let tmpActiveTheme = this._FlowView._ThemeProvider.getActiveTheme();
|
|
57
|
+
if (tmpActiveTheme && tmpActiveTheme.NodeBodyMode)
|
|
58
58
|
{
|
|
59
|
-
|
|
59
|
+
tmpNodeBodyMode = tmpActiveTheme.NodeBodyMode;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
let
|
|
85
|
-
|
|
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.
|
|
78
|
+
if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata)
|
|
94
79
|
{
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
111
|
-
|
|
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
|
-
//
|
|
124
|
-
|
|
110
|
+
// Helper: render type label + code badge + tooltip (the "middle labels")
|
|
111
|
+
let tmpRenderTypeLabels = () =>
|
|
125
112
|
{
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
//
|
|
130
|
-
if (
|
|
126
|
+
// FlowCard metadata: icon in title bar, code badge in body (hover-only via CSS)
|
|
127
|
+
if (pNodeTypeConfig && pNodeTypeConfig.CardMetadata)
|
|
131
128
|
{
|
|
132
|
-
let
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
//
|
|
142
|
-
tmpIconText.
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
188
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
tmpIndicator
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
249
|
-
tmpCircle
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
468
|
+
if (!this._registeredBodyTemplates)
|
|
318
469
|
{
|
|
319
|
-
|
|
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
|
-
|
|
472
|
+
for (let i = 0; i < tmpBodyContent.Templates.length; i++)
|
|
325
473
|
{
|
|
326
|
-
let
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
|