@weasel-js/diagram 1.9.1 → 1.9.3

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/dist/index.js CHANGED
@@ -1,11 +1,73 @@
1
- import { portOf, portsOf, diagramNodeOf, layered, force, tree, forceRelaxation, center, outlinePath } from './chunk-6VYT4WCM.js';
2
- export { COMPASS, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, force, forceRelaxation, forestOf, graphOrder, isPinned, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, pinnedSet, portOf, portsOf, portsOnOutline, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree } from './chunk-6VYT4WCM.js';
3
- import { dragPanContribution, AUTO_POSE_DESCRIPTOR, pointAlongPath, translatePoseViaDescriptor, polylineFromPoints, effectivePose, composeAffordanceLayer, useLatest, usePoseRun, RECT_POSE_DESCRIPTOR, registerNodeShape, WeaselProvider, useScene, useSelection, SceneCanvas, fitViewToBounds } from '@weasel-js/core';
1
+ import { insetOf, layered, portOf, outlinePath, diagramNodeOf, expandGroupEdges, forceRelaxation, center, force, tree, DIAGRAM_TRAIT_KEY, portsOf, DEFAULT_GROUP_INSET, sizeToBody, measureBody, buildBody } from './chunk-ERT3XWBS.js';
2
+ export { COMPASS, DEFAULT_GROUP_INSET, DEFAULT_NODE_GAP, DEFAULT_PORTS, DEFAULT_RANK_GAP, DIAGRAM_TRAIT_KEY, axesFor, backEdges, bodyOutline, bodyTrait, boxForContent, buildBody, canvasMeasure, contentBox, createDiagramNodes, dataKeyReader, diagramNodeOf, expandGroupEdges, force, forceRelaxation, forestOf, graphOrder, insetOf, isPinned, layered, layoutBody, layoutRowPorts, measureBody, outlinePath, outlinePolyline, packAcross, packClusters, pinnedSet, portOf, portsOf, portsOnOutline, rankGaps, ranksOf, rayHit, seededOrder, settle, sizeToBody, translated, tree } from './chunk-ERT3XWBS.js';
3
+ import { dragPanContribution, WeaselProvider, useScene, useSelection, useLatest, reconcileSpecs, SceneCanvas, fitViewToBounds, AUTO_POSE_DESCRIPTOR, translatePoseViaDescriptor, pointAlongPath, polylineFromPoints, registerNodeShape, effectivePose, usePoseRun, RECT_POSE_DESCRIPTOR, composeAffordanceLayer } from '@weasel-js/core';
4
4
  import { portCurvePoints } from '@weasel-js/geom';
5
5
  import { AUTO_POSE_DESCRIPTOR as AUTO_POSE_DESCRIPTOR$1 } from '@weasel-js/core/math';
6
- import { useRef, useCallback, useEffect, useMemo } from 'react';
6
+ import { forwardRef, useEffect, useMemo, useRef, useState, useImperativeHandle, useCallback } from 'react';
7
7
  import { jsx } from 'react/jsx-runtime';
8
8
 
9
+ function traitOf(node) {
10
+ const data = node.data;
11
+ if (data === null || typeof data !== "object") return null;
12
+ const trait = data[DIAGRAM_TRAIT_KEY];
13
+ return trait !== null && typeof trait === "object" ? trait : null;
14
+ }
15
+ function diagramGroupOf(node) {
16
+ const group = traitOf(node)?.group;
17
+ return group !== null && typeof group === "object" ? group : null;
18
+ }
19
+ function diagramAnchorOf(node) {
20
+ const anchor = traitOf(node)?.anchor;
21
+ return anchor !== null && typeof anchor === "object" && "at" in anchor ? anchor : null;
22
+ }
23
+ function groupDerivePose(opts = {}) {
24
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
25
+ return (node, deps) => {
26
+ const group = diagramGroupOf(node);
27
+ if (group === null) return null;
28
+ let x0 = Infinity;
29
+ let y0 = Infinity;
30
+ let x1 = -Infinity;
31
+ let y1 = -Infinity;
32
+ for (const dep of deps) {
33
+ if (dep === void 0) continue;
34
+ const b = geometry.getBounds(dep.pose);
35
+ x0 = Math.min(x0, b.x);
36
+ y0 = Math.min(y0, b.y);
37
+ x1 = Math.max(x1, b.x + b.width);
38
+ y1 = Math.max(y1, b.y + b.height);
39
+ }
40
+ if (!Number.isFinite(x0)) return null;
41
+ const inset = insetOf(group.inset);
42
+ return geometry.fromBounds(
43
+ {
44
+ x: x0 - inset.left,
45
+ y: y0 - inset.top,
46
+ width: x1 - x0 + inset.left + inset.right,
47
+ height: y1 - y0 + inset.top + inset.bottom
48
+ },
49
+ node.pose
50
+ );
51
+ };
52
+ }
53
+ function anchorDerivePose(opts = {}) {
54
+ const geometry = opts.geometry ?? AUTO_POSE_DESCRIPTOR;
55
+ return (node, deps) => {
56
+ const anchor = diagramAnchorOf(node);
57
+ const dep = deps[0];
58
+ if (anchor === null || dep === void 0) return null;
59
+ const target = geometry.getBounds(dep.pose);
60
+ const own = geometry.getBounds(node.pose);
61
+ const self = anchor.self ?? anchor.at;
62
+ const x = target.x + anchor.at.u * target.width + (anchor.offset?.x ?? 0) - self.u * own.width;
63
+ const y = target.y + anchor.at.v * target.height + (anchor.offset?.y ?? 0) - self.v * own.height;
64
+ return translatePoseViaDescriptor(node.pose, x - own.x, y - own.y, geometry);
65
+ };
66
+ }
67
+ var DIAGRAM_GROUP = "diagram:group";
68
+ var DIAGRAM_ANCHOR = "diagram:anchor";
69
+ var GROUP_DERIVE_POSE = groupDerivePose();
70
+ var ANCHOR_DERIVE_POSE = anchorDerivePose();
9
71
  var NAMED = { start: 0, mid: 0.5, end: 1 };
10
72
  function diagramLabelOf(node) {
11
73
  const data = node.data;
@@ -161,6 +223,8 @@ function withDiagramRegistry(registry = {}) {
161
223
  },
162
224
  derivePose: {
163
225
  [DIAGRAM_LABEL]: LABEL_DERIVE_POSE,
226
+ [DIAGRAM_GROUP]: GROUP_DERIVE_POSE,
227
+ [DIAGRAM_ANCHOR]: ANCHOR_DERIVE_POSE,
164
228
  ...registry.derivePose
165
229
  }
166
230
  };
@@ -403,6 +467,7 @@ function buildGraph(source, opts = {}) {
403
467
  const nodes = [];
404
468
  const byId = /* @__PURE__ */ new Map();
405
469
  const candidates = [];
470
+ const groupNodes = [];
406
471
  for (const { node, pose } of source()) {
407
472
  if (diagramEdgeOf(node) !== null) {
408
473
  const deps = node.dependsOn;
@@ -413,6 +478,15 @@ function buildGraph(source, opts = {}) {
413
478
  }
414
479
  const trait = diagramNodeOf(node, opts.read);
415
480
  if (trait === null) continue;
481
+ if (trait.group !== void 0) {
482
+ const deps = node.dependsOn;
483
+ groupNodes.push({
484
+ id: node.id,
485
+ members: Array.isArray(deps) ? deps : [],
486
+ inset: insetOf(trait.group.inset)
487
+ });
488
+ continue;
489
+ }
416
490
  const entry = {
417
491
  id: node.id,
418
492
  bounds: geometry.getBounds(pose),
@@ -421,7 +495,19 @@ function buildGraph(source, opts = {}) {
421
495
  nodes.push(entry);
422
496
  byId.set(entry.id, entry);
423
497
  }
424
- const edges = candidates.filter((e) => byId.has(e.from) && byId.has(e.to));
498
+ const groups = [];
499
+ const claimed = /* @__PURE__ */ new Set();
500
+ for (const g of groupNodes) {
501
+ const members = g.members.filter((id) => byId.has(id) && !claimed.has(id));
502
+ for (const id of members) {
503
+ claimed.add(id);
504
+ byId.get(id).group = g.id;
505
+ }
506
+ groups.push({ ...g, members });
507
+ }
508
+ const live = groups.filter((g) => g.members.length > 0);
509
+ const known = (id) => byId.has(id) || live.some((g) => g.id === id);
510
+ const edges = expandGroupEdges(candidates.filter((e) => known(e.from) && known(e.to)), live);
425
511
  const out = /* @__PURE__ */ new Map();
426
512
  const into = /* @__PURE__ */ new Map();
427
513
  for (const edge of edges) {
@@ -432,6 +518,7 @@ function buildGraph(source, opts = {}) {
432
518
  return {
433
519
  nodes,
434
520
  edges,
521
+ groups: live,
435
522
  node: (id) => byId.get(id),
436
523
  outgoing: (id) => out.get(id) ?? NONE,
437
524
  incoming: (id) => into.get(id) ?? NONE
@@ -443,23 +530,81 @@ function push(index, key, edge) {
443
530
  else list.push(edge);
444
531
  }
445
532
 
533
+ // src/dataGroups.ts
534
+ function resolveGroups(groups, ids, nodeIds, opts) {
535
+ const claimed = /* @__PURE__ */ new Set();
536
+ const out = [];
537
+ for (const group of groups) {
538
+ if (ids.has(group.id)) throw new Error(`diagramScene: group id "${group.id}" is already a node or group id`);
539
+ ids.add(group.id);
540
+ const members = group.members.filter((m) => nodeIds.has(m) && !claimed.has(m));
541
+ for (const m of members) claimed.add(m);
542
+ if (members.length === 0) continue;
543
+ const pad = group.padding ?? opts.padding;
544
+ const title = group.label ? opts.title(group.label) : null;
545
+ const inset = { top: pad + (title ? title.height + pad / 2 : 0), right: pad, bottom: pad, left: pad };
546
+ out.push({ group, members, inset, pad, title });
547
+ }
548
+ return out;
549
+ }
550
+ function groupSpecs(groups, placedAt, styleOf) {
551
+ const specs = [];
552
+ for (const g of groups) {
553
+ const { style, text } = styleOf(g.group);
554
+ const rs = g.members.map((m) => placedAt.get(m));
555
+ const x = Math.min(...rs.map((r) => r.x)) - g.inset.left;
556
+ const y = Math.min(...rs.map((r) => r.y)) - g.inset.top;
557
+ specs.push({
558
+ id: g.group.id,
559
+ kind: "leaf",
560
+ layer: "main",
561
+ pickable: false,
562
+ pose: {
563
+ x,
564
+ y,
565
+ width: Math.max(...rs.map((r) => r.x + r.width)) + g.inset.right - x,
566
+ height: Math.max(...rs.map((r) => r.y + r.height)) + g.inset.bottom - y
567
+ },
568
+ data: {
569
+ diagram: { outline: "rect", group: { inset: g.inset } },
570
+ fill: { color: style.fill },
571
+ stroke: {
572
+ paint: { color: style.stroke },
573
+ width: style.strokeWidth ?? 1,
574
+ ...style.dash ? { dash: style.dash } : {}
575
+ }
576
+ },
577
+ dependsOn: g.members,
578
+ derivePose: GROUP_DERIVE_POSE
579
+ });
580
+ if (g.title === null || !g.group.label) continue;
581
+ const anchor = { at: { u: 0, v: 0 }, offset: { x: g.pad, y: g.pad } };
582
+ specs.push({
583
+ id: `${g.group.id}/label`,
584
+ kind: "leaf",
585
+ layer: "main",
586
+ pickable: false,
587
+ pose: { x: x + g.pad, y: y + g.pad, width: g.title.width, height: g.title.height },
588
+ data: { diagram: { anchor }, text: g.group.label, style: text, fill: { color: style.text } },
589
+ dependsOn: [g.group.id],
590
+ derivePose: ANCHOR_DERIVE_POSE
591
+ });
592
+ }
593
+ return specs;
594
+ }
595
+
446
596
  // src/fromData.ts
447
597
  var FONT = { fontFamily: "sans-serif", fontSize: 12 };
448
598
  var NODE = { fill: "#16222c", stroke: "#7ba7c7", text: "#dbe7f2" };
449
599
  var EDGE = { stroke: "#7ba7c7" };
450
- var PAD = { x: 12, y: 8 };
600
+ var GROUP = { fill: "rgba(123, 167, 199, 0.08)", stroke: "#7ba7c7", text: "#9fbdd6", strokeWidth: 1, dash: [4, 3] };
601
+ var LABEL_PLACEMENT = { at: "mid", offset: 9 };
451
602
  var LINE = 1.4;
452
603
  var GLYPH = 0.6;
453
604
  var MIN_WIDTH = 64;
454
- var edgeIdOf = (e) => `edge:${e.from}->${e.to}${e.label ? `:${e.label}` : ""}`;
455
- function sizeOf(node, fontSize) {
456
- const longest = Math.max(0, ...node.lines.map((l) => l.length));
457
- return {
458
- width: Math.max(MIN_WIDTH, Math.ceil(longest * GLYPH * fontSize + 2 * PAD.x)),
459
- height: Math.ceil(Math.max(1, node.lines.length) * LINE * fontSize + 2 * PAD.y)
460
- };
461
- }
462
- function graphOf(nodes, edges) {
605
+ var LAYOUTS = { layered, tree, force };
606
+ var edgeIdOf = (e) => `edge:${e.from}${e.fromPort ? `.${e.fromPort}` : ""}->${e.to}${e.toPort ? `.${e.toPort}` : ""}${e.label ? `:${e.label}` : ""}`;
607
+ function graphOf(nodes, edges, groups = []) {
463
608
  const byId = new Map(nodes.map((n) => [n.id, n]));
464
609
  const out = /* @__PURE__ */ new Map();
465
610
  const inc = /* @__PURE__ */ new Map();
@@ -470,6 +615,7 @@ function graphOf(nodes, edges) {
470
615
  return {
471
616
  nodes,
472
617
  edges,
618
+ groups,
473
619
  node: (id) => byId.get(id),
474
620
  outgoing: (id) => out.get(id) ?? [],
475
621
  incoming: (id) => inc.get(id) ?? []
@@ -477,11 +623,26 @@ function graphOf(nodes, edges) {
477
623
  }
478
624
  function diagramScene(data, opts = {}) {
479
625
  const font = opts.font ?? FONT;
626
+ const styleOf = (s) => ({
627
+ fontFamily: s?.fontFamily ?? font.fontFamily,
628
+ fontSize: s?.fontSize ?? font.fontSize,
629
+ ...s?.bold === true ? { fontWeight: 700 } : {}
630
+ });
631
+ const measure = opts.measure ? (text, s) => opts.measure(text, { ...s, fontFamily: s.fontFamily ?? font.fontFamily, fontSize: s.fontSize ?? font.fontSize }) : (text, s) => {
632
+ const size = s.fontSize ?? font.fontSize;
633
+ return { width: text.length * GLYPH * size, height: Math.ceil(LINE * size) };
634
+ };
480
635
  const ids = /* @__PURE__ */ new Set();
481
636
  for (const n of data.nodes) {
482
637
  if (ids.has(n.id)) throw new Error(`diagramScene: node id "${n.id}" appears more than once`);
483
638
  ids.add(n.id);
484
639
  }
640
+ const nodeIds = new Set(ids);
641
+ const titleStyle = { ...font, fontSize: font.fontSize - 1 };
642
+ const groups = resolveGroups(data.groups ?? [], ids, nodeIds, {
643
+ padding: opts.groupPadding ?? DEFAULT_GROUP_INSET,
644
+ title: (text) => measure(text, titleStyle)
645
+ });
485
646
  const seen = /* @__PURE__ */ new Set();
486
647
  const edges = data.edges.filter((e) => {
487
648
  const id = edgeIdOf(e);
@@ -489,49 +650,75 @@ function diagramScene(data, opts = {}) {
489
650
  seen.add(id);
490
651
  return true;
491
652
  });
492
- const seeds = data.nodes.map((n, i) => ({
493
- id: n.id,
494
- bounds: { x: i, y: i, ...sizeOf(n, font.fontSize) },
495
- pinned: false
653
+ const bodies = data.nodes.map((n) => ({
654
+ outline: n.outline ?? opts.outline ?? "rect",
655
+ rows: n.rows ?? (n.lines ?? []).map((text) => ({ kind: "label", text })),
656
+ padding: n.padding ?? opts.padding ?? 8,
657
+ gap: n.gap ?? opts.gap ?? 4,
658
+ verticalAlign: n.verticalAlign ?? opts.verticalAlign ?? "center",
659
+ ...n.ports ? { ports: n.ports } : {}
496
660
  }));
497
- const graph = graphOf(seeds, edges.map((e) => ({ id: edgeIdOf(e), from: e.from, to: e.to })));
498
- const moved = (opts.layout ?? layered)(graph, opts.layoutOptions ?? { order: "barycenter" });
661
+ const seeds = data.nodes.map((n, i) => {
662
+ const pose = sizeToBody(
663
+ { ...n.at ?? { x: i, y: i }, width: n.width ?? opts.minWidth ?? MIN_WIDTH, height: n.height ?? 0 },
664
+ measureBody(bodies[i], measure)
665
+ );
666
+ return { id: n.id, bounds: pose, pinned: n.pinned === true };
667
+ });
668
+ const graphGroups = groups.map((g) => ({ id: g.group.id, members: g.members, inset: g.inset }));
669
+ const seedOf = new Map(seeds.map((n) => [n.id, n]));
670
+ for (const g of groups) for (const m of g.members) seedOf.get(m).group = g.group.id;
671
+ const graph = graphOf(
672
+ seeds,
673
+ expandGroupEdges(edges.map((e) => ({ id: edgeIdOf(e), from: e.from, to: e.to })), graphGroups),
674
+ graphGroups
675
+ );
676
+ const layout = typeof opts.layout === "function" ? opts.layout : opts.layout === "none" ? null : LAYOUTS[opts.layout ?? "layered"];
677
+ const moved = layout === null ? /* @__PURE__ */ new Map() : layout(graph, { order: "barycenter", ...opts.layoutOptions });
499
678
  const specs = [];
500
- for (const [i, n] of data.nodes.entries()) {
679
+ const placedAt = new Map(data.nodes.map((n, i) => {
501
680
  const seed = seeds[i].bounds;
502
681
  const at = moved.get(n.id) ?? seed;
503
- const pose = { x: at.x, y: at.y, width: seed.width, height: seed.height };
682
+ return [n.id, { x: at.x, y: at.y, width: seed.width, height: seed.height }];
683
+ }));
684
+ specs.push(...groupSpecs(groups, placedAt, (g) => ({
685
+ style: opts.groupStyle?.(g) ?? GROUP,
686
+ text: styleOf(titleStyle)
687
+ })));
688
+ for (const [i, n] of data.nodes.entries()) {
689
+ const seed = seeds[i].bounds;
690
+ const at = placedAt.get(n.id);
504
691
  const style = opts.nodeStyle?.(n) ?? NODE;
505
- specs.push({
506
- id: n.id,
507
- kind: "container",
508
- layer: "main",
509
- pose,
510
- data: {
511
- diagram: { outline: "rect" },
512
- fill: { color: style.fill },
513
- stroke: { paint: { color: style.stroke }, width: style.strokeWidth ?? 2 }
514
- }
515
- });
516
- for (const [k, text] of n.lines.entries()) {
517
- specs.push({
518
- kind: "leaf",
692
+ const textData = (text, s) => ({ text, style: styleOf(s), fill: { color: style.text } });
693
+ const built = buildBody(
694
+ bodies[i],
695
+ { x: at.x, y: at.y, width: seed.width, height: seed.height },
696
+ {
697
+ id: n.id,
519
698
  layer: "main",
520
- parent: n.id,
521
- pickable: false,
522
- pose: {
523
- x: pose.x + PAD.x,
524
- y: pose.y + PAD.y + k * LINE * font.fontSize,
525
- width: pose.width - 2 * PAD.x,
526
- height: LINE * font.fontSize
527
- },
528
- data: { text, style: font, fill: { color: style.text } }
529
- });
699
+ measure,
700
+ body: (trait) => ({
701
+ diagram: n.pinned === true ? { ...trait, pinned: true } : trait,
702
+ fill: { color: style.fill },
703
+ stroke: {
704
+ paint: { color: style.stroke },
705
+ width: style.strokeWidth ?? 2,
706
+ ...style.dash ? { dash: style.dash } : {}
707
+ }
708
+ }),
709
+ row: (text, box) => text === "" ? null : textData(text, box.row.kind === "label" || box.row.kind === "field" ? box.row.style : void 0),
710
+ portLabel: (text) => textData(text, void 0)
711
+ }
712
+ );
713
+ for (const [k, s] of built.specs.entries()) {
714
+ specs.push({ ...s, ...k === 0 ? {} : { id: `${n.id}/${k}` } });
530
715
  }
531
716
  }
532
717
  for (const e of edges) {
533
718
  const id = edgeIdOf(e);
534
719
  const style = opts.edgeStyle?.(e) ?? EDGE;
720
+ const markerStart = style.markerStart ?? null;
721
+ const markerEnd = style.markerEnd === void 0 ? "arrow" : style.markerEnd;
535
722
  specs.push({
536
723
  id,
537
724
  kind: "leaf",
@@ -539,23 +726,36 @@ function diagramScene(data, opts = {}) {
539
726
  pickable: false,
540
727
  pose: { x: 0, y: 0, width: 0, height: 0 },
541
728
  data: {
542
- diagram: { from: {}, to: {}, router: opts.router ?? "bezier" },
543
- stroke: { paint: { color: style.stroke }, width: style.width ?? 1.5, markerEnd: "arrow" }
729
+ diagram: {
730
+ from: e.fromPort ? { port: e.fromPort } : {},
731
+ to: e.toPort ? { port: e.toPort } : {},
732
+ router: e.router ?? opts.router ?? "bezier",
733
+ ...e.waypoints ? { waypoints: e.waypoints } : {}
734
+ },
735
+ stroke: {
736
+ paint: { color: style.stroke },
737
+ width: style.width ?? 1.5,
738
+ ...style.dash ? { dash: style.dash } : {},
739
+ ...markerStart !== null ? { markerStart } : {},
740
+ ...markerEnd !== null ? { markerEnd } : {}
741
+ }
544
742
  },
545
743
  dependsOn: [e.from, e.to],
546
744
  derivePath: EDGE_DERIVE_PATH
547
745
  });
548
746
  if (e.label) {
747
+ const labelStyle = { ...font, fontSize: font.fontSize - 2 };
549
748
  specs.push({
749
+ id: `${id}/label`,
550
750
  kind: "leaf",
551
751
  layer: "main",
552
752
  pickable: false,
553
- pose: { x: 0, y: 0, width: e.label.length * GLYPH * (font.fontSize - 2), height: font.fontSize },
753
+ pose: { x: 0, y: 0, width: measure(e.label, labelStyle).width, height: labelStyle.fontSize },
554
754
  data: {
555
- diagram: { label: { at: "mid", offset: 9 } },
755
+ diagram: { label: e.labelPlacement ?? opts.labelPlacement ?? LABEL_PLACEMENT },
556
756
  text: e.label,
557
- style: { ...font, fontSize: font.fontSize - 2 },
558
- fill: { color: style.stroke }
757
+ style: labelStyle,
758
+ fill: { color: style.label ?? style.stroke }
559
759
  },
560
760
  dependsOn: [id],
561
761
  derivePose: LABEL_DERIVE_POSE
@@ -564,7 +764,7 @@ function diagramScene(data, opts = {}) {
564
764
  }
565
765
  return specs;
566
766
  }
567
- var LAYOUTS = Object.freeze({
767
+ var LAYOUTS2 = Object.freeze({
568
768
  layered,
569
769
  tree,
570
770
  force
@@ -609,7 +809,7 @@ function subtreeOf(scene, id) {
609
809
  }
610
810
  var LAYOUT_ACTION_ID = "diagram.layout";
611
811
  function createLayoutAction(opts) {
612
- const algorithm = typeof opts.algorithm === "function" ? opts.algorithm : LAYOUTS[opts.algorithm ?? "layered"] ?? layered;
812
+ const algorithm = typeof opts.algorithm === "function" ? opts.algorithm : LAYOUTS2[opts.algorithm ?? "layered"] ?? layered;
613
813
  const build = {
614
814
  ...opts.read ? { read: opts.read } : {},
615
815
  ...opts.geometry ? { geometry: opts.geometry } : {}
@@ -725,7 +925,7 @@ function useLiveLayout(opts) {
725
925
  });
726
926
  return;
727
927
  }
728
- const fn = typeof algorithm === "function" ? algorithm : LAYOUTS[algorithm] ?? layered;
928
+ const fn = typeof algorithm === "function" ? algorithm : LAYOUTS2[algorithm] ?? layered;
729
929
  producer.current = easedProducer(graph, fn, {
730
930
  ...o.layout ? { layout: o.layout } : {},
731
931
  ...o.frames === void 0 ? {} : { frames: o.frames },
@@ -836,18 +1036,32 @@ function useMirroredSelection(selection, selected, onSelect) {
836
1036
  onSelectRef.current?.(selection.get()[0] ?? null);
837
1037
  }, [selection, onSelectRef]);
838
1038
  }
839
- var made = 0;
840
- var keys = /* @__PURE__ */ new WeakMap();
841
- var keyOf = (specs) => {
842
- let k = keys.get(specs);
843
- if (k === void 0) keys.set(specs, k = ++made);
844
- return k;
845
- };
846
1039
  var AMBIENT = [dragPanContribution()];
847
- function DiagramView(props) {
848
- return /* @__PURE__ */ jsx(WeaselProvider, { children: /* @__PURE__ */ jsx(Inner, { ...props }, keyOf(props.specs)) });
849
- }
850
- function Inner({ specs, width, height, selected, onSelect, minScale, anchor, className }) {
1040
+ var DiagramView = forwardRef(function DiagramView2(props, ref) {
1041
+ return /* @__PURE__ */ jsx(WeaselProvider, { children: /* @__PURE__ */ jsx(Inner, { ...props, apiRef: ref }) });
1042
+ });
1043
+ function Inner({
1044
+ specs,
1045
+ width,
1046
+ height,
1047
+ selected,
1048
+ onSelect,
1049
+ onMove,
1050
+ onConnect,
1051
+ enableKeybindings,
1052
+ canConnect,
1053
+ portOptions,
1054
+ live: liveOpts,
1055
+ minScale,
1056
+ maxScale,
1057
+ anchor,
1058
+ fitPadding,
1059
+ background,
1060
+ view,
1061
+ onViewChange,
1062
+ className,
1063
+ apiRef
1064
+ }) {
851
1065
  useEffect(() => registerDiagramShape(), []);
852
1066
  const registry = useMemo(() => withDiagramRegistry(), []);
853
1067
  const scene = useScene({
@@ -857,27 +1071,97 @@ function Inner({ specs, width, height, selected, onSelect, minScale, anchor, cla
857
1071
  });
858
1072
  const selection = useSelection({ scene, mode: "single" });
859
1073
  const onClick = useMirroredSelection(selection, selected, onSelect);
1074
+ const onMoveRef = useLatest(onMove);
1075
+ const syncing = useRef(false);
1076
+ const applied = useRef(specs);
1077
+ const known = useRef(/* @__PURE__ */ new Map());
1078
+ useEffect(() => {
1079
+ const read = () => {
1080
+ const out = /* @__PURE__ */ new Map();
1081
+ for (const n of scene.renderOrderNodes()) {
1082
+ if (n.kind === "container") out.set(n.id, { x: n.pose.x, y: n.pose.y });
1083
+ }
1084
+ return out;
1085
+ };
1086
+ known.current = read();
1087
+ return scene.subscribe(() => {
1088
+ const now = read();
1089
+ const moves = [];
1090
+ if (!syncing.current) {
1091
+ for (const [id, at] of now) {
1092
+ const was = known.current.get(id);
1093
+ if (was !== void 0 && (was.x !== at.x || was.y !== at.y)) moves.push({ id, ...at });
1094
+ }
1095
+ }
1096
+ known.current = now;
1097
+ if (moves.length > 0) onMoveRef.current?.(moves);
1098
+ });
1099
+ }, [scene, onMoveRef]);
1100
+ useEffect(() => {
1101
+ if (applied.current === specs) return;
1102
+ syncing.current = true;
1103
+ try {
1104
+ reconcileSpecs(scene, applied.current, specs);
1105
+ } finally {
1106
+ syncing.current = false;
1107
+ }
1108
+ applied.current = specs;
1109
+ }, [scene, specs]);
1110
+ const source = useMemo(() => sceneParticipants(scene), [scene]);
1111
+ const [run, setRun] = useState(null);
1112
+ const live = useLiveLayout({ ...liveOpts, scene, source, algorithm: run?.algorithm ?? "force" });
1113
+ useEffect(() => {
1114
+ if (run !== null) live.restart();
1115
+ }, [run, live]);
1116
+ useImperativeHandle(apiRef, () => ({
1117
+ layout: (algorithm = "force") => setRun({ algorithm }),
1118
+ live
1119
+ }), [live]);
1120
+ const onConnectRef = useLatest(onConnect);
1121
+ const connecting = onConnect !== void 0;
1122
+ const ports = useMemo(() => connecting ? diagramPorts({
1123
+ ...portOptions,
1124
+ participants: source,
1125
+ ...canConnect ? { canConnect } : {},
1126
+ commit: ({ from, to }) => onConnectRef.current?.({
1127
+ from: from.nodeId,
1128
+ fromPort: from.id,
1129
+ to: to.nodeId,
1130
+ toPort: to.id
1131
+ })
1132
+ }) : null, [connecting, source, canConnect, portOptions, onConnectRef]);
1133
+ const canvasRef = useRef(null);
1134
+ useEffect(() => ports ? canvasRef.current?.registerLayer(ports.layer) : void 0, [ports]);
1135
+ const ambient = useMemo(() => ports ? [...AMBIENT, ports.contribution] : AMBIENT, [ports]);
1136
+ const controlled = view !== void 0;
860
1137
  const defaultView = useMemo(
861
- () => fitDiagram(specs, { width, height }, minScale, anchor),
862
- [specs, width, height, minScale, anchor]
1138
+ () => controlled ? void 0 : fitDiagram(specs, { width, height }, { minScale, maxScale, anchor, padding: fitPadding }),
1139
+ // The fit is where the view opens; a later `specs` keeps the view it has.
1140
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1141
+ [controlled, width, height, minScale, maxScale, anchor, fitPadding]
863
1142
  );
864
1143
  return /* @__PURE__ */ jsx(
865
1144
  SceneCanvas,
866
1145
  {
1146
+ ref: canvasRef,
867
1147
  scene,
868
1148
  selection,
869
- features: ["view", "pick"],
870
- ambient: AMBIENT,
1149
+ features: onMove ? ["view", "pick", "move"] : ["view", "pick"],
1150
+ enableKeybindings: enableKeybindings ?? (onMove !== void 0 || onConnect !== void 0),
1151
+ ambient,
871
1152
  onClick,
872
1153
  width,
873
1154
  height,
874
1155
  className,
875
- ...defaultView ? { defaultView } : {}
1156
+ ...defaultView ? { defaultView } : {},
1157
+ ...view ? { view } : {},
1158
+ ...onViewChange ? { onViewChange } : {},
1159
+ ...background ? { backgroundFill: background } : {}
876
1160
  }
877
1161
  );
878
1162
  }
879
- function fitDiagram(specs, size, minScale, anchor) {
880
- const boxes = specs.filter((s) => s.kind === "container").map((s) => s.pose);
1163
+ function fitDiagram(specs, size, fit = {}) {
1164
+ const boxes = specs.filter((s) => s.kind === "container" || diagramGroupOf(s) !== null).map((s) => s.pose);
881
1165
  if (boxes.length === 0) return void 0;
882
1166
  const x = Math.min(...boxes.map((b) => b.x));
883
1167
  const y = Math.min(...boxes.map((b) => b.y));
@@ -888,12 +1172,13 @@ function fitDiagram(specs, size, minScale, anchor) {
888
1172
  height: Math.max(...boxes.map((b) => b.y + b.height)) - y
889
1173
  };
890
1174
  return fitViewToBounds(bounds, size, { x: 0, y: 0, scale: { x: 1, y: 1 } }, {
891
- maxScale: 1,
892
- ...minScale !== void 0 ? { minScale } : {},
893
- ...anchor !== void 0 ? { anchor } : {}
1175
+ maxScale: fit.maxScale ?? 1,
1176
+ ...fit.minScale !== void 0 ? { minScale: fit.minScale } : {},
1177
+ ...fit.anchor !== void 0 ? { anchor: fit.anchor } : {},
1178
+ ...fit.padding !== void 0 ? { padding: fit.padding } : {}
894
1179
  });
895
1180
  }
896
1181
 
897
- export { CONNECT_ACTION_ID, DEFAULT_EDGE_STROKE, DIAGRAM_EDGE, DIAGRAM_LABEL, DiagramView, EASED_LAYOUTS, EDGE_DERIVE_PATH, GRAB_PORT_ACTION_ID, LABEL_DERIVE_POSE, LAYOUTS, LAYOUT_ACTION_ID, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, ROUTERS, applyLayout, bezier, buildGraph, commitEdgeToScene, connectBinding, createConnectAction, createDiagramContribution, createLayoutAction, createPortAffordance, defaultCanConnect, diagramEdgeOf, diagramLabelOf, diagramPorts, diagramScene, diagramShape, easedProducer, edgeDerivePath, edgeIdOf, forceProducer, grabPortAction, grabPortBindings, labelDerivePose, layoutPoses, orthogonal, portLayer, portScratchOf, registerDiagramShape, resolveEnd, sceneParticipants, straight, useLiveLayout, useMirroredSelection, withDiagramRegistry };
1182
+ export { ANCHOR_DERIVE_POSE, CONNECT_ACTION_ID, DEFAULT_EDGE_STROKE, DIAGRAM_ANCHOR, DIAGRAM_EDGE, DIAGRAM_GROUP, DIAGRAM_LABEL, DiagramView, EASED_LAYOUTS, EDGE_DERIVE_PATH, GRAB_PORT_ACTION_ID, GROUP_DERIVE_POSE, LABEL_DERIVE_POSE, LAYOUTS2 as LAYOUTS, LAYOUT_ACTION_ID, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, ROUTERS, anchorDerivePose, applyLayout, bezier, buildGraph, commitEdgeToScene, connectBinding, createConnectAction, createDiagramContribution, createLayoutAction, createPortAffordance, defaultCanConnect, diagramAnchorOf, diagramEdgeOf, diagramGroupOf, diagramLabelOf, diagramPorts, diagramScene, diagramShape, easedProducer, edgeDerivePath, edgeIdOf, forceProducer, grabPortAction, grabPortBindings, groupDerivePose, labelDerivePose, layoutPoses, orthogonal, portLayer, portScratchOf, registerDiagramShape, resolveEnd, sceneParticipants, straight, useLiveLayout, useMirroredSelection, withDiagramRegistry };
898
1183
  //# sourceMappingURL=index.js.map
899
1184
  //# sourceMappingURL=index.js.map