@yuneta/gobj-ui 7.23.149 → 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 +24 -0
- package/dist/gobj-ui.cjs.js +302 -5
- package/dist/gobj-ui.es.js +302 -5
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +71 -5
- package/src/treedb_elbow.js +370 -0
- package/src/treedb_elbow.test.js +182 -0
package/README.md
CHANGED
|
@@ -1438,6 +1438,30 @@ direction, and a layout with no rows (`rowless_layouts`: `radial`,
|
|
|
1438
1438
|
`edge_shape` is `SDF_PERSIST` on `C_YUI_TREEDB_GRAPH`, a preference per
|
|
1439
1439
|
treedb like `node_labels`. Consumer key: `elbow edges`.
|
|
1440
1440
|
|
|
1441
|
+
Two cases are not a channel (`7.23.150`, `treedb_elbow.js`, pure and
|
|
1442
|
+
tested). An elbow that does not run **forward** — the target above its
|
|
1443
|
+
source or in the same row, which one edge of every reciprocal pair is —
|
|
1444
|
+
goes **round** the two cards instead of through them: a short run out of its
|
|
1445
|
+
port, along the outside of both, and in from the other port's own side. And
|
|
1446
|
+
the edges joining the **same two cards**, either way round, take **lanes**
|
|
1447
|
+
10px apart in the order they were made (0, +1, −1…), so a new one never
|
|
1448
|
+
moves the ones already drawn; a detour on a negative lane goes round the
|
|
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.
|
|
1464
|
+
|
|
1441
1465
|
All three take the **same spanning tree**, chosen deterministically: roots are the
|
|
1442
1466
|
nodes with no incoming edge, in node order; a node belongs to the **first
|
|
1443
1467
|
parent that reaches it** in a breadth-first walk (the place, not the
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -29630,6 +29630,254 @@ function layout_radial(nodes, edges, opts) {
|
|
|
29630
29630
|
}
|
|
29631
29631
|
return pos;
|
|
29632
29632
|
}
|
|
29633
|
+
/************************************************************
|
|
29634
|
+
* The lane of `id` among `ids`, the edges joining the same two
|
|
29635
|
+
* cards: 0, then +1, -1, +2, -2... in the order they were made.
|
|
29636
|
+
* Edge ids end in a counter, so a numeric sort is that order.
|
|
29637
|
+
************************************************************/
|
|
29638
|
+
function elbow_lane(ids, id) {
|
|
29639
|
+
let k = ids.slice().sort((a, b) => String(a).localeCompare(String(b), void 0, { numeric: true })).indexOf(id);
|
|
29640
|
+
if (k <= 0) return 0;
|
|
29641
|
+
return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
|
|
29642
|
+
}
|
|
29643
|
+
function swap(p) {
|
|
29644
|
+
return [p[1], p[0]];
|
|
29645
|
+
}
|
|
29646
|
+
function swap_box(b) {
|
|
29647
|
+
return {
|
|
29648
|
+
x1: b.y1,
|
|
29649
|
+
y1: b.x1,
|
|
29650
|
+
x2: b.y2,
|
|
29651
|
+
y2: b.x2
|
|
29652
|
+
};
|
|
29653
|
+
}
|
|
29654
|
+
/************************************************************
|
|
29655
|
+
* The control points of an elbow from the port `s` to the port
|
|
29656
|
+
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
29657
|
+
* ({x1, y1, x2, y2}) and the edge's lane.
|
|
29658
|
+
*
|
|
29659
|
+
* vertical the rows are stacked (top to bottom); false
|
|
29660
|
+
* when they are side by side (left to right)
|
|
29661
|
+
************************************************************/
|
|
29662
|
+
function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
29663
|
+
if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true).map(swap);
|
|
29664
|
+
let shift = lane * 10;
|
|
29665
|
+
if (t[1] - s[1] > 20) {
|
|
29666
|
+
let mid = (s[1] + t[1]) / 2 + shift;
|
|
29667
|
+
mid = Math.min(Math.max(mid, s[1] + 10), t[1] - 10);
|
|
29668
|
+
return [[s[0], mid], [t[0], mid]];
|
|
29669
|
+
}
|
|
29670
|
+
let out = 16 + Math.abs(shift);
|
|
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;
|
|
29674
|
+
return [
|
|
29675
|
+
[s[0], y1],
|
|
29676
|
+
[side, y1],
|
|
29677
|
+
[side, y2],
|
|
29678
|
+
[t[0], y2]
|
|
29679
|
+
];
|
|
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
|
+
}
|
|
29633
29881
|
//#endregion
|
|
29634
29882
|
//#region node_modules/@babel/runtime/helpers/esm/typeof.js
|
|
29635
29883
|
function _typeof(o) {
|
|
@@ -59009,14 +59257,64 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
|
|
|
59009
59257
|
*
|
|
59010
59258
|
* Computed on every draw from where the two ends ARE, so a
|
|
59011
59259
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59012
|
-
* between rows (top to bottom), `-h` between columns.
|
|
59260
|
+
* between rows (top to bottom), `-h` between columns. An edge that
|
|
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).
|
|
59013
59264
|
************************************************************/
|
|
59014
59265
|
var ELBOW_RADIUS = 6;
|
|
59266
|
+
function elbow_box(node) {
|
|
59267
|
+
try {
|
|
59268
|
+
let b = node.getShape("key").getBounds();
|
|
59269
|
+
return {
|
|
59270
|
+
x1: b.min[0],
|
|
59271
|
+
y1: b.min[1],
|
|
59272
|
+
x2: b.max[0],
|
|
59273
|
+
y2: b.max[1]
|
|
59274
|
+
};
|
|
59275
|
+
} catch (e) {
|
|
59276
|
+
(0, _yuneta_gobj_js.log_error)(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
|
|
59277
|
+
let c = node.getCenter();
|
|
59278
|
+
return {
|
|
59279
|
+
x1: c[0],
|
|
59280
|
+
y1: c[1],
|
|
59281
|
+
x2: c[0],
|
|
59282
|
+
y2: c[1]
|
|
59283
|
+
};
|
|
59284
|
+
}
|
|
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
|
+
}
|
|
59302
|
+
function elbow_lane_of(edge) {
|
|
59303
|
+
let graph = edge.context && edge.context.graph;
|
|
59304
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
59305
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
59306
|
+
if (!graph || s === void 0 || t === void 0 || s === t) return 0;
|
|
59307
|
+
try {
|
|
59308
|
+
return elbow_lane(graph.getRelatedEdgesData(s).filter((d) => d.source === s && d.target === t || d.source === t && d.target === s).map((d) => d.id), edge.id);
|
|
59309
|
+
} catch (e) {
|
|
59310
|
+
(0, _yuneta_gobj_js.log_error)(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
|
|
59311
|
+
return 0;
|
|
59312
|
+
}
|
|
59313
|
+
}
|
|
59015
59314
|
var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
59016
59315
|
getControlPoints(attributes) {
|
|
59017
59316
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59018
|
-
|
|
59019
|
-
return [[s[0], mid], [t[0], mid]];
|
|
59317
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
|
|
59020
59318
|
}
|
|
59021
59319
|
getKeyPath(attributes) {
|
|
59022
59320
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59025,8 +59323,7 @@ var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
|
59025
59323
|
var TreedbElbowH = class extends _antv_g6.Polyline {
|
|
59026
59324
|
getControlPoints(attributes) {
|
|
59027
59325
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59028
|
-
|
|
59029
|
-
return [[mid, s[1]], [mid, t[1]]];
|
|
59326
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
|
|
59030
59327
|
}
|
|
59031
59328
|
getKeyPath(attributes) {
|
|
59032
59329
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -29625,6 +29625,254 @@ function layout_radial(nodes, edges, opts) {
|
|
|
29625
29625
|
}
|
|
29626
29626
|
return pos;
|
|
29627
29627
|
}
|
|
29628
|
+
/************************************************************
|
|
29629
|
+
* The lane of `id` among `ids`, the edges joining the same two
|
|
29630
|
+
* cards: 0, then +1, -1, +2, -2... in the order they were made.
|
|
29631
|
+
* Edge ids end in a counter, so a numeric sort is that order.
|
|
29632
|
+
************************************************************/
|
|
29633
|
+
function elbow_lane(ids, id) {
|
|
29634
|
+
let k = ids.slice().sort((a, b) => String(a).localeCompare(String(b), void 0, { numeric: true })).indexOf(id);
|
|
29635
|
+
if (k <= 0) return 0;
|
|
29636
|
+
return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
|
|
29637
|
+
}
|
|
29638
|
+
function swap(p) {
|
|
29639
|
+
return [p[1], p[0]];
|
|
29640
|
+
}
|
|
29641
|
+
function swap_box(b) {
|
|
29642
|
+
return {
|
|
29643
|
+
x1: b.y1,
|
|
29644
|
+
y1: b.x1,
|
|
29645
|
+
x2: b.y2,
|
|
29646
|
+
y2: b.x2
|
|
29647
|
+
};
|
|
29648
|
+
}
|
|
29649
|
+
/************************************************************
|
|
29650
|
+
* The control points of an elbow from the port `s` to the port
|
|
29651
|
+
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
29652
|
+
* ({x1, y1, x2, y2}) and the edge's lane.
|
|
29653
|
+
*
|
|
29654
|
+
* vertical the rows are stacked (top to bottom); false
|
|
29655
|
+
* when they are side by side (left to right)
|
|
29656
|
+
************************************************************/
|
|
29657
|
+
function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
29658
|
+
if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true).map(swap);
|
|
29659
|
+
let shift = lane * 10;
|
|
29660
|
+
if (t[1] - s[1] > 20) {
|
|
29661
|
+
let mid = (s[1] + t[1]) / 2 + shift;
|
|
29662
|
+
mid = Math.min(Math.max(mid, s[1] + 10), t[1] - 10);
|
|
29663
|
+
return [[s[0], mid], [t[0], mid]];
|
|
29664
|
+
}
|
|
29665
|
+
let out = 16 + Math.abs(shift);
|
|
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;
|
|
29669
|
+
return [
|
|
29670
|
+
[s[0], y1],
|
|
29671
|
+
[side, y1],
|
|
29672
|
+
[side, y2],
|
|
29673
|
+
[t[0], y2]
|
|
29674
|
+
];
|
|
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
|
+
}
|
|
29628
29876
|
//#endregion
|
|
29629
29877
|
//#region node_modules/@babel/runtime/helpers/esm/typeof.js
|
|
29630
29878
|
function _typeof(o) {
|
|
@@ -59004,14 +59252,64 @@ var TreedbTreeLayout = class extends BaseLayout {
|
|
|
59004
59252
|
*
|
|
59005
59253
|
* Computed on every draw from where the two ends ARE, so a
|
|
59006
59254
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59007
|
-
* between rows (top to bottom), `-h` between columns.
|
|
59255
|
+
* between rows (top to bottom), `-h` between columns. An edge that
|
|
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).
|
|
59008
59259
|
************************************************************/
|
|
59009
59260
|
var ELBOW_RADIUS = 6;
|
|
59261
|
+
function elbow_box(node) {
|
|
59262
|
+
try {
|
|
59263
|
+
let b = node.getShape("key").getBounds();
|
|
59264
|
+
return {
|
|
59265
|
+
x1: b.min[0],
|
|
59266
|
+
y1: b.min[1],
|
|
59267
|
+
x2: b.max[0],
|
|
59268
|
+
y2: b.max[1]
|
|
59269
|
+
};
|
|
59270
|
+
} catch (e) {
|
|
59271
|
+
log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
|
|
59272
|
+
let c = node.getCenter();
|
|
59273
|
+
return {
|
|
59274
|
+
x1: c[0],
|
|
59275
|
+
y1: c[1],
|
|
59276
|
+
x2: c[0],
|
|
59277
|
+
y2: c[1]
|
|
59278
|
+
};
|
|
59279
|
+
}
|
|
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
|
+
}
|
|
59297
|
+
function elbow_lane_of(edge) {
|
|
59298
|
+
let graph = edge.context && edge.context.graph;
|
|
59299
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
59300
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
59301
|
+
if (!graph || s === void 0 || t === void 0 || s === t) return 0;
|
|
59302
|
+
try {
|
|
59303
|
+
return elbow_lane(graph.getRelatedEdgesData(s).filter((d) => d.source === s && d.target === t || d.source === t && d.target === s).map((d) => d.id), edge.id);
|
|
59304
|
+
} catch (e) {
|
|
59305
|
+
log_error(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
|
|
59306
|
+
return 0;
|
|
59307
|
+
}
|
|
59308
|
+
}
|
|
59010
59309
|
var TreedbElbowV = class extends Polyline {
|
|
59011
59310
|
getControlPoints(attributes) {
|
|
59012
59311
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59013
|
-
|
|
59014
|
-
return [[s[0], mid], [t[0], mid]];
|
|
59312
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
|
|
59015
59313
|
}
|
|
59016
59314
|
getKeyPath(attributes) {
|
|
59017
59315
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59020,8 +59318,7 @@ var TreedbElbowV = class extends Polyline {
|
|
|
59020
59318
|
var TreedbElbowH = class extends Polyline {
|
|
59021
59319
|
getControlPoints(attributes) {
|
|
59022
59320
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59023
|
-
|
|
59024
|
-
return [[mid, s[1]], [mid, t[1]]];
|
|
59321
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
|
|
59025
59322
|
}
|
|
59026
59323
|
getKeyPath(attributes) {
|
|
59027
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,6 +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, elbow_route} from "./treedb_elbow.js";
|
|
136
137
|
|
|
137
138
|
import {
|
|
138
139
|
BaseLayout,
|
|
@@ -4221,16 +4222,81 @@ class TreedbTreeLayout extends BaseLayout
|
|
|
4221
4222
|
*
|
|
4222
4223
|
* Computed on every draw from where the two ends ARE, so a
|
|
4223
4224
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
4224
|
-
* between rows (top to bottom), `-h` between columns.
|
|
4225
|
+
* between rows (top to bottom), `-h` between columns. An edge that
|
|
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).
|
|
4225
4229
|
************************************************************/
|
|
4226
4230
|
const ELBOW_RADIUS = 6;
|
|
4227
4231
|
|
|
4232
|
+
/* The box of a card on the canvas, for a detour to go round it. */
|
|
4233
|
+
function elbow_box(node)
|
|
4234
|
+
{
|
|
4235
|
+
try {
|
|
4236
|
+
let b = node.getShape('key').getBounds();
|
|
4237
|
+
return {x1: b.min[0], y1: b.min[1], x2: b.max[0], y2: b.max[1]};
|
|
4238
|
+
} catch(e) {
|
|
4239
|
+
log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
|
|
4240
|
+
let c = node.getCenter();
|
|
4241
|
+
return {x1: c[0], y1: c[1], x2: c[0], y2: c[1]};
|
|
4242
|
+
}
|
|
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
|
+
|
|
4272
|
+
/* The lane of an edge among the edges joining the same two cards,
|
|
4273
|
+
* either way round (treedb_elbow.js). */
|
|
4274
|
+
function elbow_lane_of(edge)
|
|
4275
|
+
{
|
|
4276
|
+
let graph = edge.context && edge.context.graph;
|
|
4277
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
4278
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
4279
|
+
if(!graph || s === undefined || t === undefined || s === t) {
|
|
4280
|
+
return 0;
|
|
4281
|
+
}
|
|
4282
|
+
try {
|
|
4283
|
+
let ids = graph.getRelatedEdgesData(s)
|
|
4284
|
+
.filter((d) => (d.source === s && d.target === t) ||
|
|
4285
|
+
(d.source === t && d.target === s))
|
|
4286
|
+
.map((d) => d.id);
|
|
4287
|
+
return elbow_lane(ids, edge.id);
|
|
4288
|
+
} catch(e) {
|
|
4289
|
+
log_error(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
|
|
4290
|
+
return 0;
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4228
4294
|
class TreedbElbowV extends Polyline
|
|
4229
4295
|
{
|
|
4230
4296
|
getControlPoints(attributes) {
|
|
4231
4297
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4232
|
-
|
|
4233
|
-
|
|
4298
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4299
|
+
elbow_lane_of(this), true, elbow_obstacles(this));
|
|
4234
4300
|
}
|
|
4235
4301
|
|
|
4236
4302
|
getKeyPath(attributes) {
|
|
@@ -4243,8 +4309,8 @@ class TreedbElbowH extends Polyline
|
|
|
4243
4309
|
{
|
|
4244
4310
|
getControlPoints(attributes) {
|
|
4245
4311
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4246
|
-
|
|
4247
|
-
|
|
4312
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4313
|
+
elbow_lane_of(this), false, elbow_obstacles(this));
|
|
4248
4314
|
}
|
|
4249
4315
|
|
|
4250
4316
|
getKeyPath(attributes) {
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_elbow.js
|
|
3
|
+
*
|
|
4
|
+
* Where an elbow edge of the treedb graph turns, as plain
|
|
5
|
+
* geometry: the two elbow edge types of c_g6_nodes_tree.js ask
|
|
6
|
+
* this module for their control points on every draw, and it
|
|
7
|
+
* can be tested without G6.
|
|
8
|
+
*
|
|
9
|
+
* An elbow is mxGraph's tree routing: straight out of the
|
|
10
|
+
* parent's port to the channel half way to the child, along it,
|
|
11
|
+
* and straight into the child's port. Two cases are not that:
|
|
12
|
+
*
|
|
13
|
+
* - An edge that does not run FORWARD: the child above its
|
|
14
|
+
* parent, or in the same row. One of the two edges of a
|
|
15
|
+
* reciprocal pair (A -> B and B -> A) is always one. A channel
|
|
16
|
+
* half way between would cross both cards and lie on top of
|
|
17
|
+
* its partner, so it goes ROUND: a short run out of its port,
|
|
18
|
+
* along the outside of both cards, and into the other port
|
|
19
|
+
* from that port's own side.
|
|
20
|
+
*
|
|
21
|
+
* - Several edges joining the SAME two cards: a reciprocal pair,
|
|
22
|
+
* or two hooks of one parent reaching one child. Each gets a
|
|
23
|
+
* LANE: the first stays where it is, the next moves one step to
|
|
24
|
+
* one side, the next one step to the other. The lane follows
|
|
25
|
+
* the order the edges were made, so a new edge never moves the
|
|
26
|
+
* ones already drawn.
|
|
27
|
+
*
|
|
28
|
+
* Written for the top-down reading; left to right is the same
|
|
29
|
+
* geometry with the axes swapped.
|
|
30
|
+
*
|
|
31
|
+
* Copyright (c) 2026, ArtGins.
|
|
32
|
+
* All Rights Reserved.
|
|
33
|
+
***********************************************************************/
|
|
34
|
+
|
|
35
|
+
export const ELBOW_STEP = 10; /* between two lanes */
|
|
36
|
+
export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
|
|
37
|
+
export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
|
|
38
|
+
|
|
39
|
+
/************************************************************
|
|
40
|
+
* The lane of `id` among `ids`, the edges joining the same two
|
|
41
|
+
* cards: 0, then +1, -1, +2, -2... in the order they were made.
|
|
42
|
+
* Edge ids end in a counter, so a numeric sort is that order.
|
|
43
|
+
************************************************************/
|
|
44
|
+
export function elbow_lane(ids, id)
|
|
45
|
+
{
|
|
46
|
+
let sorted = ids.slice().sort(
|
|
47
|
+
(a, b) => String(a).localeCompare(String(b), undefined, {numeric: true})
|
|
48
|
+
);
|
|
49
|
+
let k = sorted.indexOf(id);
|
|
50
|
+
if(k <= 0) {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
let side = (k % 2)? 1 : -1;
|
|
54
|
+
return side * Math.ceil(k / 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function swap(p)
|
|
58
|
+
{
|
|
59
|
+
return [p[1], p[0]];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function swap_box(b)
|
|
63
|
+
{
|
|
64
|
+
return {x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/************************************************************
|
|
68
|
+
* The control points of an elbow from the port `s` to the port
|
|
69
|
+
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
70
|
+
* ({x1, y1, x2, y2}) and the edge's lane.
|
|
71
|
+
*
|
|
72
|
+
* vertical the rows are stacked (top to bottom); false
|
|
73
|
+
* when they are side by side (left to right)
|
|
74
|
+
************************************************************/
|
|
75
|
+
export function elbow_points(s, t, box_s, box_t, lane, vertical)
|
|
76
|
+
{
|
|
77
|
+
if(!vertical) {
|
|
78
|
+
return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true)
|
|
79
|
+
.map(swap);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let shift = lane * ELBOW_STEP;
|
|
83
|
+
|
|
84
|
+
/* Forward: the channel half way down, moved by the lane and
|
|
85
|
+
* kept strictly between the two ports. */
|
|
86
|
+
if(t[1] - s[1] > 2 * ELBOW_STEP) {
|
|
87
|
+
let mid = (s[1] + t[1]) / 2 + shift;
|
|
88
|
+
mid = Math.min(Math.max(mid, s[1] + ELBOW_STEP), t[1] - ELBOW_STEP);
|
|
89
|
+
return [[s[0], mid], [t[0], mid]];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Backwards, or across one row: round the outside. A negative
|
|
93
|
+
* lane goes round the left, any other the right, so the two
|
|
94
|
+
* detours of one pair never share a side. */
|
|
95
|
+
let out = ELBOW_JETTY + Math.abs(shift);
|
|
96
|
+
let side = (lane < 0)
|
|
97
|
+
? Math.min(box_s.x1, box_t.x1) - ELBOW_CLEAR - Math.abs(shift)
|
|
98
|
+
: Math.max(box_s.x2, box_t.x2) + ELBOW_CLEAR + Math.abs(shift);
|
|
99
|
+
let y1 = s[1] + out; /* a hook port is on the bottom edge */
|
|
100
|
+
let y2 = t[1] - out; /* an fkey port is on the top edge */
|
|
101
|
+
return [[s[0], y1], [side, y1], [side, y2], [t[0], y2]];
|
|
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
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* treedb_elbow.test.js
|
|
3
|
+
*
|
|
4
|
+
* The elbow edges of the treedb graph, pinned: the lane of an
|
|
5
|
+
* edge among those joining the same two cards, the channel of a
|
|
6
|
+
* forward edge, the detour of one that is not.
|
|
7
|
+
***********************************************************************/
|
|
8
|
+
import { describe, test, expect } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
elbow_lane, elbow_points, elbow_route, elbow_path_hits, ELBOW_STEP, ELBOW_CLEAR,
|
|
11
|
+
} from "./treedb_elbow.js";
|
|
12
|
+
|
|
13
|
+
/* Two cards 100x40: A on top, B one row below it. */
|
|
14
|
+
const A = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
15
|
+
const B = {x1: 0, y1: 130, x2: 100, y2: 170};
|
|
16
|
+
const A_HOOK = [50, 40]; /* A's bottom edge */
|
|
17
|
+
const A_FKEY = [50, 0]; /* A's top edge */
|
|
18
|
+
const B_HOOK = [60, 170];
|
|
19
|
+
const B_FKEY = [50, 130];
|
|
20
|
+
|
|
21
|
+
describe("the lane", () => {
|
|
22
|
+
test("one edge alone keeps the middle", () => {
|
|
23
|
+
expect(elbow_lane(["edge-t-7"], "edge-t-7")).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("lanes follow the order the edges were made, not the string order", () => {
|
|
27
|
+
let ids = ["edge-t-10", "edge-t-9", "edge-t-11"];
|
|
28
|
+
expect(elbow_lane(ids, "edge-t-9")).toBe(0);
|
|
29
|
+
expect(elbow_lane(ids, "edge-t-10")).toBe(1);
|
|
30
|
+
expect(elbow_lane(ids, "edge-t-11")).toBe(-1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("a new edge does not move the ones already drawn", () => {
|
|
34
|
+
let before = ["edge-t-1", "edge-t-2"];
|
|
35
|
+
let after = before.concat(["edge-t-3"]);
|
|
36
|
+
for(let id of before) {
|
|
37
|
+
expect(elbow_lane(after, id)).toBe(elbow_lane(before, id));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("a forward edge", () => {
|
|
43
|
+
test("turns in the channel half way between the ports", () => {
|
|
44
|
+
let pts = elbow_points(A_HOOK, B_FKEY, A, B, 0, true);
|
|
45
|
+
expect(pts).toEqual([[50, 85], [50, 85]]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("its lane moves the channel, and keeps it between the ports", () => {
|
|
49
|
+
let pts = elbow_points(A_HOOK, B_FKEY, A, B, 1, true);
|
|
50
|
+
expect(pts[0][1]).toBe(85 + ELBOW_STEP);
|
|
51
|
+
let far = elbow_points(A_HOOK, B_FKEY, A, B, 20, true);
|
|
52
|
+
expect(far[0][1]).toBeLessThan(B_FKEY[1]);
|
|
53
|
+
expect(far[0][1]).toBeGreaterThan(A_HOOK[1]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("an edge that does not run forward", () => {
|
|
58
|
+
test("goes round the outside of both cards", () => {
|
|
59
|
+
/* B -> A: out of B's bottom, back up into A's top. */
|
|
60
|
+
let pts = elbow_points(B_HOOK, A_FKEY, B, A, 0, true);
|
|
61
|
+
expect(pts.length).toBe(4);
|
|
62
|
+
let side = pts[1][0];
|
|
63
|
+
expect(side).toBeGreaterThanOrEqual(100 + ELBOW_CLEAR);
|
|
64
|
+
expect(pts[2][0]).toBe(side);
|
|
65
|
+
expect(pts[0][1]).toBeGreaterThan(B.y2); /* below B */
|
|
66
|
+
expect(pts[3][1]).toBeLessThan(A.y1); /* above A */
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a reciprocal pair shares no segment", () => {
|
|
70
|
+
let forward = elbow_points(A_HOOK, B_FKEY, A, B, 0, true);
|
|
71
|
+
let back = elbow_points(B_HOOK, A_FKEY, B, A, 1, true);
|
|
72
|
+
let channel = forward[0][1];
|
|
73
|
+
expect(back.every((p) => p[1] !== channel)).toBe(true);
|
|
74
|
+
expect(back[1][0]).toBeGreaterThan(A.x2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a negative lane goes round the other side", () => {
|
|
78
|
+
let right = elbow_points(B_HOOK, A_FKEY, B, A, 1, true);
|
|
79
|
+
let left = elbow_points(B_HOOK, A_FKEY, B, A, -1, true);
|
|
80
|
+
expect(right[1][0]).toBeGreaterThan(A.x2);
|
|
81
|
+
expect(left[1][0]).toBeLessThan(A.x1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("two cards in one row are not joined through them", () => {
|
|
85
|
+
/* C beside A, same row: A's hook (bottom) to C's fkey (top). */
|
|
86
|
+
let C = {x1: 200, y1: 0, x2: 300, y2: 40};
|
|
87
|
+
let pts = elbow_points(A_HOOK, [250, 0], A, C, 0, true);
|
|
88
|
+
expect(pts.length).toBe(4);
|
|
89
|
+
expect(pts[1][0]).toBeGreaterThan(C.x2);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
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("read left to right", () => {
|
|
169
|
+
test("is the same geometry with the axes swapped", () => {
|
|
170
|
+
let sw = (p) => [p[1], p[0]];
|
|
171
|
+
let swb = (b) => ({x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2});
|
|
172
|
+
for(let [s, t, bs, bt, lane] of [
|
|
173
|
+
[A_HOOK, B_FKEY, A, B, 0],
|
|
174
|
+
[A_HOOK, B_FKEY, A, B, 1],
|
|
175
|
+
[B_HOOK, A_FKEY, B, A, -1],
|
|
176
|
+
]) {
|
|
177
|
+
let down = elbow_points(s, t, bs, bt, lane, true);
|
|
178
|
+
let right = elbow_points(sw(s), sw(t), swb(bs), swb(bt), lane, false);
|
|
179
|
+
expect(right).toEqual(down.map(sw));
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|