@yuneta/gobj-ui 7.23.152 → 7.23.154

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.
@@ -25,6 +25,21 @@
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
+ *
37
+ * - A card in a lower row of a STACK (treedb_layout.js lays a
38
+ * run of leaf children out as two columns with a corridor
39
+ * between them): the card above it stands in the way of the
40
+ * channel, so its edge runs down the corridor in a lane of
41
+ * its own -- a COMB -- and turns into the gap above its card.
42
+ *
28
43
  * Written for the top-down reading; left to right is the same
29
44
  * geometry with the axes swapped.
30
45
  *
@@ -32,9 +47,12 @@
32
47
  * All Rights Reserved.
33
48
  ***********************************************************************/
34
49
 
50
+ import {STACK_GAP, STACK_LANE_PAD, STACK_LANE_STEP, STACK_TRUNK_MIN} from "./treedb_layout.js";
51
+
35
52
  export const ELBOW_STEP = 10; /* between two lanes */
36
53
  export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
37
54
  export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
55
+ export const ELBOW_STAGGER_PAD = 6; /* staggered turns keep this far from the rows */
38
56
 
39
57
  /************************************************************
40
58
  * The lane of `id` among `ids`, the edges joining the same two
@@ -54,6 +72,24 @@ export function elbow_lane(ids, id)
54
72
  return side * Math.ceil(k / 2);
55
73
  }
56
74
 
75
+ /************************************************************
76
+ * The rank of `id` among `items` ([{id, span}]): the forward
77
+ * edges that share one end with it, each with how far it runs
78
+ * ACROSS. Farthest first; a tie goes to the edge made first.
79
+ * Returns {rank, count}.
80
+ ************************************************************/
81
+ export function elbow_rank(items, id)
82
+ {
83
+ let sorted = items.slice().sort((a, b) => {
84
+ if(b.span !== a.span) {
85
+ return b.span - a.span;
86
+ }
87
+ return String(a.id).localeCompare(String(b.id), undefined, {numeric: true});
88
+ });
89
+ let rank = sorted.findIndex((it) => it.id === id);
90
+ return {rank: (rank < 0)? 0 : rank, count: sorted.length};
91
+ }
92
+
57
93
  function swap(p)
58
94
  {
59
95
  return [p[1], p[0]];
@@ -64,6 +100,154 @@ function swap_box(b)
64
100
  return {x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2};
65
101
  }
66
102
 
103
+ /* Where a forward edge turns between `lo` and `hi`: the middle, or
104
+ * its staggered place among the edges sharing one of its ends --
105
+ * leaving, the farthest highest; arriving, the farthest lowest. */
106
+ function staggered_turn(lo, hi, st)
107
+ {
108
+ let mid = (lo + hi) / 2;
109
+ let group = null;
110
+ let k = 0;
111
+ if(st.dep && st.dep.count > 1) {
112
+ group = st.dep;
113
+ k = st.dep.rank;
114
+ } else if(st.arr && st.arr.count > 1) {
115
+ group = st.arr;
116
+ k = st.arr.count - 1 - st.arr.rank;
117
+ }
118
+ if(group) {
119
+ let room = Math.max(0, hi - lo - 2 * ELBOW_STAGGER_PAD);
120
+ let step = Math.min(ELBOW_STEP, room / (group.count - 1));
121
+ mid = mid - (group.count - 1) * step / 2 + k * step;
122
+ }
123
+ return mid;
124
+ }
125
+
126
+ /************************************************************
127
+ * The combs of the stacks under one card.
128
+ *
129
+ * treedb_layout.js lays a run of leaf children out as two columns
130
+ * with a corridor between them. A card in the first row is reached
131
+ * from the channel like any other; one in a lower row cannot be --
132
+ * the card above it stands in the way -- so its edge runs down
133
+ * the corridor in a LANE of its own and turns into the gap above
134
+ * its card, which it shares only with the other card of its row,
135
+ * reached from the other side.
136
+ *
137
+ * targets [{id, box}]: the forward edges leaving one card,
138
+ * each with the box of the card it reaches
139
+ * vertical as in elbow_points; false swaps the boxes
140
+ *
141
+ * Returns Map id -> {lane, z, top}: the lane, the height of the
142
+ * gap it turns into, and the top of the stack's first row (where
143
+ * the channel over the stack ends). Only for a card that stands
144
+ * under another of the group, within a stack gap and lined up
145
+ * with it on the corridor side -- what a stack looks like.
146
+ *
147
+ * Lanes go outermost first, row by row: a line into a higher row
148
+ * keeps outside the lines going deeper, and none crosses another.
149
+ ************************************************************/
150
+ export function elbow_combs(targets, vertical)
151
+ {
152
+ let list = vertical? targets : targets.map((it) => ({id: it.id, box: swap_box(it.box)}));
153
+ let cy = (b) => (b.y1 + b.y2) / 2;
154
+ let near = (a, b) => Math.abs(a - b) < 1.5;
155
+
156
+ let under = [];
157
+ for(let it of list) {
158
+ let b = it.box;
159
+ let above = null;
160
+ for(let o of list) {
161
+ let a = o.box;
162
+ if(o === it || a.y2 > b.y1 || b.y1 - a.y2 > STACK_GAP) {
163
+ continue;
164
+ }
165
+ if(!(near(a.x2, b.x2) || near(a.x1, b.x1))) {
166
+ continue;
167
+ }
168
+ if(!above || a.y2 > above.y2) {
169
+ above = a;
170
+ }
171
+ }
172
+ if(!above) {
173
+ continue;
174
+ }
175
+ /* Its column: lined up on the corridor side, which is the
176
+ * right edge of the first column and the left edge of the
177
+ * second. Two cards of one width line up on both; then the
178
+ * card beside it in its row says. */
179
+ let side;
180
+ if(near(above.x2, b.x2) && !near(above.x1, b.x1)) {
181
+ side = 0;
182
+ } else if(near(above.x1, b.x1) && !near(above.x2, b.x2)) {
183
+ side = 1;
184
+ } else {
185
+ let left_of_it = list.some((o) => o !== it && near(cy(o.box), cy(b)) &&
186
+ o.box.x2 <= b.x1);
187
+ side = left_of_it? 1 : 0;
188
+ }
189
+ under.push({id: it.id, box: b, above: above, side: side});
190
+ }
191
+
192
+ /* The corridor of a card: from the first column's edge to the
193
+ * second's, read off any row of the stack that has both. */
194
+ let corridor = (u) => {
195
+ if(u.side === 0) {
196
+ let l = u.box.x2;
197
+ let col = list.filter((o) => near(o.box.x2, l));
198
+ let r = Infinity;
199
+ for(let c of col) {
200
+ for(let o of list) {
201
+ if(o !== c && near(cy(o.box), cy(c.box)) && o.box.x1 >= l - 0.5) {
202
+ r = Math.min(r, o.box.x1);
203
+ }
204
+ }
205
+ }
206
+ return {l: l, r: isFinite(r)? r : l + STACK_TRUNK_MIN, col: col};
207
+ }
208
+ let r = u.box.x1;
209
+ let col = list.filter((o) => near(o.box.x1, r));
210
+ let l = -Infinity;
211
+ for(let c of col) {
212
+ for(let o of list) {
213
+ if(o !== c && near(cy(o.box), cy(c.box)) && o.box.x2 <= r + 0.5) {
214
+ l = Math.max(l, o.box.x2);
215
+ }
216
+ }
217
+ }
218
+ return {l: isFinite(l)? l : r - STACK_TRUNK_MIN, r: r, col: col};
219
+ };
220
+
221
+ let groups = new Map();
222
+ for(let u of under) {
223
+ let c = corridor(u);
224
+ u.top = Math.min(...c.col.map((o) => o.box.y1));
225
+ let key = `${Math.round(c.l)}|${Math.round(c.r)}`;
226
+ if(!groups.has(key)) {
227
+ groups.set(key, {l: c.l, r: c.r, left: [], right: []});
228
+ }
229
+ groups.get(key)[(u.side === 0)? 'left' : 'right'].push(u);
230
+ }
231
+
232
+ let out = new Map();
233
+ for(let g of groups.values()) {
234
+ g.left.sort((a, b) => a.box.y1 - b.box.y1);
235
+ g.right.sort((a, b) => a.box.y1 - b.box.y1);
236
+ let n = g.left.length + g.right.length;
237
+ let room = g.r - g.l - 2 * STACK_LANE_PAD;
238
+ let step = (n > 1)? Math.max(0, Math.min(STACK_LANE_STEP, room / (n - 1))) : 0;
239
+ g.left.forEach((u, i) => {
240
+ out.set(u.id, {lane: g.l + STACK_LANE_PAD + i * step,
241
+ z: (u.above.y2 + u.box.y1) / 2, top: u.top});
242
+ });
243
+ g.right.forEach((u, j) => {
244
+ out.set(u.id, {lane: g.r - STACK_LANE_PAD - j * step,
245
+ z: (u.above.y2 + u.box.y1) / 2, top: u.top});
246
+ });
247
+ }
248
+ return out;
249
+ }
250
+
67
251
  /************************************************************
68
252
  * The control points of an elbow from the port `s` to the port
69
253
  * `t` ([x, y] each), knowing the boxes of the two cards
@@ -71,21 +255,39 @@ function swap_box(b)
71
255
  *
72
256
  * vertical the rows are stacked (top to bottom); false
73
257
  * when they are side by side (left to right)
258
+ * stagger {dep, arr, comb}: the edge's rank among the
259
+ * forward edges leaving its source card and among
260
+ * those reaching its target card (elbow_rank), and
261
+ * its comb when the target is in a lower row of a
262
+ * stack (elbow_combs). Absent, the channel is the
263
+ * middle.
74
264
  ************************************************************/
75
- export function elbow_points(s, t, box_s, box_t, lane, vertical)
265
+ export function elbow_points(s, t, box_s, box_t, lane, vertical, stagger)
76
266
  {
77
267
  if(!vertical) {
78
- return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true)
79
- .map(swap);
268
+ return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
269
+ stagger).map(swap);
80
270
  }
81
271
 
82
272
  let shift = lane * ELBOW_STEP;
83
273
 
84
- /* Forward: the channel half way down, moved by the lane and
85
- * kept strictly between the two ports. */
274
+ /* Forward: the channel half way between the two cards, each
275
+ * edge of a shared end at its own height round it, moved by the
276
+ * lane, and kept between the two rows. Into a lower row of a
277
+ * stack: along the channel to its corridor lane, down the lane,
278
+ * and into the gap above its card. */
86
279
  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);
280
+ let st = stagger || {};
281
+ let lo = Math.max(s[1], box_s.y2);
282
+ if(st.comb) {
283
+ let c = st.comb;
284
+ let turn = staggered_turn(lo, c.top, {dep: st.dep}) + shift;
285
+ turn = Math.min(Math.max(turn, lo + 4), c.top - 4);
286
+ return [[s[0], turn], [c.lane, turn], [c.lane, c.z], [t[0], c.z]];
287
+ }
288
+ let hi = Math.min(t[1], box_t.y1);
289
+ let mid = staggered_turn(lo, hi, st) + shift;
290
+ mid = Math.min(Math.max(mid, lo + 4), hi - 4);
89
291
  return [[s[0], mid], [t[0], mid]];
90
292
  }
91
293
 
@@ -215,8 +417,12 @@ function heap_pop(h)
215
417
  /************************************************************
216
418
  * The shortest orthogonal line from `a` to `z` that enters none
217
419
  * 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.
420
+ * ROUTE_BEND. The line leaves `a` SIDEWAYS and reaches `z`
421
+ * SIDEWAYS: the column straight under a port and the one straight
422
+ * over it belong to the forward edges that share that port, which
423
+ * run along them to their staggered turns -- a route going down
424
+ * that column lay on top of them. So a routed line shares only
425
+ * the stub between the port and `a` (or `z`).
220
426
  *
221
427
  * The grid is SPARSE: only the lines of the boxes' borders and
222
428
  * of the two ends, which is all an orthogonal shortest path ever
@@ -302,7 +508,10 @@ function orth_search(a, z, boxes)
302
508
  let k = state >> 2;
303
509
  let dir = state & 3;
304
510
  if(k === goal) {
305
- let total = cost + ((dir === 2)? 0 : ROUTE_BEND);
511
+ if(dir >= 2) {
512
+ continue; /* down the port's own column: not allowed */
513
+ }
514
+ let total = cost + ROUTE_BEND; /* the turn into the port */
306
515
  if(total < found_cost) {
307
516
  found_cost = total;
308
517
  found = state;
@@ -310,6 +519,9 @@ function orth_search(a, z, boxes)
310
519
  continue;
311
520
  }
312
521
  for(let [nk, nd, len] of moves(k)) {
522
+ if(k === start && nd >= 2) {
523
+ continue; /* out of the port sideways, not down its column */
524
+ }
313
525
  let c = cost + len + ((nd === dir)? 0 : ROUTE_BEND);
314
526
  let ns = nk * 4 + nd;
315
527
  if(c < best[ns]) {
@@ -360,15 +572,21 @@ function orth_search(a, z, boxes)
360
572
  * coincide. Nearby cards first; all of them if that finds no way;
361
573
  * the simple elbow if nothing does.
362
574
  ************************************************************/
363
- export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes)
575
+ export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger)
364
576
  {
365
577
  let others = boxes || [];
366
578
  if(!vertical) {
367
579
  return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
368
- others.map(swap_box)).map(swap);
580
+ others.map(swap_box), stagger).map(swap);
581
+ }
582
+
583
+ /* A comb runs where the layout left room for it: its corridor
584
+ * and its gap are empty by construction. */
585
+ if(stagger && stagger.comb) {
586
+ return elbow_points(s, t, box_s, box_t, lane, true, stagger);
369
587
  }
370
588
 
371
- let simple = elbow_points(s, t, box_s, box_t, lane, true);
589
+ let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
372
590
  if(!elbow_path_hits([s, ...simple, t], others, box_s, box_t)) {
373
591
  return simple;
374
592
  }
@@ -7,8 +7,10 @@
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_combs,
11
+ ELBOW_STEP, ELBOW_CLEAR,
11
12
  } from "./treedb_elbow.js";
13
+ import { layout_compact } from "./treedb_layout.js";
12
14
 
13
15
  /* Two cards 100x40: A on top, B one row below it. */
14
16
  const A = {x1: 0, y1: 0, x2: 100, y2: 40};
@@ -165,6 +167,195 @@ describe("the route round the other cards", () => {
165
167
  });
166
168
  });
167
169
 
170
+ describe("edges sharing an end are staggered", () => {
171
+ test("the rank is farthest first, a tie to the edge made first", () => {
172
+ let items = [{id: "e-3", span: 10}, {id: "e-1", span: 300}, {id: "e-2", span: 10}];
173
+ expect(elbow_rank(items, "e-1")).toEqual({rank: 0, count: 3});
174
+ expect(elbow_rank(items, "e-2")).toEqual({rank: 1, count: 3});
175
+ expect(elbow_rank(items, "e-3")).toEqual({rank: 2, count: 3});
176
+ });
177
+
178
+ /* One parent on top (port at x 50, bottom at 40) and a row of
179
+ * children below (tops at 130). */
180
+ const P = {x1: 0, y1: 0, x2: 100, y2: 40};
181
+ const PORT = [50, 40];
182
+ const KIDS = [50, 160, 290, 420, -120].map((x, i) => ({
183
+ id: `e-${i + 1}`, x: x, box: {x1: x - 50, y1: 130, x2: x + 50, y2: 170},
184
+ }));
185
+ const channel_of = (kid) => {
186
+ let items = KIDS.map((k) => ({id: k.id, span: Math.abs(k.x - PORT[0])}));
187
+ let dep = elbow_rank(items, kid.id);
188
+ return elbow_points(PORT, [kid.x, 130], P, kid.box, 0, true, {dep: dep})[0][1];
189
+ };
190
+
191
+ test("the children of one port turn each at its own height, inside the channel", () => {
192
+ let ys = KIDS.map(channel_of);
193
+ expect(new Set(ys).size).toBe(KIDS.length);
194
+ for(let y of ys) {
195
+ expect(y).toBeGreaterThan(40);
196
+ expect(y).toBeLessThan(130);
197
+ }
198
+ });
199
+
200
+ test("the farthest turns highest, and the lines of one port do not cross", () => {
201
+ let y = Object.fromEntries(KIDS.map((k) => [k.id, channel_of(k)]));
202
+ expect(y["e-4"]).toBeLessThan(y["e-2"]); /* 420 is farther than 160 */
203
+ /* A crossing: one edge's drop (at its child's x, from its turn
204
+ * down to the row) cut by another's run (from the port to its
205
+ * child, at its own turn). */
206
+ for(let a of KIDS) {
207
+ for(let b of KIDS) {
208
+ if(a === b) {
209
+ continue;
210
+ }
211
+ let lo = Math.min(PORT[0], b.x);
212
+ let hi = Math.max(PORT[0], b.x);
213
+ let cuts = a.x > lo && a.x < hi && y[b.id] > y[a.id] && y[b.id] < 130;
214
+ expect(cuts).toBe(false);
215
+ }
216
+ }
217
+ });
218
+
219
+ test("the parents of one card: the farthest turns lowest, and they do not cross", () => {
220
+ /* Parents on top at these x (port at the middle of each bottom),
221
+ * one child below at x 200 whose fkeys all enter at its top. */
222
+ const C = {x1: 150, y1: 130, x2: 250, y2: 170};
223
+ const T0 = [200, 130];
224
+ const PARENTS = [0, 120, 330, 560].map((x, i) => ({id: `p-${i + 1}`, x: x}));
225
+ let items = PARENTS.map((p) => ({id: p.id, span: Math.abs(p.x - T0[0])}));
226
+ let y = {};
227
+ for(let p of PARENTS) {
228
+ let box = {x1: p.x - 50, y1: 0, x2: p.x + 50, y2: 40};
229
+ let arr = elbow_rank(items, p.id);
230
+ y[p.id] = elbow_points([p.x, 40], T0, box, C, 0, true, {arr: arr})[0][1];
231
+ }
232
+ expect(new Set(Object.values(y)).size).toBe(PARENTS.length);
233
+ expect(y["p-4"]).toBeGreaterThan(y["p-2"]); /* 560 is farther than 120 */
234
+ /* A crossing: one parent's drop (at its x, from its row down to
235
+ * its turn) cut by another's run (from its x to the child). */
236
+ for(let a of PARENTS) {
237
+ for(let b of PARENTS) {
238
+ if(a === b) {
239
+ continue;
240
+ }
241
+ let lo = Math.min(b.x, T0[0]);
242
+ let hi = Math.max(b.x, T0[0]);
243
+ let cuts = a.x > lo && a.x < hi && y[b.id] > 40 && y[b.id] < y[a.id];
244
+ expect(cuts).toBe(false);
245
+ }
246
+ }
247
+ });
248
+
249
+ test("a routed edge leaves and reaches its ports sideways, sharing only the stub", () => {
250
+ /* A same-row edge from A to C, with D standing between them:
251
+ * it is routed. It must not run down the column over C's
252
+ * port, which is where a forward edge into C drops. */
253
+ const A2 = {x1: 0, y1: 200, x2: 100, y2: 240};
254
+ const D2 = {x1: 150, y1: 180, x2: 250, y2: 260};
255
+ const C2 = {x1: 300, y1: 200, x2: 400, y2: 240};
256
+ let s = [50, 240];
257
+ let t = [350, 200];
258
+ let boxes = [A2, D2, C2];
259
+ let route = elbow_route(s, t, A2, C2, 0, true, boxes);
260
+ expect(route.length).toBeGreaterThan(2);
261
+ expect(elbow_path_hits([s, ...route, t], boxes, A2, C2)).toBe(false);
262
+ let a = route[0];
263
+ let z = route[route.length - 1];
264
+ expect(route[1][1]).toBe(a[1]); /* leaves a sideways */
265
+ expect(route[route.length - 2][1]).toBe(z[1]); /* reaches z sideways */
266
+ expect(t[1] - z[1]).toBeLessThan(20); /* the stub is short */
267
+ });
268
+
269
+ test("an end shared by nobody keeps the middle", () => {
270
+ let one = {rank: 0, count: 1};
271
+ expect(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true, {dep: one, arr: one}))
272
+ .toEqual(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true));
273
+ });
274
+ });
275
+
276
+ describe("the comb of a stack", () => {
277
+ /* A parent over eight leaves, laid out by the compact tree (a
278
+ * stack: four rows of two). Boxes as the graph measures them,
279
+ * with 14px ports sticking 7px out of the top and bottom. */
280
+ const P = {id: "p", w: 172, h: 96};
281
+ const LEAVES = [0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({id: `l${i}`, w: 116, h: 40}));
282
+ const pos = layout_compact([P, ...LEAVES], LEAVES.map((l) => ({source: "p", target: l.id})),
283
+ {nodesep: 18, ranksep: 90});
284
+ const box = (n) => {
285
+ let c = pos.get(n.id);
286
+ return {x1: c.x - n.w / 2, y1: c.y - n.h / 2 - 7, x2: c.x + n.w / 2, y2: c.y + n.h / 2 + 7};
287
+ };
288
+ const BOX_P = box(P);
289
+ const PORT = [pos.get("p").x, pos.get("p").y + 48];
290
+ const targets = LEAVES.map((l, i) => ({id: `e-${i + 1}`, box: box(l), leaf: l}));
291
+ const combs = elbow_combs(targets.map((t) => ({id: t.id, box: t.box})), true);
292
+ const paths = targets.map((t) => {
293
+ let comb = combs.get(t.id) || null;
294
+ let items = targets.map((u) => {
295
+ let c = combs.get(u.id);
296
+ let far = c? c.lane : pos.get(u.leaf.id).x;
297
+ return {id: u.id, span: Math.abs(far - PORT[0])};
298
+ });
299
+ let end = [pos.get(t.leaf.id).x, t.box.y1 + 7];
300
+ let pts = elbow_points(PORT, end, BOX_P, t.box, 0, true,
301
+ {dep: elbow_rank(items, t.id), comb: comb});
302
+ return {id: t.id, box: t.box, full: [PORT, ...pts, end]};
303
+ });
304
+
305
+ test("only the cards under another card get a comb", () => {
306
+ expect(combs.size).toBe(6);
307
+ expect(combs.has("e-1")).toBe(false);
308
+ expect(combs.has("e-2")).toBe(false);
309
+ });
310
+
311
+ test("no line crosses a card", () => {
312
+ let all = [BOX_P, ...targets.map((t) => t.box)];
313
+ for(let p of paths) {
314
+ expect(elbow_path_hits(p.full, all, BOX_P, p.box)).toBe(false);
315
+ }
316
+ });
317
+
318
+ test("no two lines lie on each other past the stub of the port", () => {
319
+ /* Two axis-aligned segments overlap when they are on one line
320
+ * and share more than a point of it. */
321
+ let overlap = (a, b, c, d) => {
322
+ let va = a[0] === b[0];
323
+ let vc = c[0] === d[0];
324
+ let ha = a[1] === b[1];
325
+ let hc = c[1] === d[1];
326
+ if(va && vc && Math.abs(a[0] - c[0]) < 0.5) {
327
+ return Math.min(Math.max(a[1], b[1]), Math.max(c[1], d[1])) -
328
+ Math.max(Math.min(a[1], b[1]), Math.min(c[1], d[1])) > 0.5;
329
+ }
330
+ if(ha && hc && Math.abs(a[1] - c[1]) < 0.5) {
331
+ return Math.min(Math.max(a[0], b[0]), Math.max(c[0], d[0])) -
332
+ Math.max(Math.min(a[0], b[0]), Math.min(c[0], d[0])) > 0.5;
333
+ }
334
+ return false;
335
+ };
336
+ for(let i = 0; i < paths.length; i++) {
337
+ for(let j = i + 1; j < paths.length; j++) {
338
+ let A = paths[i].full;
339
+ let B = paths[j].full;
340
+ for(let m = 1; m + 1 < A.length; m++) {
341
+ for(let n = 1; n + 1 < B.length; n++) {
342
+ expect(overlap(A[m], A[m + 1], B[n], B[n + 1])).toBe(false);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ });
348
+
349
+ test("a comb is drawn as laid out, not routed", () => {
350
+ let t = targets[6];
351
+ let c = combs.get(t.id);
352
+ let end = [pos.get(t.leaf.id).x, t.box.y1 + 7];
353
+ let st = {comb: c};
354
+ expect(elbow_route(PORT, end, BOX_P, t.box, 0, true, [BOX_P, ...targets.map((u) => u.box)], st))
355
+ .toEqual(elbow_points(PORT, end, BOX_P, t.box, 0, true, st));
356
+ });
357
+ });
358
+
168
359
  describe("the ports are obstacles too", () => {
169
360
  /* Whole-node boxes: a port of 14px sticks 7px out of the bottom
170
361
  * and the top of each card. */