@yuneta/gobj-ui 7.23.150 → 7.23.152
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 -2
- package/dist/gobj-ui.cjs.js +243 -7
- package/dist/gobj-ui.es.js +243 -7
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +43 -9
- package/src/treedb_elbow.js +293 -2
- package/src/treedb_elbow.test.js +115 -1
package/README.md
CHANGED
|
@@ -1446,8 +1446,24 @@ port, along the outside of both, and in from the other port's own side. And
|
|
|
1446
1446
|
the edges joining the **same two cards**, either way round, take **lanes**
|
|
1447
1447
|
10px apart in the order they were made (0, +1, −1…), so a new one never
|
|
1448
1448
|
moves the ones already drawn; a detour on a negative lane goes round the
|
|
1449
|
-
left.
|
|
1450
|
-
|
|
1449
|
+
left.
|
|
1450
|
+
|
|
1451
|
+
And no elbow crosses a card (`7.23.151`). Each is tried simple first — the
|
|
1452
|
+
channel, or the detour round its two cards — and only when that line crosses
|
|
1453
|
+
a card is it **routed**: out of its port, the shortest way along the gaps
|
|
1454
|
+
between the cards, and in. It is our own orthogonal search on a sparse grid
|
|
1455
|
+
(the lines of the cards' borders, grown by a 12px clearance that grows with
|
|
1456
|
+
the lane, plus the two ends; a turn costs 40px of line), over the cards within
|
|
1457
|
+
400px first and all of them if that finds no way; with no way at all the
|
|
1458
|
+
simple elbow stays. A parent and the row of children under it keep their bus;
|
|
1459
|
+
what gets routed is a detour with a third card in its way, an edge dropping
|
|
1460
|
+
several rows, an edge between two cards of one row. Since `7.23.152` a node is
|
|
1461
|
+
measured **whole** — card, ports and label — so a line keeps clear of the
|
|
1462
|
+
ports as well; the edge's own ports stick out of its own cards, so its first
|
|
1463
|
+
and last segments are not tested against their own box. G6's `shortest-path`
|
|
1464
|
+
router is not used: it is not exported, it
|
|
1465
|
+
falls back in silence to a route that crosses cards, and it rewrites its
|
|
1466
|
+
module defaults with every config it is given.
|
|
1451
1467
|
|
|
1452
1468
|
All three take the **same spanning tree**, chosen deterministically: roots are the
|
|
1453
1469
|
nodes with no incoming edge, in node order; a node belongs to the **first
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -29669,8 +29669,8 @@ function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
|
29669
29669
|
}
|
|
29670
29670
|
let out = 16 + Math.abs(shift);
|
|
29671
29671
|
let side = lane < 0 ? Math.min(box_s.x1, box_t.x1) - 24 - Math.abs(shift) : Math.max(box_s.x2, box_t.x2) + 24 + Math.abs(shift);
|
|
29672
|
-
let y1 = s[1] + out;
|
|
29673
|
-
let y2 = t[1] - out;
|
|
29672
|
+
let y1 = Math.max(s[1], box_s.y2) + out;
|
|
29673
|
+
let y2 = Math.min(t[1], box_t.y1) - out;
|
|
29674
29674
|
return [
|
|
29675
29675
|
[s[0], y1],
|
|
29676
29676
|
[side, y1],
|
|
@@ -29678,6 +29678,224 @@ function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
|
29678
29678
|
[t[0], y2]
|
|
29679
29679
|
];
|
|
29680
29680
|
}
|
|
29681
|
+
function box_expand(b, m) {
|
|
29682
|
+
return {
|
|
29683
|
+
x1: b.x1 - m,
|
|
29684
|
+
y1: b.y1 - m,
|
|
29685
|
+
x2: b.x2 + m,
|
|
29686
|
+
y2: b.y2 + m
|
|
29687
|
+
};
|
|
29688
|
+
}
|
|
29689
|
+
function box_overlaps(a, b) {
|
|
29690
|
+
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
29691
|
+
}
|
|
29692
|
+
function point_inside(p, b) {
|
|
29693
|
+
return p[0] > b.x1 && p[0] < b.x2 && p[1] > b.y1 && p[1] < b.y2;
|
|
29694
|
+
}
|
|
29695
|
+
function segment_hits(a, c, b) {
|
|
29696
|
+
let x1 = Math.min(a[0], c[0]);
|
|
29697
|
+
let x2 = Math.max(a[0], c[0]);
|
|
29698
|
+
let y1 = Math.min(a[1], c[1]);
|
|
29699
|
+
let y2 = Math.max(a[1], c[1]);
|
|
29700
|
+
return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
|
|
29701
|
+
}
|
|
29702
|
+
function same_box(a, b) {
|
|
29703
|
+
return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
|
|
29704
|
+
}
|
|
29705
|
+
/************************************************************
|
|
29706
|
+
* Does the polyline `points` (its ends included) cross any of
|
|
29707
|
+
* `boxes`? A box is shrunk by a pixel, so a line along its
|
|
29708
|
+
* border is not a hit.
|
|
29709
|
+
*
|
|
29710
|
+
* A box is a whole node, its ports included, and the edge's own
|
|
29711
|
+
* ports stick out of its own two cards: its first segment starts
|
|
29712
|
+
* inside `box_s` and its last one ends inside `box_t`. Those two
|
|
29713
|
+
* segments are not tested against their own box; every other
|
|
29714
|
+
* segment is, so a line may not run back over its own cards.
|
|
29715
|
+
************************************************************/
|
|
29716
|
+
function elbow_path_hits(points, boxes, box_s, box_t) {
|
|
29717
|
+
let last = points.length - 2;
|
|
29718
|
+
for (let b of boxes) {
|
|
29719
|
+
let inner = box_expand(b, -1);
|
|
29720
|
+
let own_s = same_box(b, box_s);
|
|
29721
|
+
let own_t = same_box(b, box_t);
|
|
29722
|
+
for (let i = 0; i <= last; i++) {
|
|
29723
|
+
if (own_s && i === 0 || own_t && i === last) continue;
|
|
29724
|
+
if (segment_hits(points[i], points[i + 1], inner)) return true;
|
|
29725
|
+
}
|
|
29726
|
+
}
|
|
29727
|
+
return false;
|
|
29728
|
+
}
|
|
29729
|
+
function heap_push(h, item) {
|
|
29730
|
+
h.push(item);
|
|
29731
|
+
let i = h.length - 1;
|
|
29732
|
+
while (i > 0) {
|
|
29733
|
+
let p = i - 1 >> 1;
|
|
29734
|
+
if (h[p][0] <= h[i][0]) break;
|
|
29735
|
+
[h[p], h[i]] = [h[i], h[p]];
|
|
29736
|
+
i = p;
|
|
29737
|
+
}
|
|
29738
|
+
}
|
|
29739
|
+
function heap_pop(h) {
|
|
29740
|
+
let top = h[0];
|
|
29741
|
+
let last = h.pop();
|
|
29742
|
+
if (h.length) {
|
|
29743
|
+
h[0] = last;
|
|
29744
|
+
let i = 0;
|
|
29745
|
+
for (;;) {
|
|
29746
|
+
let l = 2 * i + 1;
|
|
29747
|
+
let r = l + 1;
|
|
29748
|
+
let m = i;
|
|
29749
|
+
if (l < h.length && h[l][0] < h[m][0]) m = l;
|
|
29750
|
+
if (r < h.length && h[r][0] < h[m][0]) m = r;
|
|
29751
|
+
if (m === i) break;
|
|
29752
|
+
[h[m], h[i]] = [h[i], h[m]];
|
|
29753
|
+
i = m;
|
|
29754
|
+
}
|
|
29755
|
+
}
|
|
29756
|
+
return top;
|
|
29757
|
+
}
|
|
29758
|
+
/************************************************************
|
|
29759
|
+
* The shortest orthogonal line from `a` to `z` that enters none
|
|
29760
|
+
* of `boxes` (already expanded by the clearance), a turn costing
|
|
29761
|
+
* ROUTE_BEND. The line leaves `a` going down and ends at `z`
|
|
29762
|
+
* going down, since after `z` it drops into a port.
|
|
29763
|
+
*
|
|
29764
|
+
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
29765
|
+
* of the two ends, which is all an orthogonal shortest path ever
|
|
29766
|
+
* needs to turn on. Returns [a, ...turns, z], or null when every
|
|
29767
|
+
* way is closed.
|
|
29768
|
+
************************************************************/
|
|
29769
|
+
function orth_search(a, z, boxes) {
|
|
29770
|
+
for (let b of boxes) if (point_inside(a, b) || point_inside(z, b)) return null;
|
|
29771
|
+
let uniq = (list) => [...new Set(list)].sort((p, q) => p - q);
|
|
29772
|
+
let xs = uniq([a[0], z[0]].concat(...boxes.map((b) => [b.x1, b.x2])));
|
|
29773
|
+
let ys = uniq([a[1], z[1]].concat(...boxes.map((b) => [b.y1, b.y2])));
|
|
29774
|
+
let W = xs.length;
|
|
29775
|
+
let H = ys.length;
|
|
29776
|
+
let free = (x, y) => !boxes.some((b) => point_inside([x, y], b));
|
|
29777
|
+
let ok = new Uint8Array(W * H);
|
|
29778
|
+
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) ok[j * W + i] = free(xs[i], ys[j]) ? 1 : 0;
|
|
29779
|
+
let step_right = new Uint8Array(W * H);
|
|
29780
|
+
let step_down = new Uint8Array(W * H);
|
|
29781
|
+
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) {
|
|
29782
|
+
let k = j * W + i;
|
|
29783
|
+
if (!ok[k]) continue;
|
|
29784
|
+
if (i + 1 < W && ok[k + 1] && free((xs[i] + xs[i + 1]) / 2, ys[j])) step_right[k] = 1;
|
|
29785
|
+
if (j + 1 < H && ok[k + W] && free(xs[i], (ys[j] + ys[j + 1]) / 2)) step_down[k] = 1;
|
|
29786
|
+
}
|
|
29787
|
+
let start = ys.indexOf(a[1]) * W + xs.indexOf(a[0]);
|
|
29788
|
+
let goal = ys.indexOf(z[1]) * W + xs.indexOf(z[0]);
|
|
29789
|
+
let best = new Float64Array(W * H * 4).fill(Infinity);
|
|
29790
|
+
let prev = new Int32Array(W * H * 4).fill(-1);
|
|
29791
|
+
let heap = [];
|
|
29792
|
+
best[start * 4 + 2] = 0;
|
|
29793
|
+
heap_push(heap, [0, start * 4 + 2]);
|
|
29794
|
+
let moves = (k) => {
|
|
29795
|
+
let i = k % W;
|
|
29796
|
+
let j = (k - i) / W;
|
|
29797
|
+
let out = [];
|
|
29798
|
+
if (step_right[k]) out.push([
|
|
29799
|
+
k + 1,
|
|
29800
|
+
0,
|
|
29801
|
+
xs[i + 1] - xs[i]
|
|
29802
|
+
]);
|
|
29803
|
+
if (i > 0 && step_right[k - 1]) out.push([
|
|
29804
|
+
k - 1,
|
|
29805
|
+
1,
|
|
29806
|
+
xs[i] - xs[i - 1]
|
|
29807
|
+
]);
|
|
29808
|
+
if (step_down[k]) out.push([
|
|
29809
|
+
k + W,
|
|
29810
|
+
2,
|
|
29811
|
+
ys[j + 1] - ys[j]
|
|
29812
|
+
]);
|
|
29813
|
+
if (j > 0 && step_down[k - W]) out.push([
|
|
29814
|
+
k - W,
|
|
29815
|
+
3,
|
|
29816
|
+
ys[j] - ys[j - 1]
|
|
29817
|
+
]);
|
|
29818
|
+
return out;
|
|
29819
|
+
};
|
|
29820
|
+
let found = -1;
|
|
29821
|
+
let found_cost = Infinity;
|
|
29822
|
+
while (heap.length) {
|
|
29823
|
+
let [cost, state] = heap_pop(heap);
|
|
29824
|
+
if (cost > best[state] || cost >= found_cost) continue;
|
|
29825
|
+
let k = state >> 2;
|
|
29826
|
+
let dir = state & 3;
|
|
29827
|
+
if (k === goal) {
|
|
29828
|
+
let total = cost + (dir === 2 ? 0 : 40);
|
|
29829
|
+
if (total < found_cost) {
|
|
29830
|
+
found_cost = total;
|
|
29831
|
+
found = state;
|
|
29832
|
+
}
|
|
29833
|
+
continue;
|
|
29834
|
+
}
|
|
29835
|
+
for (let [nk, nd, len] of moves(k)) {
|
|
29836
|
+
let c = cost + len + (nd === dir ? 0 : 40);
|
|
29837
|
+
let ns = nk * 4 + nd;
|
|
29838
|
+
if (c < best[ns]) {
|
|
29839
|
+
best[ns] = c;
|
|
29840
|
+
prev[ns] = state;
|
|
29841
|
+
heap_push(heap, [c, ns]);
|
|
29842
|
+
}
|
|
29843
|
+
}
|
|
29844
|
+
}
|
|
29845
|
+
if (found < 0) return null;
|
|
29846
|
+
let states = [];
|
|
29847
|
+
for (let s = found; s >= 0; s = prev[s]) states.push(s);
|
|
29848
|
+
states.reverse();
|
|
29849
|
+
let pts = [a];
|
|
29850
|
+
for (let n = 1; n < states.length; n++) if (n + 1 < states.length && (states[n + 1] & 3) !== (states[n] & 3)) {
|
|
29851
|
+
let k = states[n] >> 2;
|
|
29852
|
+
pts.push([xs[k % W], ys[Math.floor(k / W)]]);
|
|
29853
|
+
}
|
|
29854
|
+
pts.push(z);
|
|
29855
|
+
return pts;
|
|
29856
|
+
}
|
|
29857
|
+
/************************************************************
|
|
29858
|
+
* The control points of an elbow that does not cross any card.
|
|
29859
|
+
*
|
|
29860
|
+
* The simple elbow first (elbow_points): the channel, or the
|
|
29861
|
+
* detour round the two cards. Only when that line crosses a card
|
|
29862
|
+
* -- a third record standing in a detour's way, a row in the way
|
|
29863
|
+
* of an edge that drops several rows -- is it ROUTED: out of the
|
|
29864
|
+
* port, the shortest way along the gaps between the cards, in.
|
|
29865
|
+
* A parent and the row of children under it keep their bus.
|
|
29866
|
+
*
|
|
29867
|
+
* `boxes` are all the nodes on screen, the two of the edge
|
|
29868
|
+
* included, each as the box of the WHOLE node -- card, ports and
|
|
29869
|
+
* label -- so a line clears the ports too. `box_s` and `box_t`
|
|
29870
|
+
* are the edge's own two, measured the same way. The route keeps
|
|
29871
|
+
* a clearance from every box, which
|
|
29872
|
+
* grows with the lane so that two routed edges of one pair do not
|
|
29873
|
+
* coincide. Nearby cards first; all of them if that finds no way;
|
|
29874
|
+
* the simple elbow if nothing does.
|
|
29875
|
+
************************************************************/
|
|
29876
|
+
function elbow_route(s, t, box_s, box_t, lane, vertical, boxes) {
|
|
29877
|
+
let others = boxes || [];
|
|
29878
|
+
if (!vertical) return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, others.map(swap_box)).map(swap);
|
|
29879
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
29880
|
+
if (!elbow_path_hits([
|
|
29881
|
+
s,
|
|
29882
|
+
...simple,
|
|
29883
|
+
t
|
|
29884
|
+
], others, box_s, box_t)) return simple;
|
|
29885
|
+
let m = 12 + Math.abs(lane) * 10;
|
|
29886
|
+
let a = [s[0], Math.max(s[1], box_s.y2) + m + 1];
|
|
29887
|
+
let z = [t[0], Math.min(t[1], box_t.y1) - m - 1];
|
|
29888
|
+
let region = box_expand({
|
|
29889
|
+
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
29890
|
+
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
|
29891
|
+
x2: Math.max(a[0], z[0], box_s.x2, box_t.x2),
|
|
29892
|
+
y2: Math.max(a[1], z[1], box_s.y2, box_t.y2)
|
|
29893
|
+
}, 400);
|
|
29894
|
+
let near = others.filter((b) => box_overlaps(b, region));
|
|
29895
|
+
let route = orth_search(a, z, near.map((b) => box_expand(b, m)));
|
|
29896
|
+
if (!route && near.length < others.length) route = orth_search(a, z, others.map((b) => box_expand(b, m)));
|
|
29897
|
+
return route || simple;
|
|
29898
|
+
}
|
|
29681
29899
|
//#endregion
|
|
29682
29900
|
//#region node_modules/@babel/runtime/helpers/esm/typeof.js
|
|
29683
29901
|
function _typeof(o) {
|
|
@@ -59058,13 +59276,15 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
|
|
|
59058
59276
|
* Computed on every draw from where the two ends ARE, so a
|
|
59059
59277
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59060
59278
|
* between rows (top to bottom), `-h` between columns. An edge that
|
|
59061
|
-
* does not run forward goes round the two cards,
|
|
59062
|
-
*
|
|
59279
|
+
* does not run forward goes round the two cards, the edges joining
|
|
59280
|
+
* the same two cards take lanes, and a line that would cross any
|
|
59281
|
+
* other card is routed round it (treedb_elbow.js).
|
|
59063
59282
|
************************************************************/
|
|
59064
59283
|
var ELBOW_RADIUS = 6;
|
|
59065
59284
|
function elbow_box(node) {
|
|
59066
59285
|
try {
|
|
59067
|
-
let b = node.
|
|
59286
|
+
let b = node.getRenderBounds();
|
|
59287
|
+
if (!b || b.isEmpty && b.isEmpty()) b = node.getShape("key").getBounds();
|
|
59068
59288
|
return {
|
|
59069
59289
|
x1: b.min[0],
|
|
59070
59290
|
y1: b.min[1],
|
|
@@ -59082,6 +59302,22 @@ function elbow_box(node) {
|
|
|
59082
59302
|
};
|
|
59083
59303
|
}
|
|
59084
59304
|
}
|
|
59305
|
+
var _elbow_boxes = null;
|
|
59306
|
+
function elbow_obstacles(edge) {
|
|
59307
|
+
if (_elbow_boxes) return _elbow_boxes;
|
|
59308
|
+
let element = edge.context && edge.context.element;
|
|
59309
|
+
if (!element) return [];
|
|
59310
|
+
try {
|
|
59311
|
+
_elbow_boxes = element.getNodes().map((n) => elbow_box(n));
|
|
59312
|
+
} catch (e) {
|
|
59313
|
+
(0, _yuneta_gobj_js.log_error)(`elbow edge ${edge.id}: cannot read the cards: ${e}`);
|
|
59314
|
+
_elbow_boxes = [];
|
|
59315
|
+
}
|
|
59316
|
+
queueMicrotask(() => {
|
|
59317
|
+
_elbow_boxes = null;
|
|
59318
|
+
});
|
|
59319
|
+
return _elbow_boxes;
|
|
59320
|
+
}
|
|
59085
59321
|
function elbow_lane_of(edge) {
|
|
59086
59322
|
let graph = edge.context && edge.context.graph;
|
|
59087
59323
|
let s = edge.sourceNode && edge.sourceNode.id;
|
|
@@ -59097,7 +59333,7 @@ function elbow_lane_of(edge) {
|
|
|
59097
59333
|
var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
59098
59334
|
getControlPoints(attributes) {
|
|
59099
59335
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59100
|
-
return
|
|
59336
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
|
|
59101
59337
|
}
|
|
59102
59338
|
getKeyPath(attributes) {
|
|
59103
59339
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59106,7 +59342,7 @@ var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
|
59106
59342
|
var TreedbElbowH = class extends _antv_g6.Polyline {
|
|
59107
59343
|
getControlPoints(attributes) {
|
|
59108
59344
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59109
|
-
return
|
|
59345
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
|
|
59110
59346
|
}
|
|
59111
59347
|
getKeyPath(attributes) {
|
|
59112
59348
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -29664,8 +29664,8 @@ function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
|
29664
29664
|
}
|
|
29665
29665
|
let out = 16 + Math.abs(shift);
|
|
29666
29666
|
let side = lane < 0 ? Math.min(box_s.x1, box_t.x1) - 24 - Math.abs(shift) : Math.max(box_s.x2, box_t.x2) + 24 + Math.abs(shift);
|
|
29667
|
-
let y1 = s[1] + out;
|
|
29668
|
-
let y2 = t[1] - out;
|
|
29667
|
+
let y1 = Math.max(s[1], box_s.y2) + out;
|
|
29668
|
+
let y2 = Math.min(t[1], box_t.y1) - out;
|
|
29669
29669
|
return [
|
|
29670
29670
|
[s[0], y1],
|
|
29671
29671
|
[side, y1],
|
|
@@ -29673,6 +29673,224 @@ function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
|
29673
29673
|
[t[0], y2]
|
|
29674
29674
|
];
|
|
29675
29675
|
}
|
|
29676
|
+
function box_expand(b, m) {
|
|
29677
|
+
return {
|
|
29678
|
+
x1: b.x1 - m,
|
|
29679
|
+
y1: b.y1 - m,
|
|
29680
|
+
x2: b.x2 + m,
|
|
29681
|
+
y2: b.y2 + m
|
|
29682
|
+
};
|
|
29683
|
+
}
|
|
29684
|
+
function box_overlaps(a, b) {
|
|
29685
|
+
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
29686
|
+
}
|
|
29687
|
+
function point_inside(p, b) {
|
|
29688
|
+
return p[0] > b.x1 && p[0] < b.x2 && p[1] > b.y1 && p[1] < b.y2;
|
|
29689
|
+
}
|
|
29690
|
+
function segment_hits(a, c, b) {
|
|
29691
|
+
let x1 = Math.min(a[0], c[0]);
|
|
29692
|
+
let x2 = Math.max(a[0], c[0]);
|
|
29693
|
+
let y1 = Math.min(a[1], c[1]);
|
|
29694
|
+
let y2 = Math.max(a[1], c[1]);
|
|
29695
|
+
return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
|
|
29696
|
+
}
|
|
29697
|
+
function same_box(a, b) {
|
|
29698
|
+
return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
|
|
29699
|
+
}
|
|
29700
|
+
/************************************************************
|
|
29701
|
+
* Does the polyline `points` (its ends included) cross any of
|
|
29702
|
+
* `boxes`? A box is shrunk by a pixel, so a line along its
|
|
29703
|
+
* border is not a hit.
|
|
29704
|
+
*
|
|
29705
|
+
* A box is a whole node, its ports included, and the edge's own
|
|
29706
|
+
* ports stick out of its own two cards: its first segment starts
|
|
29707
|
+
* inside `box_s` and its last one ends inside `box_t`. Those two
|
|
29708
|
+
* segments are not tested against their own box; every other
|
|
29709
|
+
* segment is, so a line may not run back over its own cards.
|
|
29710
|
+
************************************************************/
|
|
29711
|
+
function elbow_path_hits(points, boxes, box_s, box_t) {
|
|
29712
|
+
let last = points.length - 2;
|
|
29713
|
+
for (let b of boxes) {
|
|
29714
|
+
let inner = box_expand(b, -1);
|
|
29715
|
+
let own_s = same_box(b, box_s);
|
|
29716
|
+
let own_t = same_box(b, box_t);
|
|
29717
|
+
for (let i = 0; i <= last; i++) {
|
|
29718
|
+
if (own_s && i === 0 || own_t && i === last) continue;
|
|
29719
|
+
if (segment_hits(points[i], points[i + 1], inner)) return true;
|
|
29720
|
+
}
|
|
29721
|
+
}
|
|
29722
|
+
return false;
|
|
29723
|
+
}
|
|
29724
|
+
function heap_push(h, item) {
|
|
29725
|
+
h.push(item);
|
|
29726
|
+
let i = h.length - 1;
|
|
29727
|
+
while (i > 0) {
|
|
29728
|
+
let p = i - 1 >> 1;
|
|
29729
|
+
if (h[p][0] <= h[i][0]) break;
|
|
29730
|
+
[h[p], h[i]] = [h[i], h[p]];
|
|
29731
|
+
i = p;
|
|
29732
|
+
}
|
|
29733
|
+
}
|
|
29734
|
+
function heap_pop(h) {
|
|
29735
|
+
let top = h[0];
|
|
29736
|
+
let last = h.pop();
|
|
29737
|
+
if (h.length) {
|
|
29738
|
+
h[0] = last;
|
|
29739
|
+
let i = 0;
|
|
29740
|
+
for (;;) {
|
|
29741
|
+
let l = 2 * i + 1;
|
|
29742
|
+
let r = l + 1;
|
|
29743
|
+
let m = i;
|
|
29744
|
+
if (l < h.length && h[l][0] < h[m][0]) m = l;
|
|
29745
|
+
if (r < h.length && h[r][0] < h[m][0]) m = r;
|
|
29746
|
+
if (m === i) break;
|
|
29747
|
+
[h[m], h[i]] = [h[i], h[m]];
|
|
29748
|
+
i = m;
|
|
29749
|
+
}
|
|
29750
|
+
}
|
|
29751
|
+
return top;
|
|
29752
|
+
}
|
|
29753
|
+
/************************************************************
|
|
29754
|
+
* The shortest orthogonal line from `a` to `z` that enters none
|
|
29755
|
+
* of `boxes` (already expanded by the clearance), a turn costing
|
|
29756
|
+
* ROUTE_BEND. The line leaves `a` going down and ends at `z`
|
|
29757
|
+
* going down, since after `z` it drops into a port.
|
|
29758
|
+
*
|
|
29759
|
+
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
29760
|
+
* of the two ends, which is all an orthogonal shortest path ever
|
|
29761
|
+
* needs to turn on. Returns [a, ...turns, z], or null when every
|
|
29762
|
+
* way is closed.
|
|
29763
|
+
************************************************************/
|
|
29764
|
+
function orth_search(a, z, boxes) {
|
|
29765
|
+
for (let b of boxes) if (point_inside(a, b) || point_inside(z, b)) return null;
|
|
29766
|
+
let uniq = (list) => [...new Set(list)].sort((p, q) => p - q);
|
|
29767
|
+
let xs = uniq([a[0], z[0]].concat(...boxes.map((b) => [b.x1, b.x2])));
|
|
29768
|
+
let ys = uniq([a[1], z[1]].concat(...boxes.map((b) => [b.y1, b.y2])));
|
|
29769
|
+
let W = xs.length;
|
|
29770
|
+
let H = ys.length;
|
|
29771
|
+
let free = (x, y) => !boxes.some((b) => point_inside([x, y], b));
|
|
29772
|
+
let ok = new Uint8Array(W * H);
|
|
29773
|
+
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) ok[j * W + i] = free(xs[i], ys[j]) ? 1 : 0;
|
|
29774
|
+
let step_right = new Uint8Array(W * H);
|
|
29775
|
+
let step_down = new Uint8Array(W * H);
|
|
29776
|
+
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) {
|
|
29777
|
+
let k = j * W + i;
|
|
29778
|
+
if (!ok[k]) continue;
|
|
29779
|
+
if (i + 1 < W && ok[k + 1] && free((xs[i] + xs[i + 1]) / 2, ys[j])) step_right[k] = 1;
|
|
29780
|
+
if (j + 1 < H && ok[k + W] && free(xs[i], (ys[j] + ys[j + 1]) / 2)) step_down[k] = 1;
|
|
29781
|
+
}
|
|
29782
|
+
let start = ys.indexOf(a[1]) * W + xs.indexOf(a[0]);
|
|
29783
|
+
let goal = ys.indexOf(z[1]) * W + xs.indexOf(z[0]);
|
|
29784
|
+
let best = new Float64Array(W * H * 4).fill(Infinity);
|
|
29785
|
+
let prev = new Int32Array(W * H * 4).fill(-1);
|
|
29786
|
+
let heap = [];
|
|
29787
|
+
best[start * 4 + 2] = 0;
|
|
29788
|
+
heap_push(heap, [0, start * 4 + 2]);
|
|
29789
|
+
let moves = (k) => {
|
|
29790
|
+
let i = k % W;
|
|
29791
|
+
let j = (k - i) / W;
|
|
29792
|
+
let out = [];
|
|
29793
|
+
if (step_right[k]) out.push([
|
|
29794
|
+
k + 1,
|
|
29795
|
+
0,
|
|
29796
|
+
xs[i + 1] - xs[i]
|
|
29797
|
+
]);
|
|
29798
|
+
if (i > 0 && step_right[k - 1]) out.push([
|
|
29799
|
+
k - 1,
|
|
29800
|
+
1,
|
|
29801
|
+
xs[i] - xs[i - 1]
|
|
29802
|
+
]);
|
|
29803
|
+
if (step_down[k]) out.push([
|
|
29804
|
+
k + W,
|
|
29805
|
+
2,
|
|
29806
|
+
ys[j + 1] - ys[j]
|
|
29807
|
+
]);
|
|
29808
|
+
if (j > 0 && step_down[k - W]) out.push([
|
|
29809
|
+
k - W,
|
|
29810
|
+
3,
|
|
29811
|
+
ys[j] - ys[j - 1]
|
|
29812
|
+
]);
|
|
29813
|
+
return out;
|
|
29814
|
+
};
|
|
29815
|
+
let found = -1;
|
|
29816
|
+
let found_cost = Infinity;
|
|
29817
|
+
while (heap.length) {
|
|
29818
|
+
let [cost, state] = heap_pop(heap);
|
|
29819
|
+
if (cost > best[state] || cost >= found_cost) continue;
|
|
29820
|
+
let k = state >> 2;
|
|
29821
|
+
let dir = state & 3;
|
|
29822
|
+
if (k === goal) {
|
|
29823
|
+
let total = cost + (dir === 2 ? 0 : 40);
|
|
29824
|
+
if (total < found_cost) {
|
|
29825
|
+
found_cost = total;
|
|
29826
|
+
found = state;
|
|
29827
|
+
}
|
|
29828
|
+
continue;
|
|
29829
|
+
}
|
|
29830
|
+
for (let [nk, nd, len] of moves(k)) {
|
|
29831
|
+
let c = cost + len + (nd === dir ? 0 : 40);
|
|
29832
|
+
let ns = nk * 4 + nd;
|
|
29833
|
+
if (c < best[ns]) {
|
|
29834
|
+
best[ns] = c;
|
|
29835
|
+
prev[ns] = state;
|
|
29836
|
+
heap_push(heap, [c, ns]);
|
|
29837
|
+
}
|
|
29838
|
+
}
|
|
29839
|
+
}
|
|
29840
|
+
if (found < 0) return null;
|
|
29841
|
+
let states = [];
|
|
29842
|
+
for (let s = found; s >= 0; s = prev[s]) states.push(s);
|
|
29843
|
+
states.reverse();
|
|
29844
|
+
let pts = [a];
|
|
29845
|
+
for (let n = 1; n < states.length; n++) if (n + 1 < states.length && (states[n + 1] & 3) !== (states[n] & 3)) {
|
|
29846
|
+
let k = states[n] >> 2;
|
|
29847
|
+
pts.push([xs[k % W], ys[Math.floor(k / W)]]);
|
|
29848
|
+
}
|
|
29849
|
+
pts.push(z);
|
|
29850
|
+
return pts;
|
|
29851
|
+
}
|
|
29852
|
+
/************************************************************
|
|
29853
|
+
* The control points of an elbow that does not cross any card.
|
|
29854
|
+
*
|
|
29855
|
+
* The simple elbow first (elbow_points): the channel, or the
|
|
29856
|
+
* detour round the two cards. Only when that line crosses a card
|
|
29857
|
+
* -- a third record standing in a detour's way, a row in the way
|
|
29858
|
+
* of an edge that drops several rows -- is it ROUTED: out of the
|
|
29859
|
+
* port, the shortest way along the gaps between the cards, in.
|
|
29860
|
+
* A parent and the row of children under it keep their bus.
|
|
29861
|
+
*
|
|
29862
|
+
* `boxes` are all the nodes on screen, the two of the edge
|
|
29863
|
+
* included, each as the box of the WHOLE node -- card, ports and
|
|
29864
|
+
* label -- so a line clears the ports too. `box_s` and `box_t`
|
|
29865
|
+
* are the edge's own two, measured the same way. The route keeps
|
|
29866
|
+
* a clearance from every box, which
|
|
29867
|
+
* grows with the lane so that two routed edges of one pair do not
|
|
29868
|
+
* coincide. Nearby cards first; all of them if that finds no way;
|
|
29869
|
+
* the simple elbow if nothing does.
|
|
29870
|
+
************************************************************/
|
|
29871
|
+
function elbow_route(s, t, box_s, box_t, lane, vertical, boxes) {
|
|
29872
|
+
let others = boxes || [];
|
|
29873
|
+
if (!vertical) return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, others.map(swap_box)).map(swap);
|
|
29874
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
29875
|
+
if (!elbow_path_hits([
|
|
29876
|
+
s,
|
|
29877
|
+
...simple,
|
|
29878
|
+
t
|
|
29879
|
+
], others, box_s, box_t)) return simple;
|
|
29880
|
+
let m = 12 + Math.abs(lane) * 10;
|
|
29881
|
+
let a = [s[0], Math.max(s[1], box_s.y2) + m + 1];
|
|
29882
|
+
let z = [t[0], Math.min(t[1], box_t.y1) - m - 1];
|
|
29883
|
+
let region = box_expand({
|
|
29884
|
+
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
29885
|
+
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
|
29886
|
+
x2: Math.max(a[0], z[0], box_s.x2, box_t.x2),
|
|
29887
|
+
y2: Math.max(a[1], z[1], box_s.y2, box_t.y2)
|
|
29888
|
+
}, 400);
|
|
29889
|
+
let near = others.filter((b) => box_overlaps(b, region));
|
|
29890
|
+
let route = orth_search(a, z, near.map((b) => box_expand(b, m)));
|
|
29891
|
+
if (!route && near.length < others.length) route = orth_search(a, z, others.map((b) => box_expand(b, m)));
|
|
29892
|
+
return route || simple;
|
|
29893
|
+
}
|
|
29676
29894
|
//#endregion
|
|
29677
29895
|
//#region node_modules/@babel/runtime/helpers/esm/typeof.js
|
|
29678
29896
|
function _typeof(o) {
|
|
@@ -59053,13 +59271,15 @@ var TreedbTreeLayout = class extends BaseLayout {
|
|
|
59053
59271
|
* Computed on every draw from where the two ends ARE, so a
|
|
59054
59272
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59055
59273
|
* between rows (top to bottom), `-h` between columns. An edge that
|
|
59056
|
-
* does not run forward goes round the two cards,
|
|
59057
|
-
*
|
|
59274
|
+
* does not run forward goes round the two cards, the edges joining
|
|
59275
|
+
* the same two cards take lanes, and a line that would cross any
|
|
59276
|
+
* other card is routed round it (treedb_elbow.js).
|
|
59058
59277
|
************************************************************/
|
|
59059
59278
|
var ELBOW_RADIUS = 6;
|
|
59060
59279
|
function elbow_box(node) {
|
|
59061
59280
|
try {
|
|
59062
|
-
let b = node.
|
|
59281
|
+
let b = node.getRenderBounds();
|
|
59282
|
+
if (!b || b.isEmpty && b.isEmpty()) b = node.getShape("key").getBounds();
|
|
59063
59283
|
return {
|
|
59064
59284
|
x1: b.min[0],
|
|
59065
59285
|
y1: b.min[1],
|
|
@@ -59077,6 +59297,22 @@ function elbow_box(node) {
|
|
|
59077
59297
|
};
|
|
59078
59298
|
}
|
|
59079
59299
|
}
|
|
59300
|
+
var _elbow_boxes = null;
|
|
59301
|
+
function elbow_obstacles(edge) {
|
|
59302
|
+
if (_elbow_boxes) return _elbow_boxes;
|
|
59303
|
+
let element = edge.context && edge.context.element;
|
|
59304
|
+
if (!element) return [];
|
|
59305
|
+
try {
|
|
59306
|
+
_elbow_boxes = element.getNodes().map((n) => elbow_box(n));
|
|
59307
|
+
} catch (e) {
|
|
59308
|
+
log_error(`elbow edge ${edge.id}: cannot read the cards: ${e}`);
|
|
59309
|
+
_elbow_boxes = [];
|
|
59310
|
+
}
|
|
59311
|
+
queueMicrotask(() => {
|
|
59312
|
+
_elbow_boxes = null;
|
|
59313
|
+
});
|
|
59314
|
+
return _elbow_boxes;
|
|
59315
|
+
}
|
|
59080
59316
|
function elbow_lane_of(edge) {
|
|
59081
59317
|
let graph = edge.context && edge.context.graph;
|
|
59082
59318
|
let s = edge.sourceNode && edge.sourceNode.id;
|
|
@@ -59092,7 +59328,7 @@ function elbow_lane_of(edge) {
|
|
|
59092
59328
|
var TreedbElbowV = class extends Polyline {
|
|
59093
59329
|
getControlPoints(attributes) {
|
|
59094
59330
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59095
|
-
return
|
|
59331
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
|
|
59096
59332
|
}
|
|
59097
59333
|
getKeyPath(attributes) {
|
|
59098
59334
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59101,7 +59337,7 @@ var TreedbElbowV = class extends Polyline {
|
|
|
59101
59337
|
var TreedbElbowH = class extends Polyline {
|
|
59102
59338
|
getControlPoints(attributes) {
|
|
59103
59339
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59104
|
-
return
|
|
59340
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
|
|
59105
59341
|
}
|
|
59106
59342
|
getKeyPath(attributes) {
|
|
59107
59343
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
package/package.json
CHANGED
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -133,7 +133,7 @@ import {
|
|
|
133
133
|
fold_root_group_key,
|
|
134
134
|
} from "./treedb_fold_model.js";
|
|
135
135
|
import {layout_tree, layout_radial, layout_compact} from "./treedb_layout.js";
|
|
136
|
-
import {elbow_lane,
|
|
136
|
+
import {elbow_lane, elbow_route} from "./treedb_elbow.js";
|
|
137
137
|
|
|
138
138
|
import {
|
|
139
139
|
BaseLayout,
|
|
@@ -4223,16 +4223,23 @@ class TreedbTreeLayout extends BaseLayout
|
|
|
4223
4223
|
* Computed on every draw from where the two ends ARE, so a
|
|
4224
4224
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
4225
4225
|
* between rows (top to bottom), `-h` between columns. An edge that
|
|
4226
|
-
* does not run forward goes round the two cards,
|
|
4227
|
-
*
|
|
4226
|
+
* does not run forward goes round the two cards, the edges joining
|
|
4227
|
+
* the same two cards take lanes, and a line that would cross any
|
|
4228
|
+
* other card is routed round it (treedb_elbow.js).
|
|
4228
4229
|
************************************************************/
|
|
4229
4230
|
const ELBOW_RADIUS = 6;
|
|
4230
4231
|
|
|
4231
|
-
/* The box of a
|
|
4232
|
+
/* The box of a WHOLE node on the canvas -- the card, its ports and
|
|
4233
|
+
* its label -- for an elbow to keep clear of. The ports are the
|
|
4234
|
+
* reason it is not the key shape's box: they stick out of the
|
|
4235
|
+
* card, and a line over a port reads as a link to it. */
|
|
4232
4236
|
function elbow_box(node)
|
|
4233
4237
|
{
|
|
4234
4238
|
try {
|
|
4235
|
-
let b = node.
|
|
4239
|
+
let b = node.getRenderBounds();
|
|
4240
|
+
if(!b || b.isEmpty && b.isEmpty()) {
|
|
4241
|
+
b = node.getShape('key').getBounds();
|
|
4242
|
+
}
|
|
4236
4243
|
return {x1: b.min[0], y1: b.min[1], x2: b.max[0], y2: b.max[1]};
|
|
4237
4244
|
} catch(e) {
|
|
4238
4245
|
log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
|
|
@@ -4241,6 +4248,33 @@ function elbow_box(node)
|
|
|
4241
4248
|
}
|
|
4242
4249
|
}
|
|
4243
4250
|
|
|
4251
|
+
/* Every card on screen, as boxes: what a routed elbow keeps clear
|
|
4252
|
+
* of. Read ONCE per draw -- G6 draws all its edges in one go, so
|
|
4253
|
+
* the list lives until the next microtask and every edge of that
|
|
4254
|
+
* draw shares it. */
|
|
4255
|
+
let _elbow_boxes = null;
|
|
4256
|
+
|
|
4257
|
+
function elbow_obstacles(edge)
|
|
4258
|
+
{
|
|
4259
|
+
if(_elbow_boxes) {
|
|
4260
|
+
return _elbow_boxes;
|
|
4261
|
+
}
|
|
4262
|
+
let element = edge.context && edge.context.element;
|
|
4263
|
+
if(!element) {
|
|
4264
|
+
return [];
|
|
4265
|
+
}
|
|
4266
|
+
try {
|
|
4267
|
+
_elbow_boxes = element.getNodes().map((n) => elbow_box(n));
|
|
4268
|
+
} catch(e) {
|
|
4269
|
+
log_error(`elbow edge ${edge.id}: cannot read the cards: ${e}`);
|
|
4270
|
+
_elbow_boxes = [];
|
|
4271
|
+
}
|
|
4272
|
+
queueMicrotask(() => {
|
|
4273
|
+
_elbow_boxes = null;
|
|
4274
|
+
});
|
|
4275
|
+
return _elbow_boxes;
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4244
4278
|
/* The lane of an edge among the edges joining the same two cards,
|
|
4245
4279
|
* either way round (treedb_elbow.js). */
|
|
4246
4280
|
function elbow_lane_of(edge)
|
|
@@ -4267,8 +4301,8 @@ class TreedbElbowV extends Polyline
|
|
|
4267
4301
|
{
|
|
4268
4302
|
getControlPoints(attributes) {
|
|
4269
4303
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4270
|
-
return
|
|
4271
|
-
|
|
4304
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4305
|
+
elbow_lane_of(this), true, elbow_obstacles(this));
|
|
4272
4306
|
}
|
|
4273
4307
|
|
|
4274
4308
|
getKeyPath(attributes) {
|
|
@@ -4281,8 +4315,8 @@ class TreedbElbowH extends Polyline
|
|
|
4281
4315
|
{
|
|
4282
4316
|
getControlPoints(attributes) {
|
|
4283
4317
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4284
|
-
return
|
|
4285
|
-
|
|
4318
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4319
|
+
elbow_lane_of(this), false, elbow_obstacles(this));
|
|
4286
4320
|
}
|
|
4287
4321
|
|
|
4288
4322
|
getKeyPath(attributes) {
|
package/src/treedb_elbow.js
CHANGED
|
@@ -96,7 +96,298 @@ export function elbow_points(s, t, box_s, box_t, lane, vertical)
|
|
|
96
96
|
let side = (lane < 0)
|
|
97
97
|
? Math.min(box_s.x1, box_t.x1) - ELBOW_CLEAR - Math.abs(shift)
|
|
98
98
|
: Math.max(box_s.x2, box_t.x2) + ELBOW_CLEAR + Math.abs(shift);
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
/* From the border of the whole box, which holds the ports: a
|
|
100
|
+
* port sticks out of its card, and the run has to clear it. */
|
|
101
|
+
let y1 = Math.max(s[1], box_s.y2) + out; /* a hook port is on the bottom edge */
|
|
102
|
+
let y2 = Math.min(t[1], box_t.y1) - out; /* an fkey port is on the top edge */
|
|
101
103
|
return [[s[0], y1], [side, y1], [side, y2], [t[0], y2]];
|
|
102
104
|
}
|
|
105
|
+
|
|
106
|
+
/*----------------------------------------------------------------*
|
|
107
|
+
* Round the OTHER cards too
|
|
108
|
+
*----------------------------------------------------------------*/
|
|
109
|
+
export const ROUTE_REACH = 400; /* how far around the two cards the first search looks */
|
|
110
|
+
export const ROUTE_BEND = 40; /* a turn costs as much as this many pixels of line */
|
|
111
|
+
|
|
112
|
+
function box_expand(b, m)
|
|
113
|
+
{
|
|
114
|
+
return {x1: b.x1 - m, y1: b.y1 - m, x2: b.x2 + m, y2: b.y2 + m};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function box_overlaps(a, b)
|
|
118
|
+
{
|
|
119
|
+
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function point_inside(p, b)
|
|
123
|
+
{
|
|
124
|
+
return p[0] > b.x1 && p[0] < b.x2 && p[1] > b.y1 && p[1] < b.y2;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Does the axis-aligned segment a-c pass through the INSIDE of b?
|
|
128
|
+
* Running along its border does not count. */
|
|
129
|
+
function segment_hits(a, c, b)
|
|
130
|
+
{
|
|
131
|
+
let x1 = Math.min(a[0], c[0]);
|
|
132
|
+
let x2 = Math.max(a[0], c[0]);
|
|
133
|
+
let y1 = Math.min(a[1], c[1]);
|
|
134
|
+
let y2 = Math.max(a[1], c[1]);
|
|
135
|
+
return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function same_box(a, b)
|
|
139
|
+
{
|
|
140
|
+
return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/************************************************************
|
|
144
|
+
* Does the polyline `points` (its ends included) cross any of
|
|
145
|
+
* `boxes`? A box is shrunk by a pixel, so a line along its
|
|
146
|
+
* border is not a hit.
|
|
147
|
+
*
|
|
148
|
+
* A box is a whole node, its ports included, and the edge's own
|
|
149
|
+
* ports stick out of its own two cards: its first segment starts
|
|
150
|
+
* inside `box_s` and its last one ends inside `box_t`. Those two
|
|
151
|
+
* segments are not tested against their own box; every other
|
|
152
|
+
* segment is, so a line may not run back over its own cards.
|
|
153
|
+
************************************************************/
|
|
154
|
+
export function elbow_path_hits(points, boxes, box_s, box_t)
|
|
155
|
+
{
|
|
156
|
+
let last = points.length - 2;
|
|
157
|
+
for(let b of boxes) {
|
|
158
|
+
let inner = box_expand(b, -1);
|
|
159
|
+
let own_s = same_box(b, box_s);
|
|
160
|
+
let own_t = same_box(b, box_t);
|
|
161
|
+
for(let i = 0; i <= last; i++) {
|
|
162
|
+
if((own_s && i === 0) || (own_t && i === last)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if(segment_hits(points[i], points[i + 1], inner)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* A binary heap of [cost, state], smallest cost first. */
|
|
174
|
+
function heap_push(h, item)
|
|
175
|
+
{
|
|
176
|
+
h.push(item);
|
|
177
|
+
let i = h.length - 1;
|
|
178
|
+
while(i > 0) {
|
|
179
|
+
let p = (i - 1) >> 1;
|
|
180
|
+
if(h[p][0] <= h[i][0]) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
[h[p], h[i]] = [h[i], h[p]];
|
|
184
|
+
i = p;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function heap_pop(h)
|
|
189
|
+
{
|
|
190
|
+
let top = h[0];
|
|
191
|
+
let last = h.pop();
|
|
192
|
+
if(h.length) {
|
|
193
|
+
h[0] = last;
|
|
194
|
+
let i = 0;
|
|
195
|
+
for(;;) {
|
|
196
|
+
let l = 2 * i + 1;
|
|
197
|
+
let r = l + 1;
|
|
198
|
+
let m = i;
|
|
199
|
+
if(l < h.length && h[l][0] < h[m][0]) {
|
|
200
|
+
m = l;
|
|
201
|
+
}
|
|
202
|
+
if(r < h.length && h[r][0] < h[m][0]) {
|
|
203
|
+
m = r;
|
|
204
|
+
}
|
|
205
|
+
if(m === i) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
[h[m], h[i]] = [h[i], h[m]];
|
|
209
|
+
i = m;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return top;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/************************************************************
|
|
216
|
+
* The shortest orthogonal line from `a` to `z` that enters none
|
|
217
|
+
* of `boxes` (already expanded by the clearance), a turn costing
|
|
218
|
+
* ROUTE_BEND. The line leaves `a` going down and ends at `z`
|
|
219
|
+
* going down, since after `z` it drops into a port.
|
|
220
|
+
*
|
|
221
|
+
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
222
|
+
* of the two ends, which is all an orthogonal shortest path ever
|
|
223
|
+
* needs to turn on. Returns [a, ...turns, z], or null when every
|
|
224
|
+
* way is closed.
|
|
225
|
+
************************************************************/
|
|
226
|
+
function orth_search(a, z, boxes)
|
|
227
|
+
{
|
|
228
|
+
for(let b of boxes) {
|
|
229
|
+
if(point_inside(a, b) || point_inside(z, b)) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let uniq = (list) => [...new Set(list)].sort((p, q) => p - q);
|
|
235
|
+
let xs = uniq([a[0], z[0]].concat(...boxes.map((b) => [b.x1, b.x2])));
|
|
236
|
+
let ys = uniq([a[1], z[1]].concat(...boxes.map((b) => [b.y1, b.y2])));
|
|
237
|
+
let W = xs.length;
|
|
238
|
+
let H = ys.length;
|
|
239
|
+
|
|
240
|
+
let free = (x, y) => !boxes.some((b) => point_inside([x, y], b));
|
|
241
|
+
let ok = new Uint8Array(W * H);
|
|
242
|
+
for(let j = 0; j < H; j++) {
|
|
243
|
+
for(let i = 0; i < W; i++) {
|
|
244
|
+
ok[j * W + i] = free(xs[i], ys[j])? 1 : 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/* A step to the next line is open when both points are free and
|
|
248
|
+
* so is the middle of the step: consecutive lines have no border
|
|
249
|
+
* between them, so the middle speaks for the whole step. */
|
|
250
|
+
let step_right = new Uint8Array(W * H);
|
|
251
|
+
let step_down = new Uint8Array(W * H);
|
|
252
|
+
for(let j = 0; j < H; j++) {
|
|
253
|
+
for(let i = 0; i < W; i++) {
|
|
254
|
+
let k = j * W + i;
|
|
255
|
+
if(!ok[k]) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if(i + 1 < W && ok[k + 1] && free((xs[i] + xs[i + 1]) / 2, ys[j])) {
|
|
259
|
+
step_right[k] = 1;
|
|
260
|
+
}
|
|
261
|
+
if(j + 1 < H && ok[k + W] && free(xs[i], (ys[j] + ys[j + 1]) / 2)) {
|
|
262
|
+
step_down[k] = 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* States are (point, heading): 0 right, 1 left, 2 down, 3 up. */
|
|
268
|
+
let start = ys.indexOf(a[1]) * W + xs.indexOf(a[0]);
|
|
269
|
+
let goal = ys.indexOf(z[1]) * W + xs.indexOf(z[0]);
|
|
270
|
+
let best = new Float64Array(W * H * 4).fill(Infinity);
|
|
271
|
+
let prev = new Int32Array(W * H * 4).fill(-1);
|
|
272
|
+
let heap = [];
|
|
273
|
+
best[start * 4 + 2] = 0;
|
|
274
|
+
heap_push(heap, [0, start * 4 + 2]);
|
|
275
|
+
|
|
276
|
+
let moves = (k) => {
|
|
277
|
+
let i = k % W;
|
|
278
|
+
let j = (k - i) / W;
|
|
279
|
+
let out = [];
|
|
280
|
+
if(step_right[k]) {
|
|
281
|
+
out.push([k + 1, 0, xs[i + 1] - xs[i]]);
|
|
282
|
+
}
|
|
283
|
+
if(i > 0 && step_right[k - 1]) {
|
|
284
|
+
out.push([k - 1, 1, xs[i] - xs[i - 1]]);
|
|
285
|
+
}
|
|
286
|
+
if(step_down[k]) {
|
|
287
|
+
out.push([k + W, 2, ys[j + 1] - ys[j]]);
|
|
288
|
+
}
|
|
289
|
+
if(j > 0 && step_down[k - W]) {
|
|
290
|
+
out.push([k - W, 3, ys[j] - ys[j - 1]]);
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
let found = -1;
|
|
296
|
+
let found_cost = Infinity;
|
|
297
|
+
while(heap.length) {
|
|
298
|
+
let [cost, state] = heap_pop(heap);
|
|
299
|
+
if(cost > best[state] || cost >= found_cost) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
let k = state >> 2;
|
|
303
|
+
let dir = state & 3;
|
|
304
|
+
if(k === goal) {
|
|
305
|
+
let total = cost + ((dir === 2)? 0 : ROUTE_BEND);
|
|
306
|
+
if(total < found_cost) {
|
|
307
|
+
found_cost = total;
|
|
308
|
+
found = state;
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
for(let [nk, nd, len] of moves(k)) {
|
|
313
|
+
let c = cost + len + ((nd === dir)? 0 : ROUTE_BEND);
|
|
314
|
+
let ns = nk * 4 + nd;
|
|
315
|
+
if(c < best[ns]) {
|
|
316
|
+
best[ns] = c;
|
|
317
|
+
prev[ns] = state;
|
|
318
|
+
heap_push(heap, [c, ns]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if(found < 0) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/* Back from the goal, keeping only the points where it turns. */
|
|
327
|
+
let states = [];
|
|
328
|
+
for(let s = found; s >= 0; s = prev[s]) {
|
|
329
|
+
states.push(s);
|
|
330
|
+
}
|
|
331
|
+
states.reverse();
|
|
332
|
+
let pts = [a];
|
|
333
|
+
for(let n = 1; n < states.length; n++) {
|
|
334
|
+
let turn = (n + 1 < states.length) && ((states[n + 1] & 3) !== (states[n] & 3));
|
|
335
|
+
if(turn) {
|
|
336
|
+
let k = states[n] >> 2;
|
|
337
|
+
pts.push([xs[k % W], ys[Math.floor(k / W)]]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
pts.push(z);
|
|
341
|
+
return pts;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/************************************************************
|
|
345
|
+
* The control points of an elbow that does not cross any card.
|
|
346
|
+
*
|
|
347
|
+
* The simple elbow first (elbow_points): the channel, or the
|
|
348
|
+
* detour round the two cards. Only when that line crosses a card
|
|
349
|
+
* -- a third record standing in a detour's way, a row in the way
|
|
350
|
+
* of an edge that drops several rows -- is it ROUTED: out of the
|
|
351
|
+
* port, the shortest way along the gaps between the cards, in.
|
|
352
|
+
* A parent and the row of children under it keep their bus.
|
|
353
|
+
*
|
|
354
|
+
* `boxes` are all the nodes on screen, the two of the edge
|
|
355
|
+
* included, each as the box of the WHOLE node -- card, ports and
|
|
356
|
+
* label -- so a line clears the ports too. `box_s` and `box_t`
|
|
357
|
+
* are the edge's own two, measured the same way. The route keeps
|
|
358
|
+
* a clearance from every box, which
|
|
359
|
+
* grows with the lane so that two routed edges of one pair do not
|
|
360
|
+
* coincide. Nearby cards first; all of them if that finds no way;
|
|
361
|
+
* the simple elbow if nothing does.
|
|
362
|
+
************************************************************/
|
|
363
|
+
export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes)
|
|
364
|
+
{
|
|
365
|
+
let others = boxes || [];
|
|
366
|
+
if(!vertical) {
|
|
367
|
+
return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
|
|
368
|
+
others.map(swap_box)).map(swap);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
372
|
+
if(!elbow_path_hits([s, ...simple, t], others, box_s, box_t)) {
|
|
373
|
+
return simple;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let m = ELBOW_CLEAR / 2 + Math.abs(lane) * ELBOW_STEP;
|
|
377
|
+
/* Just outside the grown box of each end, which holds its ports. */
|
|
378
|
+
let a = [s[0], Math.max(s[1], box_s.y2) + m + 1]; /* out of a bottom port */
|
|
379
|
+
let z = [t[0], Math.min(t[1], box_t.y1) - m - 1]; /* into a top port */
|
|
380
|
+
let region = box_expand({
|
|
381
|
+
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
382
|
+
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
|
383
|
+
x2: Math.max(a[0], z[0], box_s.x2, box_t.x2),
|
|
384
|
+
y2: Math.max(a[1], z[1], box_s.y2, box_t.y2),
|
|
385
|
+
}, ROUTE_REACH);
|
|
386
|
+
let near = others.filter((b) => box_overlaps(b, region));
|
|
387
|
+
|
|
388
|
+
let route = orth_search(a, z, near.map((b) => box_expand(b, m)));
|
|
389
|
+
if(!route && near.length < others.length) {
|
|
390
|
+
route = orth_search(a, z, others.map((b) => box_expand(b, m)));
|
|
391
|
+
}
|
|
392
|
+
return route || simple;
|
|
393
|
+
}
|
package/src/treedb_elbow.test.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* forward edge, the detour of one that is not.
|
|
7
7
|
***********************************************************************/
|
|
8
8
|
import { describe, test, expect } from "vitest";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
elbow_lane, elbow_points, elbow_route, elbow_path_hits, ELBOW_STEP, ELBOW_CLEAR,
|
|
11
|
+
} from "./treedb_elbow.js";
|
|
10
12
|
|
|
11
13
|
/* Two cards 100x40: A on top, B one row below it. */
|
|
12
14
|
const A = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
@@ -88,6 +90,118 @@ describe("an edge that does not run forward", () => {
|
|
|
88
90
|
});
|
|
89
91
|
});
|
|
90
92
|
|
|
93
|
+
describe("the route round the other cards", () => {
|
|
94
|
+
test("a line that crosses no card is left as it is", () => {
|
|
95
|
+
let others = [A, B, {x1: 400, y1: 0, x2: 500, y2: 40}];
|
|
96
|
+
expect(elbow_route(A_HOOK, B_FKEY, A, B, 0, true, others))
|
|
97
|
+
.toEqual(elbow_points(A_HOOK, B_FKEY, A, B, 0, true));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a detour with a third card in its way goes round that one too", () => {
|
|
101
|
+
/* D stands right where the detour of B -> A runs, beside both. */
|
|
102
|
+
let D = {x1: 110, y1: -20, x2: 210, y2: 200};
|
|
103
|
+
let boxes = [A, B, D];
|
|
104
|
+
let simple = elbow_points(B_HOOK, A_FKEY, B, A, 0, true);
|
|
105
|
+
expect(elbow_path_hits([B_HOOK, ...simple, A_FKEY], boxes)).toBe(true);
|
|
106
|
+
let route = elbow_route(B_HOOK, A_FKEY, B, A, 0, true, boxes);
|
|
107
|
+
expect(elbow_path_hits([B_HOOK, ...route, A_FKEY], boxes)).toBe(false);
|
|
108
|
+
expect(route).not.toEqual(simple);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("an edge dropping several rows goes round the row in between", () => {
|
|
112
|
+
/* P on top, M in the row between, C two rows down. */
|
|
113
|
+
let P = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
114
|
+
let M = {x1: -40, y1: 130, x2: 140, y2: 170};
|
|
115
|
+
let C = {x1: 0, y1: 260, x2: 100, y2: 300};
|
|
116
|
+
let boxes = [P, M, C];
|
|
117
|
+
let route = elbow_route([50, 40], [50, 260], P, C, 0, true, boxes);
|
|
118
|
+
expect(elbow_path_hits([[50, 40], ...route, [50, 260]], boxes)).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("when every way is closed it keeps the simple elbow", () => {
|
|
122
|
+
let wall = {x1: -1000, y1: 60, x2: 1000, y2: 100};
|
|
123
|
+
let route = elbow_route(A_HOOK, B_FKEY, A, B, 0, true, [A, B, wall]);
|
|
124
|
+
expect(route).toEqual(elbow_points(A_HOOK, B_FKEY, A, B, 0, true));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("two routed edges of one pair keep apart", () => {
|
|
128
|
+
let D = {x1: 110, y1: -20, x2: 210, y2: 200};
|
|
129
|
+
let boxes = [A, B, D];
|
|
130
|
+
let r1 = elbow_route(B_HOOK, A_FKEY, B, A, 1, true, boxes);
|
|
131
|
+
let r2 = elbow_route(B_HOOK, A_FKEY, B, A, -1, true, boxes);
|
|
132
|
+
expect(elbow_path_hits([B_HOOK, ...r1, A_FKEY], boxes)).toBe(false);
|
|
133
|
+
expect(elbow_path_hits([B_HOOK, ...r2, A_FKEY], boxes)).toBe(false);
|
|
134
|
+
expect(r1).not.toEqual(r2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("read left to right, the route is the transpose", () => {
|
|
138
|
+
let sw = (p) => [p[1], p[0]];
|
|
139
|
+
let swb = (b) => ({x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2});
|
|
140
|
+
let D = {x1: 110, y1: -20, x2: 210, y2: 200};
|
|
141
|
+
let down = elbow_route(B_HOOK, A_FKEY, B, A, 0, true, [A, B, D]);
|
|
142
|
+
let right = elbow_route(sw(B_HOOK), sw(A_FKEY), swb(B), swb(A), 0, false,
|
|
143
|
+
[A, B, D].map(swb));
|
|
144
|
+
expect(right).toEqual(down.map(sw));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("a screen of eighty cards is routed in a few milliseconds", () => {
|
|
148
|
+
let boxes = [];
|
|
149
|
+
for(let r = 0; r < 4; r++) {
|
|
150
|
+
for(let c = 0; c < 20; c++) {
|
|
151
|
+
boxes.push({x1: c * 190, y1: r * 186, x2: c * 190 + 172, y2: r * 186 + 96});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/* From a card of the bottom row back up to one of the top row,
|
|
155
|
+
* across the whole screen. */
|
|
156
|
+
let src = boxes[3 * 20 + 18];
|
|
157
|
+
let dst = boxes[2];
|
|
158
|
+
let s = [(src.x1 + src.x2) / 2, src.y2];
|
|
159
|
+
let t = [(dst.x1 + dst.x2) / 2, dst.y1];
|
|
160
|
+
let t0 = performance.now();
|
|
161
|
+
let route = elbow_route(s, t, src, dst, 0, true, boxes);
|
|
162
|
+
let ms = performance.now() - t0;
|
|
163
|
+
expect(elbow_path_hits([s, ...route, t], boxes)).toBe(false);
|
|
164
|
+
expect(ms).toBeLessThan(200);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("the ports are obstacles too", () => {
|
|
169
|
+
/* Whole-node boxes: a port of 14px sticks 7px out of the bottom
|
|
170
|
+
* and the top of each card. */
|
|
171
|
+
const AP = {x1: 0, y1: -7, x2: 100, y2: 47};
|
|
172
|
+
const BP = {x1: 350, y1: 123, x2: 450, y2: 177};
|
|
173
|
+
const S = [50, 40];
|
|
174
|
+
const T2 = [400, 130];
|
|
175
|
+
|
|
176
|
+
test("an edge's own ports do not make its line a hit", () => {
|
|
177
|
+
let simple = elbow_points(S, T2, AP, BP, 0, true);
|
|
178
|
+
expect(elbow_path_hits([S, ...simple, T2], [AP, BP], AP, BP)).toBe(false);
|
|
179
|
+
expect(elbow_route(S, T2, AP, BP, 0, true, [AP, BP])).toEqual(simple);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("a port of a third card in the channel is gone round", () => {
|
|
183
|
+
/* F's card ends at y 80, above the channel (85); its port
|
|
184
|
+
* reaches down to 95, into it. */
|
|
185
|
+
const F_CARD = {x1: 200, y1: 0, x2: 300, y2: 80};
|
|
186
|
+
const F = {x1: 200, y1: -7, x2: 300, y2: 95};
|
|
187
|
+
let simple = elbow_points(S, T2, AP, BP, 0, true);
|
|
188
|
+
expect(elbow_path_hits([S, ...simple, T2], [AP, BP, F_CARD], AP, BP)).toBe(false);
|
|
189
|
+
expect(elbow_path_hits([S, ...simple, T2], [AP, BP, F], AP, BP)).toBe(true);
|
|
190
|
+
let route = elbow_route(S, T2, AP, BP, 0, true, [AP, BP, F]);
|
|
191
|
+
expect(elbow_path_hits([S, ...route, T2], [AP, BP, F], AP, BP)).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("a detour clears the ports of its own two cards", () => {
|
|
195
|
+
/* B below A, both with their ports: B -> A goes round. */
|
|
196
|
+
const AB = {x1: 0, y1: -7, x2: 100, y2: 47};
|
|
197
|
+
const BB = {x1: 0, y1: 123, x2: 100, y2: 177};
|
|
198
|
+
let pts = elbow_points([60, 170], [50, 0], BB, AB, 0, true);
|
|
199
|
+
expect(pts[0][1]).toBeGreaterThan(BB.y2);
|
|
200
|
+
expect(pts[3][1]).toBeLessThan(AB.y1);
|
|
201
|
+
expect(elbow_path_hits([[60, 170], ...pts, [50, 0]], [AB, BB], BB, AB)).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
91
205
|
describe("read left to right", () => {
|
|
92
206
|
test("is the same geometry with the axes swapped", () => {
|
|
93
207
|
let sw = (p) => [p[1], p[0]];
|