@likec4/diagram 1.12.2 → 1.13.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 (49) hide show
  1. package/package.json +8 -8
  2. package/src/LikeC4CustomColors.tsx +2 -2
  3. package/src/LikeC4Diagram.props.ts +15 -7
  4. package/src/LikeC4Diagram.tsx +23 -14
  5. package/src/StaticLikeC4Diagram.tsx +2 -0
  6. package/src/components/Overlay.css.ts +78 -0
  7. package/src/components/Overlay.tsx +102 -0
  8. package/src/components/OverlayContext.tsx +11 -0
  9. package/src/components/index.ts +2 -0
  10. package/src/likec4model/index.ts +1 -0
  11. package/src/likec4model/useLikeC4ElementsTree.ts +44 -0
  12. package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +13 -0
  13. package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +37 -0
  14. package/src/overlays/index.tsx +33 -0
  15. package/src/overlays/relationships-of/RelationshipsOverlay.tsx +33 -0
  16. package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +359 -0
  17. package/src/overlays/relationships-of/SelectElement.css.ts +18 -0
  18. package/src/overlays/relationships-of/SelectElement.tsx +133 -0
  19. package/src/overlays/relationships-of/_types.ts +56 -0
  20. package/src/overlays/relationships-of/styles.css.ts +34 -0
  21. package/src/overlays/relationships-of/use-layouted-relationships.ts +607 -0
  22. package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +76 -0
  23. package/src/overlays/relationships-of/xyflow/ElementNode.tsx +130 -0
  24. package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +22 -0
  25. package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +59 -0
  26. package/src/overlays/relationships-of/xyflow/styles.css.ts +170 -0
  27. package/src/state/diagram-to-xyflow.ts +1 -1
  28. package/src/state/diagramStore.ts +119 -27
  29. package/src/ui/top-left/ChangeAutoLayoutButton.tsx +1 -1
  30. package/src/ui/top-left/ManualLayoutToolsButton.tsx +110 -0
  31. package/src/ui/top-left/TopLeftPanel.tsx +15 -31
  32. package/src/ui/top-left/_shared.ts +2 -2
  33. package/src/xyflow/EdgeMarkers.tsx +19 -0
  34. package/src/xyflow/XYFlowEvents.tsx +21 -24
  35. package/src/xyflow/XYFlowInner.tsx +6 -10
  36. package/src/xyflow/edges/EdgeLabel.tsx +6 -5
  37. package/src/xyflow/edges/RelationshipEdge.tsx +4 -3
  38. package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +16 -0
  39. package/src/xyflow/edges/edges.css.ts +5 -11
  40. package/src/xyflow/nodes/compound/CompoundNode.css.ts +17 -40
  41. package/src/xyflow/nodes/compound/CompoundNode.tsx +219 -28
  42. package/src/xyflow/nodes/element/ElementLink.tsx +11 -8
  43. package/src/xyflow/nodes/element/ElementNode.tsx +211 -140
  44. package/src/xyflow/nodes/element/element.css.ts +22 -33
  45. package/src/xyflow/nodes/shared/Toolbar.tsx +81 -27
  46. package/src/xyflow/nodes/use-animate-variants.tsx +64 -0
  47. package/src/xyflow/useLayoutConstraints.ts +95 -125
  48. package/src/xyflow/utils.ts +4 -0
  49. package/src/xyflow/nodes/shared/NavigateToBtn.tsx +0 -32
@@ -33,14 +33,15 @@ import {
33
33
  Space,
34
34
  Stack,
35
35
  Text,
36
- Tooltip,
36
+ Tooltip as MantineTooltip,
37
37
  TooltipGroup
38
38
  } from '@mantine/core'
39
- import { IconCheck, IconFileSymlink, IconSelector } from '@tabler/icons-react'
39
+ import { IconCheck, IconFileSymlink, IconSelector, IconTransform } from '@tabler/icons-react'
40
40
  import { NodeToolbar, type NodeToolbarProps } from '@xyflow/react'
41
41
  import clsx from 'clsx'
42
42
  import { useEffect, useState } from 'react'
43
43
  import { useDiagramState, useDiagramStoreApi, useMantinePortalProps, useUpdateEffect } from '../../../hooks'
44
+ import { stopPropagation } from '../../utils'
44
45
 
45
46
  const {
46
47
  primary,
@@ -63,7 +64,15 @@ const colors = keys(otherColors).map(key => ({
63
64
  type ThemeColorKey = typeof themedColors[0]['key']
64
65
  type ColorKey = typeof colors[0]['key']
65
66
 
66
- const stopEventPropagation = (e: React.MouseEvent) => e.stopPropagation()
67
+ const Tooltip = MantineTooltip.withProps({
68
+ color: 'dark',
69
+ fz: 'xs',
70
+ openDelay: 400,
71
+ closeDelay: 150,
72
+ label: '',
73
+ children: null,
74
+ offset: 4
75
+ })
67
76
 
68
77
  type ToolbarProps = NodeToolbarProps & {
69
78
  element: Element
@@ -79,8 +88,12 @@ export function CompoundToolbar({
79
88
  }: ToolbarProps) {
80
89
  const targets = [element.id] as NonEmptyArray<Fqn>
81
90
  const diagramApi = useDiagramStoreApi()
82
- const { hasGoToSource } = useDiagramState(s => ({
83
- hasGoToSource: !!s.onOpenSourceElement
91
+ const {
92
+ hasGoToSource,
93
+ enableRelationshipsBrowser
94
+ } = useDiagramState(s => ({
95
+ hasGoToSource: !!s.onOpenSourceElement,
96
+ enableRelationshipsBrowser: s.enableRelationshipsBrowser
84
97
  }))
85
98
 
86
99
  const onChange: OnStyleChange = (style) => {
@@ -105,6 +118,7 @@ export function CompoundToolbar({
105
118
  onChange={onChange}
106
119
  />
107
120
  {hasGoToSource && <GoToSourceButton elementId={element.id} />}
121
+ {enableRelationshipsBrowser && <BrowseRelationshipsButton elementId={element.id} />}
108
122
  </Toolbar>
109
123
  )
110
124
  }
@@ -119,10 +133,10 @@ function Toolbar({ element, children, ...props }: Omit<ToolbarProps, 'onColorPre
119
133
  pt={4}
120
134
  radius={'sm'}
121
135
  shadow="xl"
122
- onDoubleClickCapture={stopEventPropagation}
123
- onPointerDown={stopEventPropagation}
124
- onClick={stopEventPropagation}
125
- onDoubleClick={stopEventPropagation}
136
+ onDoubleClickCapture={stopPropagation}
137
+ onPointerDown={stopPropagation}
138
+ onClick={stopPropagation}
139
+ onDoubleClick={stopPropagation}
126
140
  withBorder>
127
141
  <Stack gap={4}>
128
142
  <Box pl={2}>
@@ -144,8 +158,12 @@ export function ElementToolbar({
144
158
  }: ToolbarProps) {
145
159
  const targets = [element.id] as NonEmptyArray<Fqn>
146
160
  const diagramApi = useDiagramStoreApi()
147
- const { hasGoToSource } = useDiagramState(s => ({
148
- hasGoToSource: !!s.onOpenSourceElement
161
+ const {
162
+ hasGoToSource,
163
+ enableRelationshipsBrowser
164
+ } = useDiagramState(s => ({
165
+ hasGoToSource: !!s.onOpenSourceElement,
166
+ enableRelationshipsBrowser: s.enableRelationshipsBrowser
149
167
  }))
150
168
  const portalProps = useMantinePortalProps()
151
169
 
@@ -190,8 +208,8 @@ export function ElementToolbar({
190
208
  // className={css.menuDropdown}
191
209
  // onPointerDownCapture={stopEventPropagation}
192
210
  // onPointerDown={stopEventPropagation}
193
- onDoubleClick={stopEventPropagation}
194
- onClick={stopEventPropagation}
211
+ onDoubleClick={stopPropagation}
212
+ onClick={stopPropagation}
195
213
  >
196
214
  {ElementShapes.map(shape => (
197
215
  <MenuItem
@@ -217,6 +235,7 @@ export function ElementToolbar({
217
235
  position="right-end"
218
236
  />
219
237
  {hasGoToSource && <GoToSourceButton elementId={element.id} />}
238
+ {enableRelationshipsBrowser && <BrowseRelationshipsButton elementId={element.id} />}
220
239
  </Toolbar>
221
240
  )
222
241
  }
@@ -230,17 +249,20 @@ type ColorButtonProps = Omit<PopoverProps, 'onChange'> & {
230
249
 
231
250
  function GoToSourceButton({ elementId }: { elementId: Fqn }) {
232
251
  const diagramApi = useDiagramStoreApi()
252
+ const portalProps = useMantinePortalProps()
233
253
  return (
234
- <ActionIcon
235
- size={'sm'}
236
- variant="subtle"
237
- color="gray"
238
- onClick={e => {
239
- e.stopPropagation()
240
- diagramApi.getState().onOpenSourceElement?.(elementId)
241
- }}>
242
- <IconFileSymlink size={'70%'} stroke={1.8} />
243
- </ActionIcon>
254
+ <Tooltip label={'Open source'} {...portalProps}>
255
+ <ActionIcon
256
+ size={'sm'}
257
+ variant="subtle"
258
+ color="gray"
259
+ onClick={e => {
260
+ e.stopPropagation()
261
+ diagramApi.getState().onOpenSourceElement?.(elementId)
262
+ }}>
263
+ <IconFileSymlink size={'70%'} stroke={1.8} />
264
+ </ActionIcon>
265
+ </Tooltip>
244
266
  )
245
267
  }
246
268
 
@@ -314,7 +336,7 @@ function ColorSwatches({
314
336
  <TooltipGroup openDelay={1000} closeDelay={300}>
315
337
  <Flex maw={120} gap="6" justify="flex-start" align="flex-start" direction="row" wrap="wrap">
316
338
  {themedColors.map(({ key, value }) => (
317
- <Tooltip
339
+ <MantineTooltip
318
340
  key={key}
319
341
  label={key}
320
342
  fz={'xs'}
@@ -331,7 +353,7 @@ function ColorSwatches({
331
353
  >
332
354
  {elementColor === key && <CheckIcon style={{ width: rem(10), height: rem(10) }} />}
333
355
  </ColorSwatch>
334
- </Tooltip>
356
+ </MantineTooltip>
335
357
  ))}
336
358
  </Flex>
337
359
 
@@ -344,7 +366,7 @@ function ColorSwatches({
344
366
  direction="row"
345
367
  wrap="wrap">
346
368
  {colors.map(({ key, value }) => (
347
- <Tooltip
369
+ <MantineTooltip
348
370
  key={key}
349
371
  label={key}
350
372
  fz={'xs'}
@@ -360,7 +382,7 @@ function ColorSwatches({
360
382
  >
361
383
  {elementColor === key && <CheckIcon style={{ width: rem(10), height: rem(10) }} />}
362
384
  </ColorSwatch>
363
- </Tooltip>
385
+ </MantineTooltip>
364
386
  ))}
365
387
  </Flex>
366
388
  </TooltipGroup>
@@ -392,6 +414,12 @@ function BorderStyleOption({
392
414
  setValue(border)
393
415
  onChange({ border })
394
416
  }}
417
+ styles={{
418
+ label: {
419
+ paddingTop: 2,
420
+ paddingBottom: 2
421
+ }
422
+ }}
395
423
  data={[
396
424
  { label: 'Solid', value: 'solid' },
397
425
  { label: 'Dashed', value: 'dashed' },
@@ -424,3 +452,29 @@ function OpacityOption({
424
452
  onChangeEnd={onOpacityChange} />
425
453
  )
426
454
  }
455
+
456
+ function BrowseRelationshipsButton({ elementId }: { elementId: Fqn }) {
457
+ const diagramApi = useDiagramStoreApi()
458
+ const portalProps = useMantinePortalProps()
459
+ return (
460
+ <Tooltip label={'Browse relationships'} {...portalProps}>
461
+ <ActionIcon
462
+ size={'sm'}
463
+ variant="subtle"
464
+ color="gray"
465
+ onClick={e => {
466
+ e.stopPropagation()
467
+ diagramApi.getState().openOverlay({
468
+ relationshipsOf: elementId
469
+ })
470
+ }}>
471
+ <IconTransform
472
+ stroke={2}
473
+ style={{
474
+ width: '72%',
475
+ height: '72%'
476
+ }} />
477
+ </ActionIcon>
478
+ </Tooltip>
479
+ )
480
+ }
@@ -0,0 +1,64 @@
1
+ import { useMemo, useState } from 'react'
2
+ import { isEmpty, isString } from 'remeda'
3
+
4
+ export function useFramerAnimateVariants() {
5
+ const [variants, setVariants] = useState<string[] | null>(null)
6
+
7
+ const handlers = useMemo(() => {
8
+ const getTarget = (e: MouseEvent | undefined) => {
9
+ try {
10
+ // 🤔 Sometimes target is null
11
+ return (e?.target as HTMLElement | null)?.closest('[data-animate-target]')?.getAttribute('data-animate-target')
12
+ ?? null
13
+ } catch (_e) {
14
+ console.warn(`Failed to get animate target`, _e)
15
+ // noop
16
+ return null
17
+ }
18
+ }
19
+
20
+ const onHoverStart = (e: MouseEvent) => {
21
+ e?.stopPropagation()
22
+ const hoverTarget = getTarget(e)
23
+ if (!isString(hoverTarget) || isEmpty(hoverTarget)) {
24
+ setVariants(null)
25
+ return
26
+ }
27
+ setVariants(['hovered', `hovered:${hoverTarget}`])
28
+ }
29
+
30
+ const resetVariants = (e: MouseEvent) => {
31
+ e?.stopPropagation()
32
+ setVariants(null)
33
+ }
34
+
35
+ return ({
36
+ onTapStart: (e: MouseEvent) => {
37
+ e?.stopPropagation()
38
+ const tapTarget = getTarget(e)
39
+ if (!isString(tapTarget)) {
40
+ setVariants(null)
41
+ return
42
+ }
43
+ if (isEmpty(tapTarget)) {
44
+ setVariants([
45
+ 'hovered',
46
+ 'tap'
47
+ ])
48
+ } else {
49
+ setVariants([
50
+ 'hovered',
51
+ `hovered:${tapTarget}`,
52
+ `tap:${tapTarget}`
53
+ ])
54
+ }
55
+ },
56
+ onHoverStart,
57
+ onHoverEnd: resetVariants,
58
+ onTapCancel: resetVariants,
59
+ onTap: onHoverStart
60
+ })
61
+ }, [setVariants])
62
+
63
+ return [variants, handlers] as const
64
+ }
@@ -1,5 +1,4 @@
1
1
  import { invariant, isAncestor, nonNullable } from '@likec4/core'
2
- import { Expression, Expression as Expr, Operator, Solver, Strength, Variable } from '@lume/kiwi'
3
2
  import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyflow/react'
4
3
  import { getNodeDimensions } from '@xyflow/system'
5
4
  import { useMemo, useRef } from 'react'
@@ -9,31 +8,36 @@ import { type XYStoreApi } from '../hooks/useXYFlow'
9
8
  import type { XYFlowNode } from './types'
10
9
 
11
10
  abstract class Rect {
12
- id!: string
13
- minX = new Variable()
14
- minY = new Variable()
15
- maxX: Expression | Variable = new Variable()
16
- maxY: Expression | Variable = new Variable()
17
-
18
- get x() {
19
- return this.minX
20
- }
11
+ static readonly LeftPadding = 40
12
+ static readonly RightPadding = 40
13
+ static readonly TopPadding = 55
14
+ static readonly BottomPadding = 40
21
15
 
22
- get y() {
23
- return this.minY
24
- }
16
+ id!: string
17
+ minX: number = Infinity
18
+ minY: number = Infinity
19
+ maxX: number = -Infinity
20
+ maxY: number = -Infinity
25
21
 
26
22
  get positionAbsolute(): XYPosition {
27
23
  return {
28
- x: this.x.value(),
29
- y: this.y.value()
24
+ x: this.minX,
25
+ y: this.minY
30
26
  }
31
27
  }
32
28
 
29
+ set positionAbsolute(pos: XYPosition) {
30
+ this.maxX += pos.x - this.minX
31
+ this.maxY += pos.y - this.minY
32
+
33
+ this.minX = pos.x
34
+ this.minY = pos.y
35
+ }
36
+
33
37
  get dimensions() {
34
38
  return {
35
- width: this.maxX.value() - this.minX.value(),
36
- height: this.maxY.value() - this.minY.value()
39
+ width: this.maxX - this.minX,
40
+ height: this.maxY - this.minY
37
41
  }
38
42
  }
39
43
 
@@ -53,121 +57,51 @@ abstract class Rect {
53
57
  }
54
58
 
55
59
  class Compound extends Rect {
56
- protected readonly children = [] as Rect[]
60
+ public readonly children = [] as Rect[]
57
61
 
58
62
  constructor(
59
- protected solver: Solver,
60
63
  xynode: InternalNode<XYFlowNode>,
61
64
  protected readonly parent: Compound | null = null
62
65
  ) {
63
66
  super()
64
67
  this.id = xynode.id
65
68
 
66
- solver.createConstraint(this.maxX, Operator.Ge, this.minX)
67
- solver.createConstraint(this.maxY, Operator.Ge, this.minY)
68
-
69
69
  if (parent) {
70
- parent.addChild(this)
70
+ parent.children.push(this)
71
71
  }
72
72
  }
73
-
74
- addChild<R extends Rect>(rect: R) {
75
- this.children.push(rect)
76
-
77
- const leftPadding = new Expr(
78
- rect.minX,
79
- [-1, this.minX]
80
- )
81
- const { weak } = Strength
82
- this.solver.createConstraint(leftPadding, Operator.Ge, 40)
83
- this.solver.createConstraint(leftPadding, Operator.Eq, 41, weak)
84
-
85
- const topPadding = new Expr(
86
- rect.minY,
87
- [-1, this.minY]
88
- )
89
- this.solver.createConstraint(topPadding, Operator.Ge, 54)
90
- this.solver.createConstraint(topPadding, Operator.Eq, 55, weak)
91
-
92
- const rightPadding = new Expr(
93
- this.maxX,
94
- [-1, rect.maxX]
95
- )
96
- this.solver.createConstraint(rightPadding, Operator.Ge, 40)
97
- this.solver.createConstraint(rightPadding, Operator.Eq, 41, weak)
98
-
99
- const bottomPadding = new Expr(
100
- this.maxY,
101
- [-1, rect.maxY]
102
- )
103
- this.solver.createConstraint(bottomPadding, Operator.Ge, 40)
104
- this.solver.createConstraint(bottomPadding, Operator.Eq, 41, weak)
105
- }
106
73
  }
107
74
 
108
75
  class Leaf extends Rect {
109
76
  constructor(
110
- solver: Solver,
111
77
  xynode: InternalNode<XYFlowNode>,
112
- public readonly parent: Compound | null = null,
113
- public readonly isEditing: boolean = false
78
+ public readonly parent: Compound | null = null
114
79
  ) {
115
80
  super()
116
81
  this.id = xynode.id
117
82
 
118
- // const isEditing: boolean = xynode.dragging ?? false
119
- const x = Math.ceil(xynode.internals.positionAbsolute.x)
120
- const y = Math.ceil(xynode.internals.positionAbsolute.y)
83
+ this.positionAbsolute = xynode.internals.positionAbsolute
121
84
 
122
85
  const { width, height } = getNodeDimensions(xynode)
123
86
 
124
- this.maxX = new Expression(this.minX, Math.ceil(width))
125
- this.maxY = new Expression(this.minY, Math.ceil(height))
126
-
127
- // solver.createConstraint(this.width, Operator.Eq, width, Strength.required)
128
- // solver.createConstraint(this.height, Operator.Eq, height, Strength.required)
129
-
130
- if (isEditing) {
131
- const superStrong = Strength.create(100, 0, 0)
132
- solver.addEditVariable(this.minX, superStrong)
133
- solver.addEditVariable(this.minY, superStrong)
134
- solver.suggestValue(this.minX, x)
135
- solver.suggestValue(this.minY, y)
136
- } else {
137
- const halfMedium = Strength.create(0.0, 1.0, 0.0, 0.5)
138
- solver.createConstraint(this.minX, Operator.Eq, x, halfMedium)
139
- solver.createConstraint(this.minY, Operator.Eq, y, halfMedium)
140
- // const weaker = Strength.create(0.0, 0.0, 1.0, 0.5)
141
- // const weak = Strength.create(0.0, 0.0, 1.0)
142
- // const x = new Expr(
143
- // this.minX,
144
- // [-1, pos.x]
145
- // )
146
- // solver.createConstraint(x, Operator.Eq, 0, weaker)
147
- // solver.createConstraint(x, Operator.Ge, -30, weak)
148
- // solver.createConstraint(x, Operator.Le, 30, weak)
149
-
150
- // const y = new Expr(
151
- // this.minY,
152
- // [-1, pos.y]
153
- // )
154
- // solver.createConstraint(y, Operator.Eq, 0, weaker)
155
- // solver.createConstraint(y, Operator.Ge, -30, weak)
156
- // solver.createConstraint(y, Operator.Le, 30, weak)
157
- }
87
+ this.maxX = this.minX + Math.ceil(width)
88
+ this.maxY = this.minY + Math.ceil(height)
158
89
 
159
90
  if (parent) {
160
- parent.addChild(this)
91
+ parent.children.push(this)
161
92
  }
162
93
  }
163
94
  }
164
95
 
165
- function createLayoutConstraints(
96
+ type NodePositionUpdater = (
97
+ nodes: Array<{ rect: Rect | Compound; node: InternalNode<XYFlowNode> }>
98
+ ) => void
99
+
100
+ export function createLayoutConstraints(
166
101
  xyflowApi: XYStoreApi,
167
- draggingNodeId: string
102
+ editingNodeIds: Set<string>
168
103
  ) {
169
104
  const { parentLookup, nodeLookup } = xyflowApi.getState()
170
- const solver = new Solver()
171
105
  const rects = new Map<string, Leaf | Compound>()
172
106
 
173
107
  const traverse = new Array<{ xynode: InternalNode<XYFlowNode>; parent: Compound | null }>()
@@ -181,18 +115,15 @@ function createLayoutConstraints(
181
115
  }
182
116
  }
183
117
 
184
- const draggingNodes = new Set(nodeLookup.values()
185
- .filter(x => x.dragging === true || x.id === draggingNodeId || x.selected)
186
- .map(x => x.id))
187
-
188
118
  while (traverse.length > 0) {
189
119
  const { xynode, parent } = traverse.shift()!
190
- const isDragging = draggingNodes.has(xynode.id)
120
+ const isEditing = editingNodeIds.has(xynode.id)
191
121
 
192
122
  // Traverse children if the node is a compound, not dragging, and is an ancestor of the dragging node
193
- const shouldTraverse = !isDragging && xynode.type === 'compound' && draggingNodes.values().some(x => isAncestor(xynode.id, x))
123
+ const shouldTraverse = !isEditing && xynode.type === 'compound'
124
+ && editingNodeIds.values().some(x => isAncestor(xynode.id, x))
194
125
 
195
- const rect = shouldTraverse ? new Compound(solver, xynode, parent) : new Leaf(solver, xynode, parent, isDragging)
126
+ const rect = shouldTraverse ? new Compound(xynode, parent) : new Leaf(xynode, parent)
196
127
  rects.set(xynode.id, rect)
197
128
 
198
129
  if (shouldTraverse) {
@@ -205,12 +136,37 @@ function createLayoutConstraints(
205
136
  }
206
137
  }
207
138
 
208
- solver.updateVariables()
209
- solver.maxIterations = 1000
210
- const rectsToUpdate = [...rects.values()].filter(r => !draggingNodes.has(r.id))
139
+ const rectsToUpdate = [...rects.values()]
140
+ applyConstraints(rectsToUpdate)
141
+
142
+ function applyConstraints(targets: Rect[]) {
143
+ targets
144
+ .filter(x => x instanceof Compound)
145
+ .forEach((r) => {
146
+ applyConstraints(r.children)
147
+
148
+ const childrenBB = r.children.reduce((acc, r) => ({
149
+ minX: Math.min(acc.minX, r.minX),
150
+ minY: Math.min(acc.minY, r.minY),
151
+ maxX: Math.max(acc.maxX, r.maxX),
152
+ maxY: Math.max(acc.maxY, r.maxY)
153
+ }), {
154
+ minX: Infinity,
155
+ minY: Infinity,
156
+ maxX: -Infinity,
157
+ maxY: -Infinity
158
+ })
159
+
160
+
161
+ r.minX = childrenBB.minX - Rect.LeftPadding
162
+ r.minY = childrenBB.minY - Rect.TopPadding
163
+ r.maxX = childrenBB.maxX + Rect.RightPadding
164
+ r.maxY = childrenBB.maxY + Rect.BottomPadding
165
+ })
166
+ }
211
167
 
212
168
  function updateXYFlowNodes() {
213
- solver.updateVariables()
169
+ applyConstraints(rectsToUpdate)
214
170
  xyflowApi.getState().triggerNodeChanges(
215
171
  rectsToUpdate.reduce((acc, r) => {
216
172
  acc.push({
@@ -235,26 +191,28 @@ function createLayoutConstraints(
235
191
 
236
192
  let animationFrameId: number | null = null
237
193
 
238
- /**
239
- * Move the editing node to the given position.
240
- */
241
- function onNodeDrag(xynode: XYFlowNode) {
194
+ function onMove(
195
+ updater: NodePositionUpdater
196
+ ) {
242
197
  if (rectsToUpdate.length === 0) {
243
198
  return
244
199
  }
245
200
  animationFrameId ??= requestAnimationFrame(() => {
246
201
  animationFrameId = null
247
- const pos = nodeLookup.get(xynode.id)!.internals.positionAbsolute
248
- const rect = nonNullable(rects.get(xynode.id))
249
- solver.suggestValue(rect.minX, Math.floor(pos.x))
250
- solver.suggestValue(rect.minY, Math.floor(pos.y))
202
+ updater(
203
+ [
204
+ ...editingNodeIds.values()
205
+ .filter(id => rects.has(id) && nodeLookup.has(id))
206
+ .map(id => ({ rect: nonNullable(rects.get(id)), node: nodeLookup.get(id)! }))
207
+ ]
208
+ )
251
209
  updateXYFlowNodes()
252
210
  })
253
211
  }
254
212
 
255
213
  return {
256
- onNodeDrag,
257
- updateXYFlowNodes
214
+ updateXYFlowNodes,
215
+ onMove
258
216
  }
259
217
  }
260
218
 
@@ -268,14 +226,26 @@ export function useLayoutConstraints(): LayoutConstraints {
268
226
  return useMemo((): LayoutConstraints => ({
269
227
  onNodeDragStart: (_event, xynode) => {
270
228
  const { cancelSaveManualLayout, xystore } = diagramApi.getState()
229
+ const { nodeLookup } = xystore.getState()
271
230
  cancelSaveManualLayout()
272
- solverRef.current = createLayoutConstraints(xystore, xynode.id)
231
+
232
+ const draggingNodes = new Set(
233
+ nodeLookup.values()
234
+ .filter(x => x.dragging === true || x.id === xynode.id || x.selected)
235
+ .filter(x => x.draggable === true)
236
+ .map(x => x.id)
237
+ )
238
+ solverRef.current = createLayoutConstraints(xystore, draggingNodes)
273
239
  },
274
- onNodeDrag: (_event, xynode) => {
240
+ onNodeDrag: () => {
275
241
  invariant(solverRef.current, 'solverRef.current should be defined')
276
- solverRef.current?.onNodeDrag(xynode)
242
+ solverRef.current.onMove((nodes) => {
243
+ nodes.forEach(({ rect, node }) => {
244
+ rect.positionAbsolute = node.internals.positionAbsolute
245
+ })
246
+ })
277
247
  },
278
- onNodeDragStop: (_event, _xynode) => {
248
+ onNodeDragStop: () => {
279
249
  solverRef.current?.updateXYFlowNodes()
280
250
  diagramApi.getState().scheduleSaveManualLayout()
281
251
  solverRef.current = undefined
@@ -83,3 +83,7 @@ export function isSamePoint(a: XYPosition | Point, b: XYPosition | Point) {
83
83
  export function distanceBetweenPoints(a: XYPosition, b: XYPosition) {
84
84
  return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
85
85
  }
86
+
87
+ export function stopPropagation(e: React.MouseEvent) {
88
+ return e.stopPropagation()
89
+ }
@@ -1,32 +0,0 @@
1
- import { ActionIcon } from '@mantine/core'
2
- import { IconZoomScan } from '@tabler/icons-react'
3
- import clsx from 'clsx'
4
- import { useDiagramStoreApi } from '../../../hooks/useDiagramState'
5
-
6
- export type NavigateToBtnProps = {
7
- xynodeId: string
8
- className?: string
9
- }
10
-
11
- export function NavigateToBtn({ xynodeId, className }: NavigateToBtnProps) {
12
- const diagramApi = useDiagramStoreApi()
13
- return (
14
- <ActionIcon
15
- className={clsx('nodrag nopan', className)}
16
- radius="md"
17
- onPointerDownCapture={e => e.stopPropagation()}
18
- onClick={event => {
19
- event.stopPropagation()
20
- diagramApi.getState().triggerOnNavigateTo(xynodeId, event)
21
- }}
22
- role="button"
23
- onDoubleClick={event => event.stopPropagation()}
24
- >
25
- <IconZoomScan
26
- style={{
27
- width: '75%',
28
- height: '75%'
29
- }} />
30
- </ActionIcon>
31
- )
32
- }