@yuneta/gobj-ui 7.23.153 → 7.23.155
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 +22 -2
- package/dist/gobj-ui.cjs.js +424 -43
- package/dist/gobj-ui.es.js +425 -44
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +81 -12
- package/src/treedb_elbow.js +292 -22
- package/src/treedb_elbow.test.js +130 -1
- package/src/treedb_layout.js +146 -14
- package/src/treedb_layout.test.js +84 -12
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -133,7 +133,10 @@ 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 {
|
|
136
|
+
import {
|
|
137
|
+
elbow_lane, elbow_rank, elbow_route, elbow_combs, curve_hits, rounded_path, CURVE_RADIUS,
|
|
138
|
+
ELBOW_STEP,
|
|
139
|
+
} from "./treedb_elbow.js";
|
|
137
140
|
|
|
138
141
|
import {
|
|
139
142
|
BaseLayout,
|
|
@@ -148,6 +151,8 @@ import {
|
|
|
148
151
|
Toolbar,
|
|
149
152
|
register,
|
|
150
153
|
Polyline,
|
|
154
|
+
Cubic,
|
|
155
|
+
CubicHorizontal,
|
|
151
156
|
} from '@antv/g6';
|
|
152
157
|
|
|
153
158
|
import {Circle as CircleGeometry, Rect as RectGeometry} from '@antv/g';
|
|
@@ -942,6 +947,8 @@ function register_layouts(gobj)
|
|
|
942
947
|
register(ExtensionCategory.LAYOUT, 'treedb-compact', TreedbCompactLayout);
|
|
943
948
|
register(ExtensionCategory.EDGE, 'treedb-elbow-v', TreedbElbowV);
|
|
944
949
|
register(ExtensionCategory.EDGE, 'treedb-elbow-h', TreedbElbowH);
|
|
950
|
+
register(ExtensionCategory.EDGE, 'treedb-curve-v', TreedbCurveV);
|
|
951
|
+
register(ExtensionCategory.EDGE, 'treedb-curve-h', TreedbCurveH);
|
|
945
952
|
register(ExtensionCategory.LAYOUT, 'treedb-radial', TreedbRadialLayout);
|
|
946
953
|
register(ExtensionCategory.NODE, 'light', LightNode);
|
|
947
954
|
register(ExtensionCategory.NODE, 'treedb-card', TreedbCard);
|
|
@@ -2042,16 +2049,20 @@ function layout_direction(gobj)
|
|
|
2042
2049
|
return str_in_list(LR_LAYOUTS, gobj.priv.layout)? "LR" : "TB";
|
|
2043
2050
|
}
|
|
2044
2051
|
|
|
2045
|
-
/*
|
|
2046
|
-
* has rows
|
|
2052
|
+
/* An elbow when the reader asked for one, a curve otherwise -- ours
|
|
2053
|
+
* where the layout has rows, which keeps off the cards; G6's plain
|
|
2054
|
+
* cubic on a layout with no rows to go round by. */
|
|
2047
2055
|
function edge_type_for(gobj)
|
|
2048
2056
|
{
|
|
2049
2057
|
let lr = (layout_direction(gobj) === "LR");
|
|
2050
2058
|
let rowless = gobj_read_attr(gobj, "rowless_layouts") || [];
|
|
2051
|
-
|
|
2052
|
-
|
|
2059
|
+
let rows = !str_in_list(rowless, gobj.priv.layout);
|
|
2060
|
+
if(rows && gobj_read_str_attr(gobj, "edge_shape") === "elbow") {
|
|
2053
2061
|
return lr? "treedb-elbow-h" : "treedb-elbow-v";
|
|
2054
2062
|
}
|
|
2063
|
+
if(rows) {
|
|
2064
|
+
return lr? "treedb-curve-h" : "treedb-curve-v";
|
|
2065
|
+
}
|
|
2055
2066
|
return lr? "cubic-horizontal" : "cubic";
|
|
2056
2067
|
}
|
|
2057
2068
|
|
|
@@ -4324,13 +4335,22 @@ function elbow_stagger_of(edge, vertical)
|
|
|
4324
4335
|
try {
|
|
4325
4336
|
let ps = pos(s);
|
|
4326
4337
|
let pt = pos(t);
|
|
4327
|
-
let
|
|
4328
|
-
.filter((d) => d.source === s && pos(d.target)[along] > ps[along])
|
|
4329
|
-
|
|
4338
|
+
let leaving = graph.getRelatedEdgesData(s, 'out')
|
|
4339
|
+
.filter((d) => d.source === s && pos(d.target)[along] > ps[along]);
|
|
4340
|
+
/* The cards they reach, for the combs of a stack; a comb's
|
|
4341
|
+
* run ends at its lane, so that is what its span measures. */
|
|
4342
|
+
let combs = elbow_combs(leaving.map((d) => ({
|
|
4343
|
+
id: d.id,
|
|
4344
|
+
box: elbow_box(element.getElement(d.target)),
|
|
4345
|
+
})), vertical);
|
|
4346
|
+
let dep = leaving.map((d) => {
|
|
4347
|
+
let comb = combs.get(d.id);
|
|
4348
|
+
let far = comb? comb.lane : pos(d.target)[across];
|
|
4349
|
+
return {
|
|
4330
4350
|
id: d.id,
|
|
4331
|
-
span: Math.abs(
|
|
4332
|
-
|
|
4333
|
-
|
|
4351
|
+
span: Math.abs(far - port_of(s, d.style && d.style.sourcePort)[across]),
|
|
4352
|
+
};
|
|
4353
|
+
});
|
|
4334
4354
|
let arr = graph.getRelatedEdgesData(t, 'in')
|
|
4335
4355
|
.filter((d) => d.target === t && pos(d.source)[along] < pt[along])
|
|
4336
4356
|
.map((d) => ({
|
|
@@ -4338,7 +4358,11 @@ function elbow_stagger_of(edge, vertical)
|
|
|
4338
4358
|
span: Math.abs(port_of(d.source, d.style && d.style.sourcePort)[across] -
|
|
4339
4359
|
pt[across]),
|
|
4340
4360
|
}));
|
|
4341
|
-
return {
|
|
4361
|
+
return {
|
|
4362
|
+
dep: elbow_rank(dep, edge.id),
|
|
4363
|
+
arr: elbow_rank(arr, edge.id),
|
|
4364
|
+
comb: combs.get(edge.id) || null,
|
|
4365
|
+
};
|
|
4342
4366
|
} catch(e) {
|
|
4343
4367
|
log_error(`elbow edge ${edge.id}: cannot read the edges sharing its ends: ${e}`);
|
|
4344
4368
|
return null;
|
|
@@ -4375,6 +4399,51 @@ class TreedbElbowH extends Polyline
|
|
|
4375
4399
|
}
|
|
4376
4400
|
}
|
|
4377
4401
|
|
|
4402
|
+
/************************************************************
|
|
4403
|
+
* A CURVED edge that keeps off the cards. G6's cubic, as before,
|
|
4404
|
+
* unless that very curve -- sampled, not guessed from the
|
|
4405
|
+
* rectangle round it -- would cross a card other than its own two,
|
|
4406
|
+
* or when it does not run forward and so would cross its own:
|
|
4407
|
+
* the card above a lower row of a stack, a row between a parent
|
|
4408
|
+
* and a far child. Then it goes the way an elbow would (the comb,
|
|
4409
|
+
* the detour, the route: the same lanes and staggered turns) with
|
|
4410
|
+
* round corners, so it still reads as a curve. `-v` for rows
|
|
4411
|
+
* stacked top to bottom, `-h` for columns.
|
|
4412
|
+
************************************************************/
|
|
4413
|
+
function curve_key_path(edge, attributes, vertical, plain)
|
|
4414
|
+
{
|
|
4415
|
+
let [s, t] = edge.getEndpoints(attributes, false);
|
|
4416
|
+
let along = vertical? 1 : 0;
|
|
4417
|
+
let box_s = elbow_box(edge.sourceNode);
|
|
4418
|
+
let box_t = elbow_box(edge.targetNode);
|
|
4419
|
+
let boxes = elbow_obstacles(edge);
|
|
4420
|
+
/* An edge that does not run forward -- one of every reciprocal
|
|
4421
|
+
* pair, an edge within one row -- runs through its OWN two cards
|
|
4422
|
+
* as a cubic, which curve_hits does not look at: it goes round
|
|
4423
|
+
* them, as its elbow does. */
|
|
4424
|
+
let forward = (t[along] - s[along] > 2 * ELBOW_STEP);
|
|
4425
|
+
if(forward && !curve_hits(plain, boxes, box_s, box_t)) {
|
|
4426
|
+
return plain;
|
|
4427
|
+
}
|
|
4428
|
+
let pts = elbow_route(s, t, box_s, box_t, elbow_lane_of(edge), vertical, boxes,
|
|
4429
|
+
elbow_stagger_of(edge, vertical));
|
|
4430
|
+
return rounded_path([s, ...pts, t], CURVE_RADIUS);
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
class TreedbCurveV extends Cubic
|
|
4434
|
+
{
|
|
4435
|
+
getKeyPath(attributes) {
|
|
4436
|
+
return curve_key_path(this, attributes, true, super.getKeyPath(attributes));
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
class TreedbCurveH extends CubicHorizontal
|
|
4441
|
+
{
|
|
4442
|
+
getKeyPath(attributes) {
|
|
4443
|
+
return curve_key_path(this, attributes, false, super.getKeyPath(attributes));
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4378
4447
|
class TreedbCompactLayout extends BaseLayout
|
|
4379
4448
|
{
|
|
4380
4449
|
async execute(data, options) {
|
package/src/treedb_elbow.js
CHANGED
|
@@ -34,6 +34,16 @@
|
|
|
34
34
|
* farthest turns lowest -- the two orders in which the lines of
|
|
35
35
|
* one end nest without crossing.
|
|
36
36
|
*
|
|
37
|
+
* - A card in a lower row of a STACK (treedb_layout.js lays a
|
|
38
|
+
* run of leaf children out as two columns with a corridor
|
|
39
|
+
* between them): the card above it stands in the way of the
|
|
40
|
+
* channel, so its edge runs down the corridor in a lane of
|
|
41
|
+
* its own -- a COMB -- and turns into the gap above its card.
|
|
42
|
+
*
|
|
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
|
+
*
|
|
37
47
|
* Written for the top-down reading; left to right is the same
|
|
38
48
|
* geometry with the axes swapped.
|
|
39
49
|
*
|
|
@@ -41,6 +51,8 @@
|
|
|
41
51
|
* All Rights Reserved.
|
|
42
52
|
***********************************************************************/
|
|
43
53
|
|
|
54
|
+
import {STACK_GAP, STACK_LANE_PAD, STACK_LANE_STEP, STACK_TRUNK_MIN} from "./treedb_layout.js";
|
|
55
|
+
|
|
44
56
|
export const ELBOW_STEP = 10; /* between two lanes */
|
|
45
57
|
export const ELBOW_JETTY = 16; /* straight run out of a port before a detour turns */
|
|
46
58
|
export const ELBOW_CLEAR = 24; /* how far outside the two cards a detour runs */
|
|
@@ -92,6 +104,154 @@ function swap_box(b)
|
|
|
92
104
|
return {x1: b.y1, y1: b.x1, x2: b.y2, y2: b.x2};
|
|
93
105
|
}
|
|
94
106
|
|
|
107
|
+
/* Where a forward edge turns between `lo` and `hi`: the middle, or
|
|
108
|
+
* its staggered place among the edges sharing one of its ends --
|
|
109
|
+
* leaving, the farthest highest; arriving, the farthest lowest. */
|
|
110
|
+
function staggered_turn(lo, hi, st)
|
|
111
|
+
{
|
|
112
|
+
let mid = (lo + hi) / 2;
|
|
113
|
+
let group = null;
|
|
114
|
+
let k = 0;
|
|
115
|
+
if(st.dep && st.dep.count > 1) {
|
|
116
|
+
group = st.dep;
|
|
117
|
+
k = st.dep.rank;
|
|
118
|
+
} else if(st.arr && st.arr.count > 1) {
|
|
119
|
+
group = st.arr;
|
|
120
|
+
k = st.arr.count - 1 - st.arr.rank;
|
|
121
|
+
}
|
|
122
|
+
if(group) {
|
|
123
|
+
let room = Math.max(0, hi - lo - 2 * ELBOW_STAGGER_PAD);
|
|
124
|
+
let step = Math.min(ELBOW_STEP, room / (group.count - 1));
|
|
125
|
+
mid = mid - (group.count - 1) * step / 2 + k * step;
|
|
126
|
+
}
|
|
127
|
+
return mid;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/************************************************************
|
|
131
|
+
* The combs of the stacks under one card.
|
|
132
|
+
*
|
|
133
|
+
* treedb_layout.js lays a run of leaf children out as two columns
|
|
134
|
+
* with a corridor between them. A card in the first row is reached
|
|
135
|
+
* from the channel like any other; one in a lower row cannot be --
|
|
136
|
+
* the card above it stands in the way -- so its edge runs down
|
|
137
|
+
* the corridor in a LANE of its own and turns into the gap above
|
|
138
|
+
* its card, which it shares only with the other card of its row,
|
|
139
|
+
* reached from the other side.
|
|
140
|
+
*
|
|
141
|
+
* targets [{id, box}]: the forward edges leaving one card,
|
|
142
|
+
* each with the box of the card it reaches
|
|
143
|
+
* vertical as in elbow_points; false swaps the boxes
|
|
144
|
+
*
|
|
145
|
+
* Returns Map id -> {lane, z, top}: the lane, the height of the
|
|
146
|
+
* gap it turns into, and the top of the stack's first row (where
|
|
147
|
+
* the channel over the stack ends). Only for a card that stands
|
|
148
|
+
* under another of the group, within a stack gap and lined up
|
|
149
|
+
* with it on the corridor side -- what a stack looks like.
|
|
150
|
+
*
|
|
151
|
+
* Lanes go outermost first, row by row: a line into a higher row
|
|
152
|
+
* keeps outside the lines going deeper, and none crosses another.
|
|
153
|
+
************************************************************/
|
|
154
|
+
export function elbow_combs(targets, vertical)
|
|
155
|
+
{
|
|
156
|
+
let list = vertical? targets : targets.map((it) => ({id: it.id, box: swap_box(it.box)}));
|
|
157
|
+
let cy = (b) => (b.y1 + b.y2) / 2;
|
|
158
|
+
let near = (a, b) => Math.abs(a - b) < 1.5;
|
|
159
|
+
|
|
160
|
+
let under = [];
|
|
161
|
+
for(let it of list) {
|
|
162
|
+
let b = it.box;
|
|
163
|
+
let above = null;
|
|
164
|
+
for(let o of list) {
|
|
165
|
+
let a = o.box;
|
|
166
|
+
if(o === it || a.y2 > b.y1 || b.y1 - a.y2 > STACK_GAP) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if(!(near(a.x2, b.x2) || near(a.x1, b.x1))) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if(!above || a.y2 > above.y2) {
|
|
173
|
+
above = a;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if(!above) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
/* Its column: lined up on the corridor side, which is the
|
|
180
|
+
* right edge of the first column and the left edge of the
|
|
181
|
+
* second. Two cards of one width line up on both; then the
|
|
182
|
+
* card beside it in its row says. */
|
|
183
|
+
let side;
|
|
184
|
+
if(near(above.x2, b.x2) && !near(above.x1, b.x1)) {
|
|
185
|
+
side = 0;
|
|
186
|
+
} else if(near(above.x1, b.x1) && !near(above.x2, b.x2)) {
|
|
187
|
+
side = 1;
|
|
188
|
+
} else {
|
|
189
|
+
let left_of_it = list.some((o) => o !== it && near(cy(o.box), cy(b)) &&
|
|
190
|
+
o.box.x2 <= b.x1);
|
|
191
|
+
side = left_of_it? 1 : 0;
|
|
192
|
+
}
|
|
193
|
+
under.push({id: it.id, box: b, above: above, side: side});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* The corridor of a card: from the first column's edge to the
|
|
197
|
+
* second's, read off any row of the stack that has both. */
|
|
198
|
+
let corridor = (u) => {
|
|
199
|
+
if(u.side === 0) {
|
|
200
|
+
let l = u.box.x2;
|
|
201
|
+
let col = list.filter((o) => near(o.box.x2, l));
|
|
202
|
+
let r = Infinity;
|
|
203
|
+
for(let c of col) {
|
|
204
|
+
for(let o of list) {
|
|
205
|
+
if(o !== c && near(cy(o.box), cy(c.box)) && o.box.x1 >= l - 0.5) {
|
|
206
|
+
r = Math.min(r, o.box.x1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return {l: l, r: isFinite(r)? r : l + STACK_TRUNK_MIN, col: col};
|
|
211
|
+
}
|
|
212
|
+
let r = u.box.x1;
|
|
213
|
+
let col = list.filter((o) => near(o.box.x1, r));
|
|
214
|
+
let l = -Infinity;
|
|
215
|
+
for(let c of col) {
|
|
216
|
+
for(let o of list) {
|
|
217
|
+
if(o !== c && near(cy(o.box), cy(c.box)) && o.box.x2 <= r + 0.5) {
|
|
218
|
+
l = Math.max(l, o.box.x2);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {l: isFinite(l)? l : r - STACK_TRUNK_MIN, r: r, col: col};
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
let groups = new Map();
|
|
226
|
+
for(let u of under) {
|
|
227
|
+
let c = corridor(u);
|
|
228
|
+
u.top = Math.min(...c.col.map((o) => o.box.y1));
|
|
229
|
+
let key = `${Math.round(c.l)}|${Math.round(c.r)}`;
|
|
230
|
+
if(!groups.has(key)) {
|
|
231
|
+
groups.set(key, {l: c.l, r: c.r, left: [], right: []});
|
|
232
|
+
}
|
|
233
|
+
groups.get(key)[(u.side === 0)? 'left' : 'right'].push(u);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let out = new Map();
|
|
237
|
+
for(let g of groups.values()) {
|
|
238
|
+
g.left.sort((a, b) => a.box.y1 - b.box.y1);
|
|
239
|
+
g.right.sort((a, b) => a.box.y1 - b.box.y1);
|
|
240
|
+
let n = g.left.length + g.right.length;
|
|
241
|
+
let room = g.r - g.l - 2 * STACK_LANE_PAD;
|
|
242
|
+
let step = (n > 1)? Math.max(0, Math.min(STACK_LANE_STEP, room / (n - 1))) : 0;
|
|
243
|
+
g.left.forEach((u, i) => {
|
|
244
|
+
out.set(u.id, {lane: g.l + STACK_LANE_PAD + i * step,
|
|
245
|
+
z: (u.above.y2 + u.box.y1) / 2, top: u.top});
|
|
246
|
+
});
|
|
247
|
+
g.right.forEach((u, j) => {
|
|
248
|
+
out.set(u.id, {lane: g.r - STACK_LANE_PAD - j * step,
|
|
249
|
+
z: (u.above.y2 + u.box.y1) / 2, top: u.top});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
95
255
|
/************************************************************
|
|
96
256
|
* The control points of an elbow from the port `s` to the port
|
|
97
257
|
* `t` ([x, y] each), knowing the boxes of the two cards
|
|
@@ -99,10 +259,12 @@ function swap_box(b)
|
|
|
99
259
|
*
|
|
100
260
|
* vertical the rows are stacked (top to bottom); false
|
|
101
261
|
* when they are side by side (left to right)
|
|
102
|
-
* stagger {dep, arr}: the edge's rank among the
|
|
103
|
-
* edges leaving its source card and among
|
|
104
|
-
* reaching its target card (elbow_rank)
|
|
105
|
-
* the
|
|
262
|
+
* stagger {dep, arr, comb}: the edge's rank among the
|
|
263
|
+
* forward edges leaving its source card and among
|
|
264
|
+
* those reaching its target card (elbow_rank), and
|
|
265
|
+
* its comb when the target is in a lower row of a
|
|
266
|
+
* stack (elbow_combs). Absent, the channel is the
|
|
267
|
+
* middle.
|
|
106
268
|
************************************************************/
|
|
107
269
|
export function elbow_points(s, t, box_s, box_t, lane, vertical, stagger)
|
|
108
270
|
{
|
|
@@ -115,27 +277,20 @@ export function elbow_points(s, t, box_s, box_t, lane, vertical, stagger)
|
|
|
115
277
|
|
|
116
278
|
/* Forward: the channel half way between the two cards, each
|
|
117
279
|
* edge of a shared end at its own height round it, moved by the
|
|
118
|
-
* lane, and kept between the two rows.
|
|
280
|
+
* lane, and kept between the two rows. Into a lower row of a
|
|
281
|
+
* stack: along the channel to its corridor lane, down the lane,
|
|
282
|
+
* and into the gap above its card. */
|
|
119
283
|
if(t[1] - s[1] > 2 * ELBOW_STEP) {
|
|
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
284
|
let st = stagger || {};
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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;
|
|
285
|
+
let lo = Math.max(s[1], box_s.y2);
|
|
286
|
+
if(st.comb) {
|
|
287
|
+
let c = st.comb;
|
|
288
|
+
let turn = staggered_turn(lo, c.top, {dep: st.dep}) + shift;
|
|
289
|
+
turn = Math.min(Math.max(turn, lo + 4), c.top - 4);
|
|
290
|
+
return [[s[0], turn], [c.lane, turn], [c.lane, c.z], [t[0], c.z]];
|
|
137
291
|
}
|
|
138
|
-
|
|
292
|
+
let hi = Math.min(t[1], box_t.y1);
|
|
293
|
+
let mid = staggered_turn(lo, hi, st) + shift;
|
|
139
294
|
mid = Math.min(Math.max(mid, lo + 4), hi - 4);
|
|
140
295
|
return [[s[0], mid], [t[0], mid]];
|
|
141
296
|
}
|
|
@@ -429,6 +584,12 @@ export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger)
|
|
|
429
584
|
others.map(swap_box), stagger).map(swap);
|
|
430
585
|
}
|
|
431
586
|
|
|
587
|
+
/* A comb runs where the layout left room for it: its corridor
|
|
588
|
+
* and its gap are empty by construction. */
|
|
589
|
+
if(stagger && stagger.comb) {
|
|
590
|
+
return elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
591
|
+
}
|
|
592
|
+
|
|
432
593
|
let simple = elbow_points(s, t, box_s, box_t, lane, true, stagger);
|
|
433
594
|
if(!elbow_path_hits([s, ...simple, t], others, box_s, box_t)) {
|
|
434
595
|
return simple;
|
|
@@ -452,3 +613,112 @@ export function elbow_route(s, t, box_s, box_t, lane, vertical, boxes, stagger)
|
|
|
452
613
|
}
|
|
453
614
|
return route || simple;
|
|
454
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
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
***********************************************************************/
|
|
8
8
|
import { describe, test, expect } from "vitest";
|
|
9
9
|
import {
|
|
10
|
-
elbow_lane, elbow_points, elbow_route, elbow_path_hits, elbow_rank,
|
|
10
|
+
elbow_lane, elbow_points, elbow_route, elbow_path_hits, elbow_rank, elbow_combs,
|
|
11
|
+
curve_hits, rounded_path, ELBOW_STEP, ELBOW_CLEAR,
|
|
11
12
|
} from "./treedb_elbow.js";
|
|
13
|
+
import { layout_compact } from "./treedb_layout.js";
|
|
12
14
|
|
|
13
15
|
/* Two cards 100x40: A on top, B one row below it. */
|
|
14
16
|
const A = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
@@ -271,6 +273,133 @@ describe("edges sharing an end are staggered", () => {
|
|
|
271
273
|
});
|
|
272
274
|
});
|
|
273
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
|
+
|
|
274
403
|
describe("the ports are obstacles too", () => {
|
|
275
404
|
/* Whole-node boxes: a port of 14px sticks 7px out of the bottom
|
|
276
405
|
* and the top of each card. */
|