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.
- 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 +7 -5
- package/source/Pict-Section-Flow.js +8 -1
- package/source/PictFlowCard.js +52 -1
- package/source/panels/FlowCardPropertiesPanel-Template.js +1 -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 +136 -2
- 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 +668 -174
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
228
|
-
let
|
|
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]
|
|
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
|
|
249
|
-
tmpCircle
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
474
|
+
// Register any templates defined in the BodyContent config (once)
|
|
475
|
+
if (tmpBodyContent.Templates && Array.isArray(tmpBodyContent.Templates))
|
|
316
476
|
{
|
|
317
|
-
|
|
477
|
+
if (!this._registeredBodyTemplates)
|
|
318
478
|
{
|
|
319
|
-
|
|
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
|
-
|
|
481
|
+
for (let i = 0; i < tmpBodyContent.Templates.length; i++)
|
|
325
482
|
{
|
|
326
|
-
let
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
|