@yuneta/gobj-ui 7.23.69 → 7.23.70

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.
@@ -27309,6 +27309,329 @@ function fold_reveal(model, state, node_key) {
27309
27309
  return true;
27310
27310
  }
27311
27311
  //#endregion
27312
+ //#region src/treedb_layout.js
27313
+ /***********************************************************************
27314
+ * treedb_layout.js
27315
+ *
27316
+ * Two layouts made for a treedb, and nothing else.
27317
+ *
27318
+ * What a treedb graph IS, once it is folded (treedb_fold_model.js):
27319
+ * a forest read left to right. A main topic is a tree (places:
27320
+ * country, region, site, hall), the other topics hang from its
27321
+ * nodes through hooks (devices, users, controllers), a record may
27322
+ * hang from two parents (a device from its place AND its
27323
+ * controller), and every visible node was reached from a root.
27324
+ * Tens to a few hundred cards on screen, never the whole store.
27325
+ *
27326
+ * G6's own layouts either do not know that or pay for not knowing:
27327
+ *
27328
+ * - `dagre` gets the reading direction right and then ranks by
27329
+ * longest path and minimises crossings with a heuristic that
27330
+ * REORDERS the siblings on every run -- open one hook and the
27331
+ * column next to it shuffles. It is O(n·m) per sweep and it is
27332
+ * the one thing that takes time on a big expansion.
27333
+ * - the tree layouts (`compact-box`, `indented`, `dendrogram`,
27334
+ * `mindmap`) draw exactly the shape wanted, but G6 runs them on
27335
+ * a TREE it builds from the edges: a node with two parents is
27336
+ * placed by whichever parent the builder met first, unstably.
27337
+ * - the force family scatters a tree, which is the pile with
27338
+ * physics.
27339
+ *
27340
+ * So: a tidy tree (`tree`) and an outline (`outline`), both fed by
27341
+ * the SAME spanning tree, chosen deterministically:
27342
+ *
27343
+ * - roots are the nodes with no incoming edge, in node order;
27344
+ * - a node belongs to the FIRST parent that reaches it in a
27345
+ * breadth-first walk from the roots -- the place, not the
27346
+ * controller, because the place's column came first -- and the
27347
+ * other links are drawn as edges across the tree;
27348
+ * - the children of a node are ordered by the HOOK they hang from
27349
+ * (the parent's port order, which is the schema's column order)
27350
+ * and then by node order (record order, the pages' order);
27351
+ * - a node nothing reaches (a cycle) is a root too.
27352
+ *
27353
+ * `tree`: the classic tidy tree, left to right. Each depth is a
27354
+ * column as wide as its widest card; a node is centred on the
27355
+ * block of its children; siblings stack with `nodesep` between
27356
+ * them. O(n), and opening a hook moves NOTHING that is not below
27357
+ * or beside it -- the cards keep their order, the eye keeps its
27358
+ * place.
27359
+ *
27360
+ * `outline`: one node per row, indented by depth -- what a JSON
27361
+ * viewer draws, and what a treedb is when read as one. Tall, but
27362
+ * nothing is ever beside anything: the reader scrolls a list.
27363
+ *
27364
+ * Pure: takes plain nodes and edges, returns positions. The G6
27365
+ * classes in c_g6_nodes_tree.js are two thin adapters over it.
27366
+ *
27367
+ * Copyright (c) 2026, ArtGins.
27368
+ * All Rights Reserved.
27369
+ ***********************************************************************/
27370
+ var DEFAULTS = {
27371
+ nodesep: 18,
27372
+ ranksep: 110,
27373
+ indent: 48
27374
+ };
27375
+ /************************************************************
27376
+ * The spanning tree of a graph, deterministic.
27377
+ *
27378
+ * nodes [{id, w, h, order}] order: creation index
27379
+ * edges [{source, target, rank}] rank: the hook's position in
27380
+ * the parent's ports (0 = first)
27381
+ *
27382
+ * Returns {roots: [id], children: Map<id, [id]>, depth: Map<id, n>,
27383
+ * by_id: Map<id, node>}
27384
+ ************************************************************/
27385
+ function spanning_tree(nodes, edges) {
27386
+ let by_id = /* @__PURE__ */ new Map();
27387
+ for (let i = 0; i < nodes.length; i++) {
27388
+ let n = nodes[i];
27389
+ by_id.set(n.id, Object.assign({ order: i }, n));
27390
+ }
27391
+ let out = /* @__PURE__ */ new Map();
27392
+ let has_parent = /* @__PURE__ */ new Set();
27393
+ for (let e of edges) {
27394
+ if (!by_id.has(e.source) || !by_id.has(e.target) || e.source === e.target) continue;
27395
+ if (!out.has(e.source)) out.set(e.source, []);
27396
+ out.get(e.source).push({
27397
+ id: e.target,
27398
+ rank: e.rank > 0 ? e.rank : 0
27399
+ });
27400
+ has_parent.add(e.target);
27401
+ }
27402
+ for (let list of out.values()) list.sort((a, b) => {
27403
+ if (a.rank !== b.rank) return a.rank - b.rank;
27404
+ return by_id.get(a.id).order - by_id.get(b.id).order;
27405
+ });
27406
+ let roots = [];
27407
+ for (let n of by_id.values()) if (!has_parent.has(n.id)) roots.push(n.id);
27408
+ let children = /* @__PURE__ */ new Map();
27409
+ let depth = /* @__PURE__ */ new Map();
27410
+ let assign = (start) => {
27411
+ let queue = [start];
27412
+ depth.set(start, 0);
27413
+ while (queue.length) {
27414
+ let id = queue.shift();
27415
+ let kids = [];
27416
+ for (let link of out.get(id) || []) {
27417
+ if (depth.has(link.id)) continue;
27418
+ depth.set(link.id, depth.get(id) + 1);
27419
+ kids.push(link.id);
27420
+ queue.push(link.id);
27421
+ }
27422
+ children.set(id, kids);
27423
+ }
27424
+ };
27425
+ for (let root of roots) assign(root);
27426
+ for (let n of by_id.values()) if (!depth.has(n.id)) {
27427
+ roots.push(n.id);
27428
+ assign(n.id);
27429
+ }
27430
+ return {
27431
+ roots,
27432
+ children,
27433
+ depth,
27434
+ by_id
27435
+ };
27436
+ }
27437
+ /************************************************************
27438
+ * The tidy tree, left to right.
27439
+ *
27440
+ * Returns Map<id, {x, y}> with the CENTRE of each node, the way G6
27441
+ * positions a node.
27442
+ ************************************************************/
27443
+ function layout_tree(nodes, edges, opts) {
27444
+ let o = Object.assign({}, DEFAULTS, opts || {});
27445
+ let t = spanning_tree(nodes, edges);
27446
+ let pos = /* @__PURE__ */ new Map();
27447
+ let col_w = [];
27448
+ for (let [id, d] of t.depth) {
27449
+ let w = t.by_id.get(id).w;
27450
+ col_w[d] = Math.max(col_w[d] || 0, w);
27451
+ }
27452
+ let col_x = [];
27453
+ let x = 0;
27454
+ for (let d = 0; d < col_w.length; d++) {
27455
+ col_x[d] = x;
27456
+ x += (col_w[d] || 0) + o.ranksep;
27457
+ }
27458
+ let height = /* @__PURE__ */ new Map();
27459
+ let measure = (id) => {
27460
+ let kids = t.children.get(id) || [];
27461
+ let own = t.by_id.get(id).h;
27462
+ if (!kids.length) {
27463
+ height.set(id, own);
27464
+ return own;
27465
+ }
27466
+ let sum = 0;
27467
+ for (let i = 0; i < kids.length; i++) {
27468
+ sum += measure(kids[i]);
27469
+ if (i > 0) sum += o.nodesep;
27470
+ }
27471
+ let h = Math.max(own, sum);
27472
+ height.set(id, h);
27473
+ return h;
27474
+ };
27475
+ for (let root of t.roots) measure(root);
27476
+ let place = (id, top) => {
27477
+ let n = t.by_id.get(id);
27478
+ let d = t.depth.get(id);
27479
+ let kids = t.children.get(id) || [];
27480
+ let block = height.get(id);
27481
+ let cx = col_x[d] + (col_w[d] - n.w) / 2 + n.w / 2;
27482
+ if (!kids.length) {
27483
+ pos.set(id, {
27484
+ x: cx,
27485
+ y: top + block / 2
27486
+ });
27487
+ return;
27488
+ }
27489
+ let kids_h = 0;
27490
+ for (let i = 0; i < kids.length; i++) {
27491
+ kids_h += height.get(kids[i]);
27492
+ if (i > 0) kids_h += o.nodesep;
27493
+ }
27494
+ let cursor = top + (block - kids_h) / 2;
27495
+ for (let kid of kids) {
27496
+ place(kid, cursor);
27497
+ cursor += height.get(kid) + o.nodesep;
27498
+ }
27499
+ pos.set(id, {
27500
+ x: cx,
27501
+ y: top + block / 2
27502
+ });
27503
+ };
27504
+ let top = 0;
27505
+ for (let root of t.roots) {
27506
+ place(root, top);
27507
+ top += height.get(root) + o.nodesep * 2;
27508
+ }
27509
+ return pos;
27510
+ }
27511
+ /************************************************************
27512
+ * The radial tree: the root in the middle, a ring per depth.
27513
+ *
27514
+ * G6's `radial` places by MDS on the graph distances and pushes
27515
+ * overlaps apart afterwards, with a ring radius it is TOLD; twenty
27516
+ * cards of 172px on a ring of radius 200 have 52px each and pile
27517
+ * up. This one is the radial form of the tidy tree above: every
27518
+ * subtree gets an angular SECTOR proportional to its leaves, a node
27519
+ * sits in the middle of its sector, and the radius of each ring is
27520
+ * the larger of "one step further out than the ring before" and
27521
+ * "long enough for its nodes to sit side by side" -- so a hall
27522
+ * with a hundred devices is a wide, sparse fan, and nothing
27523
+ * overlaps by construction. Several roots share the circle around
27524
+ * a virtual centre.
27525
+ *
27526
+ * ranksep the step between rings
27527
+ * nodesep the gap between neighbours on a ring
27528
+ ************************************************************/
27529
+ function layout_radial(nodes, edges, opts) {
27530
+ let o = Object.assign({}, DEFAULTS, opts || {});
27531
+ let t = spanning_tree(nodes, edges);
27532
+ let pos = /* @__PURE__ */ new Map();
27533
+ if (!t.roots.length) return pos;
27534
+ let leaves = /* @__PURE__ */ new Map();
27535
+ let count = (id) => {
27536
+ let kids = t.children.get(id) || [];
27537
+ if (!kids.length) {
27538
+ leaves.set(id, 1);
27539
+ return 1;
27540
+ }
27541
+ let n = 0;
27542
+ for (let kid of kids) n += count(kid);
27543
+ leaves.set(id, n);
27544
+ return n;
27545
+ };
27546
+ let total = 0;
27547
+ for (let root of t.roots) total += count(root);
27548
+ let single = t.roots.length === 1;
27549
+ let ring_of = (id) => t.depth.get(id) + (single ? 0 : 1);
27550
+ let angle = /* @__PURE__ */ new Map();
27551
+ let rings = [];
27552
+ let sector = (id, from, span) => {
27553
+ let a = from + span / 2;
27554
+ angle.set(id, a);
27555
+ let ring = ring_of(id);
27556
+ if (!rings[ring]) rings[ring] = [];
27557
+ let n = t.by_id.get(id);
27558
+ rings[ring].push({
27559
+ id,
27560
+ angle: a,
27561
+ diag: Math.hypot(n.w, n.h)
27562
+ });
27563
+ let cursor = from;
27564
+ for (let kid of t.children.get(id) || []) {
27565
+ let part = span * leaves.get(kid) / leaves.get(id);
27566
+ sector(kid, cursor, part);
27567
+ cursor += part;
27568
+ }
27569
+ };
27570
+ if (single) {
27571
+ let root = t.roots[0];
27572
+ angle.set(root, 0);
27573
+ let cursor = -Math.PI / 2;
27574
+ for (let kid of t.children.get(root) || []) {
27575
+ let part = 2 * Math.PI * leaves.get(kid) / leaves.get(root);
27576
+ sector(kid, cursor, part);
27577
+ cursor += part;
27578
+ }
27579
+ } else {
27580
+ let cursor = -Math.PI / 2;
27581
+ for (let root of t.roots) {
27582
+ let part = 2 * Math.PI * leaves.get(root) / total;
27583
+ sector(root, cursor, part);
27584
+ cursor += part;
27585
+ }
27586
+ }
27587
+ let radius = [0];
27588
+ for (let ring = 1; ring < rings.length; ring++) {
27589
+ let r = radius[ring - 1] + o.ranksep;
27590
+ let list = (rings[ring] || []).slice().sort((a, b) => a.angle - b.angle);
27591
+ for (let i = 0; i < list.length && list.length > 1; i++) {
27592
+ let a = list[i];
27593
+ let b = list[(i + 1) % list.length];
27594
+ let gap = i + 1 < list.length ? b.angle - a.angle : b.angle + 2 * Math.PI - a.angle;
27595
+ if (gap > 1e-9) r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
27596
+ }
27597
+ radius[ring] = r;
27598
+ }
27599
+ for (let [id, a] of angle) {
27600
+ let r = radius[ring_of(id)] || 0;
27601
+ pos.set(id, {
27602
+ x: r * Math.cos(a),
27603
+ y: r * Math.sin(a)
27604
+ });
27605
+ }
27606
+ return pos;
27607
+ }
27608
+ /************************************************************
27609
+ * The outline: one node per row, indented by depth, in the
27610
+ * pre-order of the spanning tree. Rows are left-aligned on
27611
+ * their indent, so a column of chips reads as a list.
27612
+ ************************************************************/
27613
+ function layout_outline(nodes, edges, opts) {
27614
+ let o = Object.assign({}, DEFAULTS, opts || {});
27615
+ let t = spanning_tree(nodes, edges);
27616
+ let pos = /* @__PURE__ */ new Map();
27617
+ let y = 0;
27618
+ let visit = (id) => {
27619
+ let n = t.by_id.get(id);
27620
+ let d = t.depth.get(id);
27621
+ pos.set(id, {
27622
+ x: d * o.indent + n.w / 2,
27623
+ y: y + n.h / 2
27624
+ });
27625
+ y += n.h + o.nodesep;
27626
+ for (let kid of t.children.get(id) || []) visit(kid);
27627
+ };
27628
+ for (let root of t.roots) {
27629
+ visit(root);
27630
+ y += o.nodesep;
27631
+ }
27632
+ return pos;
27633
+ }
27634
+ //#endregion
27312
27635
  //#region node_modules/@babel/runtime/helpers/esm/typeof.js
27313
27636
  function _typeof(o) {
27314
27637
  "@babel/helpers - typeof";
@@ -54129,6 +54452,16 @@ var SELECT_RING = "rgba(59,130,246,0.95)";
54129
54452
  ***************************************************************/
54130
54453
  var _layouts = {
54131
54454
  "manual": { type: "manual" },
54455
+ "treedb-tree": {
54456
+ type: "treedb-tree",
54457
+ nodesep: 18,
54458
+ ranksep: 110
54459
+ },
54460
+ "treedb-outline": {
54461
+ type: "treedb-outline",
54462
+ nodesep: 12,
54463
+ indent: 48
54464
+ },
54132
54465
  "dagre": {
54133
54466
  type: "dagre",
54134
54467
  rankdir: "LR",
@@ -54141,6 +54474,11 @@ var _layouts = {
54141
54474
  nodesep: 30,
54142
54475
  ranksep: 80
54143
54476
  },
54477
+ "radial": {
54478
+ type: "treedb-radial",
54479
+ nodesep: 24,
54480
+ ranksep: 180
54481
+ },
54144
54482
  "d3-force": {
54145
54483
  type: "d3-force",
54146
54484
  link: {
@@ -54420,6 +54758,9 @@ function register_layouts(gobj) {
54420
54758
  if (!_g6_extensions_registered) {
54421
54759
  _g6_extensions_registered = true;
54422
54760
  register(ExtensionCategory.LAYOUT, "manual", ManualLayout);
54761
+ register(ExtensionCategory.LAYOUT, "treedb-tree", TreedbTreeLayout);
54762
+ register(ExtensionCategory.LAYOUT, "treedb-outline", TreedbOutlineLayout);
54763
+ register(ExtensionCategory.LAYOUT, "treedb-radial", TreedbRadialLayout);
54423
54764
  register(ExtensionCategory.NODE, "light", LightNode);
54424
54765
  }
54425
54766
  }
@@ -55031,8 +55372,13 @@ function build_ports(gobj, desc) {
55031
55372
  * included, because the saved arrangements were made with the
55032
55373
  * ports on the top and bottom edges.
55033
55374
  ************************************************************/
55375
+ var LR_LAYOUTS = [
55376
+ "dagre",
55377
+ "treedb-tree",
55378
+ "treedb-outline"
55379
+ ];
55034
55380
  function layout_direction(gobj) {
55035
- return gobj.priv.layout === "dagre" ? "LR" : "TB";
55381
+ return str_in_list(LR_LAYOUTS, gobj.priv.layout) ? "LR" : "TB";
55036
55382
  }
55037
55383
  function edge_type_for(gobj) {
55038
55384
  return layout_direction(gobj) === "LR" ? "cubic-horizontal" : "cubic";
@@ -56032,6 +56378,73 @@ var ManualLayout = class extends BaseLayout {
56032
56378
  }
56033
56379
  };
56034
56380
  /************************************************************
56381
+ * Custom G6 layouts: TreedbTreeLayout / TreedbOutlineLayout.
56382
+ *
56383
+ * Thin adapters over treedb_layout.js: the nodes with their sizes,
56384
+ * the edges with the RANK of the hook they leave by (the position
56385
+ * of the source port in the parent's ports, which is the schema's
56386
+ * column order), and the positions back. A `+N` chip's edge has a
56387
+ * source port too, so the chip sits at the end of its hook's
56388
+ * children, where the page ends.
56389
+ ************************************************************/
56390
+ function treedb_layout_input(data) {
56391
+ const { nodes = [], edges = [] } = data;
56392
+ let ports_of = /* @__PURE__ */ new Map();
56393
+ return {
56394
+ nodes: nodes.map((node) => {
56395
+ let size = node.style && node.style.size || [172, 96];
56396
+ let w = is_array(size) ? size[0] : size;
56397
+ let h = is_array(size) ? size.length > 1 ? size[1] : size[0] : size;
56398
+ let ports = node.style && is_array(node.style.ports) ? node.style.ports : [];
56399
+ ports_of.set(node.id, ports.map((p) => p.key));
56400
+ return {
56401
+ id: node.id,
56402
+ w,
56403
+ h
56404
+ };
56405
+ }),
56406
+ edges: edges.map((edge) => {
56407
+ let port = edge.style ? edge.style.sourcePort : void 0;
56408
+ let keys = ports_of.get(edge.source) || [];
56409
+ let rank = port ? keys.indexOf(port) : -1;
56410
+ return {
56411
+ source: edge.source,
56412
+ target: edge.target,
56413
+ rank: rank >= 0 ? rank : 0
56414
+ };
56415
+ })
56416
+ };
56417
+ }
56418
+ function treedb_layout_output(pos) {
56419
+ let out = [];
56420
+ for (let [id, p] of pos) out.push({
56421
+ id,
56422
+ style: {
56423
+ x: p.x,
56424
+ y: p.y
56425
+ }
56426
+ });
56427
+ return { nodes: out };
56428
+ }
56429
+ var TreedbTreeLayout = class extends BaseLayout {
56430
+ async execute(data, options) {
56431
+ let input = treedb_layout_input(data);
56432
+ return treedb_layout_output(layout_tree(input.nodes, input.edges, options));
56433
+ }
56434
+ };
56435
+ var TreedbOutlineLayout = class extends BaseLayout {
56436
+ async execute(data, options) {
56437
+ let input = treedb_layout_input(data);
56438
+ return treedb_layout_output(layout_outline(input.nodes, input.edges, options));
56439
+ }
56440
+ };
56441
+ var TreedbRadialLayout = class extends BaseLayout {
56442
+ async execute(data, options) {
56443
+ let input = treedb_layout_input(data);
56444
+ return treedb_layout_output(layout_radial(input.nodes, input.edges, options));
56445
+ }
56446
+ };
56447
+ /************************************************************
56035
56448
  * Node selection and resize handles
56036
56449
  ************************************************************/
56037
56450
  var RESIZE_HANDLE_SIZE = 8;
@@ -57693,7 +58106,7 @@ function auto_layout(gobj) {
57693
58106
  if (priv._layout_asked) return false;
57694
58107
  if (priv._nodes_total > 0 && priv._nodes_placed * 2 > priv._nodes_total) return false;
57695
58108
  if (!priv.graph) return false;
57696
- let name = "dagre";
58109
+ let name = "treedb-tree";
57697
58110
  gobj_write_attr(gobj, "layout", name);
57698
58111
  try {
57699
58112
  priv.graph.setLayout(_layouts[name]);
@@ -60530,6 +60943,9 @@ function option_label(name) {
60530
60943
  case "writing": return t("writing");
60531
60944
  case "edition": return t("edition");
60532
60945
  case "manual": return t("manual");
60946
+ case "treedb-tree": return t("treedb-tree");
60947
+ case "treedb-outline": return t("treedb-outline");
60948
+ case "radial": return t("radial");
60533
60949
  case "dagre": return t("dagre");
60534
60950
  case "antv-dagre": return t("antv-dagre");
60535
60951
  case "d3-force": return t("d3-force");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.69",
3
+ "version": "7.23.70",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -124,6 +124,7 @@ import {
124
124
  fold_node_key,
125
125
  fold_split_group_key,
126
126
  } from "./treedb_fold_model.js";
127
+ import {layout_tree, layout_outline, layout_radial} from "./treedb_layout.js";
127
128
 
128
129
  import {
129
130
  BaseLayout,
@@ -260,6 +261,23 @@ const _layouts = {
260
261
  * cards edge to edge. `antv-dagre` below keeps the top-down reading
261
262
  * for whoever wants it -- the two are the same algorithm read two
262
263
  * ways, so the layout picker is also the direction picker. */
264
+ /* OURS, made for a treedb (treedb_layout.js): a tidy tree read
265
+ * left to right, and an outline of one row per node. Both take
266
+ * the spanning tree of what is on screen -- the roots, the first
267
+ * parent that reaches a node, the hooks in schema order, the
268
+ * records in their order -- so opening a hook moves nothing that
269
+ * is not under or beside it. The default for a treedb nobody has
270
+ * arranged (see auto_layout). */
271
+ "treedb-tree": {
272
+ type: 'treedb-tree',
273
+ nodesep: 18,
274
+ ranksep: 110,
275
+ },
276
+ "treedb-outline": {
277
+ type: 'treedb-outline',
278
+ nodesep: 12,
279
+ indent: 48,
280
+ },
263
281
  "dagre": {
264
282
  type: 'dagre',
265
283
  rankdir: 'LR',
@@ -272,6 +290,14 @@ const _layouts = {
272
290
  nodesep: 30,
273
291
  ranksep: 80,
274
292
  },
293
+ /* Ours too: the root in the middle, a ring per depth, every ring
294
+ * as long as its cards need. G6's `radial` was tried first and
295
+ * piled twenty cards of 172px on a ring of a fixed radius. */
296
+ "radial": {
297
+ type: 'treedb-radial',
298
+ nodesep: 24,
299
+ ranksep: 180,
300
+ },
275
301
  "d3-force": {
276
302
  type: 'd3-force',
277
303
  link: {
@@ -780,6 +806,9 @@ function register_layouts(gobj)
780
806
  if(!_g6_extensions_registered) {
781
807
  _g6_extensions_registered = true;
782
808
  register(ExtensionCategory.LAYOUT, 'manual', ManualLayout);
809
+ register(ExtensionCategory.LAYOUT, 'treedb-tree', TreedbTreeLayout);
810
+ register(ExtensionCategory.LAYOUT, 'treedb-outline', TreedbOutlineLayout);
811
+ register(ExtensionCategory.LAYOUT, 'treedb-radial', TreedbRadialLayout);
783
812
  register(ExtensionCategory.NODE, 'light', LightNode);
784
813
  }
785
814
  }
@@ -1765,9 +1794,11 @@ function build_ports(gobj, desc)
1765
1794
  * included, because the saved arrangements were made with the
1766
1795
  * ports on the top and bottom edges.
1767
1796
  ************************************************************/
1797
+ const LR_LAYOUTS = ["dagre", "treedb-tree", "treedb-outline"];
1798
+
1768
1799
  function layout_direction(gobj)
1769
1800
  {
1770
- return (gobj.priv.layout === "dagre")? "LR" : "TB";
1801
+ return str_in_list(LR_LAYOUTS, gobj.priv.layout)? "LR" : "TB";
1771
1802
  }
1772
1803
 
1773
1804
  function edge_type_for(gobj)
@@ -3304,6 +3335,73 @@ class ManualLayout extends BaseLayout
3304
3335
  }
3305
3336
  }
3306
3337
 
3338
+ /************************************************************
3339
+ * Custom G6 layouts: TreedbTreeLayout / TreedbOutlineLayout.
3340
+ *
3341
+ * Thin adapters over treedb_layout.js: the nodes with their sizes,
3342
+ * the edges with the RANK of the hook they leave by (the position
3343
+ * of the source port in the parent's ports, which is the schema's
3344
+ * column order), and the positions back. A `+N` chip's edge has a
3345
+ * source port too, so the chip sits at the end of its hook's
3346
+ * children, where the page ends.
3347
+ ************************************************************/
3348
+ function treedb_layout_input(data)
3349
+ {
3350
+ const {nodes = [], edges = []} = data;
3351
+
3352
+ let ports_of = new Map();
3353
+ let in_nodes = nodes.map((node) => {
3354
+ let size = (node.style && node.style.size) || [172, 96];
3355
+ let w = is_array(size)? size[0] : size;
3356
+ let h = is_array(size)? (size.length > 1? size[1] : size[0]) : size;
3357
+ let ports = (node.style && is_array(node.style.ports))? node.style.ports : [];
3358
+ ports_of.set(node.id, ports.map((p) => p.key));
3359
+ return {id: node.id, w: w, h: h};
3360
+ });
3361
+
3362
+ let in_edges = edges.map((edge) => {
3363
+ let port = edge.style? edge.style.sourcePort : undefined;
3364
+ let keys = ports_of.get(edge.source) || [];
3365
+ let rank = port? keys.indexOf(port) : -1;
3366
+ return {source: edge.source, target: edge.target, rank: (rank >= 0)? rank : 0};
3367
+ });
3368
+
3369
+ return {nodes: in_nodes, edges: in_edges};
3370
+ }
3371
+
3372
+ function treedb_layout_output(pos)
3373
+ {
3374
+ let out = [];
3375
+ for(let [id, p] of pos) {
3376
+ out.push({id: id, style: {x: p.x, y: p.y}});
3377
+ }
3378
+ return {nodes: out};
3379
+ }
3380
+
3381
+ class TreedbTreeLayout extends BaseLayout
3382
+ {
3383
+ async execute(data, options) {
3384
+ let input = treedb_layout_input(data);
3385
+ return treedb_layout_output(layout_tree(input.nodes, input.edges, options));
3386
+ }
3387
+ }
3388
+
3389
+ class TreedbOutlineLayout extends BaseLayout
3390
+ {
3391
+ async execute(data, options) {
3392
+ let input = treedb_layout_input(data);
3393
+ return treedb_layout_output(layout_outline(input.nodes, input.edges, options));
3394
+ }
3395
+ }
3396
+
3397
+ class TreedbRadialLayout extends BaseLayout
3398
+ {
3399
+ async execute(data, options) {
3400
+ let input = treedb_layout_input(data);
3401
+ return treedb_layout_output(layout_radial(input.nodes, input.edges, options));
3402
+ }
3403
+ }
3404
+
3307
3405
  /************************************************************
3308
3406
  * Node selection and resize handles
3309
3407
  ************************************************************/
@@ -5860,7 +5958,10 @@ function auto_layout(gobj)
5860
5958
  return false;
5861
5959
  }
5862
5960
 
5863
- let name = "dagre";
5961
+ /* Ours, not dagre: the tidy tree keeps the records in their order
5962
+ * and moves nothing on a fold that is not under it (see
5963
+ * treedb_layout.js). dagre stays in the picker. */
5964
+ let name = "treedb-tree";
5864
5965
  gobj_write_attr(gobj, "layout", name);
5865
5966
  try {
5866
5967
  priv.graph.setLayout(_layouts[name]);
@@ -506,6 +506,12 @@ function option_label(name)
506
506
  /* Layouts (`layout_names` of the G6 child). */
507
507
  case "manual":
508
508
  return t("manual");
509
+ case "treedb-tree":
510
+ return t("treedb-tree");
511
+ case "treedb-outline":
512
+ return t("treedb-outline");
513
+ case "radial":
514
+ return t("radial");
509
515
  case "dagre":
510
516
  return t("dagre");
511
517
  case "antv-dagre":