pict-section-flow 0.0.10 → 0.0.13
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 +1 -1
- package/README.md +176 -0
- package/docs/.nojekyll +0 -0
- package/docs/Architecture.md +303 -0
- package/docs/Custom-Styling.md +275 -0
- package/docs/Data_Model.md +158 -0
- package/docs/Event_System.md +156 -0
- package/docs/Getting_Started.md +237 -0
- package/docs/Implementation_Reference.md +528 -0
- package/docs/Layout_Persistence.md +117 -0
- package/docs/README.md +115 -52
- package/docs/_cover.md +11 -0
- package/docs/_sidebar.md +52 -0
- package/docs/_topbar.md +8 -0
- package/docs/api/PictFlowCard.md +216 -0
- package/docs/api/PictFlowCardPropertiesPanel.md +235 -0
- package/docs/api/addConnection.md +101 -0
- package/docs/api/addNode.md +137 -0
- package/docs/api/autoLayout.md +77 -0
- package/docs/api/getFlowData.md +112 -0
- package/docs/api/marshalToView.md +95 -0
- package/docs/api/openPanel.md +128 -0
- package/docs/api/registerHandler.md +174 -0
- package/docs/api/registerNodeType.md +142 -0
- package/docs/api/removeConnection.md +57 -0
- package/docs/api/removeNode.md +80 -0
- package/docs/api/saveLayout.md +152 -0
- package/docs/api/screenToSVGCoords.md +68 -0
- package/docs/api/selectNode.md +116 -0
- package/docs/api/setTheme.md +168 -0
- package/docs/api/setZoom.md +97 -0
- package/docs/api/toggleFullscreen.md +68 -0
- package/docs/card-help/EACH.md +19 -0
- package/docs/card-help/FREAD.md +24 -0
- package/docs/card-help/FWRITE.md +24 -0
- package/docs/card-help/GET.md +22 -0
- package/docs/card-help/ITE.md +23 -0
- package/docs/card-help/LOG.md +23 -0
- package/docs/card-help/NOTE.md +17 -0
- package/docs/card-help/PREV.md +18 -0
- package/docs/card-help/SET.md +27 -0
- package/docs/card-help/SPKL.md +22 -0
- package/docs/card-help/STAT.md +23 -0
- package/docs/card-help/SW.md +25 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +169 -0
- package/docs/retold-keyword-index.json +13942 -0
- package/example_applications/simple_cards/package.json +1 -0
- package/example_applications/simple_cards/source/card-help-content.js +16 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +2 -0
- package/package.json +11 -7
- package/scripts/generate-card-help.js +214 -0
- package/source/Pict-Section-Flow.js +4 -0
- package/source/PictFlowCard.js +3 -1
- package/source/providers/PictProvider-Flow-CSS.js +245 -152
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +24 -0
- package/source/providers/PictProvider-Flow-Geometry.js +195 -38
- package/source/providers/PictProvider-Flow-PanelChrome.js +14 -12
- package/source/services/PictService-Flow-ConnectionHandleManager.js +263 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +134 -183
- package/source/services/PictService-Flow-DataManager.js +338 -0
- package/source/services/PictService-Flow-InteractionManager.js +165 -7
- package/source/services/PictService-Flow-PathGenerator.js +282 -0
- package/source/services/PictService-Flow-PortRenderer.js +269 -0
- package/source/services/PictService-Flow-RenderManager.js +281 -0
- package/source/services/PictService-Flow-Tether.js +6 -42
- package/source/views/PictView-Flow-Node.js +2 -220
- package/source/views/PictView-Flow-PropertiesPanel.js +89 -44
- package/source/views/PictView-Flow.js +130 -882
- package/test/ConnectionHandleManager_tests.js +717 -0
- package/test/ConnectionRenderer_tests.js +591 -0
- package/test/DataManager_tests.js +859 -0
- package/test/Geometry_tests.js +767 -0
- package/test/PathGenerator_tests.js +978 -0
- package/test/PortRenderer_tests.js +367 -0
- package/test/RenderManager_tests.js +756 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PictService-Flow-RenderManager
|
|
5
|
+
*
|
|
6
|
+
* Orchestrates rendering of the flow diagram: nodes, connections,
|
|
7
|
+
* tethers, panels, SVG marker definitions, and node position updates.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from PictView-Flow.js to isolate rendering orchestration
|
|
10
|
+
* from data management and interaction handling.
|
|
11
|
+
*/
|
|
12
|
+
class PictServiceFlowRenderManager extends libFableServiceProviderBase
|
|
13
|
+
{
|
|
14
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
15
|
+
{
|
|
16
|
+
super(pFable, pOptions, pServiceHash);
|
|
17
|
+
|
|
18
|
+
this.serviceType = 'PictServiceFlowRenderManager';
|
|
19
|
+
|
|
20
|
+
this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Render the complete flow diagram
|
|
25
|
+
*/
|
|
26
|
+
renderFlow()
|
|
27
|
+
{
|
|
28
|
+
if (!this._FlowView) return;
|
|
29
|
+
if (!this._FlowView._NodesLayer || !this._FlowView._ConnectionsLayer) return;
|
|
30
|
+
|
|
31
|
+
// Clear existing SVG content
|
|
32
|
+
while (this._FlowView._NodesLayer.firstChild)
|
|
33
|
+
{
|
|
34
|
+
this._FlowView._NodesLayer.removeChild(this._FlowView._NodesLayer.firstChild);
|
|
35
|
+
}
|
|
36
|
+
while (this._FlowView._ConnectionsLayer.firstChild)
|
|
37
|
+
{
|
|
38
|
+
this._FlowView._ConnectionsLayer.removeChild(this._FlowView._ConnectionsLayer.firstChild);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Render connections first (behind nodes)
|
|
42
|
+
for (let i = 0; i < this._FlowView._FlowData.Connections.length; i++)
|
|
43
|
+
{
|
|
44
|
+
let tmpConnection = this._FlowView._FlowData.Connections[i];
|
|
45
|
+
let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedConnectionHash === tmpConnection.Hash);
|
|
46
|
+
|
|
47
|
+
this._FlowView._ConnectionRenderer.renderConnection(
|
|
48
|
+
tmpConnection,
|
|
49
|
+
this._FlowView._ConnectionsLayer,
|
|
50
|
+
tmpIsSelected
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Render nodes
|
|
55
|
+
for (let i = 0; i < this._FlowView._FlowData.Nodes.length; i++)
|
|
56
|
+
{
|
|
57
|
+
let tmpNode = this._FlowView._FlowData.Nodes[i];
|
|
58
|
+
let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedNodeHash === tmpNode.Hash);
|
|
59
|
+
let tmpNodeTypeConfig = this._FlowView._NodeTypeProvider.getNodeType(tmpNode.Type);
|
|
60
|
+
|
|
61
|
+
// Enrich saved port data with metadata from the node type's DefaultPorts.
|
|
62
|
+
// Saved flow data may not include PortType or may have stale Side values,
|
|
63
|
+
// so we match each port to its DefaultPort counterpart by Label and Direction,
|
|
64
|
+
// then copy over PortType and Side from the authoritative node type definition.
|
|
65
|
+
if (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultPorts && tmpNode.Ports)
|
|
66
|
+
{
|
|
67
|
+
for (let p = 0; p < tmpNode.Ports.length; p++)
|
|
68
|
+
{
|
|
69
|
+
let tmpPort = tmpNode.Ports[p];
|
|
70
|
+
for (let d = 0; d < tmpNodeTypeConfig.DefaultPorts.length; d++)
|
|
71
|
+
{
|
|
72
|
+
let tmpDefault = tmpNodeTypeConfig.DefaultPorts[d];
|
|
73
|
+
if (tmpDefault.Label === tmpPort.Label && tmpDefault.Direction === tmpPort.Direction)
|
|
74
|
+
{
|
|
75
|
+
if (tmpDefault.PortType)
|
|
76
|
+
{
|
|
77
|
+
tmpPort.PortType = tmpDefault.PortType;
|
|
78
|
+
}
|
|
79
|
+
if (tmpDefault.Side)
|
|
80
|
+
{
|
|
81
|
+
tmpPort.Side = tmpDefault.Side;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._FlowView._NodeView.renderNode(tmpNode, this._FlowView._NodesLayer, tmpIsSelected, tmpNodeTypeConfig);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Render properties panels and tethers
|
|
93
|
+
if (this._FlowView._PropertiesPanelView && this._FlowView._PanelsLayer && this._FlowView._TethersLayer)
|
|
94
|
+
{
|
|
95
|
+
this._FlowView._PropertiesPanelView.renderPanels(
|
|
96
|
+
this._FlowView._FlowData.OpenPanels,
|
|
97
|
+
this._FlowView._PanelsLayer,
|
|
98
|
+
this._FlowView._TethersLayer,
|
|
99
|
+
this._FlowView._FlowData.ViewState.SelectedTetherHash
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Update viewport transform
|
|
104
|
+
this._FlowView.updateViewportTransform();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Re-render a single connection (remove and re-add) for smooth drag performance.
|
|
109
|
+
* @param {string} pConnectionHash
|
|
110
|
+
*/
|
|
111
|
+
renderSingleConnection(pConnectionHash)
|
|
112
|
+
{
|
|
113
|
+
if (!this._FlowView || !this._FlowView._ConnectionsLayer) return;
|
|
114
|
+
|
|
115
|
+
// Remove existing elements for this connection
|
|
116
|
+
let tmpExisting = this._FlowView._ConnectionsLayer.querySelectorAll(`[data-connection-hash="${pConnectionHash}"]`);
|
|
117
|
+
for (let i = 0; i < tmpExisting.length; i++)
|
|
118
|
+
{
|
|
119
|
+
tmpExisting[i].remove();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let tmpConnection = this._FlowView.getConnection(pConnectionHash);
|
|
123
|
+
if (!tmpConnection) return;
|
|
124
|
+
|
|
125
|
+
let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedConnectionHash === pConnectionHash);
|
|
126
|
+
this._FlowView._ConnectionRenderer.renderConnection(tmpConnection, this._FlowView._ConnectionsLayer, tmpIsSelected);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Re-render a single tether (remove and re-add) for smooth drag performance.
|
|
131
|
+
* @param {string} pPanelHash
|
|
132
|
+
*/
|
|
133
|
+
renderSingleTether(pPanelHash)
|
|
134
|
+
{
|
|
135
|
+
if (!this._FlowView || !this._FlowView._TethersLayer || !this._FlowView._TetherService) return;
|
|
136
|
+
|
|
137
|
+
// Remove existing tether elements for this panel
|
|
138
|
+
let tmpExisting = this._FlowView._TethersLayer.querySelectorAll(`[data-panel-hash="${pPanelHash}"]`);
|
|
139
|
+
for (let i = 0; i < tmpExisting.length; i++)
|
|
140
|
+
{
|
|
141
|
+
tmpExisting[i].remove();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let tmpPanel = this._FlowView._FlowData.OpenPanels.find((pPanel) => pPanel.Hash === pPanelHash);
|
|
145
|
+
if (!tmpPanel) return;
|
|
146
|
+
|
|
147
|
+
let tmpNodeData = this._FlowView.getNode(tmpPanel.NodeHash);
|
|
148
|
+
if (!tmpNodeData) return;
|
|
149
|
+
|
|
150
|
+
let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedTetherHash === pPanelHash);
|
|
151
|
+
this._FlowView._TetherService.renderTether(tmpPanel, tmpNodeData, this._FlowView._TethersLayer, tmpIsSelected, this._FlowView.options.ViewIdentifier);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update a single node's position in the SVG without full re-render (for drag performance)
|
|
156
|
+
* @param {string} pNodeHash
|
|
157
|
+
* @param {number} pX
|
|
158
|
+
* @param {number} pY
|
|
159
|
+
*/
|
|
160
|
+
updateNodePosition(pNodeHash, pX, pY)
|
|
161
|
+
{
|
|
162
|
+
if (!this._FlowView) return;
|
|
163
|
+
|
|
164
|
+
let tmpNode = this._FlowView.getNode(pNodeHash);
|
|
165
|
+
if (!tmpNode) return;
|
|
166
|
+
|
|
167
|
+
if (this._FlowView.options.EnableGridSnap)
|
|
168
|
+
{
|
|
169
|
+
pX = this._FlowView._LayoutService.snapToGrid(pX, this._FlowView.options.GridSnapSize);
|
|
170
|
+
pY = this._FlowView._LayoutService.snapToGrid(pY, this._FlowView.options.GridSnapSize);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
tmpNode.X = pX;
|
|
174
|
+
tmpNode.Y = pY;
|
|
175
|
+
|
|
176
|
+
// Reset customized handle positions for connections/tethers involving this node
|
|
177
|
+
this._FlowView._resetHandlesForNode(pNodeHash);
|
|
178
|
+
|
|
179
|
+
// Update the node's SVG group transform for smooth dragging
|
|
180
|
+
let tmpNodeGroup = this._FlowView._NodesLayer.querySelector(`[data-node-hash="${pNodeHash}"]`);
|
|
181
|
+
if (tmpNodeGroup)
|
|
182
|
+
{
|
|
183
|
+
tmpNodeGroup.setAttribute('transform', `translate(${pX}, ${pY})`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Re-render connections that involve this node
|
|
187
|
+
this.renderConnectionsForNode(pNodeHash);
|
|
188
|
+
|
|
189
|
+
// Update tethers for any panels attached to this node
|
|
190
|
+
this.renderTethersForNode(pNodeHash);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Re-render only connections that involve a specific node (for drag performance)
|
|
195
|
+
* @param {string} pNodeHash
|
|
196
|
+
*/
|
|
197
|
+
renderConnectionsForNode(pNodeHash)
|
|
198
|
+
{
|
|
199
|
+
if (!this._FlowView || !this._FlowView._ConnectionsLayer) return;
|
|
200
|
+
|
|
201
|
+
let tmpAffectedConnections = this._FlowView._FlowData.Connections.filter((pConn) =>
|
|
202
|
+
{
|
|
203
|
+
return pConn.SourceNodeHash === pNodeHash || pConn.TargetNodeHash === pNodeHash;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < tmpAffectedConnections.length; i++)
|
|
207
|
+
{
|
|
208
|
+
let tmpConn = tmpAffectedConnections[i];
|
|
209
|
+
let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedConnectionHash === tmpConn.Hash);
|
|
210
|
+
|
|
211
|
+
// Remove existing connection SVG elements
|
|
212
|
+
let tmpExisting = this._FlowView._ConnectionsLayer.querySelectorAll(`[data-connection-hash="${tmpConn.Hash}"]`);
|
|
213
|
+
for (let j = 0; j < tmpExisting.length; j++)
|
|
214
|
+
{
|
|
215
|
+
tmpExisting[j].remove();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Re-render this connection
|
|
219
|
+
this._FlowView._ConnectionRenderer.renderConnection(tmpConn, this._FlowView._ConnectionsLayer, tmpIsSelected);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Re-render tethers for panels attached to a specific node (for drag performance).
|
|
225
|
+
* @param {string} pNodeHash
|
|
226
|
+
*/
|
|
227
|
+
renderTethersForNode(pNodeHash)
|
|
228
|
+
{
|
|
229
|
+
if (!this._FlowView || !this._FlowView._TethersLayer || !this._FlowView._TetherService) return;
|
|
230
|
+
|
|
231
|
+
let tmpAffectedPanels = this._FlowView._FlowData.OpenPanels.filter((pPanel) => pPanel.NodeHash === pNodeHash);
|
|
232
|
+
if (tmpAffectedPanels.length === 0) return;
|
|
233
|
+
|
|
234
|
+
// Remove existing tethers for these panels and re-render via TetherService
|
|
235
|
+
for (let i = 0; i < tmpAffectedPanels.length; i++)
|
|
236
|
+
{
|
|
237
|
+
let tmpExisting = this._FlowView._TethersLayer.querySelectorAll(`[data-panel-hash="${tmpAffectedPanels[i].Hash}"]`);
|
|
238
|
+
for (let j = 0; j < tmpExisting.length; j++)
|
|
239
|
+
{
|
|
240
|
+
tmpExisting[j].remove();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let tmpNodeData = this._FlowView.getNode(tmpAffectedPanels[i].NodeHash);
|
|
244
|
+
if (!tmpNodeData) continue;
|
|
245
|
+
|
|
246
|
+
let tmpIsSelected = (this._FlowView._FlowData.ViewState.SelectedTetherHash === tmpAffectedPanels[i].Hash);
|
|
247
|
+
this._FlowView._TetherService.renderTether(tmpAffectedPanels[i], tmpNodeData, this._FlowView._TethersLayer, tmpIsSelected, this._FlowView.options.ViewIdentifier);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Re-inject SVG marker definitions (arrowheads).
|
|
253
|
+
* Called after a theme switch to update arrowhead colors.
|
|
254
|
+
*/
|
|
255
|
+
reinjectMarkerDefs()
|
|
256
|
+
{
|
|
257
|
+
if (!this._FlowView || !this._FlowView._ConnectorShapesProvider || !this._FlowView._SVGElement) return;
|
|
258
|
+
|
|
259
|
+
let tmpViewIdentifier = this._FlowView.options.ViewIdentifier;
|
|
260
|
+
let tmpDefs = this._FlowView._SVGElement.querySelector('defs');
|
|
261
|
+
if (!tmpDefs) return;
|
|
262
|
+
|
|
263
|
+
// Remove existing marker elements
|
|
264
|
+
let tmpExistingMarkers = tmpDefs.querySelectorAll('marker');
|
|
265
|
+
for (let i = 0; i < tmpExistingMarkers.length; i++)
|
|
266
|
+
{
|
|
267
|
+
tmpExistingMarkers[i].remove();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Re-generate and inject
|
|
271
|
+
let tmpMarkerMarkup = this._FlowView._ConnectorShapesProvider.generateMarkerDefs(tmpViewIdentifier);
|
|
272
|
+
let tmpTempSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
273
|
+
tmpTempSVG.innerHTML = tmpMarkerMarkup;
|
|
274
|
+
while (tmpTempSVG.firstChild)
|
|
275
|
+
{
|
|
276
|
+
tmpDefs.appendChild(tmpTempSVG.firstChild);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = PictServiceFlowRenderManager;
|
|
@@ -224,27 +224,7 @@ class PictServiceFlowTether extends libFableServiceProviderBase
|
|
|
224
224
|
*/
|
|
225
225
|
getAutoMidpoint(pFrom, pTo)
|
|
226
226
|
{
|
|
227
|
-
|
|
228
|
-
let tmpFromDir = this._FlowView._GeometryProvider.sideDirection(pFrom.side);
|
|
229
|
-
let tmpToDir = this._FlowView._GeometryProvider.sideDirection(pTo.side);
|
|
230
|
-
|
|
231
|
-
let tmpDepartX = pFrom.x + tmpFromDir.dx * tmpDepartDist;
|
|
232
|
-
let tmpDepartY = pFrom.y + tmpFromDir.dy * tmpDepartDist;
|
|
233
|
-
let tmpApproachX = pTo.x + tmpToDir.dx * tmpDepartDist;
|
|
234
|
-
let tmpApproachY = pTo.y + tmpToDir.dy * tmpDepartDist;
|
|
235
|
-
|
|
236
|
-
let tmpSpanX = Math.abs(tmpApproachX - tmpDepartX);
|
|
237
|
-
let tmpSpanY = Math.abs(tmpApproachY - tmpDepartY);
|
|
238
|
-
let tmpSpan = Math.max(tmpSpanX, tmpSpanY, 40);
|
|
239
|
-
let tmpCPDist = tmpSpan * 0.4;
|
|
240
|
-
|
|
241
|
-
let tmpP0 = { x: tmpDepartX, y: tmpDepartY };
|
|
242
|
-
let tmpP1 = { x: tmpDepartX + tmpFromDir.dx * tmpCPDist, y: tmpDepartY + tmpFromDir.dy * tmpCPDist };
|
|
243
|
-
let tmpP2 = { x: tmpApproachX + tmpToDir.dx * tmpCPDist, y: tmpApproachY + tmpToDir.dy * tmpCPDist };
|
|
244
|
-
let tmpP3 = { x: tmpApproachX, y: tmpApproachY };
|
|
245
|
-
|
|
246
|
-
// Evaluate cubic bezier at t=0.5
|
|
247
|
-
return this._FlowView._PathGenerator.evaluateCubicBezier(tmpP0, tmpP1, tmpP2, tmpP3, 0.5);
|
|
227
|
+
return this._FlowView._PathGenerator.getAutoMidpointSimple(pFrom, pTo, 20);
|
|
248
228
|
}
|
|
249
229
|
|
|
250
230
|
/**
|
|
@@ -541,30 +521,14 @@ class PictServiceFlowTether extends libFableServiceProviderBase
|
|
|
541
521
|
*/
|
|
542
522
|
_createHandle(pLayer, pPanelHash, pHandleType, pX, pY, pClassName)
|
|
543
523
|
{
|
|
544
|
-
|
|
524
|
+
if (!this._FlowView._ConnectorShapesProvider) return;
|
|
525
|
+
|
|
545
526
|
let tmpShapeKey = (pClassName === 'pict-flow-tether-handle-midpoint')
|
|
546
527
|
? 'tether-handle-midpoint' : 'tether-handle';
|
|
547
528
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
pPanelHash, pHandleType, pX, pY, tmpShapeKey);
|
|
552
|
-
tmpHandle.setAttribute('data-element-type', 'tether-handle');
|
|
553
|
-
tmpHandle.setAttribute('data-panel-hash', pPanelHash);
|
|
554
|
-
pLayer.appendChild(tmpHandle);
|
|
555
|
-
}
|
|
556
|
-
else
|
|
557
|
-
{
|
|
558
|
-
let tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
|
|
559
|
-
tmpCircle.setAttribute('class', pClassName);
|
|
560
|
-
tmpCircle.setAttribute('cx', String(pX));
|
|
561
|
-
tmpCircle.setAttribute('cy', String(pY));
|
|
562
|
-
tmpCircle.setAttribute('r', '6');
|
|
563
|
-
tmpCircle.setAttribute('data-element-type', 'tether-handle');
|
|
564
|
-
tmpCircle.setAttribute('data-panel-hash', pPanelHash);
|
|
565
|
-
tmpCircle.setAttribute('data-handle-type', pHandleType);
|
|
566
|
-
pLayer.appendChild(tmpCircle);
|
|
567
|
-
}
|
|
529
|
+
this._FlowView._ConnectorShapesProvider.createFullHandle(
|
|
530
|
+
pLayer, pPanelHash, pHandleType, pX, pY,
|
|
531
|
+
tmpShapeKey, 'tether-handle', 'data-panel-hash');
|
|
568
532
|
}
|
|
569
533
|
}
|
|
570
534
|
|
|
@@ -296,7 +296,7 @@ class PictViewFlowNode extends libPictView
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/**
|
|
299
|
-
* Render ports for a node
|
|
299
|
+
* Render ports for a node — delegates to the PortRenderer service.
|
|
300
300
|
* @param {Object} pNodeData
|
|
301
301
|
* @param {SVGGElement} pGroup - The node's SVG group
|
|
302
302
|
* @param {number} pWidth
|
|
@@ -305,225 +305,7 @@ class PictViewFlowNode extends libPictView
|
|
|
305
305
|
*/
|
|
306
306
|
_renderPorts(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig)
|
|
307
307
|
{
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
let tmpPortLabelsVertical = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsVertical);
|
|
311
|
-
let tmpPortLabelPadding = (pNodeTypeConfig && pNodeTypeConfig.PortLabelPadding);
|
|
312
|
-
let tmpPortLabelsOutside = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsOutside);
|
|
313
|
-
let tmpGeometryProvider = this._FlowView._GeometryProvider;
|
|
314
|
-
|
|
315
|
-
// Group ports by their Side value (supports all 12 positions)
|
|
316
|
-
let tmpPortsBySide = {};
|
|
317
|
-
for (let i = 0; i < pNodeData.Ports.length; i++)
|
|
318
|
-
{
|
|
319
|
-
let tmpPort = pNodeData.Ports[i];
|
|
320
|
-
let tmpSide = tmpPort.Side || (tmpPort.Direction === 'input' ? 'left' : 'right');
|
|
321
|
-
if (!tmpPortsBySide[tmpSide])
|
|
322
|
-
{
|
|
323
|
-
tmpPortsBySide[tmpSide] = [];
|
|
324
|
-
}
|
|
325
|
-
tmpPortsBySide[tmpSide].push(tmpPort);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
for (let tmpSide in tmpPortsBySide)
|
|
329
|
-
{
|
|
330
|
-
let tmpPorts = tmpPortsBySide[tmpSide];
|
|
331
|
-
// Determine the edge for label positioning
|
|
332
|
-
let tmpEdge = tmpGeometryProvider ? tmpGeometryProvider.getEdgeFromSide(tmpSide) : tmpSide;
|
|
333
|
-
|
|
334
|
-
for (let i = 0; i < tmpPorts.length; i++)
|
|
335
|
-
{
|
|
336
|
-
let tmpPort = tmpPorts[i];
|
|
337
|
-
let tmpPosition = this._getPortLocalPosition(tmpSide, i, tmpPorts.length, pWidth, pHeight);
|
|
338
|
-
|
|
339
|
-
// Port label badge — flush against the node edge with no
|
|
340
|
-
// border on the edge side; rendered before the port circle
|
|
341
|
-
// so the circle visually sits on top of the badge
|
|
342
|
-
let tmpLabelElement = null;
|
|
343
|
-
if (tmpPort.Label)
|
|
344
|
-
{
|
|
345
|
-
let tmpPortTypeColorMap =
|
|
346
|
-
{
|
|
347
|
-
'event-in': '#3498db',
|
|
348
|
-
'event-out': '#2ecc71',
|
|
349
|
-
'setting': '#e67e22',
|
|
350
|
-
'value': '#f1c40f',
|
|
351
|
-
'error': '#e74c3c'
|
|
352
|
-
};
|
|
353
|
-
let tmpBorderColor = tmpPort.PortType ? (tmpPortTypeColorMap[tmpPort.PortType] || '#95a5a6') : '#95a5a6';
|
|
354
|
-
|
|
355
|
-
let tmpBadgeHeight = 12;
|
|
356
|
-
let tmpBadgePadH = 5;
|
|
357
|
-
let tmpBadgeBorderW = 2;
|
|
358
|
-
let tmpEdgePad = 1;
|
|
359
|
-
let tmpPortRadius = 5;
|
|
360
|
-
|
|
361
|
-
let tmpTextLen = tmpPort.Label.length * 5;
|
|
362
|
-
let tmpBadgeX, tmpBadgeY, tmpBadgeWidth;
|
|
363
|
-
let tmpTextX, tmpTextAnchor;
|
|
364
|
-
let tmpStripeX, tmpStripeY, tmpStripeW, tmpStripeH;
|
|
365
|
-
let tmpBorderPath;
|
|
366
|
-
|
|
367
|
-
if (tmpEdge === 'left')
|
|
368
|
-
{
|
|
369
|
-
tmpBadgeWidth = tmpPortRadius + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpBadgeBorderW;
|
|
370
|
-
tmpBadgeX = tmpEdgePad;
|
|
371
|
-
tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
|
|
372
|
-
tmpTextX = tmpBadgeX + tmpPortRadius + tmpBadgePadH;
|
|
373
|
-
tmpTextAnchor = 'start';
|
|
374
|
-
tmpStripeX = tmpBadgeX + tmpBadgeWidth - tmpBadgeBorderW;
|
|
375
|
-
tmpStripeY = tmpBadgeY;
|
|
376
|
-
tmpStripeW = tmpBadgeBorderW;
|
|
377
|
-
tmpStripeH = tmpBadgeHeight;
|
|
378
|
-
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
379
|
-
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
380
|
-
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
381
|
-
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
382
|
-
}
|
|
383
|
-
else if (tmpEdge === 'right')
|
|
384
|
-
{
|
|
385
|
-
tmpBadgeWidth = tmpBadgeBorderW + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpPortRadius;
|
|
386
|
-
tmpBadgeX = pWidth - tmpBadgeWidth - tmpEdgePad;
|
|
387
|
-
tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
|
|
388
|
-
tmpTextX = tmpBadgeX + tmpBadgeBorderW + tmpBadgePadH;
|
|
389
|
-
tmpTextAnchor = 'start';
|
|
390
|
-
tmpStripeX = tmpBadgeX;
|
|
391
|
-
tmpStripeY = tmpBadgeY;
|
|
392
|
-
tmpStripeW = tmpBadgeBorderW;
|
|
393
|
-
tmpStripeH = tmpBadgeHeight;
|
|
394
|
-
tmpBorderPath = 'M ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
395
|
-
+ ' L ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
396
|
-
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
397
|
-
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
398
|
-
}
|
|
399
|
-
else if (tmpEdge === 'top')
|
|
400
|
-
{
|
|
401
|
-
tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
|
|
402
|
-
tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
|
|
403
|
-
tmpBadgeY = tmpEdgePad;
|
|
404
|
-
tmpTextX = tmpPosition.x;
|
|
405
|
-
tmpTextAnchor = 'middle';
|
|
406
|
-
tmpStripeX = tmpBadgeX;
|
|
407
|
-
tmpStripeY = tmpBadgeY + tmpBadgeHeight - tmpBadgeBorderW;
|
|
408
|
-
tmpStripeW = tmpBadgeWidth;
|
|
409
|
-
tmpStripeH = tmpBadgeBorderW;
|
|
410
|
-
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
411
|
-
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
412
|
-
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
413
|
-
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY;
|
|
414
|
-
}
|
|
415
|
-
else
|
|
416
|
-
{
|
|
417
|
-
tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
|
|
418
|
-
tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
|
|
419
|
-
tmpBadgeY = pHeight - tmpBadgeHeight - tmpEdgePad;
|
|
420
|
-
tmpTextX = tmpPosition.x;
|
|
421
|
-
tmpTextAnchor = 'middle';
|
|
422
|
-
tmpStripeX = tmpBadgeX;
|
|
423
|
-
tmpStripeY = tmpBadgeY;
|
|
424
|
-
tmpStripeW = tmpBadgeWidth;
|
|
425
|
-
tmpStripeH = tmpBadgeBorderW;
|
|
426
|
-
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
427
|
-
+ ' L ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
428
|
-
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
429
|
-
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Background rect (cream, no stroke — border drawn separately)
|
|
433
|
-
let tmpBgRect = this._FlowView._SVGHelperProvider.createSVGElement('rect');
|
|
434
|
-
tmpBgRect.setAttribute('class', 'pict-flow-port-label-bg');
|
|
435
|
-
tmpBgRect.setAttribute('x', String(tmpBadgeX));
|
|
436
|
-
tmpBgRect.setAttribute('y', String(tmpBadgeY));
|
|
437
|
-
tmpBgRect.setAttribute('width', String(tmpBadgeWidth));
|
|
438
|
-
tmpBgRect.setAttribute('height', String(tmpBadgeHeight));
|
|
439
|
-
tmpBgRect.setAttribute('fill', 'var(--pf-port-label-bg, rgba(255, 253, 240, 0.5))');
|
|
440
|
-
pGroup.appendChild(tmpBgRect);
|
|
441
|
-
|
|
442
|
-
// 3-sided border path (open on the edge-facing side)
|
|
443
|
-
let tmpBorderPathEl = this._FlowView._SVGHelperProvider.createSVGElement('path');
|
|
444
|
-
tmpBorderPathEl.setAttribute('class', 'pict-flow-port-label-bg');
|
|
445
|
-
tmpBorderPathEl.setAttribute('d', tmpBorderPath);
|
|
446
|
-
tmpBorderPathEl.setAttribute('fill', 'none');
|
|
447
|
-
tmpBorderPathEl.setAttribute('stroke', tmpBorderColor);
|
|
448
|
-
tmpBorderPathEl.setAttribute('stroke-width', '0.75');
|
|
449
|
-
pGroup.appendChild(tmpBorderPathEl);
|
|
450
|
-
|
|
451
|
-
// Colored stripe on the inner side
|
|
452
|
-
let tmpStripe = this._FlowView._SVGHelperProvider.createSVGElement('rect');
|
|
453
|
-
tmpStripe.setAttribute('class', 'pict-flow-port-label-bg');
|
|
454
|
-
tmpStripe.setAttribute('x', String(tmpStripeX));
|
|
455
|
-
tmpStripe.setAttribute('y', String(tmpStripeY));
|
|
456
|
-
tmpStripe.setAttribute('width', String(tmpStripeW));
|
|
457
|
-
tmpStripe.setAttribute('height', String(tmpStripeH));
|
|
458
|
-
tmpStripe.setAttribute('fill', tmpBorderColor);
|
|
459
|
-
pGroup.appendChild(tmpStripe);
|
|
460
|
-
|
|
461
|
-
// Text label — appended after circle for z-order
|
|
462
|
-
tmpLabelElement = this._FlowView._SVGHelperProvider.createSVGElement('text');
|
|
463
|
-
tmpLabelElement.setAttribute('class', 'pict-flow-port-label');
|
|
464
|
-
tmpLabelElement.setAttribute('fill', 'var(--pf-port-label-text, #2c3e50)');
|
|
465
|
-
tmpLabelElement.textContent = tmpPort.Label;
|
|
466
|
-
tmpLabelElement.setAttribute('x', String(tmpTextX));
|
|
467
|
-
tmpLabelElement.setAttribute('y', String(tmpBadgeY + tmpBadgeHeight / 2));
|
|
468
|
-
tmpLabelElement.setAttribute('text-anchor', tmpTextAnchor);
|
|
469
|
-
tmpLabelElement.setAttribute('dominant-baseline', 'central');
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Port circle (rendered on top of badge background)
|
|
473
|
-
let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
|
|
474
|
-
let tmpCircle;
|
|
475
|
-
if (tmpShapeProvider)
|
|
476
|
-
{
|
|
477
|
-
tmpCircle = tmpShapeProvider.createPortElement(tmpPort, tmpPosition, pNodeData.Hash);
|
|
478
|
-
}
|
|
479
|
-
else
|
|
480
|
-
{
|
|
481
|
-
tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
|
|
482
|
-
let tmpPortClass = `pict-flow-port ${tmpPort.Direction}`;
|
|
483
|
-
if (tmpPort.PortType)
|
|
484
|
-
{
|
|
485
|
-
tmpPortClass += ` port-type-${tmpPort.PortType}`;
|
|
486
|
-
}
|
|
487
|
-
tmpCircle.setAttribute('class', tmpPortClass);
|
|
488
|
-
tmpCircle.setAttribute('cx', String(tmpPosition.x));
|
|
489
|
-
tmpCircle.setAttribute('cy', String(tmpPosition.y));
|
|
490
|
-
tmpCircle.setAttribute('r', '5');
|
|
491
|
-
tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
|
|
492
|
-
tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
|
|
493
|
-
tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
|
|
494
|
-
if (tmpPort.PortType)
|
|
495
|
-
{
|
|
496
|
-
tmpCircle.setAttribute('data-port-type', tmpPort.PortType);
|
|
497
|
-
}
|
|
498
|
-
tmpCircle.setAttribute('data-element-type', 'port');
|
|
499
|
-
}
|
|
500
|
-
pGroup.appendChild(tmpCircle);
|
|
501
|
-
|
|
502
|
-
// Port label text (on top of everything)
|
|
503
|
-
if (tmpLabelElement)
|
|
504
|
-
{
|
|
505
|
-
pGroup.appendChild(tmpLabelElement);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Calculate port position relative to node origin.
|
|
513
|
-
*
|
|
514
|
-
* For left and right side ports, positioning is offset below the title bar
|
|
515
|
-
* so that ports never overlap the header area.
|
|
516
|
-
*
|
|
517
|
-
* @param {string} pSide - 'left', 'right', 'top', 'bottom'
|
|
518
|
-
* @param {number} pIndex - Index of this port on its side
|
|
519
|
-
* @param {number} pTotal - Total ports on this side
|
|
520
|
-
* @param {number} pWidth - Node width
|
|
521
|
-
* @param {number} pHeight - Node height
|
|
522
|
-
* @returns {{x: number, y: number}}
|
|
523
|
-
*/
|
|
524
|
-
_getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight)
|
|
525
|
-
{
|
|
526
|
-
return this._FlowView._GeometryProvider.getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, this.options.NodeTitleBarHeight);
|
|
308
|
+
this._FlowView._PortRenderer.renderPorts(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig, this.options.NodeTitleBarHeight);
|
|
527
309
|
}
|
|
528
310
|
|
|
529
311
|
/**
|