@eventcatalog/core 5.0.0-beta.12 → 5.0.0-beta.14

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.
Files changed (90) hide show
  1. package/LICENSE +13 -12
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/catalog-runtime.cjs +1 -1
  7. package/dist/{chunk-TSY5CC27.js → chunk-4KE3EUDJ.js} +1 -1
  8. package/dist/{chunk-FY6MSP6J.js → chunk-I3XDOICA.js} +1 -1
  9. package/dist/{chunk-QB7X34OD.js → chunk-OFK7U7BX.js} +1 -1
  10. package/dist/{chunk-CX2VB7AI.js → chunk-PI5YNZZZ.js} +1 -1
  11. package/dist/{chunk-BWC4YKW3.js → chunk-WY6M3RPT.js} +1 -1
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/eventcatalog.cjs +11 -6
  15. package/dist/eventcatalog.js +14 -9
  16. package/dist/generate.cjs +1 -1
  17. package/dist/generate.js +3 -3
  18. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  19. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  20. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  21. package/dist/runtime-dependencies/manifest.json +3 -0
  22. package/dist/utils/cli-logger.cjs +1 -1
  23. package/dist/utils/cli-logger.js +2 -2
  24. package/eventcatalog/astro.config.mjs +13 -1
  25. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  26. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  27. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  28. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +11 -3
  29. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  30. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +37 -76
  31. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  32. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +122 -1
  33. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  34. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +3 -3
  35. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +3 -1
  36. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +7 -5
  37. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +133 -71
  38. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +66 -66
  39. package/eventcatalog/src/features/studio/agent-choreography.ts +5 -4
  40. package/eventcatalog/src/features/studio/agents.ts +28 -0
  41. package/eventcatalog/src/features/studio/api/canvases-api.ts +1 -1
  42. package/eventcatalog/src/features/studio/canvas-actions.ts +22 -14
  43. package/eventcatalog/src/features/studio/canvas-preview.ts +1 -2
  44. package/eventcatalog/src/features/studio/catalog.ts +1 -4
  45. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +246 -115
  46. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +7 -3
  47. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +272 -119
  48. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +1 -2
  49. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +1 -1
  50. package/eventcatalog/src/features/studio/components/JoinForm.tsx +101 -26
  51. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +102 -241
  52. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  53. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +1 -1
  54. package/eventcatalog/src/features/studio/components/Presence.tsx +6 -8
  55. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +277 -141
  56. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  57. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1005 -354
  58. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  59. package/eventcatalog/src/features/studio/from-visualiser.ts +1 -1
  60. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +19 -2
  61. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +9 -3
  62. package/eventcatalog/src/features/studio/hooks/use-comments.ts +2 -2
  63. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +162 -23
  64. package/eventcatalog/src/features/studio/identity.ts +23 -1
  65. package/eventcatalog/src/features/studio/pages/[id].astro +2 -1
  66. package/eventcatalog/src/features/studio/pages/index.astro +1 -2
  67. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +7 -22
  68. package/eventcatalog/src/features/studio/server/runtime.ts +4 -5
  69. package/package.json +3 -3
  70. package/eventcatalog/src/features/studio/canvas-doc.ts +0 -569
  71. package/eventcatalog/src/features/studio/clipboard.ts +0 -97
  72. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +0 -119
  73. package/eventcatalog/src/features/studio/components/Comments.tsx +0 -777
  74. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +0 -145
  75. package/eventcatalog/src/features/studio/components/DragGhost.tsx +0 -115
  76. package/eventcatalog/src/features/studio/components/Picture.tsx +0 -21
  77. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +0 -212
  78. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +0 -271
  79. package/eventcatalog/src/features/studio/components/icons.ts +0 -53
  80. package/eventcatalog/src/features/studio/components/status.ts +0 -53
  81. package/eventcatalog/src/features/studio/edges.ts +0 -56
  82. package/eventcatalog/src/features/studio/flow-state.ts +0 -121
  83. package/eventcatalog/src/features/studio/grouping.ts +0 -230
  84. package/eventcatalog/src/features/studio/hooks/use-counter-scale.ts +0 -20
  85. package/eventcatalog/src/features/studio/layout.ts +0 -56
  86. package/eventcatalog/src/features/studio/level-layout.ts +0 -145
  87. package/eventcatalog/src/features/studio/levels.ts +0 -337
  88. package/eventcatalog/src/features/studio/node-types.ts +0 -245
  89. package/eventcatalog/src/features/studio/placement.ts +0 -197
  90. package/eventcatalog/src/features/studio/studio.css +0 -273
@@ -1,230 +0,0 @@
1
- import type { Node, XYPosition } from '@xyflow/react';
2
- import { canGoInContainer, getNodeSize, isGroupType } from './node-types';
3
-
4
- /**
5
- * Containers: domains and systems shown as boxes that other nodes sit in.
6
- * A node inside one has its parentId, and its position is relative to the container.
7
- */
8
-
9
- /** Room around what's in a container, and for its header */
10
- export const GROUP_PADDING = { top: 80, right: 40, bottom: 40, left: 40 };
11
-
12
- type Rect = { x: number; y: number; width: number; height: number };
13
-
14
- const byId = (nodes: Node[]) => new Map(nodes.map((node) => [node.id, node]));
15
-
16
- /** A node's size: as rendered (in the browser), as set (containers, notes), or what its type renders at */
17
- export const sizeOf = (node: Node) => ({
18
- width: node.measured?.width ?? node.width ?? getNodeSize(node.type).width,
19
- height: node.measured?.height ?? node.height ?? getNodeSize(node.type).height,
20
- });
21
-
22
- /** Nodes are positioned relative to their container, so walk up the chain to get the canvas position */
23
- export const getAbsolutePosition = (node: Node, lookup: Map<string, Node>): XYPosition => {
24
- const parent = node.parentId ? lookup.get(node.parentId) : undefined;
25
- if (!parent) return node.position;
26
- const parentPosition = getAbsolutePosition(parent, lookup);
27
- return { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y };
28
- };
29
-
30
- export const getAbsoluteRect = (node: Node, lookup: Map<string, Node>): Rect => ({
31
- ...getAbsolutePosition(node, lookup),
32
- ...sizeOf(node),
33
- });
34
-
35
- const getDepth = (node: Node, lookup: Map<string, Node>): number => {
36
- const parent = node.parentId ? lookup.get(node.parentId) : undefined;
37
- return parent ? 1 + getDepth(parent, lookup) : 0;
38
- };
39
-
40
- const isDescendantOf = (node: Node, ancestorId: string, lookup: Map<string, Node>): boolean => {
41
- if (!node.parentId) return false;
42
- if (node.parentId === ancestorId) return true;
43
- const parent = lookup.get(node.parentId);
44
- return parent ? isDescendantOf(parent, ancestorId, lookup) : false;
45
- };
46
-
47
- const containsPoint = (rect: Rect, point: XYPosition) =>
48
- point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
49
-
50
- /** React Flow needs containers before what's in them in the nodes array */
51
- export const sortByHierarchy = (nodes: Node[]): Node[] => {
52
- const lookup = byId(nodes);
53
- return nodes
54
- .map((node, index) => ({ node, index, depth: getDepth(node, lookup) }))
55
- .sort((a, b) => a.depth - b.depth || a.index - b.index)
56
- .map(({ node }) => node);
57
- };
58
-
59
- /** The innermost container under the centre of a node (never itself, or one of its own children) */
60
- export const findDropTarget = (nodeId: string, nodes: Node[]): Node | undefined => {
61
- const lookup = byId(nodes);
62
- const node = lookup.get(nodeId);
63
- // Sticky notes go anywhere, never in a container (one that's in one is taken out when it's next moved)
64
- if (!node || !canGoInContainer(node.type)) return undefined;
65
- const rect = getAbsoluteRect(node, lookup);
66
- const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
67
- return nodes
68
- .filter((candidate) => isGroupType(candidate.type) && candidate.id !== nodeId && !isDescendantOf(candidate, nodeId, lookup))
69
- .filter((candidate) => containsPoint(getAbsoluteRect(candidate, lookup), center))
70
- .sort((a, b) => getDepth(b, lookup) - getDepth(a, lookup))[0];
71
- };
72
-
73
- /** The innermost container at a canvas point, for things dropped there */
74
- export const findGroupAtPoint = (point: XYPosition, nodes: Node[]): Node | undefined => {
75
- const lookup = byId(nodes);
76
- return nodes
77
- .filter((candidate) => isGroupType(candidate.type) && containsPoint(getAbsoluteRect(candidate, lookup), point))
78
- .sort((a, b) => getDepth(b, lookup) - getDepth(a, lookup))[0];
79
- };
80
-
81
- /** The container something added goes in: the one asked for (by id), else the one it's dropped on */
82
- export const containerFor = (point: XYPosition, nodes: Node[], inside?: string): Node | undefined =>
83
- inside ? nodes.find((node) => node.id === inside && isGroupType(node.type)) : findGroupAtPoint(point, nodes);
84
-
85
- /** A node moved into (or out of, without a parentId) a container, staying in the same place on the canvas */
86
- export const withParent = (node: Node, parentId: string | undefined, nodes: Node[]): Node => {
87
- const lookup = byId(nodes);
88
- const absolute = getAbsolutePosition(node, lookup);
89
- const parent = parentId ? lookup.get(parentId) : undefined;
90
- const parentPosition = parent ? getAbsolutePosition(parent, lookup) : { x: 0, y: 0 };
91
- const { parentId: _, extent: __, ...rest } = node;
92
- return {
93
- ...rest,
94
- ...(parent && { parentId: parent.id }),
95
- position: { x: absolute.x - parentPosition.x, y: absolute.y - parentPosition.y },
96
- };
97
- };
98
-
99
- /** A canvas point, relative to a container */
100
- export const toRelativePosition = (point: XYPosition, parentId: string, nodes: Node[]): XYPosition => {
101
- const lookup = byId(nodes);
102
- const parent = lookup.get(parentId);
103
- if (!parent) return point;
104
- const parentPosition = getAbsolutePosition(parent, lookup);
105
- return { x: point.x - parentPosition.x, y: point.y - parentPosition.y };
106
- };
107
-
108
- /** Nodes and everything inside them (deleted with their container) */
109
- export const withDescendants = (ids: string[], nodes: Node[]): Set<string> => {
110
- const lookup = byId(nodes);
111
- const result = new Set(ids);
112
- nodes.forEach((node) => {
113
- if (ids.some((id) => isDescendantOf(node, id, lookup))) result.add(node.id);
114
- });
115
- return result;
116
- };
117
-
118
- /**
119
- * The size a container needs to fit what's inside it, never smaller than it is, and how far its children
120
- * must shift when some sit above or left of its content area (so the header stays clear).
121
- */
122
- export const fitGroup = (groupId: string, nodes: Node[]) => {
123
- const lookup = byId(nodes);
124
- const group = lookup.get(groupId);
125
- const children = nodes.filter((node) => node.parentId === groupId);
126
- if (!group || children.length === 0) return undefined;
127
- const rects = children.map((child) => ({ ...child.position, ...sizeOf(child) }));
128
- const shift = {
129
- x: Math.max(0, GROUP_PADDING.left - Math.min(...rects.map((rect) => rect.x))),
130
- y: Math.max(0, GROUP_PADDING.top - Math.min(...rects.map((rect) => rect.y))),
131
- };
132
- const current = sizeOf(group);
133
- return {
134
- shift,
135
- width: Math.max(current.width, Math.max(...rects.map((rect) => rect.x + rect.width)) + shift.x + GROUP_PADDING.right),
136
- height: Math.max(current.height, Math.max(...rects.map((rect) => rect.y + rect.height)) + shift.y + GROUP_PADDING.bottom),
137
- };
138
- };
139
-
140
- /** How far a dragged node's middle can go past a container's edge and still be kept in it (growing it) */
141
- export const KEEP_IN_CONTAINER_MARGIN = 160;
142
-
143
- type Size = { width: number; height: number };
144
- /** A node's place and size as shown while something's dragged */
145
- export type NodePreview = { position: XYPosition; width?: number; height?: number };
146
-
147
- /**
148
- * Containers growing to fit what's being dragged in them, on every side, as a preview of where things will be.
149
- * `containerIds` are the containers around what's dragged, innermost first. `start` is the canvas when the drag
150
- * started, and `now` the canvas as shown (with the dragged nodes where they are now). A dragged node is kept in a
151
- * container while its middle is within `KEEP_IN_CONTAINER_MARGIN` of where the container was, and dropping it
152
- * further away takes it out, so the container goes back to its size. A container growing up or left moves, and
153
- * what's in it moves the other way, so nothing moves on the canvas.
154
- *
155
- * Returns, by id, where containers and the nodes in them that aren't dragged go (positions relative to their
156
- * containers), and their sizes for containers.
157
- */
158
- export const previewContainerGrowth = (
159
- containerIds: string[],
160
- draggedIds: ReadonlySet<string>,
161
- start: ReadonlyMap<string, Node>,
162
- now: ReadonlyMap<string, Node>
163
- ): Map<string, NodePreview> => {
164
- const previews = new Map<string, NodePreview>();
165
- // Where each container's top left was on the canvas when the drag started (moving a container grown up or
166
- // left is offset by what's in it moving the other way, so only its own shift changes it)
167
- const startOrigin = (id: string) => getAbsolutePosition(start.get(id)!, start as Map<string, Node>);
168
- const shifts = new Map<string, XYPosition>();
169
-
170
- for (const id of containerIds) {
171
- const container = start.get(id);
172
- if (!container) continue;
173
- const origin = startOrigin(id);
174
- const startSize = sizeOf(container);
175
- // Everything in it, in its coordinates when the drag started
176
- const items: { rect: Rect }[] = [];
177
- for (const node of start.values()) {
178
- if (node.parentId !== id) continue;
179
- if (draggedIds.has(node.id)) {
180
- const current = now.get(node.id);
181
- if (!current) continue;
182
- const at = getAbsoluteRect(current, now as Map<string, Node>);
183
- const middle = { x: at.x + at.width / 2, y: at.y + at.height / 2 };
184
- const kept =
185
- middle.x >= origin.x - KEEP_IN_CONTAINER_MARGIN &&
186
- middle.x <= origin.x + startSize.width + KEEP_IN_CONTAINER_MARGIN &&
187
- middle.y >= origin.y - KEEP_IN_CONTAINER_MARGIN &&
188
- middle.y <= origin.y + startSize.height + KEEP_IN_CONTAINER_MARGIN;
189
- if (kept) items.push({ rect: { ...at, x: at.x - origin.x, y: at.y - origin.y } });
190
- continue;
191
- }
192
- // A container inside it that's grown: where it's grown to
193
- const inner = previews.get(node.id);
194
- const innerShift = shifts.get(node.id) ?? { x: 0, y: 0 };
195
- items.push({
196
- rect: {
197
- x: node.position.x - innerShift.x,
198
- y: node.position.y - innerShift.y,
199
- ...(inner?.width && inner.height ? { width: inner.width, height: inner.height } : sizeOf(node)),
200
- },
201
- });
202
- }
203
-
204
- const shift = { x: 0, y: 0 };
205
- let width = startSize.width;
206
- let height = startSize.height;
207
- if (items.length > 0) {
208
- shift.x = Math.max(0, GROUP_PADDING.left - Math.min(...items.map(({ rect }) => rect.x)));
209
- shift.y = Math.max(0, GROUP_PADDING.top - Math.min(...items.map(({ rect }) => rect.y)));
210
- width =
211
- Math.max(startSize.width, Math.max(...items.map(({ rect }) => rect.x + rect.width)) + GROUP_PADDING.right) + shift.x;
212
- height =
213
- Math.max(startSize.height, Math.max(...items.map(({ rect }) => rect.y + rect.height)) + GROUP_PADDING.bottom) + shift.y;
214
- }
215
- shifts.set(id, shift);
216
- previews.set(id, {
217
- position: { x: container.position.x - shift.x, y: container.position.y - shift.y },
218
- width,
219
- height,
220
- });
221
- // What's in it moves the other way. Not what's dragged: React Flow places it against the container as it is.
222
- for (const node of start.values()) {
223
- if (node.parentId !== id || draggedIds.has(node.id)) continue;
224
- const own = previews.get(node.id);
225
- const base = own?.position ?? node.position;
226
- previews.set(node.id, { ...own, position: { x: base.x + shift.x, y: base.y + shift.y } });
227
- }
228
- }
229
- return previews;
230
- };
@@ -1,20 +0,0 @@
1
- import { useLayoutEffect, type RefObject } from 'react';
2
- import { useStoreApi } from '@xyflow/react';
3
-
4
- /** 1 / zoom, for things drawn in the canvas's coordinates that stay the same size on screen (`scale(var(...))`) */
5
- export const COUNTER_SCALE = '--studio-counter-scale';
6
-
7
- /**
8
- * Keeps COUNTER_SCALE on an element as the canvas zooms, outside React: what's counter-scaled with it (e.g. comment
9
- * pins) stays the same size on screen without re-rendering on every frame of a zoom
10
- */
11
- export function useCounterScale(ref: RefObject<HTMLElement | null>) {
12
- const store = useStoreApi();
13
- useLayoutEffect(() => {
14
- const apply = (zoom: number) => ref.current?.style.setProperty(COUNTER_SCALE, String(1 / zoom));
15
- apply(store.getState().transform[2]);
16
- return store.subscribe((state, previous) => {
17
- if (state.transform[2] !== previous.transform[2]) apply(state.transform[2]);
18
- });
19
- }, [store, ref]);
20
- }
@@ -1,56 +0,0 @@
1
- import type { Edge, Node, XYPosition } from '@xyflow/react';
2
- import { layoutWithElk } from '@eventcatalog/visualiser/layout';
3
- import { getEdgeRoute, type EdgeRoute } from './canvas-doc';
4
- import { isGroupType, isOnLevel } from './node-types';
5
-
6
- /**
7
- * Lays a canvas out with the same ELK layout as EventCatalog's visualiser (left to right, edges routed
8
- * around nodes, containers laid out with what's in them and sized to fit). Nodes are sized as rendered when
9
- * they have been (in the browser), or by their type.
10
- */
11
- export const layoutGraph = (nodes: Node[], edges: Edge[]) => layoutWithElk({ nodes, edges });
12
-
13
- /** The layout sizes containers with style.width/height; the canvas sizes them with width/height */
14
- export const withGroupSizes = (nodes: Node[]) =>
15
- nodes.map((node) => {
16
- const { width, height, ...style } = node.style ?? {};
17
- if (typeof width !== 'number' || typeof height !== 'number') return node;
18
- return { ...node, width, height, style };
19
- });
20
-
21
- export type LayoutResult = {
22
- /** Where each node goes (relative to its container, if it's in one), and containers' new sizes */
23
- nodes: Map<string, { position: XYPosition; size?: { width: number; height: number } }>;
24
- /** How each connection is drawn (around the nodes), by edge id */
25
- routes: Map<string, EdgeRoute>;
26
- };
27
-
28
- /**
29
- * The canvas laid out: new positions for its nodes, new sizes for containers, and routes for the connections
30
- * (drawn like the visualiser's: around the nodes, following them when they're moved, and as a plain step once
31
- * they've moved too far for it). Notes added on L1 or L2 aren't on this canvas, so they stay where they are.
32
- */
33
- export const getLayoutPositions = async (nodes: Node[], edges: Edge[]): Promise<LayoutResult> => {
34
- const onCanvas = nodes.filter((node) => isOnLevel(node, 3));
35
- if (onCanvas.length === 0) return { nodes: new Map(), routes: new Map() };
36
- const ids = new Set(onCanvas.map((node) => node.id));
37
- const between = edges.filter((edge) => ids.has(edge.source) && ids.has(edge.target));
38
- const laidOut = await layoutGraph(onCanvas, between);
39
- return {
40
- nodes: new Map(
41
- withGroupSizes(laidOut.nodes).map((node) => [
42
- node.id,
43
- {
44
- position: node.position,
45
- ...(isGroupType(node.type) && node.width && node.height && { size: { width: node.width, height: node.height } }),
46
- },
47
- ])
48
- ),
49
- routes: new Map(
50
- laidOut.edges.flatMap((edge) => {
51
- const route = getEdgeRoute(edge);
52
- return route ? [[edge.id, route] as const] : [];
53
- })
54
- ),
55
- };
56
- };
@@ -1,145 +0,0 @@
1
- import type { Edge, Node } from '@xyflow/react';
2
- import { getNodeSize as getLayoutSize } from '@eventcatalog/visualiser/layout';
3
- import type { LevelPlace } from './canvas-doc';
4
- import { fitGroup, getAbsolutePosition } from './grouping';
5
- import { getNodeSize, isGroupType } from './node-types';
6
- import { placeNodes } from './placement';
7
-
8
- /**
9
- * L1 and L2 as people arrange them. A level's content comes from the canvas (see levels.ts), but where things are on
10
- * it is its own: each level keeps a layout (a LevelPlace per node, in the canvas), so things stay where they're put.
11
- * Nothing on a level moves when something's added: new things are placed next to what they connect to (like agents
12
- * place what they add) and containers grow to fit.
13
- */
14
-
15
- type Graph = { nodes: Node[]; edges: Edge[] };
16
-
17
- /** A container's size: as it's being worked on (width and height), or as laid out (levels size containers with style) */
18
- const containerSize = (node: Node) => {
19
- if (node.width && node.height) return { width: node.width, height: node.height };
20
- const { width, height } = node.style ?? {};
21
- return typeof width === 'number' && typeof height === 'number' ? { width, height } : undefined;
22
- };
23
-
24
- /** The size the visualiser's layout gives a container with nothing in it (levels leave containers' size to it) */
25
- const emptyContainerSize = ({ type }: Pick<Node, 'type'>) => getLayoutSize({ id: '', type, position: { x: 0, y: 0 }, data: {} });
26
-
27
- /**
28
- * The size something new of a type is shown at on L1 or L2: L1 shows domains and systems with nothing in them as
29
- * cards, L2 as empty containers
30
- */
31
- export const getLevelNodeSize = (type: string | undefined, level: 1 | 2) => {
32
- if (!isGroupType(type)) return getNodeSize(type);
33
- return level === 1 ? getNodeSize() : emptyContainerSize({ type });
34
- };
35
-
36
- /** Where a node is on a level, and a container's size */
37
- const placeOf = (node: Node): LevelPlace => ({
38
- x: node.position.x,
39
- y: node.position.y,
40
- ...(isGroupType(node.type) && containerSize(node)),
41
- });
42
-
43
- /** A level's nodes where they are, by id (not its notes, which are kept on the canvas): what its layout keeps */
44
- export const levelPlaces = (nodes: Node[]) =>
45
- new Map(nodes.filter((node) => node.type !== 'note').map((node) => [node.id, placeOf(node)]));
46
-
47
- /**
48
- * A level arranged: each node where the level's layout has it, else where the diagram the canvas was opened from had
49
- * it (`fallback`), and anything with neither placed next to what it connects to. Containers grow to fit what's in
50
- * them. `placed` is where things the layout didn't have were put, and containers that grew (to keep, so they stay
51
- * there). Undefined when nothing on the level has a place yet: it's laid out from scratch then.
52
- */
53
- export const arrangeLevel = (
54
- graph: Graph,
55
- layout: ReadonlyMap<string, LevelPlace>,
56
- fallback: ReadonlyMap<string, LevelPlace> = new Map()
57
- ): { graph: Graph; placed: Map<string, LevelPlace> } | undefined => {
58
- const known = (id: string) => layout.get(id) ?? fallback.get(id);
59
- if (!graph.nodes.some((node) => known(node.id))) return undefined;
60
-
61
- const placed = new Map<string, LevelPlace>();
62
- // Containers carry their size as width and height while they're worked on (what placement and fitting read)
63
- let nodes: Node[] = graph.nodes.map((node) => {
64
- const place = known(node.id);
65
- const size = isGroupType(node.type)
66
- ? place?.width && place.height
67
- ? { width: place.width, height: place.height }
68
- : (containerSize(node) ?? emptyContainerSize(node))
69
- : undefined;
70
- return { ...node, position: place ? { x: place.x, y: place.y } : { x: 0, y: 0 }, ...size };
71
- });
72
-
73
- // Anything new, next to what it connects to (on the level), moving nothing that's there
74
- const toPlace = nodes.filter((node) => !known(node.id)).map((node) => node.id);
75
- if (toPlace.length) {
76
- const positions = placeNodes(nodes, toPlace, graph.edges);
77
- const lookup = new Map(nodes.map((node) => [node.id, node]));
78
- nodes = nodes.map((node) => {
79
- const at = positions.get(node.id);
80
- if (!at) return node;
81
- const parent = node.parentId ? lookup.get(node.parentId) : undefined;
82
- const origin = parent ? getAbsolutePosition(parent, lookup) : { x: 0, y: 0 };
83
- const moved = { ...node, position: { x: at.x - origin.x, y: at.y - origin.y } };
84
- placed.set(node.id, placeOf(moved));
85
- return moved;
86
- });
87
- }
88
-
89
- // Containers grow to fit what's in them (the innermost first), moving nothing on the level
90
- const byId = new Map(nodes.map((node) => [node.id, node]));
91
- const depth = (node: Node): number => {
92
- const parent = node.parentId ? byId.get(node.parentId) : undefined;
93
- return parent ? 1 + depth(parent) : 0;
94
- };
95
- const containers = nodes.filter((node) => isGroupType(node.type)).sort((a, b) => depth(b) - depth(a));
96
- for (const { id } of containers) {
97
- const fit = fitGroup(id, nodes);
98
- const current = nodes.find((node) => node.id === id)!;
99
- const { x: dx, y: dy } = fit?.shift ?? { x: 0, y: 0 };
100
- if (!fit || (!dx && !dy && fit.width === current.width && fit.height === current.height)) continue;
101
- nodes = nodes.map((node) => {
102
- if (node.parentId === id && (dx || dy)) {
103
- const shifted = { ...node, position: { x: node.position.x + dx, y: node.position.y + dy } };
104
- placed.set(node.id, placeOf(shifted));
105
- return shifted;
106
- }
107
- if (node.id !== id) return node;
108
- const grown = {
109
- ...node,
110
- position: { x: node.position.x - dx, y: node.position.y - dy },
111
- width: fit.width,
112
- height: fit.height,
113
- };
114
- placed.set(id, placeOf(grown));
115
- return grown;
116
- });
117
- }
118
-
119
- return {
120
- graph: {
121
- edges: graph.edges,
122
- // Containers drawn at their size with style, like levels laid out from scratch
123
- nodes: nodes.map(({ width, height, ...node }) =>
124
- isGroupType(node.type) && width && height ? { ...node, style: { ...node.style, width, height } } : node
125
- ),
126
- },
127
- placed,
128
- };
129
- };
130
-
131
- /**
132
- * A level's edges drawn along the routes the diagram the canvas was opened from had for the same connections (between
133
- * the same two nodes), once the level's worked out from the canvas: routes follow their nodes when they move, so
134
- * connections the diagram had stay drawn around what's in their way
135
- */
136
- export const withDiagramRoutes = (edges: Edge[], diagramEdges: Edge[] = []) => {
137
- const routes = new Map(
138
- diagramEdges.flatMap((edge) => (edge.data?.route ? [[`${edge.source}>${edge.target}`, edge.data.route] as const] : []))
139
- );
140
- if (routes.size === 0) return edges;
141
- return edges.map((edge) => {
142
- const route = routes.get(`${edge.source}>${edge.target}`);
143
- return route && !edge.data?.route ? { ...edge, data: { ...edge.data, route } } : edge;
144
- });
145
- };