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,134 @@
|
|
|
1
|
+
const libCoerce = require('./Layout-Coerce.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layout-Columnar
|
|
5
|
+
*
|
|
6
|
+
* N-column layout with deterministic fill order. Distinct from Grid in
|
|
7
|
+
* two ways:
|
|
8
|
+
* - Columns is always explicit (no 'auto')
|
|
9
|
+
* - FillOrder controls whether nodes flow row-first or column-first
|
|
10
|
+
*
|
|
11
|
+
* The column count is `Number` (integer index); spacing/origin values
|
|
12
|
+
* are `PreciseNumber` so they survive solver chains.
|
|
13
|
+
*/
|
|
14
|
+
module.exports =
|
|
15
|
+
{
|
|
16
|
+
Name: 'Columnar',
|
|
17
|
+
Label: 'Columnar (N Columns)',
|
|
18
|
+
Description: 'Explicit N columns; flow row-first or column-first.',
|
|
19
|
+
DefaultEdgeTheme: 'Orthogonal',
|
|
20
|
+
|
|
21
|
+
Apply: function (pNodes, pConnections, pParameters)
|
|
22
|
+
{
|
|
23
|
+
if (!pNodes || pNodes.length === 0) return;
|
|
24
|
+
|
|
25
|
+
let tmpParams = pParameters || {};
|
|
26
|
+
let tmpSpacing = libCoerce.toFloat(tmpParams.Spacing, 1.0);
|
|
27
|
+
let tmpColumns = Math.max(1, libCoerce.toInt(tmpParams.Columns, 3));
|
|
28
|
+
let tmpColumnSpacing = libCoerce.toFloat(tmpParams.ColumnSpacing, 40) * tmpSpacing;
|
|
29
|
+
let tmpRowSpacing = libCoerce.toFloat(tmpParams.RowSpacing, 40) * tmpSpacing;
|
|
30
|
+
let tmpStartX = libCoerce.toFloat(tmpParams.StartX, 100);
|
|
31
|
+
let tmpStartY = libCoerce.toFloat(tmpParams.StartY, 100);
|
|
32
|
+
let tmpFillOrder = (tmpParams.FillOrder === 'column') ? 'column' : 'row';
|
|
33
|
+
let tmpOrderBy = tmpParams.OrderBy || 'index';
|
|
34
|
+
|
|
35
|
+
// Compute cell dimensions from largest node
|
|
36
|
+
let tmpMaxWidth = 0;
|
|
37
|
+
let tmpMaxHeight = 0;
|
|
38
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
39
|
+
{
|
|
40
|
+
tmpMaxWidth = Math.max(tmpMaxWidth, pNodes[i].Width || 180);
|
|
41
|
+
tmpMaxHeight = Math.max(tmpMaxHeight, pNodes[i].Height || 80);
|
|
42
|
+
}
|
|
43
|
+
let tmpCellWidth = tmpMaxWidth + tmpColumnSpacing;
|
|
44
|
+
let tmpCellHeight = tmpMaxHeight + tmpRowSpacing;
|
|
45
|
+
|
|
46
|
+
let tmpOrdered = pNodes.slice();
|
|
47
|
+
if (tmpOrderBy === 'hash')
|
|
48
|
+
{
|
|
49
|
+
tmpOrdered.sort((pA, pB) => String(pA.Hash).localeCompare(String(pB.Hash)));
|
|
50
|
+
}
|
|
51
|
+
else if (tmpOrderBy === 'title')
|
|
52
|
+
{
|
|
53
|
+
tmpOrdered.sort((pA, pB) => String(pA.Title || pA.Hash).localeCompare(String(pB.Title || pB.Hash)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let tmpRows = Math.ceil(tmpOrdered.length / tmpColumns);
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < tmpOrdered.length; i++)
|
|
59
|
+
{
|
|
60
|
+
let tmpRow;
|
|
61
|
+
let tmpCol;
|
|
62
|
+
if (tmpFillOrder === 'column')
|
|
63
|
+
{
|
|
64
|
+
tmpCol = Math.floor(i / tmpRows);
|
|
65
|
+
tmpRow = i % tmpRows;
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
{
|
|
69
|
+
tmpRow = Math.floor(i / tmpColumns);
|
|
70
|
+
tmpCol = i % tmpColumns;
|
|
71
|
+
}
|
|
72
|
+
tmpOrdered[i].X = tmpStartX + tmpCol * tmpCellWidth;
|
|
73
|
+
tmpOrdered[i].Y = tmpStartY + tmpRow * tmpCellHeight;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
DefaultParameters:
|
|
78
|
+
{
|
|
79
|
+
Spacing: 1.0,
|
|
80
|
+
Columns: 3,
|
|
81
|
+
ColumnSpacing: 40,
|
|
82
|
+
RowSpacing: 40,
|
|
83
|
+
StartX: 100,
|
|
84
|
+
StartY: 100,
|
|
85
|
+
FillOrder: 'row',
|
|
86
|
+
OrderBy: 'index'
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
ParameterSchema:
|
|
90
|
+
{
|
|
91
|
+
Spacing: { Type: 'PreciseNumber', Label: 'Spacing (multiplier)', Default: 1.0, Min: 0.1, Max: 5 },
|
|
92
|
+
Columns: { Type: 'Number', Label: 'Columns', Default: 3, Min: 1, Max: 50 },
|
|
93
|
+
ColumnSpacing: { Type: 'PreciseNumber', Label: 'Column spacing', Default: 40, Min: 0, Max: 1000 },
|
|
94
|
+
RowSpacing: { Type: 'PreciseNumber', Label: 'Row spacing', Default: 40, Min: 0, Max: 1000 },
|
|
95
|
+
StartX: { Type: 'PreciseNumber', Label: 'Start X', Default: 100, Min: -10000, Max: 10000 },
|
|
96
|
+
StartY: { Type: 'PreciseNumber', Label: 'Start Y', Default: 100, Min: -10000, Max: 10000 },
|
|
97
|
+
FillOrder: { Type: 'enum', Label: 'Fill order', Default: 'row', Options: ['row', 'column'] },
|
|
98
|
+
OrderBy: { Type: 'enum', Label: 'Order by', Default: 'index', Options: ['index', 'hash', 'title'] }
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
ParameterManifest:
|
|
102
|
+
{
|
|
103
|
+
Scope: 'PictFlowLayout-Columnar',
|
|
104
|
+
Sections:
|
|
105
|
+
[
|
|
106
|
+
{ Name: 'Columnar Parameters', Hash: 'PFLColumnarSection', Groups: [{ Name: 'Defaults', Hash: 'PFLColumnarGroup' }] }
|
|
107
|
+
],
|
|
108
|
+
Descriptors:
|
|
109
|
+
{
|
|
110
|
+
'PictFlowLayoutEditor.Parameters.Spacing':
|
|
111
|
+
{ Name: 'Spacing (multiplier)', Hash: 'Spacing', DataType: 'PreciseNumber', Default: 1.0, PictForm: { Section: 'PFLColumnarSection', Group: 'PFLColumnarGroup', Row: 0, Width: 12, Min: 0.1, Max: 5 } },
|
|
112
|
+
'PictFlowLayoutEditor.Parameters.Columns':
|
|
113
|
+
{ Name: 'Columns', Hash: 'Columns', DataType: 'Number', Default: 3, PictForm: { Section: 'PFLColumnarSection', Group: 'PFLColumnarGroup', Row: 1, Width: 6, Min: 1, Max: 50 } },
|
|
114
|
+
'PictFlowLayoutEditor.Parameters.FillOrder':
|
|
115
|
+
{
|
|
116
|
+
Name: 'Fill order', Hash: 'FillOrder', DataType: 'String', Default: 'row',
|
|
117
|
+
PictForm: { Section: 'PFLColumnarSection', Group: 'PFLColumnarGroup', Row: 1, Width: 6, InputType: 'Option', SelectOptions: [{ Value: 'row', Name: 'Row-first' }, { Value: 'column', Name: 'Column-first' }] }
|
|
118
|
+
},
|
|
119
|
+
'PictFlowLayoutEditor.Parameters.ColumnSpacing':
|
|
120
|
+
{ Name: 'Column spacing', Hash: 'ColumnSpacing', DataType: 'PreciseNumber', Default: 40, PictForm: { Section: 'PFLColumnarSection', Group: 'PFLColumnarGroup', Row: 2, Width: 6, Min: 0, Max: 1000 } },
|
|
121
|
+
'PictFlowLayoutEditor.Parameters.RowSpacing':
|
|
122
|
+
{ Name: 'Row spacing', Hash: 'RowSpacing', DataType: 'PreciseNumber', Default: 40, PictForm: { Section: 'PFLColumnarSection', Group: 'PFLColumnarGroup', Row: 2, Width: 6, Min: 0, Max: 1000 } },
|
|
123
|
+
'PictFlowLayoutEditor.Parameters.StartX':
|
|
124
|
+
{ Name: 'Start X', Hash: 'StartX', DataType: 'PreciseNumber', Default: 100, PictForm: { Section: 'PFLColumnarSection', Group: 'PFLColumnarGroup', Row: 3, Width: 6, Min: -10000, Max: 10000 } },
|
|
125
|
+
'PictFlowLayoutEditor.Parameters.StartY':
|
|
126
|
+
{ Name: 'Start Y', Hash: 'StartY', DataType: 'PreciseNumber', Default: 100, PictForm: { Section: 'PFLColumnarSection', Group: 'PFLColumnarGroup', Row: 3, Width: 6, Min: -10000, Max: 10000 } },
|
|
127
|
+
'PictFlowLayoutEditor.Parameters.OrderBy':
|
|
128
|
+
{
|
|
129
|
+
Name: 'Order by', Hash: 'OrderBy', DataType: 'String', Default: 'index',
|
|
130
|
+
PictForm: { Section: 'PFLColumnarSection', Group: 'PFLColumnarGroup', Row: 4, Width: 12, InputType: 'Option', SelectOptions: [{ Value: 'index', Name: 'Index' }, { Value: 'hash', Name: 'Hash' }, { Value: 'title', Name: 'Title' }] }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout-Custom
|
|
3
|
+
*
|
|
4
|
+
* No-op layout algorithm. Preserves the X/Y values currently on each
|
|
5
|
+
* node so users can hand-place nodes without an algorithm clobbering
|
|
6
|
+
* positions on the next render.
|
|
7
|
+
*
|
|
8
|
+
* Selecting "Custom" with auto-apply enabled is effectively a no-op
|
|
9
|
+
* on every structural change — useful as a way to disable the
|
|
10
|
+
* configured algorithm without unsetting it.
|
|
11
|
+
*/
|
|
12
|
+
module.exports =
|
|
13
|
+
{
|
|
14
|
+
Name: 'Custom',
|
|
15
|
+
Label: 'Custom (Hand-placed)',
|
|
16
|
+
Description: 'Preserve hand-placed positions. No automatic arrangement.',
|
|
17
|
+
DefaultEdgeTheme: 'Bezier',
|
|
18
|
+
|
|
19
|
+
Apply: function (pNodes, pConnections, pParameters)
|
|
20
|
+
{
|
|
21
|
+
// Intentionally empty — Custom is a no-op.
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
DefaultParameters: {},
|
|
25
|
+
|
|
26
|
+
ParameterSchema: {}
|
|
27
|
+
};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
const libCoerce = require('./Layout-Coerce.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layout-ForcedFromCenter
|
|
5
|
+
*
|
|
6
|
+
* Force-directed simulation in the Fruchterman-Reingold style:
|
|
7
|
+
* - Spring (attractive) forces along each connection
|
|
8
|
+
* - Coulomb-style repulsion between every pair of nodes
|
|
9
|
+
* - Center-attraction force pulling all nodes toward (CenterX, CenterY)
|
|
10
|
+
* - Cooling schedule: max displacement per iteration shrinks over time
|
|
11
|
+
*
|
|
12
|
+
* Deterministic by default — initial positions for unplaced nodes are
|
|
13
|
+
* generated from a seedable Mulberry32 PRNG (inline implementation, no
|
|
14
|
+
* new dependencies). Tests pin `Seed` to assert byte-identical output.
|
|
15
|
+
*
|
|
16
|
+
* Performance: O(n^2) per iteration. With Iterations=200 this is fine
|
|
17
|
+
* for ~100 nodes; keep ForcedFromCenter for moderate-sized graphs.
|
|
18
|
+
*
|
|
19
|
+
* Numeric parameters (other than Iterations and Seed) are typed
|
|
20
|
+
* `PreciseNumber` so they round-trip cleanly through the
|
|
21
|
+
* ExpressionParser; the simulation coerces back to JS floats via
|
|
22
|
+
* Layout-Coerce. Iterations and Seed stay `Number` because they're
|
|
23
|
+
* loop counters / bitwise-PRNG state.
|
|
24
|
+
*/
|
|
25
|
+
module.exports =
|
|
26
|
+
{
|
|
27
|
+
Name: 'ForcedFromCenter',
|
|
28
|
+
Label: 'Forced from Center',
|
|
29
|
+
Description: 'Spring + repulsion simulation pulling toward a center point.',
|
|
30
|
+
DefaultEdgeTheme: 'Bezier',
|
|
31
|
+
|
|
32
|
+
Apply: function (pNodes, pConnections, pParameters)
|
|
33
|
+
{
|
|
34
|
+
if (!pNodes || pNodes.length === 0) return;
|
|
35
|
+
|
|
36
|
+
let tmpParams = pParameters || {};
|
|
37
|
+
let tmpSpacing = libCoerce.toFloat(tmpParams.Spacing, 1.0);
|
|
38
|
+
let tmpIterations = libCoerce.toInt(tmpParams.Iterations, 200);
|
|
39
|
+
let tmpCenterX = libCoerce.toFloat(tmpParams.CenterX, 1000);
|
|
40
|
+
let tmpCenterY = libCoerce.toFloat(tmpParams.CenterY, 750);
|
|
41
|
+
let tmpSpringLength = libCoerce.toFloat(tmpParams.SpringLength, 200) * tmpSpacing;
|
|
42
|
+
let tmpSpringStiffness = libCoerce.toFloat(tmpParams.SpringStiffness, 0.05);
|
|
43
|
+
let tmpRepulsion = libCoerce.toFloat(tmpParams.Repulsion, 8000);
|
|
44
|
+
let tmpCenterAttraction = libCoerce.toFloat(tmpParams.CenterAttraction, 0.01);
|
|
45
|
+
let tmpCoolingFactor = libCoerce.toFloat(tmpParams.CoolingFactor, 0.95);
|
|
46
|
+
let tmpInitialTemperature = libCoerce.toFloat(tmpParams.InitialTemperature, 100);
|
|
47
|
+
let tmpSeed = libCoerce.toInt(tmpParams.Seed, 42);
|
|
48
|
+
let tmpPreservePositions = !!tmpParams.PreservePositions;
|
|
49
|
+
let tmpInitialSpread = libCoerce.toFloat(tmpParams.InitialSpread, 400);
|
|
50
|
+
|
|
51
|
+
let tmpConnections = Array.isArray(pConnections) ? pConnections : [];
|
|
52
|
+
|
|
53
|
+
// Mulberry32 seeded PRNG — deterministic initial-position generator.
|
|
54
|
+
let tmpRand = _makeMulberry32(tmpSeed >>> 0);
|
|
55
|
+
|
|
56
|
+
// Initial positions
|
|
57
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
58
|
+
{
|
|
59
|
+
let tmpNode = pNodes[i];
|
|
60
|
+
let tmpHasPosition = (typeof tmpNode.X === 'number' && typeof tmpNode.Y === 'number');
|
|
61
|
+
if (tmpPreservePositions && tmpHasPosition) continue;
|
|
62
|
+
tmpNode.X = tmpCenterX + (tmpRand() - 0.5) * tmpInitialSpread;
|
|
63
|
+
tmpNode.Y = tmpCenterY + (tmpRand() - 0.5) * tmpInitialSpread;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Index nodes by hash for connection lookup
|
|
67
|
+
let tmpNodeMap = {};
|
|
68
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
69
|
+
{
|
|
70
|
+
tmpNodeMap[pNodes[i].Hash] = pNodes[i];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let tmpTemperature = tmpInitialTemperature;
|
|
74
|
+
|
|
75
|
+
for (let tmpIter = 0; tmpIter < tmpIterations; tmpIter++)
|
|
76
|
+
{
|
|
77
|
+
// Force accumulators
|
|
78
|
+
let tmpForceX = new Array(pNodes.length).fill(0);
|
|
79
|
+
let tmpForceY = new Array(pNodes.length).fill(0);
|
|
80
|
+
|
|
81
|
+
// Repulsion between every pair of nodes
|
|
82
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
83
|
+
{
|
|
84
|
+
let tmpA = pNodes[i];
|
|
85
|
+
for (let j = i + 1; j < pNodes.length; j++)
|
|
86
|
+
{
|
|
87
|
+
let tmpB = pNodes[j];
|
|
88
|
+
let tmpDX = tmpA.X - tmpB.X;
|
|
89
|
+
let tmpDY = tmpA.Y - tmpB.Y;
|
|
90
|
+
let tmpDistSq = tmpDX * tmpDX + tmpDY * tmpDY;
|
|
91
|
+
if (tmpDistSq < 1) tmpDistSq = 1; // avoid singularities
|
|
92
|
+
let tmpDist = Math.sqrt(tmpDistSq);
|
|
93
|
+
let tmpForce = tmpRepulsion / tmpDistSq;
|
|
94
|
+
let tmpFX = (tmpDX / tmpDist) * tmpForce;
|
|
95
|
+
let tmpFY = (tmpDY / tmpDist) * tmpForce;
|
|
96
|
+
tmpForceX[i] += tmpFX;
|
|
97
|
+
tmpForceY[i] += tmpFY;
|
|
98
|
+
tmpForceX[j] -= tmpFX;
|
|
99
|
+
tmpForceY[j] -= tmpFY;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Spring forces along connections
|
|
104
|
+
for (let i = 0; i < tmpConnections.length; i++)
|
|
105
|
+
{
|
|
106
|
+
let tmpConn = tmpConnections[i];
|
|
107
|
+
let tmpSource = tmpNodeMap[tmpConn.SourceNodeHash];
|
|
108
|
+
let tmpTarget = tmpNodeMap[tmpConn.TargetNodeHash];
|
|
109
|
+
if (!tmpSource || !tmpTarget) continue;
|
|
110
|
+
|
|
111
|
+
let tmpSourceIdx = pNodes.indexOf(tmpSource);
|
|
112
|
+
let tmpTargetIdx = pNodes.indexOf(tmpTarget);
|
|
113
|
+
if (tmpSourceIdx < 0 || tmpTargetIdx < 0) continue;
|
|
114
|
+
|
|
115
|
+
let tmpDX = tmpTarget.X - tmpSource.X;
|
|
116
|
+
let tmpDY = tmpTarget.Y - tmpSource.Y;
|
|
117
|
+
let tmpDist = Math.sqrt(tmpDX * tmpDX + tmpDY * tmpDY);
|
|
118
|
+
if (tmpDist < 0.0001) tmpDist = 0.0001;
|
|
119
|
+
|
|
120
|
+
let tmpDelta = tmpDist - tmpSpringLength;
|
|
121
|
+
let tmpForce = tmpSpringStiffness * tmpDelta;
|
|
122
|
+
let tmpFX = (tmpDX / tmpDist) * tmpForce;
|
|
123
|
+
let tmpFY = (tmpDY / tmpDist) * tmpForce;
|
|
124
|
+
|
|
125
|
+
tmpForceX[tmpSourceIdx] += tmpFX;
|
|
126
|
+
tmpForceY[tmpSourceIdx] += tmpFY;
|
|
127
|
+
tmpForceX[tmpTargetIdx] -= tmpFX;
|
|
128
|
+
tmpForceY[tmpTargetIdx] -= tmpFY;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Center attraction
|
|
132
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
133
|
+
{
|
|
134
|
+
let tmpNode = pNodes[i];
|
|
135
|
+
tmpForceX[i] += (tmpCenterX - tmpNode.X) * tmpCenterAttraction;
|
|
136
|
+
tmpForceY[i] += (tmpCenterY - tmpNode.Y) * tmpCenterAttraction;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Apply forces with temperature clamp
|
|
140
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
141
|
+
{
|
|
142
|
+
let tmpNode = pNodes[i];
|
|
143
|
+
let tmpFX = tmpForceX[i];
|
|
144
|
+
let tmpFY = tmpForceY[i];
|
|
145
|
+
let tmpMag = Math.sqrt(tmpFX * tmpFX + tmpFY * tmpFY);
|
|
146
|
+
if (tmpMag > tmpTemperature)
|
|
147
|
+
{
|
|
148
|
+
tmpFX = (tmpFX / tmpMag) * tmpTemperature;
|
|
149
|
+
tmpFY = (tmpFY / tmpMag) * tmpTemperature;
|
|
150
|
+
}
|
|
151
|
+
tmpNode.X += tmpFX;
|
|
152
|
+
tmpNode.Y += tmpFY;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
tmpTemperature *= tmpCoolingFactor;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Round to whole pixels for stable rendering and predictable tests
|
|
159
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
160
|
+
{
|
|
161
|
+
pNodes[i].X = Math.round(pNodes[i].X);
|
|
162
|
+
pNodes[i].Y = Math.round(pNodes[i].Y);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
DefaultParameters:
|
|
167
|
+
{
|
|
168
|
+
Spacing: 1.0,
|
|
169
|
+
Iterations: 200,
|
|
170
|
+
CenterX: 1000,
|
|
171
|
+
CenterY: 750,
|
|
172
|
+
SpringLength: 200,
|
|
173
|
+
SpringStiffness: 0.05,
|
|
174
|
+
Repulsion: 8000,
|
|
175
|
+
CenterAttraction: 0.01,
|
|
176
|
+
CoolingFactor: 0.95,
|
|
177
|
+
InitialTemperature: 100,
|
|
178
|
+
Seed: 42,
|
|
179
|
+
PreservePositions: false,
|
|
180
|
+
InitialSpread: 400
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
ParameterSchema:
|
|
184
|
+
{
|
|
185
|
+
Spacing: { Type: 'PreciseNumber', Label: 'Spacing (multiplier)', Default: 1.0, Min: 0.1, Max: 5 },
|
|
186
|
+
Iterations: { Type: 'Number', Label: 'Iterations', Default: 200, Min: 1, Max: 2000 },
|
|
187
|
+
CenterX: { Type: 'PreciseNumber', Label: 'Center X', Default: 1000, Min: -10000, Max: 10000 },
|
|
188
|
+
CenterY: { Type: 'PreciseNumber', Label: 'Center Y', Default: 750, Min: -10000, Max: 10000 },
|
|
189
|
+
SpringLength: { Type: 'PreciseNumber', Label: 'Spring length', Default: 200, Min: 1, Max: 2000 },
|
|
190
|
+
SpringStiffness: { Type: 'PreciseNumber', Label: 'Spring stiffness', Default: 0.05, Min: 0, Max: 1 },
|
|
191
|
+
Repulsion: { Type: 'PreciseNumber', Label: 'Repulsion', Default: 8000, Min: 0, Max: 100000 },
|
|
192
|
+
CenterAttraction: { Type: 'PreciseNumber', Label: 'Center attraction', Default: 0.01, Min: 0, Max: 1 },
|
|
193
|
+
CoolingFactor: { Type: 'PreciseNumber', Label: 'Cooling factor', Default: 0.95, Min: 0.5, Max: 1 },
|
|
194
|
+
InitialTemperature: { Type: 'PreciseNumber', Label: 'Initial temperature', Default: 100, Min: 1, Max: 1000 },
|
|
195
|
+
Seed: { Type: 'Number', Label: 'Random seed', Default: 42, Min: 0, Max: 2147483647 },
|
|
196
|
+
PreservePositions: { Type: 'boolean', Label: 'Preserve positions', Default: false },
|
|
197
|
+
InitialSpread: { Type: 'PreciseNumber', Label: 'Initial spread', Default: 400, Min: 0, Max: 5000 }
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
ParameterManifest:
|
|
201
|
+
{
|
|
202
|
+
Scope: 'PictFlowLayout-ForcedFromCenter',
|
|
203
|
+
Sections:
|
|
204
|
+
[
|
|
205
|
+
{ Name: 'Center', Hash: 'PFLCenterSection', Groups: [{ Name: 'Defaults', Hash: 'PFLCenterGroup' }] },
|
|
206
|
+
{ Name: 'Forces', Hash: 'PFLForcesSection', Groups: [{ Name: 'Defaults', Hash: 'PFLForcesGroup' }] },
|
|
207
|
+
{ Name: 'Simulation', Hash: 'PFLSimSection', Groups: [{ Name: 'Defaults', Hash: 'PFLSimGroup' }] },
|
|
208
|
+
{ Name: 'Initialization', Hash: 'PFLInitSection', Groups: [{ Name: 'Defaults', Hash: 'PFLInitGroup' }] }
|
|
209
|
+
],
|
|
210
|
+
Descriptors:
|
|
211
|
+
{
|
|
212
|
+
'PictFlowLayoutEditor.Parameters.Spacing':
|
|
213
|
+
{ Name: 'Spacing (multiplier)', Hash: 'Spacing', DataType: 'PreciseNumber', Default: 1.0, PictForm: { Section: 'PFLCenterSection', Group: 'PFLCenterGroup', Row: 0, Width: 12, Min: 0.1, Max: 5 } },
|
|
214
|
+
'PictFlowLayoutEditor.Parameters.CenterX':
|
|
215
|
+
{ Name: 'Center X', Hash: 'CenterX', DataType: 'PreciseNumber', Default: 1000, PictForm: { Section: 'PFLCenterSection', Group: 'PFLCenterGroup', Row: 1, Width: 6, Min: -10000, Max: 10000 } },
|
|
216
|
+
'PictFlowLayoutEditor.Parameters.CenterY':
|
|
217
|
+
{ Name: 'Center Y', Hash: 'CenterY', DataType: 'PreciseNumber', Default: 750, PictForm: { Section: 'PFLCenterSection', Group: 'PFLCenterGroup', Row: 1, Width: 6, Min: -10000, Max: 10000 } },
|
|
218
|
+
'PictFlowLayoutEditor.Parameters.CenterAttraction':
|
|
219
|
+
{ Name: 'Center attraction', Hash: 'CenterAttraction', DataType: 'PreciseNumber', Default: 0.01, PictForm: { Section: 'PFLCenterSection', Group: 'PFLCenterGroup', Row: 2, Width: 12, Min: 0, Max: 1 } },
|
|
220
|
+
|
|
221
|
+
'PictFlowLayoutEditor.Parameters.SpringLength':
|
|
222
|
+
{ Name: 'Spring length', Hash: 'SpringLength', DataType: 'PreciseNumber', Default: 200, PictForm: { Section: 'PFLForcesSection', Group: 'PFLForcesGroup', Row: 1, Width: 6, Min: 1, Max: 2000 } },
|
|
223
|
+
'PictFlowLayoutEditor.Parameters.SpringStiffness':
|
|
224
|
+
{ Name: 'Spring stiffness', Hash: 'SpringStiffness', DataType: 'PreciseNumber', Default: 0.05, PictForm: { Section: 'PFLForcesSection', Group: 'PFLForcesGroup', Row: 1, Width: 6, Min: 0, Max: 1 } },
|
|
225
|
+
'PictFlowLayoutEditor.Parameters.Repulsion':
|
|
226
|
+
{ Name: 'Repulsion', Hash: 'Repulsion', DataType: 'PreciseNumber', Default: 8000, PictForm: { Section: 'PFLForcesSection', Group: 'PFLForcesGroup', Row: 2, Width: 12, Min: 0, Max: 100000 } },
|
|
227
|
+
|
|
228
|
+
'PictFlowLayoutEditor.Parameters.Iterations':
|
|
229
|
+
{ Name: 'Iterations', Hash: 'Iterations', DataType: 'Number', Default: 200, PictForm: { Section: 'PFLSimSection', Group: 'PFLSimGroup', Row: 1, Width: 6, Min: 1, Max: 2000 } },
|
|
230
|
+
'PictFlowLayoutEditor.Parameters.CoolingFactor':
|
|
231
|
+
{ Name: 'Cooling factor', Hash: 'CoolingFactor', DataType: 'PreciseNumber', Default: 0.95, PictForm: { Section: 'PFLSimSection', Group: 'PFLSimGroup', Row: 1, Width: 6, Min: 0.5, Max: 1 } },
|
|
232
|
+
'PictFlowLayoutEditor.Parameters.InitialTemperature':
|
|
233
|
+
{ Name: 'Initial temperature', Hash: 'InitialTemperature', DataType: 'PreciseNumber', Default: 100, PictForm: { Section: 'PFLSimSection', Group: 'PFLSimGroup', Row: 2, Width: 12, Min: 1, Max: 1000 } },
|
|
234
|
+
|
|
235
|
+
'PictFlowLayoutEditor.Parameters.Seed':
|
|
236
|
+
{ Name: 'Random seed', Hash: 'Seed', DataType: 'Number', Default: 42, PictForm: { Section: 'PFLInitSection', Group: 'PFLInitGroup', Row: 1, Width: 6, Min: 0, Max: 2147483647 } },
|
|
237
|
+
'PictFlowLayoutEditor.Parameters.InitialSpread':
|
|
238
|
+
{ Name: 'Initial spread', Hash: 'InitialSpread', DataType: 'PreciseNumber', Default: 400, PictForm: { Section: 'PFLInitSection', Group: 'PFLInitGroup', Row: 1, Width: 6, Min: 0, Max: 5000 } },
|
|
239
|
+
'PictFlowLayoutEditor.Parameters.PreservePositions':
|
|
240
|
+
{ Name: 'Preserve existing positions', Hash: 'PreservePositions', DataType: 'Boolean', Default: false, PictForm: { Section: 'PFLInitSection', Group: 'PFLInitGroup', Row: 2, Width: 12, InputType: 'Boolean' } }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function _makeMulberry32(pSeed)
|
|
246
|
+
{
|
|
247
|
+
let tmpState = pSeed >>> 0;
|
|
248
|
+
return function ()
|
|
249
|
+
{
|
|
250
|
+
tmpState = (tmpState + 0x6D2B79F5) >>> 0;
|
|
251
|
+
let tmpT = tmpState;
|
|
252
|
+
tmpT = Math.imul(tmpT ^ (tmpT >>> 15), tmpT | 1);
|
|
253
|
+
tmpT ^= tmpT + Math.imul(tmpT ^ (tmpT >>> 7), tmpT | 61);
|
|
254
|
+
return ((tmpT ^ (tmpT >>> 14)) >>> 0) / 4294967296;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const libCoerce = require('./Layout-Coerce.js');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Layout-Grid
|
|
5
|
+
*
|
|
6
|
+
* Auto-arrange nodes in a roughly-square grid. Cell width and height
|
|
7
|
+
* default to the largest node dimensions plus margins. Column count
|
|
8
|
+
* defaults to ceil(sqrt(n)) when `Columns` is 'auto'.
|
|
9
|
+
*
|
|
10
|
+
* Continuous params (margins, cell sizes, origin) are typed
|
|
11
|
+
* `PreciseNumber` so they survive solver chains; the integer column
|
|
12
|
+
* count and the categorical OrderBy stay `Number` / enum.
|
|
13
|
+
*/
|
|
14
|
+
module.exports =
|
|
15
|
+
{
|
|
16
|
+
Name: 'Grid',
|
|
17
|
+
Label: 'Grid',
|
|
18
|
+
Description: 'Auto-arrange in a roughly-square grid.',
|
|
19
|
+
DefaultEdgeTheme: 'Orthogonal',
|
|
20
|
+
|
|
21
|
+
Apply: function (pNodes, pConnections, pParameters)
|
|
22
|
+
{
|
|
23
|
+
if (!pNodes || pNodes.length === 0) return;
|
|
24
|
+
|
|
25
|
+
let tmpParams = pParameters || {};
|
|
26
|
+
let tmpSpacing = libCoerce.toFloat(tmpParams.Spacing, 1.0);
|
|
27
|
+
let tmpColumnsParam = tmpParams.Columns;
|
|
28
|
+
let tmpCellWidthParam = tmpParams.CellWidth;
|
|
29
|
+
let tmpCellHeightParam = tmpParams.CellHeight;
|
|
30
|
+
let tmpHorizontalMargin = libCoerce.toFloat(tmpParams.HorizontalMargin, 40) * tmpSpacing;
|
|
31
|
+
let tmpVerticalMargin = libCoerce.toFloat(tmpParams.VerticalMargin, 40) * tmpSpacing;
|
|
32
|
+
let tmpStartX = libCoerce.toFloat(tmpParams.StartX, 100);
|
|
33
|
+
let tmpStartY = libCoerce.toFloat(tmpParams.StartY, 100);
|
|
34
|
+
let tmpOrderBy = tmpParams.OrderBy || 'index';
|
|
35
|
+
|
|
36
|
+
let tmpColumns;
|
|
37
|
+
if (tmpColumnsParam === 'auto' || tmpColumnsParam == null || tmpColumnsParam === '')
|
|
38
|
+
{
|
|
39
|
+
tmpColumns = Math.max(1, Math.ceil(Math.sqrt(pNodes.length)));
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
{
|
|
43
|
+
tmpColumns = Math.max(1, libCoerce.toInt(tmpColumnsParam, Math.max(1, Math.ceil(Math.sqrt(pNodes.length)))));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Compute cell dimensions from largest node if not specified
|
|
47
|
+
let tmpMaxWidth = 0;
|
|
48
|
+
let tmpMaxHeight = 0;
|
|
49
|
+
for (let i = 0; i < pNodes.length; i++)
|
|
50
|
+
{
|
|
51
|
+
tmpMaxWidth = Math.max(tmpMaxWidth, pNodes[i].Width || 180);
|
|
52
|
+
tmpMaxHeight = Math.max(tmpMaxHeight, pNodes[i].Height || 80);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let tmpCellWidth = (tmpCellWidthParam == null) ? tmpMaxWidth + tmpHorizontalMargin : libCoerce.toFloat(tmpCellWidthParam, tmpMaxWidth + tmpHorizontalMargin);
|
|
56
|
+
let tmpCellHeight = (tmpCellHeightParam == null) ? tmpMaxHeight + tmpVerticalMargin : libCoerce.toFloat(tmpCellHeightParam, tmpMaxHeight + tmpVerticalMargin);
|
|
57
|
+
|
|
58
|
+
// Ordered iteration
|
|
59
|
+
let tmpOrdered = pNodes.slice();
|
|
60
|
+
if (tmpOrderBy === 'hash')
|
|
61
|
+
{
|
|
62
|
+
tmpOrdered.sort((pA, pB) => String(pA.Hash).localeCompare(String(pB.Hash)));
|
|
63
|
+
}
|
|
64
|
+
else if (tmpOrderBy === 'title')
|
|
65
|
+
{
|
|
66
|
+
tmpOrdered.sort((pA, pB) => String(pA.Title || pA.Hash).localeCompare(String(pB.Title || pB.Hash)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < tmpOrdered.length; i++)
|
|
70
|
+
{
|
|
71
|
+
let tmpRow = Math.floor(i / tmpColumns);
|
|
72
|
+
let tmpCol = i % tmpColumns;
|
|
73
|
+
tmpOrdered[i].X = tmpStartX + tmpCol * tmpCellWidth;
|
|
74
|
+
tmpOrdered[i].Y = tmpStartY + tmpRow * tmpCellHeight;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
DefaultParameters:
|
|
79
|
+
{
|
|
80
|
+
Spacing: 1.0,
|
|
81
|
+
Columns: 'auto',
|
|
82
|
+
HorizontalMargin: 40,
|
|
83
|
+
VerticalMargin: 40,
|
|
84
|
+
StartX: 100,
|
|
85
|
+
StartY: 100,
|
|
86
|
+
OrderBy: 'index'
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
ParameterSchema:
|
|
90
|
+
{
|
|
91
|
+
Spacing: { Type: 'PreciseNumber', Label: 'Spacing (multiplier)', Default: 1.0, Min: 0.1, Max: 5 },
|
|
92
|
+
Columns: { Type: 'string', Label: 'Columns', Default: 'auto', Description: '"auto" or an integer' },
|
|
93
|
+
CellWidth: { Type: 'PreciseNumber', Label: 'Cell width', Description: 'Defaults to largest node width + horizontal margin', Min: 1, Max: 5000 },
|
|
94
|
+
CellHeight: { Type: 'PreciseNumber', Label: 'Cell height', Description: 'Defaults to largest node height + vertical margin', Min: 1, Max: 5000 },
|
|
95
|
+
HorizontalMargin: { Type: 'PreciseNumber', Label: 'Horizontal margin', Default: 40, Min: 0, Max: 1000 },
|
|
96
|
+
VerticalMargin: { Type: 'PreciseNumber', Label: 'Vertical margin', Default: 40, Min: 0, Max: 1000 },
|
|
97
|
+
StartX: { Type: 'PreciseNumber', Label: 'Start X', Default: 100, Min: -10000, Max: 10000 },
|
|
98
|
+
StartY: { Type: 'PreciseNumber', Label: 'Start Y', Default: 100, Min: -10000, Max: 10000 },
|
|
99
|
+
OrderBy: { Type: 'enum', Label: 'Order by', Default: 'index', Options: ['index', 'hash', 'title'] }
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
ParameterManifest:
|
|
103
|
+
{
|
|
104
|
+
Scope: 'PictFlowLayout-Grid',
|
|
105
|
+
Sections:
|
|
106
|
+
[
|
|
107
|
+
{ Name: 'Grid Parameters', Hash: 'PFLGridSection', Groups: [{ Name: 'Defaults', Hash: 'PFLGridGroup' }] }
|
|
108
|
+
],
|
|
109
|
+
Descriptors:
|
|
110
|
+
{
|
|
111
|
+
'PictFlowLayoutEditor.Parameters.Spacing':
|
|
112
|
+
{ Name: 'Spacing (multiplier)', Hash: 'Spacing', DataType: 'PreciseNumber', Default: 1.0, PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 0, Width: 12, Min: 0.1, Max: 5 } },
|
|
113
|
+
'PictFlowLayoutEditor.Parameters.Columns':
|
|
114
|
+
{ Name: 'Columns ("auto" or integer)', Hash: 'Columns', DataType: 'String', Default: 'auto', PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 1, Width: 6 } },
|
|
115
|
+
'PictFlowLayoutEditor.Parameters.OrderBy':
|
|
116
|
+
{
|
|
117
|
+
Name: 'Order by', Hash: 'OrderBy', DataType: 'String', Default: 'index',
|
|
118
|
+
PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 1, Width: 6, InputType: 'Option', SelectOptions: [{ Value: 'index', Name: 'Index' }, { Value: 'hash', Name: 'Hash' }, { Value: 'title', Name: 'Title' }] }
|
|
119
|
+
},
|
|
120
|
+
'PictFlowLayoutEditor.Parameters.CellWidth':
|
|
121
|
+
{ Name: 'Cell width (auto = blank)', Hash: 'CellWidth', DataType: 'PreciseNumber', PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 2, Width: 6, Min: 1, Max: 5000 } },
|
|
122
|
+
'PictFlowLayoutEditor.Parameters.CellHeight':
|
|
123
|
+
{ Name: 'Cell height (auto = blank)', Hash: 'CellHeight', DataType: 'PreciseNumber', PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 2, Width: 6, Min: 1, Max: 5000 } },
|
|
124
|
+
'PictFlowLayoutEditor.Parameters.HorizontalMargin':
|
|
125
|
+
{ Name: 'Horizontal margin', Hash: 'HorizontalMargin', DataType: 'PreciseNumber', Default: 40, PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 3, Width: 6, Min: 0, Max: 1000 } },
|
|
126
|
+
'PictFlowLayoutEditor.Parameters.VerticalMargin':
|
|
127
|
+
{ Name: 'Vertical margin', Hash: 'VerticalMargin', DataType: 'PreciseNumber', Default: 40, PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 3, Width: 6, Min: 0, Max: 1000 } },
|
|
128
|
+
'PictFlowLayoutEditor.Parameters.StartX':
|
|
129
|
+
{ Name: 'Start X', Hash: 'StartX', DataType: 'PreciseNumber', Default: 100, PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 4, Width: 6, Min: -10000, Max: 10000 } },
|
|
130
|
+
'PictFlowLayoutEditor.Parameters.StartY':
|
|
131
|
+
{ Name: 'Start Y', Hash: 'StartY', DataType: 'PreciseNumber', Default: 100, PictForm: { Section: 'PFLGridSection', Group: 'PFLGridGroup', Row: 4, Width: 6, Min: -10000, Max: 10000 } }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|