@yuneta/gobj-ui 7.23.156 → 7.23.158
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 +38 -71
- package/dist/gobj-ui.cjs.js +299 -959
- package/dist/gobj-ui.es.js +300 -960
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +227 -241
- package/src/c_yui_treedb_graph.js +91 -13
- package/src/treedb_elbow.js +13 -344
- package/src/treedb_elbow.test.js +1 -236
- package/src/treedb_layout.js +39 -620
- package/src/treedb_layout.test.js +3 -269
package/src/treedb_elbow.test.js
CHANGED
|
@@ -7,10 +7,8 @@
|
|
|
7
7
|
***********************************************************************/
|
|
8
8
|
import { describe, test, expect } from "vitest";
|
|
9
9
|
import {
|
|
10
|
-
elbow_lane, elbow_points, elbow_route, elbow_path_hits,
|
|
11
|
-
curve_hits, rounded_path, ELBOW_STEP, ELBOW_CLEAR,
|
|
10
|
+
elbow_lane, elbow_points, elbow_route, elbow_path_hits, ELBOW_STEP, ELBOW_CLEAR,
|
|
12
11
|
} from "./treedb_elbow.js";
|
|
13
|
-
import { layout_compact } from "./treedb_layout.js";
|
|
14
12
|
|
|
15
13
|
/* Two cards 100x40: A on top, B one row below it. */
|
|
16
14
|
const A = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
@@ -167,239 +165,6 @@ describe("the route round the other cards", () => {
|
|
|
167
165
|
});
|
|
168
166
|
});
|
|
169
167
|
|
|
170
|
-
describe("edges sharing an end are staggered", () => {
|
|
171
|
-
test("the rank is farthest first, a tie to the edge made first", () => {
|
|
172
|
-
let items = [{id: "e-3", span: 10}, {id: "e-1", span: 300}, {id: "e-2", span: 10}];
|
|
173
|
-
expect(elbow_rank(items, "e-1")).toEqual({rank: 0, count: 3});
|
|
174
|
-
expect(elbow_rank(items, "e-2")).toEqual({rank: 1, count: 3});
|
|
175
|
-
expect(elbow_rank(items, "e-3")).toEqual({rank: 2, count: 3});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
/* One parent on top (port at x 50, bottom at 40) and a row of
|
|
179
|
-
* children below (tops at 130). */
|
|
180
|
-
const P = {x1: 0, y1: 0, x2: 100, y2: 40};
|
|
181
|
-
const PORT = [50, 40];
|
|
182
|
-
const KIDS = [50, 160, 290, 420, -120].map((x, i) => ({
|
|
183
|
-
id: `e-${i + 1}`, x: x, box: {x1: x - 50, y1: 130, x2: x + 50, y2: 170},
|
|
184
|
-
}));
|
|
185
|
-
const channel_of = (kid) => {
|
|
186
|
-
let items = KIDS.map((k) => ({id: k.id, span: Math.abs(k.x - PORT[0])}));
|
|
187
|
-
let dep = elbow_rank(items, kid.id);
|
|
188
|
-
return elbow_points(PORT, [kid.x, 130], P, kid.box, 0, true, {dep: dep})[0][1];
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
test("the children of one port turn each at its own height, inside the channel", () => {
|
|
192
|
-
let ys = KIDS.map(channel_of);
|
|
193
|
-
expect(new Set(ys).size).toBe(KIDS.length);
|
|
194
|
-
for(let y of ys) {
|
|
195
|
-
expect(y).toBeGreaterThan(40);
|
|
196
|
-
expect(y).toBeLessThan(130);
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("the farthest turns highest, and the lines of one port do not cross", () => {
|
|
201
|
-
let y = Object.fromEntries(KIDS.map((k) => [k.id, channel_of(k)]));
|
|
202
|
-
expect(y["e-4"]).toBeLessThan(y["e-2"]); /* 420 is farther than 160 */
|
|
203
|
-
/* A crossing: one edge's drop (at its child's x, from its turn
|
|
204
|
-
* down to the row) cut by another's run (from the port to its
|
|
205
|
-
* child, at its own turn). */
|
|
206
|
-
for(let a of KIDS) {
|
|
207
|
-
for(let b of KIDS) {
|
|
208
|
-
if(a === b) {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
let lo = Math.min(PORT[0], b.x);
|
|
212
|
-
let hi = Math.max(PORT[0], b.x);
|
|
213
|
-
let cuts = a.x > lo && a.x < hi && y[b.id] > y[a.id] && y[b.id] < 130;
|
|
214
|
-
expect(cuts).toBe(false);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("the parents of one card: the farthest turns lowest, and they do not cross", () => {
|
|
220
|
-
/* Parents on top at these x (port at the middle of each bottom),
|
|
221
|
-
* one child below at x 200 whose fkeys all enter at its top. */
|
|
222
|
-
const C = {x1: 150, y1: 130, x2: 250, y2: 170};
|
|
223
|
-
const T0 = [200, 130];
|
|
224
|
-
const PARENTS = [0, 120, 330, 560].map((x, i) => ({id: `p-${i + 1}`, x: x}));
|
|
225
|
-
let items = PARENTS.map((p) => ({id: p.id, span: Math.abs(p.x - T0[0])}));
|
|
226
|
-
let y = {};
|
|
227
|
-
for(let p of PARENTS) {
|
|
228
|
-
let box = {x1: p.x - 50, y1: 0, x2: p.x + 50, y2: 40};
|
|
229
|
-
let arr = elbow_rank(items, p.id);
|
|
230
|
-
y[p.id] = elbow_points([p.x, 40], T0, box, C, 0, true, {arr: arr})[0][1];
|
|
231
|
-
}
|
|
232
|
-
expect(new Set(Object.values(y)).size).toBe(PARENTS.length);
|
|
233
|
-
expect(y["p-4"]).toBeGreaterThan(y["p-2"]); /* 560 is farther than 120 */
|
|
234
|
-
/* A crossing: one parent's drop (at its x, from its row down to
|
|
235
|
-
* its turn) cut by another's run (from its x to the child). */
|
|
236
|
-
for(let a of PARENTS) {
|
|
237
|
-
for(let b of PARENTS) {
|
|
238
|
-
if(a === b) {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
let lo = Math.min(b.x, T0[0]);
|
|
242
|
-
let hi = Math.max(b.x, T0[0]);
|
|
243
|
-
let cuts = a.x > lo && a.x < hi && y[b.id] > 40 && y[b.id] < y[a.id];
|
|
244
|
-
expect(cuts).toBe(false);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("a routed edge leaves and reaches its ports sideways, sharing only the stub", () => {
|
|
250
|
-
/* A same-row edge from A to C, with D standing between them:
|
|
251
|
-
* it is routed. It must not run down the column over C's
|
|
252
|
-
* port, which is where a forward edge into C drops. */
|
|
253
|
-
const A2 = {x1: 0, y1: 200, x2: 100, y2: 240};
|
|
254
|
-
const D2 = {x1: 150, y1: 180, x2: 250, y2: 260};
|
|
255
|
-
const C2 = {x1: 300, y1: 200, x2: 400, y2: 240};
|
|
256
|
-
let s = [50, 240];
|
|
257
|
-
let t = [350, 200];
|
|
258
|
-
let boxes = [A2, D2, C2];
|
|
259
|
-
let route = elbow_route(s, t, A2, C2, 0, true, boxes);
|
|
260
|
-
expect(route.length).toBeGreaterThan(2);
|
|
261
|
-
expect(elbow_path_hits([s, ...route, t], boxes, A2, C2)).toBe(false);
|
|
262
|
-
let a = route[0];
|
|
263
|
-
let z = route[route.length - 1];
|
|
264
|
-
expect(route[1][1]).toBe(a[1]); /* leaves a sideways */
|
|
265
|
-
expect(route[route.length - 2][1]).toBe(z[1]); /* reaches z sideways */
|
|
266
|
-
expect(t[1] - z[1]).toBeLessThan(20); /* the stub is short */
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("an end shared by nobody keeps the middle", () => {
|
|
270
|
-
let one = {rank: 0, count: 1};
|
|
271
|
-
expect(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true, {dep: one, arr: one}))
|
|
272
|
-
.toEqual(elbow_points(PORT, [160, 130], P, KIDS[1].box, 0, true));
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
describe("the comb of a stack", () => {
|
|
277
|
-
/* A parent over eight leaves, laid out by the compact tree (a
|
|
278
|
-
* stack: four rows of two). Boxes as the graph measures them,
|
|
279
|
-
* with 14px ports sticking 7px out of the top and bottom. */
|
|
280
|
-
const P = {id: "p", w: 172, h: 96};
|
|
281
|
-
const LEAVES = [0, 1, 2, 3, 4, 5, 6, 7].map((i) => ({id: `l${i}`, w: 116, h: 40}));
|
|
282
|
-
const pos = layout_compact([P, ...LEAVES], LEAVES.map((l) => ({source: "p", target: l.id})),
|
|
283
|
-
{nodesep: 18, ranksep: 90});
|
|
284
|
-
const box = (n) => {
|
|
285
|
-
let c = pos.get(n.id);
|
|
286
|
-
return {x1: c.x - n.w / 2, y1: c.y - n.h / 2 - 7, x2: c.x + n.w / 2, y2: c.y + n.h / 2 + 7};
|
|
287
|
-
};
|
|
288
|
-
const BOX_P = box(P);
|
|
289
|
-
const PORT = [pos.get("p").x, pos.get("p").y + 48];
|
|
290
|
-
const targets = LEAVES.map((l, i) => ({id: `e-${i + 1}`, box: box(l), leaf: l}));
|
|
291
|
-
const combs = elbow_combs(targets.map((t) => ({id: t.id, box: t.box})), true);
|
|
292
|
-
const paths = targets.map((t) => {
|
|
293
|
-
let comb = combs.get(t.id) || null;
|
|
294
|
-
let items = targets.map((u) => {
|
|
295
|
-
let c = combs.get(u.id);
|
|
296
|
-
let far = c? c.lane : pos.get(u.leaf.id).x;
|
|
297
|
-
return {id: u.id, span: Math.abs(far - PORT[0])};
|
|
298
|
-
});
|
|
299
|
-
let end = [pos.get(t.leaf.id).x, t.box.y1 + 7];
|
|
300
|
-
let pts = elbow_points(PORT, end, BOX_P, t.box, 0, true,
|
|
301
|
-
{dep: elbow_rank(items, t.id), comb: comb});
|
|
302
|
-
return {id: t.id, box: t.box, full: [PORT, ...pts, end]};
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test("only the cards under another card get a comb", () => {
|
|
306
|
-
expect(combs.size).toBe(6);
|
|
307
|
-
expect(combs.has("e-1")).toBe(false);
|
|
308
|
-
expect(combs.has("e-2")).toBe(false);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test("no line crosses a card", () => {
|
|
312
|
-
let all = [BOX_P, ...targets.map((t) => t.box)];
|
|
313
|
-
for(let p of paths) {
|
|
314
|
-
expect(elbow_path_hits(p.full, all, BOX_P, p.box)).toBe(false);
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("no two lines lie on each other past the stub of the port", () => {
|
|
319
|
-
/* Two axis-aligned segments overlap when they are on one line
|
|
320
|
-
* and share more than a point of it. */
|
|
321
|
-
let overlap = (a, b, c, d) => {
|
|
322
|
-
let va = a[0] === b[0];
|
|
323
|
-
let vc = c[0] === d[0];
|
|
324
|
-
let ha = a[1] === b[1];
|
|
325
|
-
let hc = c[1] === d[1];
|
|
326
|
-
if(va && vc && Math.abs(a[0] - c[0]) < 0.5) {
|
|
327
|
-
return Math.min(Math.max(a[1], b[1]), Math.max(c[1], d[1])) -
|
|
328
|
-
Math.max(Math.min(a[1], b[1]), Math.min(c[1], d[1])) > 0.5;
|
|
329
|
-
}
|
|
330
|
-
if(ha && hc && Math.abs(a[1] - c[1]) < 0.5) {
|
|
331
|
-
return Math.min(Math.max(a[0], b[0]), Math.max(c[0], d[0])) -
|
|
332
|
-
Math.max(Math.min(a[0], b[0]), Math.min(c[0], d[0])) > 0.5;
|
|
333
|
-
}
|
|
334
|
-
return false;
|
|
335
|
-
};
|
|
336
|
-
for(let i = 0; i < paths.length; i++) {
|
|
337
|
-
for(let j = i + 1; j < paths.length; j++) {
|
|
338
|
-
let A = paths[i].full;
|
|
339
|
-
let B = paths[j].full;
|
|
340
|
-
for(let m = 1; m + 1 < A.length; m++) {
|
|
341
|
-
for(let n = 1; n + 1 < B.length; n++) {
|
|
342
|
-
expect(overlap(A[m], A[m + 1], B[n], B[n + 1])).toBe(false);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
test("a comb is drawn as laid out, not routed", () => {
|
|
350
|
-
let t = targets[6];
|
|
351
|
-
let c = combs.get(t.id);
|
|
352
|
-
let end = [pos.get(t.leaf.id).x, t.box.y1 + 7];
|
|
353
|
-
let st = {comb: c};
|
|
354
|
-
expect(elbow_route(PORT, end, BOX_P, t.box, 0, true, [BOX_P, ...targets.map((u) => u.box)], st))
|
|
355
|
-
.toEqual(elbow_points(PORT, end, BOX_P, t.box, 0, true, st));
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
describe("a curve that would cross a card", () => {
|
|
360
|
-
/* Whole-node boxes (ports 7px out of top and bottom). */
|
|
361
|
-
const PB = {x1: 0, y1: -7, x2: 172, y2: 103}; /* a parent */
|
|
362
|
-
const R0 = {x1: 0, y1: 183, x2: 116, y2: 237}; /* row 0 of a stack */
|
|
363
|
-
const R1 = {x1: 0, y1: 277, x2: 116, y2: 331}; /* row 1, under it */
|
|
364
|
-
const SIB = {x1: 200, y1: -7, x2: 372, y2: 103}; /* the parent's sibling */
|
|
365
|
-
const S = [86, 96];
|
|
366
|
-
|
|
367
|
-
/* The cubic G6 draws between two vertical ports: tangents along
|
|
368
|
-
* the line, as its default curveOffset gives. */
|
|
369
|
-
const cubic = (s, t) => [["M", s[0], s[1]],
|
|
370
|
-
["C", s[0], (s[1] + t[1]) / 2, t[0], (s[1] + t[1]) / 2, t[0], t[1]]];
|
|
371
|
-
|
|
372
|
-
test("a curve into a card of the next row crosses nothing", () => {
|
|
373
|
-
expect(curve_hits(cubic(S, [58, 190]), [PB, R0, R1, SIB], PB, R0)).toBe(false);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("a curve into a lower row of a stack crosses the card above it", () => {
|
|
377
|
-
expect(curve_hits(cubic(S, [58, 284]), [PB, R0, R1, SIB], PB, R1)).toBe(true);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
test("a curve that passes NEAR a card, beside its ports, does not count", () => {
|
|
381
|
-
/* N0 in the target row with its port poking 7px up, beside
|
|
382
|
-
* where the curve lands; the rectangle round the curve took it. */
|
|
383
|
-
const T0 = {x1: 250, y1: 183, x2: 366, y2: 237};
|
|
384
|
-
const N0 = {x1: 120, y1: 183, x2: 236, y2: 237};
|
|
385
|
-
expect(curve_hits(cubic(S, [308, 190]), [PB, SIB, T0, N0], PB, T0)).toBe(false);
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
test("the rounded path: a straight line is one run", () => {
|
|
389
|
-
expect(rounded_path([[0, 0], [0, 100]], 24)).toEqual([["M", 0, 0], ["L", 0, 100]]);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
test("the rounded path: each corner a quadratic, never past half a short run", () => {
|
|
393
|
-
let path = rounded_path([[0, 0], [0, 60], [100, 60], [100, 70]], 24);
|
|
394
|
-
expect(path[0]).toEqual(["M", 0, 0]);
|
|
395
|
-
expect(path[1]).toEqual(["L", 0, 36]); /* 24 before the corner */
|
|
396
|
-
expect(path[2]).toEqual(["Q", 0, 60, 24, 60]);
|
|
397
|
-
expect(path[3]).toEqual(["L", 95, 60]); /* the last run is 10: r = 5 */
|
|
398
|
-
expect(path[4]).toEqual(["Q", 100, 60, 100, 65]);
|
|
399
|
-
expect(path[5]).toEqual(["L", 100, 70]);
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
|
|
403
168
|
describe("the ports are obstacles too", () => {
|
|
404
169
|
/* Whole-node boxes: a port of 14px sticks 7px out of the bottom
|
|
405
170
|
* and the top of each card. */
|