pict-section-flow 0.0.16 → 0.0.18
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/README.md +18 -18
- package/docs/Architecture.md +1 -1
- package/docs/Data_Model.md +2 -2
- package/docs/Getting_Started.md +5 -5
- package/docs/Implementation_Reference.md +6 -6
- package/docs/Layout_Persistence.md +3 -3
- package/docs/README.md +12 -12
- package/docs/_cover.md +1 -1
- package/docs/_sidebar.md +6 -6
- package/docs/_version.json +7 -0
- package/docs/api/PictFlowCard.md +6 -6
- package/docs/api/PictFlowCardPropertiesPanel.md +2 -2
- package/docs/api/addConnection.md +4 -4
- package/docs/api/addNode.md +6 -6
- package/docs/api/autoLayout.md +2 -2
- package/docs/api/getFlowData.md +5 -5
- package/docs/api/marshalToView.md +3 -3
- package/docs/api/openPanel.md +2 -2
- package/docs/api/registerHandler.md +3 -3
- package/docs/api/registerNodeType.md +3 -3
- package/docs/api/removeConnection.md +5 -5
- package/docs/api/removeNode.md +6 -6
- package/docs/api/saveLayout.md +2 -2
- package/docs/api/screenToSVGCoords.md +2 -2
- package/docs/api/selectNode.md +3 -3
- package/docs/api/setTheme.md +2 -2
- package/docs/api/setZoom.md +3 -3
- package/docs/api/toggleFullscreen.md +2 -2
- package/docs/card-help/EACH.md +3 -3
- package/docs/card-help/FREAD.md +5 -5
- package/docs/card-help/FWRITE.md +5 -5
- package/docs/card-help/GET.md +2 -2
- package/docs/card-help/ITE.md +3 -3
- package/docs/card-help/LOG.md +4 -4
- package/docs/card-help/NOTE.md +1 -1
- package/docs/card-help/PREV.md +2 -2
- package/docs/card-help/SET.md +5 -5
- package/docs/card-help/SPKL.md +2 -2
- package/docs/card-help/STAT.md +3 -3
- package/docs/card-help/SW.md +4 -4
- package/docs/css/docuserve.css +277 -23
- package/docs/index.html +2 -2
- package/docs/retold-catalog.json +1 -1
- package/docs/retold-keyword-index.json +1 -1
- package/example_applications/simple_cards/css/flowexample.css +2 -2
- package/example_applications/simple_cards/source/card-help-content.js +12 -12
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +1 -1
- package/example_applications/simple_cards/source/sample-flows.js +410 -0
- package/example_applications/simple_cards/source/views/PictView-FlowExample-About.js +5 -5
- package/example_applications/simple_cards/source/views/PictView-FlowExample-Documentation.js +5 -5
- package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +4 -4
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +141 -8
- package/example_applications/simple_cards/source/views/PictView-FlowExample-TopBar.js +2 -2
- package/package.json +3 -2
- package/source/Pict-Section-Flow.js +26 -0
- package/source/providers/PictProvider-Flow-CSS.js +244 -14
- package/source/providers/PictProvider-Flow-Theme.js +7 -7
- package/source/providers/edges/Edge-Bezier.js +41 -0
- package/source/providers/edges/Edge-Orthogonal.js +37 -0
- package/source/providers/edges/Edge-OrthogonalSnap.js +72 -0
- package/source/providers/edges/Edge-Perimeter-Linear.js +31 -0
- package/source/providers/edges/Edge-Perimeter-Orthogonal.js +39 -0
- package/source/providers/edges/Edge-Perimeter.js +48 -0
- package/source/providers/edges/Edge-PerimeterMath.js +92 -0
- package/source/providers/edges/Edge-Straight.js +24 -0
- package/source/providers/layouts/Layout-Circular.js +203 -0
- package/source/providers/layouts/Layout-Coerce.js +40 -0
- package/source/providers/layouts/Layout-Columnar.js +134 -0
- package/source/providers/layouts/Layout-Custom.js +27 -0
- package/source/providers/layouts/Layout-ForcedFromCenter.js +256 -0
- package/source/providers/layouts/Layout-Grid.js +134 -0
- package/source/providers/layouts/Layout-Layered.js +209 -0
- package/source/providers/layouts/Layout-Tabular.js +94 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +532 -28
- package/source/services/PictService-Flow-DataManager.js +12 -1
- package/source/services/PictService-Flow-Layout.js +305 -121
- package/source/services/PictService-Flow-PortRenderer.js +122 -26
- package/source/services/PictService-Flow-RenderManager.js +41 -11
- package/source/views/PictView-Flow-FloatingToolbar.js +3 -3
- package/source/views/PictView-Flow-Node.js +28 -0
- package/source/views/PictView-Flow-Toolbar.js +715 -10
- package/source/views/PictView-Flow.js +272 -5
- package/test/Layout_tests.js +1400 -0
- package/test/PortRenderer_tests.js +11 -2
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
|
+
const libPerimeterMath = require('../providers/edges/Edge-PerimeterMath.js');
|
|
3
|
+
|
|
4
|
+
// Chip (port-label badge) geometry — must mirror PortRenderer's badge
|
|
5
|
+
// dimensions exactly so hint paths land on the chip's actual outer
|
|
6
|
+
// edge instead of where we *guess* it might be.
|
|
7
|
+
const _CHIP_HEIGHT = 12;
|
|
8
|
+
const _CHIP_PAD_H = 5;
|
|
9
|
+
const _CHIP_EDGE_PAD = 1;
|
|
10
|
+
const _CHIP_PORT_RADIUS = 5;
|
|
11
|
+
const _CHIP_BORDER_WIDTH = 2;
|
|
12
|
+
const _CHIP_PER_CHAR_PX = 5;
|
|
13
|
+
|
|
14
|
+
// Port-type → stroke color map. Mirrors the table PortRenderer uses for
|
|
15
|
+
// badge borders so the hint bezier matches its port's affinity color.
|
|
16
|
+
const PORT_TYPE_COLORS =
|
|
17
|
+
{
|
|
18
|
+
'event-in': '#3498db',
|
|
19
|
+
'event-out': '#2ecc71',
|
|
20
|
+
'setting': '#e67e22',
|
|
21
|
+
'value': '#f1c40f',
|
|
22
|
+
'error': '#e74c3c'
|
|
23
|
+
};
|
|
24
|
+
const PORT_TYPE_DEFAULT_COLOR = '#95a5a6';
|
|
2
25
|
|
|
3
26
|
class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
4
27
|
{
|
|
@@ -24,6 +47,16 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
24
47
|
let tmpSourcePos = this._FlowView.getPortPosition(pConnection.SourceNodeHash, pConnection.SourcePortHash);
|
|
25
48
|
let tmpTargetPos = this._FlowView.getPortPosition(pConnection.TargetNodeHash, pConnection.TargetPortHash);
|
|
26
49
|
|
|
50
|
+
// Let the active edge theme reshape the attachment points (e.g. a
|
|
51
|
+
// "perimeter" theme that exits whichever side of the node faces the
|
|
52
|
+
// other end). The static port positions are still computed above so
|
|
53
|
+
// themes that don't override fall through unchanged.
|
|
54
|
+
let tmpStaticSourcePos = tmpSourcePos;
|
|
55
|
+
let tmpStaticTargetPos = tmpTargetPos;
|
|
56
|
+
let tmpAttachOverride = this._resolveAttachmentsViaEdgeTheme(pConnection, tmpSourcePos, tmpTargetPos);
|
|
57
|
+
if (tmpAttachOverride.source) tmpSourcePos = tmpAttachOverride.source;
|
|
58
|
+
if (tmpAttachOverride.target) tmpTargetPos = tmpAttachOverride.target;
|
|
59
|
+
|
|
27
60
|
// Look up the source port's PortType for connection coloring
|
|
28
61
|
let tmpSourcePortType = null;
|
|
29
62
|
let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
|
|
@@ -42,34 +75,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
42
75
|
if (!tmpSourcePos || !tmpTargetPos) return;
|
|
43
76
|
|
|
44
77
|
let tmpData = pConnection.Data || {};
|
|
45
|
-
let
|
|
46
|
-
let tmpPath;
|
|
47
|
-
|
|
48
|
-
if (tmpLineMode === 'orthogonal')
|
|
49
|
-
{
|
|
50
|
-
let tmpCorners = null;
|
|
51
|
-
if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
|
|
52
|
-
{
|
|
53
|
-
tmpCorners =
|
|
54
|
-
{
|
|
55
|
-
corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
|
|
56
|
-
corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
tmpPath = this._generateOrthogonalPath(tmpSourcePos, tmpTargetPos, tmpCorners, tmpData.OrthoMidOffset || 0);
|
|
60
|
-
}
|
|
61
|
-
else
|
|
62
|
-
{
|
|
63
|
-
let tmpHandles = this._getBezierHandles(tmpData);
|
|
64
|
-
if (tmpHandles.length > 0)
|
|
65
|
-
{
|
|
66
|
-
tmpPath = this._generateMultiHandleBezierPath(tmpSourcePos, tmpTargetPos, tmpHandles);
|
|
67
|
-
}
|
|
68
|
-
else
|
|
69
|
-
{
|
|
70
|
-
tmpPath = this._generateDirectionalPath(tmpSourcePos, tmpTargetPos);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
78
|
+
let tmpPath = this._generatePathViaEdgeTheme(pConnection, tmpSourcePos, tmpTargetPos, tmpData);
|
|
73
79
|
|
|
74
80
|
// Apply theme noise post-processing to the path
|
|
75
81
|
if (this._FlowView._ThemeProvider)
|
|
@@ -154,6 +160,26 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
154
160
|
pConnectionsLayer.appendChild(tmpPathElement);
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
// Render the colored endpoint dot at each end of the connection
|
|
164
|
+
// into the dedicated endpoints layer (sits *above* the nodes
|
|
165
|
+
// layer so the dot doesn't get hidden under the card chrome
|
|
166
|
+
// when an edge theme places it on the node's perimeter).
|
|
167
|
+
// PortRenderer suppresses its own circle for any port that
|
|
168
|
+
// participates in a connection — we own the dot here.
|
|
169
|
+
let tmpEndpointsLayer = this._FlowView._EndpointsLayer || pConnectionsLayer;
|
|
170
|
+
this._renderEndpointDots(pConnection, tmpEndpointsLayer, tmpSourcePos, tmpTargetPos);
|
|
171
|
+
|
|
172
|
+
// When the resolved attachment differs from the card-defined port
|
|
173
|
+
// position (e.g. a Perimeter theme moved the dot to the edge of the
|
|
174
|
+
// card), paint a hidden hint bezier connecting the badge to the
|
|
175
|
+
// dot. CSS reveals it on hover so users can see "this 'In' chip
|
|
176
|
+
// corresponds to that dot up at the top".
|
|
177
|
+
if (this._FlowView._PortHintsLayer)
|
|
178
|
+
{
|
|
179
|
+
this._renderPortHints(pConnection, this._FlowView._PortHintsLayer,
|
|
180
|
+
tmpStaticSourcePos, tmpSourcePos, tmpStaticTargetPos, tmpTargetPos);
|
|
181
|
+
}
|
|
182
|
+
|
|
157
183
|
// Render drag handles when selected
|
|
158
184
|
if (pIsSelected)
|
|
159
185
|
{
|
|
@@ -161,6 +187,284 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
161
187
|
}
|
|
162
188
|
}
|
|
163
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Append the colored endpoint dots for both ends of a connection
|
|
192
|
+
* onto the destination layer (absolute coords). Reuses
|
|
193
|
+
* `ConnectorShapesProvider.createPortElement` so the styling
|
|
194
|
+
* matches the static port circle exactly.
|
|
195
|
+
*
|
|
196
|
+
* @param {Object} pConnection
|
|
197
|
+
* @param {SVGGElement} pLayer - the endpoints layer (or fallback)
|
|
198
|
+
* @param {{x: number, y: number}} pSourcePos
|
|
199
|
+
* @param {{x: number, y: number}} pTargetPos
|
|
200
|
+
*/
|
|
201
|
+
_renderEndpointDots(pConnection, pLayer, pSourcePos, pTargetPos)
|
|
202
|
+
{
|
|
203
|
+
let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
|
|
204
|
+
if (!tmpShapeProvider) return;
|
|
205
|
+
|
|
206
|
+
let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
|
|
207
|
+
let tmpTargetNode = this._FlowView.getNode(pConnection.TargetNodeHash);
|
|
208
|
+
let tmpSourcePort = this._findPort(tmpSourceNode, pConnection.SourcePortHash);
|
|
209
|
+
let tmpTargetPort = this._findPort(tmpTargetNode, pConnection.TargetPortHash);
|
|
210
|
+
|
|
211
|
+
if (tmpSourcePort && pSourcePos)
|
|
212
|
+
{
|
|
213
|
+
let tmpDot = tmpShapeProvider.createPortElement(tmpSourcePort, pSourcePos, pConnection.SourceNodeHash);
|
|
214
|
+
tmpDot.setAttribute('data-connection-hash', pConnection.Hash);
|
|
215
|
+
tmpDot.setAttribute('data-connection-end', 'source');
|
|
216
|
+
pLayer.appendChild(tmpDot);
|
|
217
|
+
}
|
|
218
|
+
if (tmpTargetPort && pTargetPos)
|
|
219
|
+
{
|
|
220
|
+
let tmpDot = tmpShapeProvider.createPortElement(tmpTargetPort, pTargetPos, pConnection.TargetNodeHash);
|
|
221
|
+
tmpDot.setAttribute('data-connection-hash', pConnection.Hash);
|
|
222
|
+
tmpDot.setAttribute('data-connection-end', 'target');
|
|
223
|
+
pLayer.appendChild(tmpDot);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Render the per-end "where did the dot go" hint paths. Drawn into
|
|
229
|
+
* the port-hints layer (above nodes) but kept opacity:0 by CSS;
|
|
230
|
+
* PortRenderer/NodeView toggle a `data-active` attribute on hover.
|
|
231
|
+
*
|
|
232
|
+
* Skipped when the resolved attachment matches the static port
|
|
233
|
+
* position (no theme rerouting → no need for a hint).
|
|
234
|
+
*
|
|
235
|
+
* @param {Object} pConnection
|
|
236
|
+
* @param {SVGGElement} pLayer
|
|
237
|
+
* @param {{x: number, y: number, side?: string}} pStaticSrc - card-defined port position
|
|
238
|
+
* @param {{x: number, y: number, side?: string}} pSrc - resolved attachment
|
|
239
|
+
* @param {{x: number, y: number, side?: string}} pStaticTgt
|
|
240
|
+
* @param {{x: number, y: number, side?: string}} pTgt
|
|
241
|
+
*/
|
|
242
|
+
_renderPortHints(pConnection, pLayer, pStaticSrc, pSrc, pStaticTgt, pTgt)
|
|
243
|
+
{
|
|
244
|
+
let tmpPositionsDiffer = function (pA, pB)
|
|
245
|
+
{
|
|
246
|
+
if (!pA || !pB) return false;
|
|
247
|
+
return (Math.abs(pA.x - pB.x) > 0.5) || (Math.abs(pA.y - pB.y) > 0.5);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
|
|
251
|
+
let tmpTargetNode = this._FlowView.getNode(pConnection.TargetNodeHash);
|
|
252
|
+
let tmpSourcePort = this._findPort(tmpSourceNode, pConnection.SourcePortHash);
|
|
253
|
+
let tmpTargetPort = this._findPort(tmpTargetNode, pConnection.TargetPortHash);
|
|
254
|
+
|
|
255
|
+
// Hints are colored by the *other* end's node identity — looking
|
|
256
|
+
// at a hub with 8 hints fanning out, each hint is the color of
|
|
257
|
+
// the spoke that connection terminates at, so you can tell at a
|
|
258
|
+
// glance which line goes where without tracing it. (Mirrors the
|
|
259
|
+
// Ultravisor card-category color model: gray=flow-control,
|
|
260
|
+
// purple=core, orange=data, teal=llm, etc.)
|
|
261
|
+
// Anchor each hint at the chip's outer edge facing the dot
|
|
262
|
+
// (not the port's static position, which sits on the node's
|
|
263
|
+
// edge — visually inside the chip's stripe). The hint ends at
|
|
264
|
+
// the dot but its tail "points at" the chip's center.
|
|
265
|
+
if (pStaticSrc && tmpPositionsDiffer(pStaticSrc, pSrc))
|
|
266
|
+
{
|
|
267
|
+
let tmpChipExit = this._chipEdgeAimingAt(tmpSourcePort, pStaticSrc, pSrc);
|
|
268
|
+
pLayer.appendChild(this._buildPortHintPath(
|
|
269
|
+
tmpChipExit, pSrc,
|
|
270
|
+
pConnection.SourceNodeHash, pConnection.SourcePortHash, pConnection.Hash, 'source',
|
|
271
|
+
this._hintColor(tmpSourcePort, tmpTargetNode)));
|
|
272
|
+
}
|
|
273
|
+
if (pStaticTgt && tmpPositionsDiffer(pStaticTgt, pTgt))
|
|
274
|
+
{
|
|
275
|
+
let tmpChipExit = this._chipEdgeAimingAt(tmpTargetPort, pStaticTgt, pTgt);
|
|
276
|
+
pLayer.appendChild(this._buildPortHintPath(
|
|
277
|
+
tmpChipExit, pTgt,
|
|
278
|
+
pConnection.TargetNodeHash, pConnection.TargetPortHash, pConnection.Hash, 'target',
|
|
279
|
+
this._hintColor(tmpTargetPort, tmpSourceNode)));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Compute the chip (port-label badge) bounding box for a given
|
|
285
|
+
* port. The chip is a small rectangle that lives just inside the
|
|
286
|
+
* node's outer edge, anchored to the port's static position. We
|
|
287
|
+
* mirror PortRenderer's badge geometry exactly so the hint path
|
|
288
|
+
* lands on real chip-edge pixels.
|
|
289
|
+
*
|
|
290
|
+
* Returns null when we don't have enough info (no label or
|
|
291
|
+
* unknown side) — caller should fall back to the static position.
|
|
292
|
+
*
|
|
293
|
+
* @param {Object|null} pPort
|
|
294
|
+
* @param {{x: number, y: number, side?: string}} pStaticPos - the
|
|
295
|
+
* port's nominal position (returned by getPortPosition); the
|
|
296
|
+
* chip extends *inward* from here.
|
|
297
|
+
* @returns {{X: number, Y: number, Width: number, Height: number}|null}
|
|
298
|
+
*/
|
|
299
|
+
_chipBoundsFor(pPort, pStaticPos)
|
|
300
|
+
{
|
|
301
|
+
if (!pPort || !pStaticPos) return null;
|
|
302
|
+
let tmpSide = pStaticPos.side || pPort.Side || (pPort.Direction === 'input' ? 'left' : 'right');
|
|
303
|
+
let tmpLabel = pPort.Label || '';
|
|
304
|
+
// PortRenderer skips badge rendering entirely when there is no
|
|
305
|
+
// label, so there's nothing for the hint to anchor against.
|
|
306
|
+
if (tmpLabel === '') return null;
|
|
307
|
+
|
|
308
|
+
let tmpTextPx = tmpLabel.length * _CHIP_PER_CHAR_PX;
|
|
309
|
+
let tmpWidth = _CHIP_PORT_RADIUS + _CHIP_PAD_H + tmpTextPx + _CHIP_PAD_H + _CHIP_BORDER_WIDTH;
|
|
310
|
+
let tmpHeight = _CHIP_HEIGHT;
|
|
311
|
+
|
|
312
|
+
// Chip center sits halfway along the line from the port's static
|
|
313
|
+
// position toward the inside of the node, offset by half the
|
|
314
|
+
// chip's extent in that direction (plus the 1px edge padding
|
|
315
|
+
// PortRenderer leaves between the chip and the node edge).
|
|
316
|
+
let tmpCx, tmpCy;
|
|
317
|
+
if (tmpSide.indexOf('left') === 0)
|
|
318
|
+
{
|
|
319
|
+
tmpCx = pStaticPos.x + tmpWidth / 2 + _CHIP_EDGE_PAD;
|
|
320
|
+
tmpCy = pStaticPos.y;
|
|
321
|
+
}
|
|
322
|
+
else if (tmpSide.indexOf('right') === 0)
|
|
323
|
+
{
|
|
324
|
+
tmpCx = pStaticPos.x - tmpWidth / 2 - _CHIP_EDGE_PAD;
|
|
325
|
+
tmpCy = pStaticPos.y;
|
|
326
|
+
}
|
|
327
|
+
else if (tmpSide.indexOf('top') === 0)
|
|
328
|
+
{
|
|
329
|
+
tmpCx = pStaticPos.x;
|
|
330
|
+
tmpCy = pStaticPos.y + tmpHeight / 2 + _CHIP_EDGE_PAD;
|
|
331
|
+
}
|
|
332
|
+
else if (tmpSide.indexOf('bottom') === 0)
|
|
333
|
+
{
|
|
334
|
+
tmpCx = pStaticPos.x;
|
|
335
|
+
tmpCy = pStaticPos.y - tmpHeight / 2 - _CHIP_EDGE_PAD;
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
{
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { X: tmpCx - tmpWidth / 2, Y: tmpCy - tmpHeight / 2, Width: tmpWidth, Height: tmpHeight };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve the point on the chip's outer perimeter where a line
|
|
347
|
+
* from the chip's center toward the dot would exit the chip. Used
|
|
348
|
+
* as the hint's start point so the line visually emerges from the
|
|
349
|
+
* chip's edge facing the dot, aimed at the chip's center.
|
|
350
|
+
*
|
|
351
|
+
* Falls back to the static port position when chip geometry can't
|
|
352
|
+
* be computed (no label, unknown side).
|
|
353
|
+
*
|
|
354
|
+
* @param {Object|null} pPort
|
|
355
|
+
* @param {{x: number, y: number, side?: string}} pStaticPos
|
|
356
|
+
* @param {{x: number, y: number}} pDotPos
|
|
357
|
+
* @returns {{x: number, y: number}}
|
|
358
|
+
*/
|
|
359
|
+
_chipEdgeAimingAt(pPort, pStaticPos, pDotPos)
|
|
360
|
+
{
|
|
361
|
+
let tmpBounds = this._chipBoundsFor(pPort, pStaticPos);
|
|
362
|
+
if (!tmpBounds) return pStaticPos;
|
|
363
|
+
let tmpExit = libPerimeterMath.resolvePerimeterAttachment({
|
|
364
|
+
Node: tmpBounds,
|
|
365
|
+
OtherDefaultPosition: pDotPos
|
|
366
|
+
});
|
|
367
|
+
if (!tmpExit) return pStaticPos;
|
|
368
|
+
return tmpExit;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Resolve a node's identity color. Looks at the inline node fields
|
|
373
|
+
* first (per-node overrides win), then the registered node-type
|
|
374
|
+
* config from the NodeTypeProvider. Returns null when the node has
|
|
375
|
+
* no opinion — caller should fall back to port-type color.
|
|
376
|
+
*
|
|
377
|
+
* @param {Object|null} pNode
|
|
378
|
+
* @returns {string|null}
|
|
379
|
+
*/
|
|
380
|
+
_resolveNodeIdentityColor(pNode)
|
|
381
|
+
{
|
|
382
|
+
if (!pNode) return null;
|
|
383
|
+
if (pNode.TitleBarColor) return pNode.TitleBarColor;
|
|
384
|
+
if (pNode.BodyStyle && pNode.BodyStyle.stroke) return pNode.BodyStyle.stroke;
|
|
385
|
+
let tmpProvider = this._FlowView ? this._FlowView._NodeTypeProvider : null;
|
|
386
|
+
if (tmpProvider && typeof tmpProvider.getNodeType === 'function' && pNode.Type)
|
|
387
|
+
{
|
|
388
|
+
let tmpType = tmpProvider.getNodeType(pNode.Type);
|
|
389
|
+
if (tmpType)
|
|
390
|
+
{
|
|
391
|
+
if (tmpType.TitleBarColor) return tmpType.TitleBarColor;
|
|
392
|
+
if (tmpType.BodyStyle && tmpType.BodyStyle.stroke) return tmpType.BodyStyle.stroke;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Resolve the hint stroke color. Priority chain:
|
|
400
|
+
* 1. The OTHER end's node identity color (TitleBarColor /
|
|
401
|
+
* BodyStyle.stroke / NodeTypeConfig color) — the strongest
|
|
402
|
+
* signal: "this hint terminates at *that* card".
|
|
403
|
+
* 2. This end's PortType color — semantic-role fallback.
|
|
404
|
+
* 3. Direction-based default that matches the visible dot color.
|
|
405
|
+
* 4. Neutral gray.
|
|
406
|
+
*
|
|
407
|
+
* @param {Object|null} pPort - this end's port
|
|
408
|
+
* @param {Object|null} pOtherNode - the node at the other end
|
|
409
|
+
* @returns {string}
|
|
410
|
+
*/
|
|
411
|
+
_hintColor(pPort, pOtherNode)
|
|
412
|
+
{
|
|
413
|
+
let tmpOtherColor = this._resolveNodeIdentityColor(pOtherNode);
|
|
414
|
+
if (tmpOtherColor) return tmpOtherColor;
|
|
415
|
+
if (pPort && pPort.PortType && PORT_TYPE_COLORS[pPort.PortType])
|
|
416
|
+
{
|
|
417
|
+
return PORT_TYPE_COLORS[pPort.PortType];
|
|
418
|
+
}
|
|
419
|
+
if (pPort && pPort.Direction === 'input') return PORT_TYPE_COLORS['event-in'];
|
|
420
|
+
if (pPort && pPort.Direction === 'output') return PORT_TYPE_COLORS['event-out'];
|
|
421
|
+
return PORT_TYPE_DEFAULT_COLOR;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Build a single hint-path SVG element: a soft S-curve from the
|
|
426
|
+
* card-defined port position to the resolved attachment position.
|
|
427
|
+
* Tagged with port + node + connection hashes so hover handlers can
|
|
428
|
+
* find the right hint by attribute.
|
|
429
|
+
*
|
|
430
|
+
* @param {{x: number, y: number}} pStart - badge / chip position
|
|
431
|
+
* @param {{x: number, y: number}} pEnd - actual dot position
|
|
432
|
+
* @returns {SVGPathElement}
|
|
433
|
+
*/
|
|
434
|
+
_buildPortHintPath(pStart, pEnd, pNodeHash, pPortHash, pConnectionHash, pEndLabel, pStrokeColor)
|
|
435
|
+
{
|
|
436
|
+
let tmpEl = this._FlowView._SVGHelperProvider.createSVGElement('path');
|
|
437
|
+
|
|
438
|
+
// Soft S-curve: control points pulled along the perpendicular bisector
|
|
439
|
+
let tmpDX = pEnd.x - pStart.x;
|
|
440
|
+
let tmpDY = pEnd.y - pStart.y;
|
|
441
|
+
let tmpDist = Math.sqrt(tmpDX * tmpDX + tmpDY * tmpDY);
|
|
442
|
+
let tmpBend = Math.min(60, tmpDist * 0.35);
|
|
443
|
+
// Perpendicular unit vector
|
|
444
|
+
let tmpPX = (tmpDist > 0) ? -tmpDY / tmpDist : 0;
|
|
445
|
+
let tmpPY = (tmpDist > 0) ? tmpDX / tmpDist : 0;
|
|
446
|
+
let tmpCp1X = pStart.x + tmpDX * 0.35 + tmpPX * tmpBend * 0.3;
|
|
447
|
+
let tmpCp1Y = pStart.y + tmpDY * 0.35 + tmpPY * tmpBend * 0.3;
|
|
448
|
+
let tmpCp2X = pStart.x + tmpDX * 0.65 + tmpPX * tmpBend * 0.3;
|
|
449
|
+
let tmpCp2Y = pStart.y + tmpDY * 0.65 + tmpPY * tmpBend * 0.3;
|
|
450
|
+
let tmpD = `M ${pStart.x} ${pStart.y} C ${tmpCp1X} ${tmpCp1Y} ${tmpCp2X} ${tmpCp2Y} ${pEnd.x} ${pEnd.y}`;
|
|
451
|
+
|
|
452
|
+
tmpEl.setAttribute('class', 'pict-flow-port-hint');
|
|
453
|
+
tmpEl.setAttribute('d', tmpD);
|
|
454
|
+
tmpEl.setAttribute('data-node-hash', pNodeHash);
|
|
455
|
+
tmpEl.setAttribute('data-port-hash', pPortHash);
|
|
456
|
+
tmpEl.setAttribute('data-connection-hash', pConnectionHash);
|
|
457
|
+
tmpEl.setAttribute('data-connection-end', pEndLabel);
|
|
458
|
+
// Affinity color from port type (matches the badge border color).
|
|
459
|
+
// Inline so PortRenderer's color map stays the single source of
|
|
460
|
+
// truth without dragging CSS variables around per port type.
|
|
461
|
+
if (pStrokeColor)
|
|
462
|
+
{
|
|
463
|
+
tmpEl.setAttribute('stroke', pStrokeColor);
|
|
464
|
+
}
|
|
465
|
+
return tmpEl;
|
|
466
|
+
}
|
|
467
|
+
|
|
164
468
|
/**
|
|
165
469
|
* Compute the departure and approach points plus control points
|
|
166
470
|
* for a direction-aware bezier between two ports.
|
|
@@ -206,6 +510,206 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
206
510
|
);
|
|
207
511
|
}
|
|
208
512
|
|
|
513
|
+
/**
|
|
514
|
+
* Dispatch to an edge theme to generate the path string. Falls back
|
|
515
|
+
* to the renderer's built-in bezier / orthogonal helpers when no
|
|
516
|
+
* edge-theme registry is available (older host integrations) or
|
|
517
|
+
* the resolved theme errors out.
|
|
518
|
+
*
|
|
519
|
+
* @param {Object} pConnection
|
|
520
|
+
* @param {{x: number, y: number, side?: string}} pSourcePos
|
|
521
|
+
* @param {{x: number, y: number, side?: string}} pTargetPos
|
|
522
|
+
* @param {Object} pData - connection data (LineMode, custom handles, etc.)
|
|
523
|
+
* @returns {string} SVG path 'd' attribute
|
|
524
|
+
*/
|
|
525
|
+
_generatePathViaEdgeTheme(pConnection, pSourcePos, pTargetPos, pData)
|
|
526
|
+
{
|
|
527
|
+
let tmpLayoutService = this._FlowView ? this._FlowView._LayoutService : null;
|
|
528
|
+
let tmpTheme = (tmpLayoutService && typeof tmpLayoutService.resolveActiveEdgeTheme === 'function')
|
|
529
|
+
? tmpLayoutService.resolveActiveEdgeTheme(pConnection)
|
|
530
|
+
: null;
|
|
531
|
+
|
|
532
|
+
if (!tmpTheme || typeof tmpTheme.GeneratePath !== 'function')
|
|
533
|
+
{
|
|
534
|
+
return this._builtInPathFallback(pSourcePos, pTargetPos, pData);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
let tmpContext =
|
|
538
|
+
{
|
|
539
|
+
Source: pSourcePos,
|
|
540
|
+
Target: pTargetPos,
|
|
541
|
+
Connection: pConnection,
|
|
542
|
+
AllNodes: this._FlowView._FlowData ? this._FlowView._FlowData.Nodes : [],
|
|
543
|
+
AllConnections: this._FlowView._FlowData ? this._FlowView._FlowData.Connections : [],
|
|
544
|
+
FlowView: this._FlowView,
|
|
545
|
+
Helpers: this._buildEdgeThemeHelpers(),
|
|
546
|
+
Parameters: tmpLayoutService.getMergedEdgeThemeParameters(
|
|
547
|
+
tmpTheme.Name,
|
|
548
|
+
(this._FlowView._FlowData && this._FlowView._FlowData.EdgeThemeParameters) || {}
|
|
549
|
+
)
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
try
|
|
553
|
+
{
|
|
554
|
+
let tmpPath = tmpTheme.GeneratePath(tmpContext);
|
|
555
|
+
if (typeof tmpPath !== 'string' || tmpPath === '')
|
|
556
|
+
{
|
|
557
|
+
return this._builtInPathFallback(pSourcePos, pTargetPos, pData);
|
|
558
|
+
}
|
|
559
|
+
return tmpPath;
|
|
560
|
+
}
|
|
561
|
+
catch (pError)
|
|
562
|
+
{
|
|
563
|
+
this._FlowView.log.warn(`PictServiceFlowConnectionRenderer edge theme '${tmpTheme.Name}' threw: ${pError.message}`);
|
|
564
|
+
return this._builtInPathFallback(pSourcePos, pTargetPos, pData);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Resolve per-end attachment overrides via the active edge theme's
|
|
570
|
+
* optional `ResolveAttachment(context)` hook. The theme can return
|
|
571
|
+
* `{ x, y, side }` for either end (or null to fall through). Used by
|
|
572
|
+
* themes like Perimeter that route through the node's bounding box
|
|
573
|
+
* rather than the static port position.
|
|
574
|
+
*
|
|
575
|
+
* @param {Object} pConnection
|
|
576
|
+
* @param {{x: number, y: number, side?: string}} pSourcePos - default port position
|
|
577
|
+
* @param {{x: number, y: number, side?: string}} pTargetPos - default port position
|
|
578
|
+
* @returns {{source: ?Object, target: ?Object}}
|
|
579
|
+
*/
|
|
580
|
+
_resolveAttachmentsViaEdgeTheme(pConnection, pSourcePos, pTargetPos)
|
|
581
|
+
{
|
|
582
|
+
let tmpEmpty = { source: null, target: null };
|
|
583
|
+
let tmpLayoutService = this._FlowView ? this._FlowView._LayoutService : null;
|
|
584
|
+
let tmpTheme = (tmpLayoutService && typeof tmpLayoutService.resolveActiveEdgeTheme === 'function')
|
|
585
|
+
? tmpLayoutService.resolveActiveEdgeTheme(pConnection)
|
|
586
|
+
: null;
|
|
587
|
+
if (!tmpTheme || typeof tmpTheme.ResolveAttachment !== 'function') return tmpEmpty;
|
|
588
|
+
|
|
589
|
+
let tmpSourceNode = this._FlowView.getNode(pConnection.SourceNodeHash);
|
|
590
|
+
let tmpTargetNode = this._FlowView.getNode(pConnection.TargetNodeHash);
|
|
591
|
+
let tmpSourcePort = this._findPort(tmpSourceNode, pConnection.SourcePortHash);
|
|
592
|
+
let tmpTargetPort = this._findPort(tmpTargetNode, pConnection.TargetPortHash);
|
|
593
|
+
|
|
594
|
+
let tmpParams = tmpLayoutService.getMergedEdgeThemeParameters(
|
|
595
|
+
tmpTheme.Name,
|
|
596
|
+
(this._FlowView._FlowData && this._FlowView._FlowData.EdgeThemeParameters) || {}
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
let tmpSrc = null, tmpTgt = null;
|
|
600
|
+
try
|
|
601
|
+
{
|
|
602
|
+
tmpSrc = tmpTheme.ResolveAttachment({
|
|
603
|
+
Node: tmpSourceNode, Port: tmpSourcePort,
|
|
604
|
+
DefaultPosition: pSourcePos,
|
|
605
|
+
Connection: pConnection, IsSource: true,
|
|
606
|
+
OtherNode: tmpTargetNode, OtherPort: tmpTargetPort,
|
|
607
|
+
OtherDefaultPosition: pTargetPos,
|
|
608
|
+
AllNodes: this._FlowView._FlowData ? this._FlowView._FlowData.Nodes : [],
|
|
609
|
+
FlowView: this._FlowView,
|
|
610
|
+
Parameters: tmpParams
|
|
611
|
+
});
|
|
612
|
+
tmpTgt = tmpTheme.ResolveAttachment({
|
|
613
|
+
Node: tmpTargetNode, Port: tmpTargetPort,
|
|
614
|
+
DefaultPosition: pTargetPos,
|
|
615
|
+
Connection: pConnection, IsSource: false,
|
|
616
|
+
OtherNode: tmpSourceNode, OtherPort: tmpSourcePort,
|
|
617
|
+
OtherDefaultPosition: pSourcePos,
|
|
618
|
+
AllNodes: this._FlowView._FlowData ? this._FlowView._FlowData.Nodes : [],
|
|
619
|
+
FlowView: this._FlowView,
|
|
620
|
+
Parameters: tmpParams
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
catch (pError)
|
|
624
|
+
{
|
|
625
|
+
this._FlowView.log.warn(`PictServiceFlowConnectionRenderer edge theme '${tmpTheme.Name}' ResolveAttachment threw: ${pError.message}`);
|
|
626
|
+
return tmpEmpty;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return { source: tmpSrc || null, target: tmpTgt || null };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Find a port by hash on a node. Returns null if missing.
|
|
634
|
+
* @param {Object} pNode
|
|
635
|
+
* @param {string} pPortHash
|
|
636
|
+
* @returns {Object|null}
|
|
637
|
+
*/
|
|
638
|
+
_findPort(pNode, pPortHash)
|
|
639
|
+
{
|
|
640
|
+
if (!pNode || !Array.isArray(pNode.Ports)) return null;
|
|
641
|
+
for (let i = 0; i < pNode.Ports.length; i++)
|
|
642
|
+
{
|
|
643
|
+
if (pNode.Ports[i].Hash === pPortHash) return pNode.Ports[i];
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Path-generation helpers exposed to edge themes via the GeneratePath
|
|
650
|
+
* context object. Themes can compose these to derive their own routing.
|
|
651
|
+
*
|
|
652
|
+
* @returns {Object}
|
|
653
|
+
*/
|
|
654
|
+
_buildEdgeThemeHelpers()
|
|
655
|
+
{
|
|
656
|
+
let tmpRenderer = this;
|
|
657
|
+
return {
|
|
658
|
+
generateBezier: function (pStart, pEnd)
|
|
659
|
+
{
|
|
660
|
+
return tmpRenderer._generateDirectionalPath(pStart, pEnd);
|
|
661
|
+
},
|
|
662
|
+
generateMultiBezier: function (pStart, pEnd, pHandles)
|
|
663
|
+
{
|
|
664
|
+
return tmpRenderer._generateMultiHandleBezierPath(pStart, pEnd, pHandles);
|
|
665
|
+
},
|
|
666
|
+
generateOrthogonal: function (pStart, pEnd, pCorners, pMidOffset)
|
|
667
|
+
{
|
|
668
|
+
return tmpRenderer._generateOrthogonalPath(pStart, pEnd, pCorners || null, pMidOffset || 0);
|
|
669
|
+
},
|
|
670
|
+
getBezierHandles: function (pData)
|
|
671
|
+
{
|
|
672
|
+
return tmpRenderer._getBezierHandles(pData);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Built-in fallback when no edge theme is registered. Mirrors the
|
|
679
|
+
* pre-edge-theme dispatch (LineMode → bezier or orthogonal).
|
|
680
|
+
*
|
|
681
|
+
* @param {{x: number, y: number, side?: string}} pSourcePos
|
|
682
|
+
* @param {{x: number, y: number, side?: string}} pTargetPos
|
|
683
|
+
* @param {Object} pData
|
|
684
|
+
* @returns {string}
|
|
685
|
+
*/
|
|
686
|
+
_builtInPathFallback(pSourcePos, pTargetPos, pData)
|
|
687
|
+
{
|
|
688
|
+
let tmpData = pData || {};
|
|
689
|
+
let tmpLineMode = tmpData.LineMode || 'bezier';
|
|
690
|
+
|
|
691
|
+
if (tmpLineMode === 'orthogonal')
|
|
692
|
+
{
|
|
693
|
+
let tmpCorners = null;
|
|
694
|
+
if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
|
|
695
|
+
{
|
|
696
|
+
tmpCorners =
|
|
697
|
+
{
|
|
698
|
+
corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
|
|
699
|
+
corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return this._generateOrthogonalPath(pSourcePos, pTargetPos, tmpCorners, tmpData.OrthoMidOffset || 0);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
let tmpHandles = this._getBezierHandles(tmpData);
|
|
706
|
+
if (tmpHandles.length > 0)
|
|
707
|
+
{
|
|
708
|
+
return this._generateMultiHandleBezierPath(pSourcePos, pTargetPos, tmpHandles);
|
|
709
|
+
}
|
|
710
|
+
return this._generateDirectionalPath(pSourcePos, pTargetPos);
|
|
711
|
+
}
|
|
712
|
+
|
|
209
713
|
/**
|
|
210
714
|
* Get the bezier handles array from connection data, with backward
|
|
211
715
|
* compatibility for the legacy BezierHandleX/Y single-handle format.
|
|
@@ -94,6 +94,12 @@ class PictServiceFlowDataManager extends libFableServiceProviderBase
|
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
let tmpDefaultAlgorithm = (this._FlowView.options && this._FlowView.options.DefaultLayoutAlgorithm) || 'Custom';
|
|
98
|
+
let tmpDefaultParameters = (this._FlowView.options && this._FlowView.options.DefaultLayoutParameters) || {};
|
|
99
|
+
let tmpDefaultAutoApply = !!(this._FlowView.options && this._FlowView.options.DefaultLayoutAutoApply);
|
|
100
|
+
let tmpDefaultEdgeTheme = (this._FlowView.options && this._FlowView.options.DefaultEdgeTheme) || null;
|
|
101
|
+
let tmpDefaultEdgeThemeParameters = (this._FlowView.options && this._FlowView.options.DefaultEdgeThemeParameters) || {};
|
|
102
|
+
|
|
97
103
|
this._FlowView._FlowData = {
|
|
98
104
|
Nodes: Array.isArray(pFlowData.Nodes) ? pFlowData.Nodes : [],
|
|
99
105
|
Connections: Array.isArray(pFlowData.Connections) ? pFlowData.Connections : [],
|
|
@@ -102,7 +108,12 @@ class PictServiceFlowDataManager extends libFableServiceProviderBase
|
|
|
102
108
|
ViewState: Object.assign(
|
|
103
109
|
{ PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null, SelectedTetherHash: null },
|
|
104
110
|
pFlowData.ViewState || {}
|
|
105
|
-
)
|
|
111
|
+
),
|
|
112
|
+
LayoutAlgorithm: (typeof pFlowData.LayoutAlgorithm === 'string' && pFlowData.LayoutAlgorithm !== '') ? pFlowData.LayoutAlgorithm : tmpDefaultAlgorithm,
|
|
113
|
+
LayoutParameters: (pFlowData.LayoutParameters && typeof pFlowData.LayoutParameters === 'object') ? JSON.parse(JSON.stringify(pFlowData.LayoutParameters)) : JSON.parse(JSON.stringify(tmpDefaultParameters)),
|
|
114
|
+
LayoutAutoApply: (typeof pFlowData.LayoutAutoApply === 'boolean') ? pFlowData.LayoutAutoApply : tmpDefaultAutoApply,
|
|
115
|
+
EdgeTheme: (typeof pFlowData.EdgeTheme === 'string' && pFlowData.EdgeTheme !== '') ? pFlowData.EdgeTheme : tmpDefaultEdgeTheme,
|
|
116
|
+
EdgeThemeParameters: (pFlowData.EdgeThemeParameters && typeof pFlowData.EdgeThemeParameters === 'object') ? JSON.parse(JSON.stringify(pFlowData.EdgeThemeParameters)) : JSON.parse(JSON.stringify(tmpDefaultEdgeThemeParameters))
|
|
106
117
|
};
|
|
107
118
|
|
|
108
119
|
// Merge any browser-persisted layouts into the newly loaded data
|