@yuneta/gobj-ui 7.23.151 → 7.23.153
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 +16 -2
- package/dist/gobj-ui.cjs.js +126 -26
- package/dist/gobj-ui.es.js +126 -26
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +63 -7
- package/src/treedb_elbow.js +108 -24
- package/src/treedb_elbow.test.js +144 -1
package/README.md
CHANGED
|
@@ -1457,8 +1457,22 @@ the lane, plus the two ends; a turn costs 40px of line), over the cards within
|
|
|
1457
1457
|
400px first and all of them if that finds no way; with no way at all the
|
|
1458
1458
|
simple elbow stays. A parent and the row of children under it keep their bus;
|
|
1459
1459
|
what gets routed is a detour with a third card in its way, an edge dropping
|
|
1460
|
-
several rows, an edge between two cards of one row.
|
|
1461
|
-
|
|
1460
|
+
several rows, an edge between two cards of one row. Since `7.23.152` a node is
|
|
1461
|
+
measured **whole** — card, ports and label — so a line keeps clear of the
|
|
1462
|
+
ports as well; the edge's own ports stick out of its own cards, so its first
|
|
1463
|
+
and last segments are not tested against their own box.
|
|
1464
|
+
|
|
1465
|
+
The edges sharing an **end** do not lie on top of each other either
|
|
1466
|
+
(`7.23.153`): the forward edges leaving one card — all of them, not only one
|
|
1467
|
+
port's, or two hooks side by side would stack their runs — and those reaching
|
|
1468
|
+
one card (all its fkeys enter by one point) are **staggered**, each turning at
|
|
1469
|
+
its own height in the channel, 10px apart at most and closer when the channel
|
|
1470
|
+
is short. Leaving, the farthest turns highest; arriving, the farthest turns
|
|
1471
|
+
lowest: the orders in which the lines of one end nest without crossing. A
|
|
1472
|
+
routed line leaves its port and reaches the other one **sideways**, because
|
|
1473
|
+
the column straight under or over a port is where the staggered edges of that
|
|
1474
|
+
port run. Only the stub at the port is shared. G6's `shortest-path` router is not used: it is
|
|
1475
|
+
not exported, it
|
|
1462
1476
|
falls back in silence to a route that crosses cards, and it rewrites its
|
|
1463
1477
|
module defaults with every config it is given.
|
|
1464
1478
|
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -29640,6 +29640,23 @@ function elbow_lane(ids, id) {
|
|
|
29640
29640
|
if (k <= 0) return 0;
|
|
29641
29641
|
return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
|
|
29642
29642
|
}
|
|
29643
|
+
/************************************************************
|
|
29644
|
+
* The rank of `id` among `items` ([{id, span}]): the forward
|
|
29645
|
+
* edges that share one end with it, each with how far it runs
|
|
29646
|
+
* ACROSS. Farthest first; a tie goes to the edge made first.
|
|
29647
|
+
* Returns {rank, count}.
|
|
29648
|
+
************************************************************/
|
|
29649
|
+
function elbow_rank(items, id) {
|
|
29650
|
+
let sorted = items.slice().sort((a, b) => {
|
|
29651
|
+
if (b.span !== a.span) return b.span - a.span;
|
|
29652
|
+
return String(a.id).localeCompare(String(b.id), void 0, { numeric: true });
|
|
29653
|
+
});
|
|
29654
|
+
let rank = sorted.findIndex((it) => it.id === id);
|
|
29655
|
+
return {
|
|
29656
|
+
rank: rank < 0 ? 0 : rank,
|
|
29657
|
+
count: sorted.length
|
|
29658
|
+
};
|
|
29659
|
+
}
|
|
29643
29660
|
function swap(p) {
|
|
29644
29661
|
return [p[1], p[0]];
|
|
29645
29662
|
}
|
|
@@ -29658,19 +29675,41 @@ function swap_box(b) {
|
|
|
29658
29675
|
*
|
|
29659
29676
|
* vertical the rows are stacked (top to bottom); false
|
|
29660
29677
|
* when they are side by side (left to right)
|
|
29678
|
+
* stagger {dep, arr}: the edge's rank among the forward
|
|
29679
|
+
* edges leaving its source card and among those
|
|
29680
|
+
* reaching its target card (elbow_rank). Absent,
|
|
29681
|
+
* the channel is the middle.
|
|
29661
29682
|
************************************************************/
|
|
29662
|
-
function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
29663
|
-
if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true).map(swap);
|
|
29683
|
+
function elbow_points(s, t, box_s, box_t, lane, vertical, stagger) {
|
|
29684
|
+
if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, stagger).map(swap);
|
|
29664
29685
|
let shift = lane * 10;
|
|
29665
29686
|
if (t[1] - s[1] > 20) {
|
|
29666
|
-
let
|
|
29667
|
-
|
|
29687
|
+
let lo = Math.max(s[1], box_s.y2);
|
|
29688
|
+
let hi = Math.min(t[1], box_t.y1);
|
|
29689
|
+
let mid = (lo + hi) / 2;
|
|
29690
|
+
let st = stagger || {};
|
|
29691
|
+
let group = null;
|
|
29692
|
+
let k = 0;
|
|
29693
|
+
if (st.dep && st.dep.count > 1) {
|
|
29694
|
+
group = st.dep;
|
|
29695
|
+
k = st.dep.rank;
|
|
29696
|
+
} else if (st.arr && st.arr.count > 1) {
|
|
29697
|
+
group = st.arr;
|
|
29698
|
+
k = st.arr.count - 1 - st.arr.rank;
|
|
29699
|
+
}
|
|
29700
|
+
if (group) {
|
|
29701
|
+
let room = Math.max(0, hi - lo - 12);
|
|
29702
|
+
let step = Math.min(10, room / (group.count - 1));
|
|
29703
|
+
mid = mid - (group.count - 1) * step / 2 + k * step;
|
|
29704
|
+
}
|
|
29705
|
+
mid += shift;
|
|
29706
|
+
mid = Math.min(Math.max(mid, lo + 4), hi - 4);
|
|
29668
29707
|
return [[s[0], mid], [t[0], mid]];
|
|
29669
29708
|
}
|
|
29670
29709
|
let out = 16 + Math.abs(shift);
|
|
29671
29710
|
let side = lane < 0 ? Math.min(box_s.x1, box_t.x1) - 24 - Math.abs(shift) : Math.max(box_s.x2, box_t.x2) + 24 + Math.abs(shift);
|
|
29672
|
-
let y1 = s[1] + out;
|
|
29673
|
-
let y2 = t[1] - out;
|
|
29711
|
+
let y1 = Math.max(s[1], box_s.y2) + out;
|
|
29712
|
+
let y2 = Math.min(t[1], box_t.y1) - out;
|
|
29674
29713
|
return [
|
|
29675
29714
|
[s[0], y1],
|
|
29676
29715
|
[side, y1],
|
|
@@ -29699,15 +29738,30 @@ function segment_hits(a, c, b) {
|
|
|
29699
29738
|
let y2 = Math.max(a[1], c[1]);
|
|
29700
29739
|
return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
|
|
29701
29740
|
}
|
|
29741
|
+
function same_box(a, b) {
|
|
29742
|
+
return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
|
|
29743
|
+
}
|
|
29702
29744
|
/************************************************************
|
|
29703
29745
|
* Does the polyline `points` (its ends included) cross any of
|
|
29704
|
-
* `boxes`? A
|
|
29705
|
-
*
|
|
29746
|
+
* `boxes`? A box is shrunk by a pixel, so a line along its
|
|
29747
|
+
* border is not a hit.
|
|
29748
|
+
*
|
|
29749
|
+
* A box is a whole node, its ports included, and the edge's own
|
|
29750
|
+
* ports stick out of its own two cards: its first segment starts
|
|
29751
|
+
* inside `box_s` and its last one ends inside `box_t`. Those two
|
|
29752
|
+
* segments are not tested against their own box; every other
|
|
29753
|
+
* segment is, so a line may not run back over its own cards.
|
|
29706
29754
|
************************************************************/
|
|
29707
|
-
function elbow_path_hits(points, boxes) {
|
|
29755
|
+
function elbow_path_hits(points, boxes, box_s, box_t) {
|
|
29756
|
+
let last = points.length - 2;
|
|
29708
29757
|
for (let b of boxes) {
|
|
29709
29758
|
let inner = box_expand(b, -1);
|
|
29710
|
-
|
|
29759
|
+
let own_s = same_box(b, box_s);
|
|
29760
|
+
let own_t = same_box(b, box_t);
|
|
29761
|
+
for (let i = 0; i <= last; i++) {
|
|
29762
|
+
if (own_s && i === 0 || own_t && i === last) continue;
|
|
29763
|
+
if (segment_hits(points[i], points[i + 1], inner)) return true;
|
|
29764
|
+
}
|
|
29711
29765
|
}
|
|
29712
29766
|
return false;
|
|
29713
29767
|
}
|
|
@@ -29743,8 +29797,12 @@ function heap_pop(h) {
|
|
|
29743
29797
|
/************************************************************
|
|
29744
29798
|
* The shortest orthogonal line from `a` to `z` that enters none
|
|
29745
29799
|
* of `boxes` (already expanded by the clearance), a turn costing
|
|
29746
|
-
* ROUTE_BEND. The line leaves `a`
|
|
29747
|
-
*
|
|
29800
|
+
* ROUTE_BEND. The line leaves `a` SIDEWAYS and reaches `z`
|
|
29801
|
+
* SIDEWAYS: the column straight under a port and the one straight
|
|
29802
|
+
* over it belong to the forward edges that share that port, which
|
|
29803
|
+
* run along them to their staggered turns -- a route going down
|
|
29804
|
+
* that column lay on top of them. So a routed line shares only
|
|
29805
|
+
* the stub between the port and `a` (or `z`).
|
|
29748
29806
|
*
|
|
29749
29807
|
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
29750
29808
|
* of the two ends, which is all an orthogonal shortest path ever
|
|
@@ -29810,7 +29868,8 @@ function orth_search(a, z, boxes) {
|
|
|
29810
29868
|
let k = state >> 2;
|
|
29811
29869
|
let dir = state & 3;
|
|
29812
29870
|
if (k === goal) {
|
|
29813
|
-
|
|
29871
|
+
if (dir >= 2) continue;
|
|
29872
|
+
let total = cost + 40;
|
|
29814
29873
|
if (total < found_cost) {
|
|
29815
29874
|
found_cost = total;
|
|
29816
29875
|
found = state;
|
|
@@ -29818,6 +29877,7 @@ function orth_search(a, z, boxes) {
|
|
|
29818
29877
|
continue;
|
|
29819
29878
|
}
|
|
29820
29879
|
for (let [nk, nd, len] of moves(k)) {
|
|
29880
|
+
if (k === start && nd >= 2) continue;
|
|
29821
29881
|
let c = cost + len + (nd === dir ? 0 : 40);
|
|
29822
29882
|
let ns = nk * 4 + nd;
|
|
29823
29883
|
if (c < best[ns]) {
|
|
@@ -29849,24 +29909,27 @@ function orth_search(a, z, boxes) {
|
|
|
29849
29909
|
* port, the shortest way along the gaps between the cards, in.
|
|
29850
29910
|
* A parent and the row of children under it keep their bus.
|
|
29851
29911
|
*
|
|
29852
|
-
* `boxes` are all the
|
|
29853
|
-
* included
|
|
29912
|
+
* `boxes` are all the nodes on screen, the two of the edge
|
|
29913
|
+
* included, each as the box of the WHOLE node -- card, ports and
|
|
29914
|
+
* label -- so a line clears the ports too. `box_s` and `box_t`
|
|
29915
|
+
* are the edge's own two, measured the same way. The route keeps
|
|
29916
|
+
* a clearance from every box, which
|
|
29854
29917
|
* grows with the lane so that two routed edges of one pair do not
|
|
29855
29918
|
* coincide. Nearby cards first; all of them if that finds no way;
|
|
29856
29919
|
* the simple elbow if nothing does.
|
|
29857
29920
|
************************************************************/
|
|
29858
|
-
function elbow_route(s, t, box_s, box_t, lane, vertical, boxes) {
|
|
29921
|
+
function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger) {
|
|
29859
29922
|
let others = boxes || [];
|
|
29860
|
-
if (!vertical) return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, others.map(swap_box)).map(swap);
|
|
29861
|
-
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
29923
|
+
if (!vertical) return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, others.map(swap_box), stagger).map(swap);
|
|
29924
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
29862
29925
|
if (!elbow_path_hits([
|
|
29863
29926
|
s,
|
|
29864
29927
|
...simple,
|
|
29865
29928
|
t
|
|
29866
|
-
], others)) return simple;
|
|
29929
|
+
], others, box_s, box_t)) return simple;
|
|
29867
29930
|
let m = 12 + Math.abs(lane) * 10;
|
|
29868
|
-
let a = [s[0], s[1] + m +
|
|
29869
|
-
let z = [t[0], t[1] - m -
|
|
29931
|
+
let a = [s[0], Math.max(s[1], box_s.y2) + m + 1];
|
|
29932
|
+
let z = [t[0], Math.min(t[1], box_t.y1) - m - 1];
|
|
29870
29933
|
let region = box_expand({
|
|
29871
29934
|
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
29872
29935
|
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
|
@@ -59259,13 +59322,15 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
|
|
|
59259
59322
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59260
59323
|
* between rows (top to bottom), `-h` between columns. An edge that
|
|
59261
59324
|
* does not run forward goes round the two cards, the edges joining
|
|
59262
|
-
* the same two cards take lanes,
|
|
59263
|
-
*
|
|
59325
|
+
* the same two cards take lanes, the edges sharing an end turn at
|
|
59326
|
+
* staggered heights, and a line that would cross any other card is
|
|
59327
|
+
* routed round it (treedb_elbow.js).
|
|
59264
59328
|
************************************************************/
|
|
59265
59329
|
var ELBOW_RADIUS = 6;
|
|
59266
59330
|
function elbow_box(node) {
|
|
59267
59331
|
try {
|
|
59268
|
-
let b = node.
|
|
59332
|
+
let b = node.getRenderBounds();
|
|
59333
|
+
if (!b || b.isEmpty && b.isEmpty()) b = node.getShape("key").getBounds();
|
|
59269
59334
|
return {
|
|
59270
59335
|
x1: b.min[0],
|
|
59271
59336
|
y1: b.min[1],
|
|
@@ -59311,10 +59376,45 @@ function elbow_lane_of(edge) {
|
|
|
59311
59376
|
return 0;
|
|
59312
59377
|
}
|
|
59313
59378
|
}
|
|
59379
|
+
function elbow_stagger_of(edge, vertical) {
|
|
59380
|
+
let graph = edge.context && edge.context.graph;
|
|
59381
|
+
let element = edge.context && edge.context.element;
|
|
59382
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
59383
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
59384
|
+
if (!graph || !element || s === void 0 || t === void 0) return null;
|
|
59385
|
+
let across = vertical ? 0 : 1;
|
|
59386
|
+
let along = vertical ? 1 : 0;
|
|
59387
|
+
let pos = (id) => graph.getElementPosition(id);
|
|
59388
|
+
let port_of = (node_id, key) => {
|
|
59389
|
+
let node = element.getElement(node_id);
|
|
59390
|
+
let ports = node && typeof node.getPorts === "function" ? node.getPorts() : {};
|
|
59391
|
+
let port = key ? ports[key] : null;
|
|
59392
|
+
return port ? port.getPosition() : pos(node_id);
|
|
59393
|
+
};
|
|
59394
|
+
try {
|
|
59395
|
+
let ps = pos(s);
|
|
59396
|
+
let pt = pos(t);
|
|
59397
|
+
let dep = graph.getRelatedEdgesData(s, "out").filter((d) => d.source === s && pos(d.target)[along] > ps[along]).map((d) => ({
|
|
59398
|
+
id: d.id,
|
|
59399
|
+
span: Math.abs(pos(d.target)[across] - port_of(s, d.style && d.style.sourcePort)[across])
|
|
59400
|
+
}));
|
|
59401
|
+
let arr = graph.getRelatedEdgesData(t, "in").filter((d) => d.target === t && pos(d.source)[along] < pt[along]).map((d) => ({
|
|
59402
|
+
id: d.id,
|
|
59403
|
+
span: Math.abs(port_of(d.source, d.style && d.style.sourcePort)[across] - pt[across])
|
|
59404
|
+
}));
|
|
59405
|
+
return {
|
|
59406
|
+
dep: elbow_rank(dep, edge.id),
|
|
59407
|
+
arr: elbow_rank(arr, edge.id)
|
|
59408
|
+
};
|
|
59409
|
+
} catch (e) {
|
|
59410
|
+
(0, _yuneta_gobj_js.log_error)(`elbow edge ${edge.id}: cannot read the edges sharing its ends: ${e}`);
|
|
59411
|
+
return null;
|
|
59412
|
+
}
|
|
59413
|
+
}
|
|
59314
59414
|
var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
59315
59415
|
getControlPoints(attributes) {
|
|
59316
59416
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59317
|
-
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
|
|
59417
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this), elbow_stagger_of(this, true));
|
|
59318
59418
|
}
|
|
59319
59419
|
getKeyPath(attributes) {
|
|
59320
59420
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59323,7 +59423,7 @@ var TreedbElbowV = class extends _antv_g6.Polyline {
|
|
|
59323
59423
|
var TreedbElbowH = class extends _antv_g6.Polyline {
|
|
59324
59424
|
getControlPoints(attributes) {
|
|
59325
59425
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59326
|
-
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
|
|
59426
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this), elbow_stagger_of(this, false));
|
|
59327
59427
|
}
|
|
59328
59428
|
getKeyPath(attributes) {
|
|
59329
59429
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -29635,6 +29635,23 @@ function elbow_lane(ids, id) {
|
|
|
29635
29635
|
if (k <= 0) return 0;
|
|
29636
29636
|
return (k % 2 ? 1 : -1) * Math.ceil(k / 2);
|
|
29637
29637
|
}
|
|
29638
|
+
/************************************************************
|
|
29639
|
+
* The rank of `id` among `items` ([{id, span}]): the forward
|
|
29640
|
+
* edges that share one end with it, each with how far it runs
|
|
29641
|
+
* ACROSS. Farthest first; a tie goes to the edge made first.
|
|
29642
|
+
* Returns {rank, count}.
|
|
29643
|
+
************************************************************/
|
|
29644
|
+
function elbow_rank(items, id) {
|
|
29645
|
+
let sorted = items.slice().sort((a, b) => {
|
|
29646
|
+
if (b.span !== a.span) return b.span - a.span;
|
|
29647
|
+
return String(a.id).localeCompare(String(b.id), void 0, { numeric: true });
|
|
29648
|
+
});
|
|
29649
|
+
let rank = sorted.findIndex((it) => it.id === id);
|
|
29650
|
+
return {
|
|
29651
|
+
rank: rank < 0 ? 0 : rank,
|
|
29652
|
+
count: sorted.length
|
|
29653
|
+
};
|
|
29654
|
+
}
|
|
29638
29655
|
function swap(p) {
|
|
29639
29656
|
return [p[1], p[0]];
|
|
29640
29657
|
}
|
|
@@ -29653,19 +29670,41 @@ function swap_box(b) {
|
|
|
29653
29670
|
*
|
|
29654
29671
|
* vertical the rows are stacked (top to bottom); false
|
|
29655
29672
|
* when they are side by side (left to right)
|
|
29673
|
+
* stagger {dep, arr}: the edge's rank among the forward
|
|
29674
|
+
* edges leaving its source card and among those
|
|
29675
|
+
* reaching its target card (elbow_rank). Absent,
|
|
29676
|
+
* the channel is the middle.
|
|
29656
29677
|
************************************************************/
|
|
29657
|
-
function elbow_points(s, t, box_s, box_t, lane, vertical) {
|
|
29658
|
-
if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true).map(swap);
|
|
29678
|
+
function elbow_points(s, t, box_s, box_t, lane, vertical, stagger) {
|
|
29679
|
+
if (!vertical) return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, stagger).map(swap);
|
|
29659
29680
|
let shift = lane * 10;
|
|
29660
29681
|
if (t[1] - s[1] > 20) {
|
|
29661
|
-
let
|
|
29662
|
-
|
|
29682
|
+
let lo = Math.max(s[1], box_s.y2);
|
|
29683
|
+
let hi = Math.min(t[1], box_t.y1);
|
|
29684
|
+
let mid = (lo + hi) / 2;
|
|
29685
|
+
let st = stagger || {};
|
|
29686
|
+
let group = null;
|
|
29687
|
+
let k = 0;
|
|
29688
|
+
if (st.dep && st.dep.count > 1) {
|
|
29689
|
+
group = st.dep;
|
|
29690
|
+
k = st.dep.rank;
|
|
29691
|
+
} else if (st.arr && st.arr.count > 1) {
|
|
29692
|
+
group = st.arr;
|
|
29693
|
+
k = st.arr.count - 1 - st.arr.rank;
|
|
29694
|
+
}
|
|
29695
|
+
if (group) {
|
|
29696
|
+
let room = Math.max(0, hi - lo - 12);
|
|
29697
|
+
let step = Math.min(10, room / (group.count - 1));
|
|
29698
|
+
mid = mid - (group.count - 1) * step / 2 + k * step;
|
|
29699
|
+
}
|
|
29700
|
+
mid += shift;
|
|
29701
|
+
mid = Math.min(Math.max(mid, lo + 4), hi - 4);
|
|
29663
29702
|
return [[s[0], mid], [t[0], mid]];
|
|
29664
29703
|
}
|
|
29665
29704
|
let out = 16 + Math.abs(shift);
|
|
29666
29705
|
let side = lane < 0 ? Math.min(box_s.x1, box_t.x1) - 24 - Math.abs(shift) : Math.max(box_s.x2, box_t.x2) + 24 + Math.abs(shift);
|
|
29667
|
-
let y1 = s[1] + out;
|
|
29668
|
-
let y2 = t[1] - out;
|
|
29706
|
+
let y1 = Math.max(s[1], box_s.y2) + out;
|
|
29707
|
+
let y2 = Math.min(t[1], box_t.y1) - out;
|
|
29669
29708
|
return [
|
|
29670
29709
|
[s[0], y1],
|
|
29671
29710
|
[side, y1],
|
|
@@ -29694,15 +29733,30 @@ function segment_hits(a, c, b) {
|
|
|
29694
29733
|
let y2 = Math.max(a[1], c[1]);
|
|
29695
29734
|
return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
|
|
29696
29735
|
}
|
|
29736
|
+
function same_box(a, b) {
|
|
29737
|
+
return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
|
|
29738
|
+
}
|
|
29697
29739
|
/************************************************************
|
|
29698
29740
|
* Does the polyline `points` (its ends included) cross any of
|
|
29699
|
-
* `boxes`? A
|
|
29700
|
-
*
|
|
29741
|
+
* `boxes`? A box is shrunk by a pixel, so a line along its
|
|
29742
|
+
* border is not a hit.
|
|
29743
|
+
*
|
|
29744
|
+
* A box is a whole node, its ports included, and the edge's own
|
|
29745
|
+
* ports stick out of its own two cards: its first segment starts
|
|
29746
|
+
* inside `box_s` and its last one ends inside `box_t`. Those two
|
|
29747
|
+
* segments are not tested against their own box; every other
|
|
29748
|
+
* segment is, so a line may not run back over its own cards.
|
|
29701
29749
|
************************************************************/
|
|
29702
|
-
function elbow_path_hits(points, boxes) {
|
|
29750
|
+
function elbow_path_hits(points, boxes, box_s, box_t) {
|
|
29751
|
+
let last = points.length - 2;
|
|
29703
29752
|
for (let b of boxes) {
|
|
29704
29753
|
let inner = box_expand(b, -1);
|
|
29705
|
-
|
|
29754
|
+
let own_s = same_box(b, box_s);
|
|
29755
|
+
let own_t = same_box(b, box_t);
|
|
29756
|
+
for (let i = 0; i <= last; i++) {
|
|
29757
|
+
if (own_s && i === 0 || own_t && i === last) continue;
|
|
29758
|
+
if (segment_hits(points[i], points[i + 1], inner)) return true;
|
|
29759
|
+
}
|
|
29706
29760
|
}
|
|
29707
29761
|
return false;
|
|
29708
29762
|
}
|
|
@@ -29738,8 +29792,12 @@ function heap_pop(h) {
|
|
|
29738
29792
|
/************************************************************
|
|
29739
29793
|
* The shortest orthogonal line from `a` to `z` that enters none
|
|
29740
29794
|
* of `boxes` (already expanded by the clearance), a turn costing
|
|
29741
|
-
* ROUTE_BEND. The line leaves `a`
|
|
29742
|
-
*
|
|
29795
|
+
* ROUTE_BEND. The line leaves `a` SIDEWAYS and reaches `z`
|
|
29796
|
+
* SIDEWAYS: the column straight under a port and the one straight
|
|
29797
|
+
* over it belong to the forward edges that share that port, which
|
|
29798
|
+
* run along them to their staggered turns -- a route going down
|
|
29799
|
+
* that column lay on top of them. So a routed line shares only
|
|
29800
|
+
* the stub between the port and `a` (or `z`).
|
|
29743
29801
|
*
|
|
29744
29802
|
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
29745
29803
|
* of the two ends, which is all an orthogonal shortest path ever
|
|
@@ -29805,7 +29863,8 @@ function orth_search(a, z, boxes) {
|
|
|
29805
29863
|
let k = state >> 2;
|
|
29806
29864
|
let dir = state & 3;
|
|
29807
29865
|
if (k === goal) {
|
|
29808
|
-
|
|
29866
|
+
if (dir >= 2) continue;
|
|
29867
|
+
let total = cost + 40;
|
|
29809
29868
|
if (total < found_cost) {
|
|
29810
29869
|
found_cost = total;
|
|
29811
29870
|
found = state;
|
|
@@ -29813,6 +29872,7 @@ function orth_search(a, z, boxes) {
|
|
|
29813
29872
|
continue;
|
|
29814
29873
|
}
|
|
29815
29874
|
for (let [nk, nd, len] of moves(k)) {
|
|
29875
|
+
if (k === start && nd >= 2) continue;
|
|
29816
29876
|
let c = cost + len + (nd === dir ? 0 : 40);
|
|
29817
29877
|
let ns = nk * 4 + nd;
|
|
29818
29878
|
if (c < best[ns]) {
|
|
@@ -29844,24 +29904,27 @@ function orth_search(a, z, boxes) {
|
|
|
29844
29904
|
* port, the shortest way along the gaps between the cards, in.
|
|
29845
29905
|
* A parent and the row of children under it keep their bus.
|
|
29846
29906
|
*
|
|
29847
|
-
* `boxes` are all the
|
|
29848
|
-
* included
|
|
29907
|
+
* `boxes` are all the nodes on screen, the two of the edge
|
|
29908
|
+
* included, each as the box of the WHOLE node -- card, ports and
|
|
29909
|
+
* label -- so a line clears the ports too. `box_s` and `box_t`
|
|
29910
|
+
* are the edge's own two, measured the same way. The route keeps
|
|
29911
|
+
* a clearance from every box, which
|
|
29849
29912
|
* grows with the lane so that two routed edges of one pair do not
|
|
29850
29913
|
* coincide. Nearby cards first; all of them if that finds no way;
|
|
29851
29914
|
* the simple elbow if nothing does.
|
|
29852
29915
|
************************************************************/
|
|
29853
|
-
function elbow_route(s, t, box_s, box_t, lane, vertical, boxes) {
|
|
29916
|
+
function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger) {
|
|
29854
29917
|
let others = boxes || [];
|
|
29855
|
-
if (!vertical) return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, others.map(swap_box)).map(swap);
|
|
29856
|
-
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
29918
|
+
if (!vertical) return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true, others.map(swap_box), stagger).map(swap);
|
|
29919
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
29857
29920
|
if (!elbow_path_hits([
|
|
29858
29921
|
s,
|
|
29859
29922
|
...simple,
|
|
29860
29923
|
t
|
|
29861
|
-
], others)) return simple;
|
|
29924
|
+
], others, box_s, box_t)) return simple;
|
|
29862
29925
|
let m = 12 + Math.abs(lane) * 10;
|
|
29863
|
-
let a = [s[0], s[1] + m +
|
|
29864
|
-
let z = [t[0], t[1] - m -
|
|
29926
|
+
let a = [s[0], Math.max(s[1], box_s.y2) + m + 1];
|
|
29927
|
+
let z = [t[0], Math.min(t[1], box_t.y1) - m - 1];
|
|
29865
29928
|
let region = box_expand({
|
|
29866
29929
|
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
29867
29930
|
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
|
@@ -59254,13 +59317,15 @@ var TreedbTreeLayout = class extends BaseLayout {
|
|
|
59254
59317
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
59255
59318
|
* between rows (top to bottom), `-h` between columns. An edge that
|
|
59256
59319
|
* does not run forward goes round the two cards, the edges joining
|
|
59257
|
-
* the same two cards take lanes,
|
|
59258
|
-
*
|
|
59320
|
+
* the same two cards take lanes, the edges sharing an end turn at
|
|
59321
|
+
* staggered heights, and a line that would cross any other card is
|
|
59322
|
+
* routed round it (treedb_elbow.js).
|
|
59259
59323
|
************************************************************/
|
|
59260
59324
|
var ELBOW_RADIUS = 6;
|
|
59261
59325
|
function elbow_box(node) {
|
|
59262
59326
|
try {
|
|
59263
|
-
let b = node.
|
|
59327
|
+
let b = node.getRenderBounds();
|
|
59328
|
+
if (!b || b.isEmpty && b.isEmpty()) b = node.getShape("key").getBounds();
|
|
59264
59329
|
return {
|
|
59265
59330
|
x1: b.min[0],
|
|
59266
59331
|
y1: b.min[1],
|
|
@@ -59306,10 +59371,45 @@ function elbow_lane_of(edge) {
|
|
|
59306
59371
|
return 0;
|
|
59307
59372
|
}
|
|
59308
59373
|
}
|
|
59374
|
+
function elbow_stagger_of(edge, vertical) {
|
|
59375
|
+
let graph = edge.context && edge.context.graph;
|
|
59376
|
+
let element = edge.context && edge.context.element;
|
|
59377
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
59378
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
59379
|
+
if (!graph || !element || s === void 0 || t === void 0) return null;
|
|
59380
|
+
let across = vertical ? 0 : 1;
|
|
59381
|
+
let along = vertical ? 1 : 0;
|
|
59382
|
+
let pos = (id) => graph.getElementPosition(id);
|
|
59383
|
+
let port_of = (node_id, key) => {
|
|
59384
|
+
let node = element.getElement(node_id);
|
|
59385
|
+
let ports = node && typeof node.getPorts === "function" ? node.getPorts() : {};
|
|
59386
|
+
let port = key ? ports[key] : null;
|
|
59387
|
+
return port ? port.getPosition() : pos(node_id);
|
|
59388
|
+
};
|
|
59389
|
+
try {
|
|
59390
|
+
let ps = pos(s);
|
|
59391
|
+
let pt = pos(t);
|
|
59392
|
+
let dep = graph.getRelatedEdgesData(s, "out").filter((d) => d.source === s && pos(d.target)[along] > ps[along]).map((d) => ({
|
|
59393
|
+
id: d.id,
|
|
59394
|
+
span: Math.abs(pos(d.target)[across] - port_of(s, d.style && d.style.sourcePort)[across])
|
|
59395
|
+
}));
|
|
59396
|
+
let arr = graph.getRelatedEdgesData(t, "in").filter((d) => d.target === t && pos(d.source)[along] < pt[along]).map((d) => ({
|
|
59397
|
+
id: d.id,
|
|
59398
|
+
span: Math.abs(port_of(d.source, d.style && d.style.sourcePort)[across] - pt[across])
|
|
59399
|
+
}));
|
|
59400
|
+
return {
|
|
59401
|
+
dep: elbow_rank(dep, edge.id),
|
|
59402
|
+
arr: elbow_rank(arr, edge.id)
|
|
59403
|
+
};
|
|
59404
|
+
} catch (e) {
|
|
59405
|
+
log_error(`elbow edge ${edge.id}: cannot read the edges sharing its ends: ${e}`);
|
|
59406
|
+
return null;
|
|
59407
|
+
}
|
|
59408
|
+
}
|
|
59309
59409
|
var TreedbElbowV = class extends Polyline {
|
|
59310
59410
|
getControlPoints(attributes) {
|
|
59311
59411
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59312
|
-
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this));
|
|
59412
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), true, elbow_obstacles(this), elbow_stagger_of(this, true));
|
|
59313
59413
|
}
|
|
59314
59414
|
getKeyPath(attributes) {
|
|
59315
59415
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
|
@@ -59318,7 +59418,7 @@ var TreedbElbowV = class extends Polyline {
|
|
|
59318
59418
|
var TreedbElbowH = class extends Polyline {
|
|
59319
59419
|
getControlPoints(attributes) {
|
|
59320
59420
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
59321
|
-
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this));
|
|
59421
|
+
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode), elbow_lane_of(this), false, elbow_obstacles(this), elbow_stagger_of(this, false));
|
|
59322
59422
|
}
|
|
59323
59423
|
getKeyPath(attributes) {
|
|
59324
59424
|
return super.getKeyPath(Object.assign({}, attributes, { radius: attributes.radius || ELBOW_RADIUS }));
|
package/package.json
CHANGED
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -133,7 +133,7 @@ import {
|
|
|
133
133
|
fold_root_group_key,
|
|
134
134
|
} from "./treedb_fold_model.js";
|
|
135
135
|
import {layout_tree, layout_radial, layout_compact} from "./treedb_layout.js";
|
|
136
|
-
import {elbow_lane, elbow_route} from "./treedb_elbow.js";
|
|
136
|
+
import {elbow_lane, elbow_rank, elbow_route} from "./treedb_elbow.js";
|
|
137
137
|
|
|
138
138
|
import {
|
|
139
139
|
BaseLayout,
|
|
@@ -4224,16 +4224,23 @@ class TreedbTreeLayout extends BaseLayout
|
|
|
4224
4224
|
* dragged card, a fold or a layout needs nothing else. `-v` runs
|
|
4225
4225
|
* between rows (top to bottom), `-h` between columns. An edge that
|
|
4226
4226
|
* does not run forward goes round the two cards, the edges joining
|
|
4227
|
-
* the same two cards take lanes,
|
|
4228
|
-
*
|
|
4227
|
+
* the same two cards take lanes, the edges sharing an end turn at
|
|
4228
|
+
* staggered heights, and a line that would cross any other card is
|
|
4229
|
+
* routed round it (treedb_elbow.js).
|
|
4229
4230
|
************************************************************/
|
|
4230
4231
|
const ELBOW_RADIUS = 6;
|
|
4231
4232
|
|
|
4232
|
-
/* The box of a
|
|
4233
|
+
/* The box of a WHOLE node on the canvas -- the card, its ports and
|
|
4234
|
+
* its label -- for an elbow to keep clear of. The ports are the
|
|
4235
|
+
* reason it is not the key shape's box: they stick out of the
|
|
4236
|
+
* card, and a line over a port reads as a link to it. */
|
|
4233
4237
|
function elbow_box(node)
|
|
4234
4238
|
{
|
|
4235
4239
|
try {
|
|
4236
|
-
let b = node.
|
|
4240
|
+
let b = node.getRenderBounds();
|
|
4241
|
+
if(!b || b.isEmpty && b.isEmpty()) {
|
|
4242
|
+
b = node.getShape('key').getBounds();
|
|
4243
|
+
}
|
|
4237
4244
|
return {x1: b.min[0], y1: b.min[1], x2: b.max[0], y2: b.max[1]};
|
|
4238
4245
|
} catch(e) {
|
|
4239
4246
|
log_error(`elbow edge: no bounds for node ${node && node.id}: ${e}`);
|
|
@@ -4291,12 +4298,60 @@ function elbow_lane_of(edge)
|
|
|
4291
4298
|
}
|
|
4292
4299
|
}
|
|
4293
4300
|
|
|
4301
|
+
/* Where `edge` stands among the forward edges sharing one of its
|
|
4302
|
+
* ends, for the stagger (treedb_elbow.js): the ones leaving its
|
|
4303
|
+
* source card, and the ones reaching its target card. How far each
|
|
4304
|
+
* runs ACROSS is measured from the real port it leaves by -- the
|
|
4305
|
+
* hooks of one card sit at different places along its edge. */
|
|
4306
|
+
function elbow_stagger_of(edge, vertical)
|
|
4307
|
+
{
|
|
4308
|
+
let graph = edge.context && edge.context.graph;
|
|
4309
|
+
let element = edge.context && edge.context.element;
|
|
4310
|
+
let s = edge.sourceNode && edge.sourceNode.id;
|
|
4311
|
+
let t = edge.targetNode && edge.targetNode.id;
|
|
4312
|
+
if(!graph || !element || s === undefined || t === undefined) {
|
|
4313
|
+
return null;
|
|
4314
|
+
}
|
|
4315
|
+
let across = vertical? 0 : 1;
|
|
4316
|
+
let along = vertical? 1 : 0;
|
|
4317
|
+
let pos = (id) => graph.getElementPosition(id);
|
|
4318
|
+
let port_of = (node_id, key) => {
|
|
4319
|
+
let node = element.getElement(node_id);
|
|
4320
|
+
let ports = (node && typeof node.getPorts === 'function')? node.getPorts() : {};
|
|
4321
|
+
let port = key? ports[key] : null;
|
|
4322
|
+
return port? port.getPosition() : pos(node_id);
|
|
4323
|
+
};
|
|
4324
|
+
try {
|
|
4325
|
+
let ps = pos(s);
|
|
4326
|
+
let pt = pos(t);
|
|
4327
|
+
let dep = graph.getRelatedEdgesData(s, 'out')
|
|
4328
|
+
.filter((d) => d.source === s && pos(d.target)[along] > ps[along])
|
|
4329
|
+
.map((d) => ({
|
|
4330
|
+
id: d.id,
|
|
4331
|
+
span: Math.abs(pos(d.target)[across] -
|
|
4332
|
+
port_of(s, d.style && d.style.sourcePort)[across]),
|
|
4333
|
+
}));
|
|
4334
|
+
let arr = graph.getRelatedEdgesData(t, 'in')
|
|
4335
|
+
.filter((d) => d.target === t && pos(d.source)[along] < pt[along])
|
|
4336
|
+
.map((d) => ({
|
|
4337
|
+
id: d.id,
|
|
4338
|
+
span: Math.abs(port_of(d.source, d.style && d.style.sourcePort)[across] -
|
|
4339
|
+
pt[across]),
|
|
4340
|
+
}));
|
|
4341
|
+
return {dep: elbow_rank(dep, edge.id), arr: elbow_rank(arr, edge.id)};
|
|
4342
|
+
} catch(e) {
|
|
4343
|
+
log_error(`elbow edge ${edge.id}: cannot read the edges sharing its ends: ${e}`);
|
|
4344
|
+
return null;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4294
4348
|
class TreedbElbowV extends Polyline
|
|
4295
4349
|
{
|
|
4296
4350
|
getControlPoints(attributes) {
|
|
4297
4351
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4298
4352
|
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4299
|
-
elbow_lane_of(this), true, elbow_obstacles(this)
|
|
4353
|
+
elbow_lane_of(this), true, elbow_obstacles(this),
|
|
4354
|
+
elbow_stagger_of(this, true));
|
|
4300
4355
|
}
|
|
4301
4356
|
|
|
4302
4357
|
getKeyPath(attributes) {
|
|
@@ -4310,7 +4365,8 @@ class TreedbElbowH extends Polyline
|
|
|
4310
4365
|
getControlPoints(attributes) {
|
|
4311
4366
|
let [s, t] = this.getEndpoints(attributes, false);
|
|
4312
4367
|
return elbow_route(s, t, elbow_box(this.sourceNode), elbow_box(this.targetNode),
|
|
4313
|
-
elbow_lane_of(this), false, elbow_obstacles(this)
|
|
4368
|
+
elbow_lane_of(this), false, elbow_obstacles(this),
|
|
4369
|
+
elbow_stagger_of(this, false));
|
|
4314
4370
|
}
|
|
4315
4371
|
|
|
4316
4372
|
getKeyPath(attributes) {
|
package/src/treedb_elbow.js
CHANGED
|
@@ -25,6 +25,15 @@
|
|
|
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
|
+
*
|
|
28
37
|
* Written for the top-down reading; left to right is the same
|
|
29
38
|
* geometry with the axes swapped.
|
|
30
39
|
*
|
|
@@ -35,6 +44,7 @@
|
|
|
35
44
|
export const ELBOW_STEP = 10; /* between two lanes */
|
|
36
45
|
export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
|
|
37
46
|
export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
|
|
47
|
+
export const ELBOW_STAGGER_PAD = 6; /* staggered turns keep this far from the rows */
|
|
38
48
|
|
|
39
49
|
/************************************************************
|
|
40
50
|
* The lane of `id` among `ids`, the edges joining the same two
|
|
@@ -54,6 +64,24 @@ export function elbow_lane(ids, id)
|
|
|
54
64
|
return side * Math.ceil(k / 2);
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
/************************************************************
|
|
68
|
+
* The rank of `id` among `items` ([{id, span}]): the forward
|
|
69
|
+
* edges that share one end with it, each with how far it runs
|
|
70
|
+
* ACROSS. Farthest first; a tie goes to the edge made first.
|
|
71
|
+
* Returns {rank, count}.
|
|
72
|
+
************************************************************/
|
|
73
|
+
export function elbow_rank(items, id)
|
|
74
|
+
{
|
|
75
|
+
let sorted = items.slice().sort((a, b) => {
|
|
76
|
+
if(b.span !== a.span) {
|
|
77
|
+
return b.span - a.span;
|
|
78
|
+
}
|
|
79
|
+
return String(a.id).localeCompare(String(b.id), undefined, {numeric: true});
|
|
80
|
+
});
|
|
81
|
+
let rank = sorted.findIndex((it) => it.id === id);
|
|
82
|
+
return {rank: (rank < 0)? 0 : rank, count: sorted.length};
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
function swap(p)
|
|
58
86
|
{
|
|
59
87
|
return [p[1], p[0]];
|
|
@@ -71,21 +99,44 @@ function swap_box(b)
|
|
|
71
99
|
*
|
|
72
100
|
* vertical the rows are stacked (top to bottom); false
|
|
73
101
|
* when they are side by side (left to right)
|
|
102
|
+
* stagger {dep, arr}: the edge's rank among the forward
|
|
103
|
+
* edges leaving its source card and among those
|
|
104
|
+
* reaching its target card (elbow_rank). Absent,
|
|
105
|
+
* the channel is the middle.
|
|
74
106
|
************************************************************/
|
|
75
|
-
export function elbow_points(s, t, box_s, box_t, lane, vertical)
|
|
107
|
+
export function elbow_points(s, t, box_s, box_t, lane, vertical, stagger)
|
|
76
108
|
{
|
|
77
109
|
if(!vertical) {
|
|
78
|
-
return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true
|
|
79
|
-
|
|
110
|
+
return elbow_points(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
|
|
111
|
+
stagger).map(swap);
|
|
80
112
|
}
|
|
81
113
|
|
|
82
114
|
let shift = lane * ELBOW_STEP;
|
|
83
115
|
|
|
84
|
-
/* Forward: the channel half way
|
|
85
|
-
*
|
|
116
|
+
/* Forward: the channel half way between the two cards, each
|
|
117
|
+
* edge of a shared end at its own height round it, moved by the
|
|
118
|
+
* lane, and kept between the two rows. */
|
|
86
119
|
if(t[1] - s[1] > 2 * ELBOW_STEP) {
|
|
87
|
-
let
|
|
88
|
-
|
|
120
|
+
let lo = Math.max(s[1], box_s.y2);
|
|
121
|
+
let hi = Math.min(t[1], box_t.y1);
|
|
122
|
+
let mid = (lo + hi) / 2;
|
|
123
|
+
let st = stagger || {};
|
|
124
|
+
let group = null;
|
|
125
|
+
let k = 0;
|
|
126
|
+
if(st.dep && st.dep.count > 1) {
|
|
127
|
+
group = st.dep;
|
|
128
|
+
k = st.dep.rank; /* farthest highest */
|
|
129
|
+
} else if(st.arr && st.arr.count > 1) {
|
|
130
|
+
group = st.arr;
|
|
131
|
+
k = st.arr.count - 1 - st.arr.rank; /* farthest lowest */
|
|
132
|
+
}
|
|
133
|
+
if(group) {
|
|
134
|
+
let room = Math.max(0, hi - lo - 2 * ELBOW_STAGGER_PAD);
|
|
135
|
+
let step = Math.min(ELBOW_STEP, room / (group.count - 1));
|
|
136
|
+
mid = mid - (group.count - 1) * step / 2 + k * step;
|
|
137
|
+
}
|
|
138
|
+
mid += shift;
|
|
139
|
+
mid = Math.min(Math.max(mid, lo + 4), hi - 4);
|
|
89
140
|
return [[s[0], mid], [t[0], mid]];
|
|
90
141
|
}
|
|
91
142
|
|
|
@@ -96,8 +147,10 @@ export function elbow_points(s, t, box_s, box_t, lane, vertical)
|
|
|
96
147
|
let side = (lane < 0)
|
|
97
148
|
? Math.min(box_s.x1, box_t.x1) - ELBOW_CLEAR - Math.abs(shift)
|
|
98
149
|
: Math.max(box_s.x2, box_t.x2) + ELBOW_CLEAR + Math.abs(shift);
|
|
99
|
-
|
|
100
|
-
|
|
150
|
+
/* From the border of the whole box, which holds the ports: a
|
|
151
|
+
* port sticks out of its card, and the run has to clear it. */
|
|
152
|
+
let y1 = Math.max(s[1], box_s.y2) + out; /* a hook port is on the bottom edge */
|
|
153
|
+
let y2 = Math.min(t[1], box_t.y1) - out; /* an fkey port is on the top edge */
|
|
101
154
|
return [[s[0], y1], [side, y1], [side, y2], [t[0], y2]];
|
|
102
155
|
}
|
|
103
156
|
|
|
@@ -133,16 +186,33 @@ function segment_hits(a, c, b)
|
|
|
133
186
|
return x1 < b.x2 && x2 > b.x1 && y1 < b.y2 && y2 > b.y1;
|
|
134
187
|
}
|
|
135
188
|
|
|
189
|
+
function same_box(a, b)
|
|
190
|
+
{
|
|
191
|
+
return !!a && !!b && a.x1 === b.x1 && a.y1 === b.y1 && a.x2 === b.x2 && a.y2 === b.y2;
|
|
192
|
+
}
|
|
193
|
+
|
|
136
194
|
/************************************************************
|
|
137
195
|
* Does the polyline `points` (its ends included) cross any of
|
|
138
|
-
* `boxes`? A
|
|
139
|
-
*
|
|
196
|
+
* `boxes`? A box is shrunk by a pixel, so a line along its
|
|
197
|
+
* border is not a hit.
|
|
198
|
+
*
|
|
199
|
+
* A box is a whole node, its ports included, and the edge's own
|
|
200
|
+
* ports stick out of its own two cards: its first segment starts
|
|
201
|
+
* inside `box_s` and its last one ends inside `box_t`. Those two
|
|
202
|
+
* segments are not tested against their own box; every other
|
|
203
|
+
* segment is, so a line may not run back over its own cards.
|
|
140
204
|
************************************************************/
|
|
141
|
-
export function elbow_path_hits(points, boxes)
|
|
205
|
+
export function elbow_path_hits(points, boxes, box_s, box_t)
|
|
142
206
|
{
|
|
207
|
+
let last = points.length - 2;
|
|
143
208
|
for(let b of boxes) {
|
|
144
209
|
let inner = box_expand(b, -1);
|
|
145
|
-
|
|
210
|
+
let own_s = same_box(b, box_s);
|
|
211
|
+
let own_t = same_box(b, box_t);
|
|
212
|
+
for(let i = 0; i <= last; i++) {
|
|
213
|
+
if((own_s && i === 0) || (own_t && i === last)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
146
216
|
if(segment_hits(points[i], points[i + 1], inner)) {
|
|
147
217
|
return true;
|
|
148
218
|
}
|
|
@@ -196,8 +266,12 @@ function heap_pop(h)
|
|
|
196
266
|
/************************************************************
|
|
197
267
|
* The shortest orthogonal line from `a` to `z` that enters none
|
|
198
268
|
* of `boxes` (already expanded by the clearance), a turn costing
|
|
199
|
-
* ROUTE_BEND. The line leaves `a`
|
|
200
|
-
*
|
|
269
|
+
* ROUTE_BEND. The line leaves `a` SIDEWAYS and reaches `z`
|
|
270
|
+
* SIDEWAYS: the column straight under a port and the one straight
|
|
271
|
+
* over it belong to the forward edges that share that port, which
|
|
272
|
+
* run along them to their staggered turns -- a route going down
|
|
273
|
+
* that column lay on top of them. So a routed line shares only
|
|
274
|
+
* the stub between the port and `a` (or `z`).
|
|
201
275
|
*
|
|
202
276
|
* The grid is SPARSE: only the lines of the boxes' borders and
|
|
203
277
|
* of the two ends, which is all an orthogonal shortest path ever
|
|
@@ -283,7 +357,10 @@ function orth_search(a, z, boxes)
|
|
|
283
357
|
let k = state >> 2;
|
|
284
358
|
let dir = state & 3;
|
|
285
359
|
if(k === goal) {
|
|
286
|
-
|
|
360
|
+
if(dir >= 2) {
|
|
361
|
+
continue; /* down the port's own column: not allowed */
|
|
362
|
+
}
|
|
363
|
+
let total = cost + ROUTE_BEND; /* the turn into the port */
|
|
287
364
|
if(total < found_cost) {
|
|
288
365
|
found_cost = total;
|
|
289
366
|
found = state;
|
|
@@ -291,6 +368,9 @@ function orth_search(a, z, boxes)
|
|
|
291
368
|
continue;
|
|
292
369
|
}
|
|
293
370
|
for(let [nk, nd, len] of moves(k)) {
|
|
371
|
+
if(k === start && nd >= 2) {
|
|
372
|
+
continue; /* out of the port sideways, not down its column */
|
|
373
|
+
}
|
|
294
374
|
let c = cost + len + ((nd === dir)? 0 : ROUTE_BEND);
|
|
295
375
|
let ns = nk * 4 + nd;
|
|
296
376
|
if(c < best[ns]) {
|
|
@@ -332,28 +412,32 @@ function orth_search(a, z, boxes)
|
|
|
332
412
|
* port, the shortest way along the gaps between the cards, in.
|
|
333
413
|
* A parent and the row of children under it keep their bus.
|
|
334
414
|
*
|
|
335
|
-
* `boxes` are all the
|
|
336
|
-
* included
|
|
415
|
+
* `boxes` are all the nodes on screen, the two of the edge
|
|
416
|
+
* included, each as the box of the WHOLE node -- card, ports and
|
|
417
|
+
* label -- so a line clears the ports too. `box_s` and `box_t`
|
|
418
|
+
* are the edge's own two, measured the same way. The route keeps
|
|
419
|
+
* a clearance from every box, which
|
|
337
420
|
* grows with the lane so that two routed edges of one pair do not
|
|
338
421
|
* coincide. Nearby cards first; all of them if that finds no way;
|
|
339
422
|
* the simple elbow if nothing does.
|
|
340
423
|
************************************************************/
|
|
341
|
-
export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes)
|
|
424
|
+
export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger)
|
|
342
425
|
{
|
|
343
426
|
let others = boxes || [];
|
|
344
427
|
if(!vertical) {
|
|
345
428
|
return elbow_route(swap(s), swap(t), swap_box(box_s), swap_box(box_t), lane, true,
|
|
346
|
-
others.map(swap_box)).map(swap);
|
|
429
|
+
others.map(swap_box), stagger).map(swap);
|
|
347
430
|
}
|
|
348
431
|
|
|
349
|
-
let simple = elbow_points(s, t, box_s, box_t, lane, true);
|
|
350
|
-
if(!elbow_path_hits([s, ...simple, t], others)) {
|
|
432
|
+
let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
433
|
+
if(!elbow_path_hits([s, ...simple, t], others, box_s, box_t)) {
|
|
351
434
|
return simple;
|
|
352
435
|
}
|
|
353
436
|
|
|
354
437
|
let m = ELBOW_CLEAR / 2 + Math.abs(lane) * ELBOW_STEP;
|
|
355
|
-
|
|
356
|
-
let
|
|
438
|
+
/* Just outside the grown box of each end, which holds its ports. */
|
|
439
|
+
let a = [s[0], Math.max(s[1], box_s.y2) + m + 1]; /* out of a bottom port */
|
|
440
|
+
let z = [t[0], Math.min(t[1], box_t.y1) - m - 1]; /* into a top port */
|
|
357
441
|
let region = box_expand({
|
|
358
442
|
x1: Math.min(a[0], z[0], box_s.x1, box_t.x1),
|
|
359
443
|
y1: Math.min(a[1], z[1], box_s.y1, box_t.y1),
|
package/src/treedb_elbow.test.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
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_STEP, ELBOW_CLEAR,
|
|
11
11
|
} from "./treedb_elbow.js";
|
|
12
12
|
|
|
13
13
|
/* Two cards 100x40: A on top, B one row below it. */
|
|
@@ -165,6 +165,149 @@ describe("the route round the other cards", () => {
|
|
|
165
165
|
});
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
describe("edges sharing an end are staggered", () => {
|
|
169
|
+
test("the rank is farthest first, a tie to the edge made first", () => {
|
|
170
|
+
let items = [{id: "e-3", span: 10}, {id: "e-1", span: 300}, {id: "e-2", span: 10}];
|
|
171
|
+
expect(elbow_rank(items, "e-1")).toEqual({rank: 0, count: 3});
|
|
172
|
+
expect(elbow_rank(items, "e-2")).toEqual({rank: 1, count: 3});
|
|
173
|
+
expect(elbow_rank(items, "e-3")).toEqual({rank: 2, count: 3});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/* One parent on top (port at x 50, bottom at 40) and a row of
|
|
177
|
+
* children below (tops at 130). */
|
|
178
|
+
const P = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
179
|
+
const PORT = [50, 40];
|
|
180
|
+
const KIDS = [50, 160, 290, 420, -120].map((x, i) => ({
|
|
181
|
+
id: `e-${i + 1}`, x: x, box: {x1: x - 50, y1: 130, x2: x + 50, y2: 170},
|
|
182
|
+
}));
|
|
183
|
+
const channel_of = (kid) => {
|
|
184
|
+
let items = KIDS.map((k) => ({id: k.id, span: Math.abs(k.x - PORT[0])}));
|
|
185
|
+
let dep = elbow_rank(items, kid.id);
|
|
186
|
+
return elbow_points(PORT, [kid.x, 130], P, kid.box, 0, true, {dep: dep})[0][1];
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
test("the children of one port turn each at its own height, inside the channel", () => {
|
|
190
|
+
let ys = KIDS.map(channel_of);
|
|
191
|
+
expect(new Set(ys).size).toBe(KIDS.length);
|
|
192
|
+
for(let y of ys) {
|
|
193
|
+
expect(y).toBeGreaterThan(40);
|
|
194
|
+
expect(y).toBeLessThan(130);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("the farthest turns highest, and the lines of one port do not cross", () => {
|
|
199
|
+
let y = Object.fromEntries(KIDS.map((k) => [k.id, channel_of(k)]));
|
|
200
|
+
expect(y["e-4"]).toBeLessThan(y["e-2"]); /* 420 is farther than 160 */
|
|
201
|
+
/* A crossing: one edge's drop (at its child's x, from its turn
|
|
202
|
+
* down to the row) cut by another's run (from the port to its
|
|
203
|
+
* child, at its own turn). */
|
|
204
|
+
for(let a of KIDS) {
|
|
205
|
+
for(let b of KIDS) {
|
|
206
|
+
if(a === b) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
let lo = Math.min(PORT[0], b.x);
|
|
210
|
+
let hi = Math.max(PORT[0], b.x);
|
|
211
|
+
let cuts = a.x > lo && a.x < hi && y[b.id] > y[a.id] && y[b.id] < 130;
|
|
212
|
+
expect(cuts).toBe(false);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("the parents of one card: the farthest turns lowest, and they do not cross", () => {
|
|
218
|
+
/* Parents on top at these x (port at the middle of each bottom),
|
|
219
|
+
* one child below at x 200 whose fkeys all enter at its top. */
|
|
220
|
+
const C = {x1: 150, y1: 130, x2: 250, y2: 170};
|
|
221
|
+
const T0 = [200, 130];
|
|
222
|
+
const PARENTS = [0, 120, 330, 560].map((x, i) => ({id: `p-${i + 1}`, x: x}));
|
|
223
|
+
let items = PARENTS.map((p) => ({id: p.id, span: Math.abs(p.x - T0[0])}));
|
|
224
|
+
let y = {};
|
|
225
|
+
for(let p of PARENTS) {
|
|
226
|
+
let box = {x1: p.x - 50, y1: 0, x2: p.x + 50, y2: 40};
|
|
227
|
+
let arr = elbow_rank(items, p.id);
|
|
228
|
+
y[p.id] = elbow_points([p.x, 40], T0, box, C, 0, true, {arr: arr})[0][1];
|
|
229
|
+
}
|
|
230
|
+
expect(new Set(Object.values(y)).size).toBe(PARENTS.length);
|
|
231
|
+
expect(y["p-4"]).toBeGreaterThan(y["p-2"]); /* 560 is farther than 120 */
|
|
232
|
+
/* A crossing: one parent's drop (at its x, from its row down to
|
|
233
|
+
* its turn) cut by another's run (from its x to the child). */
|
|
234
|
+
for(let a of PARENTS) {
|
|
235
|
+
for(let b of PARENTS) {
|
|
236
|
+
if(a === b) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
let lo = Math.min(b.x, T0[0]);
|
|
240
|
+
let hi = Math.max(b.x, T0[0]);
|
|
241
|
+
let cuts = a.x > lo && a.x < hi && y[b.id] > 40 && y[b.id] < y[a.id];
|
|
242
|
+
expect(cuts).toBe(false);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("a routed edge leaves and reaches its ports sideways, sharing only the stub", () => {
|
|
248
|
+
/* A same-row edge from A to C, with D standing between them:
|
|
249
|
+
* it is routed. It must not run down the column over C's
|
|
250
|
+
* port, which is where a forward edge into C drops. */
|
|
251
|
+
const A2 = {x1: 0, y1: 200, x2: 100, y2: 240};
|
|
252
|
+
const D2 = {x1: 150, y1: 180, x2: 250, y2: 260};
|
|
253
|
+
const C2 = {x1: 300, y1: 200, x2: 400, y2: 240};
|
|
254
|
+
let s = [50, 240];
|
|
255
|
+
let t = [350, 200];
|
|
256
|
+
let boxes = [A2, D2, C2];
|
|
257
|
+
let route = elbow_route(s, t, A2, C2, 0, true, boxes);
|
|
258
|
+
expect(route.length).toBeGreaterThan(2);
|
|
259
|
+
expect(elbow_path_hits([s, ...route, t], boxes, A2, C2)).toBe(false);
|
|
260
|
+
let a = route[0];
|
|
261
|
+
let z = route[route.length - 1];
|
|
262
|
+
expect(route[1][1]).toBe(a[1]); /* leaves a sideways */
|
|
263
|
+
expect(route[route.length - 2][1]).toBe(z[1]); /* reaches z sideways */
|
|
264
|
+
expect(t[1] - z[1]).toBeLessThan(20); /* the stub is short */
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("an end shared by nobody keeps the middle", () => {
|
|
268
|
+
let one = {rank: 0, count: 1};
|
|
269
|
+
expect(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true, {dep: one, arr: one}))
|
|
270
|
+
.toEqual(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true));
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("the ports are obstacles too", () => {
|
|
275
|
+
/* Whole-node boxes: a port of 14px sticks 7px out of the bottom
|
|
276
|
+
* and the top of each card. */
|
|
277
|
+
const AP = {x1: 0, y1: -7, x2: 100, y2: 47};
|
|
278
|
+
const BP = {x1: 350, y1: 123, x2: 450, y2: 177};
|
|
279
|
+
const S = [50, 40];
|
|
280
|
+
const T2 = [400, 130];
|
|
281
|
+
|
|
282
|
+
test("an edge's own ports do not make its line a hit", () => {
|
|
283
|
+
let simple = elbow_points(S, T2, AP, BP, 0, true);
|
|
284
|
+
expect(elbow_path_hits([S, ...simple, T2], [AP, BP], AP, BP)).toBe(false);
|
|
285
|
+
expect(elbow_route(S, T2, AP, BP, 0, true, [AP, BP])).toEqual(simple);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("a port of a third card in the channel is gone round", () => {
|
|
289
|
+
/* F's card ends at y 80, above the channel (85); its port
|
|
290
|
+
* reaches down to 95, into it. */
|
|
291
|
+
const F_CARD = {x1: 200, y1: 0, x2: 300, y2: 80};
|
|
292
|
+
const F = {x1: 200, y1: -7, x2: 300, y2: 95};
|
|
293
|
+
let simple = elbow_points(S, T2, AP, BP, 0, true);
|
|
294
|
+
expect(elbow_path_hits([S, ...simple, T2], [AP, BP, F_CARD], AP, BP)).toBe(false);
|
|
295
|
+
expect(elbow_path_hits([S, ...simple, T2], [AP, BP, F], AP, BP)).toBe(true);
|
|
296
|
+
let route = elbow_route(S, T2, AP, BP, 0, true, [AP, BP, F]);
|
|
297
|
+
expect(elbow_path_hits([S, ...route, T2], [AP, BP, F], AP, BP)).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("a detour clears the ports of its own two cards", () => {
|
|
301
|
+
/* B below A, both with their ports: B -> A goes round. */
|
|
302
|
+
const AB = {x1: 0, y1: -7, x2: 100, y2: 47};
|
|
303
|
+
const BB = {x1: 0, y1: 123, x2: 100, y2: 177};
|
|
304
|
+
let pts = elbow_points([60, 170], [50, 0], BB, AB, 0, true);
|
|
305
|
+
expect(pts[0][1]).toBeGreaterThan(BB.y2);
|
|
306
|
+
expect(pts[3][1]).toBeLessThan(AB.y1);
|
|
307
|
+
expect(elbow_path_hits([[60, 170], ...pts, [50, 0]], [AB, BB], BB, AB)).toBe(false);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
168
311
|
describe("read left to right", () => {
|
|
169
312
|
test("is the same geometry with the axes swapped", () => {
|
|
170
313
|
let sw = (p) => [p[1], p[0]];
|