@yuneta/gobj-ui 7.23.151 → 7.23.153

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
@@ -1457,8 +1457,22 @@ the lane, plus the two ends; a turn costs 40px of line), over the cards within
1457
1457
  400px first and all of them if that finds no way; with no way at all the
1458
1458
  simple elbow stays. A parent and the row of children under it keep their bus;
1459
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
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.
1464
+
1465
+ The edges sharing an **end** do not lie on top of each other either
1466
+ (`7.23.153`): the forward edges leaving one card — all of them, not only one
1467
+ port's, or two hooks side by side would stack their runs — and those reaching
1468
+ one card (all its fkeys enter by one point) are **staggered**, each turning at
1469
+ its own height in the channel, 10px apart at most and closer when the channel
1470
+ is short. Leaving, the farthest turns highest; arriving, the farthest turns
1471
+ lowest: the orders in which the lines of one end nest without crossing. A
1472
+ routed line leaves its port and reaches the other one **sideways**, because
1473
+ the column straight under or over a port is where the staggered edges of that
1474
+ port run. Only the stub at the port is shared. G6's `shortest-path` router is not used: it is
1475
+ not exported, it
1462
1476
  falls back in silence to a route that crosses cards, and it rewrites its
1463
1477
  module defaults with every config it is given.
1464
1478
 
@@ -29640,6 +29640,23 @@ function elbow_lane(ids, id) {
29640
29640
  if (k <= 0) return 0;
29641
29641
  return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
29642
29642
  }
29643
+ /************************************************************
29644
+ * The rank of `id` among `items` ([{id, span}]): the forward
29645
+ * edges that share one end with it, each with how far it runs
29646
+ * ACROSS. Farthest first; a tie goes to the edge made first.
29647
+ * Returns {rank, count}.
29648
+ ************************************************************/
29649
+ function elbow_rank(items, id) {
29650
+ let sorted = items.slice().sort((a, b) => {
29651
+ if (b.span !== a.span) return b.span - a.span;
29652
+ return String(a.id).localeCompare(String(b.id), void 0, { numeric: true });
29653
+ });
29654
+ let rank = sorted.findIndex((it) => it.id === id);
29655
+ return {
29656
+ rank: rank < 0 ? 0 : rank,
29657
+ count: sorted.length
29658
+ };
29659
+ }
29643
29660
  function swap(p) {
29644
29661
  return [p[1], p[0]];
29645
29662
  }
@@ -29658,19 +29675,41 @@ function swap_box(b) {
29658
29675
  *
29659
29676
  * vertical the rows are stacked (top to bottom); false
29660
29677
  * when they are side by side (left to right)
29678
+ * stagger {dep, arr}: the edge's rank among the forward
29679
+ * edges leaving its source card and among those
29680
+ * reaching its target card (elbow_rank). Absent,
29681
+ * the channel is the middle.
29661
29682
  ************************************************************/
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);
29683
+ function elbow_points(s, t, box_s, box_t, lane, vertical, stagger) {
29684
+ if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, stagger).map(swap);
29664
29685
  let shift = lane * 10;
29665
29686
  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);
29687
+ let lo = Math.max(s[1], box_s.y2);
29688
+ let hi = Math.min(t[1], box_t.y1);
29689
+ let mid = (lo + hi) / 2;
29690
+ let st = stagger || {};
29691
+ let group = null;
29692
+ let k = 0;
29693
+ if (st.dep && st.dep.count > 1) {
29694
+ group = st.dep;
29695
+ k = st.dep.rank;
29696
+ } else if (st.arr && st.arr.count > 1) {
29697
+ group = st.arr;
29698
+ k = st.arr.count - 1 - st.arr.rank;
29699
+ }
29700
+ if (group) {
29701
+ let room = Math.max(0, hi - lo - 12);
29702
+ let step = Math.min(10, room / (group.count - 1));
29703
+ mid = mid - (group.count - 1) * step / 2 + k * step;
29704
+ }
29705
+ mid += shift;
29706
+ mid = Math.min(Math.max(mid, lo + 4), hi - 4);
29668
29707
  return [[s[0], mid], [t[0], mid]];
29669
29708
  }
29670
29709
  let out = 16 + Math.abs(shift);
29671
29710
  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;
29711
+ let y1 = Math.max(s[1], box_s.y2) + out;
29712
+ let y2 = Math.min(t[1], box_t.y1) - out;
29674
29713
  return [
29675
29714
  [s[0], y1],
29676
29715
  [side, y1],
@@ -29699,15 +29738,30 @@ function segment_hits(a, c, b) {
29699
29738
  let y2 = Math.max(a[1], c[1]);
29700
29739
  return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
29701
29740
  }
29741
+ function same_box(a, b) {
29742
+ return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
29743
+ }
29702
29744
  /************************************************************
29703
29745
  * 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.
29746
+ * `boxes`? A box is shrunk by a pixel, so a line along its
29747
+ * border is not a hit.
29748
+ *
29749
+ * A box is a whole node, its ports included, and the edge's own
29750
+ * ports stick out of its own two cards: its first segment starts
29751
+ * inside `box_s` and its last one ends inside `box_t`. Those two
29752
+ * segments are not tested against their own box; every other
29753
+ * segment is, so a line may not run back over its own cards.
29706
29754
  ************************************************************/
29707
- function elbow_path_hits(points, boxes) {
29755
+ function elbow_path_hits(points, boxes, box_s, box_t) {
29756
+ let last = points.length - 2;
29708
29757
  for (let b of boxes) {
29709
29758
  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;
29759
+ let own_s = same_box(b, box_s);
29760
+ let own_t = same_box(b, box_t);
29761
+ for (let i = 0; i <= last; i++) {
29762
+ if (own_s && i === 0 || own_t && i === last) continue;
29763
+ if (segment_hits(points[i], points[i + 1], inner)) return true;
29764
+ }
29711
29765
  }
29712
29766
  return false;
29713
29767
  }
@@ -29743,8 +29797,12 @@ function heap_pop(h) {
29743
29797
  /************************************************************
29744
29798
  * The shortest orthogonal line from `a` to `z` that enters none
29745
29799
  * 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.
29800
+ * ROUTE_BEND. The line leaves `a` SIDEWAYS and reaches `z`
29801
+ * SIDEWAYS: the column straight under a port and the one straight
29802
+ * over it belong to the forward edges that share that port, which
29803
+ * run along them to their staggered turns -- a route going down
29804
+ * that column lay on top of them. So a routed line shares only
29805
+ * the stub between the port and `a` (or `z`).
29748
29806
  *
29749
29807
  * The grid is SPARSE: only the lines of the boxes' borders and
29750
29808
  * of the two ends, which is all an orthogonal shortest path ever
@@ -29810,7 +29868,8 @@ function orth_search(a, z, boxes) {
29810
29868
  let k = state >> 2;
29811
29869
  let dir = state & 3;
29812
29870
  if (k === goal) {
29813
- let total = cost + (dir === 2 ? 0 : 40);
29871
+ if (dir >= 2) continue;
29872
+ let total = cost + 40;
29814
29873
  if (total < found_cost) {
29815
29874
  found_cost = total;
29816
29875
  found = state;
@@ -29818,6 +29877,7 @@ function orth_search(a, z, boxes) {
29818
29877
  continue;
29819
29878
  }
29820
29879
  for (let [nk, nd, len] of moves(k)) {
29880
+ if (k === start && nd >= 2) continue;
29821
29881
  let c = cost + len + (nd === dir ? 0 : 40);
29822
29882
  let ns = nk * 4 + nd;
29823
29883
  if (c < best[ns]) {
@@ -29849,24 +29909,27 @@ function orth_search(a, z, boxes) {
29849
29909
  * port, the shortest way along the gaps between the cards, in.
29850
29910
  * A parent and the row of children under it keep their bus.
29851
29911
  *
29852
- * `boxes` are all the cards on screen, the two of the edge
29853
- * included. The route keeps a clearance from every card, which
29912
+ * `boxes` are all the nodes on screen, the two of the edge
29913
+ * included, each as the box of the WHOLE node -- card, ports and
29914
+ * label -- so a line clears the ports too. `box_s` and `box_t`
29915
+ * are the edge's own two, measured the same way. The route keeps
29916
+ * a clearance from every box, which
29854
29917
  * grows with the lane so that two routed edges of one pair do not
29855
29918
  * coincide. Nearby cards first; all of them if that finds no way;
29856
29919
  * the simple elbow if nothing does.
29857
29920
  ************************************************************/
29858
- function elbow_route(s, t, box_s, box_t, lane, vertical, boxes) {
29921
+ function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger) {
29859
29922
  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);
29923
+ if (!vertical) return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, others.map(swap_box), stagger).map(swap);
29924
+ let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
29862
29925
  if (!elbow_path_hits([
29863
29926
  s,
29864
29927
  ...simple,
29865
29928
  t
29866
- ], others)) return simple;
29929
+ ], others, box_s, box_t)) return simple;
29867
29930
  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];
29931
+ let a = [s[0], Math.max(s[1], box_s.y2) + m + 1];
29932
+ let z = [t[0], Math.min(t[1], box_t.y1) - m - 1];
29870
29933
  let region = box_expand({
29871
29934
  x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
29872
29935
  y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
@@ -59259,13 +59322,15 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
59259
59322
  * dragged card, a fold or a layout needs nothing else. `-v` runs
59260
59323
  * between rows (top to bottom), `-h` between columns. An edge that
59261
59324
  * 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).
59325
+ * the same two cards take lanes, the edges sharing an end turn at
59326
+ * staggered heights, and a line that would cross any other card is
59327
+ * routed round it (treedb_elbow.js).
59264
59328
  ************************************************************/
59265
59329
  var ELBOW_RADIUS = 6;
59266
59330
  function elbow_box(node) {
59267
59331
  try {
59268
- let b = node.getShape("key").getBounds();
59332
+ let b = node.getRenderBounds();
59333
+ if (!b || b.isEmpty && b.isEmpty()) b = node.getShape("key").getBounds();
59269
59334
  return {
59270
59335
  x1: b.min[0],
59271
59336
  y1: b.min[1],
@@ -59311,10 +59376,45 @@ function elbow_lane_of(edge) {
59311
59376
  return 0;
59312
59377
  }
59313
59378
  }
59379
+ function elbow_stagger_of(edge, vertical) {
59380
+ let graph = edge.context && edge.context.graph;
59381
+ let element = edge.context && edge.context.element;
59382
+ let s = edge.sourceNode && edge.sourceNode.id;
59383
+ let t = edge.targetNode && edge.targetNode.id;
59384
+ if (!graph || !element || s === void 0 || t === void 0) return null;
59385
+ let across = vertical ? 0 : 1;
59386
+ let along = vertical ? 1 : 0;
59387
+ let pos = (id) => graph.getElementPosition(id);
59388
+ let port_of = (node_id, key) => {
59389
+ let node = element.getElement(node_id);
59390
+ let ports = node && typeof node.getPorts === "function" ? node.getPorts() : {};
59391
+ let port = key ? ports[key] : null;
59392
+ return port ? port.getPosition() : pos(node_id);
59393
+ };
59394
+ try {
59395
+ let ps = pos(s);
59396
+ let pt = pos(t);
59397
+ let dep = graph.getRelatedEdgesData(s, "out").filter((d) => d.source === s && pos(d.target)[along] > ps[along]).map((d) => ({
59398
+ id: d.id,
59399
+ span: Math.abs(pos(d.target)[across] - port_of(s, d.style && d.style.sourcePort)[across])
59400
+ }));
59401
+ let arr = graph.getRelatedEdgesData(t, "in").filter((d) => d.target === t && pos(d.source)[along] < pt[along]).map((d) => ({
59402
+ id: d.id,
59403
+ span: Math.abs(port_of(d.source, d.style && d.style.sourcePort)[across] - pt[across])
59404
+ }));
59405
+ return {
59406
+ dep: elbow_rank(dep, edge.id),
59407
+ arr: elbow_rank(arr, edge.id)
59408
+ };
59409
+ } catch (e) {
59410
+ (0, _yuneta_gobj_js.log_error)(`elbow edge ${edge.id}: cannot read the edges sharing its ends: ${e}`);
59411
+ return null;
59412
+ }
59413
+ }
59314
59414
  var TreedbElbowV = class extends _antv_g6.Polyline {
59315
59415
  getControlPoints(attributes) {
59316
59416
  let [s, t] = this.getEndpoints(attributes, false);
59317
- return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
59417
+ return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this), elbow_stagger_of(this, true));
59318
59418
  }
59319
59419
  getKeyPath(attributes) {
59320
59420
  return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
@@ -59323,7 +59423,7 @@ var TreedbElbowV = class extends _antv_g6.Polyline {
59323
59423
  var TreedbElbowH = class extends _antv_g6.Polyline {
59324
59424
  getControlPoints(attributes) {
59325
59425
  let [s, t] = this.getEndpoints(attributes, false);
59326
- return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
59426
+ return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this), elbow_stagger_of(this, false));
59327
59427
  }
59328
59428
  getKeyPath(attributes) {
59329
59429
  return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
@@ -29635,6 +29635,23 @@ function elbow_lane(ids, id) {
29635
29635
  if (k <= 0) return 0;
29636
29636
  return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
29637
29637
  }
29638
+ /************************************************************
29639
+ * The rank of `id` among `items` ([{id, span}]): the forward
29640
+ * edges that share one end with it, each with how far it runs
29641
+ * ACROSS. Farthest first; a tie goes to the edge made first.
29642
+ * Returns {rank, count}.
29643
+ ************************************************************/
29644
+ function elbow_rank(items, id) {
29645
+ let sorted = items.slice().sort((a, b) => {
29646
+ if (b.span !== a.span) return b.span - a.span;
29647
+ return String(a.id).localeCompare(String(b.id), void 0, { numeric: true });
29648
+ });
29649
+ let rank = sorted.findIndex((it) => it.id === id);
29650
+ return {
29651
+ rank: rank < 0 ? 0 : rank,
29652
+ count: sorted.length
29653
+ };
29654
+ }
29638
29655
  function swap(p) {
29639
29656
  return [p[1], p[0]];
29640
29657
  }
@@ -29653,19 +29670,41 @@ function swap_box(b) {
29653
29670
  *
29654
29671
  * vertical the rows are stacked (top to bottom); false
29655
29672
  * when they are side by side (left to right)
29673
+ * stagger {dep, arr}: the edge's rank among the forward
29674
+ * edges leaving its source card and among those
29675
+ * reaching its target card (elbow_rank). Absent,
29676
+ * the channel is the middle.
29656
29677
  ************************************************************/
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);
29678
+ function elbow_points(s, t, box_s, box_t, lane, vertical, stagger) {
29679
+ if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, stagger).map(swap);
29659
29680
  let shift = lane * 10;
29660
29681
  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);
29682
+ let lo = Math.max(s[1], box_s.y2);
29683
+ let hi = Math.min(t[1], box_t.y1);
29684
+ let mid = (lo + hi) / 2;
29685
+ let st = stagger || {};
29686
+ let group = null;
29687
+ let k = 0;
29688
+ if (st.dep && st.dep.count > 1) {
29689
+ group = st.dep;
29690
+ k = st.dep.rank;
29691
+ } else if (st.arr && st.arr.count > 1) {
29692
+ group = st.arr;
29693
+ k = st.arr.count - 1 - st.arr.rank;
29694
+ }
29695
+ if (group) {
29696
+ let room = Math.max(0, hi - lo - 12);
29697
+ let step = Math.min(10, room / (group.count - 1));
29698
+ mid = mid - (group.count - 1) * step / 2 + k * step;
29699
+ }
29700
+ mid += shift;
29701
+ mid = Math.min(Math.max(mid, lo + 4), hi - 4);
29663
29702
  return [[s[0], mid], [t[0], mid]];
29664
29703
  }
29665
29704
  let out = 16 + Math.abs(shift);
29666
29705
  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;
29706
+ let y1 = Math.max(s[1], box_s.y2) + out;
29707
+ let y2 = Math.min(t[1], box_t.y1) - out;
29669
29708
  return [
29670
29709
  [s[0], y1],
29671
29710
  [side, y1],
@@ -29694,15 +29733,30 @@ function segment_hits(a, c, b) {
29694
29733
  let y2 = Math.max(a[1], c[1]);
29695
29734
  return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
29696
29735
  }
29736
+ function same_box(a, b) {
29737
+ return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
29738
+ }
29697
29739
  /************************************************************
29698
29740
  * 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.
29741
+ * `boxes`? A box is shrunk by a pixel, so a line along its
29742
+ * border is not a hit.
29743
+ *
29744
+ * A box is a whole node, its ports included, and the edge's own
29745
+ * ports stick out of its own two cards: its first segment starts
29746
+ * inside `box_s` and its last one ends inside `box_t`. Those two
29747
+ * segments are not tested against their own box; every other
29748
+ * segment is, so a line may not run back over its own cards.
29701
29749
  ************************************************************/
29702
- function elbow_path_hits(points, boxes) {
29750
+ function elbow_path_hits(points, boxes, box_s, box_t) {
29751
+ let last = points.length - 2;
29703
29752
  for (let b of boxes) {
29704
29753
  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;
29754
+ let own_s = same_box(b, box_s);
29755
+ let own_t = same_box(b, box_t);
29756
+ for (let i = 0; i <= last; i++) {
29757
+ if (own_s && i === 0 || own_t && i === last) continue;
29758
+ if (segment_hits(points[i], points[i + 1], inner)) return true;
29759
+ }
29706
29760
  }
29707
29761
  return false;
29708
29762
  }
@@ -29738,8 +29792,12 @@ function heap_pop(h) {
29738
29792
  /************************************************************
29739
29793
  * The shortest orthogonal line from `a` to `z` that enters none
29740
29794
  * 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.
29795
+ * ROUTE_BEND. The line leaves `a` SIDEWAYS and reaches `z`
29796
+ * SIDEWAYS: the column straight under a port and the one straight
29797
+ * over it belong to the forward edges that share that port, which
29798
+ * run along them to their staggered turns -- a route going down
29799
+ * that column lay on top of them. So a routed line shares only
29800
+ * the stub between the port and `a` (or `z`).
29743
29801
  *
29744
29802
  * The grid is SPARSE: only the lines of the boxes' borders and
29745
29803
  * of the two ends, which is all an orthogonal shortest path ever
@@ -29805,7 +29863,8 @@ function orth_search(a, z, boxes) {
29805
29863
  let k = state >> 2;
29806
29864
  let dir = state & 3;
29807
29865
  if (k === goal) {
29808
- let total = cost + (dir === 2 ? 0 : 40);
29866
+ if (dir >= 2) continue;
29867
+ let total = cost + 40;
29809
29868
  if (total < found_cost) {
29810
29869
  found_cost = total;
29811
29870
  found = state;
@@ -29813,6 +29872,7 @@ function orth_search(a, z, boxes) {
29813
29872
  continue;
29814
29873
  }
29815
29874
  for (let [nk, nd, len] of moves(k)) {
29875
+ if (k === start && nd >= 2) continue;
29816
29876
  let c = cost + len + (nd === dir ? 0 : 40);
29817
29877
  let ns = nk * 4 + nd;
29818
29878
  if (c < best[ns]) {
@@ -29844,24 +29904,27 @@ function orth_search(a, z, boxes) {
29844
29904
  * port, the shortest way along the gaps between the cards, in.
29845
29905
  * A parent and the row of children under it keep their bus.
29846
29906
  *
29847
- * `boxes` are all the cards on screen, the two of the edge
29848
- * included. The route keeps a clearance from every card, which
29907
+ * `boxes` are all the nodes on screen, the two of the edge
29908
+ * included, each as the box of the WHOLE node -- card, ports and
29909
+ * label -- so a line clears the ports too. `box_s` and `box_t`
29910
+ * are the edge's own two, measured the same way. The route keeps
29911
+ * a clearance from every box, which
29849
29912
  * grows with the lane so that two routed edges of one pair do not
29850
29913
  * coincide. Nearby cards first; all of them if that finds no way;
29851
29914
  * the simple elbow if nothing does.
29852
29915
  ************************************************************/
29853
- function elbow_route(s, t, box_s, box_t, lane, vertical, boxes) {
29916
+ function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger) {
29854
29917
  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);
29918
+ if (!vertical) return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, others.map(swap_box), stagger).map(swap);
29919
+ let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
29857
29920
  if (!elbow_path_hits([
29858
29921
  s,
29859
29922
  ...simple,
29860
29923
  t
29861
- ], others)) return simple;
29924
+ ], others, box_s, box_t)) return simple;
29862
29925
  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];
29926
+ let a = [s[0], Math.max(s[1], box_s.y2) + m + 1];
29927
+ let z = [t[0], Math.min(t[1], box_t.y1) - m - 1];
29865
29928
  let region = box_expand({
29866
29929
  x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
29867
29930
  y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
@@ -59254,13 +59317,15 @@ var TreedbTreeLayout = class extends BaseLayout {
59254
59317
  * dragged card, a fold or a layout needs nothing else. `-v` runs
59255
59318
  * between rows (top to bottom), `-h` between columns. An edge that
59256
59319
  * 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).
59320
+ * the same two cards take lanes, the edges sharing an end turn at
59321
+ * staggered heights, and a line that would cross any other card is
59322
+ * routed round it (treedb_elbow.js).
59259
59323
  ************************************************************/
59260
59324
  var ELBOW_RADIUS = 6;
59261
59325
  function elbow_box(node) {
59262
59326
  try {
59263
- let b = node.getShape("key").getBounds();
59327
+ let b = node.getRenderBounds();
59328
+ if (!b || b.isEmpty && b.isEmpty()) b = node.getShape("key").getBounds();
59264
59329
  return {
59265
59330
  x1: b.min[0],
59266
59331
  y1: b.min[1],
@@ -59306,10 +59371,45 @@ function elbow_lane_of(edge) {
59306
59371
  return 0;
59307
59372
  }
59308
59373
  }
59374
+ function elbow_stagger_of(edge, vertical) {
59375
+ let graph = edge.context && edge.context.graph;
59376
+ let element = edge.context && edge.context.element;
59377
+ let s = edge.sourceNode && edge.sourceNode.id;
59378
+ let t = edge.targetNode && edge.targetNode.id;
59379
+ if (!graph || !element || s === void 0 || t === void 0) return null;
59380
+ let across = vertical ? 0 : 1;
59381
+ let along = vertical ? 1 : 0;
59382
+ let pos = (id) => graph.getElementPosition(id);
59383
+ let port_of = (node_id, key) => {
59384
+ let node = element.getElement(node_id);
59385
+ let ports = node && typeof node.getPorts === "function" ? node.getPorts() : {};
59386
+ let port = key ? ports[key] : null;
59387
+ return port ? port.getPosition() : pos(node_id);
59388
+ };
59389
+ try {
59390
+ let ps = pos(s);
59391
+ let pt = pos(t);
59392
+ let dep = graph.getRelatedEdgesData(s, "out").filter((d) => d.source === s && pos(d.target)[along] > ps[along]).map((d) => ({
59393
+ id: d.id,
59394
+ span: Math.abs(pos(d.target)[across] - port_of(s, d.style && d.style.sourcePort)[across])
59395
+ }));
59396
+ let arr = graph.getRelatedEdgesData(t, "in").filter((d) => d.target === t && pos(d.source)[along] < pt[along]).map((d) => ({
59397
+ id: d.id,
59398
+ span: Math.abs(port_of(d.source, d.style && d.style.sourcePort)[across] - pt[across])
59399
+ }));
59400
+ return {
59401
+ dep: elbow_rank(dep, edge.id),
59402
+ arr: elbow_rank(arr, edge.id)
59403
+ };
59404
+ } catch (e) {
59405
+ log_error(`elbow edge ${edge.id}: cannot read the edges sharing its ends: ${e}`);
59406
+ return null;
59407
+ }
59408
+ }
59309
59409
  var TreedbElbowV = class extends Polyline {
59310
59410
  getControlPoints(attributes) {
59311
59411
  let [s, t] = this.getEndpoints(attributes, false);
59312
- return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
59412
+ return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this), elbow_stagger_of(this, true));
59313
59413
  }
59314
59414
  getKeyPath(attributes) {
59315
59415
  return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
@@ -59318,7 +59418,7 @@ var TreedbElbowV = class extends Polyline {
59318
59418
  var TreedbElbowH = class extends Polyline {
59319
59419
  getControlPoints(attributes) {
59320
59420
  let [s, t] = this.getEndpoints(attributes, false);
59321
- return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
59421
+ return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this), elbow_stagger_of(this, false));
59322
59422
  }
59323
59423
  getKeyPath(attributes) {
59324
59424
  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.151",
3
+ "version": "7.23.153",
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_route} from "./treedb_elbow.js";
136
+ import {elbow_lane, elbow_rank, elbow_route} from "./treedb_elbow.js";
137
137
 
138
138
  import {
139
139
  BaseLayout,
@@ -4224,16 +4224,23 @@ class TreedbTreeLayout extends BaseLayout
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
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).
4227
+ * the same two cards take lanes, the edges sharing an end turn at
4228
+ * staggered heights, and a line that would cross any other card is
4229
+ * routed round it (treedb_elbow.js).
4229
4230
  ************************************************************/
4230
4231
  const ELBOW_RADIUS = 6;
4231
4232
 
4232
- /* The box of a card on the canvas, for a detour to go round it. */
4233
+ /* The box of a WHOLE node on the canvas -- the card, its ports and
4234
+ * its label -- for an elbow to keep clear of. The ports are the
4235
+ * reason it is not the key shape's box: they stick out of the
4236
+ * card, and a line over a port reads as a link to it. */
4233
4237
  function elbow_box(node)
4234
4238
  {
4235
4239
  try {
4236
- let b = node.getShape('key').getBounds();
4240
+ let b = node.getRenderBounds();
4241
+ if(!b || b.isEmpty && b.isEmpty()) {
4242
+ b = node.getShape('key').getBounds();
4243
+ }
4237
4244
  return {x1: b.min[0], y1: b.min[1], x2: b.max[0], y2: b.max[1]};
4238
4245
  } catch(e) {
4239
4246
  log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
@@ -4291,12 +4298,60 @@ function elbow_lane_of(edge)
4291
4298
  }
4292
4299
  }
4293
4300
 
4301
+ /* Where `edge` stands among the forward edges sharing one of its
4302
+ * ends, for the stagger (treedb_elbow.js): the ones leaving its
4303
+ * source card, and the ones reaching its target card. How far each
4304
+ * runs ACROSS is measured from the real port it leaves by -- the
4305
+ * hooks of one card sit at different places along its edge. */
4306
+ function elbow_stagger_of(edge, vertical)
4307
+ {
4308
+ let graph = edge.context && edge.context.graph;
4309
+ let element = edge.context && edge.context.element;
4310
+ let s = edge.sourceNode && edge.sourceNode.id;
4311
+ let t = edge.targetNode && edge.targetNode.id;
4312
+ if(!graph || !element || s === undefined || t === undefined) {
4313
+ return null;
4314
+ }
4315
+ let across = vertical? 0 : 1;
4316
+ let along = vertical? 1 : 0;
4317
+ let pos = (id) => graph.getElementPosition(id);
4318
+ let port_of = (node_id, key) => {
4319
+ let node = element.getElement(node_id);
4320
+ let ports = (node && typeof node.getPorts === 'function')? node.getPorts() : {};
4321
+ let port = key? ports[key] : null;
4322
+ return port? port.getPosition() : pos(node_id);
4323
+ };
4324
+ try {
4325
+ let ps = pos(s);
4326
+ let pt = pos(t);
4327
+ let dep = graph.getRelatedEdgesData(s, 'out')
4328
+ .filter((d) => d.source === s && pos(d.target)[along] > ps[along])
4329
+ .map((d) => ({
4330
+ id: d.id,
4331
+ span: Math.abs(pos(d.target)[across] -
4332
+ port_of(s, d.style && d.style.sourcePort)[across]),
4333
+ }));
4334
+ let arr = graph.getRelatedEdgesData(t, 'in')
4335
+ .filter((d) => d.target === t && pos(d.source)[along] < pt[along])
4336
+ .map((d) => ({
4337
+ id: d.id,
4338
+ span: Math.abs(port_of(d.source, d.style && d.style.sourcePort)[across] -
4339
+ pt[across]),
4340
+ }));
4341
+ return {dep: elbow_rank(dep, edge.id), arr: elbow_rank(arr, edge.id)};
4342
+ } catch(e) {
4343
+ log_error(`elbow edge ${edge.id}: cannot read the edges sharing its ends: ${e}`);
4344
+ return null;
4345
+ }
4346
+ }
4347
+
4294
4348
  class TreedbElbowV extends Polyline
4295
4349
  {
4296
4350
  getControlPoints(attributes) {
4297
4351
  let [s, t] = this.getEndpoints(attributes, false);
4298
4352
  return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
4299
- elbow_lane_of(this), true, elbow_obstacles(this));
4353
+ elbow_lane_of(this), true, elbow_obstacles(this),
4354
+ elbow_stagger_of(this, true));
4300
4355
  }
4301
4356
 
4302
4357
  getKeyPath(attributes) {
@@ -4310,7 +4365,8 @@ class TreedbElbowH extends Polyline
4310
4365
  getControlPoints(attributes) {
4311
4366
  let [s, t] = this.getEndpoints(attributes, false);
4312
4367
  return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
4313
- elbow_lane_of(this), false, elbow_obstacles(this));
4368
+ elbow_lane_of(this), false, elbow_obstacles(this),
4369
+ elbow_stagger_of(this, false));
4314
4370
  }
4315
4371
 
4316
4372
  getKeyPath(attributes) {
@@ -25,6 +25,15 @@
25
25
  * the order the edges were made, so a new edge never moves the
26
26
  * ones already drawn.
27
27
  *
28
+ * - Several forward edges sharing one END: the children of one
29
+ * card, or the parents of one card (all its fkeys enter by one
30
+ * point). They are STAGGERED: each turns at its own height in
31
+ * the channel, so their runs lie side by side instead of on top
32
+ * of each other. Leaving a card, the one that runs farthest
33
+ * turns highest; reaching a card, the one that comes from
34
+ * farthest turns lowest -- the two orders in which the lines of
35
+ * one end nest without crossing.
36
+ *
28
37
  * Written for the top-down reading; left to right is the same
29
38
  * geometry with the axes swapped.
30
39
  *
@@ -35,6 +44,7 @@
35
44
  export const ELBOW_STEP = 10; /* between two lanes */
36
45
  export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
37
46
  export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
47
+ export const ELBOW_STAGGER_PAD = 6; /* staggered turns keep this far from the rows */
38
48
 
39
49
  /************************************************************
40
50
  * The lane of `id` among `ids`, the edges joining the same two
@@ -54,6 +64,24 @@ export function elbow_lane(ids, id)
54
64
  return side * Math.ceil(k / 2);
55
65
  }
56
66
 
67
+ /************************************************************
68
+ * The rank of `id` among `items` ([{id, span}]): the forward
69
+ * edges that share one end with it, each with how far it runs
70
+ * ACROSS. Farthest first; a tie goes to the edge made first.
71
+ * Returns {rank, count}.
72
+ ************************************************************/
73
+ export function elbow_rank(items, id)
74
+ {
75
+ let sorted = items.slice().sort((a, b) => {
76
+ if(b.span !== a.span) {
77
+ return b.span - a.span;
78
+ }
79
+ return String(a.id).localeCompare(String(b.id), undefined, {numeric: true});
80
+ });
81
+ let rank = sorted.findIndex((it) => it.id === id);
82
+ return {rank: (rank < 0)? 0 : rank, count: sorted.length};
83
+ }
84
+
57
85
  function swap(p)
58
86
  {
59
87
  return [p[1], p[0]];
@@ -71,21 +99,44 @@ function swap_box(b)
71
99
  *
72
100
  * vertical the rows are stacked (top to bottom); false
73
101
  * when they are side by side (left to right)
102
+ * stagger {dep, arr}: the edge's rank among the forward
103
+ * edges leaving its source card and among those
104
+ * reaching its target card (elbow_rank). Absent,
105
+ * the channel is the middle.
74
106
  ************************************************************/
75
- export function elbow_points(s, t, box_s, box_t, lane, vertical)
107
+ export function elbow_points(s, t, box_s, box_t, lane, vertical, stagger)
76
108
  {
77
109
  if(!vertical) {
78
- return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true)
79
- .map(swap);
110
+ return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
111
+ stagger).map(swap);
80
112
  }
81
113
 
82
114
  let shift = lane * ELBOW_STEP;
83
115
 
84
- /* Forward: the channel half way down, moved by the lane and
85
- * kept strictly between the two ports. */
116
+ /* Forward: the channel half way between the two cards, each
117
+ * edge of a shared end at its own height round it, moved by the
118
+ * lane, and kept between the two rows. */
86
119
  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);
120
+ let lo = Math.max(s[1], box_s.y2);
121
+ let hi = Math.min(t[1], box_t.y1);
122
+ let mid = (lo + hi) / 2;
123
+ let st = stagger || {};
124
+ let group = null;
125
+ let k = 0;
126
+ if(st.dep && st.dep.count > 1) {
127
+ group = st.dep;
128
+ k = st.dep.rank; /* farthest highest */
129
+ } else if(st.arr && st.arr.count > 1) {
130
+ group = st.arr;
131
+ k = st.arr.count - 1 - st.arr.rank; /* farthest lowest */
132
+ }
133
+ if(group) {
134
+ let room = Math.max(0, hi - lo - 2 * ELBOW_STAGGER_PAD);
135
+ let step = Math.min(ELBOW_STEP, room / (group.count - 1));
136
+ mid = mid - (group.count - 1) * step / 2 + k * step;
137
+ }
138
+ mid += shift;
139
+ mid = Math.min(Math.max(mid, lo + 4), hi - 4);
89
140
  return [[s[0], mid], [t[0], mid]];
90
141
  }
91
142
 
@@ -96,8 +147,10 @@ export function elbow_points(s, t, box_s, box_t, lane, vertical)
96
147
  let side = (lane < 0)
97
148
  ? Math.min(box_s.x1, box_t.x1) - ELBOW_CLEAR - Math.abs(shift)
98
149
  : 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 */
150
+ /* From the border of the whole box, which holds the ports: a
151
+ * port sticks out of its card, and the run has to clear it. */
152
+ let y1 = Math.max(s[1], box_s.y2) + out; /* a hook port is on the bottom edge */
153
+ let y2 = Math.min(t[1], box_t.y1) - out; /* an fkey port is on the top edge */
101
154
  return [[s[0], y1], [side, y1], [side, y2], [t[0], y2]];
102
155
  }
103
156
 
@@ -133,16 +186,33 @@ function segment_hits(a, c, b)
133
186
  return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
134
187
  }
135
188
 
189
+ function same_box(a, b)
190
+ {
191
+ return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
192
+ }
193
+
136
194
  /************************************************************
137
195
  * 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.
196
+ * `boxes`? A box is shrunk by a pixel, so a line along its
197
+ * border is not a hit.
198
+ *
199
+ * A box is a whole node, its ports included, and the edge's own
200
+ * ports stick out of its own two cards: its first segment starts
201
+ * inside `box_s` and its last one ends inside `box_t`. Those two
202
+ * segments are not tested against their own box; every other
203
+ * segment is, so a line may not run back over its own cards.
140
204
  ************************************************************/
141
- export function elbow_path_hits(points, boxes)
205
+ export function elbow_path_hits(points, boxes, box_s, box_t)
142
206
  {
207
+ let last = points.length - 2;
143
208
  for(let b of boxes) {
144
209
  let inner = box_expand(b, -1);
145
- for(let i = 0; i + 1 < points.length; i++) {
210
+ let own_s = same_box(b, box_s);
211
+ let own_t = same_box(b, box_t);
212
+ for(let i = 0; i <= last; i++) {
213
+ if((own_s && i === 0) || (own_t && i === last)) {
214
+ continue;
215
+ }
146
216
  if(segment_hits(points[i], points[i + 1], inner)) {
147
217
  return true;
148
218
  }
@@ -196,8 +266,12 @@ function heap_pop(h)
196
266
  /************************************************************
197
267
  * The shortest orthogonal line from `a` to `z` that enters none
198
268
  * 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.
269
+ * ROUTE_BEND. The line leaves `a` SIDEWAYS and reaches `z`
270
+ * SIDEWAYS: the column straight under a port and the one straight
271
+ * over it belong to the forward edges that share that port, which
272
+ * run along them to their staggered turns -- a route going down
273
+ * that column lay on top of them. So a routed line shares only
274
+ * the stub between the port and `a` (or `z`).
201
275
  *
202
276
  * The grid is SPARSE: only the lines of the boxes' borders and
203
277
  * of the two ends, which is all an orthogonal shortest path ever
@@ -283,7 +357,10 @@ function orth_search(a, z, boxes)
283
357
  let k = state >> 2;
284
358
  let dir = state & 3;
285
359
  if(k === goal) {
286
- let total = cost + ((dir === 2)? 0 : ROUTE_BEND);
360
+ if(dir >= 2) {
361
+ continue; /* down the port's own column: not allowed */
362
+ }
363
+ let total = cost + ROUTE_BEND; /* the turn into the port */
287
364
  if(total < found_cost) {
288
365
  found_cost = total;
289
366
  found = state;
@@ -291,6 +368,9 @@ function orth_search(a, z, boxes)
291
368
  continue;
292
369
  }
293
370
  for(let [nk, nd, len] of moves(k)) {
371
+ if(k === start && nd >= 2) {
372
+ continue; /* out of the port sideways, not down its column */
373
+ }
294
374
  let c = cost + len + ((nd === dir)? 0 : ROUTE_BEND);
295
375
  let ns = nk * 4 + nd;
296
376
  if(c < best[ns]) {
@@ -332,28 +412,32 @@ function orth_search(a, z, boxes)
332
412
  * port, the shortest way along the gaps between the cards, in.
333
413
  * A parent and the row of children under it keep their bus.
334
414
  *
335
- * `boxes` are all the cards on screen, the two of the edge
336
- * included. The route keeps a clearance from every card, which
415
+ * `boxes` are all the nodes on screen, the two of the edge
416
+ * included, each as the box of the WHOLE node -- card, ports and
417
+ * label -- so a line clears the ports too. `box_s` and `box_t`
418
+ * are the edge's own two, measured the same way. The route keeps
419
+ * a clearance from every box, which
337
420
  * grows with the lane so that two routed edges of one pair do not
338
421
  * coincide. Nearby cards first; all of them if that finds no way;
339
422
  * the simple elbow if nothing does.
340
423
  ************************************************************/
341
- export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes)
424
+ export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger)
342
425
  {
343
426
  let others = boxes || [];
344
427
  if(!vertical) {
345
428
  return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
346
- others.map(swap_box)).map(swap);
429
+ others.map(swap_box), stagger).map(swap);
347
430
  }
348
431
 
349
- let simple = elbow_points(s, t, box_s, box_t, lane, true);
350
- if(!elbow_path_hits([s, ...simple, t], others)) {
432
+ let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
433
+ if(!elbow_path_hits([s, ...simple, t], others, box_s, box_t)) {
351
434
  return simple;
352
435
  }
353
436
 
354
437
  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 */
438
+ /* Just outside the grown box of each end, which holds its ports. */
439
+ let a = [s[0], Math.max(s[1], box_s.y2) + m + 1]; /* out of a bottom port */
440
+ let z = [t[0], Math.min(t[1], box_t.y1) - m - 1]; /* into a top port */
357
441
  let region = box_expand({
358
442
  x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
359
443
  y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
@@ -7,7 +7,7 @@
7
7
  ***********************************************************************/
8
8
  import { describe, test, expect } from "vitest";
9
9
  import {
10
- elbow_lane, elbow_points, elbow_route, elbow_path_hits, ELBOW_STEP, ELBOW_CLEAR,
10
+ elbow_lane, elbow_points, elbow_route, elbow_path_hits, elbow_rank, ELBOW_STEP, ELBOW_CLEAR,
11
11
  } from "./treedb_elbow.js";
12
12
 
13
13
  /* Two cards 100x40: A on top, B one row below it. */
@@ -165,6 +165,149 @@ describe("the route round the other cards", () => {
165
165
  });
166
166
  });
167
167
 
168
+ describe("edges sharing an end are staggered", () => {
169
+ test("the rank is farthest first, a tie to the edge made first", () => {
170
+ let items = [{id: "e-3", span: 10}, {id: "e-1", span: 300}, {id: "e-2", span: 10}];
171
+ expect(elbow_rank(items, "e-1")).toEqual({rank: 0, count: 3});
172
+ expect(elbow_rank(items, "e-2")).toEqual({rank: 1, count: 3});
173
+ expect(elbow_rank(items, "e-3")).toEqual({rank: 2, count: 3});
174
+ });
175
+
176
+ /* One parent on top (port at x 50, bottom at 40) and a row of
177
+ * children below (tops at 130). */
178
+ const P = {x1: 0, y1: 0, x2: 100, y2: 40};
179
+ const PORT = [50, 40];
180
+ const KIDS = [50, 160, 290, 420, -120].map((x, i) => ({
181
+ id: `e-${i + 1}`, x: x, box: {x1: x - 50, y1: 130, x2: x + 50, y2: 170},
182
+ }));
183
+ const channel_of = (kid) => {
184
+ let items = KIDS.map((k) => ({id: k.id, span: Math.abs(k.x - PORT[0])}));
185
+ let dep = elbow_rank(items, kid.id);
186
+ return elbow_points(PORT, [kid.x, 130], P, kid.box, 0, true, {dep: dep})[0][1];
187
+ };
188
+
189
+ test("the children of one port turn each at its own height, inside the channel", () => {
190
+ let ys = KIDS.map(channel_of);
191
+ expect(new Set(ys).size).toBe(KIDS.length);
192
+ for(let y of ys) {
193
+ expect(y).toBeGreaterThan(40);
194
+ expect(y).toBeLessThan(130);
195
+ }
196
+ });
197
+
198
+ test("the farthest turns highest, and the lines of one port do not cross", () => {
199
+ let y = Object.fromEntries(KIDS.map((k) => [k.id, channel_of(k)]));
200
+ expect(y["e-4"]).toBeLessThan(y["e-2"]); /* 420 is farther than 160 */
201
+ /* A crossing: one edge's drop (at its child's x, from its turn
202
+ * down to the row) cut by another's run (from the port to its
203
+ * child, at its own turn). */
204
+ for(let a of KIDS) {
205
+ for(let b of KIDS) {
206
+ if(a === b) {
207
+ continue;
208
+ }
209
+ let lo = Math.min(PORT[0], b.x);
210
+ let hi = Math.max(PORT[0], b.x);
211
+ let cuts = a.x > lo && a.x < hi && y[b.id] > y[a.id] && y[b.id] < 130;
212
+ expect(cuts).toBe(false);
213
+ }
214
+ }
215
+ });
216
+
217
+ test("the parents of one card: the farthest turns lowest, and they do not cross", () => {
218
+ /* Parents on top at these x (port at the middle of each bottom),
219
+ * one child below at x 200 whose fkeys all enter at its top. */
220
+ const C = {x1: 150, y1: 130, x2: 250, y2: 170};
221
+ const T0 = [200, 130];
222
+ const PARENTS = [0, 120, 330, 560].map((x, i) => ({id: `p-${i + 1}`, x: x}));
223
+ let items = PARENTS.map((p) => ({id: p.id, span: Math.abs(p.x - T0[0])}));
224
+ let y = {};
225
+ for(let p of PARENTS) {
226
+ let box = {x1: p.x - 50, y1: 0, x2: p.x + 50, y2: 40};
227
+ let arr = elbow_rank(items, p.id);
228
+ y[p.id] = elbow_points([p.x, 40], T0, box, C, 0, true, {arr: arr})[0][1];
229
+ }
230
+ expect(new Set(Object.values(y)).size).toBe(PARENTS.length);
231
+ expect(y["p-4"]).toBeGreaterThan(y["p-2"]); /* 560 is farther than 120 */
232
+ /* A crossing: one parent's drop (at its x, from its row down to
233
+ * its turn) cut by another's run (from its x to the child). */
234
+ for(let a of PARENTS) {
235
+ for(let b of PARENTS) {
236
+ if(a === b) {
237
+ continue;
238
+ }
239
+ let lo = Math.min(b.x, T0[0]);
240
+ let hi = Math.max(b.x, T0[0]);
241
+ let cuts = a.x > lo && a.x < hi && y[b.id] > 40 && y[b.id] < y[a.id];
242
+ expect(cuts).toBe(false);
243
+ }
244
+ }
245
+ });
246
+
247
+ test("a routed edge leaves and reaches its ports sideways, sharing only the stub", () => {
248
+ /* A same-row edge from A to C, with D standing between them:
249
+ * it is routed. It must not run down the column over C's
250
+ * port, which is where a forward edge into C drops. */
251
+ const A2 = {x1: 0, y1: 200, x2: 100, y2: 240};
252
+ const D2 = {x1: 150, y1: 180, x2: 250, y2: 260};
253
+ const C2 = {x1: 300, y1: 200, x2: 400, y2: 240};
254
+ let s = [50, 240];
255
+ let t = [350, 200];
256
+ let boxes = [A2, D2, C2];
257
+ let route = elbow_route(s, t, A2, C2, 0, true, boxes);
258
+ expect(route.length).toBeGreaterThan(2);
259
+ expect(elbow_path_hits([s, ...route, t], boxes, A2, C2)).toBe(false);
260
+ let a = route[0];
261
+ let z = route[route.length - 1];
262
+ expect(route[1][1]).toBe(a[1]); /* leaves a sideways */
263
+ expect(route[route.length - 2][1]).toBe(z[1]); /* reaches z sideways */
264
+ expect(t[1] - z[1]).toBeLessThan(20); /* the stub is short */
265
+ });
266
+
267
+ test("an end shared by nobody keeps the middle", () => {
268
+ let one = {rank: 0, count: 1};
269
+ expect(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true, {dep: one, arr: one}))
270
+ .toEqual(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true));
271
+ });
272
+ });
273
+
274
+ describe("the ports are obstacles too", () => {
275
+ /* Whole-node boxes: a port of 14px sticks 7px out of the bottom
276
+ * and the top of each card. */
277
+ const AP = {x1: 0, y1: -7, x2: 100, y2: 47};
278
+ const BP = {x1: 350, y1: 123, x2: 450, y2: 177};
279
+ const S = [50, 40];
280
+ const T2 = [400, 130];
281
+
282
+ test("an edge's own ports do not make its line a hit", () => {
283
+ let simple = elbow_points(S, T2, AP, BP, 0, true);
284
+ expect(elbow_path_hits([S, ...simple, T2], [AP, BP], AP, BP)).toBe(false);
285
+ expect(elbow_route(S, T2, AP, BP, 0, true, [AP, BP])).toEqual(simple);
286
+ });
287
+
288
+ test("a port of a third card in the channel is gone round", () => {
289
+ /* F's card ends at y 80, above the channel (85); its port
290
+ * reaches down to 95, into it. */
291
+ const F_CARD = {x1: 200, y1: 0, x2: 300, y2: 80};
292
+ const F = {x1: 200, y1: -7, x2: 300, y2: 95};
293
+ let simple = elbow_points(S, T2, AP, BP, 0, true);
294
+ expect(elbow_path_hits([S, ...simple, T2], [AP, BP, F_CARD], AP, BP)).toBe(false);
295
+ expect(elbow_path_hits([S, ...simple, T2], [AP, BP, F], AP, BP)).toBe(true);
296
+ let route = elbow_route(S, T2, AP, BP, 0, true, [AP, BP, F]);
297
+ expect(elbow_path_hits([S, ...route, T2], [AP, BP, F], AP, BP)).toBe(false);
298
+ });
299
+
300
+ test("a detour clears the ports of its own two cards", () => {
301
+ /* B below A, both with their ports: B -> A goes round. */
302
+ const AB = {x1: 0, y1: -7, x2: 100, y2: 47};
303
+ const BB = {x1: 0, y1: 123, x2: 100, y2: 177};
304
+ let pts = elbow_points([60, 170], [50, 0], BB, AB, 0, true);
305
+ expect(pts[0][1]).toBeGreaterThan(BB.y2);
306
+ expect(pts[3][1]).toBeLessThan(AB.y1);
307
+ expect(elbow_path_hits([[60, 170], ...pts, [50, 0]], [AB, BB], BB, AB)).toBe(false);
308
+ });
309
+ });
310
+
168
311
  describe("read left to right", () => {
169
312
  test("is the same geometry with the axes swapped", () => {
170
313
  let sw = (p) => [p[1], p[0]];