@yuneta/gobj-ui 7.23.147 → 7.23.148

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 CHANGED
@@ -1387,8 +1387,8 @@ G6 5.1 registers these layouts, and this is how each meets that shape:
1387
1387
  | `circular`, `concentric`, `grid`, `mds`, `random` | rings, rings by degree, a grid, projection, noise | none knows a parent from a child |
1388
1388
  | `combo-combined`, `fishbone` | combos / cause-effect | not this data |
1389
1389
 
1390
- So two of our own, in `treedb_layout.js` (pure, tested), registered as G6
1391
- layouts by two thin adapters in `c_g6_nodes_tree.js`:
1390
+ So three of our own, in `treedb_layout.js` (pure, tested), registered as G6
1391
+ layouts by three thin adapters in `c_g6_nodes_tree.js`:
1392
1392
 
1393
1393
  - **`treedb-tree`** — the classic tidy tree, **top to bottom**. A row per
1394
1394
  depth as tall as its tallest card, a node centred over the block of its
@@ -1402,6 +1402,18 @@ layouts by two thin adapters in `c_g6_nodes_tree.js`:
1402
1402
  keeps the other reading for a host that wants it). The outline that shipped
1403
1403
  beside it (`treedb-outline`, one row per node) was removed in `7.23.75`: a
1404
1404
  list that indents is a JSON viewer, and this library already has one.
1405
+ - **`compact-tree`** (`treedb-compact`, `7.23.148`) — the same tree packed by
1406
+ **contour** instead of by block. The tidy tree gives each subtree its whole
1407
+ block, so a hall opened with twenty-four devices pushes the siblings of its
1408
+ ancestors apart at depths where nothing of theirs touches; here every
1409
+ sibling slides against the outline of the ones before it, and a closed
1410
+ branch sits beside an open one at the depth they share. Same rows and order
1411
+ as `treedb-tree`, so switching moves cards sideways and never reorders them.
1412
+ One region open with a 24-device hall is 70% of the tidy width on a treedb
1413
+ shaped like a real one; a uniform tree is the same width. The contour
1414
+ arithmetic is Moen's (1990) as mxGraph wrote it in `mxCompactTreeLayout`
1415
+ (JGraph Ltd, Apache-2.0), read in maxGraph — a port, not a dependency,
1416
+ because maxGraph's layouts drive its own graph model.
1405
1417
  - **`radial`** (`treedb-radial`) — the root in the middle, a ring per depth,
1406
1418
  every subtree an angular **sector** proportional to its leaves, and the
1407
1419
  radius of each ring the largest of three: one `ranksep` out from the ring
@@ -1413,7 +1425,7 @@ layouts by two thin adapters in `c_g6_nodes_tree.js`:
1413
1425
  construction, across the ring or along the radius; several roots share the
1414
1426
  circle around an empty centre.
1415
1427
 
1416
- Both take the **same spanning tree**, chosen deterministically: roots are the
1428
+ All three take the **same spanning tree**, chosen deterministically: roots are the
1417
1429
  nodes with no incoming edge, in node order; a node belongs to the **first
1418
1430
  parent that reaches it** in a breadth-first walk (the place, not the
1419
1431
  controller, because the place's column came first) and its other links are
@@ -29314,6 +29314,203 @@ function tidy_tree_lr(nodes, edges, o) {
29314
29314
  }
29315
29315
  return pos;
29316
29316
  }
29317
+ /************************************************************
29318
+ * The compact tree.
29319
+ *
29320
+ * The tidy tree above gives every subtree its whole BLOCK: open a
29321
+ * hall with twenty-four devices and its block widens, and with it
29322
+ * every block above it, so the siblings of its ancestors move apart
29323
+ * at depths where nothing of theirs touches. This one keeps the
29324
+ * CONTOUR of each subtree instead -- its outline on each side, depth
29325
+ * by depth -- and slides every sibling against the outline of the
29326
+ * ones before it. A folded region then sits as close to an open one
29327
+ * as their cards allow at each depth, which is what a folded treedb
29328
+ * is: one branch open among closed ones.
29329
+ *
29330
+ * Same spanning tree, same order, same rows as the tidy tree: only
29331
+ * the packing across the depths differs, so switching between the
29332
+ * two moves cards sideways and never reorders them.
29333
+ *
29334
+ * The contour arithmetic is Moen's ("Drawing dynamic trees", IEEE
29335
+ * Software, 1990) in the form mxGraph gave it in mxCompactTreeLayout
29336
+ * -- Copyright (c) 2006-2015 JGraph Ltd, Apache License 2.0 -- read in
29337
+ * maxGraph's CompactTreeLayout.ts: leaf, join, merge, offset and
29338
+ * bridge below follow that form. What is ours: it runs on the
29339
+ * spanning tree of a FOREST, every depth is a row as wide as its
29340
+ * widest card (mxGraph's `alignRanks`), several roots sit side by
29341
+ * side, and there is no recursion.
29342
+ ************************************************************/
29343
+ function layout_compact(nodes, edges, opts) {
29344
+ let o = Object.assign({}, DEFAULTS, opts || {});
29345
+ if (o.direction === "LR") return compact_tree_lr(nodes, edges, o);
29346
+ let lr = compact_tree_lr(nodes.map((n) => Object.assign({}, n, {
29347
+ w: n.h,
29348
+ h: n.w
29349
+ })), edges, o);
29350
+ let pos = /* @__PURE__ */ new Map();
29351
+ for (let [id, p] of lr) pos.set(id, {
29352
+ x: p.y,
29353
+ y: p.x
29354
+ });
29355
+ return pos;
29356
+ }
29357
+ function contour_line(dx, dy, next) {
29358
+ return {
29359
+ dx,
29360
+ dy,
29361
+ next: next || null
29362
+ };
29363
+ }
29364
+ function contour_offset(p1, p2, a1, a2, b1, b2) {
29365
+ if (b1 <= p1 || p1 + a1 <= 0) return 0;
29366
+ let d;
29367
+ if (b1 * a2 - a1 * b2 > 0) {
29368
+ if (p1 < 0) d = p1 * a2 / a1 - p2;
29369
+ else if (p1 > 0) d = p1 * b2 / b1 - p2;
29370
+ else d = -p2;
29371
+ } else if (b1 < p1 + a1) d = b2 - (p2 + (b1 - p1) * a2 / a1);
29372
+ else if (b1 > p1 + a1) d = (a1 + p1) * b2 / b1 - (p2 + a2);
29373
+ else d = b2 - (p2 + a2);
29374
+ return d > 0 ? d : 0;
29375
+ }
29376
+ function contour_bridge(line1, x1, y1, line2, x2, y2) {
29377
+ let dx = x2 + line2.dx - x1;
29378
+ let dy = line2.dx === 0 ? line2.dy : dx * line2.dy / line2.dx;
29379
+ let r = contour_line(dx, dy, line2.next);
29380
+ line1.next = contour_line(0, y2 + line2.dy - dy - y1, r);
29381
+ return r;
29382
+ }
29383
+ function contour_merge(p1, p2) {
29384
+ let x = 0;
29385
+ let y = 0;
29386
+ let total = 0;
29387
+ let upper = p1.lower_head;
29388
+ let lower = p2.upper_head;
29389
+ while (lower && upper) {
29390
+ let d = contour_offset(x, y, lower.dx, lower.dy, upper.dx, upper.dy);
29391
+ y += d;
29392
+ total += d;
29393
+ if (x + lower.dx <= upper.dx) {
29394
+ x += lower.dx;
29395
+ y += lower.dy;
29396
+ lower = lower.next;
29397
+ } else {
29398
+ x -= upper.dx;
29399
+ y -= upper.dy;
29400
+ upper = upper.next;
29401
+ }
29402
+ }
29403
+ if (lower) {
29404
+ let b = contour_bridge(p1.upper_tail, 0, 0, lower, x, y);
29405
+ p1.upper_tail = b.next ? p2.upper_tail : b;
29406
+ p1.lower_tail = p2.lower_tail;
29407
+ } else {
29408
+ let b = contour_bridge(p2.lower_tail, x, y, upper, 0, 0);
29409
+ if (!b.next) p1.lower_tail = b;
29410
+ }
29411
+ p1.lower_head = p2.lower_head;
29412
+ return total;
29413
+ }
29414
+ function compact_tree_lr(nodes, edges, o) {
29415
+ let t = spanning_tree(nodes, edges);
29416
+ let order = walk_order(t);
29417
+ let pos = /* @__PURE__ */ new Map();
29418
+ let col_w = [];
29419
+ for (let [id, d] of t.depth) col_w[d] = Math.max(col_w[d] || 0, t.by_id.get(id).w);
29420
+ let col_x = [];
29421
+ let x = 0;
29422
+ for (let d = 0; d < col_w.length; d++) {
29423
+ col_x[d] = x;
29424
+ x += (col_w[d] || 0) + o.ranksep;
29425
+ }
29426
+ let pad = o.nodesep / 2;
29427
+ let dist = o.nodesep;
29428
+ let level_distance = o.ranksep - pad;
29429
+ let tn = /* @__PURE__ */ new Map();
29430
+ for (let id of order) tn.set(id, {
29431
+ len: col_w[t.depth.get(id)],
29432
+ ext: t.by_id.get(id).h,
29433
+ off_x: 0,
29434
+ off_y: 0,
29435
+ y: 0,
29436
+ contour: null
29437
+ });
29438
+ for (let i = order.length - 1; i >= 0; i--) {
29439
+ let id = order[i];
29440
+ let n = tn.get(id);
29441
+ let kids = t.children.get(id) || [];
29442
+ if (!kids.length) {
29443
+ let upper = contour_line(n.len + dist, 0);
29444
+ let lower_tail = contour_line(0, -n.ext - dist);
29445
+ n.contour = {
29446
+ upper_head: upper,
29447
+ upper_tail: upper,
29448
+ lower_tail,
29449
+ lower_head: contour_line(n.len + dist, 0, lower_tail)
29450
+ };
29451
+ continue;
29452
+ }
29453
+ let first = tn.get(kids[0]);
29454
+ n.contour = first.contour;
29455
+ let h = first.ext + dist;
29456
+ let span = h;
29457
+ for (let k = 1; k < kids.length; k++) {
29458
+ let kid = tn.get(kids[k]);
29459
+ let d = contour_merge(n.contour, kid.contour);
29460
+ kid.off_y = d + h;
29461
+ kid.off_x = 0;
29462
+ h = kid.ext + dist;
29463
+ span += d + h;
29464
+ }
29465
+ let gap = pad + level_distance;
29466
+ let y2 = (span - n.ext) / 2 - pad;
29467
+ let y1 = y2 + n.ext + 2 * pad - span;
29468
+ first.off_x = gap + n.len;
29469
+ first.off_y = y1;
29470
+ n.contour.upper_head = contour_line(n.len, 0, contour_line(gap, y1, n.contour.upper_head));
29471
+ n.contour.lower_head = contour_line(n.len, 0, contour_line(gap, y2, n.contour.lower_head));
29472
+ }
29473
+ let root_of = /* @__PURE__ */ new Map();
29474
+ for (let id of order) {
29475
+ let n = tn.get(id);
29476
+ if (!root_of.has(id)) root_of.set(id, id);
29477
+ let kids = t.children.get(id) || [];
29478
+ if (!kids.length) continue;
29479
+ let running = n.y + tn.get(kids[0]).off_y;
29480
+ tn.get(kids[0]).y = running;
29481
+ root_of.set(kids[0], root_of.get(id));
29482
+ for (let k = 1; k < kids.length; k++) {
29483
+ let kid = tn.get(kids[k]);
29484
+ kid.y = running + kid.off_y;
29485
+ running = kid.y;
29486
+ root_of.set(kids[k], root_of.get(id));
29487
+ }
29488
+ }
29489
+ let lo = /* @__PURE__ */ new Map();
29490
+ let hi = /* @__PURE__ */ new Map();
29491
+ for (let id of order) {
29492
+ let r = root_of.get(id);
29493
+ let n = tn.get(id);
29494
+ lo.set(r, Math.min(lo.has(r) ? lo.get(r) : Infinity, n.y));
29495
+ hi.set(r, Math.max(hi.has(r) ? hi.get(r) : -Infinity, n.y + n.ext));
29496
+ }
29497
+ let shift = /* @__PURE__ */ new Map();
29498
+ let top = 0;
29499
+ for (let r of t.roots) {
29500
+ shift.set(r, top - lo.get(r));
29501
+ top += hi.get(r) - lo.get(r) + o.nodesep * 2;
29502
+ }
29503
+ for (let id of order) {
29504
+ let d = t.depth.get(id);
29505
+ let n = tn.get(id);
29506
+ let y = n.y + shift.get(root_of.get(id));
29507
+ pos.set(id, {
29508
+ x: col_x[d] + col_w[d] / 2,
29509
+ y: y + n.ext / 2
29510
+ });
29511
+ }
29512
+ return pos;
29513
+ }
29317
29514
  function extent_at(n, a) {
29318
29515
  if (!n) return 0;
29319
29516
  return Math.abs(n.w * Math.cos(a)) + Math.abs(n.h * Math.sin(a));
@@ -56297,6 +56494,12 @@ var _layouts = {
56297
56494
  nodesep: 18,
56298
56495
  ranksep: 90
56299
56496
  },
56497
+ "compact-tree": {
56498
+ type: "treedb-compact",
56499
+ direction: "TB",
56500
+ nodesep: 18,
56501
+ ranksep: 90
56502
+ },
56300
56503
  "dagre": {
56301
56504
  type: "dagre",
56302
56505
  rankdir: "LR",
@@ -56618,6 +56821,7 @@ function register_layouts(gobj) {
56618
56821
  _g6_extensions_registered = true;
56619
56822
  (0, _antv_g6.register)(_antv_g6.ExtensionCategory.LAYOUT, "manual", ManualLayout);
56620
56823
  (0, _antv_g6.register)(_antv_g6.ExtensionCategory.LAYOUT, "treedb-tree", TreedbTreeLayout);
56824
+ (0, _antv_g6.register)(_antv_g6.ExtensionCategory.LAYOUT, "treedb-compact", TreedbCompactLayout);
56621
56825
  (0, _antv_g6.register)(_antv_g6.ExtensionCategory.LAYOUT, "treedb-radial", TreedbRadialLayout);
56622
56826
  (0, _antv_g6.register)(_antv_g6.ExtensionCategory.NODE, "light", LightNode);
56623
56827
  (0, _antv_g6.register)(_antv_g6.ExtensionCategory.NODE, "treedb-card", TreedbCard);
@@ -58785,6 +58989,12 @@ var TreedbTreeLayout = class extends _antv_g6.BaseLayout {
58785
58989
  return treedb_layout_output(layout_tree(input.nodes, input.edges, options));
58786
58990
  }
58787
58991
  };
58992
+ var TreedbCompactLayout = class extends _antv_g6.BaseLayout {
58993
+ async execute(data, options) {
58994
+ let input = treedb_layout_input(data);
58995
+ return treedb_layout_output(layout_compact(input.nodes, input.edges, options));
58996
+ }
58997
+ };
58788
58998
  var TreedbRadialLayout = class extends _antv_g6.BaseLayout {
58789
58999
  async execute(data, options) {
58790
59000
  let input = treedb_layout_input(data);
@@ -64256,6 +64466,7 @@ function option_label(name) {
64256
64466
  case "edition": return (0, i18next.t)("edition");
64257
64467
  case "manual": return (0, i18next.t)("manual");
64258
64468
  case "treedb-tree": return (0, i18next.t)("treedb-tree");
64469
+ case "compact-tree": return (0, i18next.t)("compact-tree");
64259
64470
  case "radial": return (0, i18next.t)("radial");
64260
64471
  case "dagre": return (0, i18next.t)("dagre");
64261
64472
  case "antv-dagre": return (0, i18next.t)("antv-dagre");
@@ -29309,6 +29309,203 @@ function tidy_tree_lr(nodes, edges, o) {
29309
29309
  }
29310
29310
  return pos;
29311
29311
  }
29312
+ /************************************************************
29313
+ * The compact tree.
29314
+ *
29315
+ * The tidy tree above gives every subtree its whole BLOCK: open a
29316
+ * hall with twenty-four devices and its block widens, and with it
29317
+ * every block above it, so the siblings of its ancestors move apart
29318
+ * at depths where nothing of theirs touches. This one keeps the
29319
+ * CONTOUR of each subtree instead -- its outline on each side, depth
29320
+ * by depth -- and slides every sibling against the outline of the
29321
+ * ones before it. A folded region then sits as close to an open one
29322
+ * as their cards allow at each depth, which is what a folded treedb
29323
+ * is: one branch open among closed ones.
29324
+ *
29325
+ * Same spanning tree, same order, same rows as the tidy tree: only
29326
+ * the packing across the depths differs, so switching between the
29327
+ * two moves cards sideways and never reorders them.
29328
+ *
29329
+ * The contour arithmetic is Moen's ("Drawing dynamic trees", IEEE
29330
+ * Software, 1990) in the form mxGraph gave it in mxCompactTreeLayout
29331
+ * -- Copyright (c) 2006-2015 JGraph Ltd, Apache License 2.0 -- read in
29332
+ * maxGraph's CompactTreeLayout.ts: leaf, join, merge, offset and
29333
+ * bridge below follow that form. What is ours: it runs on the
29334
+ * spanning tree of a FOREST, every depth is a row as wide as its
29335
+ * widest card (mxGraph's `alignRanks`), several roots sit side by
29336
+ * side, and there is no recursion.
29337
+ ************************************************************/
29338
+ function layout_compact(nodes, edges, opts) {
29339
+ let o = Object.assign({}, DEFAULTS, opts || {});
29340
+ if (o.direction === "LR") return compact_tree_lr(nodes, edges, o);
29341
+ let lr = compact_tree_lr(nodes.map((n) => Object.assign({}, n, {
29342
+ w: n.h,
29343
+ h: n.w
29344
+ })), edges, o);
29345
+ let pos = /* @__PURE__ */ new Map();
29346
+ for (let [id, p] of lr) pos.set(id, {
29347
+ x: p.y,
29348
+ y: p.x
29349
+ });
29350
+ return pos;
29351
+ }
29352
+ function contour_line(dx, dy, next) {
29353
+ return {
29354
+ dx,
29355
+ dy,
29356
+ next: next || null
29357
+ };
29358
+ }
29359
+ function contour_offset(p1, p2, a1, a2, b1, b2) {
29360
+ if (b1 <= p1 || p1 + a1 <= 0) return 0;
29361
+ let d;
29362
+ if (b1 * a2 - a1 * b2 > 0) {
29363
+ if (p1 < 0) d = p1 * a2 / a1 - p2;
29364
+ else if (p1 > 0) d = p1 * b2 / b1 - p2;
29365
+ else d = -p2;
29366
+ } else if (b1 < p1 + a1) d = b2 - (p2 + (b1 - p1) * a2 / a1);
29367
+ else if (b1 > p1 + a1) d = (a1 + p1) * b2 / b1 - (p2 + a2);
29368
+ else d = b2 - (p2 + a2);
29369
+ return d > 0 ? d : 0;
29370
+ }
29371
+ function contour_bridge(line1, x1, y1, line2, x2, y2) {
29372
+ let dx = x2 + line2.dx - x1;
29373
+ let dy = line2.dx === 0 ? line2.dy : dx * line2.dy / line2.dx;
29374
+ let r = contour_line(dx, dy, line2.next);
29375
+ line1.next = contour_line(0, y2 + line2.dy - dy - y1, r);
29376
+ return r;
29377
+ }
29378
+ function contour_merge(p1, p2) {
29379
+ let x = 0;
29380
+ let y = 0;
29381
+ let total = 0;
29382
+ let upper = p1.lower_head;
29383
+ let lower = p2.upper_head;
29384
+ while (lower && upper) {
29385
+ let d = contour_offset(x, y, lower.dx, lower.dy, upper.dx, upper.dy);
29386
+ y += d;
29387
+ total += d;
29388
+ if (x + lower.dx <= upper.dx) {
29389
+ x += lower.dx;
29390
+ y += lower.dy;
29391
+ lower = lower.next;
29392
+ } else {
29393
+ x -= upper.dx;
29394
+ y -= upper.dy;
29395
+ upper = upper.next;
29396
+ }
29397
+ }
29398
+ if (lower) {
29399
+ let b = contour_bridge(p1.upper_tail, 0, 0, lower, x, y);
29400
+ p1.upper_tail = b.next ? p2.upper_tail : b;
29401
+ p1.lower_tail = p2.lower_tail;
29402
+ } else {
29403
+ let b = contour_bridge(p2.lower_tail, x, y, upper, 0, 0);
29404
+ if (!b.next) p1.lower_tail = b;
29405
+ }
29406
+ p1.lower_head = p2.lower_head;
29407
+ return total;
29408
+ }
29409
+ function compact_tree_lr(nodes, edges, o) {
29410
+ let t = spanning_tree(nodes, edges);
29411
+ let order = walk_order(t);
29412
+ let pos = /* @__PURE__ */ new Map();
29413
+ let col_w = [];
29414
+ for (let [id, d] of t.depth) col_w[d] = Math.max(col_w[d] || 0, t.by_id.get(id).w);
29415
+ let col_x = [];
29416
+ let x = 0;
29417
+ for (let d = 0; d < col_w.length; d++) {
29418
+ col_x[d] = x;
29419
+ x += (col_w[d] || 0) + o.ranksep;
29420
+ }
29421
+ let pad = o.nodesep / 2;
29422
+ let dist = o.nodesep;
29423
+ let level_distance = o.ranksep - pad;
29424
+ let tn = /* @__PURE__ */ new Map();
29425
+ for (let id of order) tn.set(id, {
29426
+ len: col_w[t.depth.get(id)],
29427
+ ext: t.by_id.get(id).h,
29428
+ off_x: 0,
29429
+ off_y: 0,
29430
+ y: 0,
29431
+ contour: null
29432
+ });
29433
+ for (let i = order.length - 1; i >= 0; i--) {
29434
+ let id = order[i];
29435
+ let n = tn.get(id);
29436
+ let kids = t.children.get(id) || [];
29437
+ if (!kids.length) {
29438
+ let upper = contour_line(n.len + dist, 0);
29439
+ let lower_tail = contour_line(0, -n.ext - dist);
29440
+ n.contour = {
29441
+ upper_head: upper,
29442
+ upper_tail: upper,
29443
+ lower_tail,
29444
+ lower_head: contour_line(n.len + dist, 0, lower_tail)
29445
+ };
29446
+ continue;
29447
+ }
29448
+ let first = tn.get(kids[0]);
29449
+ n.contour = first.contour;
29450
+ let h = first.ext + dist;
29451
+ let span = h;
29452
+ for (let k = 1; k < kids.length; k++) {
29453
+ let kid = tn.get(kids[k]);
29454
+ let d = contour_merge(n.contour, kid.contour);
29455
+ kid.off_y = d + h;
29456
+ kid.off_x = 0;
29457
+ h = kid.ext + dist;
29458
+ span += d + h;
29459
+ }
29460
+ let gap = pad + level_distance;
29461
+ let y2 = (span - n.ext) / 2 - pad;
29462
+ let y1 = y2 + n.ext + 2 * pad - span;
29463
+ first.off_x = gap + n.len;
29464
+ first.off_y = y1;
29465
+ n.contour.upper_head = contour_line(n.len, 0, contour_line(gap, y1, n.contour.upper_head));
29466
+ n.contour.lower_head = contour_line(n.len, 0, contour_line(gap, y2, n.contour.lower_head));
29467
+ }
29468
+ let root_of = /* @__PURE__ */ new Map();
29469
+ for (let id of order) {
29470
+ let n = tn.get(id);
29471
+ if (!root_of.has(id)) root_of.set(id, id);
29472
+ let kids = t.children.get(id) || [];
29473
+ if (!kids.length) continue;
29474
+ let running = n.y + tn.get(kids[0]).off_y;
29475
+ tn.get(kids[0]).y = running;
29476
+ root_of.set(kids[0], root_of.get(id));
29477
+ for (let k = 1; k < kids.length; k++) {
29478
+ let kid = tn.get(kids[k]);
29479
+ kid.y = running + kid.off_y;
29480
+ running = kid.y;
29481
+ root_of.set(kids[k], root_of.get(id));
29482
+ }
29483
+ }
29484
+ let lo = /* @__PURE__ */ new Map();
29485
+ let hi = /* @__PURE__ */ new Map();
29486
+ for (let id of order) {
29487
+ let r = root_of.get(id);
29488
+ let n = tn.get(id);
29489
+ lo.set(r, Math.min(lo.has(r) ? lo.get(r) : Infinity, n.y));
29490
+ hi.set(r, Math.max(hi.has(r) ? hi.get(r) : -Infinity, n.y + n.ext));
29491
+ }
29492
+ let shift = /* @__PURE__ */ new Map();
29493
+ let top = 0;
29494
+ for (let r of t.roots) {
29495
+ shift.set(r, top - lo.get(r));
29496
+ top += hi.get(r) - lo.get(r) + o.nodesep * 2;
29497
+ }
29498
+ for (let id of order) {
29499
+ let d = t.depth.get(id);
29500
+ let n = tn.get(id);
29501
+ let y = n.y + shift.get(root_of.get(id));
29502
+ pos.set(id, {
29503
+ x: col_x[d] + col_w[d] / 2,
29504
+ y: y + n.ext / 2
29505
+ });
29506
+ }
29507
+ return pos;
29508
+ }
29312
29509
  function extent_at(n, a) {
29313
29510
  if (!n) return 0;
29314
29511
  return Math.abs(n.w * Math.cos(a)) + Math.abs(n.h * Math.sin(a));
@@ -56292,6 +56489,12 @@ var _layouts = {
56292
56489
  nodesep: 18,
56293
56490
  ranksep: 90
56294
56491
  },
56492
+ "compact-tree": {
56493
+ type: "treedb-compact",
56494
+ direction: "TB",
56495
+ nodesep: 18,
56496
+ ranksep: 90
56497
+ },
56295
56498
  "dagre": {
56296
56499
  type: "dagre",
56297
56500
  rankdir: "LR",
@@ -56613,6 +56816,7 @@ function register_layouts(gobj) {
56613
56816
  _g6_extensions_registered = true;
56614
56817
  register(ExtensionCategory.LAYOUT, "manual", ManualLayout);
56615
56818
  register(ExtensionCategory.LAYOUT, "treedb-tree", TreedbTreeLayout);
56819
+ register(ExtensionCategory.LAYOUT, "treedb-compact", TreedbCompactLayout);
56616
56820
  register(ExtensionCategory.LAYOUT, "treedb-radial", TreedbRadialLayout);
56617
56821
  register(ExtensionCategory.NODE, "light", LightNode);
56618
56822
  register(ExtensionCategory.NODE, "treedb-card", TreedbCard);
@@ -58780,6 +58984,12 @@ var TreedbTreeLayout = class extends BaseLayout {
58780
58984
  return treedb_layout_output(layout_tree(input.nodes, input.edges, options));
58781
58985
  }
58782
58986
  };
58987
+ var TreedbCompactLayout = class extends BaseLayout {
58988
+ async execute(data, options) {
58989
+ let input = treedb_layout_input(data);
58990
+ return treedb_layout_output(layout_compact(input.nodes, input.edges, options));
58991
+ }
58992
+ };
58783
58993
  var TreedbRadialLayout = class extends BaseLayout {
58784
58994
  async execute(data, options) {
58785
58995
  let input = treedb_layout_input(data);
@@ -64251,6 +64461,7 @@ function option_label(name) {
64251
64461
  case "edition": return t("edition");
64252
64462
  case "manual": return t("manual");
64253
64463
  case "treedb-tree": return t("treedb-tree");
64464
+ case "compact-tree": return t("compact-tree");
64254
64465
  case "radial": return t("radial");
64255
64466
  case "dagre": return t("dagre");
64256
64467
  case "antv-dagre": return t("antv-dagre");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.147",
3
+ "version": "7.23.148",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -132,7 +132,7 @@ import {
132
132
  fold_split_group_key,
133
133
  fold_root_group_key,
134
134
  } from "./treedb_fold_model.js";
135
- import {layout_tree, layout_radial} from "./treedb_layout.js";
135
+ import {layout_tree, layout_radial, layout_compact} from "./treedb_layout.js";
136
136
 
137
137
  import {
138
138
  BaseLayout,
@@ -350,6 +350,16 @@ const _layouts = {
350
350
  nodesep: 18,
351
351
  ranksep: 90,
352
352
  },
353
+ /* The same tree packed by CONTOUR (Moen, as mxGraph drew it): a
354
+ * closed branch sits beside an open one at the depth they share,
355
+ * instead of beside the whole block of the open one. Same rows
356
+ * and order as `treedb-tree` (treedb_layout.js). */
357
+ "compact-tree": {
358
+ type: 'treedb-compact',
359
+ direction: 'TB',
360
+ nodesep: 18,
361
+ ranksep: 90,
362
+ },
353
363
  "dagre": {
354
364
  type: 'dagre',
355
365
  rankdir: 'LR',
@@ -925,6 +935,7 @@ function register_layouts(gobj)
925
935
  _g6_extensions_registered = true;
926
936
  register(ExtensionCategory.LAYOUT, 'manual', ManualLayout);
927
937
  register(ExtensionCategory.LAYOUT, 'treedb-tree', TreedbTreeLayout);
938
+ register(ExtensionCategory.LAYOUT, 'treedb-compact', TreedbCompactLayout);
928
939
  register(ExtensionCategory.LAYOUT, 'treedb-radial', TreedbRadialLayout);
929
940
  register(ExtensionCategory.NODE, 'light', LightNode);
930
941
  register(ExtensionCategory.NODE, 'treedb-card', TreedbCard);
@@ -4188,6 +4199,14 @@ class TreedbTreeLayout extends BaseLayout
4188
4199
  }
4189
4200
  }
4190
4201
 
4202
+ class TreedbCompactLayout extends BaseLayout
4203
+ {
4204
+ async execute(data, options) {
4205
+ let input = treedb_layout_input(data);
4206
+ return treedb_layout_output(layout_compact(input.nodes, input.edges, options));
4207
+ }
4208
+ }
4209
+
4191
4210
  class TreedbRadialLayout extends BaseLayout
4192
4211
  {
4193
4212
  async execute(data, options) {
@@ -553,6 +553,8 @@ function option_label(name)
553
553
  return t("manual");
554
554
  case "treedb-tree":
555
555
  return t("treedb-tree");
556
+ case "compact-tree":
557
+ return t("compact-tree");
556
558
  case "radial":
557
559
  return t("radial");
558
560
  case "dagre":
@@ -274,6 +274,264 @@ function tidy_tree_lr(nodes, edges, o)
274
274
  return pos;
275
275
  }
276
276
 
277
+ /************************************************************
278
+ * The compact tree.
279
+ *
280
+ * The tidy tree above gives every subtree its whole BLOCK: open a
281
+ * hall with twenty-four devices and its block widens, and with it
282
+ * every block above it, so the siblings of its ancestors move apart
283
+ * at depths where nothing of theirs touches. This one keeps the
284
+ * CONTOUR of each subtree instead -- its outline on each side, depth
285
+ * by depth -- and slides every sibling against the outline of the
286
+ * ones before it. A folded region then sits as close to an open one
287
+ * as their cards allow at each depth, which is what a folded treedb
288
+ * is: one branch open among closed ones.
289
+ *
290
+ * Same spanning tree, same order, same rows as the tidy tree: only
291
+ * the packing across the depths differs, so switching between the
292
+ * two moves cards sideways and never reorders them.
293
+ *
294
+ * The contour arithmetic is Moen's ("Drawing dynamic trees", IEEE
295
+ * Software, 1990) in the form mxGraph gave it in mxCompactTreeLayout
296
+ * -- Copyright (c) 2006-2015 JGraph Ltd, Apache License 2.0 -- read in
297
+ * maxGraph's CompactTreeLayout.ts: leaf, join, merge, offset and
298
+ * bridge below follow that form. What is ours: it runs on the
299
+ * spanning tree of a FOREST, every depth is a row as wide as its
300
+ * widest card (mxGraph's `alignRanks`), several roots sit side by
301
+ * side, and there is no recursion.
302
+ ************************************************************/
303
+ export function layout_compact(nodes, edges, opts)
304
+ {
305
+ let o = Object.assign({}, DEFAULTS, opts || {});
306
+ if(o.direction === "LR") {
307
+ return compact_tree_lr(nodes, edges, o);
308
+ }
309
+ let transposed = nodes.map((n) => Object.assign({}, n, {w: n.h, h: n.w}));
310
+ let lr = compact_tree_lr(transposed, edges, o);
311
+ let pos = new Map();
312
+ for(let [id, p] of lr) {
313
+ pos.set(id, {x: p.y, y: p.x});
314
+ }
315
+ return pos;
316
+ }
317
+
318
+ /* One segment of a contour: a step of `dx` along the depth and `dy`
319
+ * across it, then the next segment. */
320
+ function contour_line(dx, dy, next)
321
+ {
322
+ return {dx: dx, dy: dy, next: next || null};
323
+ }
324
+
325
+ /* How far the segment (a1, a2) at (p1, p2) has to move across to
326
+ * clear the segment (b1, b2) at the origin. */
327
+ function contour_offset(p1, p2, a1, a2, b1, b2)
328
+ {
329
+ if(b1 <= p1 || p1 + a1 <= 0) {
330
+ return 0;
331
+ }
332
+ let d;
333
+ let t = b1 * a2 - a1 * b2;
334
+ if(t > 0) {
335
+ if(p1 < 0) {
336
+ d = (p1 * a2) / a1 - p2;
337
+ } else if(p1 > 0) {
338
+ d = (p1 * b2) / b1 - p2;
339
+ } else {
340
+ d = -p2;
341
+ }
342
+ } else if(b1 < p1 + a1) {
343
+ d = b2 - (p2 + ((b1 - p1) * a2) / a1);
344
+ } else if(b1 > p1 + a1) {
345
+ d = ((a1 + p1) * b2) / b1 - (p2 + a2);
346
+ } else {
347
+ d = b2 - (p2 + a2);
348
+ }
349
+ return (d > 0)? d : 0;
350
+ }
351
+
352
+ function contour_bridge(line1, x1, y1, line2, x2, y2)
353
+ {
354
+ let dx = x2 + line2.dx - x1;
355
+ let dy = (line2.dx === 0)? line2.dy : (dx * line2.dy) / line2.dx;
356
+ let r = contour_line(dx, dy, line2.next);
357
+ line1.next = contour_line(0, y2 + line2.dy - dy - y1, r);
358
+ return r;
359
+ }
360
+
361
+ /* Slide contour `p2` against `p1` until they clear; returns how far,
362
+ * and leaves in `p1` the outline of the two together. */
363
+ function contour_merge(p1, p2)
364
+ {
365
+ let x = 0;
366
+ let y = 0;
367
+ let total = 0;
368
+ let upper = p1.lower_head;
369
+ let lower = p2.upper_head;
370
+
371
+ while(lower && upper) {
372
+ let d = contour_offset(x, y, lower.dx, lower.dy, upper.dx, upper.dy);
373
+ y += d;
374
+ total += d;
375
+ if(x + lower.dx <= upper.dx) {
376
+ x += lower.dx;
377
+ y += lower.dy;
378
+ lower = lower.next;
379
+ } else {
380
+ x -= upper.dx;
381
+ y -= upper.dy;
382
+ upper = upper.next;
383
+ }
384
+ }
385
+
386
+ if(lower) {
387
+ let b = contour_bridge(p1.upper_tail, 0, 0, lower, x, y);
388
+ p1.upper_tail = b.next? p2.upper_tail : b;
389
+ p1.lower_tail = p2.lower_tail;
390
+ } else {
391
+ let b = contour_bridge(p2.lower_tail, x, y, upper, 0, 0);
392
+ if(!b.next) {
393
+ p1.lower_tail = b;
394
+ }
395
+ }
396
+ p1.lower_head = p2.lower_head;
397
+ return total;
398
+ }
399
+
400
+ function compact_tree_lr(nodes, edges, o)
401
+ {
402
+ let t = spanning_tree(nodes, edges);
403
+ let order = walk_order(t);
404
+ let pos = new Map();
405
+
406
+ /* Columns, as in the tidy tree: a depth is as wide as its widest
407
+ * card, and a column's gap is `ranksep`. */
408
+ let col_w = [];
409
+ for(let [id, d] of t.depth) {
410
+ col_w[d] = Math.max(col_w[d] || 0, t.by_id.get(id).w);
411
+ }
412
+ let col_x = [];
413
+ let x = 0;
414
+ for(let d = 0; d < col_w.length; d++) {
415
+ col_x[d] = x;
416
+ x += (col_w[d] || 0) + o.ranksep;
417
+ }
418
+
419
+ /* mxGraph's nodeDistance is padding on EACH side of a card, so two
420
+ * neighbours end `nodesep` apart; its levelDistance is what is
421
+ * left of the column gap after that padding. */
422
+ let pad = o.nodesep / 2;
423
+ let dist = o.nodesep;
424
+ let level_distance = o.ranksep - pad;
425
+
426
+ /* `len` along the depth (the column's width: the rows are aligned),
427
+ * `ext` across it (the card's own height). */
428
+ let tn = new Map();
429
+ for(let id of order) {
430
+ tn.set(id, {
431
+ len: col_w[t.depth.get(id)],
432
+ ext: t.by_id.get(id).h,
433
+ off_x: 0,
434
+ off_y: 0,
435
+ y: 0,
436
+ contour: null,
437
+ });
438
+ }
439
+
440
+ /* Contours, children before parents. */
441
+ for(let i = order.length - 1; i >= 0; i--) {
442
+ let id = order[i];
443
+ let n = tn.get(id);
444
+ let kids = t.children.get(id) || [];
445
+
446
+ if(!kids.length) {
447
+ let upper = contour_line(n.len + dist, 0);
448
+ let lower_tail = contour_line(0, -n.ext - dist);
449
+ n.contour = {
450
+ upper_head: upper,
451
+ upper_tail: upper,
452
+ lower_tail: lower_tail,
453
+ lower_head: contour_line(n.len + dist, 0, lower_tail),
454
+ };
455
+ continue;
456
+ }
457
+
458
+ /* join: the children side by side, each slid against the
459
+ * outline of the ones before it. */
460
+ let first = tn.get(kids[0]);
461
+ n.contour = first.contour;
462
+ let h = first.ext + dist;
463
+ let span = h;
464
+ for(let k = 1; k < kids.length; k++) {
465
+ let kid = tn.get(kids[k]);
466
+ let d = contour_merge(n.contour, kid.contour);
467
+ kid.off_y = d + h;
468
+ kid.off_x = 0;
469
+ h = kid.ext + dist;
470
+ span += d + h;
471
+ }
472
+
473
+ /* attach: the parent centred across the span of its children,
474
+ * one column before them. */
475
+ let gap = pad + level_distance;
476
+ let y2 = (span - n.ext) / 2 - pad;
477
+ let y1 = y2 + n.ext + 2 * pad - span;
478
+ first.off_x = gap + n.len;
479
+ first.off_y = y1;
480
+ n.contour.upper_head = contour_line(n.len, 0,
481
+ contour_line(gap, y1, n.contour.upper_head));
482
+ n.contour.lower_head = contour_line(n.len, 0,
483
+ contour_line(gap, y2, n.contour.lower_head));
484
+ }
485
+
486
+ /* Positions across, parents before children: the first child at
487
+ * its offset from the parent, each next one at its offset from
488
+ * the one before. */
489
+ let root_of = new Map();
490
+ for(let id of order) {
491
+ let n = tn.get(id);
492
+ if(!root_of.has(id)) {
493
+ root_of.set(id, id);
494
+ }
495
+ let kids = t.children.get(id) || [];
496
+ if(!kids.length) {
497
+ continue;
498
+ }
499
+ let running = n.y + tn.get(kids[0]).off_y;
500
+ tn.get(kids[0]).y = running;
501
+ root_of.set(kids[0], root_of.get(id));
502
+ for(let k = 1; k < kids.length; k++) {
503
+ let kid = tn.get(kids[k]);
504
+ kid.y = running + kid.off_y;
505
+ running = kid.y;
506
+ root_of.set(kids[k], root_of.get(id));
507
+ }
508
+ }
509
+
510
+ /* Several roots: each tree after the one before, by its extent. */
511
+ let lo = new Map();
512
+ let hi = new Map();
513
+ for(let id of order) {
514
+ let r = root_of.get(id);
515
+ let n = tn.get(id);
516
+ lo.set(r, Math.min(lo.has(r)? lo.get(r) : Infinity, n.y));
517
+ hi.set(r, Math.max(hi.has(r)? hi.get(r) : -Infinity, n.y + n.ext));
518
+ }
519
+ let shift = new Map();
520
+ let top = 0;
521
+ for(let r of t.roots) {
522
+ shift.set(r, top - lo.get(r));
523
+ top += hi.get(r) - lo.get(r) + o.nodesep * 2;
524
+ }
525
+
526
+ for(let id of order) {
527
+ let d = t.depth.get(id);
528
+ let n = tn.get(id);
529
+ let y = n.y + shift.get(root_of.get(id));
530
+ pos.set(id, {x: col_x[d] + col_w[d] / 2, y: y + n.ext / 2});
531
+ }
532
+ return pos;
533
+ }
534
+
277
535
  /* How much of a card lies along the direction `a`: the projection
278
536
  * of its rectangle on that direction. Full width when the card is
279
537
  * read along its width, full height across it, and the honest
@@ -6,7 +6,7 @@
6
6
  * tidy tree's rows and centring, the radial tree's rings.
7
7
  ***********************************************************************/
8
8
  import { describe, test, expect } from "vitest";
9
- import { spanning_tree, layout_tree, layout_radial } from "./treedb_layout.js";
9
+ import { spanning_tree, layout_tree, layout_radial, layout_compact } from "./treedb_layout.js";
10
10
 
11
11
  /* es -> (norte, sur); norte -> nave; nave -> d0, d1 (hook rank 1) and
12
12
  * c1 (hook rank 2); c1 -> d0 as well (second parent). */
@@ -97,6 +97,107 @@ describe("the tidy tree, read down (the default)", () => {
97
97
  });
98
98
  });
99
99
 
100
+ /* Every two cards of one row stand at least `gap` apart. */
101
+ function expect_no_overlap_in_rows(list, pos, gap)
102
+ {
103
+ for(let i = 0; i < list.length; i++) {
104
+ for(let j = i + 1; j < list.length; j++) {
105
+ let a = list[i];
106
+ let b = list[j];
107
+ let pa = pos.get(a.id);
108
+ let pb = pos.get(b.id);
109
+ if(Math.abs(pa.y - pb.y) > 1e-6) {
110
+ continue;
111
+ }
112
+ expect(Math.abs(pa.x - pb.x) + 1e-6).toBeGreaterThanOrEqual((a.w + b.w) / 2 + gap);
113
+ }
114
+ }
115
+ }
116
+
117
+ function width_of(list, pos)
118
+ {
119
+ let lo = Infinity;
120
+ let hi = -Infinity;
121
+ for(let n of list) {
122
+ let p = pos.get(n.id);
123
+ lo = Math.min(lo, p.x - n.w / 2);
124
+ hi = Math.max(hi, p.x + n.w / 2);
125
+ }
126
+ return hi - lo;
127
+ }
128
+
129
+ /* r -> a, b; a -> hall -> six devices; b is a closed sibling. */
130
+ const wide = [N("r", 172, 96), N("a", 172, 96), N("b", 172, 96), N("hall", 172, 96)]
131
+ .concat([0, 1, 2, 3, 4, 5].map((i) => N(`d${i}`, 116, 40)));
132
+ const wide_edges = [
133
+ {source: "r", target: "a"}, {source: "r", target: "b"}, {source: "a", target: "hall"},
134
+ ].concat([0, 1, 2, 3, 4, 5].map((i) => ({source: "hall", target: `d${i}`})));
135
+
136
+ describe("the compact tree", () => {
137
+ test("the rows are the tidy tree's rows", () => {
138
+ let tidy = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100});
139
+ let pos = layout_compact(nodes, edges, {nodesep: 10, ranksep: 100});
140
+ for(let n of nodes) {
141
+ expect(pos.get(n.id).y).toBe(tidy.get(n.id).y);
142
+ }
143
+ });
144
+
145
+ test("no two cards of a row are closer than the gap", () => {
146
+ expect_no_overlap_in_rows(nodes, layout_compact(nodes, edges, {nodesep: 10, ranksep: 100}), 10);
147
+ expect_no_overlap_in_rows(wide, layout_compact(wide, wide_edges, {nodesep: 18, ranksep: 90}), 18);
148
+ });
149
+
150
+ test("a parent is centred over its children, which keep their order", () => {
151
+ let pos = layout_compact(nodes, edges, {nodesep: 10, ranksep: 100});
152
+ expect(pos.get("d0").x).toBeLessThan(pos.get("d1").x);
153
+ expect(pos.get("d1").x).toBeLessThan(pos.get("c1").x);
154
+ let first = pos.get("d0").x - 58;
155
+ let last = pos.get("c1").x + 86;
156
+ expect(pos.get("nave").x).toBeCloseTo((first + last) / 2, 6);
157
+ expect(pos.get("norte").x).toBeLessThan(pos.get("sur").x);
158
+ });
159
+
160
+ test("a closed sibling sits beside an open branch, not beside its block", () => {
161
+ let opts = {nodesep: 18, ranksep: 90};
162
+ let tidy = layout_tree(wide, wide_edges, opts);
163
+ let pos = layout_compact(wide, wide_edges, opts);
164
+ /* In the tidy tree `b` waits for the whole fan of devices; here it
165
+ * only has to clear `a`, the card beside it. */
166
+ expect(pos.get("b").x - pos.get("a").x).toBeLessThan(tidy.get("b").x - tidy.get("a").x);
167
+ expect(pos.get("b").x - pos.get("a").x).toBeGreaterThanOrEqual(172 + 18 - 1e-6);
168
+ expect(width_of(wide, pos)).toBeLessThanOrEqual(width_of(wide, tidy));
169
+ });
170
+
171
+ test("two trees sit side by side, not overlapped", () => {
172
+ let pos = layout_compact([N("a", 100, 50), N("b", 100, 50)], [], {nodesep: 10});
173
+ expect(pos.get("b").x - pos.get("a").x).toBe(100 + 20);
174
+ });
175
+
176
+ test("read right, it is the same tree with the axes swapped", () => {
177
+ let down = layout_compact(wide, wide_edges, {nodesep: 18, ranksep: 90});
178
+ let swapped = wide.map((n) => N(n.id, n.h, n.w));
179
+ let right = layout_compact(swapped, wide_edges, {nodesep: 18, ranksep: 90, direction: "LR"});
180
+ for(let n of wide) {
181
+ expect(right.get(n.id).x).toBeCloseTo(down.get(n.id).y, 6);
182
+ expect(right.get(n.id).y).toBeCloseTo(down.get(n.id).x, 6);
183
+ }
184
+ });
185
+
186
+ test("a tree as deep as the data places every node, without recursion", () => {
187
+ let chain = [];
188
+ let links = [];
189
+ for(let i = 0; i < 5000; i++) {
190
+ chain.push(N(`n${i}`, 40, 20));
191
+ if(i > 0) {
192
+ links.push({source: `n${i - 1}`, target: `n${i}`});
193
+ }
194
+ }
195
+ let pos = layout_compact(chain, links, {});
196
+ expect(pos.size).toBe(5000);
197
+ expect(Number.isFinite(pos.get("n4999").x)).toBe(true);
198
+ });
199
+ });
200
+
100
201
  describe("the tidy tree, read right", () => {
101
202
  test("depths are columns; a column is as wide as its widest card", () => {
102
203
  let pos = layout_tree(nodes, edges, {nodesep: 10, ranksep: 100, direction: "LR"});