@yuneta/gobj-ui 7.23.154 → 7.23.156
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 +21 -3
- package/dist/gobj-ui.cjs.js +365 -72
- package/dist/gobj-ui.es.js +366 -73
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +61 -5
- package/src/treedb_elbow.js +113 -0
- package/src/treedb_elbow.test.js +45 -1
- package/src/treedb_layout.js +274 -83
- package/src/treedb_layout.test.js +95 -2
package/src/treedb_elbow.js
CHANGED
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
* channel, so its edge runs down the corridor in a lane of
|
|
41
41
|
* its own -- a COMB -- and turns into the gap above its card.
|
|
42
42
|
*
|
|
43
|
+
* And for CURVED edges (curve_hits, rounded_path): a curve
|
|
44
|
+
* that would cross another card follows the same way an elbow
|
|
45
|
+
* would, with round corners, so it reads as a curve.
|
|
46
|
+
*
|
|
43
47
|
* Written for the top-down reading; left to right is the same
|
|
44
48
|
* geometry with the axes swapped.
|
|
45
49
|
*
|
|
@@ -609,3 +613,112 @@ export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger)
|
|
|
609
613
|
}
|
|
610
614
|
return route || simple;
|
|
611
615
|
}
|
|
616
|
+
|
|
617
|
+
/*----------------------------------------------------------------*
|
|
618
|
+
* CURVED edges that would cross a card
|
|
619
|
+
*----------------------------------------------------------------*/
|
|
620
|
+
export const CURVE_RADIUS = 24; /* the corners of a curve that goes round */
|
|
621
|
+
export const CURVE_STEPS = 16; /* samples of each curved stretch */
|
|
622
|
+
|
|
623
|
+
/* Points along a G6 path: the ends of its runs and CURVE_STEPS
|
|
624
|
+
* samples of each quadratic or cubic stretch. */
|
|
625
|
+
function path_points(path)
|
|
626
|
+
{
|
|
627
|
+
let pts = [];
|
|
628
|
+
let cur = null;
|
|
629
|
+
for(let c of path) {
|
|
630
|
+
if(c[0] === 'M' || c[0] === 'L') {
|
|
631
|
+
cur = [c[1], c[2]];
|
|
632
|
+
pts.push(cur);
|
|
633
|
+
} else if(c[0] === 'C' && cur) {
|
|
634
|
+
let [x0, y0] = cur;
|
|
635
|
+
for(let k = 1; k <= CURVE_STEPS; k++) {
|
|
636
|
+
let u = k / CURVE_STEPS;
|
|
637
|
+
let v = 1 - u;
|
|
638
|
+
pts.push([
|
|
639
|
+
v * v * v * x0 + 3 * v * v * u * c[1] + 3 * v * u * u * c[3] + u * u * u * c[5],
|
|
640
|
+
v * v * v * y0 + 3 * v * v * u * c[2] + 3 * v * u * u * c[4] + u * u * u * c[6],
|
|
641
|
+
]);
|
|
642
|
+
}
|
|
643
|
+
cur = [c[5], c[6]];
|
|
644
|
+
} else if(c[0] === 'Q' && cur) {
|
|
645
|
+
let [x0, y0] = cur;
|
|
646
|
+
for(let k = 1; k <= CURVE_STEPS; k++) {
|
|
647
|
+
let u = k / CURVE_STEPS;
|
|
648
|
+
let v = 1 - u;
|
|
649
|
+
pts.push([
|
|
650
|
+
v * v * x0 + 2 * v * u * c[1] + u * u * c[3],
|
|
651
|
+
v * v * y0 + 2 * v * u * c[2] + u * u * c[4],
|
|
652
|
+
]);
|
|
653
|
+
}
|
|
654
|
+
cur = [c[3], c[4]];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return pts;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/************************************************************
|
|
661
|
+
* Does the G6 path `path` of a curved edge cross a card other than
|
|
662
|
+
* its own two? Measured on the curve itself -- the path G6 would
|
|
663
|
+
* draw, sampled -- and not on the rectangle round it, which took
|
|
664
|
+
* every card the curve merely passed near: the ports of a row
|
|
665
|
+
* stick well into the space above it. Its own two cards are left
|
|
666
|
+
* out: the curve starts and ends on their ports.
|
|
667
|
+
************************************************************/
|
|
668
|
+
export function curve_hits(path, boxes, box_s, box_t)
|
|
669
|
+
{
|
|
670
|
+
let pts = path_points(path);
|
|
671
|
+
if(pts.length < 2) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
let bb = {x1: Infinity, y1: Infinity, x2: -Infinity, y2: -Infinity};
|
|
675
|
+
for(let p of pts) {
|
|
676
|
+
bb.x1 = Math.min(bb.x1, p[0]);
|
|
677
|
+
bb.y1 = Math.min(bb.y1, p[1]);
|
|
678
|
+
bb.x2 = Math.max(bb.x2, p[0]);
|
|
679
|
+
bb.y2 = Math.max(bb.y2, p[1]);
|
|
680
|
+
}
|
|
681
|
+
for(let b of (boxes || [])) {
|
|
682
|
+
if(same_box(b, box_s) || same_box(b, box_t) || !box_overlaps(b, box_expand(bb, 1))) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
let inner = box_expand(b, -1);
|
|
686
|
+
for(let k = 0; k + 1 < pts.length; k++) {
|
|
687
|
+
if(segment_hits(pts[k], pts[k + 1], inner)) {
|
|
688
|
+
return true;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/************************************************************
|
|
696
|
+
* A G6 path (['M'|'L'|'Q', ...]) along `points` with every corner
|
|
697
|
+
* rounded -- by `radius`, or by half the shorter of its two
|
|
698
|
+
* segments, so a short run is never overshot. The rounding is the
|
|
699
|
+
* quadratic G6's own polyline uses.
|
|
700
|
+
************************************************************/
|
|
701
|
+
export function rounded_path(points, radius)
|
|
702
|
+
{
|
|
703
|
+
let path = [['M', points[0][0], points[0][1]]];
|
|
704
|
+
for(let i = 1; i + 1 < points.length; i++) {
|
|
705
|
+
let a = points[i - 1];
|
|
706
|
+
let m = points[i];
|
|
707
|
+
let b = points[i + 1];
|
|
708
|
+
let len_in = Math.hypot(m[0] - a[0], m[1] - a[1]);
|
|
709
|
+
let len_out = Math.hypot(b[0] - m[0], b[1] - m[1]);
|
|
710
|
+
let r = Math.min(radius, len_in / 2, len_out / 2);
|
|
711
|
+
let collinear = Math.abs((m[0] - a[0]) * (b[1] - m[1]) - (m[1] - a[1]) * (b[0] - m[0])) < 1e-9;
|
|
712
|
+
if(r <= 0 || collinear) {
|
|
713
|
+
path.push(['L', m[0], m[1]]);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
let p1 = [m[0] - (m[0] - a[0]) / len_in * r, m[1] - (m[1] - a[1]) / len_in * r];
|
|
717
|
+
let p2 = [m[0] + (b[0] - m[0]) / len_out * r, m[1] + (b[1] - m[1]) / len_out * r];
|
|
718
|
+
path.push(['L', p1[0], p1[1]]);
|
|
719
|
+
path.push(['Q', m[0], m[1], p2[0], p2[1]]);
|
|
720
|
+
}
|
|
721
|
+
let last = points[points.length - 1];
|
|
722
|
+
path.push(['L', last[0], last[1]]);
|
|
723
|
+
return path;
|
|
724
|
+
}
|
package/src/treedb_elbow.test.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { describe, test, expect } from "vitest";
|
|
9
9
|
import {
|
|
10
10
|
elbow_lane, elbow_points, elbow_route, elbow_path_hits, elbow_rank, elbow_combs,
|
|
11
|
-
ELBOW_STEP, ELBOW_CLEAR,
|
|
11
|
+
curve_hits, rounded_path, ELBOW_STEP, ELBOW_CLEAR,
|
|
12
12
|
} from "./treedb_elbow.js";
|
|
13
13
|
import { layout_compact } from "./treedb_layout.js";
|
|
14
14
|
|
|
@@ -356,6 +356,50 @@ describe("the comb of a stack", () => {
|
|
|
356
356
|
});
|
|
357
357
|
});
|
|
358
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
|
+
|
|
359
403
|
describe("the ports are obstacles too", () => {
|
|
360
404
|
/* Whole-node boxes: a port of 14px sticks 7px out of the bottom
|
|
361
405
|
* and the top of each card. */
|
package/src/treedb_layout.js
CHANGED
|
@@ -426,6 +426,63 @@ export function stack_trunk_width(k)
|
|
|
426
426
|
2 * STACK_LANE_PAD + Math.max(0, lanes - 1) * STACK_LANE_STEP);
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
/************************************************************
|
|
430
|
+
* The children of every node, with each run of STACK_MIN_RUN or
|
|
431
|
+
* more consecutive LEAF children replaced by one STACK. Runs, not
|
|
432
|
+
* all the leaves of a parent: the children keep their order. A
|
|
433
|
+
* stack's key is an OBJECT, so it can never be taken for a node id,
|
|
434
|
+
* which is a string. Shared by the compact tree and the radial.
|
|
435
|
+
*
|
|
436
|
+
* Returns {kids_of: Map<id, [id | stack]>, stacks: Map<stack, {ids, depth}>}
|
|
437
|
+
************************************************************/
|
|
438
|
+
function leaf_runs(t)
|
|
439
|
+
{
|
|
440
|
+
let kids_of = new Map();
|
|
441
|
+
let stacks = new Map();
|
|
442
|
+
for(let [id, kids] of t.children) {
|
|
443
|
+
let out = [];
|
|
444
|
+
let run = [];
|
|
445
|
+
let flush = () => {
|
|
446
|
+
if(run.length >= STACK_MIN_RUN) {
|
|
447
|
+
let key = {stack: stacks.size};
|
|
448
|
+
stacks.set(key, {ids: run, depth: t.depth.get(run[0])});
|
|
449
|
+
out.push(key);
|
|
450
|
+
} else {
|
|
451
|
+
out.push(...run);
|
|
452
|
+
}
|
|
453
|
+
run = [];
|
|
454
|
+
};
|
|
455
|
+
for(let kid of kids) {
|
|
456
|
+
if((t.children.get(kid) || []).length === 0) {
|
|
457
|
+
run.push(kid);
|
|
458
|
+
} else {
|
|
459
|
+
flush();
|
|
460
|
+
out.push(kid);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
flush();
|
|
464
|
+
kids_of.set(id, out);
|
|
465
|
+
}
|
|
466
|
+
return {kids_of: kids_of, stacks: stacks};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* Parents before children, a stack being one child (walk_order's
|
|
470
|
+
* form, over `kids_of`). */
|
|
471
|
+
function stacked_order(t, kids_of)
|
|
472
|
+
{
|
|
473
|
+
let order = [];
|
|
474
|
+
let todo = t.roots.slice().reverse();
|
|
475
|
+
while(todo.length) {
|
|
476
|
+
let id = todo.pop();
|
|
477
|
+
order.push(id);
|
|
478
|
+
let kids = kids_of.get(id) || [];
|
|
479
|
+
for(let i = kids.length - 1; i >= 0; i--) {
|
|
480
|
+
todo.push(kids[i]);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return order;
|
|
484
|
+
}
|
|
485
|
+
|
|
429
486
|
/* A stack in the LR frame (depth along x, siblings along y): `side`
|
|
430
487
|
* is how far each column reaches across, `starts`/`row_len` where
|
|
431
488
|
* each row begins and how long it is along the depth. Its first row
|
|
@@ -479,50 +536,11 @@ function compact_tree_lr(nodes, edges, o)
|
|
|
479
536
|
let t = spanning_tree(nodes, edges);
|
|
480
537
|
let pos = new Map();
|
|
481
538
|
|
|
482
|
-
/*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
}
|
|
539
|
+
/* Runs of leaf children become STACKS (leaf_runs); the contour
|
|
540
|
+
* packing below takes a stack as one leaf of its own length, so
|
|
541
|
+
* nothing is packed under it. */
|
|
542
|
+
let {kids_of, stacks} = leaf_runs(t);
|
|
543
|
+
let order = stacked_order(t, kids_of);
|
|
526
544
|
|
|
527
545
|
/* Columns, as in the tidy tree: a depth is as wide as its widest
|
|
528
546
|
* card, and a column's gap is `ranksep`. */
|
|
@@ -693,76 +711,148 @@ function extent_at(n, a)
|
|
|
693
711
|
*
|
|
694
712
|
* ranksep the step between rings
|
|
695
713
|
* nodesep the gap between neighbours on a ring
|
|
714
|
+
* stack (default true) a run of three or more LEAF children
|
|
715
|
+
* is a STACK, as in the compact tree: it asks its
|
|
716
|
+
* ring for one row of about the square root of its
|
|
717
|
+
* cards, not for every leaf -- a hall with a hundred
|
|
718
|
+
* devices no longer blows the ring up -- and lays
|
|
719
|
+
* them in rows going OUTWARD, each row as full as
|
|
720
|
+
* the room it has: rings round the parent when it is
|
|
721
|
+
* alone, a fan inside its sector when it is not.
|
|
722
|
+
* false keeps every leaf on the ring.
|
|
696
723
|
************************************************************/
|
|
697
724
|
export function layout_radial(nodes, edges, opts)
|
|
698
725
|
{
|
|
699
|
-
let o = Object.assign({}, DEFAULTS, opts || {});
|
|
726
|
+
let o = Object.assign({}, DEFAULTS, {stack: true}, opts || {});
|
|
700
727
|
let t = spanning_tree(nodes, edges);
|
|
701
728
|
let pos = new Map();
|
|
702
729
|
if(!t.roots.length) {
|
|
703
730
|
return pos;
|
|
704
731
|
}
|
|
705
732
|
|
|
706
|
-
|
|
707
|
-
let
|
|
708
|
-
let
|
|
733
|
+
let runs = o.stack? leaf_runs(t) : {kids_of: t.children, stacks: new Map()};
|
|
734
|
+
let kids_of = runs.kids_of;
|
|
735
|
+
let stacks = runs.stacks;
|
|
736
|
+
let order = stacked_order(t, kids_of);
|
|
737
|
+
|
|
738
|
+
/* A stack asks its ring for one row of about the square root of
|
|
739
|
+
* its cards; how many each row really holds is decided once the
|
|
740
|
+
* ring's radius is known (pass 2), by the room it has there. */
|
|
741
|
+
for(let st of stacks.values()) {
|
|
742
|
+
st.per_row = Math.ceil(Math.sqrt(st.ids.length));
|
|
743
|
+
st.cards = st.ids.map((id) => t.by_id.get(id));
|
|
744
|
+
}
|
|
745
|
+
/* A stack across its ring, at angle `a`: a card's width there, and
|
|
746
|
+
* the row it asks for. Along the radius: how far one row reaches. */
|
|
747
|
+
let stack_across = (st, a) => {
|
|
748
|
+
let w = 0;
|
|
749
|
+
for(let c of st.cards) {
|
|
750
|
+
w = Math.max(w, extent_at(c, a + Math.PI / 2));
|
|
751
|
+
}
|
|
752
|
+
return {card: w, row: st.per_row * w + (st.per_row - 1) * o.nodesep};
|
|
753
|
+
};
|
|
754
|
+
let stack_along = (st, a) => {
|
|
755
|
+
let e = 0;
|
|
756
|
+
for(let c of st.cards) {
|
|
757
|
+
e = Math.max(e, extent_at(c, a));
|
|
758
|
+
}
|
|
759
|
+
return e;
|
|
760
|
+
};
|
|
761
|
+
/* The widest a set of cards is along the direction `th`, over
|
|
762
|
+
* the arc a-half..a+half: a stack's cards stand anywhere in its
|
|
763
|
+
* sector, and a card is wider slanted than square. */
|
|
764
|
+
let widest_along = (cards, a, half) => {
|
|
765
|
+
let e = 0;
|
|
766
|
+
for(let k = 0; k <= 16; k++) {
|
|
767
|
+
let th = a - half + 2 * half * k / 16;
|
|
768
|
+
for(let c of cards) {
|
|
769
|
+
e = Math.max(e, extent_at(c, th));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return e;
|
|
773
|
+
};
|
|
774
|
+
let depth_of = (id) => stacks.has(id)? stacks.get(id).depth : t.depth.get(id);
|
|
775
|
+
let circ = (a, b) => {
|
|
776
|
+
let d = Math.abs(a - b) % (2 * Math.PI);
|
|
777
|
+
return (d > Math.PI)? 2 * Math.PI - d : d;
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
/* What each node needs across its ring, the sector it deserves: a
|
|
781
|
+
* card is one, a stack is one ROW of cards, a parent the sum of
|
|
782
|
+
* its children. Post-order. */
|
|
783
|
+
let weight = new Map();
|
|
709
784
|
for(let i = order.length - 1; i >= 0; i--) {
|
|
710
785
|
let id = order[i];
|
|
711
|
-
let
|
|
786
|
+
let st = stacks.get(id);
|
|
787
|
+
if(st) {
|
|
788
|
+
weight.set(id, st.per_row);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
let kids = kids_of.get(id) || [];
|
|
712
792
|
if(!kids.length) {
|
|
713
|
-
|
|
793
|
+
weight.set(id, 1);
|
|
714
794
|
continue;
|
|
715
795
|
}
|
|
716
796
|
let n = 0;
|
|
717
797
|
for(let kid of kids) {
|
|
718
|
-
n +=
|
|
798
|
+
n += weight.get(kid);
|
|
719
799
|
}
|
|
720
|
-
|
|
800
|
+
weight.set(id, n);
|
|
721
801
|
}
|
|
722
802
|
let total = 0;
|
|
723
803
|
for(let root of t.roots) {
|
|
724
|
-
total +=
|
|
804
|
+
total += weight.get(root);
|
|
725
805
|
}
|
|
726
806
|
|
|
727
807
|
/* One root sits at the centre; several sit on the first ring
|
|
728
808
|
* around an empty centre, so every depth shifts out by one. */
|
|
729
809
|
let single = (t.roots.length === 1);
|
|
730
|
-
let ring_of = (id) =>
|
|
810
|
+
let ring_of = (id) => depth_of(id) + (single? 0 : 1);
|
|
731
811
|
|
|
732
812
|
/* Pass 1, the ANGLES: a node's children split its sector by
|
|
733
|
-
* their
|
|
813
|
+
* their weight, and a node sits at the middle of its sector.
|
|
734
814
|
* Angles do not depend on the radii, so they come first. */
|
|
735
815
|
let angle = new Map();
|
|
736
|
-
let rings = []; /* ring -> [{id, angle, diag}] */
|
|
816
|
+
let rings = []; /* ring -> [{id, angle, span, diag, ext, parent_ext}] */
|
|
737
817
|
/* An explicit stack, not recursion: the sectors of a ring are
|
|
738
818
|
* sorted by angle in pass 2, so the order they are pushed in does
|
|
739
819
|
* not matter -- only that every node gets its own. */
|
|
740
820
|
let sector = (root_id, root_from, root_span, root_parent) => {
|
|
741
|
-
let
|
|
742
|
-
while(
|
|
743
|
-
let {id, from, span, parent} =
|
|
821
|
+
let todo = [{id: root_id, from: root_from, span: root_span, parent: root_parent}];
|
|
822
|
+
while(todo.length) {
|
|
823
|
+
let {id, from, span, parent} = todo.pop();
|
|
744
824
|
let a = from + span / 2;
|
|
745
825
|
angle.set(id, a);
|
|
746
826
|
let ring = ring_of(id);
|
|
747
827
|
if(!rings[ring]) {
|
|
748
828
|
rings[ring] = [];
|
|
749
829
|
}
|
|
750
|
-
let
|
|
830
|
+
let st = stacks.get(id);
|
|
831
|
+
let n = st? null : t.by_id.get(id);
|
|
751
832
|
rings[ring].push({
|
|
752
833
|
id: id,
|
|
753
834
|
angle: a,
|
|
754
|
-
|
|
835
|
+
span: span,
|
|
836
|
+
/* How much it needs along the ring: a card's diagonal,
|
|
837
|
+
* a stack's first row. */
|
|
838
|
+
diag: st? stack_across(st, a).row : Math.hypot(n.w, n.h),
|
|
755
839
|
/* How much of the card lies along the RADIUS at this
|
|
756
840
|
* angle -- the projection of the rectangle on that
|
|
757
841
|
* direction -- for itself and for the node it hangs
|
|
758
842
|
* from, which is the one it has to clear. */
|
|
759
|
-
|
|
760
|
-
|
|
843
|
+
/* A stack is measured, and measures its parent, over
|
|
844
|
+
* the whole sector it can cover: round a parent its
|
|
845
|
+
* first row stands at every angle. */
|
|
846
|
+
ext: st? widest_along(st.cards, a, Math.min(span / 2, Math.PI))
|
|
847
|
+
: extent_at(n, a),
|
|
848
|
+
parent_ext: !parent? 0
|
|
849
|
+
: (st? widest_along([t.by_id.get(parent)], a, Math.min(span / 2, Math.PI))
|
|
850
|
+
: extent_at(t.by_id.get(parent), a)),
|
|
761
851
|
});
|
|
762
852
|
let cursor = from;
|
|
763
|
-
for(let kid of (
|
|
764
|
-
let part = span *
|
|
765
|
-
|
|
853
|
+
for(let kid of (kids_of.get(id) || [])) {
|
|
854
|
+
let part = span * weight.get(kid) / weight.get(id);
|
|
855
|
+
todo.push({id: kid, from: cursor, span: part, parent: id});
|
|
766
856
|
cursor += part;
|
|
767
857
|
}
|
|
768
858
|
}
|
|
@@ -771,15 +861,15 @@ export function layout_radial(nodes, edges, opts)
|
|
|
771
861
|
let root = t.roots[0];
|
|
772
862
|
angle.set(root, 0);
|
|
773
863
|
let cursor = -Math.PI / 2; /* the first child at the top */
|
|
774
|
-
for(let kid of (
|
|
775
|
-
let part = 2 * Math.PI *
|
|
864
|
+
for(let kid of (kids_of.get(root) || [])) {
|
|
865
|
+
let part = 2 * Math.PI * weight.get(kid) / weight.get(root);
|
|
776
866
|
sector(kid, cursor, part, root);
|
|
777
867
|
cursor += part;
|
|
778
868
|
}
|
|
779
869
|
} else {
|
|
780
870
|
let cursor = -Math.PI / 2;
|
|
781
871
|
for(let root of t.roots) {
|
|
782
|
-
let part = 2 * Math.PI *
|
|
872
|
+
let part = 2 * Math.PI * weight.get(root) / total;
|
|
783
873
|
sector(root, cursor, part, null);
|
|
784
874
|
cursor += part;
|
|
785
875
|
}
|
|
@@ -803,31 +893,132 @@ export function layout_radial(nodes, edges, opts)
|
|
|
803
893
|
* the one it HANGS FROM, in the direction it hangs in -- the
|
|
804
894
|
* two are radially aligned, or nearly, since a child lives
|
|
805
895
|
* inside its parent's sector -- and the ring goes out far
|
|
806
|
-
* enough for the worst of them.
|
|
896
|
+
* enough for the worst of them.
|
|
897
|
+
*
|
|
898
|
+
* Then the STACKS of the ring get their rows: each row holds as
|
|
899
|
+
* many cards as fit in the room the stack has -- its own sector,
|
|
900
|
+
* less what its neighbours on the ring take up -- at that row's
|
|
901
|
+
* radius. A stack alone on its ring has the whole circle, and its
|
|
902
|
+
* rows are rings round its parent; one between neighbours fans
|
|
903
|
+
* out inside its sector, wider as it goes. The rows reaching out
|
|
904
|
+
* are neighbours of the outer rings too, by the ANGLE they cover. */
|
|
905
|
+
let fit = (list, r0) => {
|
|
906
|
+
let r = r0;
|
|
907
|
+
let sorted = list.slice().sort((a, b) => a.angle - b.angle);
|
|
908
|
+
for(let i = 0; i < sorted.length && sorted.length > 1; i++) {
|
|
909
|
+
let a = sorted[i];
|
|
910
|
+
let b = sorted[(i + 1) % sorted.length];
|
|
911
|
+
let gap = (i + 1 < sorted.length)
|
|
912
|
+
? b.angle - a.angle
|
|
913
|
+
: b.angle + 2 * Math.PI - a.angle;
|
|
914
|
+
if(gap > 1e-9) {
|
|
915
|
+
r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return r;
|
|
919
|
+
};
|
|
807
920
|
let radius = [0];
|
|
921
|
+
let reaching = []; /* planned stacks: {angle, half, reach} */
|
|
808
922
|
for(let ring = 1; ring < rings.length; ring++) {
|
|
923
|
+
let members = rings[ring] || [];
|
|
809
924
|
let r = radius[ring - 1] + o.ranksep;
|
|
810
|
-
|
|
925
|
+
let band = 0;
|
|
926
|
+
for(let n of members) {
|
|
811
927
|
r = Math.max(r, radius[ring - 1] + (n.parent_ext + n.ext) / 2 + o.nodesep);
|
|
928
|
+
band = Math.max(band, n.ext);
|
|
812
929
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
930
|
+
r = fit(members, r);
|
|
931
|
+
/* Clear the rows of the inner stacks that reach this far: by
|
|
932
|
+
* the angle they cover, or, for a card standing in it, by
|
|
933
|
+
* going past their reach. */
|
|
934
|
+
for(let pass = 0; pass < 2; pass++) {
|
|
935
|
+
for(let g of reaching) {
|
|
936
|
+
if(g.reach < r - band / 2 - o.nodesep) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
for(let m of members) {
|
|
940
|
+
let d = circ(g.angle, m.angle);
|
|
941
|
+
if(d > g.half + 1e-9) {
|
|
942
|
+
r = Math.max(r, (m.diag / 2 + o.nodesep) / (d - g.half));
|
|
943
|
+
} else {
|
|
944
|
+
r = Math.max(r, g.reach + m.ext / 2 + o.nodesep);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
822
947
|
}
|
|
823
948
|
}
|
|
824
949
|
radius[ring] = r;
|
|
950
|
+
|
|
951
|
+
let sorted = members.slice().sort((a, b) => a.angle - b.angle);
|
|
952
|
+
for(let i = 0; i < sorted.length; i++) {
|
|
953
|
+
let m = sorted[i];
|
|
954
|
+
let st = stacks.get(m.id);
|
|
955
|
+
if(!st) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
/* The room each side: to the neighbour, less its half and
|
|
959
|
+
* the gap; never past the stack's own sector. */
|
|
960
|
+
let room = m.span / 2;
|
|
961
|
+
if(sorted.length > 1) {
|
|
962
|
+
let left = sorted[(i - 1 + sorted.length) % sorted.length];
|
|
963
|
+
let right = sorted[(i + 1) % sorted.length];
|
|
964
|
+
room = Math.min(room,
|
|
965
|
+
circ(m.angle, left.angle) - (left.diag / 2 + o.nodesep) / r,
|
|
966
|
+
circ(m.angle, right.angle) - (right.diag / 2 + o.nodesep) / r);
|
|
967
|
+
}
|
|
968
|
+
room = Math.min(room, Math.PI);
|
|
969
|
+
/* Measured over the whole arc it will cover, not at its
|
|
970
|
+
* middle: round a parent a card stands at every angle,
|
|
971
|
+
* and a 172x96 card is wider at 45 degrees than facing
|
|
972
|
+
* down. */
|
|
973
|
+
let across = 0;
|
|
974
|
+
let along = 0;
|
|
975
|
+
for(let k = 0; k <= 16; k++) {
|
|
976
|
+
let th = m.angle - room + 2 * room * k / 16;
|
|
977
|
+
for(let c of st.cards) {
|
|
978
|
+
across = Math.max(across, extent_at(c, th + Math.PI / 2));
|
|
979
|
+
along = Math.max(along, extent_at(c, th));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
let rows = [];
|
|
983
|
+
let left_to_place = st.ids.length;
|
|
984
|
+
let rr = r;
|
|
985
|
+
let half = 0;
|
|
986
|
+
while(left_to_place > 0) {
|
|
987
|
+
let fits = Math.floor(2 * room * rr / (across + o.nodesep));
|
|
988
|
+
let cnt = Math.max(1, Math.min(left_to_place, fits));
|
|
989
|
+
rows.push({rr: rr, cnt: cnt});
|
|
990
|
+
half = Math.max(half, ((cnt * (across + o.nodesep) - o.nodesep) / 2) / rr);
|
|
991
|
+
left_to_place -= cnt;
|
|
992
|
+
rr += along + STACK_GAP;
|
|
993
|
+
}
|
|
994
|
+
st.plan = {angle: m.angle, across: across, rows: rows};
|
|
995
|
+
reaching.push({
|
|
996
|
+
angle: m.angle,
|
|
997
|
+
half: half,
|
|
998
|
+
reach: rows[rows.length - 1].rr + along / 2,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
825
1001
|
}
|
|
826
1002
|
|
|
827
|
-
/* Pass 3, the positions
|
|
1003
|
+
/* Pass 3, the positions: a node at its angle on its ring; a
|
|
1004
|
+
* stack's cards row by row outward, each row centred on the
|
|
1005
|
+
* stack's angle, a card's width plus the gap apart along the arc. */
|
|
828
1006
|
for(let [id, a] of angle) {
|
|
829
|
-
let
|
|
830
|
-
|
|
1007
|
+
let st = stacks.get(id);
|
|
1008
|
+
if(!st) {
|
|
1009
|
+
let r0 = radius[ring_of(id)] || 0;
|
|
1010
|
+
pos.set(id, {x: r0 * Math.cos(a), y: r0 * Math.sin(a)});
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
let i = 0;
|
|
1014
|
+
for(let row of st.plan.rows) {
|
|
1015
|
+
let step = (st.plan.across + o.nodesep) / row.rr;
|
|
1016
|
+
for(let j = 0; j < row.cnt; j++) {
|
|
1017
|
+
let th = a + (j - (row.cnt - 1) / 2) * step;
|
|
1018
|
+
pos.set(st.ids[i], {x: row.rr * Math.cos(th), y: row.rr * Math.sin(th)});
|
|
1019
|
+
i++;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
831
1022
|
}
|
|
832
1023
|
|
|
833
1024
|
return pos;
|