@yuneta/gobj-ui 7.23.150 → 7.23.151
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 +15 -2
- package/dist/gobj-ui.cjs.js +221 -4
- package/dist/gobj-ui.es.js +221 -4
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +35 -7
- package/src/treedb_elbow.js +268 -0
- package/src/treedb_elbow.test.js +78 -1
package/README.md
CHANGED
|
@@ -1446,8 +1446,21 @@ 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. Ports are not obstacles,
|
|
1461
|
+
only cards. G6's `shortest-path` router is not used: it is not exported, it
|
|
1462
|
+
falls back in silence to a route that crosses cards, and it rewrites its
|
|
1463
|
+
module defaults with every config it is given.
|
|
1451
1464
|
|
|
1452
1465
|
All three take the **same spanning tree**, chosen deterministically: roots are the
|
|
1453
1466
|
nodes with no incoming edge, in node order; a node belongs to the **first
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -29678,6 +29678,206 @@ 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
|
+
/************************************************************
|
|
29703
|
+
* Does the polyline `points` (its ends included) cross any of
|
|
29704
|
+
* `boxes`? A card is a box shrunk by a pixel, so a line along
|
|
29705
|
+
* its border or leaving from a port on it is not a hit.
|
|
29706
|
+
************************************************************/
|
|
29707
|
+
function elbow_path_hits(points, boxes) {
|
|
29708
|
+
for (let b of boxes) {
|
|
29709
|
+
let inner = box_expand(b, -1);
|
|
29710
|
+
for (let i = 0; i + 1 < points.length; i++) if (segment_hits(points[i], points[i + 1], inner)) return true;
|
|
29711
|
+
}
|
|
29712
|
+
return false;
|
|
29713
|
+
}
|
|
29714
|
+
function heap_push(h, item) {
|
|
29715
|
+
h.push(item);
|
|
29716
|
+
let i = h.length - 1;
|
|
29717
|
+
while (i > 0) {
|
|
29718
|
+
let p = i - 1 >> 1;
|
|
29719
|
+
if (h[p][0] <= h[i][0]) break;
|
|
29720
|
+
[h[p], h[i]] = [h[i], h[p]];
|
|
29721
|
+
i = p;
|
|
29722
|
+
}
|
|
29723
|
+
}
|
|
29724
|
+
function heap_pop(h) {
|
|
29725
|
+
let top = h[0];
|
|
29726
|
+
let last = h.pop();
|
|
29727
|
+
if (h.length) {
|
|
29728
|
+
h[0] = last;
|
|
29729
|
+
let i = 0;
|
|
29730
|
+
for (;;) {
|
|
29731
|
+
let l = 2 * i + 1;
|
|
29732
|
+
let r = l + 1;
|
|
29733
|
+
let m = i;
|
|
29734
|
+
if (l < h.length && h[l][0] < h[m][0]) m = l;
|
|
29735
|
+
if (r < h.length && h[r][0] < h[m][0]) m = r;
|
|
29736
|
+
if (m === i) break;
|
|
29737
|
+
[h[m], h[i]] = [h[i], h[m]];
|
|
29738
|
+
i = m;
|
|
29739
|
+
}
|
|
29740
|
+
}
|
|
29741
|
+
return top;
|
|
29742
|
+
}
|
|
29743
|
+
/************************************************************
|
|
29744
|
+
* The shortest orthogonal line from `a` to `z` that enters none
|
|
29745
|
+
* of `boxes` (already expanded by the clearance), a turn costing
|
|
29746
|
+
* ROUTE_BEND. The line leaves `a` going down and ends at `z`
|
|
29747
|
+
* going down, since after `z` it drops into a port.
|
|
29748
|
+
*
|
|
29749
|
+
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
29750
|
+
* of the two ends, which is all an orthogonal shortest path ever
|
|
29751
|
+
* needs to turn on. Returns [a, ...turns, z], or null when every
|
|
29752
|
+
* way is closed.
|
|
29753
|
+
************************************************************/
|
|
29754
|
+
function orth_search(a, z, boxes) {
|
|
29755
|
+
for (let b of boxes) if (point_inside(a, b) || point_inside(z, b)) return null;
|
|
29756
|
+
let uniq = (list) => [...new Set(list)].sort((p, q) => p - q);
|
|
29757
|
+
let xs = uniq([a[0], z[0]].concat(...boxes.map((b) => [b.x1, b.x2])));
|
|
29758
|
+
let ys = uniq([a[1], z[1]].concat(...boxes.map((b) => [b.y1, b.y2])));
|
|
29759
|
+
let W = xs.length;
|
|
29760
|
+
let H = ys.length;
|
|
29761
|
+
let free = (x, y) => !boxes.some((b) => point_inside([x, y], b));
|
|
29762
|
+
let ok = new Uint8Array(W * H);
|
|
29763
|
+
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;
|
|
29764
|
+
let step_right = new Uint8Array(W * H);
|
|
29765
|
+
let step_down = new Uint8Array(W * H);
|
|
29766
|
+
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) {
|
|
29767
|
+
let k = j * W + i;
|
|
29768
|
+
if (!ok[k]) continue;
|
|
29769
|
+
if (i + 1 < W && ok[k + 1] && free((xs[i] + xs[i + 1]) / 2, ys[j])) step_right[k] = 1;
|
|
29770
|
+
if (j + 1 < H && ok[k + W] && free(xs[i], (ys[j] + ys[j + 1]) / 2)) step_down[k] = 1;
|
|
29771
|
+
}
|
|
29772
|
+
let start = ys.indexOf(a[1]) * W + xs.indexOf(a[0]);
|
|
29773
|
+
let goal = ys.indexOf(z[1]) * W + xs.indexOf(z[0]);
|
|
29774
|
+
let best = new Float64Array(W * H * 4).fill(Infinity);
|
|
29775
|
+
let prev = new Int32Array(W * H * 4).fill(-1);
|
|
29776
|
+
let heap = [];
|
|
29777
|
+
best[start * 4 + 2] = 0;
|
|
29778
|
+
heap_push(heap, [0, start * 4 + 2]);
|
|
29779
|
+
let moves = (k) => {
|
|
29780
|
+
let i = k % W;
|
|
29781
|
+
let j = (k - i) / W;
|
|
29782
|
+
let out = [];
|
|
29783
|
+
if (step_right[k]) out.push([
|
|
29784
|
+
k + 1,
|
|
29785
|
+
0,
|
|
29786
|
+
xs[i + 1] - xs[i]
|
|
29787
|
+
]);
|
|
29788
|
+
if (i > 0 && step_right[k - 1]) out.push([
|
|
29789
|
+
k - 1,
|
|
29790
|
+
1,
|
|
29791
|
+
xs[i] - xs[i - 1]
|
|
29792
|
+
]);
|
|
29793
|
+
if (step_down[k]) out.push([
|
|
29794
|
+
k + W,
|
|
29795
|
+
2,
|
|
29796
|
+
ys[j + 1] - ys[j]
|
|
29797
|
+
]);
|
|
29798
|
+
if (j > 0 && step_down[k - W]) out.push([
|
|
29799
|
+
k - W,
|
|
29800
|
+
3,
|
|
29801
|
+
ys[j] - ys[j - 1]
|
|
29802
|
+
]);
|
|
29803
|
+
return out;
|
|
29804
|
+
};
|
|
29805
|
+
let found = -1;
|
|
29806
|
+
let found_cost = Infinity;
|
|
29807
|
+
while (heap.length) {
|
|
29808
|
+
let [cost, state] = heap_pop(heap);
|
|
29809
|
+
if (cost > best[state] || cost >= found_cost) continue;
|
|
29810
|
+
let k = state >> 2;
|
|
29811
|
+
let dir = state & 3;
|
|
29812
|
+
if (k === goal) {
|
|
29813
|
+
let total = cost + (dir === 2 ? 0 : 40);
|
|
29814
|
+
if (total < found_cost) {
|
|
29815
|
+
found_cost = total;
|
|
29816
|
+
found = state;
|
|
29817
|
+
}
|
|
29818
|
+
continue;
|
|
29819
|
+
}
|
|
29820
|
+
for (let [nk, nd, len] of moves(k)) {
|
|
29821
|
+
let c = cost + len + (nd === dir ? 0 : 40);
|
|
29822
|
+
let ns = nk * 4 + nd;
|
|
29823
|
+
if (c < best[ns]) {
|
|
29824
|
+
best[ns] = c;
|
|
29825
|
+
prev[ns] = state;
|
|
29826
|
+
heap_push(heap, [c, ns]);
|
|
29827
|
+
}
|
|
29828
|
+
}
|
|
29829
|
+
}
|
|
29830
|
+
if (found < 0) return null;
|
|
29831
|
+
let states = [];
|
|
29832
|
+
for (let s = found; s >= 0; s = prev[s]) states.push(s);
|
|
29833
|
+
states.reverse();
|
|
29834
|
+
let pts = [a];
|
|
29835
|
+
for (let n = 1; n < states.length; n++) if (n + 1 < states.length && (states[n + 1] & 3) !== (states[n] & 3)) {
|
|
29836
|
+
let k = states[n] >> 2;
|
|
29837
|
+
pts.push([xs[k % W], ys[Math.floor(k / W)]]);
|
|
29838
|
+
}
|
|
29839
|
+
pts.push(z);
|
|
29840
|
+
return pts;
|
|
29841
|
+
}
|
|
29842
|
+
/************************************************************
|
|
29843
|
+
* The control points of an elbow that does not cross any card.
|
|
29844
|
+
*
|
|
29845
|
+
* The simple elbow first (elbow_points): the channel, or the
|
|
29846
|
+
* detour round the two cards. Only when that line crosses a card
|
|
29847
|
+
* -- a third record standing in a detour's way, a row in the way
|
|
29848
|
+
* of an edge that drops several rows -- is it ROUTED: out of the
|
|
29849
|
+
* port, the shortest way along the gaps between the cards, in.
|
|
29850
|
+
* A parent and the row of children under it keep their bus.
|
|
29851
|
+
*
|
|
29852
|
+
* `boxes` are all the cards on screen, the two of the edge
|
|
29853
|
+
* included. The route keeps a clearance from every card, which
|
|
29854
|
+
* grows with the lane so that two routed edges of one pair do not
|
|
29855
|
+
* coincide. Nearby cards first; all of them if that finds no way;
|
|
29856
|
+
* the simple elbow if nothing does.
|
|
29857
|
+
************************************************************/
|
|
29858
|
+
function elbow_route(s, t, box_s, box_t, lane, vertical, boxes) {
|
|
29859
|
+
let others = boxes || [];
|
|
29860
|
+
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);
|
|
29861
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
29862
|
+
if (!elbow_path_hits([
|
|
29863
|
+
s,
|
|
29864
|
+
...simple,
|
|
29865
|
+
t
|
|
29866
|
+
], others)) return simple;
|
|
29867
|
+
let m = 12 + Math.abs(lane) * 10;
|
|
29868
|
+
let a = [s[0], s[1] + m + 4];
|
|
29869
|
+
let z = [t[0], t[1] - m - 4];
|
|
29870
|
+
let region = box_expand({
|
|
29871
|
+
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
29872
|
+
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
|
29873
|
+
x2: Math.max(a[0], z[0], box_s.x2, box_t.x2),
|
|
29874
|
+
y2: Math.max(a[1], z[1], box_s.y2, box_t.y2)
|
|
29875
|
+
}, 400);
|
|
29876
|
+
let near = others.filter((b) => box_overlaps(b, region));
|
|
29877
|
+
let route = orth_search(a, z, near.map((b) => box_expand(b, m)));
|
|
29878
|
+
if (!route && near.length < others.length) route = orth_search(a, z, others.map((b) => box_expand(b, m)));
|
|
29879
|
+
return route || simple;
|
|
29880
|
+
}
|
|
29681
29881
|
//#endregion
|
|
29682
29882
|
//#region node_modules/@babel/runtime/helpers/esm/typeof.js
|
|
29683
29883
|
function _typeof(o) {
|
|
@@ -59058,8 +59258,9 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
|
|
|
59058
59258
|
* Computed on every draw from where the two ends ARE, so a
|
|
59059
59259
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59060
59260
|
* between rows (top to bottom), `-h` between columns. An edge that
|
|
59061
|
-
* does not run forward goes round the two cards,
|
|
59062
|
-
*
|
|
59261
|
+
* does not run forward goes round the two cards, the edges joining
|
|
59262
|
+
* the same two cards take lanes, and a line that would cross any
|
|
59263
|
+
* other card is routed round it (treedb_elbow.js).
|
|
59063
59264
|
************************************************************/
|
|
59064
59265
|
var ELBOW_RADIUS = 6;
|
|
59065
59266
|
function elbow_box(node) {
|
|
@@ -59082,6 +59283,22 @@ function elbow_box(node) {
|
|
|
59082
59283
|
};
|
|
59083
59284
|
}
|
|
59084
59285
|
}
|
|
59286
|
+
var _elbow_boxes = null;
|
|
59287
|
+
function elbow_obstacles(edge) {
|
|
59288
|
+
if (_elbow_boxes) return _elbow_boxes;
|
|
59289
|
+
let element = edge.context && edge.context.element;
|
|
59290
|
+
if (!element) return [];
|
|
59291
|
+
try {
|
|
59292
|
+
_elbow_boxes = element.getNodes().map((n) => elbow_box(n));
|
|
59293
|
+
} catch (e) {
|
|
59294
|
+
(0, _yuneta_gobj_js.log_error)(`elbow edge ${edge.id}: cannot read the cards: ${e}`);
|
|
59295
|
+
_elbow_boxes = [];
|
|
59296
|
+
}
|
|
59297
|
+
queueMicrotask(() => {
|
|
59298
|
+
_elbow_boxes = null;
|
|
59299
|
+
});
|
|
59300
|
+
return _elbow_boxes;
|
|
59301
|
+
}
|
|
59085
59302
|
function elbow_lane_of(edge) {
|
|
59086
59303
|
let graph = edge.context && edge.context.graph;
|
|
59087
59304
|
let s = edge.sourceNode && edge.sourceNode.id;
|
|
@@ -59097,7 +59314,7 @@ function elbow_lane_of(edge) {
|
|
|
59097
59314
|
var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
59098
59315
|
getControlPoints(attributes) {
|
|
59099
59316
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59100
|
-
return
|
|
59317
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
|
|
59101
59318
|
}
|
|
59102
59319
|
getKeyPath(attributes) {
|
|
59103
59320
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59106,7 +59323,7 @@ var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
|
59106
59323
|
var TreedbElbowH = class extends _antv_g6.Polyline {
|
|
59107
59324
|
getControlPoints(attributes) {
|
|
59108
59325
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59109
|
-
return
|
|
59326
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
|
|
59110
59327
|
}
|
|
59111
59328
|
getKeyPath(attributes) {
|
|
59112
59329
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -29673,6 +29673,206 @@ 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
|
+
/************************************************************
|
|
29698
|
+
* Does the polyline `points` (its ends included) cross any of
|
|
29699
|
+
* `boxes`? A card is a box shrunk by a pixel, so a line along
|
|
29700
|
+
* its border or leaving from a port on it is not a hit.
|
|
29701
|
+
************************************************************/
|
|
29702
|
+
function elbow_path_hits(points, boxes) {
|
|
29703
|
+
for (let b of boxes) {
|
|
29704
|
+
let inner = box_expand(b, -1);
|
|
29705
|
+
for (let i = 0; i + 1 < points.length; i++) if (segment_hits(points[i], points[i + 1], inner)) return true;
|
|
29706
|
+
}
|
|
29707
|
+
return false;
|
|
29708
|
+
}
|
|
29709
|
+
function heap_push(h, item) {
|
|
29710
|
+
h.push(item);
|
|
29711
|
+
let i = h.length - 1;
|
|
29712
|
+
while (i > 0) {
|
|
29713
|
+
let p = i - 1 >> 1;
|
|
29714
|
+
if (h[p][0] <= h[i][0]) break;
|
|
29715
|
+
[h[p], h[i]] = [h[i], h[p]];
|
|
29716
|
+
i = p;
|
|
29717
|
+
}
|
|
29718
|
+
}
|
|
29719
|
+
function heap_pop(h) {
|
|
29720
|
+
let top = h[0];
|
|
29721
|
+
let last = h.pop();
|
|
29722
|
+
if (h.length) {
|
|
29723
|
+
h[0] = last;
|
|
29724
|
+
let i = 0;
|
|
29725
|
+
for (;;) {
|
|
29726
|
+
let l = 2 * i + 1;
|
|
29727
|
+
let r = l + 1;
|
|
29728
|
+
let m = i;
|
|
29729
|
+
if (l < h.length && h[l][0] < h[m][0]) m = l;
|
|
29730
|
+
if (r < h.length && h[r][0] < h[m][0]) m = r;
|
|
29731
|
+
if (m === i) break;
|
|
29732
|
+
[h[m], h[i]] = [h[i], h[m]];
|
|
29733
|
+
i = m;
|
|
29734
|
+
}
|
|
29735
|
+
}
|
|
29736
|
+
return top;
|
|
29737
|
+
}
|
|
29738
|
+
/************************************************************
|
|
29739
|
+
* The shortest orthogonal line from `a` to `z` that enters none
|
|
29740
|
+
* of `boxes` (already expanded by the clearance), a turn costing
|
|
29741
|
+
* ROUTE_BEND. The line leaves `a` going down and ends at `z`
|
|
29742
|
+
* going down, since after `z` it drops into a port.
|
|
29743
|
+
*
|
|
29744
|
+
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
29745
|
+
* of the two ends, which is all an orthogonal shortest path ever
|
|
29746
|
+
* needs to turn on. Returns [a, ...turns, z], or null when every
|
|
29747
|
+
* way is closed.
|
|
29748
|
+
************************************************************/
|
|
29749
|
+
function orth_search(a, z, boxes) {
|
|
29750
|
+
for (let b of boxes) if (point_inside(a, b) || point_inside(z, b)) return null;
|
|
29751
|
+
let uniq = (list) => [...new Set(list)].sort((p, q) => p - q);
|
|
29752
|
+
let xs = uniq([a[0], z[0]].concat(...boxes.map((b) => [b.x1, b.x2])));
|
|
29753
|
+
let ys = uniq([a[1], z[1]].concat(...boxes.map((b) => [b.y1, b.y2])));
|
|
29754
|
+
let W = xs.length;
|
|
29755
|
+
let H = ys.length;
|
|
29756
|
+
let free = (x, y) => !boxes.some((b) => point_inside([x, y], b));
|
|
29757
|
+
let ok = new Uint8Array(W * H);
|
|
29758
|
+
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;
|
|
29759
|
+
let step_right = new Uint8Array(W * H);
|
|
29760
|
+
let step_down = new Uint8Array(W * H);
|
|
29761
|
+
for (let j = 0; j < H; j++) for (let i = 0; i < W; i++) {
|
|
29762
|
+
let k = j * W + i;
|
|
29763
|
+
if (!ok[k]) continue;
|
|
29764
|
+
if (i + 1 < W && ok[k + 1] && free((xs[i] + xs[i + 1]) / 2, ys[j])) step_right[k] = 1;
|
|
29765
|
+
if (j + 1 < H && ok[k + W] && free(xs[i], (ys[j] + ys[j + 1]) / 2)) step_down[k] = 1;
|
|
29766
|
+
}
|
|
29767
|
+
let start = ys.indexOf(a[1]) * W + xs.indexOf(a[0]);
|
|
29768
|
+
let goal = ys.indexOf(z[1]) * W + xs.indexOf(z[0]);
|
|
29769
|
+
let best = new Float64Array(W * H * 4).fill(Infinity);
|
|
29770
|
+
let prev = new Int32Array(W * H * 4).fill(-1);
|
|
29771
|
+
let heap = [];
|
|
29772
|
+
best[start * 4 + 2] = 0;
|
|
29773
|
+
heap_push(heap, [0, start * 4 + 2]);
|
|
29774
|
+
let moves = (k) => {
|
|
29775
|
+
let i = k % W;
|
|
29776
|
+
let j = (k - i) / W;
|
|
29777
|
+
let out = [];
|
|
29778
|
+
if (step_right[k]) out.push([
|
|
29779
|
+
k + 1,
|
|
29780
|
+
0,
|
|
29781
|
+
xs[i + 1] - xs[i]
|
|
29782
|
+
]);
|
|
29783
|
+
if (i > 0 && step_right[k - 1]) out.push([
|
|
29784
|
+
k - 1,
|
|
29785
|
+
1,
|
|
29786
|
+
xs[i] - xs[i - 1]
|
|
29787
|
+
]);
|
|
29788
|
+
if (step_down[k]) out.push([
|
|
29789
|
+
k + W,
|
|
29790
|
+
2,
|
|
29791
|
+
ys[j + 1] - ys[j]
|
|
29792
|
+
]);
|
|
29793
|
+
if (j > 0 && step_down[k - W]) out.push([
|
|
29794
|
+
k - W,
|
|
29795
|
+
3,
|
|
29796
|
+
ys[j] - ys[j - 1]
|
|
29797
|
+
]);
|
|
29798
|
+
return out;
|
|
29799
|
+
};
|
|
29800
|
+
let found = -1;
|
|
29801
|
+
let found_cost = Infinity;
|
|
29802
|
+
while (heap.length) {
|
|
29803
|
+
let [cost, state] = heap_pop(heap);
|
|
29804
|
+
if (cost > best[state] || cost >= found_cost) continue;
|
|
29805
|
+
let k = state >> 2;
|
|
29806
|
+
let dir = state & 3;
|
|
29807
|
+
if (k === goal) {
|
|
29808
|
+
let total = cost + (dir === 2 ? 0 : 40);
|
|
29809
|
+
if (total < found_cost) {
|
|
29810
|
+
found_cost = total;
|
|
29811
|
+
found = state;
|
|
29812
|
+
}
|
|
29813
|
+
continue;
|
|
29814
|
+
}
|
|
29815
|
+
for (let [nk, nd, len] of moves(k)) {
|
|
29816
|
+
let c = cost + len + (nd === dir ? 0 : 40);
|
|
29817
|
+
let ns = nk * 4 + nd;
|
|
29818
|
+
if (c < best[ns]) {
|
|
29819
|
+
best[ns] = c;
|
|
29820
|
+
prev[ns] = state;
|
|
29821
|
+
heap_push(heap, [c, ns]);
|
|
29822
|
+
}
|
|
29823
|
+
}
|
|
29824
|
+
}
|
|
29825
|
+
if (found < 0) return null;
|
|
29826
|
+
let states = [];
|
|
29827
|
+
for (let s = found; s >= 0; s = prev[s]) states.push(s);
|
|
29828
|
+
states.reverse();
|
|
29829
|
+
let pts = [a];
|
|
29830
|
+
for (let n = 1; n < states.length; n++) if (n + 1 < states.length && (states[n + 1] & 3) !== (states[n] & 3)) {
|
|
29831
|
+
let k = states[n] >> 2;
|
|
29832
|
+
pts.push([xs[k % W], ys[Math.floor(k / W)]]);
|
|
29833
|
+
}
|
|
29834
|
+
pts.push(z);
|
|
29835
|
+
return pts;
|
|
29836
|
+
}
|
|
29837
|
+
/************************************************************
|
|
29838
|
+
* The control points of an elbow that does not cross any card.
|
|
29839
|
+
*
|
|
29840
|
+
* The simple elbow first (elbow_points): the channel, or the
|
|
29841
|
+
* detour round the two cards. Only when that line crosses a card
|
|
29842
|
+
* -- a third record standing in a detour's way, a row in the way
|
|
29843
|
+
* of an edge that drops several rows -- is it ROUTED: out of the
|
|
29844
|
+
* port, the shortest way along the gaps between the cards, in.
|
|
29845
|
+
* A parent and the row of children under it keep their bus.
|
|
29846
|
+
*
|
|
29847
|
+
* `boxes` are all the cards on screen, the two of the edge
|
|
29848
|
+
* included. The route keeps a clearance from every card, which
|
|
29849
|
+
* grows with the lane so that two routed edges of one pair do not
|
|
29850
|
+
* coincide. Nearby cards first; all of them if that finds no way;
|
|
29851
|
+
* the simple elbow if nothing does.
|
|
29852
|
+
************************************************************/
|
|
29853
|
+
function elbow_route(s, t, box_s, box_t, lane, vertical, boxes) {
|
|
29854
|
+
let others = boxes || [];
|
|
29855
|
+
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);
|
|
29856
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
29857
|
+
if (!elbow_path_hits([
|
|
29858
|
+
s,
|
|
29859
|
+
...simple,
|
|
29860
|
+
t
|
|
29861
|
+
], others)) return simple;
|
|
29862
|
+
let m = 12 + Math.abs(lane) * 10;
|
|
29863
|
+
let a = [s[0], s[1] + m + 4];
|
|
29864
|
+
let z = [t[0], t[1] - m - 4];
|
|
29865
|
+
let region = box_expand({
|
|
29866
|
+
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
29867
|
+
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
|
29868
|
+
x2: Math.max(a[0], z[0], box_s.x2, box_t.x2),
|
|
29869
|
+
y2: Math.max(a[1], z[1], box_s.y2, box_t.y2)
|
|
29870
|
+
}, 400);
|
|
29871
|
+
let near = others.filter((b) => box_overlaps(b, region));
|
|
29872
|
+
let route = orth_search(a, z, near.map((b) => box_expand(b, m)));
|
|
29873
|
+
if (!route && near.length < others.length) route = orth_search(a, z, others.map((b) => box_expand(b, m)));
|
|
29874
|
+
return route || simple;
|
|
29875
|
+
}
|
|
29676
29876
|
//#endregion
|
|
29677
29877
|
//#region node_modules/@babel/runtime/helpers/esm/typeof.js
|
|
29678
29878
|
function _typeof(o) {
|
|
@@ -59053,8 +59253,9 @@ var TreedbTreeLayout = class extends BaseLayout {
|
|
|
59053
59253
|
* Computed on every draw from where the two ends ARE, so a
|
|
59054
59254
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59055
59255
|
* between rows (top to bottom), `-h` between columns. An edge that
|
|
59056
|
-
* does not run forward goes round the two cards,
|
|
59057
|
-
*
|
|
59256
|
+
* does not run forward goes round the two cards, the edges joining
|
|
59257
|
+
* the same two cards take lanes, and a line that would cross any
|
|
59258
|
+
* other card is routed round it (treedb_elbow.js).
|
|
59058
59259
|
************************************************************/
|
|
59059
59260
|
var ELBOW_RADIUS = 6;
|
|
59060
59261
|
function elbow_box(node) {
|
|
@@ -59077,6 +59278,22 @@ function elbow_box(node) {
|
|
|
59077
59278
|
};
|
|
59078
59279
|
}
|
|
59079
59280
|
}
|
|
59281
|
+
var _elbow_boxes = null;
|
|
59282
|
+
function elbow_obstacles(edge) {
|
|
59283
|
+
if (_elbow_boxes) return _elbow_boxes;
|
|
59284
|
+
let element = edge.context && edge.context.element;
|
|
59285
|
+
if (!element) return [];
|
|
59286
|
+
try {
|
|
59287
|
+
_elbow_boxes = element.getNodes().map((n) => elbow_box(n));
|
|
59288
|
+
} catch (e) {
|
|
59289
|
+
log_error(`elbow edge ${edge.id}: cannot read the cards: ${e}`);
|
|
59290
|
+
_elbow_boxes = [];
|
|
59291
|
+
}
|
|
59292
|
+
queueMicrotask(() => {
|
|
59293
|
+
_elbow_boxes = null;
|
|
59294
|
+
});
|
|
59295
|
+
return _elbow_boxes;
|
|
59296
|
+
}
|
|
59080
59297
|
function elbow_lane_of(edge) {
|
|
59081
59298
|
let graph = edge.context && edge.context.graph;
|
|
59082
59299
|
let s = edge.sourceNode && edge.sourceNode.id;
|
|
@@ -59092,7 +59309,7 @@ function elbow_lane_of(edge) {
|
|
|
59092
59309
|
var TreedbElbowV = class extends Polyline {
|
|
59093
59310
|
getControlPoints(attributes) {
|
|
59094
59311
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59095
|
-
return
|
|
59312
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
|
|
59096
59313
|
}
|
|
59097
59314
|
getKeyPath(attributes) {
|
|
59098
59315
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59101,7 +59318,7 @@ var TreedbElbowV = class extends Polyline {
|
|
|
59101
59318
|
var TreedbElbowH = class extends Polyline {
|
|
59102
59319
|
getControlPoints(attributes) {
|
|
59103
59320
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59104
|
-
return
|
|
59321
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
|
|
59105
59322
|
}
|
|
59106
59323
|
getKeyPath(attributes) {
|
|
59107
59324
|
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,8 +4223,9 @@ 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
|
|
|
@@ -4241,6 +4242,33 @@ function elbow_box(node)
|
|
|
4241
4242
|
}
|
|
4242
4243
|
}
|
|
4243
4244
|
|
|
4245
|
+
/* Every card on screen, as boxes: what a routed elbow keeps clear
|
|
4246
|
+
* of. Read ONCE per draw -- G6 draws all its edges in one go, so
|
|
4247
|
+
* the list lives until the next microtask and every edge of that
|
|
4248
|
+
* draw shares it. */
|
|
4249
|
+
let _elbow_boxes = null;
|
|
4250
|
+
|
|
4251
|
+
function elbow_obstacles(edge)
|
|
4252
|
+
{
|
|
4253
|
+
if(_elbow_boxes) {
|
|
4254
|
+
return _elbow_boxes;
|
|
4255
|
+
}
|
|
4256
|
+
let element = edge.context && edge.context.element;
|
|
4257
|
+
if(!element) {
|
|
4258
|
+
return [];
|
|
4259
|
+
}
|
|
4260
|
+
try {
|
|
4261
|
+
_elbow_boxes = element.getNodes().map((n) => elbow_box(n));
|
|
4262
|
+
} catch(e) {
|
|
4263
|
+
log_error(`elbow edge ${edge.id}: cannot read the cards: ${e}`);
|
|
4264
|
+
_elbow_boxes = [];
|
|
4265
|
+
}
|
|
4266
|
+
queueMicrotask(() => {
|
|
4267
|
+
_elbow_boxes = null;
|
|
4268
|
+
});
|
|
4269
|
+
return _elbow_boxes;
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4244
4272
|
/* The lane of an edge among the edges joining the same two cards,
|
|
4245
4273
|
* either way round (treedb_elbow.js). */
|
|
4246
4274
|
function elbow_lane_of(edge)
|
|
@@ -4267,8 +4295,8 @@ class TreedbElbowV extends Polyline
|
|
|
4267
4295
|
{
|
|
4268
4296
|
getControlPoints(attributes) {
|
|
4269
4297
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4270
|
-
return
|
|
4271
|
-
|
|
4298
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4299
|
+
elbow_lane_of(this), true, elbow_obstacles(this));
|
|
4272
4300
|
}
|
|
4273
4301
|
|
|
4274
4302
|
getKeyPath(attributes) {
|
|
@@ -4281,8 +4309,8 @@ class TreedbElbowH extends Polyline
|
|
|
4281
4309
|
{
|
|
4282
4310
|
getControlPoints(attributes) {
|
|
4283
4311
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4284
|
-
return
|
|
4285
|
-
|
|
4312
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4313
|
+
elbow_lane_of(this), false, elbow_obstacles(this));
|
|
4286
4314
|
}
|
|
4287
4315
|
|
|
4288
4316
|
getKeyPath(attributes) {
|
package/src/treedb_elbow.js
CHANGED
|
@@ -100,3 +100,271 @@ export function elbow_points(s, t, box_s, box_t, lane, vertical)
|
|
|
100
100
|
let y2 = t[1] - out; /* an fkey port is on the top edge */
|
|
101
101
|
return [[s[0], y1], [side, y1], [side, y2], [t[0], y2]];
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/*----------------------------------------------------------------*
|
|
105
|
+
* Round the OTHER cards too
|
|
106
|
+
*----------------------------------------------------------------*/
|
|
107
|
+
export const ROUTE_REACH = 400; /* how far around the two cards the first search looks */
|
|
108
|
+
export const ROUTE_BEND = 40; /* a turn costs as much as this many pixels of line */
|
|
109
|
+
|
|
110
|
+
function box_expand(b, m)
|
|
111
|
+
{
|
|
112
|
+
return {x1: b.x1 - m, y1: b.y1 - m, x2: b.x2 + m, y2: b.y2 + m};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function box_overlaps(a, b)
|
|
116
|
+
{
|
|
117
|
+
return a.x1 < b.x2 && a.x2 > b.x1 && a.y1 < b.y2 && a.y2 > b.y1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function point_inside(p, b)
|
|
121
|
+
{
|
|
122
|
+
return p[0] > b.x1 && p[0] < b.x2 && p[1] > b.y1 && p[1] < b.y2;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Does the axis-aligned segment a-c pass through the INSIDE of b?
|
|
126
|
+
* Running along its border does not count. */
|
|
127
|
+
function segment_hits(a, c, b)
|
|
128
|
+
{
|
|
129
|
+
let x1 = Math.min(a[0], c[0]);
|
|
130
|
+
let x2 = Math.max(a[0], c[0]);
|
|
131
|
+
let y1 = Math.min(a[1], c[1]);
|
|
132
|
+
let y2 = Math.max(a[1], c[1]);
|
|
133
|
+
return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/************************************************************
|
|
137
|
+
* Does the polyline `points` (its ends included) cross any of
|
|
138
|
+
* `boxes`? A card is a box shrunk by a pixel, so a line along
|
|
139
|
+
* its border or leaving from a port on it is not a hit.
|
|
140
|
+
************************************************************/
|
|
141
|
+
export function elbow_path_hits(points, boxes)
|
|
142
|
+
{
|
|
143
|
+
for(let b of boxes) {
|
|
144
|
+
let inner = box_expand(b, -1);
|
|
145
|
+
for(let i = 0; i + 1 < points.length; i++) {
|
|
146
|
+
if(segment_hits(points[i], points[i + 1], inner)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* A binary heap of [cost, state], smallest cost first. */
|
|
155
|
+
function heap_push(h, item)
|
|
156
|
+
{
|
|
157
|
+
h.push(item);
|
|
158
|
+
let i = h.length - 1;
|
|
159
|
+
while(i > 0) {
|
|
160
|
+
let p = (i - 1) >> 1;
|
|
161
|
+
if(h[p][0] <= h[i][0]) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
[h[p], h[i]] = [h[i], h[p]];
|
|
165
|
+
i = p;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function heap_pop(h)
|
|
170
|
+
{
|
|
171
|
+
let top = h[0];
|
|
172
|
+
let last = h.pop();
|
|
173
|
+
if(h.length) {
|
|
174
|
+
h[0] = last;
|
|
175
|
+
let i = 0;
|
|
176
|
+
for(;;) {
|
|
177
|
+
let l = 2 * i + 1;
|
|
178
|
+
let r = l + 1;
|
|
179
|
+
let m = i;
|
|
180
|
+
if(l < h.length && h[l][0] < h[m][0]) {
|
|
181
|
+
m = l;
|
|
182
|
+
}
|
|
183
|
+
if(r < h.length && h[r][0] < h[m][0]) {
|
|
184
|
+
m = r;
|
|
185
|
+
}
|
|
186
|
+
if(m === i) {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
[h[m], h[i]] = [h[i], h[m]];
|
|
190
|
+
i = m;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return top;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/************************************************************
|
|
197
|
+
* The shortest orthogonal line from `a` to `z` that enters none
|
|
198
|
+
* of `boxes` (already expanded by the clearance), a turn costing
|
|
199
|
+
* ROUTE_BEND. The line leaves `a` going down and ends at `z`
|
|
200
|
+
* going down, since after `z` it drops into a port.
|
|
201
|
+
*
|
|
202
|
+
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
203
|
+
* of the two ends, which is all an orthogonal shortest path ever
|
|
204
|
+
* needs to turn on. Returns [a, ...turns, z], or null when every
|
|
205
|
+
* way is closed.
|
|
206
|
+
************************************************************/
|
|
207
|
+
function orth_search(a, z, boxes)
|
|
208
|
+
{
|
|
209
|
+
for(let b of boxes) {
|
|
210
|
+
if(point_inside(a, b) || point_inside(z, b)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let uniq = (list) => [...new Set(list)].sort((p, q) => p - q);
|
|
216
|
+
let xs = uniq([a[0], z[0]].concat(...boxes.map((b) => [b.x1, b.x2])));
|
|
217
|
+
let ys = uniq([a[1], z[1]].concat(...boxes.map((b) => [b.y1, b.y2])));
|
|
218
|
+
let W = xs.length;
|
|
219
|
+
let H = ys.length;
|
|
220
|
+
|
|
221
|
+
let free = (x, y) => !boxes.some((b) => point_inside([x, y], b));
|
|
222
|
+
let ok = new Uint8Array(W * H);
|
|
223
|
+
for(let j = 0; j < H; j++) {
|
|
224
|
+
for(let i = 0; i < W; i++) {
|
|
225
|
+
ok[j * W + i] = free(xs[i], ys[j])? 1 : 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/* A step to the next line is open when both points are free and
|
|
229
|
+
* so is the middle of the step: consecutive lines have no border
|
|
230
|
+
* between them, so the middle speaks for the whole step. */
|
|
231
|
+
let step_right = new Uint8Array(W * H);
|
|
232
|
+
let step_down = new Uint8Array(W * H);
|
|
233
|
+
for(let j = 0; j < H; j++) {
|
|
234
|
+
for(let i = 0; i < W; i++) {
|
|
235
|
+
let k = j * W + i;
|
|
236
|
+
if(!ok[k]) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if(i + 1 < W && ok[k + 1] && free((xs[i] + xs[i + 1]) / 2, ys[j])) {
|
|
240
|
+
step_right[k] = 1;
|
|
241
|
+
}
|
|
242
|
+
if(j + 1 < H && ok[k + W] && free(xs[i], (ys[j] + ys[j + 1]) / 2)) {
|
|
243
|
+
step_down[k] = 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* States are (point, heading): 0 right, 1 left, 2 down, 3 up. */
|
|
249
|
+
let start = ys.indexOf(a[1]) * W + xs.indexOf(a[0]);
|
|
250
|
+
let goal = ys.indexOf(z[1]) * W + xs.indexOf(z[0]);
|
|
251
|
+
let best = new Float64Array(W * H * 4).fill(Infinity);
|
|
252
|
+
let prev = new Int32Array(W * H * 4).fill(-1);
|
|
253
|
+
let heap = [];
|
|
254
|
+
best[start * 4 + 2] = 0;
|
|
255
|
+
heap_push(heap, [0, start * 4 + 2]);
|
|
256
|
+
|
|
257
|
+
let moves = (k) => {
|
|
258
|
+
let i = k % W;
|
|
259
|
+
let j = (k - i) / W;
|
|
260
|
+
let out = [];
|
|
261
|
+
if(step_right[k]) {
|
|
262
|
+
out.push([k + 1, 0, xs[i + 1] - xs[i]]);
|
|
263
|
+
}
|
|
264
|
+
if(i > 0 && step_right[k - 1]) {
|
|
265
|
+
out.push([k - 1, 1, xs[i] - xs[i - 1]]);
|
|
266
|
+
}
|
|
267
|
+
if(step_down[k]) {
|
|
268
|
+
out.push([k + W, 2, ys[j + 1] - ys[j]]);
|
|
269
|
+
}
|
|
270
|
+
if(j > 0 && step_down[k - W]) {
|
|
271
|
+
out.push([k - W, 3, ys[j] - ys[j - 1]]);
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let found = -1;
|
|
277
|
+
let found_cost = Infinity;
|
|
278
|
+
while(heap.length) {
|
|
279
|
+
let [cost, state] = heap_pop(heap);
|
|
280
|
+
if(cost > best[state] || cost >= found_cost) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
let k = state >> 2;
|
|
284
|
+
let dir = state & 3;
|
|
285
|
+
if(k === goal) {
|
|
286
|
+
let total = cost + ((dir === 2)? 0 : ROUTE_BEND);
|
|
287
|
+
if(total < found_cost) {
|
|
288
|
+
found_cost = total;
|
|
289
|
+
found = state;
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
for(let [nk, nd, len] of moves(k)) {
|
|
294
|
+
let c = cost + len + ((nd === dir)? 0 : ROUTE_BEND);
|
|
295
|
+
let ns = nk * 4 + nd;
|
|
296
|
+
if(c < best[ns]) {
|
|
297
|
+
best[ns] = c;
|
|
298
|
+
prev[ns] = state;
|
|
299
|
+
heap_push(heap, [c, ns]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if(found < 0) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* Back from the goal, keeping only the points where it turns. */
|
|
308
|
+
let states = [];
|
|
309
|
+
for(let s = found; s >= 0; s = prev[s]) {
|
|
310
|
+
states.push(s);
|
|
311
|
+
}
|
|
312
|
+
states.reverse();
|
|
313
|
+
let pts = [a];
|
|
314
|
+
for(let n = 1; n < states.length; n++) {
|
|
315
|
+
let turn = (n + 1 < states.length) && ((states[n + 1] & 3) !== (states[n] & 3));
|
|
316
|
+
if(turn) {
|
|
317
|
+
let k = states[n] >> 2;
|
|
318
|
+
pts.push([xs[k % W], ys[Math.floor(k / W)]]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
pts.push(z);
|
|
322
|
+
return pts;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/************************************************************
|
|
326
|
+
* The control points of an elbow that does not cross any card.
|
|
327
|
+
*
|
|
328
|
+
* The simple elbow first (elbow_points): the channel, or the
|
|
329
|
+
* detour round the two cards. Only when that line crosses a card
|
|
330
|
+
* -- a third record standing in a detour's way, a row in the way
|
|
331
|
+
* of an edge that drops several rows -- is it ROUTED: out of the
|
|
332
|
+
* port, the shortest way along the gaps between the cards, in.
|
|
333
|
+
* A parent and the row of children under it keep their bus.
|
|
334
|
+
*
|
|
335
|
+
* `boxes` are all the cards on screen, the two of the edge
|
|
336
|
+
* included. The route keeps a clearance from every card, which
|
|
337
|
+
* grows with the lane so that two routed edges of one pair do not
|
|
338
|
+
* coincide. Nearby cards first; all of them if that finds no way;
|
|
339
|
+
* the simple elbow if nothing does.
|
|
340
|
+
************************************************************/
|
|
341
|
+
export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes)
|
|
342
|
+
{
|
|
343
|
+
let others = boxes || [];
|
|
344
|
+
if(!vertical) {
|
|
345
|
+
return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
|
|
346
|
+
others.map(swap_box)).map(swap);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
350
|
+
if(!elbow_path_hits([s, ...simple, t], others)) {
|
|
351
|
+
return simple;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let m = ELBOW_CLEAR / 2 + Math.abs(lane) * ELBOW_STEP;
|
|
355
|
+
let a = [s[0], s[1] + m + 4]; /* out of a bottom port */
|
|
356
|
+
let z = [t[0], t[1] - m - 4]; /* into a top port */
|
|
357
|
+
let region = box_expand({
|
|
358
|
+
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
359
|
+
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
|
360
|
+
x2: Math.max(a[0], z[0], box_s.x2, box_t.x2),
|
|
361
|
+
y2: Math.max(a[1], z[1], box_s.y2, box_t.y2),
|
|
362
|
+
}, ROUTE_REACH);
|
|
363
|
+
let near = others.filter((b) => box_overlaps(b, region));
|
|
364
|
+
|
|
365
|
+
let route = orth_search(a, z, near.map((b) => box_expand(b, m)));
|
|
366
|
+
if(!route && near.length < others.length) {
|
|
367
|
+
route = orth_search(a, z, others.map((b) => box_expand(b, m)));
|
|
368
|
+
}
|
|
369
|
+
return route || simple;
|
|
370
|
+
}
|
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,81 @@ 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
|
+
|
|
91
168
|
describe("read left to right", () => {
|
|
92
169
|
test("is the same geometry with the axes swapped", () => {
|
|
93
170
|
let sw = (p) => [p[1], p[0]];
|