@likec4/diagram 1.17.0 → 1.18.0

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 (53) hide show
  1. package/package.json +24 -18
  2. package/src/LikeC4Diagram.props.ts +27 -12
  3. package/src/LikeC4Diagram.tsx +14 -9
  4. package/src/LikeC4Search.tsx +149 -0
  5. package/src/StaticLikeC4Diagram.tsx +1 -0
  6. package/src/hooks/useDiagramState.ts +8 -3
  7. package/src/hooks/useXYFlow.ts +2 -1
  8. package/src/likec4model/useLikeC4ElementsTree.ts +6 -13
  9. package/src/likec4model/useLikeC4Model.ts +21 -11
  10. package/src/overlays/Overlays.tsx +96 -63
  11. package/src/overlays/edge-details/EdgeDetailsXYFlow.tsx +3 -1
  12. package/src/overlays/edge-details/_types.ts +7 -3
  13. package/src/overlays/edge-details/use-layouted-edge-details.ts +21 -14
  14. package/src/overlays/edge-details/xyflow/ElementNode.tsx +4 -2
  15. package/src/overlays/edge-details/xyflow/RelationshipEdge.tsx +4 -4
  16. package/src/overlays/element-details/ElementDetailsCard.tsx +94 -81
  17. package/src/overlays/element-details/TabPanelDeployments.css.ts +69 -0
  18. package/src/overlays/element-details/TabPanelDeployments.tsx +217 -0
  19. package/src/overlays/element-details/TabPanelRelationships.tsx +8 -9
  20. package/src/overlays/element-details/TabPanelStructure.tsx +10 -10
  21. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +1 -1
  22. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +3 -11
  23. package/src/overlays/relationships-of/SelectElement.tsx +2 -2
  24. package/src/overlays/relationships-of/_types.ts +2 -2
  25. package/src/overlays/relationships-of/use-layouted-relationships.ts +18 -19
  26. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +1 -3
  27. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +12 -6
  28. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +0 -1
  29. package/src/state/aligners.ts +473 -0
  30. package/src/state/diagramStore.layout.ts +67 -0
  31. package/src/state/diagramStore.ts +31 -77
  32. package/src/ui/DiagramTitlePanel.tsx +2 -29
  33. package/src/ui/Link.tsx +40 -0
  34. package/src/ui/notation/NotationPanel.tsx +4 -2
  35. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +2 -2
  36. package/src/ui/top-left/ManualLayoutToolsButton.tsx +15 -0
  37. package/src/ui/top-left/TopLeftPanel.tsx +16 -3
  38. package/src/xyflow/edges/EdgeLabel.tsx +4 -4
  39. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +20 -7
  40. package/src/xyflow/nodes/compound/CompoundNode.css.ts +28 -13
  41. package/src/xyflow/nodes/compound/CompoundNode.tsx +51 -19
  42. package/src/xyflow/nodes/element/ElementNode.tsx +153 -92
  43. package/src/xyflow/nodes/element/element.css.ts +53 -11
  44. package/src/xyflow/nodes/shared/ElementIcon.tsx +1 -1
  45. package/src/xyflow/nodes/shared/Toolbar.tsx +24 -8
  46. package/src/xyflow/useLayoutConstraints.ts +1 -1
  47. package/src/xyflow/useXYFlowEvents.ts +18 -5
  48. package/src/ui/OptionsPanel.css.ts +0 -9
  49. package/src/ui/OptionsPanel.tsx +0 -70
  50. package/src/ui/options/EdgeOptions.tsx +0 -142
  51. package/src/ui/options/NodeOptions.tsx +0 -362
  52. package/src/xyflow/nodes/element/ElementDetails.css.ts +0 -101
  53. package/src/xyflow/nodes/element/ElementDetails.tsx +0 -393
@@ -0,0 +1,473 @@
1
+ import { invariant, type NonEmptyArray } from '@likec4/core'
2
+ import type { Rect, XYPosition } from '@xyflow/react'
3
+ import { map, pick, pipe, reduce, sortBy } from 'remeda'
4
+
5
+ export type GridAlignmentMode = 'Column' | 'Row'
6
+ export type LinearAlignmentMode = 'Left' | 'Center' | 'Right' | 'Top' | 'Middle' | 'Bottom'
7
+
8
+ export abstract class Aligner {
9
+ abstract computeLayout(nodes: NodeRect[]): void
10
+ abstract applyPosition(node: NodeRect): Partial<XYPosition>
11
+ }
12
+
13
+ export class LinearAligner extends Aligner {
14
+ private alignTo: number | undefined
15
+ constructor(
16
+ private getEdgePosition: (nodes: NodeRect[]) => number,
17
+ private computePosition: (alignTo: number, node: NodeRect) => number,
18
+ private propertyToEdit: keyof XYPosition
19
+ ) {
20
+ super()
21
+ }
22
+
23
+ override computeLayout(nodes: NodeRect[]) {
24
+ this.alignTo = this.getEdgePosition(nodes)
25
+ }
26
+
27
+ override applyPosition(node: NodeRect): Partial<XYPosition> {
28
+ return {
29
+ [this.propertyToEdit]: this.computePosition(this.alignTo!, node)
30
+ }
31
+ }
32
+ }
33
+
34
+ export type NodeRect = Rect & { id: string }
35
+ type Layer = {
36
+ primaryAxisSize: number
37
+ nodes: NodeRect[]
38
+ occupiedSpace: number
39
+ layout: Layout | null
40
+ }
41
+ type Layout = {
42
+ nodePositions: Map<string, XYPosition>
43
+ refLayer: Layer | null
44
+ }
45
+
46
+ export class GridAligner extends Aligner {
47
+ private layout: Map<string, XYPosition> = new Map()
48
+ private axisPreset: {
49
+ primaryAxisDimension: 'height' | 'width'
50
+ secondaryAxisDimension: 'height' | 'width'
51
+ primaryAxisCoord: 'x' | 'y'
52
+ secondaryAxisCoord: 'x' | 'y'
53
+ }
54
+
55
+ private get primaryAxisCoord() {
56
+ return this.axisPreset.primaryAxisCoord
57
+ }
58
+ private get secondaryAxisCoord() {
59
+ return this.axisPreset.secondaryAxisCoord
60
+ }
61
+ private get primaryAxisDimension() {
62
+ return this.axisPreset.primaryAxisDimension
63
+ }
64
+ private get secondaryAxisDimension() {
65
+ return this.axisPreset.secondaryAxisDimension
66
+ }
67
+
68
+ constructor(alignmentMode: GridAlignmentMode) {
69
+ super()
70
+
71
+ this.axisPreset = alignmentMode === 'Column'
72
+ ? {
73
+ primaryAxisDimension: 'width',
74
+ secondaryAxisDimension: 'height',
75
+ primaryAxisCoord: 'x',
76
+ secondaryAxisCoord: 'y'
77
+ }
78
+ : {
79
+ primaryAxisDimension: 'height',
80
+ secondaryAxisDimension: 'width',
81
+ primaryAxisCoord: 'y',
82
+ secondaryAxisCoord: 'x'
83
+ }
84
+ }
85
+
86
+ override applyPosition(node: NodeRect): Partial<XYPosition> {
87
+ return this.layout?.get(node.id) ?? {}
88
+ }
89
+
90
+ override computeLayout(nodes: NodeRect[]) {
91
+ // Sort by primary axis
92
+ const sortedNodeRects = pipe(
93
+ nodes,
94
+ sortBy(r => r[this.primaryAxisCoord])
95
+ )
96
+
97
+ const layoutRect = this.getLayoutRect(sortedNodeRects)
98
+
99
+ const layers = this.getLayers(sortedNodeRects)
100
+
101
+ this.layout = this.buildLayout(layers, layoutRect, sortedNodeRects)
102
+ }
103
+
104
+ private getLayoutRect(nodeRects: NodeRect[]): Rect {
105
+ const x = Math.min(...nodeRects.map(n => n.x))
106
+ const y = Math.min(...nodeRects.map(n => n.y))
107
+ const right = Math.max(...nodeRects.map(n => n.x + n.width))
108
+ const bottom = Math.max(...nodeRects.map(n => n.y + n.height))
109
+
110
+ return {
111
+ x,
112
+ y,
113
+ width: right - x,
114
+ height: bottom - y
115
+ }
116
+ }
117
+
118
+ private getLayers(sortedNodeRects: NodeRect[]): NonEmptyArray<Layer> {
119
+ const layers: Layer[] = []
120
+ let layerEnd = 0
121
+ let layer = null
122
+
123
+ for (let node of sortedNodeRects) {
124
+ if (!!layer && node[this.primaryAxisCoord] < layerEnd) {
125
+ layer.nodes.push(node)
126
+ layer.primaryAxisSize = Math.max(layer.primaryAxisSize, node[this.primaryAxisDimension])
127
+ layer.occupiedSpace += node[this.secondaryAxisDimension]
128
+ layerEnd = Math.max(
129
+ node[this.primaryAxisCoord] + node[this.primaryAxisDimension],
130
+ layerEnd
131
+ )
132
+ } else {
133
+ layer = {
134
+ primaryAxisSize: node[this.primaryAxisDimension],
135
+ nodes: [node],
136
+ occupiedSpace: node[this.secondaryAxisDimension],
137
+ layout: null
138
+ }
139
+ layers.push(layer)
140
+ layerEnd = node[this.primaryAxisCoord] + node[this.primaryAxisDimension]
141
+ continue
142
+ }
143
+ }
144
+
145
+ layers.forEach(l =>
146
+ l.nodes.sort((a, b) => a[this.secondaryAxisCoord] - b[this.secondaryAxisCoord])
147
+ )
148
+
149
+ return layers as NonEmptyArray<Layer>
150
+ }
151
+
152
+ private buildLayout(layers: NonEmptyArray<Layer>, layoutRect: Rect, nodeRects: NodeRect[]): Map<string, XYPosition> {
153
+ const nodeMap = new Map(nodeRects.map(n => [n.id, n]))
154
+ const layout: [string, XYPosition][] = []
155
+ const occupiedSpace = layers.reduce((a, b) => a + b.primaryAxisSize, 0)
156
+ const rowMargin = layers.length > 1
157
+ ? (layoutRect[this.primaryAxisDimension] - occupiedSpace) / (layers.length - 1)
158
+ : 0
159
+
160
+ // Find the widest layer and layout diagram from there
161
+ const baseLayerIndex = layers.reduce(
162
+ (widestLayerIndex, layer, i) =>
163
+ layers[widestLayerIndex]!.occupiedSpace < layer.occupiedSpace ? i : widestLayerIndex,
164
+ 0
165
+ )
166
+ const baseLayer = layers[baseLayerIndex]!
167
+ const baseLayerPosition = layers.slice(0, baseLayerIndex).reduce(
168
+ (a, layer) => a + layer.primaryAxisSize + rowMargin,
169
+ layoutRect[this.primaryAxisCoord]
170
+ )
171
+ const baseLayerLayout = this.buildLayerLayout(
172
+ baseLayer,
173
+ layoutRect,
174
+ baseLayerPosition,
175
+ nodeMap,
176
+ null
177
+ )
178
+ baseLayer.layout = baseLayerLayout
179
+ layout.push(...baseLayerLayout.nodePositions)
180
+
181
+ // Layout layers after the base layer
182
+ let placeNextLayerAt = baseLayerPosition + baseLayer.primaryAxisSize + rowMargin
183
+ let refLayer = baseLayer
184
+ for (let i = baseLayerIndex + 1; i < layers.length; i++) {
185
+ const layer = layers[i]!
186
+ layer.layout = this.buildLayerLayout(layer, layoutRect, placeNextLayerAt, nodeMap, refLayer)
187
+
188
+ layout.push(...layer.layout.nodePositions)
189
+ refLayer = layer.layout.refLayer ?? layer
190
+ placeNextLayerAt += layer.primaryAxisSize + rowMargin
191
+ }
192
+
193
+ // Layout layers before the base layer
194
+ placeNextLayerAt = baseLayerPosition
195
+ refLayer = baseLayer
196
+ for (let i = baseLayerIndex - 1; i >= 0; i--) {
197
+ const layer = layers[i]!
198
+ placeNextLayerAt -= layer.primaryAxisSize + rowMargin
199
+
200
+ layer.layout = this.buildLayerLayout(layer, layoutRect, placeNextLayerAt, nodeMap, refLayer)
201
+
202
+ layout.push(...layer.layout.nodePositions)
203
+ refLayer = layer.layout.refLayer ?? layer
204
+ }
205
+
206
+ return new Map(layout)
207
+ }
208
+
209
+ private buildLayerLayout(
210
+ layer: Layer,
211
+ layoutRect: Rect,
212
+ placeNextLayerAt: number,
213
+ nodeMap: Map<string, NodeRect>,
214
+ refLayer: Layer | null
215
+ ): Layout {
216
+ let bestLayerLayout = this.scoreLayout(
217
+ this.spaceAround(layer, layoutRect, placeNextLayerAt),
218
+ nodeMap
219
+ )
220
+
221
+ if (layer.nodes.length != 1) {
222
+ const currentlayerLayout = this.scoreLayout(
223
+ this.spaceBetween(layer, layoutRect, placeNextLayerAt),
224
+ nodeMap
225
+ )
226
+ bestLayerLayout = currentlayerLayout[0] < bestLayerLayout[0] ? currentlayerLayout : bestLayerLayout
227
+ }
228
+
229
+ if (refLayer && refLayer.nodes.length - 1 >= layer.nodes.length) {
230
+ const currentlayerLayout = this.scoreLayout(
231
+ this.placeInGaps(layer, placeNextLayerAt, refLayer),
232
+ nodeMap
233
+ )
234
+ bestLayerLayout = currentlayerLayout[0] < bestLayerLayout[0] ? currentlayerLayout : bestLayerLayout
235
+ }
236
+
237
+ if (refLayer && refLayer.nodes.length >= layer.nodes.length) {
238
+ const currentlayerLayout = this.scoreLayout(
239
+ this.placeInCells(layer, placeNextLayerAt, refLayer),
240
+ nodeMap
241
+ )
242
+ bestLayerLayout = currentlayerLayout[0] < bestLayerLayout[0] ? currentlayerLayout : bestLayerLayout
243
+ }
244
+
245
+ return bestLayerLayout[1]
246
+ }
247
+
248
+ private spaceBetween(
249
+ layer: Layer,
250
+ layoutRect: Rect,
251
+ placeNextLayerAt: number
252
+ ): Layout {
253
+ const freeSpace = layoutRect[this.secondaryAxisDimension] - layer.occupiedSpace
254
+ const margin = freeSpace / (layer.nodes.length - 1)
255
+
256
+ let placeNextNodeAt = layoutRect[this.secondaryAxisCoord]
257
+ const result = new Map<string, XYPosition>()
258
+
259
+ let i = 0
260
+ for (let node of layer.nodes) {
261
+ result.set(node.id, {
262
+ [this.secondaryAxisCoord]: placeNextNodeAt,
263
+ [this.primaryAxisCoord]: placeNextLayerAt
264
+ } as XYPosition)
265
+ placeNextNodeAt += node[this.secondaryAxisDimension] + margin
266
+ i++
267
+ }
268
+
269
+ return { nodePositions: result, refLayer: null }
270
+ }
271
+
272
+ private spaceAround(
273
+ layer: Layer,
274
+ layoutRect: Rect,
275
+ placeNextLayerAt: number
276
+ ): Layout {
277
+ const freeSpace = layoutRect[this.secondaryAxisDimension] - layer.occupiedSpace
278
+ const margin = freeSpace / (layer.nodes.length + 1)
279
+
280
+ let placeNextNodeAt = layoutRect[this.secondaryAxisCoord] + margin
281
+ const result = new Map<string, XYPosition>()
282
+
283
+ for (let node of sortBy(layer.nodes, n => n[this.secondaryAxisCoord])) {
284
+ result.set(node.id, {
285
+ [this.secondaryAxisCoord]: placeNextNodeAt,
286
+ [this.primaryAxisCoord]: placeNextLayerAt
287
+ } as XYPosition)
288
+ placeNextNodeAt += node[this.secondaryAxisDimension] + margin
289
+ }
290
+
291
+ return { nodePositions: result, refLayer: null }
292
+ }
293
+
294
+ private placeInGaps(
295
+ layer: Layer,
296
+ placeNextLayerAt: number,
297
+ refLayer: Layer
298
+ ): Layout {
299
+ const result = new Map<string, XYPosition>()
300
+
301
+ const nodes = layer.nodes
302
+ const placementOptions = this.getGapsPositions(refLayer)
303
+
304
+ let optionIndex = 0
305
+ for (let i = 0, node = nodes[i]!; i < nodes.length; i++, node = nodes[i]!) {
306
+ const nodeCenter = node[this.secondaryAxisCoord] + node[this.secondaryAxisDimension] / 2
307
+
308
+ let bestOffset = Infinity
309
+ while (optionIndex - i <= placementOptions.length - nodes.length) {
310
+ let position = placementOptions[optionIndex]!
311
+ const offset = position - nodeCenter
312
+
313
+ if (Math.abs(offset) < Math.abs(bestOffset)) {
314
+ bestOffset = offset
315
+
316
+ optionIndex++
317
+ }
318
+ else {
319
+ break
320
+ }
321
+ }
322
+
323
+ result.set(node.id, {
324
+ [this.secondaryAxisCoord]: node[this.secondaryAxisCoord] + bestOffset,
325
+ [this.primaryAxisCoord]: placeNextLayerAt
326
+ } as XYPosition)
327
+ }
328
+
329
+ return { nodePositions: result, refLayer }
330
+ }
331
+
332
+ private placeInCells(
333
+ layer: Layer,
334
+ placeNextLayerAt: number,
335
+ refLayer: Layer
336
+ ): Layout {
337
+ const result = new Map<string, XYPosition>()
338
+
339
+ const nodes = layer.nodes
340
+ const placementOptions = this.getNodePositions(refLayer)
341
+
342
+ let optionIndex = 0
343
+ for (let i = 0, node = nodes[i]!; i < nodes.length; i++, node = nodes[i]!) {
344
+ const nodeCenter = node[this.secondaryAxisCoord] + node[this.secondaryAxisDimension] / 2
345
+
346
+ let bestOffset = Infinity
347
+
348
+ while (optionIndex - i <= placementOptions.length - nodes.length) {
349
+ let position = placementOptions[optionIndex]!
350
+ const offset = position - nodeCenter
351
+
352
+ if (Math.abs(offset) < Math.abs(bestOffset)) {
353
+ bestOffset = offset
354
+
355
+ optionIndex++
356
+ }
357
+ else {
358
+ break
359
+ }
360
+ }
361
+
362
+ result.set(node.id, {
363
+ [this.secondaryAxisCoord]: node[this.secondaryAxisCoord] + bestOffset,
364
+ [this.primaryAxisCoord]: placeNextLayerAt
365
+ } as XYPosition)
366
+ }
367
+
368
+ return { nodePositions: result, refLayer }
369
+ }
370
+
371
+ private scoreLayout(
372
+ layout: Layout,
373
+ originalRects: Map<string, NodeRect>
374
+ ): [number, Layout] {
375
+ return [
376
+ pipe(
377
+ Array.from(layout.nodePositions),
378
+ map(([id, position]) => {
379
+ const originalRect = originalRects.get(id)
380
+ invariant(originalRect, `Could not find original rect for node ${id}`)
381
+ return [pick(originalRect, ['x', 'y']), position]
382
+ }),
383
+ map(([original, suggested]) =>
384
+ Math.abs(original![this.secondaryAxisCoord] - suggested![this.secondaryAxisCoord])
385
+ ),
386
+ reduce((a, b) => a + b, 0)
387
+ ),
388
+ layout
389
+ ]
390
+ }
391
+
392
+ private getGapsPositions(layer: Layer): number[] {
393
+ const result = []
394
+ const { layout, nodes } = layer
395
+
396
+ invariant(layout, 'Layout of the layer must be computed before calling getGapsPositions')
397
+
398
+ for (let i = 1; i < nodes.length; i++) {
399
+ const previousNode = nodes[i - 1]!
400
+ const currentNode = nodes[i]!
401
+ const previousNodePosition = layout.nodePositions.get(previousNode.id)!
402
+ const currentNodePosition = layout.nodePositions.get(currentNode.id)!
403
+
404
+ result.push(
405
+ (currentNodePosition[this.secondaryAxisCoord]
406
+ + previousNodePosition[this.secondaryAxisCoord]
407
+ + previousNode[this.secondaryAxisDimension]) / 2
408
+ )
409
+ }
410
+
411
+ return result
412
+ }
413
+
414
+ private getNodePositions(layer: Layer): number[] {
415
+ const result = []
416
+ const { layout, nodes } = layer
417
+
418
+ invariant(layout, 'Layout of the layer must be computed before calling getGapsPositions')
419
+
420
+ for (let i = 0; i < nodes.length; i++) {
421
+ const node = nodes[i]!
422
+ const nodePosition = layout.nodePositions.get(node.id)!
423
+
424
+ result.push(
425
+ nodePosition[this.secondaryAxisCoord]
426
+ + node[this.secondaryAxisDimension] / 2
427
+ )
428
+ }
429
+
430
+ return result
431
+ }
432
+ }
433
+
434
+ export function getLinearAligner(mode: LinearAlignmentMode): Aligner {
435
+ switch (mode) {
436
+ case 'Left':
437
+ return new LinearAligner(
438
+ nodes => Math.min(...nodes.map(n => n.x)),
439
+ (alignTo, _) => Math.floor(alignTo),
440
+ 'x'
441
+ )
442
+ case 'Top':
443
+ return new LinearAligner(
444
+ nodes => Math.min(...nodes.map(n => n.y)),
445
+ (alignTo, _) => Math.floor(alignTo),
446
+ 'y'
447
+ )
448
+ case 'Right':
449
+ return new LinearAligner(
450
+ nodes => Math.max(...nodes.map(n => n.x + n.width)),
451
+ (alignTo, node) => Math.floor(alignTo - node.width!),
452
+ 'x'
453
+ )
454
+ case 'Bottom':
455
+ return new LinearAligner(
456
+ nodes => Math.max(...nodes.map(n => n.y + n.height)),
457
+ (alignTo, node) => Math.floor(alignTo - node.height!),
458
+ 'y'
459
+ )
460
+ case 'Center':
461
+ return new LinearAligner(
462
+ nodes => Math.min(...nodes.map(n => n.x + n.width / 2)),
463
+ (alignTo, node) => Math.floor(alignTo - node.width / 2),
464
+ 'x'
465
+ )
466
+ case 'Middle':
467
+ return new LinearAligner(
468
+ nodes => Math.min(...nodes.map(n => n.y + n.height / 2)),
469
+ (alignTo, node) => Math.floor(alignTo - node.height / 2),
470
+ 'y'
471
+ )
472
+ }
473
+ }
@@ -0,0 +1,67 @@
1
+ import { nonexhaustive } from '@likec4/core'
2
+ import type { InternalNode } from '@xyflow/react'
3
+ import { getNodeDimensions } from '@xyflow/system'
4
+ import { hasAtLeast } from 'remeda'
5
+ import type { DiagramState } from '../hooks'
6
+ import type { XYFlowNode } from '../xyflow/types'
7
+ import { createLayoutConstraints } from '../xyflow/useLayoutConstraints'
8
+ import { type Aligner, getLinearAligner, GridAligner, type GridAlignmentMode, type LinearAlignmentMode, type NodeRect } from './aligners'
9
+
10
+ export type AlignmentMode = LinearAlignmentMode | GridAlignmentMode
11
+
12
+ export function align(get: () => DiagramState) {
13
+ return (mode: AlignmentMode) => {
14
+ const { scheduleSaveManualLayout, xystore } = get()
15
+ const { nodeLookup, parentLookup } = xystore.getState()
16
+
17
+ const selectedNodes = new Set(nodeLookup.values().filter(n => n.selected).map(n => n.id))
18
+ const nodesToAlign = [...selectedNodes.difference(new Set(parentLookup.keys()))]
19
+
20
+ if (!hasAtLeast(nodesToAlign, 2)) {
21
+ console.warn('At least 2 nodes must be selected to align')
22
+ return
23
+ }
24
+ const constraints = createLayoutConstraints(xystore, nodesToAlign)
25
+
26
+ const aligner = getAligner(mode)
27
+
28
+ constraints.onMove(nodes => {
29
+ aligner.computeLayout(nodes.map(({ node }) => toNodeRect(node)))
30
+
31
+ nodes.forEach(({ rect, node }) => {
32
+ rect.positionAbsolute = {
33
+ ...rect.positionAbsolute,
34
+ ...aligner.applyPosition(toNodeRect(node))
35
+ }
36
+ })
37
+ })
38
+
39
+ scheduleSaveManualLayout()
40
+ }
41
+ }
42
+
43
+ function toNodeRect(node: InternalNode<XYFlowNode>): NodeRect {
44
+ return {
45
+ ...node.internals.positionAbsolute,
46
+ id: node.id,
47
+ width: getNodeDimensions(node).width,
48
+ height: getNodeDimensions(node).height
49
+ }
50
+ }
51
+
52
+ function getAligner(mode: AlignmentMode): Aligner {
53
+ switch (mode) {
54
+ case 'Left':
55
+ case 'Right':
56
+ case 'Top':
57
+ case 'Bottom':
58
+ case 'Center':
59
+ case 'Middle':
60
+ return getLinearAligner(mode)
61
+ case 'Column':
62
+ case 'Row':
63
+ return new GridAligner(mode)
64
+ default:
65
+ nonexhaustive(mode)
66
+ }
67
+ }