@yuneta/gobj-ui 7.23.155 → 7.23.157

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.
@@ -7,10 +7,8 @@
7
7
  ***********************************************************************/
8
8
  import { describe, test, expect } from "vitest";
9
9
  import {
10
- elbow_lane, elbow_points, elbow_route, elbow_path_hits, elbow_rank, elbow_combs,
11
- curve_hits, rounded_path, ELBOW_STEP, ELBOW_CLEAR,
10
+ elbow_lane, elbow_points, elbow_route, elbow_path_hits, ELBOW_STEP, ELBOW_CLEAR,
12
11
  } from "./treedb_elbow.js";
13
- import { layout_compact } from "./treedb_layout.js";
14
12
 
15
13
  /* Two cards 100x40: A on top, B one row below it. */
16
14
  const A = {x1: 0, y1: 0, x2: 100, y2: 40};
@@ -167,239 +165,6 @@ describe("the route round the other cards", () => {
167
165
  });
168
166
  });
169
167
 
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
-
359
- describe("a curve that would cross a card", () => {
360
- /* Whole-node boxes (ports 7px out of top and bottom). */
361
- const PB = {x1: 0, y1: -7, x2: 172, y2: 103}; /* a parent */
362
- const R0 = {x1: 0, y1: 183, x2: 116, y2: 237}; /* row 0 of a stack */
363
- const R1 = {x1: 0, y1: 277, x2: 116, y2: 331}; /* row 1, under it */
364
- const SIB = {x1: 200, y1: -7, x2: 372, y2: 103}; /* the parent's sibling */
365
- const S = [86, 96];
366
-
367
- /* The cubic G6 draws between two vertical ports: tangents along
368
- * the line, as its default curveOffset gives. */
369
- const cubic = (s, t) => [["M", s[0], s[1]],
370
- ["C", s[0], (s[1] + t[1]) / 2, t[0], (s[1] + t[1]) / 2, t[0], t[1]]];
371
-
372
- test("a curve into a card of the next row crosses nothing", () => {
373
- expect(curve_hits(cubic(S, [58, 190]), [PB, R0, R1, SIB], PB, R0)).toBe(false);
374
- });
375
-
376
- test("a curve into a lower row of a stack crosses the card above it", () => {
377
- expect(curve_hits(cubic(S, [58, 284]), [PB, R0, R1, SIB], PB, R1)).toBe(true);
378
- });
379
-
380
- test("a curve that passes NEAR a card, beside its ports, does not count", () => {
381
- /* N0 in the target row with its port poking 7px up, beside
382
- * where the curve lands; the rectangle round the curve took it. */
383
- const T0 = {x1: 250, y1: 183, x2: 366, y2: 237};
384
- const N0 = {x1: 120, y1: 183, x2: 236, y2: 237};
385
- expect(curve_hits(cubic(S, [308, 190]), [PB, SIB, T0, N0], PB, T0)).toBe(false);
386
- });
387
-
388
- test("the rounded path: a straight line is one run", () => {
389
- expect(rounded_path([[0, 0], [0, 100]], 24)).toEqual([["M", 0, 0], ["L", 0, 100]]);
390
- });
391
-
392
- test("the rounded path: each corner a quadratic, never past half a short run", () => {
393
- let path = rounded_path([[0, 0], [0, 60], [100, 60], [100, 70]], 24);
394
- expect(path[0]).toEqual(["M", 0, 0]);
395
- expect(path[1]).toEqual(["L", 0, 36]); /* 24 before the corner */
396
- expect(path[2]).toEqual(["Q", 0, 60, 24, 60]);
397
- expect(path[3]).toEqual(["L", 95, 60]); /* the last run is 10: r = 5 */
398
- expect(path[4]).toEqual(["Q", 100, 60, 100, 65]);
399
- expect(path[5]).toEqual(["L", 100, 70]);
400
- });
401
- });
402
-
403
168
  describe("the ports are obstacles too", () => {
404
169
  /* Whole-node boxes: a port of 14px sticks 7px out of the bottom
405
170
  * and the top of each card. */
@@ -274,396 +274,6 @@ function tidy_tree_lr(nodes, edges, o)
274
274
  return pos;
275
275
  }
276
276
 
277
- /************************************************************
278
- * The compact tree.
279
- *
280
- * The tidy tree above gives every subtree its whole BLOCK: open a
281
- * hall with twenty-four devices and its block widens, and with it
282
- * every block above it, so the siblings of its ancestors move apart
283
- * at depths where nothing of theirs touches. This one keeps the
284
- * CONTOUR of each subtree instead -- its outline on each side, depth
285
- * by depth -- and slides every sibling against the outline of the
286
- * ones before it. A folded region then sits as close to an open one
287
- * as their cards allow at each depth, which is what a folded treedb
288
- * is: one branch open among closed ones.
289
- *
290
- * Same spanning tree, same order, same rows as the tidy tree: only
291
- * the packing across the depths differs, so switching between the
292
- * two moves cards sideways and never reorders them.
293
- *
294
- * And a run of three or more LEAF children -- a hall's devices, a
295
- * region's places when none is open -- is STACKED: two columns
296
- * under the parent with a corridor between them, row after row.
297
- * That is what makes it compact when a whole level is open, where
298
- * the contour alone gains nothing: every parent needed the whole
299
- * row of its children beside it. The tree grows taller instead of
300
- * wider. Its elbow edges run down the corridor (elbow_combs).
301
- *
302
- * The contour arithmetic is Moen's ("Drawing dynamic trees", IEEE
303
- * Software, 1990) in the form mxGraph gave it in mxCompactTreeLayout
304
- * -- Copyright (c) 2006-2015 JGraph Ltd, Apache License 2.0 -- read in
305
- * maxGraph's CompactTreeLayout.ts: leaf, join, merge, offset and
306
- * bridge below follow that form. What is ours: it runs on the
307
- * spanning tree of a FOREST, every depth is a row as wide as its
308
- * widest card (mxGraph's `alignRanks`), several roots sit side by
309
- * side, and there is no recursion.
310
- ************************************************************/
311
- export function layout_compact(nodes, edges, opts)
312
- {
313
- let o = Object.assign({}, DEFAULTS, opts || {});
314
- if(o.direction === "LR") {
315
- return compact_tree_lr(nodes, edges, o);
316
- }
317
- let transposed = nodes.map((n) => Object.assign({}, n, {w: n.h, h: n.w}));
318
- let lr = compact_tree_lr(transposed, edges, o);
319
- let pos = new Map();
320
- for(let [id, p] of lr) {
321
- pos.set(id, {x: p.y, y: p.x});
322
- }
323
- return pos;
324
- }
325
-
326
- /* One segment of a contour: a step of `dx` along the depth and `dy`
327
- * across it, then the next segment. */
328
- function contour_line(dx, dy, next)
329
- {
330
- return {dx: dx, dy: dy, next: next || null};
331
- }
332
-
333
- /* How far the segment (a1, a2) at (p1, p2) has to move across to
334
- * clear the segment (b1, b2) at the origin. */
335
- function contour_offset(p1, p2, a1, a2, b1, b2)
336
- {
337
- if(b1 <= p1 || p1 + a1 <= 0) {
338
- return 0;
339
- }
340
- let d;
341
- let t = b1 * a2 - a1 * b2;
342
- if(t > 0) {
343
- if(p1 < 0) {
344
- d = (p1 * a2) / a1 - p2;
345
- } else if(p1 > 0) {
346
- d = (p1 * b2) / b1 - p2;
347
- } else {
348
- d = -p2;
349
- }
350
- } else if(b1 < p1 + a1) {
351
- d = b2 - (p2 + ((b1 - p1) * a2) / a1);
352
- } else if(b1 > p1 + a1) {
353
- d = ((a1 + p1) * b2) / b1 - (p2 + a2);
354
- } else {
355
- d = b2 - (p2 + a2);
356
- }
357
- return (d > 0)? d : 0;
358
- }
359
-
360
- function contour_bridge(line1, x1, y1, line2, x2, y2)
361
- {
362
- let dx = x2 + line2.dx - x1;
363
- let dy = (line2.dx === 0)? line2.dy : (dx * line2.dy) / line2.dx;
364
- let r = contour_line(dx, dy, line2.next);
365
- line1.next = contour_line(0, y2 + line2.dy - dy - y1, r);
366
- return r;
367
- }
368
-
369
- /* Slide contour `p2` against `p1` until they clear; returns how far,
370
- * and leaves in `p1` the outline of the two together. */
371
- function contour_merge(p1, p2)
372
- {
373
- let x = 0;
374
- let y = 0;
375
- let total = 0;
376
- let upper = p1.lower_head;
377
- let lower = p2.upper_head;
378
-
379
- while(lower && upper) {
380
- let d = contour_offset(x, y, lower.dx, lower.dy, upper.dx, upper.dy);
381
- y += d;
382
- total += d;
383
- if(x + lower.dx <= upper.dx) {
384
- x += lower.dx;
385
- y += lower.dy;
386
- lower = lower.next;
387
- } else {
388
- x -= upper.dx;
389
- y -= upper.dy;
390
- upper = upper.next;
391
- }
392
- }
393
-
394
- if(lower) {
395
- let b = contour_bridge(p1.upper_tail, 0, 0, lower, x, y);
396
- p1.upper_tail = b.next? p2.upper_tail : b;
397
- p1.lower_tail = p2.lower_tail;
398
- } else {
399
- let b = contour_bridge(p2.lower_tail, x, y, upper, 0, 0);
400
- if(!b.next) {
401
- p1.lower_tail = b;
402
- }
403
- }
404
- p1.lower_head = p2.lower_head;
405
- return total;
406
- }
407
-
408
- /* How the compact tree STACKS a run of leaf children: two columns
409
- * under their parent, a CORRIDOR between them that the elbow edges
410
- * run down (treedb_elbow.js, elbow_combs), a gap between the rows
411
- * that they turn into. The corridor is sized here from how many
412
- * lines will run down it; the edges count the lanes from where the
413
- * cards stand. */
414
- export const STACK_MIN_RUN = 3; /* shorter runs stay in the row */
415
- export const STACK_GAP = 40; /* between two rows of a stack */
416
- export const STACK_LANE_PAD = 10; /* from a column to its first lane */
417
- export const STACK_LANE_STEP = 6; /* between two lanes */
418
- export const STACK_TRUNK_MIN = 36;
419
-
420
- /* The corridor of a stack of `k` cards: every card below the first
421
- * row gets a lane. */
422
- export function stack_trunk_width(k)
423
- {
424
- let lanes = Math.max(0, k - 2);
425
- return Math.max(STACK_TRUNK_MIN,
426
- 2 * STACK_LANE_PAD + Math.max(0, lanes - 1) * STACK_LANE_STEP);
427
- }
428
-
429
- /* A stack in the LR frame (depth along x, siblings along y): `side`
430
- * is how far each column reaches across, `starts`/`row_len` where
431
- * each row begins and how long it is along the depth. Its first row
432
- * IS the depth's column, so it lines up with every other card there. */
433
- function stack_geometry(t, st, col_x, col_w)
434
- {
435
- let d = st.depth;
436
- let k = st.ids.length;
437
- let rows = Math.ceil(k / 2);
438
- let side = [0, 0];
439
- let row_len = [];
440
- for(let i = 0; i < k; i++) {
441
- let n = t.by_id.get(st.ids[i]);
442
- let r = Math.floor(i / 2);
443
- side[i % 2] = Math.max(side[i % 2], n.h);
444
- row_len[r] = Math.max(row_len[r] || 0, n.w);
445
- }
446
- row_len[0] = col_w[d];
447
- let trunk = stack_trunk_width(k);
448
- let starts = [col_x[d]];
449
- for(let r = 1; r < rows; r++) {
450
- starts[r] = starts[r - 1] + row_len[r - 1] + STACK_GAP;
451
- }
452
- return {
453
- side: side,
454
- trunk: trunk,
455
- starts: starts,
456
- row_len: row_len,
457
- len: starts[rows - 1] + row_len[rows - 1] - col_x[d],
458
- ext: side[0] + trunk + side[1],
459
- };
460
- }
461
-
462
- /* The cards of a stack whose block starts across at `top`: row by
463
- * row, the first of each row against the corridor from one side,
464
- * the second against it from the other. */
465
- function place_stack(t, st, g, top, pos)
466
- {
467
- for(let i = 0; i < st.ids.length; i++) {
468
- let n = t.by_id.get(st.ids[i]);
469
- let r = Math.floor(i / 2);
470
- let across = (i % 2 === 0)
471
- ? top + g.side[0] - n.h / 2
472
- : top + g.side[0] + g.trunk + n.h / 2;
473
- pos.set(st.ids[i], {x: g.starts[r] + g.row_len[r] / 2, y: across});
474
- }
475
- }
476
-
477
- function compact_tree_lr(nodes, edges, o)
478
- {
479
- let t = spanning_tree(nodes, edges);
480
- let pos = new Map();
481
-
482
- /* A run of STACK_MIN_RUN or more consecutive LEAF children is laid
483
- * out as one block, a STACK. The contour packing below takes the
484
- * block as one leaf of its own length, so nothing is packed under
485
- * it. Runs, not all the leaves of a parent: the children keep
486
- * their order. A stack's key is an OBJECT, so it can never be
487
- * taken for a node id, which is a string. */
488
- let kids_of = new Map();
489
- let stacks = new Map();
490
- for(let [id, kids] of t.children) {
491
- let out = [];
492
- let run = [];
493
- let flush = () => {
494
- if(run.length >= STACK_MIN_RUN) {
495
- let key = {stack: stacks.size};
496
- stacks.set(key, {ids: run, depth: t.depth.get(run[0])});
497
- out.push(key);
498
- } else {
499
- out.push(...run);
500
- }
501
- run = [];
502
- };
503
- for(let kid of kids) {
504
- if((t.children.get(kid) || []).length === 0) {
505
- run.push(kid);
506
- } else {
507
- flush();
508
- out.push(kid);
509
- }
510
- }
511
- flush();
512
- kids_of.set(id, out);
513
- }
514
-
515
- /* Parents before children, a stack being one child. */
516
- let order = [];
517
- let todo = t.roots.slice().reverse();
518
- while(todo.length) {
519
- let id = todo.pop();
520
- order.push(id);
521
- let kids = kids_of.get(id) || [];
522
- for(let i = kids.length - 1; i >= 0; i--) {
523
- todo.push(kids[i]);
524
- }
525
- }
526
-
527
- /* Columns, as in the tidy tree: a depth is as wide as its widest
528
- * card, and a column's gap is `ranksep`. */
529
- let col_w = [];
530
- for(let [id, d] of t.depth) {
531
- col_w[d] = Math.max(col_w[d] || 0, t.by_id.get(id).w);
532
- }
533
- let col_x = [];
534
- let x = 0;
535
- for(let d = 0; d < col_w.length; d++) {
536
- col_x[d] = x;
537
- x += (col_w[d] || 0) + o.ranksep;
538
- }
539
-
540
- /* mxGraph's nodeDistance is padding on EACH side of a card, so two
541
- * neighbours end `nodesep` apart; its levelDistance is what is
542
- * left of the column gap after that padding. */
543
- let pad = o.nodesep / 2;
544
- let dist = o.nodesep;
545
- let level_distance = o.ranksep - pad;
546
-
547
- /* `len` along the depth (the column's width: the rows are aligned;
548
- * a stack's own length), `ext` across it. */
549
- let tn = new Map();
550
- for(let id of order) {
551
- let st = stacks.get(id);
552
- if(st) {
553
- let g = stack_geometry(t, st, col_x, col_w);
554
- tn.set(id, {len: g.len, ext: g.ext, geom: g, off_x: 0, off_y: 0, y: 0, contour: null});
555
- } else {
556
- tn.set(id, {
557
- len: col_w[t.depth.get(id)],
558
- ext: t.by_id.get(id).h,
559
- off_x: 0,
560
- off_y: 0,
561
- y: 0,
562
- contour: null,
563
- });
564
- }
565
- }
566
-
567
- /* Contours, children before parents. */
568
- for(let i = order.length - 1; i >= 0; i--) {
569
- let id = order[i];
570
- let n = tn.get(id);
571
- let kids = kids_of.get(id) || [];
572
-
573
- if(!kids.length) {
574
- let upper = contour_line(n.len + dist, 0);
575
- let lower_tail = contour_line(0, -n.ext - dist);
576
- n.contour = {
577
- upper_head: upper,
578
- upper_tail: upper,
579
- lower_tail: lower_tail,
580
- lower_head: contour_line(n.len + dist, 0, lower_tail),
581
- };
582
- continue;
583
- }
584
-
585
- /* join: the children side by side, each slid against the
586
- * outline of the ones before it. */
587
- let first = tn.get(kids[0]);
588
- n.contour = first.contour;
589
- let h = first.ext + dist;
590
- let span = h;
591
- for(let k = 1; k < kids.length; k++) {
592
- let kid = tn.get(kids[k]);
593
- let d = contour_merge(n.contour, kid.contour);
594
- kid.off_y = d + h;
595
- kid.off_x = 0;
596
- h = kid.ext + dist;
597
- span += d + h;
598
- }
599
-
600
- /* attach: the parent centred across the span of its children,
601
- * one column before them. */
602
- let gap = pad + level_distance;
603
- let y2 = (span - n.ext) / 2 - pad;
604
- let y1 = y2 + n.ext + 2 * pad - span;
605
- first.off_x = gap + n.len;
606
- first.off_y = y1;
607
- n.contour.upper_head = contour_line(n.len, 0,
608
- contour_line(gap, y1, n.contour.upper_head));
609
- n.contour.lower_head = contour_line(n.len, 0,
610
- contour_line(gap, y2, n.contour.lower_head));
611
- }
612
-
613
- /* Positions across, parents before children: the first child at
614
- * its offset from the parent, each next one at its offset from
615
- * the one before. */
616
- let root_of = new Map();
617
- for(let id of order) {
618
- let n = tn.get(id);
619
- if(!root_of.has(id)) {
620
- root_of.set(id, id);
621
- }
622
- let kids = kids_of.get(id) || [];
623
- if(!kids.length) {
624
- continue;
625
- }
626
- let running = n.y + tn.get(kids[0]).off_y;
627
- tn.get(kids[0]).y = running;
628
- root_of.set(kids[0], root_of.get(id));
629
- for(let k = 1; k < kids.length; k++) {
630
- let kid = tn.get(kids[k]);
631
- kid.y = running + kid.off_y;
632
- running = kid.y;
633
- root_of.set(kids[k], root_of.get(id));
634
- }
635
- }
636
-
637
- /* Several roots: each tree after the one before, by its extent. */
638
- let lo = new Map();
639
- let hi = new Map();
640
- for(let id of order) {
641
- let r = root_of.get(id);
642
- let n = tn.get(id);
643
- lo.set(r, Math.min(lo.has(r)? lo.get(r) : Infinity, n.y));
644
- hi.set(r, Math.max(hi.has(r)? hi.get(r) : -Infinity, n.y + n.ext));
645
- }
646
- let shift = new Map();
647
- let top = 0;
648
- for(let r of t.roots) {
649
- shift.set(r, top - lo.get(r));
650
- top += hi.get(r) - lo.get(r) + o.nodesep * 2;
651
- }
652
-
653
- for(let id of order) {
654
- let n = tn.get(id);
655
- let y = n.y + shift.get(root_of.get(id));
656
- let st = stacks.get(id);
657
- if(st) {
658
- place_stack(t, st, n.geom, y, pos);
659
- continue;
660
- }
661
- let d = t.depth.get(id);
662
- pos.set(id, {x: col_x[d] + col_w[d] / 2, y: y + n.ext / 2});
663
- }
664
- return pos;
665
- }
666
-
667
277
  /* How much of a card lies along the direction `a`: the projection
668
278
  * of its rectangle on that direction. Full width when the card is
669
279
  * read along its width, full height across it, and the honest