@yuneta/gobj-ui 7.23.150 → 7.23.152

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