@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.
- package/README.md +5 -48
- package/dist/gobj-ui.cjs.js +21 -694
- package/dist/gobj-ui.es.js +22 -695
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +10 -148
- package/src/c_yui_treedb_graph.js +0 -2
- package/src/treedb_elbow.js +13 -344
- package/src/treedb_elbow.test.js +1 -236
- package/src/treedb_layout.js +0 -390
- package/src/treedb_layout.test.js +1 -174
package/src/treedb_elbow.test.js
CHANGED
|
@@ -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,
|
|
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. */
|
package/src/treedb_layout.js
CHANGED
|
@@ -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
|