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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-Bezier
|
|
3
|
+
*
|
|
4
|
+
* Default edge theme: smooth cubic bezier between source and target ports,
|
|
5
|
+
* with departure/approach geometry derived from each port's `Side`. This
|
|
6
|
+
* is a thin wrapper around the renderer's pre-existing bezier path
|
|
7
|
+
* generator — selecting it is equivalent to the historical default.
|
|
8
|
+
*
|
|
9
|
+
* Honors per-connection multi-handle waypoints (BezierHandles) when
|
|
10
|
+
* present so user-edited curves still survive.
|
|
11
|
+
*/
|
|
12
|
+
module.exports =
|
|
13
|
+
{
|
|
14
|
+
Name: 'Bezier',
|
|
15
|
+
Label: 'Bezier (smooth)',
|
|
16
|
+
Description: 'Smooth cubic curves with side-aware departures.',
|
|
17
|
+
|
|
18
|
+
GeneratePath: function (pContext)
|
|
19
|
+
{
|
|
20
|
+
let tmpHelpers = pContext.Helpers;
|
|
21
|
+
let tmpSrc = pContext.Source;
|
|
22
|
+
let tmpTgt = pContext.Target;
|
|
23
|
+
let tmpData = (pContext.Connection && pContext.Connection.Data) || {};
|
|
24
|
+
|
|
25
|
+
// Preserve user-customized multi-handle bezier waypoints.
|
|
26
|
+
if (tmpData.HandleCustomized)
|
|
27
|
+
{
|
|
28
|
+
let tmpHandles = tmpHelpers.getBezierHandles(tmpData);
|
|
29
|
+
if (tmpHandles.length > 0)
|
|
30
|
+
{
|
|
31
|
+
return tmpHelpers.generateMultiBezier(tmpSrc, tmpTgt, tmpHandles);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return tmpHelpers.generateBezier(tmpSrc, tmpTgt);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
DefaultParameters: {},
|
|
39
|
+
|
|
40
|
+
ParameterSchema: {}
|
|
41
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-Orthogonal
|
|
3
|
+
*
|
|
4
|
+
* Right-angle (Manhattan) routing between source and target. Reads
|
|
5
|
+
* cleanly for layered DAGs and grid-shaped layouts. Per-connection
|
|
6
|
+
* `OrthoMidOffset` and customized corner overrides are preserved.
|
|
7
|
+
*/
|
|
8
|
+
module.exports =
|
|
9
|
+
{
|
|
10
|
+
Name: 'Orthogonal',
|
|
11
|
+
Label: 'Orthogonal (right-angle)',
|
|
12
|
+
Description: 'Right-angle routing — reads well for DAGs and grids.',
|
|
13
|
+
|
|
14
|
+
GeneratePath: function (pContext)
|
|
15
|
+
{
|
|
16
|
+
let tmpHelpers = pContext.Helpers;
|
|
17
|
+
let tmpSrc = pContext.Source;
|
|
18
|
+
let tmpTgt = pContext.Target;
|
|
19
|
+
let tmpData = (pContext.Connection && pContext.Connection.Data) || {};
|
|
20
|
+
|
|
21
|
+
let tmpCorners = null;
|
|
22
|
+
if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
|
|
23
|
+
{
|
|
24
|
+
tmpCorners =
|
|
25
|
+
{
|
|
26
|
+
corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
|
|
27
|
+
corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return tmpHelpers.generateOrthogonal(tmpSrc, tmpTgt, tmpCorners, tmpData.OrthoMidOffset || 0);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
DefaultParameters: {},
|
|
35
|
+
|
|
36
|
+
ParameterSchema: {}
|
|
37
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const libCoerce = require('../layouts/Layout-Coerce.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Edge-OrthogonalSnap
|
|
5
|
+
*
|
|
6
|
+
* Demonstrates the "edge theme can affect node placement" axis. Same
|
|
7
|
+
* right-angle routing as Edge-Orthogonal, but with an `AdjustLayout`
|
|
8
|
+
* pass that snaps every node's X/Y to a grid so corner segments line
|
|
9
|
+
* up cleanly across the diagram.
|
|
10
|
+
*
|
|
11
|
+
* Trade-off: nodes shift slightly from the layout's natural positions
|
|
12
|
+
* to gain visual alignment. The user opts into this by picking the
|
|
13
|
+
* theme — the layout algorithm itself is unaware.
|
|
14
|
+
*/
|
|
15
|
+
module.exports =
|
|
16
|
+
{
|
|
17
|
+
Name: 'OrthogonalSnap',
|
|
18
|
+
Label: 'Orthogonal (snap to grid)',
|
|
19
|
+
Description: 'Right-angle routing; snaps node positions to a grid for cleaner corner alignment. Demonstrates an edge theme that adjusts node placement.',
|
|
20
|
+
|
|
21
|
+
GeneratePath: function (pContext)
|
|
22
|
+
{
|
|
23
|
+
let tmpHelpers = pContext.Helpers;
|
|
24
|
+
let tmpSrc = pContext.Source;
|
|
25
|
+
let tmpTgt = pContext.Target;
|
|
26
|
+
let tmpData = (pContext.Connection && pContext.Connection.Data) || {};
|
|
27
|
+
|
|
28
|
+
let tmpCorners = null;
|
|
29
|
+
if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
|
|
30
|
+
{
|
|
31
|
+
tmpCorners =
|
|
32
|
+
{
|
|
33
|
+
corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
|
|
34
|
+
corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return tmpHelpers.generateOrthogonal(tmpSrc, tmpTgt, tmpCorners, tmpData.OrthoMidOffset || 0);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Snap each node's (X, Y) to the configured grid after the layout
|
|
43
|
+
* has run. Mutates pNodes in place.
|
|
44
|
+
*
|
|
45
|
+
* @param {Array} pNodes
|
|
46
|
+
* @param {Array} pConnections
|
|
47
|
+
* @param {Object} pParameters
|
|
48
|
+
*/
|
|
49
|
+
AdjustLayout: function (pNodes, pConnections, pParameters)
|
|
50
|
+
{
|
|
51
|
+
let tmpGridSize = libCoerce.toFloat((pParameters || {}).GridSize, 20);
|
|
52
|
+
if (tmpGridSize <= 0) return;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
55
|
+
{
|
|
56
|
+
let tmpNode = pNodes[i];
|
|
57
|
+
if (typeof tmpNode.X !== 'number' || typeof tmpNode.Y !== 'number') continue;
|
|
58
|
+
tmpNode.X = Math.round(tmpNode.X / tmpGridSize) * tmpGridSize;
|
|
59
|
+
tmpNode.Y = Math.round(tmpNode.Y / tmpGridSize) * tmpGridSize;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
DefaultParameters:
|
|
64
|
+
{
|
|
65
|
+
GridSize: 20
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
ParameterSchema:
|
|
69
|
+
{
|
|
70
|
+
GridSize: { Type: 'PreciseNumber', Label: 'Grid size', Default: 20, Min: 1, Max: 200 }
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const libPerimeterMath = require('./Edge-PerimeterMath.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Edge-Perimeter-Linear
|
|
5
|
+
*
|
|
6
|
+
* Perimeter attachment + literal straight line between the two
|
|
7
|
+
* resolved exit points. Reads cleanly for force-directed and circular
|
|
8
|
+
* layouts where the topology already determines edge angles, and the
|
|
9
|
+
* curves of bezier add visual noise.
|
|
10
|
+
*/
|
|
11
|
+
module.exports =
|
|
12
|
+
{
|
|
13
|
+
Name: 'Perimeter-Linear',
|
|
14
|
+
Label: 'Perimeter (linear)',
|
|
15
|
+
Description: 'Each connection exits the node at the perimeter, then runs as a straight line to the other end.',
|
|
16
|
+
|
|
17
|
+
GeneratePath: function (pContext)
|
|
18
|
+
{
|
|
19
|
+
let tmpS = pContext.Source;
|
|
20
|
+
let tmpT = pContext.Target;
|
|
21
|
+
return `M ${tmpS.x} ${tmpS.y} L ${tmpT.x} ${tmpT.y}`;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
ResolveAttachment: function (pContext)
|
|
25
|
+
{
|
|
26
|
+
return libPerimeterMath.resolvePerimeterAttachment(pContext);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
DefaultParameters: {},
|
|
30
|
+
ParameterSchema: {}
|
|
31
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const libPerimeterMath = require('./Edge-PerimeterMath.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Edge-Perimeter-Orthogonal
|
|
5
|
+
*
|
|
6
|
+
* Perimeter attachment + right-angle (Manhattan) routing between
|
|
7
|
+
* the two resolved exit points. Best when the layout already has
|
|
8
|
+
* grid-like structure (Grid, Mesh) and you want connections to
|
|
9
|
+
* leave from whichever side faces the partner.
|
|
10
|
+
*/
|
|
11
|
+
module.exports =
|
|
12
|
+
{
|
|
13
|
+
Name: 'Perimeter-Orthogonal',
|
|
14
|
+
Label: 'Perimeter (orthogonal)',
|
|
15
|
+
Description: 'Each connection exits the node at the perimeter, then routes via right-angle segments to the other end.',
|
|
16
|
+
|
|
17
|
+
GeneratePath: function (pContext)
|
|
18
|
+
{
|
|
19
|
+
let tmpData = (pContext.Connection && pContext.Connection.Data) || {};
|
|
20
|
+
let tmpCorners = null;
|
|
21
|
+
if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
|
|
22
|
+
{
|
|
23
|
+
tmpCorners =
|
|
24
|
+
{
|
|
25
|
+
corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
|
|
26
|
+
corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return pContext.Helpers.generateOrthogonal(pContext.Source, pContext.Target, tmpCorners, tmpData.OrthoMidOffset || 0);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
ResolveAttachment: function (pContext)
|
|
33
|
+
{
|
|
34
|
+
return libPerimeterMath.resolvePerimeterAttachment(pContext);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
DefaultParameters: {},
|
|
38
|
+
ParameterSchema: {}
|
|
39
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const libPerimeterMath = require('./Edge-PerimeterMath.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Edge-Perimeter (Bezier)
|
|
5
|
+
*
|
|
6
|
+
* "Smart exit" routing with smooth bezier curves. Each connection
|
|
7
|
+
* exits the node at the perimeter point closest to its target —
|
|
8
|
+
* solves star/hub topologies where many connections share one port.
|
|
9
|
+
*
|
|
10
|
+
* Renders the path as a side-aware bezier (uses the side returned by
|
|
11
|
+
* `ResolveAttachment` so the curve departs in the right direction).
|
|
12
|
+
*
|
|
13
|
+
* See `Edge-Perimeter-Linear` and `Edge-Perimeter-Orthogonal` for the
|
|
14
|
+
* straight-line and right-angle variants.
|
|
15
|
+
*/
|
|
16
|
+
module.exports =
|
|
17
|
+
{
|
|
18
|
+
Name: 'Perimeter',
|
|
19
|
+
Label: 'Perimeter (bezier)',
|
|
20
|
+
Description: 'Each connection exits the node at the perimeter point closest to its target, with smooth bezier curves. Fixes star/hub topologies where many lines share one port.',
|
|
21
|
+
|
|
22
|
+
GeneratePath: function (pContext)
|
|
23
|
+
{
|
|
24
|
+
// Honor user-edited bezier handles when the connection is selected
|
|
25
|
+
// and dragged. Without this, the drag handles render but moving
|
|
26
|
+
// them does nothing because Perimeter would always re-emit a
|
|
27
|
+
// fresh side-aware bezier.
|
|
28
|
+
let tmpHelpers = pContext.Helpers;
|
|
29
|
+
let tmpData = (pContext.Connection && pContext.Connection.Data) || {};
|
|
30
|
+
if (tmpData.HandleCustomized)
|
|
31
|
+
{
|
|
32
|
+
let tmpHandles = tmpHelpers.getBezierHandles(tmpData);
|
|
33
|
+
if (tmpHandles.length > 0)
|
|
34
|
+
{
|
|
35
|
+
return tmpHelpers.generateMultiBezier(pContext.Source, pContext.Target, tmpHandles);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return tmpHelpers.generateBezier(pContext.Source, pContext.Target);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
ResolveAttachment: function (pContext)
|
|
42
|
+
{
|
|
43
|
+
return libPerimeterMath.resolvePerimeterAttachment(pContext);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
DefaultParameters: {},
|
|
47
|
+
ParameterSchema: {}
|
|
48
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-PerimeterMath
|
|
3
|
+
*
|
|
4
|
+
* Shared geometry helper for the Perimeter family of edge themes
|
|
5
|
+
* (Perimeter, Perimeter-Linear, Perimeter-Orthogonal). Solves the
|
|
6
|
+
* "where on this node's bounding box should the line attach so it
|
|
7
|
+
* points at the other end" question.
|
|
8
|
+
*
|
|
9
|
+
* The themes themselves only differ in how they render the path
|
|
10
|
+
* between the two attachment points; the attachment math is identical.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a perimeter attachment point for one end of a connection.
|
|
15
|
+
* Traces a line from the node's center toward the other end and returns
|
|
16
|
+
* the first crossing of the node's bounding-box perimeter, plus the
|
|
17
|
+
* `side` of that crossing (so the bezier helper departs in the right
|
|
18
|
+
* direction).
|
|
19
|
+
*
|
|
20
|
+
* @param {Object} pContext - the same shape the renderer passes into
|
|
21
|
+
* ResolveAttachment (Node, OtherNode, OtherDefaultPosition,
|
|
22
|
+
* DefaultPosition, …).
|
|
23
|
+
* @returns {{x: number, y: number, side: string}|Object|null}
|
|
24
|
+
* Returns null when node geometry is missing; returns
|
|
25
|
+
* DefaultPosition (verbatim) for degenerate same-center cases.
|
|
26
|
+
*/
|
|
27
|
+
function _resolvePerimeterAttachment(pContext)
|
|
28
|
+
{
|
|
29
|
+
let tmpNode = pContext.Node;
|
|
30
|
+
if (!tmpNode || typeof tmpNode.X !== 'number' || typeof tmpNode.Y !== 'number') return null;
|
|
31
|
+
|
|
32
|
+
let tmpW = tmpNode.Width || 180;
|
|
33
|
+
let tmpH = tmpNode.Height || 80;
|
|
34
|
+
let tmpCx = tmpNode.X + tmpW / 2;
|
|
35
|
+
let tmpCy = tmpNode.Y + tmpH / 2;
|
|
36
|
+
|
|
37
|
+
let tmpAimX, tmpAimY;
|
|
38
|
+
if (pContext.OtherNode && typeof pContext.OtherNode.X === 'number')
|
|
39
|
+
{
|
|
40
|
+
let tmpOW = pContext.OtherNode.Width || 180;
|
|
41
|
+
let tmpOH = pContext.OtherNode.Height || 80;
|
|
42
|
+
tmpAimX = pContext.OtherNode.X + tmpOW / 2;
|
|
43
|
+
tmpAimY = pContext.OtherNode.Y + tmpOH / 2;
|
|
44
|
+
}
|
|
45
|
+
else if (pContext.OtherDefaultPosition)
|
|
46
|
+
{
|
|
47
|
+
tmpAimX = pContext.OtherDefaultPosition.x;
|
|
48
|
+
tmpAimY = pContext.OtherDefaultPosition.y;
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
{
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let tmpDX = tmpAimX - tmpCx;
|
|
56
|
+
let tmpDY = tmpAimY - tmpCy;
|
|
57
|
+
|
|
58
|
+
if (tmpDX === 0 && tmpDY === 0) return pContext.DefaultPosition || null;
|
|
59
|
+
|
|
60
|
+
let tmpHalfW = tmpW / 2;
|
|
61
|
+
let tmpHalfH = tmpH / 2;
|
|
62
|
+
|
|
63
|
+
let tmpTX = (tmpDX > 0) ? tmpHalfW / tmpDX
|
|
64
|
+
: (tmpDX < 0) ? -tmpHalfW / tmpDX
|
|
65
|
+
: Infinity;
|
|
66
|
+
let tmpTY = (tmpDY > 0) ? tmpHalfH / tmpDY
|
|
67
|
+
: (tmpDY < 0) ? -tmpHalfH / tmpDY
|
|
68
|
+
: Infinity;
|
|
69
|
+
|
|
70
|
+
let tmpT, tmpSide;
|
|
71
|
+
if (tmpTX < tmpTY)
|
|
72
|
+
{
|
|
73
|
+
tmpT = tmpTX;
|
|
74
|
+
tmpSide = (tmpDX > 0) ? 'right' : 'left';
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
{
|
|
78
|
+
tmpT = tmpTY;
|
|
79
|
+
tmpSide = (tmpDY > 0) ? 'bottom' : 'top';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
x: tmpCx + tmpT * tmpDX,
|
|
84
|
+
y: tmpCy + tmpT * tmpDY,
|
|
85
|
+
side: tmpSide
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports =
|
|
90
|
+
{
|
|
91
|
+
resolvePerimeterAttachment: _resolvePerimeterAttachment
|
|
92
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-Straight
|
|
3
|
+
*
|
|
4
|
+
* Literal straight line from source port to target port. Useful for
|
|
5
|
+
* dense clusters where curves add visual noise, and for force-directed
|
|
6
|
+
* layouts where the topology already determines edge angles.
|
|
7
|
+
*/
|
|
8
|
+
module.exports =
|
|
9
|
+
{
|
|
10
|
+
Name: 'Straight',
|
|
11
|
+
Label: 'Straight line',
|
|
12
|
+
Description: 'Plain straight line between ports.',
|
|
13
|
+
|
|
14
|
+
GeneratePath: function (pContext)
|
|
15
|
+
{
|
|
16
|
+
let tmpSrc = pContext.Source;
|
|
17
|
+
let tmpTgt = pContext.Target;
|
|
18
|
+
return `M ${tmpSrc.x} ${tmpSrc.y} L ${tmpTgt.x} ${tmpTgt.y}`;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
DefaultParameters: {},
|
|
22
|
+
|
|
23
|
+
ParameterSchema: {}
|
|
24
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
const libCoerce = require('./Layout-Coerce.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layout-Circular
|
|
5
|
+
*
|
|
6
|
+
* Concentric-ring layout.
|
|
7
|
+
*
|
|
8
|
+
* Two modes, decided by the presence of connections:
|
|
9
|
+
*
|
|
10
|
+
* With connections: BFS from the root nodes (in-degree 0; or all
|
|
11
|
+
* nodes if every node has incoming edges). Each BFS depth becomes
|
|
12
|
+
* one ring. Roots cluster near the center, leaves on the outer rings.
|
|
13
|
+
*
|
|
14
|
+
* Without connections: all nodes share a single ring with equal
|
|
15
|
+
* angular spacing.
|
|
16
|
+
*
|
|
17
|
+
* Within each ring, nodes are placed at equal angles starting from
|
|
18
|
+
* `StartAngle` (in degrees, 0 = +X axis), going clockwise or
|
|
19
|
+
* counter-clockwise per `Direction`.
|
|
20
|
+
*/
|
|
21
|
+
module.exports =
|
|
22
|
+
{
|
|
23
|
+
Name: 'Circular',
|
|
24
|
+
Label: 'Circular',
|
|
25
|
+
Description: 'Concentric rings; uses connections for ring assignment when available.',
|
|
26
|
+
DefaultEdgeTheme: 'Perimeter',
|
|
27
|
+
|
|
28
|
+
Apply: function (pNodes, pConnections, pParameters)
|
|
29
|
+
{
|
|
30
|
+
if (!pNodes || pNodes.length === 0) return;
|
|
31
|
+
|
|
32
|
+
let tmpParams = pParameters || {};
|
|
33
|
+
let tmpSpacing = libCoerce.toFloat(tmpParams.Spacing, 1.0);
|
|
34
|
+
let tmpCenterX = libCoerce.toFloat(tmpParams.CenterX, 1000);
|
|
35
|
+
let tmpCenterY = libCoerce.toFloat(tmpParams.CenterY, 750);
|
|
36
|
+
let tmpRingSpacing = libCoerce.toFloat(tmpParams.RingSpacing, 220) * tmpSpacing;
|
|
37
|
+
let tmpInnerRadius = libCoerce.toFloat(tmpParams.InnerRadius, 0) * tmpSpacing;
|
|
38
|
+
let tmpStartAngle = libCoerce.toFloat(tmpParams.StartAngle, -90);
|
|
39
|
+
let tmpDirection = (tmpParams.Direction === 'ccw') ? 'ccw' : 'cw';
|
|
40
|
+
|
|
41
|
+
let tmpConnections = Array.isArray(pConnections) ? pConnections : [];
|
|
42
|
+
|
|
43
|
+
// Build in-degree map and adjacency
|
|
44
|
+
let tmpInDegree = {};
|
|
45
|
+
let tmpAdjacency = {};
|
|
46
|
+
let tmpNodeMap = {};
|
|
47
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
48
|
+
{
|
|
49
|
+
tmpNodeMap[pNodes[i].Hash] = pNodes[i];
|
|
50
|
+
tmpInDegree[pNodes[i].Hash] = 0;
|
|
51
|
+
tmpAdjacency[pNodes[i].Hash] = [];
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0; i < tmpConnections.length; i++)
|
|
54
|
+
{
|
|
55
|
+
let tmpConn = tmpConnections[i];
|
|
56
|
+
if (tmpInDegree.hasOwnProperty(tmpConn.TargetNodeHash))
|
|
57
|
+
{
|
|
58
|
+
tmpInDegree[tmpConn.TargetNodeHash]++;
|
|
59
|
+
}
|
|
60
|
+
if (tmpAdjacency.hasOwnProperty(tmpConn.SourceNodeHash))
|
|
61
|
+
{
|
|
62
|
+
tmpAdjacency[tmpConn.SourceNodeHash].push(tmpConn.TargetNodeHash);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Identify roots
|
|
67
|
+
let tmpRoots = [];
|
|
68
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
69
|
+
{
|
|
70
|
+
if (tmpInDegree[pNodes[i].Hash] === 0)
|
|
71
|
+
{
|
|
72
|
+
tmpRoots.push(pNodes[i].Hash);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let tmpRings = [];
|
|
77
|
+
|
|
78
|
+
if (tmpConnections.length === 0 || tmpRoots.length === 0)
|
|
79
|
+
{
|
|
80
|
+
tmpRings.push(pNodes.map((pNode) => pNode.Hash));
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
{
|
|
84
|
+
// BFS by depth
|
|
85
|
+
let tmpVisited = {};
|
|
86
|
+
let tmpCurrent = tmpRoots.slice();
|
|
87
|
+
while (tmpCurrent.length > 0)
|
|
88
|
+
{
|
|
89
|
+
let tmpRing = [];
|
|
90
|
+
let tmpNext = [];
|
|
91
|
+
for (let i = 0; i < tmpCurrent.length; i++)
|
|
92
|
+
{
|
|
93
|
+
let tmpHash = tmpCurrent[i];
|
|
94
|
+
if (tmpVisited[tmpHash]) continue;
|
|
95
|
+
tmpVisited[tmpHash] = true;
|
|
96
|
+
tmpRing.push(tmpHash);
|
|
97
|
+
|
|
98
|
+
let tmpChildren = tmpAdjacency[tmpHash] || [];
|
|
99
|
+
for (let j = 0; j < tmpChildren.length; j++)
|
|
100
|
+
{
|
|
101
|
+
if (!tmpVisited[tmpChildren[j]])
|
|
102
|
+
{
|
|
103
|
+
tmpNext.push(tmpChildren[j]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (tmpRing.length > 0) tmpRings.push(tmpRing);
|
|
108
|
+
tmpCurrent = tmpNext;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let tmpRemaining = [];
|
|
112
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
113
|
+
{
|
|
114
|
+
if (!tmpVisited[pNodes[i].Hash]) tmpRemaining.push(pNodes[i].Hash);
|
|
115
|
+
}
|
|
116
|
+
if (tmpRemaining.length > 0) tmpRings.push(tmpRemaining);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Place nodes
|
|
120
|
+
let tmpAngleSign = (tmpDirection === 'cw') ? 1 : -1;
|
|
121
|
+
let tmpStartRad = tmpStartAngle * Math.PI / 180;
|
|
122
|
+
|
|
123
|
+
for (let tmpRingIdx = 0; tmpRingIdx < tmpRings.length; tmpRingIdx++)
|
|
124
|
+
{
|
|
125
|
+
let tmpRing = tmpRings[tmpRingIdx];
|
|
126
|
+
let tmpRadius = tmpInnerRadius + tmpRingIdx * tmpRingSpacing;
|
|
127
|
+
|
|
128
|
+
if (tmpRing.length === 1 && tmpRingIdx === 0 && tmpInnerRadius === 0)
|
|
129
|
+
{
|
|
130
|
+
let tmpNode = tmpNodeMap[tmpRing[0]];
|
|
131
|
+
if (tmpNode)
|
|
132
|
+
{
|
|
133
|
+
tmpNode.X = tmpCenterX - (tmpNode.Width || 180) / 2;
|
|
134
|
+
tmpNode.Y = tmpCenterY - (tmpNode.Height || 80) / 2;
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let tmpAngleStep = (2 * Math.PI) / tmpRing.length;
|
|
140
|
+
for (let i = 0; i < tmpRing.length; i++)
|
|
141
|
+
{
|
|
142
|
+
let tmpNode = tmpNodeMap[tmpRing[i]];
|
|
143
|
+
if (!tmpNode) continue;
|
|
144
|
+
let tmpAngle = tmpStartRad + tmpAngleSign * i * tmpAngleStep;
|
|
145
|
+
let tmpW = tmpNode.Width || 180;
|
|
146
|
+
let tmpH = tmpNode.Height || 80;
|
|
147
|
+
tmpNode.X = tmpCenterX + Math.cos(tmpAngle) * tmpRadius - tmpW / 2;
|
|
148
|
+
tmpNode.Y = tmpCenterY + Math.sin(tmpAngle) * tmpRadius - tmpH / 2;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
DefaultParameters:
|
|
154
|
+
{
|
|
155
|
+
Spacing: 1.0,
|
|
156
|
+
CenterX: 1000,
|
|
157
|
+
CenterY: 750,
|
|
158
|
+
RingSpacing: 220,
|
|
159
|
+
InnerRadius: 0,
|
|
160
|
+
StartAngle: -90,
|
|
161
|
+
Direction: 'cw'
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
ParameterSchema:
|
|
165
|
+
{
|
|
166
|
+
Spacing: { Type: 'PreciseNumber', Label: 'Spacing (multiplier)', Default: 1.0, Min: 0.1, Max: 5 },
|
|
167
|
+
CenterX: { Type: 'PreciseNumber', Label: 'Center X', Default: 1000, Min: -10000, Max: 10000 },
|
|
168
|
+
CenterY: { Type: 'PreciseNumber', Label: 'Center Y', Default: 750, Min: -10000, Max: 10000 },
|
|
169
|
+
RingSpacing: { Type: 'PreciseNumber', Label: 'Ring spacing', Default: 220, Min: 1, Max: 5000 },
|
|
170
|
+
InnerRadius: { Type: 'PreciseNumber', Label: 'Inner radius', Default: 0, Min: 0, Max: 5000 },
|
|
171
|
+
StartAngle: { Type: 'PreciseNumber', Label: 'Start angle (deg)', Default: -90, Min: -360, Max: 360 },
|
|
172
|
+
Direction: { Type: 'enum', Label: 'Direction', Default: 'cw', Options: ['cw', 'ccw'] }
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
ParameterManifest:
|
|
176
|
+
{
|
|
177
|
+
Scope: 'PictFlowLayout-Circular',
|
|
178
|
+
Sections:
|
|
179
|
+
[
|
|
180
|
+
{ Name: 'Circular Parameters', Hash: 'PFLCircularSection', Groups: [{ Name: 'Defaults', Hash: 'PFLCircularGroup' }] }
|
|
181
|
+
],
|
|
182
|
+
Descriptors:
|
|
183
|
+
{
|
|
184
|
+
'PictFlowLayoutEditor.Parameters.Spacing':
|
|
185
|
+
{ Name: 'Spacing (multiplier)', Hash: 'Spacing', DataType: 'PreciseNumber', Default: 1.0, PictForm: { Section: 'PFLCircularSection', Group: 'PFLCircularGroup', Row: 0, Width: 12, Min: 0.1, Max: 5 } },
|
|
186
|
+
'PictFlowLayoutEditor.Parameters.CenterX':
|
|
187
|
+
{ Name: 'Center X', Hash: 'CenterX', DataType: 'PreciseNumber', Default: 1000, PictForm: { Section: 'PFLCircularSection', Group: 'PFLCircularGroup', Row: 1, Width: 6, Min: -10000, Max: 10000 } },
|
|
188
|
+
'PictFlowLayoutEditor.Parameters.CenterY':
|
|
189
|
+
{ Name: 'Center Y', Hash: 'CenterY', DataType: 'PreciseNumber', Default: 750, PictForm: { Section: 'PFLCircularSection', Group: 'PFLCircularGroup', Row: 1, Width: 6, Min: -10000, Max: 10000 } },
|
|
190
|
+
'PictFlowLayoutEditor.Parameters.RingSpacing':
|
|
191
|
+
{ Name: 'Ring spacing', Hash: 'RingSpacing', DataType: 'PreciseNumber', Default: 220, PictForm: { Section: 'PFLCircularSection', Group: 'PFLCircularGroup', Row: 2, Width: 6, Min: 1, Max: 5000 } },
|
|
192
|
+
'PictFlowLayoutEditor.Parameters.InnerRadius':
|
|
193
|
+
{ Name: 'Inner radius', Hash: 'InnerRadius', DataType: 'PreciseNumber', Default: 0, PictForm: { Section: 'PFLCircularSection', Group: 'PFLCircularGroup', Row: 2, Width: 6, Min: 0, Max: 5000 } },
|
|
194
|
+
'PictFlowLayoutEditor.Parameters.StartAngle':
|
|
195
|
+
{ Name: 'Start angle (deg)', Hash: 'StartAngle', DataType: 'PreciseNumber', Default: -90, PictForm: { Section: 'PFLCircularSection', Group: 'PFLCircularGroup', Row: 3, Width: 6, Min: -360, Max: 360 } },
|
|
196
|
+
'PictFlowLayoutEditor.Parameters.Direction':
|
|
197
|
+
{
|
|
198
|
+
Name: 'Direction', Hash: 'Direction', DataType: 'String', Default: 'cw',
|
|
199
|
+
PictForm: { Section: 'PFLCircularSection', Group: 'PFLCircularGroup', Row: 3, Width: 6, InputType: 'Option', SelectOptions: [{ Value: 'cw', Name: 'Clockwise' }, { Value: 'ccw', Name: 'Counter-clockwise' }] }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout-Coerce
|
|
3
|
+
*
|
|
4
|
+
* Tiny coercion helpers for layout-algorithm parameters. Manyfest's
|
|
5
|
+
* `PreciseNumber` DataType stores values as strings (so big.js /
|
|
6
|
+
* fable.ExpressionParser can do arbitrary-precision math on them
|
|
7
|
+
* without float drift). The simulation code in each layout algorithm
|
|
8
|
+
* uses native JS Math, so we coerce at the entry point.
|
|
9
|
+
*
|
|
10
|
+
* Apply functions accept either format (number or string) and use
|
|
11
|
+
* these helpers to normalize.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function _toFloat(pValue, pDefault)
|
|
15
|
+
{
|
|
16
|
+
if (typeof pValue === 'number' && !isNaN(pValue)) return pValue;
|
|
17
|
+
if (typeof pValue === 'string' && pValue !== '')
|
|
18
|
+
{
|
|
19
|
+
let tmpNum = parseFloat(pValue);
|
|
20
|
+
if (!isNaN(tmpNum)) return tmpNum;
|
|
21
|
+
}
|
|
22
|
+
return pDefault;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function _toInt(pValue, pDefault)
|
|
26
|
+
{
|
|
27
|
+
if (typeof pValue === 'number' && !isNaN(pValue)) return Math.floor(pValue);
|
|
28
|
+
if (typeof pValue === 'string' && pValue !== '')
|
|
29
|
+
{
|
|
30
|
+
let tmpNum = parseInt(pValue, 10);
|
|
31
|
+
if (!isNaN(tmpNum)) return tmpNum;
|
|
32
|
+
}
|
|
33
|
+
return pDefault;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports =
|
|
37
|
+
{
|
|
38
|
+
toFloat: _toFloat,
|
|
39
|
+
toInt: _toInt
|
|
40
|
+
};
|