@yuneta/gobj-ui 7.23.146 → 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
@@ -497,8 +497,10 @@ cloned toolbar cannot drift apart again. The vocabulary is the treedb graph's
497
497
  sprite and so cannot share the code, only the decisions): zoom in / zoom out, a
498
498
  live **zoom readout**, `yi-fit` for fit — the same corner brackets the sprite
499
499
  draws for `g6-icon-fit` — and the WRITTEN `1:1` for actual size, which is
500
- written in every editor that offers it and never drawn. Fold is the chevron
501
- pair, from the lazy tree viewer's own toolbar. The readout hangs off G6's
500
+ written in every editor that offers it and never drawn. Fold is the
501
+ double-chevron pair (`yi-angles-down` / `yi-angles-up`, `7.23.147`), the same
502
+ as the lazy tree viewer's toolbar: not the single chevron of a node's handle,
503
+ which opens ONE node. The readout hangs off G6's
502
504
  `aftertransform`, the one hook that also covers the wheel.
503
505
 
504
506
  Both graph toolbars are arranged the same way (`7.23.7`): the **global**
@@ -1385,8 +1387,8 @@ G6 5.1 registers these layouts, and this is how each meets that shape:
1385
1387
  | `circular`, `concentric`, `grid`, `mds`, `random` | rings, rings by degree, a grid, projection, noise | none knows a parent from a child |
1386
1388
  | `combo-combined`, `fishbone` | combos / cause-effect | not this data |
1387
1389
 
1388
- So two of our own, in `treedb_layout.js` (pure, tested), registered as G6
1389
- 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`:
1390
1392
 
1391
1393
  - **`treedb-tree`** — the classic tidy tree, **top to bottom**. A row per
1392
1394
  depth as tall as its tallest card, a node centred over the block of its
@@ -1400,6 +1402,18 @@ layouts by two thin adapters in `c_g6_nodes_tree.js`:
1400
1402
  keeps the other reading for a host that wants it). The outline that shipped
1401
1403
  beside it (`treedb-outline`, one row per node) was removed in `7.23.75`: a
1402
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.
1403
1417
  - **`radial`** (`treedb-radial`) — the root in the middle, a ring per depth,
1404
1418
  every subtree an angular **sector** proportional to its leaves, and the
1405
1419
  radius of each ring the largest of three: one `ranksep` out from the ring
@@ -1411,7 +1425,7 @@ layouts by two thin adapters in `c_g6_nodes_tree.js`:
1411
1425
  construction, across the ring or along the radius; several roots share the
1412
1426
  circle around an empty centre.
1413
1427
 
1414
- Both take the **same spanning tree**, chosen deterministically: roots are the
1428
+ All three take the **same spanning tree**, chosen deterministically: roots are the
1415
1429
  nodes with no incoming edge, in node order; a node belongs to the **first
1416
1430
  parent that reaches it** in a breadth-first walk (the place, not the
1417
1431
  controller, because the place's column came first) and its other links are
@@ -1664,7 +1678,10 @@ forwards them as `EV_SET_HIDDEN_TOPICS`, `EV_SET_MAIN_TOPIC`,
1664
1678
  `main topic`, `highlight topic`, `loose records`, `hidden topics`; `legend`
1665
1679
  is no longer used.
1666
1680
 
1667
- **The layout select has a stepper beside it** (`7.23.146`): ▲ and ▼ step to
1681
+ **The layout select has a stepper beside it** (`7.23.146`): ↑ and ↓
1682
+ (`yi-arrow-up` / `yi-arrow-down` since `7.23.147`, a shafted arrow steps a
1683
+ sequence; the fold stepper's level buttons are tree boxes, `yi-square-minus`
1684
+ / `yi-square-plus`) step to
1668
1685
  the layout before or after the current one, in the order the engine offers
1669
1686
  them (`layout_names`), without opening the list. Layouts are picked by trying
1670
1687
  them one after another, and a list closes on every pick. The stepper reads the
@@ -11305,13 +11305,13 @@ function yui_graph_camera_items(gobj, graph, wide) {
11305
11305
  ];
11306
11306
  }
11307
11307
  /************************************************************
11308
- * The fold pair: expand all, collapse all. Chevrons, the
11309
- * open one rotated down — the same pair the lazy tree
11310
- * viewer's toolbar uses, and the same direction the
11311
- * per-node handles speak.
11308
+ * The fold pair: expand all, collapse all. Double chevrons,
11309
+ * down and up -- the same pair the lazy tree viewer's toolbar
11310
+ * uses. Not the single chevron of a node's handle: that opens
11311
+ * ONE node, these act on all of them.
11312
11312
  ************************************************************/
11313
11313
  function yui_graph_fold_items(gobj, wide) {
11314
- return [camera_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand all", wide, " transform: rotate(90deg);"), camera_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all", wide)];
11314
+ return [camera_button(gobj, "yi-angles-down", "EV_EXPAND_ALL", "expand all", wide), camera_button(gobj, "yi-angles-up", "EV_COLLAPSE_ALL", "collapse all", wide)];
11315
11315
  }
11316
11316
  /************************************************************
11317
11317
  * The ANCHOR toggle: pick one element and every camera move
@@ -13851,7 +13851,7 @@ function make_toolbar$4(gobj) {
13851
13851
  [yui_toolbar_icon("yi-magnifying-glass")]
13852
13852
  ]]
13853
13853
  ]);
13854
- left_items.push(icon_button$1(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"), icon_button$1(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"));
13854
+ left_items.push(icon_button$1(gobj, "yi-angles-down", "EV_EXPAND_ALL", "expand loaded"), icon_button$1(gobj, "yi-angles-up", "EV_COLLAPSE_ALL", "collapse all"));
13855
13855
  attach_clear($search_control, $search_input);
13856
13856
  left_items.push($search_control);
13857
13857
  let right_items = [view_mode_switch$1(gobj), icon_button$1(gobj, "yi-copy", "EV_COPY_ALL", "copy json")];
@@ -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");
@@ -64418,7 +64629,7 @@ function fold_level_items(gobj, wide) {
64418
64629
  style: "margin:0; flex:0 0 auto; flex-wrap:nowrap; align-items:stretch;"
64419
64630
  },
64420
64631
  [
64421
- step_button("GRAPH_FOLD_COLLAPSE", "yi-chevron-right", "collapse one level", "EV_COLLAPSE_LEVEL"),
64632
+ step_button("GRAPH_FOLD_COLLAPSE", "yi-square-minus", "collapse one level", "EV_COLLAPSE_LEVEL"),
64422
64633
  [
64423
64634
  "div",
64424
64635
  {
@@ -64459,7 +64670,7 @@ function fold_level_items(gobj, wide) {
64459
64670
  ]
64460
64671
  ]
64461
64672
  ],
64462
- step_button("GRAPH_FOLD_EXPAND", "yi-chevron-down", "expand one level", "EV_EXPAND_LEVEL")
64673
+ step_button("GRAPH_FOLD_EXPAND", "yi-square-plus", "expand one level", "EV_EXPAND_LEVEL")
64463
64674
  ]
64464
64675
  ]];
64465
64676
  }
@@ -64562,7 +64773,7 @@ function make_toolbar(gobj) {
64562
64773
  class: "GRAPH_LAYOUT_STEPPER buttons has-addons",
64563
64774
  style: "margin:0 0 0 .25rem; flex:0 0 auto; flex-wrap:nowrap; align-items:stretch;"
64564
64775
  },
64565
- [layout_step_button(gobj, "GRAPH_LAYOUT_PREV", "yi-chevron-up", "previous layout", -1), layout_step_button(gobj, "GRAPH_LAYOUT_NEXT", "yi-chevron-down", "next layout", 1)]
64776
+ [layout_step_button(gobj, "GRAPH_LAYOUT_PREV", "yi-arrow-up", "previous layout", -1), layout_step_button(gobj, "GRAPH_LAYOUT_NEXT", "yi-arrow-down", "next layout", 1)]
64566
64777
  ],
64567
64778
  [
64568
64779
  "span",
@@ -69869,7 +70080,7 @@ function version_banner(gobj, topic) {
69869
70080
  [[
69870
70081
  "span",
69871
70082
  { class: "icon" },
69872
- [["i", { class: "yi-chevron-up" }]]
70083
+ [["i", { class: "yi-circle-up" }]]
69873
70084
  ], [
69874
70085
  "span",
69875
70086
  { i18n: "raise" },
@@ -11300,13 +11300,13 @@ function yui_graph_camera_items(gobj, graph, wide) {
11300
11300
  ];
11301
11301
  }
11302
11302
  /************************************************************
11303
- * The fold pair: expand all, collapse all. Chevrons, the
11304
- * open one rotated down — the same pair the lazy tree
11305
- * viewer's toolbar uses, and the same direction the
11306
- * per-node handles speak.
11303
+ * The fold pair: expand all, collapse all. Double chevrons,
11304
+ * down and up -- the same pair the lazy tree viewer's toolbar
11305
+ * uses. Not the single chevron of a node's handle: that opens
11306
+ * ONE node, these act on all of them.
11307
11307
  ************************************************************/
11308
11308
  function yui_graph_fold_items(gobj, wide) {
11309
- return [camera_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand all", wide, " transform: rotate(90deg);"), camera_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all", wide)];
11309
+ return [camera_button(gobj, "yi-angles-down", "EV_EXPAND_ALL", "expand all", wide), camera_button(gobj, "yi-angles-up", "EV_COLLAPSE_ALL", "collapse all", wide)];
11310
11310
  }
11311
11311
  /************************************************************
11312
11312
  * The ANCHOR toggle: pick one element and every camera move
@@ -13846,7 +13846,7 @@ function make_toolbar$4(gobj) {
13846
13846
  [yui_toolbar_icon("yi-magnifying-glass")]
13847
13847
  ]]
13848
13848
  ]);
13849
- left_items.push(icon_button$1(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"), icon_button$1(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all"));
13849
+ left_items.push(icon_button$1(gobj, "yi-angles-down", "EV_EXPAND_ALL", "expand loaded"), icon_button$1(gobj, "yi-angles-up", "EV_COLLAPSE_ALL", "collapse all"));
13850
13850
  attach_clear($search_control, $search_input);
13851
13851
  left_items.push($search_control);
13852
13852
  let right_items = [view_mode_switch$1(gobj), icon_button$1(gobj, "yi-copy", "EV_COPY_ALL", "copy json")];
@@ -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");
@@ -64413,7 +64624,7 @@ function fold_level_items(gobj, wide) {
64413
64624
  style: "margin:0; flex:0 0 auto; flex-wrap:nowrap; align-items:stretch;"
64414
64625
  },
64415
64626
  [
64416
- step_button("GRAPH_FOLD_COLLAPSE", "yi-chevron-right", "collapse one level", "EV_COLLAPSE_LEVEL"),
64627
+ step_button("GRAPH_FOLD_COLLAPSE", "yi-square-minus", "collapse one level", "EV_COLLAPSE_LEVEL"),
64417
64628
  [
64418
64629
  "div",
64419
64630
  {
@@ -64454,7 +64665,7 @@ function fold_level_items(gobj, wide) {
64454
64665
  ]
64455
64666
  ]
64456
64667
  ],
64457
- step_button("GRAPH_FOLD_EXPAND", "yi-chevron-down", "expand one level", "EV_EXPAND_LEVEL")
64668
+ step_button("GRAPH_FOLD_EXPAND", "yi-square-plus", "expand one level", "EV_EXPAND_LEVEL")
64458
64669
  ]
64459
64670
  ]];
64460
64671
  }
@@ -64557,7 +64768,7 @@ function make_toolbar(gobj) {
64557
64768
  class: "GRAPH_LAYOUT_STEPPER buttons has-addons",
64558
64769
  style: "margin:0 0 0 .25rem; flex:0 0 auto; flex-wrap:nowrap; align-items:stretch;"
64559
64770
  },
64560
- [layout_step_button(gobj, "GRAPH_LAYOUT_PREV", "yi-chevron-up", "previous layout", -1), layout_step_button(gobj, "GRAPH_LAYOUT_NEXT", "yi-chevron-down", "next layout", 1)]
64771
+ [layout_step_button(gobj, "GRAPH_LAYOUT_PREV", "yi-arrow-up", "previous layout", -1), layout_step_button(gobj, "GRAPH_LAYOUT_NEXT", "yi-arrow-down", "next layout", 1)]
64561
64772
  ],
64562
64773
  [
64563
64774
  "span",
@@ -69864,7 +70075,7 @@ function version_banner(gobj, topic) {
69864
70075
  [[
69865
70076
  "span",
69866
70077
  { class: "icon" },
69867
- [["i", { class: "yi-chevron-up" }]]
70078
+ [["i", { class: "yi-circle-up" }]]
69868
70079
  ], [
69869
70080
  "span",
69870
70081
  { i18n: "raise" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.146",
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) {
@@ -155,13 +155,6 @@
155
155
  transform: rotate(90deg);
156
156
  }
157
157
 
158
- /* "Expand all" reuses the chevron (the only one available) rotated to point
159
- * DOWN — the open state — while "collapse all" keeps it pointing right (the
160
- * closed state), mirroring the per-node toggle. */
161
- .C_YUI_JSON .EV_EXPAND_ALL i {
162
- transform: rotate(90deg);
163
- }
164
-
165
158
  .C_YUI_JSON .JSON_TOGGLE_REMOTE i {
166
159
  font-size: 0.8em;
167
160
  color: var(--bulma-link-on-scheme, var(--bulma-link, #3273dc));
package/src/c_yui_json.js CHANGED
@@ -399,8 +399,8 @@ function make_toolbar(gobj)
399
399
  * place both graphs put theirs. It opens the whole document; the
400
400
  * per-row toggles live in the rows. */
401
401
  left_items.push(
402
- icon_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand loaded"),
403
- icon_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all")
402
+ icon_button(gobj, "yi-angles-down", "EV_EXPAND_ALL", "expand loaded"),
403
+ icon_button(gobj, "yi-angles-up", "EV_COLLAPSE_ALL", "collapse all")
404
404
  );
405
405
 
406
406
  attach_clear($search_control, $search_input);
@@ -1264,7 +1264,7 @@ function version_banner(gobj, topic)
1264
1264
  title: t("raise the version"), "aria-label": t("raise the version"),
1265
1265
  "data-i18n-title": "raise the version",
1266
1266
  "data-i18n-aria-label": "raise the version"}, [
1267
- ["span", {class: "icon"}, [["i", {class: "yi-chevron-up"}]]],
1267
+ ["span", {class: "icon"}, [["i", {class: "yi-circle-up"}]]],
1268
1268
  ["span", {i18n: "raise"}, t("raise")]
1269
1269
  ]]
1270
1270
  ]]
@@ -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":
@@ -716,8 +718,9 @@ function fold_level_items(gobj, wide)
716
718
  return [
717
719
  ['div', {class: 'GRAPH_FOLD_STEPPER buttons has-addons',
718
720
  style: 'margin:0; flex:0 0 auto; flex-wrap:nowrap; align-items:stretch;'}, [
719
- /* The same two chevrons the pills speak: `▸` folded, `▾` open. */
720
- step_button('GRAPH_FOLD_COLLAPSE', 'yi-chevron-right',
721
+ /* Tree boxes, not the pills' chevrons: a pill opens ONE hook,
722
+ * this steps a LEVEL of the whole tree. */
723
+ step_button('GRAPH_FOLD_COLLAPSE', 'yi-square-minus',
721
724
  'collapse one level', 'EV_COLLAPSE_LEVEL'),
722
725
  /* A `div` with the button skin, like the legend's main
723
726
  * chip: a readout between two buttons of one group, not
@@ -740,7 +743,7 @@ function fold_level_items(gobj, wide)
740
743
  ['span', {class: 'GRAPH_FOLD_COUNT',
741
744
  style: 'font-variant-numeric:tabular-nums;'}, '–'],
742
745
  ]],
743
- step_button('GRAPH_FOLD_EXPAND', 'yi-chevron-down',
746
+ step_button('GRAPH_FOLD_EXPAND', 'yi-square-plus',
744
747
  'expand one level', 'EV_EXPAND_LEVEL'),
745
748
  ]],
746
749
  ];
@@ -881,8 +884,8 @@ function make_toolbar(gobj)
881
884
  * arrive with the child (populate_nodes_tree_options). */
882
885
  ['div', {class: 'GRAPH_LAYOUT_STEPPER buttons has-addons',
883
886
  style: 'margin:0 0 0 .25rem; flex:0 0 auto; flex-wrap:nowrap; align-items:stretch;'}, [
884
- layout_step_button(gobj, 'GRAPH_LAYOUT_PREV', 'yi-chevron-up', 'previous layout', -1),
885
- layout_step_button(gobj, 'GRAPH_LAYOUT_NEXT', 'yi-chevron-down', 'next layout', 1),
887
+ layout_step_button(gobj, 'GRAPH_LAYOUT_PREV', 'yi-arrow-up', 'previous layout', -1),
888
+ layout_step_button(gobj, 'GRAPH_LAYOUT_NEXT', 'yi-arrow-down', 'next layout', 1),
886
889
  ]],
887
890
 
888
891
  ['span', {class: 'GRAPH_MODE_LABEL is-hidden-mobile', style: 'padding-left:10px; padding-right:5px;', i18n: 'operation mode'}, 'operation mode'],
@@ -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"});
@@ -181,17 +181,16 @@ export function yui_graph_camera_items(gobj, graph, wide)
181
181
  }
182
182
 
183
183
  /************************************************************
184
- * The fold pair: expand all, collapse all. Chevrons, the
185
- * open one rotated down — the same pair the lazy tree
186
- * viewer's toolbar uses, and the same direction the
187
- * per-node handles speak.
184
+ * The fold pair: expand all, collapse all. Double chevrons,
185
+ * down and up -- the same pair the lazy tree viewer's toolbar
186
+ * uses. Not the single chevron of a node's handle: that opens
187
+ * ONE node, these act on all of them.
188
188
  ************************************************************/
189
189
  export function yui_graph_fold_items(gobj, wide)
190
190
  {
191
191
  return [
192
- camera_button(gobj, "yi-chevron-right", "EV_EXPAND_ALL", "expand all", wide,
193
- ' transform: rotate(90deg);'),
194
- camera_button(gobj, "yi-chevron-right", "EV_COLLAPSE_ALL", "collapse all", wide),
192
+ camera_button(gobj, "yi-angles-down", "EV_EXPAND_ALL", "expand all", wide),
193
+ camera_button(gobj, "yi-angles-up", "EV_COLLAPSE_ALL", "collapse all", wide),
195
194
  ];
196
195
  }
197
196
 
package/src/yui_icons.css CHANGED
@@ -404,3 +404,43 @@
404
404
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M352 64L480 160 352 256V192H64V128H352ZM160 256L32 352 160 448V384H448V320H160Z'/%3E%3C/svg%3E");
405
405
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M352 64L480 160 352 256V192H64V128H352ZM160 256L32 352 160 448V384H448V320H160Z'/%3E%3C/svg%3E");
406
406
  }
407
+
408
+ /* --- One shape per meaning (FontAwesome Free 7.3.1, solid) ---
409
+ * A chevron opens or closes ONE thing, or scrolls; a shafted arrow
410
+ * steps through a sequence; a tree box steps one LEVEL of a tree;
411
+ * a double chevron acts on ALL of it. */
412
+
413
+ .yi-square-plus::before {
414
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zM200 344l0-64-64 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l64 0 0-64c0-13.3 10.7-24 24-24s24 10.7 24 24l0 64 64 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-64 0 0 64c0 13.3-10.7 24-24 24s-24-10.7-24-24z'/%3E%3C/svg%3E");
415
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zM200 344l0-64-64 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l64 0 0-64c0-13.3 10.7-24 24-24s24 10.7 24 24l0 64 64 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-64 0 0 64c0 13.3-10.7 24-24 24s-24-10.7-24-24z'/%3E%3C/svg%3E");
416
+ }
417
+
418
+ .yi-square-minus::before {
419
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm72 200l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z'/%3E%3C/svg%3E");
420
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'%3E%3Cpath d='M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm72 200l176 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-176 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z'/%3E%3C/svg%3E");
421
+ }
422
+
423
+ .yi-angles-down::before {
424
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M214.6 470.6c-12.5 12.5-32.8 12.5-45.3 0l-160-160c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 402.7 329.4 265.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-160 160zm160-352l-160 160c-12.5 12.5-32.8 12.5-45.3 0l-160-160c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 210.7 329.4 73.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3z'/%3E%3C/svg%3E");
425
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M214.6 470.6c-12.5 12.5-32.8 12.5-45.3 0l-160-160c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 402.7 329.4 265.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-160 160zm160-352l-160 160c-12.5 12.5-32.8 12.5-45.3 0l-160-160c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 210.7 329.4 73.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3z'/%3E%3C/svg%3E");
426
+ }
427
+
428
+ .yi-angles-up::before {
429
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3 329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160zm160 352l-160-160c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 329.4 438.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3z'/%3E%3C/svg%3E");
430
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 109.3 329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160zm160 352l-160-160c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 329.4 438.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3z'/%3E%3C/svg%3E");
431
+ }
432
+
433
+ .yi-arrow-up::before {
434
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M214.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 109.3 160 480c0 17.7 14.3 32 32 32s32-14.3 32-32l0-370.7 105.4 105.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z'/%3E%3C/svg%3E");
435
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M214.6 9.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 109.3 160 480c0 17.7 14.3 32 32 32s32-14.3 32-32l0-370.7 105.4 105.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z'/%3E%3C/svg%3E");
436
+ }
437
+
438
+ .yi-arrow-down::before {
439
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M169.4 502.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 402.7 224 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 370.7-105.4-105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z'/%3E%3C/svg%3E");
440
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'%3E%3Cpath d='M169.4 502.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 402.7 224 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 370.7-105.4-105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z'/%3E%3C/svg%3E");
441
+ }
442
+
443
+ .yi-circle-up::before {
444
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zm11.3-387.3l104 104c4.6 4.6 5.9 11.5 3.5 17.4S366.5 256 360 256l-56 0 0 96c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-96-56 0c-6.5 0-12.3-3.9-14.8-9.9s-1.1-12.9 3.5-17.4l104-104c6.2-6.2 16.4-6.2 22.6 0z'/%3E%3C/svg%3E");
445
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zm11.3-387.3l104 104c4.6 4.6 5.9 11.5 3.5 17.4S366.5 256 360 256l-56 0 0 96c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32l0-96-56 0c-6.5 0-12.3-3.9-14.8-9.9s-1.1-12.9 3.5-17.4l104-104c6.2-6.2 16.4-6.2 22.6 0z'/%3E%3C/svg%3E");
446
+ }