@yuneta/gobj-ui 7.23.152 → 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
@@ -1460,8 +1460,19 @@ what gets routed is a detour with a third card in its way, an edge dropping
1460
1460
  several rows, an edge between two cards of one row. Since `7.23.152` a node is
1461
1461
  measured **whole** — card, ports and label — so a line keeps clear of the
1462
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
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
1465
1476
  falls back in silence to a route that crosses cards, and it rewrites its
1466
1477
  module defaults with every config it is given.
1467
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,13 +29675,35 @@ 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);
@@ -29758,8 +29797,12 @@ function heap_pop(h) {
29758
29797
  /************************************************************
29759
29798
  * The shortest orthogonal line from `a` to `z` that enters none
29760
29799
  * 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.
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`).
29763
29806
  *
29764
29807
  * The grid is SPARSE: only the lines of the boxes' borders and
29765
29808
  * of the two ends, which is all an orthogonal shortest path ever
@@ -29825,7 +29868,8 @@ function orth_search(a, z, boxes) {
29825
29868
  let k = state >> 2;
29826
29869
  let dir = state & 3;
29827
29870
  if (k === goal) {
29828
- let total = cost + (dir === 2 ? 0 : 40);
29871
+ if (dir >= 2) continue;
29872
+ let total = cost + 40;
29829
29873
  if (total < found_cost) {
29830
29874
  found_cost = total;
29831
29875
  found = state;
@@ -29833,6 +29877,7 @@ function orth_search(a, z, boxes) {
29833
29877
  continue;
29834
29878
  }
29835
29879
  for (let [nk, nd, len] of moves(k)) {
29880
+ if (k === start && nd >= 2) continue;
29836
29881
  let c = cost + len + (nd === dir ? 0 : 40);
29837
29882
  let ns = nk * 4 + nd;
29838
29883
  if (c < best[ns]) {
@@ -29873,10 +29918,10 @@ function orth_search(a, z, boxes) {
29873
29918
  * coincide. Nearby cards first; all of them if that finds no way;
29874
29919
  * the simple elbow if nothing does.
29875
29920
  ************************************************************/
29876
- 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) {
29877
29922
  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);
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);
29880
29925
  if (!elbow_path_hits([
29881
29926
  s,
29882
29927
  ...simple,
@@ -59277,8 +59322,9 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
59277
59322
  * dragged card, a fold or a layout needs nothing else. `-v` runs
59278
59323
  * between rows (top to bottom), `-h` between columns. An edge that
59279
59324
  * 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).
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).
59282
59328
  ************************************************************/
59283
59329
  var ELBOW_RADIUS = 6;
59284
59330
  function elbow_box(node) {
@@ -59330,10 +59376,45 @@ function elbow_lane_of(edge) {
59330
59376
  return 0;
59331
59377
  }
59332
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
+ }
59333
59414
  var TreedbElbowV = class extends _antv_g6.Polyline {
59334
59415
  getControlPoints(attributes) {
59335
59416
  let [s, t] = this.getEndpoints(attributes, false);
59336
- 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));
59337
59418
  }
59338
59419
  getKeyPath(attributes) {
59339
59420
  return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
@@ -59342,7 +59423,7 @@ var TreedbElbowV = class extends _antv_g6.Polyline {
59342
59423
  var TreedbElbowH = class extends _antv_g6.Polyline {
59343
59424
  getControlPoints(attributes) {
59344
59425
  let [s, t] = this.getEndpoints(attributes, false);
59345
- 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));
59346
59427
  }
59347
59428
  getKeyPath(attributes) {
59348
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,13 +29670,35 @@ 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);
@@ -29753,8 +29792,12 @@ function heap_pop(h) {
29753
29792
  /************************************************************
29754
29793
  * The shortest orthogonal line from `a` to `z` that enters none
29755
29794
  * 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.
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`).
29758
29801
  *
29759
29802
  * The grid is SPARSE: only the lines of the boxes' borders and
29760
29803
  * of the two ends, which is all an orthogonal shortest path ever
@@ -29820,7 +29863,8 @@ function orth_search(a, z, boxes) {
29820
29863
  let k = state >> 2;
29821
29864
  let dir = state & 3;
29822
29865
  if (k === goal) {
29823
- let total = cost + (dir === 2 ? 0 : 40);
29866
+ if (dir >= 2) continue;
29867
+ let total = cost + 40;
29824
29868
  if (total < found_cost) {
29825
29869
  found_cost = total;
29826
29870
  found = state;
@@ -29828,6 +29872,7 @@ function orth_search(a, z, boxes) {
29828
29872
  continue;
29829
29873
  }
29830
29874
  for (let [nk, nd, len] of moves(k)) {
29875
+ if (k === start && nd >= 2) continue;
29831
29876
  let c = cost + len + (nd === dir ? 0 : 40);
29832
29877
  let ns = nk * 4 + nd;
29833
29878
  if (c < best[ns]) {
@@ -29868,10 +29913,10 @@ function orth_search(a, z, boxes) {
29868
29913
  * coincide. Nearby cards first; all of them if that finds no way;
29869
29914
  * the simple elbow if nothing does.
29870
29915
  ************************************************************/
29871
- 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) {
29872
29917
  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);
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);
29875
29920
  if (!elbow_path_hits([
29876
29921
  s,
29877
29922
  ...simple,
@@ -59272,8 +59317,9 @@ var TreedbTreeLayout = class extends BaseLayout {
59272
59317
  * dragged card, a fold or a layout needs nothing else. `-v` runs
59273
59318
  * between rows (top to bottom), `-h` between columns. An edge that
59274
59319
  * 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).
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).
59277
59323
  ************************************************************/
59278
59324
  var ELBOW_RADIUS = 6;
59279
59325
  function elbow_box(node) {
@@ -59325,10 +59371,45 @@ function elbow_lane_of(edge) {
59325
59371
  return 0;
59326
59372
  }
59327
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
+ }
59328
59409
  var TreedbElbowV = class extends Polyline {
59329
59410
  getControlPoints(attributes) {
59330
59411
  let [s, t] = this.getEndpoints(attributes, false);
59331
- 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));
59332
59413
  }
59333
59414
  getKeyPath(attributes) {
59334
59415
  return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
@@ -59337,7 +59418,7 @@ var TreedbElbowV = class extends Polyline {
59337
59418
  var TreedbElbowH = class extends Polyline {
59338
59419
  getControlPoints(attributes) {
59339
59420
  let [s, t] = this.getEndpoints(attributes, false);
59340
- 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));
59341
59422
  }
59342
59423
  getKeyPath(attributes) {
59343
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.152",
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,8 +4224,9 @@ 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
 
@@ -4297,12 +4298,60 @@ function elbow_lane_of(edge)
4297
4298
  }
4298
4299
  }
4299
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
+
4300
4348
  class TreedbElbowV extends Polyline
4301
4349
  {
4302
4350
  getControlPoints(attributes) {
4303
4351
  let [s, t] = this.getEndpoints(attributes, false);
4304
4352
  return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
4305
- elbow_lane_of(this), true, elbow_obstacles(this));
4353
+ elbow_lane_of(this), true, elbow_obstacles(this),
4354
+ elbow_stagger_of(this, true));
4306
4355
  }
4307
4356
 
4308
4357
  getKeyPath(attributes) {
@@ -4316,7 +4365,8 @@ class TreedbElbowH extends Polyline
4316
4365
  getControlPoints(attributes) {
4317
4366
  let [s, t] = this.getEndpoints(attributes, false);
4318
4367
  return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
4319
- elbow_lane_of(this), false, elbow_obstacles(this));
4368
+ elbow_lane_of(this), false, elbow_obstacles(this),
4369
+ elbow_stagger_of(this, false));
4320
4370
  }
4321
4371
 
4322
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
 
@@ -215,8 +266,12 @@ function heap_pop(h)
215
266
  /************************************************************
216
267
  * The shortest orthogonal line from `a` to `z` that enters none
217
268
  * 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.
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`).
220
275
  *
221
276
  * The grid is SPARSE: only the lines of the boxes' borders and
222
277
  * of the two ends, which is all an orthogonal shortest path ever
@@ -302,7 +357,10 @@ function orth_search(a, z, boxes)
302
357
  let k = state >> 2;
303
358
  let dir = state & 3;
304
359
  if(k === goal) {
305
- 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 */
306
364
  if(total < found_cost) {
307
365
  found_cost = total;
308
366
  found = state;
@@ -310,6 +368,9 @@ function orth_search(a, z, boxes)
310
368
  continue;
311
369
  }
312
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
+ }
313
374
  let c = cost + len + ((nd === dir)? 0 : ROUTE_BEND);
314
375
  let ns = nk * 4 + nd;
315
376
  if(c < best[ns]) {
@@ -360,15 +421,15 @@ function orth_search(a, z, boxes)
360
421
  * coincide. Nearby cards first; all of them if that finds no way;
361
422
  * the simple elbow if nothing does.
362
423
  ************************************************************/
363
- 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)
364
425
  {
365
426
  let others = boxes || [];
366
427
  if(!vertical) {
367
428
  return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
368
- others.map(swap_box)).map(swap);
429
+ others.map(swap_box), stagger).map(swap);
369
430
  }
370
431
 
371
- let simple = elbow_points(s, t, box_s, box_t, lane, true);
432
+ let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
372
433
  if(!elbow_path_hits([s, ...simple, t], others, box_s, box_t)) {
373
434
  return simple;
374
435
  }
@@ -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,112 @@ 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
+
168
274
  describe("the ports are obstacles too", () => {
169
275
  /* Whole-node boxes: a port of 14px sticks 7px out of the bottom
170
276
  * and the top of each card. */