@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 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. The detour clears the two cards it joins, not the others: a card of a
1450
- third record standing in its way is crossed, as a curve would cross it.
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
@@ -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, and the edges
59062
- * joining the same two cards take lanes (treedb_elbow.js).
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 elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true);
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 elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false);
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 }));
@@ -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, and the edges
59057
- * joining the same two cards take lanes (treedb_elbow.js).
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 elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true);
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 elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.150",
3
+ "version": "7.23.151",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -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, elbow_points} from "./treedb_elbow.js";
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, and the edges
4227
- * joining the same two cards take lanes (treedb_elbow.js).
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 elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
4271
- elbow_lane_of(this), true);
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 elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
4285
- elbow_lane_of(this), false);
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) {
@@ -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
+ }
@@ -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 { elbow_lane, elbow_points, ELBOW_STEP, ELBOW_CLEAR } from "./treedb_elbow.js";
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]];