@yuneta/gobj-ui 7.23.149 → 7.23.150

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
@@ -1438,6 +1438,17 @@ direction, and a layout with no rows (`rowless_layouts`: `radial`,
1438
1438
  `edge_shape` is `SDF_PERSIST` on `C_YUI_TREEDB_GRAPH`, a preference per
1439
1439
  treedb like `node_labels`. Consumer key: `elbow edges`.
1440
1440
 
1441
+ Two cases are not a channel (`7.23.150`, `treedb_elbow.js`, pure and
1442
+ tested). An elbow that does not run **forward** — the target above its
1443
+ source or in the same row, which one edge of every reciprocal pair is —
1444
+ goes **round** the two cards instead of through them: a short run out of its
1445
+ port, along the outside of both, and in from the other port's own side. And
1446
+ the edges joining the **same two cards**, either way round, take **lanes**
1447
+ 10px apart in the order they were made (0, +1, −1…), so a new one never
1448
+ moves the ones already drawn; a detour on a negative lane goes round the
1449
+ left. 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.
1451
+
1441
1452
  All three take the **same spanning tree**, chosen deterministically: roots are the
1442
1453
  nodes with no incoming edge, in node order; a node belongs to the **first
1443
1454
  parent that reaches it** in a breadth-first walk (the place, not the
@@ -29630,6 +29630,54 @@ function layout_radial(nodes, edges, opts) {
29630
29630
  }
29631
29631
  return pos;
29632
29632
  }
29633
+ /************************************************************
29634
+ * The lane of `id` among `ids`, the edges joining the same two
29635
+ * cards: 0, then +1, -1, +2, -2... in the order they were made.
29636
+ * Edge ids end in a counter, so a numeric sort is that order.
29637
+ ************************************************************/
29638
+ function elbow_lane(ids, id) {
29639
+ let k = ids.slice().sort((a, b) => String(a).localeCompare(String(b), void 0, { numeric: true })).indexOf(id);
29640
+ if (k <= 0) return 0;
29641
+ return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
29642
+ }
29643
+ function swap(p) {
29644
+ return [p[1], p[0]];
29645
+ }
29646
+ function swap_box(b) {
29647
+ return {
29648
+ x1: b.y1,
29649
+ y1: b.x1,
29650
+ x2: b.y2,
29651
+ y2: b.x2
29652
+ };
29653
+ }
29654
+ /************************************************************
29655
+ * The control points of an elbow from the port `s` to the port
29656
+ * `t` ([x, y] each), knowing the boxes of the two cards
29657
+ * ({x1, y1, x2, y2}) and the edge's lane.
29658
+ *
29659
+ * vertical the rows are stacked (top to bottom); false
29660
+ * when they are side by side (left to right)
29661
+ ************************************************************/
29662
+ function elbow_points(s, t, box_s, box_t, lane, vertical) {
29663
+ if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true).map(swap);
29664
+ let shift = lane * 10;
29665
+ if (t[1] - s[1] > 20) {
29666
+ let mid = (s[1] + t[1]) / 2 + shift;
29667
+ mid = Math.min(Math.max(mid, s[1] + 10), t[1] - 10);
29668
+ return [[s[0], mid], [t[0], mid]];
29669
+ }
29670
+ let out = 16 + Math.abs(shift);
29671
+ let side = lane < 0 ? Math.min(box_s.x1, box_t.x1) - 24 - Math.abs(shift) : Math.max(box_s.x2, box_t.x2) + 24 + Math.abs(shift);
29672
+ let y1 = s[1] + out;
29673
+ let y2 = t[1] - out;
29674
+ return [
29675
+ [s[0], y1],
29676
+ [side, y1],
29677
+ [side, y2],
29678
+ [t[0], y2]
29679
+ ];
29680
+ }
29633
29681
  //#endregion
29634
29682
  //#region node_modules/@babel/runtime/helpers/esm/typeof.js
29635
29683
  function _typeof(o) {
@@ -59009,14 +59057,47 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
59009
59057
  *
59010
59058
  * Computed on every draw from where the two ends ARE, so a
59011
59059
  * dragged card, a fold or a layout needs nothing else. `-v` runs
59012
- * between rows (top to bottom), `-h` between columns.
59060
+ * 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).
59013
59063
  ************************************************************/
59014
59064
  var ELBOW_RADIUS = 6;
59065
+ function elbow_box(node) {
59066
+ try {
59067
+ let b = node.getShape("key").getBounds();
59068
+ return {
59069
+ x1: b.min[0],
59070
+ y1: b.min[1],
59071
+ x2: b.max[0],
59072
+ y2: b.max[1]
59073
+ };
59074
+ } catch (e) {
59075
+ (0, _yuneta_gobj_js.log_error)(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
59076
+ let c = node.getCenter();
59077
+ return {
59078
+ x1: c[0],
59079
+ y1: c[1],
59080
+ x2: c[0],
59081
+ y2: c[1]
59082
+ };
59083
+ }
59084
+ }
59085
+ function elbow_lane_of(edge) {
59086
+ let graph = edge.context && edge.context.graph;
59087
+ let s = edge.sourceNode && edge.sourceNode.id;
59088
+ let t = edge.targetNode && edge.targetNode.id;
59089
+ if (!graph || s === void 0 || t === void 0 || s === t) return 0;
59090
+ try {
59091
+ return elbow_lane(graph.getRelatedEdgesData(s).filter((d) => d.source === s && d.target === t || d.source === t && d.target === s).map((d) => d.id), edge.id);
59092
+ } catch (e) {
59093
+ (0, _yuneta_gobj_js.log_error)(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
59094
+ return 0;
59095
+ }
59096
+ }
59015
59097
  var TreedbElbowV = class extends _antv_g6.Polyline {
59016
59098
  getControlPoints(attributes) {
59017
59099
  let [s, t] = this.getEndpoints(attributes, false);
59018
- let mid = (s[1] + t[1]) / 2;
59019
- return [[s[0], mid], [t[0], mid]];
59100
+ return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true);
59020
59101
  }
59021
59102
  getKeyPath(attributes) {
59022
59103
  return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
@@ -59025,8 +59106,7 @@ var TreedbElbowV = class extends _antv_g6.Polyline {
59025
59106
  var TreedbElbowH = class extends _antv_g6.Polyline {
59026
59107
  getControlPoints(attributes) {
59027
59108
  let [s, t] = this.getEndpoints(attributes, false);
59028
- let mid = (s[0] + t[0]) / 2;
59029
- return [[mid, s[1]], [mid, t[1]]];
59109
+ return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false);
59030
59110
  }
59031
59111
  getKeyPath(attributes) {
59032
59112
  return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
@@ -29625,6 +29625,54 @@ function layout_radial(nodes, edges, opts) {
29625
29625
  }
29626
29626
  return pos;
29627
29627
  }
29628
+ /************************************************************
29629
+ * The lane of `id` among `ids`, the edges joining the same two
29630
+ * cards: 0, then +1, -1, +2, -2... in the order they were made.
29631
+ * Edge ids end in a counter, so a numeric sort is that order.
29632
+ ************************************************************/
29633
+ function elbow_lane(ids, id) {
29634
+ let k = ids.slice().sort((a, b) => String(a).localeCompare(String(b), void 0, { numeric: true })).indexOf(id);
29635
+ if (k <= 0) return 0;
29636
+ return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
29637
+ }
29638
+ function swap(p) {
29639
+ return [p[1], p[0]];
29640
+ }
29641
+ function swap_box(b) {
29642
+ return {
29643
+ x1: b.y1,
29644
+ y1: b.x1,
29645
+ x2: b.y2,
29646
+ y2: b.x2
29647
+ };
29648
+ }
29649
+ /************************************************************
29650
+ * The control points of an elbow from the port `s` to the port
29651
+ * `t` ([x, y] each), knowing the boxes of the two cards
29652
+ * ({x1, y1, x2, y2}) and the edge's lane.
29653
+ *
29654
+ * vertical the rows are stacked (top to bottom); false
29655
+ * when they are side by side (left to right)
29656
+ ************************************************************/
29657
+ function elbow_points(s, t, box_s, box_t, lane, vertical) {
29658
+ if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true).map(swap);
29659
+ let shift = lane * 10;
29660
+ if (t[1] - s[1] > 20) {
29661
+ let mid = (s[1] + t[1]) / 2 + shift;
29662
+ mid = Math.min(Math.max(mid, s[1] + 10), t[1] - 10);
29663
+ return [[s[0], mid], [t[0], mid]];
29664
+ }
29665
+ let out = 16 + Math.abs(shift);
29666
+ let side = lane < 0 ? Math.min(box_s.x1, box_t.x1) - 24 - Math.abs(shift) : Math.max(box_s.x2, box_t.x2) + 24 + Math.abs(shift);
29667
+ let y1 = s[1] + out;
29668
+ let y2 = t[1] - out;
29669
+ return [
29670
+ [s[0], y1],
29671
+ [side, y1],
29672
+ [side, y2],
29673
+ [t[0], y2]
29674
+ ];
29675
+ }
29628
29676
  //#endregion
29629
29677
  //#region node_modules/@babel/runtime/helpers/esm/typeof.js
29630
29678
  function _typeof(o) {
@@ -59004,14 +59052,47 @@ var TreedbTreeLayout = class extends BaseLayout {
59004
59052
  *
59005
59053
  * Computed on every draw from where the two ends ARE, so a
59006
59054
  * dragged card, a fold or a layout needs nothing else. `-v` runs
59007
- * between rows (top to bottom), `-h` between columns.
59055
+ * 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).
59008
59058
  ************************************************************/
59009
59059
  var ELBOW_RADIUS = 6;
59060
+ function elbow_box(node) {
59061
+ try {
59062
+ let b = node.getShape("key").getBounds();
59063
+ return {
59064
+ x1: b.min[0],
59065
+ y1: b.min[1],
59066
+ x2: b.max[0],
59067
+ y2: b.max[1]
59068
+ };
59069
+ } catch (e) {
59070
+ log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
59071
+ let c = node.getCenter();
59072
+ return {
59073
+ x1: c[0],
59074
+ y1: c[1],
59075
+ x2: c[0],
59076
+ y2: c[1]
59077
+ };
59078
+ }
59079
+ }
59080
+ function elbow_lane_of(edge) {
59081
+ let graph = edge.context && edge.context.graph;
59082
+ let s = edge.sourceNode && edge.sourceNode.id;
59083
+ let t = edge.targetNode && edge.targetNode.id;
59084
+ if (!graph || s === void 0 || t === void 0 || s === t) return 0;
59085
+ try {
59086
+ return elbow_lane(graph.getRelatedEdgesData(s).filter((d) => d.source === s && d.target === t || d.source === t && d.target === s).map((d) => d.id), edge.id);
59087
+ } catch (e) {
59088
+ log_error(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
59089
+ return 0;
59090
+ }
59091
+ }
59010
59092
  var TreedbElbowV = class extends Polyline {
59011
59093
  getControlPoints(attributes) {
59012
59094
  let [s, t] = this.getEndpoints(attributes, false);
59013
- let mid = (s[1] + t[1]) / 2;
59014
- return [[s[0], mid], [t[0], mid]];
59095
+ return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true);
59015
59096
  }
59016
59097
  getKeyPath(attributes) {
59017
59098
  return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
@@ -59020,8 +59101,7 @@ var TreedbElbowV = class extends Polyline {
59020
59101
  var TreedbElbowH = class extends Polyline {
59021
59102
  getControlPoints(attributes) {
59022
59103
  let [s, t] = this.getEndpoints(attributes, false);
59023
- let mid = (s[0] + t[0]) / 2;
59024
- return [[mid, s[1]], [mid, t[1]]];
59104
+ return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false);
59025
59105
  }
59026
59106
  getKeyPath(attributes) {
59027
59107
  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.149",
3
+ "version": "7.23.150",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -133,6 +133,7 @@ import {
133
133
  fold_root_group_key,
134
134
  } from "./treedb_fold_model.js";
135
135
  import {layout_tree, layout_radial, layout_compact} from "./treedb_layout.js";
136
+ import {elbow_lane, elbow_points} from "./treedb_elbow.js";
136
137
 
137
138
  import {
138
139
  BaseLayout,
@@ -4221,16 +4222,53 @@ class TreedbTreeLayout extends BaseLayout
4221
4222
  *
4222
4223
  * Computed on every draw from where the two ends ARE, so a
4223
4224
  * dragged card, a fold or a layout needs nothing else. `-v` runs
4224
- * between rows (top to bottom), `-h` between columns.
4225
+ * between rows (top to bottom), `-h` between columns. An edge that
4226
+ * does not run forward goes round the two cards, and the edges
4227
+ * joining the same two cards take lanes (treedb_elbow.js).
4225
4228
  ************************************************************/
4226
4229
  const ELBOW_RADIUS = 6;
4227
4230
 
4231
+ /* The box of a card on the canvas, for a detour to go round it. */
4232
+ function elbow_box(node)
4233
+ {
4234
+ try {
4235
+ let b = node.getShape('key').getBounds();
4236
+ return {x1: b.min[0], y1: b.min[1], x2: b.max[0], y2: b.max[1]};
4237
+ } catch(e) {
4238
+ log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
4239
+ let c = node.getCenter();
4240
+ return {x1: c[0], y1: c[1], x2: c[0], y2: c[1]};
4241
+ }
4242
+ }
4243
+
4244
+ /* The lane of an edge among the edges joining the same two cards,
4245
+ * either way round (treedb_elbow.js). */
4246
+ function elbow_lane_of(edge)
4247
+ {
4248
+ let graph = edge.context && edge.context.graph;
4249
+ let s = edge.sourceNode && edge.sourceNode.id;
4250
+ let t = edge.targetNode && edge.targetNode.id;
4251
+ if(!graph || s === undefined || t === undefined || s === t) {
4252
+ return 0;
4253
+ }
4254
+ try {
4255
+ let ids = graph.getRelatedEdgesData(s)
4256
+ .filter((d) => (d.source === s && d.target === t) ||
4257
+ (d.source === t && d.target === s))
4258
+ .map((d) => d.id);
4259
+ return elbow_lane(ids, edge.id);
4260
+ } catch(e) {
4261
+ log_error(`elbow edge ${edge.id}: cannot read the edges of ${s}: ${e}`);
4262
+ return 0;
4263
+ }
4264
+ }
4265
+
4228
4266
  class TreedbElbowV extends Polyline
4229
4267
  {
4230
4268
  getControlPoints(attributes) {
4231
4269
  let [s, t] = this.getEndpoints(attributes, false);
4232
- let mid = (s[1] + t[1]) / 2;
4233
- return [[s[0], mid], [t[0], mid]];
4270
+ return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
4271
+ elbow_lane_of(this), true);
4234
4272
  }
4235
4273
 
4236
4274
  getKeyPath(attributes) {
@@ -4243,8 +4281,8 @@ class TreedbElbowH extends Polyline
4243
4281
  {
4244
4282
  getControlPoints(attributes) {
4245
4283
  let [s, t] = this.getEndpoints(attributes, false);
4246
- let mid = (s[0] + t[0]) / 2;
4247
- return [[mid, s[1]], [mid, t[1]]];
4284
+ return elbow_points(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
4285
+ elbow_lane_of(this), false);
4248
4286
  }
4249
4287
 
4250
4288
  getKeyPath(attributes) {
@@ -0,0 +1,102 @@
1
+ /***********************************************************************
2
+ * treedb_elbow.js
3
+ *
4
+ * Where an elbow edge of the treedb graph turns, as plain
5
+ * geometry: the two elbow edge types of c_g6_nodes_tree.js ask
6
+ * this module for their control points on every draw, and it
7
+ * can be tested without G6.
8
+ *
9
+ * An elbow is mxGraph's tree routing: straight out of the
10
+ * parent's port to the channel half way to the child, along it,
11
+ * and straight into the child's port. Two cases are not that:
12
+ *
13
+ * - An edge that does not run FORWARD: the child above its
14
+ * parent, or in the same row. One of the two edges of a
15
+ * reciprocal pair (A -> B and B -> A) is always one. A channel
16
+ * half way between would cross both cards and lie on top of
17
+ * its partner, so it goes ROUND: a short run out of its port,
18
+ * along the outside of both cards, and into the other port
19
+ * from that port's own side.
20
+ *
21
+ * - Several edges joining the SAME two cards: a reciprocal pair,
22
+ * or two hooks of one parent reaching one child. Each gets a
23
+ * LANE: the first stays where it is, the next moves one step to
24
+ * one side, the next one step to the other. The lane follows
25
+ * the order the edges were made, so a new edge never moves the
26
+ * ones already drawn.
27
+ *
28
+ * Written for the top-down reading; left to right is the same
29
+ * geometry with the axes swapped.
30
+ *
31
+ * Copyright (c) 2026, ArtGins.
32
+ * All Rights Reserved.
33
+ ***********************************************************************/
34
+
35
+ export const ELBOW_STEP = 10; /* between two lanes */
36
+ export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
37
+ export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
38
+
39
+ /************************************************************
40
+ * The lane of `id` among `ids`, the edges joining the same two
41
+ * cards: 0, then +1, -1, +2, -2... in the order they were made.
42
+ * Edge ids end in a counter, so a numeric sort is that order.
43
+ ************************************************************/
44
+ export function elbow_lane(ids, id)
45
+ {
46
+ let sorted = ids.slice().sort(
47
+ (a, b) => String(a).localeCompare(String(b), undefined, {numeric: true})
48
+ );
49
+ let k = sorted.indexOf(id);
50
+ if(k <= 0) {
51
+ return 0;
52
+ }
53
+ let side = (k % 2)? 1 : -1;
54
+ return side * Math.ceil(k / 2);
55
+ }
56
+
57
+ function swap(p)
58
+ {
59
+ return [p[1], p[0]];
60
+ }
61
+
62
+ function swap_box(b)
63
+ {
64
+ return {x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2};
65
+ }
66
+
67
+ /************************************************************
68
+ * The control points of an elbow from the port `s` to the port
69
+ * `t` ([x, y] each), knowing the boxes of the two cards
70
+ * ({x1, y1, x2, y2}) and the edge's lane.
71
+ *
72
+ * vertical the rows are stacked (top to bottom); false
73
+ * when they are side by side (left to right)
74
+ ************************************************************/
75
+ export function elbow_points(s, t, box_s, box_t, lane, vertical)
76
+ {
77
+ if(!vertical) {
78
+ return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true)
79
+ .map(swap);
80
+ }
81
+
82
+ let shift = lane * ELBOW_STEP;
83
+
84
+ /* Forward: the channel half way down, moved by the lane and
85
+ * kept strictly between the two ports. */
86
+ if(t[1] - s[1] > 2 * ELBOW_STEP) {
87
+ let mid = (s[1] + t[1]) / 2 + shift;
88
+ mid = Math.min(Math.max(mid, s[1] + ELBOW_STEP), t[1] - ELBOW_STEP);
89
+ return [[s[0], mid], [t[0], mid]];
90
+ }
91
+
92
+ /* Backwards, or across one row: round the outside. A negative
93
+ * lane goes round the left, any other the right, so the two
94
+ * detours of one pair never share a side. */
95
+ let out = ELBOW_JETTY + Math.abs(shift);
96
+ let side = (lane < 0)
97
+ ? Math.min(box_s.x1, box_t.x1) - ELBOW_CLEAR - Math.abs(shift)
98
+ : Math.max(box_s.x2, box_t.x2) + ELBOW_CLEAR + Math.abs(shift);
99
+ let y1 = s[1] + out; /* a hook port is on the bottom edge */
100
+ let y2 = t[1] - out; /* an fkey port is on the top edge */
101
+ return [[s[0], y1], [side, y1], [side, y2], [t[0], y2]];
102
+ }
@@ -0,0 +1,105 @@
1
+ /***********************************************************************
2
+ * treedb_elbow.test.js
3
+ *
4
+ * The elbow edges of the treedb graph, pinned: the lane of an
5
+ * edge among those joining the same two cards, the channel of a
6
+ * forward edge, the detour of one that is not.
7
+ ***********************************************************************/
8
+ import { describe, test, expect } from "vitest";
9
+ import { elbow_lane, elbow_points, ELBOW_STEP, ELBOW_CLEAR } from "./treedb_elbow.js";
10
+
11
+ /* Two cards 100x40: A on top, B one row below it. */
12
+ const A = {x1: 0, y1: 0, x2: 100, y2: 40};
13
+ const B = {x1: 0, y1: 130, x2: 100, y2: 170};
14
+ const A_HOOK = [50, 40]; /* A's bottom edge */
15
+ const A_FKEY = [50, 0]; /* A's top edge */
16
+ const B_HOOK = [60, 170];
17
+ const B_FKEY = [50, 130];
18
+
19
+ describe("the lane", () => {
20
+ test("one edge alone keeps the middle", () => {
21
+ expect(elbow_lane(["edge-t-7"], "edge-t-7")).toBe(0);
22
+ });
23
+
24
+ test("lanes follow the order the edges were made, not the string order", () => {
25
+ let ids = ["edge-t-10", "edge-t-9", "edge-t-11"];
26
+ expect(elbow_lane(ids, "edge-t-9")).toBe(0);
27
+ expect(elbow_lane(ids, "edge-t-10")).toBe(1);
28
+ expect(elbow_lane(ids, "edge-t-11")).toBe(-1);
29
+ });
30
+
31
+ test("a new edge does not move the ones already drawn", () => {
32
+ let before = ["edge-t-1", "edge-t-2"];
33
+ let after = before.concat(["edge-t-3"]);
34
+ for(let id of before) {
35
+ expect(elbow_lane(after, id)).toBe(elbow_lane(before, id));
36
+ }
37
+ });
38
+ });
39
+
40
+ describe("a forward edge", () => {
41
+ test("turns in the channel half way between the ports", () => {
42
+ let pts = elbow_points(A_HOOK, B_FKEY, A, B, 0, true);
43
+ expect(pts).toEqual([[50, 85], [50, 85]]);
44
+ });
45
+
46
+ test("its lane moves the channel, and keeps it between the ports", () => {
47
+ let pts = elbow_points(A_HOOK, B_FKEY, A, B, 1, true);
48
+ expect(pts[0][1]).toBe(85 + ELBOW_STEP);
49
+ let far = elbow_points(A_HOOK, B_FKEY, A, B, 20, true);
50
+ expect(far[0][1]).toBeLessThan(B_FKEY[1]);
51
+ expect(far[0][1]).toBeGreaterThan(A_HOOK[1]);
52
+ });
53
+ });
54
+
55
+ describe("an edge that does not run forward", () => {
56
+ test("goes round the outside of both cards", () => {
57
+ /* B -> A: out of B's bottom, back up into A's top. */
58
+ let pts = elbow_points(B_HOOK, A_FKEY, B, A, 0, true);
59
+ expect(pts.length).toBe(4);
60
+ let side = pts[1][0];
61
+ expect(side).toBeGreaterThanOrEqual(100 + ELBOW_CLEAR);
62
+ expect(pts[2][0]).toBe(side);
63
+ expect(pts[0][1]).toBeGreaterThan(B.y2); /* below B */
64
+ expect(pts[3][1]).toBeLessThan(A.y1); /* above A */
65
+ });
66
+
67
+ test("a reciprocal pair shares no segment", () => {
68
+ let forward = elbow_points(A_HOOK, B_FKEY, A, B, 0, true);
69
+ let back = elbow_points(B_HOOK, A_FKEY, B, A, 1, true);
70
+ let channel = forward[0][1];
71
+ expect(back.every((p) => p[1] !== channel)).toBe(true);
72
+ expect(back[1][0]).toBeGreaterThan(A.x2);
73
+ });
74
+
75
+ test("a negative lane goes round the other side", () => {
76
+ let right = elbow_points(B_HOOK, A_FKEY, B, A, 1, true);
77
+ let left = elbow_points(B_HOOK, A_FKEY, B, A, -1, true);
78
+ expect(right[1][0]).toBeGreaterThan(A.x2);
79
+ expect(left[1][0]).toBeLessThan(A.x1);
80
+ });
81
+
82
+ test("two cards in one row are not joined through them", () => {
83
+ /* C beside A, same row: A's hook (bottom) to C's fkey (top). */
84
+ let C = {x1: 200, y1: 0, x2: 300, y2: 40};
85
+ let pts = elbow_points(A_HOOK, [250, 0], A, C, 0, true);
86
+ expect(pts.length).toBe(4);
87
+ expect(pts[1][0]).toBeGreaterThan(C.x2);
88
+ });
89
+ });
90
+
91
+ describe("read left to right", () => {
92
+ test("is the same geometry with the axes swapped", () => {
93
+ let sw = (p) => [p[1], p[0]];
94
+ let swb = (b) => ({x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2});
95
+ for(let [s, t, bs, bt, lane] of [
96
+ [A_HOOK, B_FKEY, A, B, 0],
97
+ [A_HOOK, B_FKEY, A, B, 1],
98
+ [B_HOOK, A_FKEY, B, A, -1],
99
+ ]) {
100
+ let down = elbow_points(s, t, bs, bt, lane, true);
101
+ let right = elbow_points(sw(s), sw(t), swb(bs), swb(bt), lane, false);
102
+ expect(right).toEqual(down.map(sw));
103
+ }
104
+ });
105
+ });