@likec4/diagram 1.12.1 → 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.
- package/package.json +9 -9
- package/src/LikeC4CustomColors.tsx +2 -2
- package/src/LikeC4Diagram.props.ts +15 -7
- package/src/LikeC4Diagram.tsx +23 -14
- package/src/StaticLikeC4Diagram.tsx +2 -0
- package/src/components/Overlay.css.ts +78 -0
- package/src/components/Overlay.tsx +102 -0
- package/src/components/OverlayContext.tsx +11 -0
- package/src/components/index.ts +2 -0
- package/src/likec4model/index.ts +1 -0
- package/src/likec4model/useLikeC4ElementsTree.ts +44 -0
- package/src/overlays/edge-details/EdgeDetailsOverlay.css.ts +13 -0
- package/src/overlays/edge-details/EdgeDetailsOverlay.tsx +37 -0
- package/src/overlays/index.tsx +33 -0
- package/src/overlays/relationships-of/RelationshipsOverlay.tsx +33 -0
- package/src/overlays/relationships-of/RelationshipsXYFlow.tsx +359 -0
- package/src/overlays/relationships-of/SelectElement.css.ts +18 -0
- package/src/overlays/relationships-of/SelectElement.tsx +133 -0
- package/src/overlays/relationships-of/_types.ts +56 -0
- package/src/overlays/relationships-of/styles.css.ts +34 -0
- package/src/overlays/relationships-of/use-layouted-relationships.ts +607 -0
- package/src/overlays/relationships-of/xyflow/CompoundNode.tsx +76 -0
- package/src/overlays/relationships-of/xyflow/ElementNode.tsx +130 -0
- package/src/overlays/relationships-of/xyflow/EmptyNode.tsx +22 -0
- package/src/overlays/relationships-of/xyflow/RelationshipEdge.tsx +59 -0
- package/src/overlays/relationships-of/xyflow/styles.css.ts +170 -0
- package/src/state/diagram-to-xyflow.ts +1 -1
- package/src/state/diagramStore.ts +119 -27
- package/src/ui/top-left/ChangeAutoLayoutButton.tsx +35 -18
- package/src/ui/top-left/ManualLayoutToolsButton.tsx +110 -0
- package/src/ui/top-left/TopLeftPanel.tsx +15 -31
- package/src/ui/top-left/_shared.ts +2 -2
- package/src/xyflow/EdgeMarkers.tsx +19 -0
- package/src/xyflow/XYFlowEvents.tsx +21 -24
- package/src/xyflow/XYFlowInner.tsx +6 -10
- package/src/xyflow/edges/EdgeLabel.tsx +6 -5
- package/src/xyflow/edges/RelationshipEdge.tsx +4 -3
- package/src/xyflow/edges/RelationshipsDropdownMenu.tsx +16 -0
- package/src/xyflow/edges/edges.css.ts +5 -11
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +17 -40
- package/src/xyflow/nodes/compound/CompoundNode.tsx +219 -28
- package/src/xyflow/nodes/element/ElementLink.tsx +11 -8
- package/src/xyflow/nodes/element/ElementNode.tsx +211 -140
- package/src/xyflow/nodes/element/element.css.ts +22 -33
- package/src/xyflow/nodes/shared/Toolbar.tsx +81 -27
- package/src/xyflow/nodes/use-animate-variants.tsx +64 -0
- package/src/xyflow/useLayoutConstraints.ts +95 -125
- package/src/xyflow/utils.ts +4 -0
- 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
|
|
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 {
|
|
83
|
-
hasGoToSource
|
|
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={
|
|
123
|
-
onPointerDown={
|
|
124
|
-
onClick={
|
|
125
|
-
onDoubleClick={
|
|
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 {
|
|
148
|
-
hasGoToSource
|
|
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={
|
|
194
|
-
onClick={
|
|
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
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
e
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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.
|
|
29
|
-
y: this.
|
|
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
|
|
36
|
-
height: this.maxY
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
125
|
-
this.maxY =
|
|
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.
|
|
91
|
+
parent.children.push(this)
|
|
161
92
|
}
|
|
162
93
|
}
|
|
163
94
|
}
|
|
164
95
|
|
|
165
|
-
|
|
96
|
+
type NodePositionUpdater = (
|
|
97
|
+
nodes: Array<{ rect: Rect | Compound; node: InternalNode<XYFlowNode> }>
|
|
98
|
+
) => void
|
|
99
|
+
|
|
100
|
+
export function createLayoutConstraints(
|
|
166
101
|
xyflowApi: XYStoreApi,
|
|
167
|
-
|
|
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
|
|
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 = !
|
|
123
|
+
const shouldTraverse = !isEditing && xynode.type === 'compound'
|
|
124
|
+
&& editingNodeIds.values().some(x => isAncestor(xynode.id, x))
|
|
194
125
|
|
|
195
|
-
const rect = shouldTraverse ? new Compound(
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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: (
|
|
240
|
+
onNodeDrag: () => {
|
|
275
241
|
invariant(solverRef.current, 'solverRef.current should be defined')
|
|
276
|
-
solverRef.current
|
|
242
|
+
solverRef.current.onMove((nodes) => {
|
|
243
|
+
nodes.forEach(({ rect, node }) => {
|
|
244
|
+
rect.positionAbsolute = node.internals.positionAbsolute
|
|
245
|
+
})
|
|
246
|
+
})
|
|
277
247
|
},
|
|
278
|
-
onNodeDragStop: (
|
|
248
|
+
onNodeDragStop: () => {
|
|
279
249
|
solverRef.current?.updateXYFlowNodes()
|
|
280
250
|
diagramApi.getState().scheduleSaveManualLayout()
|
|
281
251
|
solverRef.current = undefined
|
package/src/xyflow/utils.ts
CHANGED
|
@@ -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
|
-
}
|