@weasel-js/diagram 1.9.1 → 1.9.2

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,9 +1,9 @@
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 { layered, portOf, outlinePath, diagramNodeOf, forceRelaxation, center, force, tree, portsOf, sizeToBody, measureBody, buildBody } from './chunk-UBM2J3HJ.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-UBM2J3HJ.js';
3
+ import { dragPanContribution, WeaselProvider, useScene, useSelection, useLatest, reconcileSpecs, SceneCanvas, fitViewToBounds, AUTO_POSE_DESCRIPTOR, pointAlongPath, translatePoseViaDescriptor, 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
9
  var NAMED = { start: 0, mid: 0.5, end: 1 };
@@ -447,18 +447,12 @@ function push(index, key, edge) {
447
447
  var FONT = { fontFamily: "sans-serif", fontSize: 12 };
448
448
  var NODE = { fill: "#16222c", stroke: "#7ba7c7", text: "#dbe7f2" };
449
449
  var EDGE = { stroke: "#7ba7c7" };
450
- var PAD = { x: 12, y: 8 };
450
+ var LABEL_PLACEMENT = { at: "mid", offset: 9 };
451
451
  var LINE = 1.4;
452
452
  var GLYPH = 0.6;
453
453
  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
- }
454
+ var LAYOUTS = { layered, tree, force };
455
+ var edgeIdOf = (e) => `edge:${e.from}${e.fromPort ? `.${e.fromPort}` : ""}->${e.to}${e.toPort ? `.${e.toPort}` : ""}${e.label ? `:${e.label}` : ""}`;
462
456
  function graphOf(nodes, edges) {
463
457
  const byId = new Map(nodes.map((n) => [n.id, n]));
464
458
  const out = /* @__PURE__ */ new Map();
@@ -477,6 +471,15 @@ function graphOf(nodes, edges) {
477
471
  }
478
472
  function diagramScene(data, opts = {}) {
479
473
  const font = opts.font ?? FONT;
474
+ const styleOf = (s) => ({
475
+ fontFamily: s?.fontFamily ?? font.fontFamily,
476
+ fontSize: s?.fontSize ?? font.fontSize,
477
+ ...s?.bold === true ? { fontWeight: 700 } : {}
478
+ });
479
+ const measure = opts.measure ? (text, s) => opts.measure(text, { ...s, fontFamily: s.fontFamily ?? font.fontFamily, fontSize: s.fontSize ?? font.fontSize }) : (text, s) => {
480
+ const size = s.fontSize ?? font.fontSize;
481
+ return { width: text.length * GLYPH * size, height: Math.ceil(LINE * size) };
482
+ };
480
483
  const ids = /* @__PURE__ */ new Set();
481
484
  for (const n of data.nodes) {
482
485
  if (ids.has(n.id)) throw new Error(`diagramScene: node id "${n.id}" appears more than once`);
@@ -489,49 +492,59 @@ function diagramScene(data, opts = {}) {
489
492
  seen.add(id);
490
493
  return true;
491
494
  });
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
495
+ const bodies = data.nodes.map((n) => ({
496
+ outline: n.outline ?? opts.outline ?? "rect",
497
+ rows: n.rows ?? (n.lines ?? []).map((text) => ({ kind: "label", text })),
498
+ padding: n.padding ?? opts.padding ?? 8,
499
+ gap: n.gap ?? opts.gap ?? 4,
500
+ verticalAlign: n.verticalAlign ?? opts.verticalAlign ?? "center",
501
+ ...n.ports ? { ports: n.ports } : {}
496
502
  }));
503
+ const seeds = data.nodes.map((n, i) => {
504
+ const pose = sizeToBody(
505
+ { ...n.at ?? { x: i, y: i }, width: n.width ?? opts.minWidth ?? MIN_WIDTH, height: n.height ?? 0 },
506
+ measureBody(bodies[i], measure)
507
+ );
508
+ return { id: n.id, bounds: pose, pinned: n.pinned === true };
509
+ });
497
510
  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" });
511
+ const layout = typeof opts.layout === "function" ? opts.layout : opts.layout === "none" ? null : LAYOUTS[opts.layout ?? "layered"];
512
+ const moved = layout === null ? /* @__PURE__ */ new Map() : layout(graph, { order: "barycenter", ...opts.layoutOptions });
499
513
  const specs = [];
500
514
  for (const [i, n] of data.nodes.entries()) {
501
515
  const seed = seeds[i].bounds;
502
516
  const at = moved.get(n.id) ?? seed;
503
- const pose = { x: at.x, y: at.y, width: seed.width, height: seed.height };
504
517
  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",
518
+ const textData = (text, s) => ({ text, style: styleOf(s), fill: { color: style.text } });
519
+ const built = buildBody(
520
+ bodies[i],
521
+ { x: at.x, y: at.y, width: seed.width, height: seed.height },
522
+ {
523
+ id: n.id,
519
524
  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
- });
525
+ measure,
526
+ body: (trait) => ({
527
+ diagram: n.pinned === true ? { ...trait, pinned: true } : trait,
528
+ fill: { color: style.fill },
529
+ stroke: {
530
+ paint: { color: style.stroke },
531
+ width: style.strokeWidth ?? 2,
532
+ ...style.dash ? { dash: style.dash } : {}
533
+ }
534
+ }),
535
+ row: (text, box) => text === "" ? null : textData(text, box.row.kind === "label" || box.row.kind === "field" ? box.row.style : void 0),
536
+ portLabel: (text) => textData(text, void 0)
537
+ }
538
+ );
539
+ for (const [k, s] of built.specs.entries()) {
540
+ specs.push({ ...s, ...k === 0 ? {} : { id: `${n.id}/${k}` } });
530
541
  }
531
542
  }
532
543
  for (const e of edges) {
533
544
  const id = edgeIdOf(e);
534
545
  const style = opts.edgeStyle?.(e) ?? EDGE;
546
+ const markerStart = style.markerStart ?? null;
547
+ const markerEnd = style.markerEnd === void 0 ? "arrow" : style.markerEnd;
535
548
  specs.push({
536
549
  id,
537
550
  kind: "leaf",
@@ -539,23 +552,36 @@ function diagramScene(data, opts = {}) {
539
552
  pickable: false,
540
553
  pose: { x: 0, y: 0, width: 0, height: 0 },
541
554
  data: {
542
- diagram: { from: {}, to: {}, router: opts.router ?? "bezier" },
543
- stroke: { paint: { color: style.stroke }, width: style.width ?? 1.5, markerEnd: "arrow" }
555
+ diagram: {
556
+ from: e.fromPort ? { port: e.fromPort } : {},
557
+ to: e.toPort ? { port: e.toPort } : {},
558
+ router: e.router ?? opts.router ?? "bezier",
559
+ ...e.waypoints ? { waypoints: e.waypoints } : {}
560
+ },
561
+ stroke: {
562
+ paint: { color: style.stroke },
563
+ width: style.width ?? 1.5,
564
+ ...style.dash ? { dash: style.dash } : {},
565
+ ...markerStart !== null ? { markerStart } : {},
566
+ ...markerEnd !== null ? { markerEnd } : {}
567
+ }
544
568
  },
545
569
  dependsOn: [e.from, e.to],
546
570
  derivePath: EDGE_DERIVE_PATH
547
571
  });
548
572
  if (e.label) {
573
+ const labelStyle = { ...font, fontSize: font.fontSize - 2 };
549
574
  specs.push({
575
+ id: `${id}/label`,
550
576
  kind: "leaf",
551
577
  layer: "main",
552
578
  pickable: false,
553
- pose: { x: 0, y: 0, width: e.label.length * GLYPH * (font.fontSize - 2), height: font.fontSize },
579
+ pose: { x: 0, y: 0, width: measure(e.label, labelStyle).width, height: labelStyle.fontSize },
554
580
  data: {
555
- diagram: { label: { at: "mid", offset: 9 } },
581
+ diagram: { label: e.labelPlacement ?? opts.labelPlacement ?? LABEL_PLACEMENT },
556
582
  text: e.label,
557
- style: { ...font, fontSize: font.fontSize - 2 },
558
- fill: { color: style.stroke }
583
+ style: labelStyle,
584
+ fill: { color: style.label ?? style.stroke }
559
585
  },
560
586
  dependsOn: [id],
561
587
  derivePose: LABEL_DERIVE_POSE
@@ -564,7 +590,7 @@ function diagramScene(data, opts = {}) {
564
590
  }
565
591
  return specs;
566
592
  }
567
- var LAYOUTS = Object.freeze({
593
+ var LAYOUTS2 = Object.freeze({
568
594
  layered,
569
595
  tree,
570
596
  force
@@ -609,7 +635,7 @@ function subtreeOf(scene, id) {
609
635
  }
610
636
  var LAYOUT_ACTION_ID = "diagram.layout";
611
637
  function createLayoutAction(opts) {
612
- const algorithm = typeof opts.algorithm === "function" ? opts.algorithm : LAYOUTS[opts.algorithm ?? "layered"] ?? layered;
638
+ const algorithm = typeof opts.algorithm === "function" ? opts.algorithm : LAYOUTS2[opts.algorithm ?? "layered"] ?? layered;
613
639
  const build = {
614
640
  ...opts.read ? { read: opts.read } : {},
615
641
  ...opts.geometry ? { geometry: opts.geometry } : {}
@@ -725,7 +751,7 @@ function useLiveLayout(opts) {
725
751
  });
726
752
  return;
727
753
  }
728
- const fn = typeof algorithm === "function" ? algorithm : LAYOUTS[algorithm] ?? layered;
754
+ const fn = typeof algorithm === "function" ? algorithm : LAYOUTS2[algorithm] ?? layered;
729
755
  producer.current = easedProducer(graph, fn, {
730
756
  ...o.layout ? { layout: o.layout } : {},
731
757
  ...o.frames === void 0 ? {} : { frames: o.frames },
@@ -836,18 +862,31 @@ function useMirroredSelection(selection, selected, onSelect) {
836
862
  onSelectRef.current?.(selection.get()[0] ?? null);
837
863
  }, [selection, onSelectRef]);
838
864
  }
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
865
  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 }) {
866
+ var DiagramView = forwardRef(function DiagramView2(props, ref) {
867
+ return /* @__PURE__ */ jsx(WeaselProvider, { children: /* @__PURE__ */ jsx(Inner, { ...props, apiRef: ref }) });
868
+ });
869
+ function Inner({
870
+ specs,
871
+ width,
872
+ height,
873
+ selected,
874
+ onSelect,
875
+ onMove,
876
+ onConnect,
877
+ canConnect,
878
+ portOptions,
879
+ live: liveOpts,
880
+ minScale,
881
+ maxScale,
882
+ anchor,
883
+ fitPadding,
884
+ background,
885
+ view,
886
+ onViewChange,
887
+ className,
888
+ apiRef
889
+ }) {
851
890
  useEffect(() => registerDiagramShape(), []);
852
891
  const registry = useMemo(() => withDiagramRegistry(), []);
853
892
  const scene = useScene({
@@ -857,26 +896,95 @@ function Inner({ specs, width, height, selected, onSelect, minScale, anchor, cla
857
896
  });
858
897
  const selection = useSelection({ scene, mode: "single" });
859
898
  const onClick = useMirroredSelection(selection, selected, onSelect);
899
+ const onMoveRef = useLatest(onMove);
900
+ const syncing = useRef(false);
901
+ const applied = useRef(specs);
902
+ const known = useRef(/* @__PURE__ */ new Map());
903
+ useEffect(() => {
904
+ const read = () => {
905
+ const out = /* @__PURE__ */ new Map();
906
+ for (const n of scene.renderOrderNodes()) {
907
+ if (n.kind === "container") out.set(n.id, { x: n.pose.x, y: n.pose.y });
908
+ }
909
+ return out;
910
+ };
911
+ known.current = read();
912
+ return scene.subscribe(() => {
913
+ const now = read();
914
+ const moves = [];
915
+ if (!syncing.current) {
916
+ for (const [id, at] of now) {
917
+ const was = known.current.get(id);
918
+ if (was !== void 0 && (was.x !== at.x || was.y !== at.y)) moves.push({ id, ...at });
919
+ }
920
+ }
921
+ known.current = now;
922
+ if (moves.length > 0) onMoveRef.current?.(moves);
923
+ });
924
+ }, [scene, onMoveRef]);
925
+ useEffect(() => {
926
+ if (applied.current === specs) return;
927
+ syncing.current = true;
928
+ try {
929
+ reconcileSpecs(scene, applied.current, specs);
930
+ } finally {
931
+ syncing.current = false;
932
+ }
933
+ applied.current = specs;
934
+ }, [scene, specs]);
935
+ const source = useMemo(() => sceneParticipants(scene), [scene]);
936
+ const [run, setRun] = useState(null);
937
+ const live = useLiveLayout({ ...liveOpts, scene, source, algorithm: run?.algorithm ?? "force" });
938
+ useEffect(() => {
939
+ if (run !== null) live.restart();
940
+ }, [run, live]);
941
+ useImperativeHandle(apiRef, () => ({
942
+ layout: (algorithm = "force") => setRun({ algorithm }),
943
+ live
944
+ }), [live]);
945
+ const onConnectRef = useLatest(onConnect);
946
+ const connecting = onConnect !== void 0;
947
+ const ports = useMemo(() => connecting ? diagramPorts({
948
+ ...portOptions,
949
+ participants: source,
950
+ ...canConnect ? { canConnect } : {},
951
+ commit: ({ from, to }) => onConnectRef.current?.({
952
+ from: from.nodeId,
953
+ fromPort: from.id,
954
+ to: to.nodeId,
955
+ toPort: to.id
956
+ })
957
+ }) : null, [connecting, source, canConnect, portOptions, onConnectRef]);
958
+ const canvasRef = useRef(null);
959
+ useEffect(() => ports ? canvasRef.current?.registerLayer(ports.layer) : void 0, [ports]);
960
+ const ambient = useMemo(() => ports ? [...AMBIENT, ports.contribution] : AMBIENT, [ports]);
961
+ const controlled = view !== void 0;
860
962
  const defaultView = useMemo(
861
- () => fitDiagram(specs, { width, height }, minScale, anchor),
862
- [specs, width, height, minScale, anchor]
963
+ () => controlled ? void 0 : fitDiagram(specs, { width, height }, { minScale, maxScale, anchor, padding: fitPadding }),
964
+ // The fit is where the view opens; a later `specs` keeps the view it has.
965
+ // eslint-disable-next-line react-hooks/exhaustive-deps
966
+ [controlled, width, height, minScale, maxScale, anchor, fitPadding]
863
967
  );
864
968
  return /* @__PURE__ */ jsx(
865
969
  SceneCanvas,
866
970
  {
971
+ ref: canvasRef,
867
972
  scene,
868
973
  selection,
869
- features: ["view", "pick"],
870
- ambient: AMBIENT,
974
+ features: onMove ? ["view", "pick", "move"] : ["view", "pick"],
975
+ ambient,
871
976
  onClick,
872
977
  width,
873
978
  height,
874
979
  className,
875
- ...defaultView ? { defaultView } : {}
980
+ ...defaultView ? { defaultView } : {},
981
+ ...view ? { view } : {},
982
+ ...onViewChange ? { onViewChange } : {},
983
+ ...background ? { backgroundFill: background } : {}
876
984
  }
877
985
  );
878
986
  }
879
- function fitDiagram(specs, size, minScale, anchor) {
987
+ function fitDiagram(specs, size, fit = {}) {
880
988
  const boxes = specs.filter((s) => s.kind === "container").map((s) => s.pose);
881
989
  if (boxes.length === 0) return void 0;
882
990
  const x = Math.min(...boxes.map((b) => b.x));
@@ -888,12 +996,13 @@ function fitDiagram(specs, size, minScale, anchor) {
888
996
  height: Math.max(...boxes.map((b) => b.y + b.height)) - y
889
997
  };
890
998
  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 } : {}
999
+ maxScale: fit.maxScale ?? 1,
1000
+ ...fit.minScale !== void 0 ? { minScale: fit.minScale } : {},
1001
+ ...fit.anchor !== void 0 ? { anchor: fit.anchor } : {},
1002
+ ...fit.padding !== void 0 ? { padding: fit.padding } : {}
894
1003
  });
895
1004
  }
896
1005
 
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 };
1006
+ export { CONNECT_ACTION_ID, DEFAULT_EDGE_STROKE, DIAGRAM_EDGE, DIAGRAM_LABEL, DiagramView, EASED_LAYOUTS, EDGE_DERIVE_PATH, GRAB_PORT_ACTION_ID, LABEL_DERIVE_POSE, LAYOUTS2 as 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 };
898
1007
  //# sourceMappingURL=index.js.map
899
1008
  //# sourceMappingURL=index.js.map