@weasel-js/diagram 1.9.2 → 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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as Port, a as PortsOptions, D as DiagramNodeLike, b as DiagramNodeReader, R as Row, O as Outline, c as PortSpec, B as BodySpec, d as DiagramNode, L as LayoutFn, e as LayoutOptions, M as MeasureRowText, G as GraphSource, F as ForceOptions, f as LayoutResult, g as Graph } from './layout.entry-W-8PYfGM.js';
2
- export { h as BodyFloor, i as BodyNodeSpec, j as Bounds, k as BuildBodyOptions, l as BuildGraphOptions, C as COMPASS, m as DEFAULT_NODE_GAP, n as DEFAULT_PORTS, o as DEFAULT_RANK_GAP, p as DIAGRAM_TRAIT_KEY, q as DiagramNodeEntry, r as ForceBody, s as ForceRelaxation, t as GraphEdge, u as GraphNode, v as GraphNodeLike, w as LayoutAxes, x as LayoutDirection, y as PortAnchor, z as RowBox, A as RowNodeData, E as RowPort, H as RowPortBox, I as RowTextStyle, S as Slot, J as axesFor, K as backEdges, N as bodyOutline, Q as bodyTrait, T as boxForContent, U as buildBody, V as buildGraph, W as canvasMeasure, X as contentBox, Y as createDiagramNodes, Z as dataKeyReader, _ as diagramNodeOf, $ as force, a0 as forceRelaxation, a1 as forestOf, a2 as graphOrder, a3 as isPinned, a4 as layered, a5 as layoutBody, a6 as layoutRowPorts, a7 as measureBody, a8 as outlinePath, a9 as outlinePolyline, aa as packAcross, ab as pinnedSet, ac as portOf, ad as portsOf, ae as portsOnOutline, af as ranksOf, ag as rayHit, ah as seededOrder, ai as settle, aj as sizeToBody, ak as translated, al as tree } from './layout.entry-W-8PYfGM.js';
1
+ import { P as Port, a as PortsOptions, D as DiagramNodeLike, b as DiagramNodeReader, c as PortAnchor, d as DiagramGroup, R as Row, O as Outline, e as PortSpec, B as BodySpec, f as DiagramNode, L as LayoutFn, g as LayoutOptions, M as MeasureRowText, G as GraphSource, F as ForceOptions, h as LayoutResult, i as Graph } from './layout.entry-KHbx5uGC.js';
2
+ export { j as BodyFloor, k as BodyNodeSpec, l as Bounds, m as BuildBodyOptions, n as BuildGraphOptions, C as COMPASS, o as DEFAULT_GROUP_INSET, p as DEFAULT_NODE_GAP, q as DEFAULT_PORTS, r as DEFAULT_RANK_GAP, s as DIAGRAM_TRAIT_KEY, t as DiagramNodeEntry, u as ForceBody, v as ForceRelaxation, w as GraphEdge, x as GraphGroup, y as GraphNode, z as GraphNodeLike, A as GroupInset, E as LayoutAxes, H as LayoutDirection, I as RowBox, J as RowNodeData, K as RowPort, N as RowPortBox, Q as RowTextStyle, S as Slot, T as axesFor, U as backEdges, V as bodyOutline, W as bodyTrait, X as boxForContent, Y as buildBody, Z as buildGraph, _ as canvasMeasure, $ as contentBox, a0 as createDiagramNodes, a1 as dataKeyReader, a2 as diagramNodeOf, a3 as expandGroupEdges, a4 as force, a5 as forceRelaxation, a6 as forestOf, a7 as graphOrder, a8 as insetOf, a9 as isPinned, aa as layered, ab as layoutBody, ac as layoutRowPorts, ad as measureBody, ae as outlinePath, af as outlinePolyline, ag as packAcross, ah as packClusters, ai as pinnedSet, aj as portOf, ak as portsOf, al as portsOnOutline, am as rankGaps, an as ranksOf, ao as rayHit, ap as seededOrder, aq as settle, ar as sizeToBody, as as translated, at as tree } from './layout.entry-KHbx5uGC.js';
3
3
  import { Vec2, DerivedDep, Path, SceneRegistry, PoseDescriptor, ChromeState, FillStyle, Stroke, CursorSpec, Affordance, InvocationCtx, GestureBinding, Action, Contribution, RenderLayer, MarkerRef, AddNodeSpec, RectPose, Scene, EasingFn, NodeId, NodeShapeEntry, View, SelectionApi } from '@weasel-js/core';
4
4
  import { Vec2 as Vec2$1 } from '@weasel-js/core/math';
5
5
  import * as react from 'react';
@@ -417,6 +417,64 @@ declare const LABEL_DERIVE_POSE: (node: {
417
417
  data: unknown;
418
418
  }, deps: readonly (DerivedDep<unknown, unknown, string> | undefined)[]) => unknown;
419
419
 
420
+ /**
421
+ * A group's box, and anything pinned to a point on another node.
422
+ *
423
+ * The box is an ordinary node that depends on its members and derives its pose
424
+ * from theirs: their union, grown by the inset. Drag a member and the box
425
+ * follows; lay the diagram out and the box lands around wherever the members
426
+ * went. Nothing has to keep it in sync, because nothing stores it.
427
+ *
428
+ * An anchored node — a group's title is the first — derives its pose from one
429
+ * dependency's bounds the same way, so it travels with the box it labels.
430
+ */
431
+
432
+ /** Where an anchored node sits on its dependency. */
433
+ interface DiagramAnchor {
434
+ /** The point on the dependency's bounds, normalized as a port's is. */
435
+ at: PortAnchor;
436
+ /** The point on the node's own bounds that lands there. Default `at`, so
437
+ * `{ u: 0, v: 0 }` puts the node's top-left on the dependency's. */
438
+ self?: PortAnchor;
439
+ /** Then moved by this much, in world units. */
440
+ offset?: Vec2$1;
441
+ }
442
+ /** The group trait on a node's data, or `null`. */
443
+ declare function diagramGroupOf(node: {
444
+ data: unknown;
445
+ }): DiagramGroup | null;
446
+ /** The anchor trait on a node's data, or `null`. */
447
+ declare function diagramAnchorOf(node: {
448
+ data: unknown;
449
+ }): DiagramAnchor | null;
450
+ /** Options for {@link groupDerivePose} and {@link anchorDerivePose}.
451
+ * `geometry` defaults to the kit's `AUTO_POSE_DESCRIPTOR`. */
452
+ interface GroupPoseOptions<TPose> {
453
+ geometry?: PoseDescriptor<TPose>;
454
+ }
455
+ type DerivePose<TPose> = (node: {
456
+ pose: TPose;
457
+ data: unknown;
458
+ }, deps: readonly (DerivedDep<TPose> | undefined)[]) => TPose | null;
459
+ /**
460
+ * A `derivePose` that draws a group's box around its members: their union,
461
+ * grown by the trait's inset. `null` — the authored pose stands — when the node
462
+ * carries no group trait or every member is gone.
463
+ */
464
+ declare function groupDerivePose<TPose>(opts?: GroupPoseOptions<TPose>): DerivePose<TPose>;
465
+ /**
466
+ * A `derivePose` that keeps a node at a point on its dependency's bounds.
467
+ * `null` when the node carries no anchor trait or the dependency is gone.
468
+ */
469
+ declare function anchorDerivePose<TPose>(opts?: GroupPoseOptions<TPose>): DerivePose<TPose>;
470
+ /** Registry key for {@link GROUP_DERIVE_POSE}. */
471
+ declare const DIAGRAM_GROUP = "diagram:group";
472
+ /** Registry key for {@link ANCHOR_DERIVE_POSE}. */
473
+ declare const DIAGRAM_ANCHOR = "diagram:anchor";
474
+ /** The default derivations, as stable references so `toJSON` can find their keys. */
475
+ declare const GROUP_DERIVE_POSE: DerivePose<unknown>;
476
+ declare const ANCHOR_DERIVE_POSE: DerivePose<unknown>;
477
+
420
478
  /**
421
479
  * `diagramScene` — a laid-out diagram from plain `{ nodes, edges }`.
422
480
  *
@@ -452,6 +510,7 @@ interface DataNode {
452
510
  verticalAlign?: BodySpec['verticalAlign'];
453
511
  }
454
512
  interface DataEdge {
513
+ /** A node's id, or a group's: an edge naming a group meets its box. */
455
514
  from: string;
456
515
  to: string;
457
516
  /** The port to leave from or arrive at. Unset, the end facing the other
@@ -466,9 +525,26 @@ interface DataEdge {
466
525
  /** World points the route passes through. Layout does not move them. */
467
526
  waypoints?: readonly Vec2$1[];
468
527
  }
528
+ /** A box drawn around some nodes, which layout keeps together. */
529
+ interface DataGroup {
530
+ /** Shares one namespace with node ids. */
531
+ id: string;
532
+ /** Node ids. A node a later group also names stays in this one, and an id
533
+ * that is not a node is ignored. */
534
+ members: readonly string[];
535
+ /** Drawn inside the box, above its members. */
536
+ label?: string;
537
+ /** Between the box and its members, and around the label. Default
538
+ * `DiagramSceneOptions.groupPadding`. */
539
+ padding?: number;
540
+ }
469
541
  interface DiagramData {
470
542
  nodes: readonly DataNode[];
471
543
  edges: readonly DataEdge[];
544
+ /** Flat: a group is never a member of another. `layered` and `tree` keep
545
+ * each group's members together; `force` and `'none'` ignore groups but
546
+ * still draw them. */
547
+ groups?: readonly DataGroup[];
472
548
  }
473
549
  interface NodeStyle {
474
550
  fill: string;
@@ -497,6 +573,8 @@ type TextStyle = {
497
573
  interface DiagramSceneData {
498
574
  diagram?: DiagramNode | DiagramEdge | {
499
575
  label: DiagramLabel;
576
+ } | {
577
+ anchor: DiagramAnchor;
500
578
  };
501
579
  fill?: {
502
580
  color: string;
@@ -535,12 +613,17 @@ interface DiagramSceneOptions {
535
613
  /** Where edge labels sit. Default `{ at: 'mid', offset: 9 }`. */
536
614
  labelPlacement?: DiagramLabel;
537
615
  nodeStyle?: (node: DataNode) => NodeStyle;
616
+ /** Default 12. */
617
+ groupPadding?: number;
618
+ /** A group's box and label. `text` colors the label. */
619
+ groupStyle?: (group: DataGroup) => NodeStyle;
538
620
  edgeStyle?: (edge: DataEdge) => EdgeStyle;
539
621
  }
540
622
  declare const edgeIdOf: (e: DataEdge) => string;
541
- /** Every spec carries an id — a node's own, `<id>/<k>` for its text, and
542
- * `<edge id>/label` — so a later call's specs can be reconciled against this
543
- * one's. Throws on a repeated node id, which names two nodes as one. An edge whose
623
+ /** Every spec carries an id — a node's own, `<id>/<k>` for its text,
624
+ * `<edge id>/label`, a group's own and `<group id>/label` — so a later call's
625
+ * specs can be reconciled against this one's. Throws on a repeated id among
626
+ * nodes and groups, which names two things as one. An edge whose
544
627
  * end is not a node is dropped, and so is a repeat of an earlier edge with
545
628
  * the same ends, ports and label: it would draw exactly on top of the first. */
546
629
  declare function diagramScene(data: DiagramData, opts?: DiagramSceneOptions): DiagramSpec[];
@@ -845,6 +928,11 @@ interface DiagramViewProps {
845
928
  /** Set to make ports grabbable. Dragging one port onto another reports the
846
929
  * edge it describes and draws nothing; add it to the data to keep it. */
847
930
  onConnect?: (edge: DataEdge) => void;
931
+ /** Whether the canvas's keyboard shortcuts — held Space to pan among them —
932
+ * run. They listen page-wide, so a view-only diagram beside other UI would
933
+ * claim keys meant for the page. Default: on when `onMove` or `onConnect`
934
+ * is set. */
935
+ enableKeybindings?: boolean;
848
936
  /** Which ports may be joined. Default: a port may not join itself, and two
849
937
  * typed ports must share a type. */
850
938
  canConnect?: CanConnect;
@@ -896,4 +984,4 @@ declare const DiagramView: react.ForwardRefExoticComponent<DiagramViewProps & re
896
984
  * `<SceneCanvas onClick>`. */
897
985
  declare function useMirroredSelection(selection: Pick<SelectionApi, 'get' | 'set'>, selected: string | null | undefined, onSelect: ((id: string | null) => void) | undefined): () => void;
898
986
 
899
- export { type ApplyLayoutOptions, BodySpec, CONNECT_ACTION_ID, type CanConnect, type ConnectActionOptions, DEFAULT_EDGE_STROKE, DIAGRAM_EDGE, DIAGRAM_LABEL, type DataEdge, type DataNode, type DiagramContributionOptions, type DiagramData, type DiagramEdge, type DiagramLabel, DiagramNode, DiagramNodeLike, DiagramNodeReader, type DiagramSceneData, type DiagramSceneOptions, type DiagramShapeOptions, type DiagramSpec, DiagramView, type DiagramViewApi, type DiagramViewProps, EASED_LAYOUTS, EDGE_DERIVE_PATH, type EasedProducerOptions, type EdgeEnd, type EdgeRouteOptions, type EdgeStyle, ForceOptions, type ForceProducerOptions, GRAB_PORT_ACTION_ID, Graph, GraphSource, LABEL_DERIVE_POSE, LAYOUTS, LAYOUT_ACTION_ID, type LabelPoseOptions, type LayoutActionOptions, LayoutFn, LayoutOptions, LayoutResult, type LiveLayout, type LiveLayoutCtx, type LiveLayoutFrame, type LiveLayoutProducer, MeasureRowText, type NodeMove, type NodeStyle, Outline, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, type ParticipantPose, type ParticipantScene, type ParticipantSource, type PendingEdge, Port, type PortAffordanceOptions, type PortScratch, PortSpec, PortsOptions, ROUTERS, type RouteRequest, type Router$1 as Router, Row, type UseLiveLayoutOptions, applyLayout, bezier, 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 };
987
+ export { ANCHOR_DERIVE_POSE, type ApplyLayoutOptions, BodySpec, CONNECT_ACTION_ID, type CanConnect, type ConnectActionOptions, DEFAULT_EDGE_STROKE, DIAGRAM_ANCHOR, DIAGRAM_EDGE, DIAGRAM_GROUP, DIAGRAM_LABEL, type DataEdge, type DataGroup, type DataNode, type DiagramAnchor, type DiagramContributionOptions, type DiagramData, type DiagramEdge, DiagramGroup, type DiagramLabel, DiagramNode, DiagramNodeLike, DiagramNodeReader, type DiagramSceneData, type DiagramSceneOptions, type DiagramShapeOptions, type DiagramSpec, DiagramView, type DiagramViewApi, type DiagramViewProps, EASED_LAYOUTS, EDGE_DERIVE_PATH, type EasedProducerOptions, type EdgeEnd, type EdgeRouteOptions, type EdgeStyle, ForceOptions, type ForceProducerOptions, GRAB_PORT_ACTION_ID, GROUP_DERIVE_POSE, Graph, GraphSource, type GroupPoseOptions, LABEL_DERIVE_POSE, LAYOUTS, LAYOUT_ACTION_ID, type LabelPoseOptions, type LayoutActionOptions, LayoutFn, LayoutOptions, LayoutResult, type LiveLayout, type LiveLayoutCtx, type LiveLayoutFrame, type LiveLayoutProducer, MeasureRowText, type NodeMove, type NodeStyle, Outline, PORT_AFFORDANCE_KIND, PORT_LAYER_ID, type ParticipantPose, type ParticipantScene, type ParticipantSource, type PendingEdge, Port, type PortAffordanceOptions, PortAnchor, type PortScratch, PortSpec, PortsOptions, ROUTERS, type RouteRequest, type Router$1 as Router, Row, type UseLiveLayoutOptions, anchorDerivePose, applyLayout, bezier, 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 };
package/dist/index.js CHANGED
@@ -1,11 +1,73 @@
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';
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
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,17 +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" };
600
+ var GROUP = { fill: "rgba(123, 167, 199, 0.08)", stroke: "#7ba7c7", text: "#9fbdd6", strokeWidth: 1, dash: [4, 3] };
450
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
605
  var LAYOUTS = { layered, tree, force };
455
606
  var edgeIdOf = (e) => `edge:${e.from}${e.fromPort ? `.${e.fromPort}` : ""}->${e.to}${e.toPort ? `.${e.toPort}` : ""}${e.label ? `:${e.label}` : ""}`;
456
- function graphOf(nodes, edges) {
607
+ function graphOf(nodes, edges, groups = []) {
457
608
  const byId = new Map(nodes.map((n) => [n.id, n]));
458
609
  const out = /* @__PURE__ */ new Map();
459
610
  const inc = /* @__PURE__ */ new Map();
@@ -464,6 +615,7 @@ function graphOf(nodes, edges) {
464
615
  return {
465
616
  nodes,
466
617
  edges,
618
+ groups,
467
619
  node: (id) => byId.get(id),
468
620
  outgoing: (id) => out.get(id) ?? [],
469
621
  incoming: (id) => inc.get(id) ?? []
@@ -485,6 +637,12 @@ function diagramScene(data, opts = {}) {
485
637
  if (ids.has(n.id)) throw new Error(`diagramScene: node id "${n.id}" appears more than once`);
486
638
  ids.add(n.id);
487
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
+ });
488
646
  const seen = /* @__PURE__ */ new Set();
489
647
  const edges = data.edges.filter((e) => {
490
648
  const id = edgeIdOf(e);
@@ -507,13 +665,29 @@ function diagramScene(data, opts = {}) {
507
665
  );
508
666
  return { id: n.id, bounds: pose, pinned: n.pinned === true };
509
667
  });
510
- const graph = graphOf(seeds, edges.map((e) => ({ id: edgeIdOf(e), from: e.from, to: e.to })));
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
+ );
511
676
  const layout = typeof opts.layout === "function" ? opts.layout : opts.layout === "none" ? null : LAYOUTS[opts.layout ?? "layered"];
512
677
  const moved = layout === null ? /* @__PURE__ */ new Map() : layout(graph, { order: "barycenter", ...opts.layoutOptions });
513
678
  const specs = [];
514
- for (const [i, n] of data.nodes.entries()) {
679
+ const placedAt = new Map(data.nodes.map((n, i) => {
515
680
  const seed = seeds[i].bounds;
516
681
  const at = moved.get(n.id) ?? seed;
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);
517
691
  const style = opts.nodeStyle?.(n) ?? NODE;
518
692
  const textData = (text, s) => ({ text, style: styleOf(s), fill: { color: style.text } });
519
693
  const built = buildBody(
@@ -874,6 +1048,7 @@ function Inner({
874
1048
  onSelect,
875
1049
  onMove,
876
1050
  onConnect,
1051
+ enableKeybindings,
877
1052
  canConnect,
878
1053
  portOptions,
879
1054
  live: liveOpts,
@@ -972,6 +1147,7 @@ function Inner({
972
1147
  scene,
973
1148
  selection,
974
1149
  features: onMove ? ["view", "pick", "move"] : ["view", "pick"],
1150
+ enableKeybindings: enableKeybindings ?? (onMove !== void 0 || onConnect !== void 0),
975
1151
  ambient,
976
1152
  onClick,
977
1153
  width,
@@ -985,7 +1161,7 @@ function Inner({
985
1161
  );
986
1162
  }
987
1163
  function fitDiagram(specs, size, fit = {}) {
988
- const boxes = specs.filter((s) => s.kind === "container").map((s) => s.pose);
1164
+ const boxes = specs.filter((s) => s.kind === "container" || diagramGroupOf(s) !== null).map((s) => s.pose);
989
1165
  if (boxes.length === 0) return void 0;
990
1166
  const x = Math.min(...boxes.map((b) => b.x));
991
1167
  const y = Math.min(...boxes.map((b) => b.y));
@@ -1003,6 +1179,6 @@ function fitDiagram(specs, size, fit = {}) {
1003
1179
  });
1004
1180
  }
1005
1181
 
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 };
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 };
1007
1183
  //# sourceMappingURL=index.js.map
1008
1184
  //# sourceMappingURL=index.js.map