@likec4/diagram 1.2.2 → 1.3.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/dist/LikeC4Diagram.d.ts +1 -1
- package/dist/LikeC4Diagram.props.d.ts +21 -3
- package/dist/index.mjs +1335 -1240
- package/dist/state/DiagramContext.d.ts +2263 -175
- package/dist/state/diagramStore.d.ts +2272 -180
- package/dist/state/useDiagramStore.d.ts +2409 -321
- package/dist/style.css +123 -92
- package/dist/theme.css.d.ts +0 -1
- package/dist/ui/OptionsPanel.d.ts +3 -1
- package/dist/xyflow/XYFlow.d.ts +2 -1
- package/dist/xyflow/XYFlowInner.d.ts +1 -1
- package/dist/xyflow/const.d.ts +1 -1
- package/dist/xyflow/diagram-to-xyflow.d.ts +1 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -4
- package/dist/xyflow/edges/utils.d.ts +4 -2
- package/dist/xyflow/nodes/compound/CompoundNode.css.d.ts +3 -1
- package/dist/xyflow/nodes/element/ElementNode.d.ts +1 -1
- package/dist/xyflow/nodes/element/index.d.ts +1 -1
- package/dist/xyflow/types.d.ts +2 -2
- package/dist/xyflow/utils.d.ts +3 -0
- package/package.json +15 -14
- package/src/LikeC4Diagram.css.ts +4 -4
- package/src/LikeC4Diagram.props.ts +23 -2
- package/src/LikeC4Diagram.tsx +79 -31
- package/src/globals.css.ts +0 -1
- package/src/mantine/theme.ts +1 -0
- package/src/state/DiagramContext.tsx +0 -1
- package/src/state/diagramStore.ts +177 -100
- package/src/theme.css.ts +0 -1
- package/src/ui/DiagramTitlePanel.tsx +3 -3
- package/src/ui/DynamicViewWalkthrough.css.ts +3 -2
- package/src/ui/DynamicViewWalkthrough.tsx +36 -17
- package/src/ui/OptionsPanel.tsx +12 -12
- package/src/ui/options/NodeOptions.tsx +16 -19
- package/src/xyflow/FitviewOnDiagramChange.tsx +15 -23
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +3 -6
- package/src/xyflow/SyncWithDiagram.tsx +21 -2
- package/src/xyflow/XYFlow.tsx +20 -23
- package/src/xyflow/XYFlowEvents.tsx +26 -8
- package/src/xyflow/XYFlowInner.tsx +5 -4
- package/src/xyflow/const.ts +1 -1
- package/src/xyflow/diagram-to-xyflow.ts +5 -31
- package/src/xyflow/edges/RelationshipEdge.tsx +158 -112
- package/src/xyflow/edges/edges.css.ts +32 -21
- package/src/xyflow/edges/utils.ts +5 -2
- package/src/xyflow/hooks/useLayoutConstraints.ts +17 -3
- package/src/xyflow/nodes/compound/CompoundNode.css.ts +40 -32
- package/src/xyflow/nodes/compound/CompoundNode.tsx +36 -26
- package/src/xyflow/nodes/element/ElementNode.tsx +20 -6
- package/src/xyflow/nodes/element/element.css.ts +15 -15
- package/src/xyflow/nodes/element/index.ts +1 -1
- package/src/xyflow/nodes/shared/NavigateToBtn.tsx +7 -3
- package/src/xyflow/types.ts +5 -2
- package/src/xyflow/utils.ts +48 -0
- package/dist/ui/index.d.ts +0 -2
- package/src/ui/index.ts +0 -3
|
@@ -9,41 +9,15 @@ import {
|
|
|
9
9
|
type Point
|
|
10
10
|
} from '@likec4/core'
|
|
11
11
|
import { Bezier } from 'bezier-js'
|
|
12
|
-
import { hasAtLeast } from 'remeda'
|
|
12
|
+
import { hasAtLeast, reduce } from 'remeda'
|
|
13
13
|
import type { XYFlowData } from '../xyflow/types'
|
|
14
|
-
|
|
15
|
-
function deriveEdgePoints(bezierSpline: NonEmptyArray<Point>) {
|
|
16
|
-
let [start, ...bezierPoints] = bezierSpline
|
|
17
|
-
invariant(start, 'start should be defined')
|
|
18
|
-
const handles = [
|
|
19
|
-
// start
|
|
20
|
-
] as Point[]
|
|
21
|
-
|
|
22
|
-
while (hasAtLeast(bezierPoints, 3)) {
|
|
23
|
-
const [cp1, cp2, end, ...rest] = bezierPoints
|
|
24
|
-
const bezier = new Bezier(start[0], start[1], cp1[0], cp1[1], cp2[0], cp2[1], end[0], end[1])
|
|
25
|
-
// const { x, y } = bezier.get(0.5)
|
|
26
|
-
const inflections = bezier.inflections()
|
|
27
|
-
if (inflections.length === 0) {
|
|
28
|
-
inflections.push(0.5)
|
|
29
|
-
}
|
|
30
|
-
inflections.forEach(t => {
|
|
31
|
-
const { x, y } = bezier.get(t)
|
|
32
|
-
handles.push([x, y])
|
|
33
|
-
})
|
|
34
|
-
bezierPoints = rest
|
|
35
|
-
start = end
|
|
36
|
-
}
|
|
37
|
-
invariant(bezierPoints.length === 0, 'all points should be consumed')
|
|
38
|
-
|
|
39
|
-
return handles
|
|
40
|
-
}
|
|
14
|
+
import { toDomPrecision } from './utils'
|
|
41
15
|
|
|
42
16
|
// const nodeZIndex = (node: DiagramNode) => node.level + (hasAtLeast(node.children, 1) ? 2 : 1)
|
|
43
17
|
const nodeZIndex = (node: DiagramNode) => node.level + 1
|
|
44
18
|
|
|
45
19
|
export function diagramViewToXYFlowData(
|
|
46
|
-
view: Pick<DiagramView, 'nodes' | 'edges' | '__'>,
|
|
20
|
+
view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__' | 'manualLayout'>,
|
|
47
21
|
opts: {
|
|
48
22
|
draggable: boolean
|
|
49
23
|
selectable: boolean
|
|
@@ -147,7 +121,7 @@ export function diagramViewToXYFlowData(
|
|
|
147
121
|
const target = edge.target
|
|
148
122
|
const id = ns + edge.id
|
|
149
123
|
// const points = isElkEdge(edge) ? edge.points : deriveEdgePoints(edge.points)
|
|
150
|
-
const controlPoints = deriveEdgePoints(edge.points)
|
|
124
|
+
// const controlPoints = deriveEdgePoints(edge.points)
|
|
151
125
|
// if (edge.tailArrowPoint) {
|
|
152
126
|
// controlPoints.unshift([...edge.tailArrowPoint])
|
|
153
127
|
// }
|
|
@@ -169,7 +143,7 @@ export function diagramViewToXYFlowData(
|
|
|
169
143
|
data: {
|
|
170
144
|
edge,
|
|
171
145
|
type: 'bezier',
|
|
172
|
-
controlPoints,
|
|
146
|
+
controlPoints: view.manualLayout?.edges[edge.id]?.controlPoints || null,
|
|
173
147
|
headPoint: edge.headArrowPoint ?? null,
|
|
174
148
|
tailPoint: edge.tailArrowPoint ?? null,
|
|
175
149
|
stepNum: isDynamicView ? extractStep(edge.id) : null,
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import { invariant, type NonEmptyArray, nonNullable, type Point } from '@likec4/core'
|
|
2
2
|
import { Box } from '@mantine/core'
|
|
3
|
+
import { useIsomorphicLayoutEffect } from '@react-hookz/web'
|
|
3
4
|
import { assignInlineVars } from '@vanilla-extract/dynamic'
|
|
4
5
|
import type { EdgeProps, XYPosition } from '@xyflow/react'
|
|
5
|
-
import { EdgeLabelRenderer
|
|
6
|
+
import { EdgeLabelRenderer } from '@xyflow/react'
|
|
6
7
|
import { getNodePositionWithOrigin } from '@xyflow/system'
|
|
7
8
|
import clsx from 'clsx'
|
|
8
9
|
import { curveCatmullRomOpen, line as d3line } from 'd3-shape'
|
|
9
10
|
import { deepEqual as eq } from 'fast-equals'
|
|
10
|
-
import {
|
|
11
|
-
import { first, hasAtLeast,
|
|
12
|
-
import { useDiagramState } from '../../state'
|
|
11
|
+
import { memo, useRef, useState } from 'react'
|
|
12
|
+
import { first, hasAtLeast, isArray, isTruthy, last } from 'remeda'
|
|
13
|
+
import { useDiagramState, useDiagramStoreApi } from '../../state'
|
|
13
14
|
import { ZIndexes } from '../const'
|
|
14
|
-
import { useXYStoreApi } from '../hooks'
|
|
15
|
+
import { useXYFlow, useXYStoreApi } from '../hooks'
|
|
15
16
|
import { type XYFlowEdge } from '../types'
|
|
17
|
+
import { bezierControlPoints, toDomPrecision } from '../utils'
|
|
16
18
|
import * as edgesCss from './edges.css'
|
|
17
|
-
import {
|
|
19
|
+
import { getNodeIntersectionFromCenterToPoint } from './utils'
|
|
18
20
|
// import { getEdgeParams } from './utils'
|
|
19
21
|
|
|
20
22
|
// function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size = 8): string {
|
|
@@ -63,12 +65,20 @@ function bezierPath(bezierSpline: NonEmptyArray<Point>) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
// If points are within 3px, consider them the same
|
|
66
|
-
const
|
|
68
|
+
const isSame = (a: number, b: number) => {
|
|
67
69
|
return Math.abs(a - b) < 3.1
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
+
const isSamePoint = (a: XYPosition | Point, b: XYPosition | Point) => {
|
|
73
|
+
const [ax, ay] = isArray(a) ? a : [a.x, a.y]
|
|
74
|
+
const [bx, by] = isArray(b) ? b : [b.x, b.y]
|
|
75
|
+
return isSame(ax, bx) && isSame(ay, by)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sameControlPoints = (a: XYPosition[] | null, b: XYPosition[] | null) => {
|
|
79
|
+
if (a === b) return true
|
|
80
|
+
if (!a || !b || a.length !== b.length) return false
|
|
81
|
+
return a.every((ap, i) => isSamePoint(ap, b[i]!))
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>) => (
|
|
@@ -76,19 +86,19 @@ const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>)
|
|
|
76
86
|
&& prev.source === next.source
|
|
77
87
|
&& prev.target === next.target
|
|
78
88
|
&& eq(prev.selected ?? false, next.selected ?? false)
|
|
79
|
-
&&
|
|
80
|
-
&&
|
|
81
|
-
&&
|
|
82
|
-
&&
|
|
89
|
+
&& isSame(prev.sourceX, next.sourceX)
|
|
90
|
+
&& isSame(prev.sourceY, next.sourceY)
|
|
91
|
+
&& isSame(prev.targetX, next.targetX)
|
|
92
|
+
&& isSame(prev.targetY, next.targetY)
|
|
83
93
|
&& eq(prev.data.stepNum, next.data.stepNum)
|
|
94
|
+
&& sameControlPoints(prev.data.controlPoints, next.data.controlPoints)
|
|
84
95
|
&& eq(prev.data.edge, next.data.edge)
|
|
85
|
-
&& zip(prev.data.controlPoints, next.data.controlPoints).every(([a, b]) =>
|
|
86
|
-
isSamePoint(a[0], b[0]) && isSamePoint(a[1], b[1])
|
|
87
|
-
)
|
|
88
96
|
)
|
|
89
97
|
|
|
90
|
-
const curve = d3line<
|
|
98
|
+
const curve = d3line<XYPosition>()
|
|
91
99
|
.curve(curveCatmullRomOpen)
|
|
100
|
+
.x(d => d.x)
|
|
101
|
+
.y(d => d.y)
|
|
92
102
|
|
|
93
103
|
export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(function RelationshipEdgeR({
|
|
94
104
|
id,
|
|
@@ -99,26 +109,38 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
99
109
|
target,
|
|
100
110
|
interactionWidth
|
|
101
111
|
}) {
|
|
102
|
-
const
|
|
112
|
+
const diagramStore = useDiagramStoreApi()
|
|
113
|
+
const xyflowStore = useXYStoreApi()
|
|
114
|
+
const { isActive, isEditable, isEdgePathEditable, isHovered, isDimmed } = useDiagramState(s => ({
|
|
115
|
+
isEditable: s.readonly !== true,
|
|
116
|
+
isEdgePathEditable: s.readonly !== true && s.experimentalEdgeEditing === true,
|
|
117
|
+
isActive: s.focusedNodeId === source || s.focusedNodeId === target
|
|
118
|
+
|| (s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum),
|
|
119
|
+
isHovered: s.hoveredEdgeId === id,
|
|
120
|
+
isDimmed: s.dimmed.has(id)
|
|
121
|
+
}))
|
|
122
|
+
const { nodeLookup, edgeLookup } = xyflowStore.getState()
|
|
103
123
|
const sourceNode = nonNullable(nodeLookup.get(source)!, `source node ${source} not found`)
|
|
104
124
|
const targetNode = nonNullable(nodeLookup.get(target)!, `target node ${target} not found`)
|
|
105
125
|
|
|
106
|
-
const isModified =
|
|
107
|
-
||
|
|
126
|
+
const isModified = isEditable && (0
|
|
127
|
+
|| isTruthy(data.controlPoints)
|
|
128
|
+
|| !isSamePoint(sourceNode.internals.positionAbsolute, sourceNode.data.element.position)
|
|
129
|
+
|| !isSamePoint(targetNode.internals.positionAbsolute, targetNode.data.element.position))
|
|
108
130
|
|
|
109
|
-
const {
|
|
131
|
+
const {
|
|
132
|
+
label,
|
|
133
|
+
edge: {
|
|
134
|
+
points: diagramEdgePoints,
|
|
135
|
+
line = 'dashed',
|
|
136
|
+
color = 'gray',
|
|
137
|
+
...diagramEdge
|
|
138
|
+
}
|
|
139
|
+
} = data
|
|
110
140
|
|
|
111
|
-
|
|
141
|
+
let controlPoints = data.controlPoints ?? bezierControlPoints(data.edge)
|
|
112
142
|
|
|
113
|
-
const color = diagramEdge.color ?? 'gray'
|
|
114
143
|
const isStepEdge = data.stepNum !== null
|
|
115
|
-
const { isActive, isHovered, isDimmed } = useDiagramState(s => ({
|
|
116
|
-
isActive: s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum,
|
|
117
|
-
isHovered: s.hoveredEdgeId === id,
|
|
118
|
-
isDimmed: s.dimmed.has(id)
|
|
119
|
-
}))
|
|
120
|
-
|
|
121
|
-
const line = diagramEdge.line ?? 'dashed'
|
|
122
144
|
const isDotted = line === 'dotted'
|
|
123
145
|
const isDashed = isDotted || line === 'dashed'
|
|
124
146
|
|
|
@@ -129,45 +151,89 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
129
151
|
strokeDasharray = '8,10'
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
|
|
154
|
+
let labelX = label?.bbox.x ?? 0,
|
|
155
|
+
labelY = label?.bbox.y ?? 0
|
|
156
|
+
|
|
157
|
+
const [labelPos, setLabelPosition] = useState<XYPosition>({
|
|
158
|
+
x: labelX,
|
|
159
|
+
y: labelY
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
let edgePath: string
|
|
133
163
|
|
|
134
|
-
|
|
164
|
+
if (isModified) {
|
|
165
|
+
labelX = labelPos.x
|
|
166
|
+
labelY = labelPos.y
|
|
167
|
+
const sourceCenterPos = getNodePositionWithOrigin(sourceNode, [-0.5, -0.5])
|
|
168
|
+
const targetCenterPos = getNodePositionWithOrigin(targetNode, [-0.5, -0.5])
|
|
169
|
+
|
|
170
|
+
const points = [
|
|
171
|
+
sourceCenterPos.positionAbsolute,
|
|
172
|
+
getNodeIntersectionFromCenterToPoint(sourceNode, first(controlPoints)!),
|
|
173
|
+
...controlPoints,
|
|
174
|
+
getNodeIntersectionFromCenterToPoint(targetNode, last(controlPoints)!),
|
|
175
|
+
targetCenterPos.positionAbsolute
|
|
176
|
+
]
|
|
135
177
|
|
|
136
|
-
|
|
137
|
-
|
|
178
|
+
edgePath = nonNullable(curve(points))
|
|
179
|
+
} else {
|
|
180
|
+
edgePath = bezierPath(diagramEdgePoints)
|
|
138
181
|
// if (diagramEdge.tailArrowPoint) {
|
|
139
182
|
// edgePath = `M ${diagramEdge.tailArrowPoint[0]},${diagramEdge.tailArrowPoint[1]} L ${edgePath.substring(1)}`
|
|
140
183
|
// }
|
|
141
184
|
// if (diagramEdge.headArrowPoint) {
|
|
142
185
|
// edgePath += ` L ${diagramEdge.headArrowPoint[0]},${diagramEdge.headArrowPoint[1]}`
|
|
143
186
|
// }
|
|
144
|
-
|
|
145
|
-
labelY = data.label?.bbox.y ?? 0
|
|
146
|
-
} else {
|
|
147
|
-
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode)
|
|
187
|
+
}
|
|
148
188
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
189
|
+
const svgPathRef = useRef<SVGPathElement>(null)
|
|
190
|
+
useIsomorphicLayoutEffect(() => {
|
|
191
|
+
const path = svgPathRef.current
|
|
192
|
+
if (!path) return
|
|
193
|
+
const dompoint = path.getPointAtLength(path.getTotalLength() * 0.5)
|
|
194
|
+
const point = {
|
|
195
|
+
x: Math.round(dompoint.x),
|
|
196
|
+
y: Math.round(dompoint.y)
|
|
197
|
+
}
|
|
198
|
+
setLabelPosition(current => isSamePoint(current, point) ? current : point)
|
|
199
|
+
}, [edgePath])
|
|
160
200
|
|
|
161
|
-
|
|
162
|
-
const
|
|
201
|
+
const onControlPointerDown = (index: number, e: React.PointerEvent) => {
|
|
202
|
+
const { domNode } = xyflowStore.getState()
|
|
203
|
+
if (!domNode) return
|
|
204
|
+
const { xyflow } = diagramStore.getState()
|
|
205
|
+
e.stopPropagation()
|
|
206
|
+
let hasMoved = false
|
|
207
|
+
let pointer = { x: e.clientX, y: e.clientY }
|
|
208
|
+
const onPointerMove = (e: PointerEvent) => {
|
|
209
|
+
if (!isSamePoint(pointer, [e.clientX, e.clientY])) {
|
|
210
|
+
hasMoved = true
|
|
211
|
+
pointer = { x: e.clientX, y: e.clientY }
|
|
212
|
+
const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
|
|
213
|
+
const newControlPoints = controlPoints.slice()
|
|
214
|
+
newControlPoints[index] = {
|
|
215
|
+
x: toDomPrecision(x),
|
|
216
|
+
y: toDomPrecision(y)
|
|
217
|
+
}
|
|
218
|
+
xyflow.updateEdgeData(id, { controlPoints: newControlPoints })
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const onPointerUp = () => {
|
|
222
|
+
domNode.removeEventListener('pointermove', onPointerMove)
|
|
223
|
+
domNode.removeEventListener('pointerup', onPointerUp)
|
|
224
|
+
if (hasMoved) {
|
|
225
|
+
diagramStore.getState().triggerSaveManualLayout(xyflowStore)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
domNode.addEventListener('pointermove', onPointerMove)
|
|
229
|
+
domNode.addEventListener('pointerup', onPointerUp, { once: true })
|
|
230
|
+
}
|
|
163
231
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
[targetCenterPos.positionAbsolute.x, targetCenterPos.positionAbsolute.y]
|
|
170
|
-
])!
|
|
232
|
+
const marker = `url(#arrow-${id})`
|
|
233
|
+
let markerStart = diagramEdge.tailArrow ? marker : undefined
|
|
234
|
+
let markerEnd = diagramEdge.headArrow ? marker : undefined
|
|
235
|
+
if (isModified && diagramEdge.dir === 'back') {
|
|
236
|
+
;[markerStart, markerEnd] = [markerEnd, markerStart]
|
|
171
237
|
}
|
|
172
238
|
|
|
173
239
|
return (
|
|
@@ -185,7 +251,8 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
185
251
|
<marker
|
|
186
252
|
id={`arrow-${id}`}
|
|
187
253
|
viewBox="0 0 16 10"
|
|
188
|
-
|
|
254
|
+
// TODO: graphviz cut bezier path at arrow, we don't
|
|
255
|
+
refX={isModified ? '15' : '1'}
|
|
189
256
|
refY="5"
|
|
190
257
|
markerWidth="5"
|
|
191
258
|
markerHeight="4"
|
|
@@ -195,20 +262,37 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
195
262
|
<path d="M 0 0 L 16 5 L 0 10 z" stroke="context-stroke" fill="context-stroke" />
|
|
196
263
|
</marker>
|
|
197
264
|
</defs>
|
|
265
|
+
<path
|
|
266
|
+
className={clsx('react-flow__edge-path', edgesCss.edgePathBg)}
|
|
267
|
+
d={edgePath}
|
|
268
|
+
style={style}
|
|
269
|
+
strokeLinecap={'round'}
|
|
270
|
+
/>
|
|
271
|
+
<path
|
|
272
|
+
ref={svgPathRef}
|
|
273
|
+
className={clsx('react-flow__edge-path', edgesCss.cssEdgePath)}
|
|
274
|
+
d={edgePath}
|
|
275
|
+
style={style}
|
|
276
|
+
strokeLinecap={'round'}
|
|
277
|
+
strokeDasharray={strokeDasharray}
|
|
278
|
+
markerStart={markerStart}
|
|
279
|
+
markerEnd={markerEnd}
|
|
280
|
+
/>
|
|
198
281
|
</g>
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
{
|
|
282
|
+
<path
|
|
283
|
+
className={clsx('react-flow__edge-interaction')}
|
|
284
|
+
d={edgePath}
|
|
285
|
+
fill="none"
|
|
286
|
+
strokeOpacity={0}
|
|
287
|
+
strokeWidth={interactionWidth ?? 10}
|
|
288
|
+
/>
|
|
289
|
+
{isEdgePathEditable && controlPoints.map((p, i) => (
|
|
207
290
|
<circle
|
|
291
|
+
onPointerDown={e => onControlPointerDown(i, e)}
|
|
208
292
|
className={edgesCss.controlPoint}
|
|
209
293
|
key={i}
|
|
210
|
-
cx={p
|
|
211
|
-
cy={p
|
|
294
|
+
cx={p.x}
|
|
295
|
+
cy={p.y}
|
|
212
296
|
r={3}
|
|
213
297
|
/>
|
|
214
298
|
))}
|
|
@@ -223,8 +307,11 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
223
307
|
data-likec4-color={color}
|
|
224
308
|
style={{
|
|
225
309
|
...assignInlineVars({
|
|
226
|
-
[edgesCss.varLabelX]:
|
|
227
|
-
[edgesCss.varLabelY]:
|
|
310
|
+
[edgesCss.varLabelX]: isModified ? `calc(${labelX}px - 10%)` : `${labelX}px`,
|
|
311
|
+
[edgesCss.varLabelY]: isModified ? `${labelY - 5}px` : `${labelY}px`
|
|
312
|
+
}),
|
|
313
|
+
...(isEdgePathEditable && {
|
|
314
|
+
pointerEvents: 'none'
|
|
228
315
|
}),
|
|
229
316
|
...(data.label && {
|
|
230
317
|
maxWidth: data.label.bbox.width + 12
|
|
@@ -277,44 +364,3 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
277
364
|
</g>
|
|
278
365
|
)
|
|
279
366
|
}, isEqualProps)
|
|
280
|
-
|
|
281
|
-
const RelationshipPath = ({
|
|
282
|
-
edgePath,
|
|
283
|
-
interactionWidth,
|
|
284
|
-
strokeDasharray,
|
|
285
|
-
markerStart,
|
|
286
|
-
markerEnd,
|
|
287
|
-
style
|
|
288
|
-
}: {
|
|
289
|
-
edgePath: string
|
|
290
|
-
interactionWidth: number
|
|
291
|
-
strokeDasharray: string | undefined
|
|
292
|
-
markerStart: string | undefined
|
|
293
|
-
markerEnd: string | undefined
|
|
294
|
-
style: CSSProperties | undefined
|
|
295
|
-
}) => (
|
|
296
|
-
<>
|
|
297
|
-
<path
|
|
298
|
-
className={clsx('react-flow__edge-path', edgesCss.edgePathBg)}
|
|
299
|
-
d={edgePath}
|
|
300
|
-
style={style}
|
|
301
|
-
strokeLinecap={'round'}
|
|
302
|
-
/>
|
|
303
|
-
<path
|
|
304
|
-
className={clsx('react-flow__edge-path', edgesCss.cssEdgePath)}
|
|
305
|
-
d={edgePath}
|
|
306
|
-
style={style}
|
|
307
|
-
strokeLinecap={'round'}
|
|
308
|
-
strokeDasharray={strokeDasharray}
|
|
309
|
-
markerStart={markerStart}
|
|
310
|
-
markerEnd={markerEnd}
|
|
311
|
-
/>
|
|
312
|
-
<path
|
|
313
|
-
className={clsx('react-flow__edge-interaction')}
|
|
314
|
-
d={edgePath}
|
|
315
|
-
fill="none"
|
|
316
|
-
strokeOpacity={0}
|
|
317
|
-
strokeWidth={interactionWidth}
|
|
318
|
-
/>
|
|
319
|
-
</>
|
|
320
|
-
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { rem } from '@mantine/core'
|
|
2
2
|
import { createVar, fallbackVar, generateIdentifier, globalKeyframes, globalStyle, style } from '@vanilla-extract/css'
|
|
3
|
+
import { mantine } from '../../mantine.css'
|
|
3
4
|
import { vars, xyvars } from '../../theme.css'
|
|
4
5
|
|
|
5
6
|
const mixColor = createVar('mix-color')
|
|
@@ -15,13 +16,6 @@ export const container = style({
|
|
|
15
16
|
}
|
|
16
17
|
})
|
|
17
18
|
|
|
18
|
-
export const dimmed = style({})
|
|
19
|
-
|
|
20
|
-
globalStyle(`svg:has(${dimmed})`, {
|
|
21
|
-
filter: vars.filterDimmed,
|
|
22
|
-
transition: 'filter 800ms ease-out'
|
|
23
|
-
})
|
|
24
|
-
|
|
25
19
|
globalStyle(`:where([data-mantine-color-scheme="dark"]) ${container}`, {
|
|
26
20
|
vars: {
|
|
27
21
|
[mixColor]: `white`,
|
|
@@ -61,9 +55,16 @@ globalStyle(`:where(${isSelected}) ${container}[data-edge-hovered='true']`, {
|
|
|
61
55
|
globalStyle(`.react-flow__edges > svg`, {
|
|
62
56
|
mixBlendMode: 'screen'
|
|
63
57
|
})
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
58
|
+
|
|
59
|
+
export const dimmed = style({})
|
|
60
|
+
|
|
61
|
+
globalStyle(`.react-flow__edges > svg:has(${dimmed})`, {
|
|
62
|
+
opacity: 0.8,
|
|
63
|
+
transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
|
|
64
|
+
transitionDelay: '200ms',
|
|
65
|
+
filter: 'grayscale(0.85) blur(1px)',
|
|
66
|
+
willChange: 'opacity, filter'
|
|
67
|
+
})
|
|
67
68
|
|
|
68
69
|
export const edgePathBg = style({
|
|
69
70
|
strokeWidth: xyvars.edge.strokeWidth,
|
|
@@ -84,8 +85,7 @@ export const edgePathBg = style({
|
|
|
84
85
|
// To fix issue with marker not inheriting color from path - we need to create container
|
|
85
86
|
export const markerContext = style({
|
|
86
87
|
fill: xyvars.edge.stroke,
|
|
87
|
-
stroke: xyvars.edge.stroke
|
|
88
|
-
pointerEvents: 'none'
|
|
88
|
+
stroke: xyvars.edge.stroke
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
export const controlPoint = style({
|
|
@@ -94,12 +94,20 @@ export const controlPoint = style({
|
|
|
94
94
|
fillOpacity: 0.75,
|
|
95
95
|
strokeWidth: 1,
|
|
96
96
|
cursor: 'grab',
|
|
97
|
-
pointerEvents: '
|
|
97
|
+
pointerEvents: 'auto',
|
|
98
|
+
visibility: 'hidden',
|
|
99
|
+
':hover': {
|
|
100
|
+
stroke: mantine.colors.primaryColors.filledHover,
|
|
101
|
+
strokeWidth: 9,
|
|
102
|
+
transition: 'stroke 100ms ease-out, stroke-width 100ms ease-out'
|
|
103
|
+
},
|
|
98
104
|
selectors: {
|
|
99
|
-
[`:where(${isSelected}, [data-edge-
|
|
100
|
-
|
|
105
|
+
[`:where(${isSelected}, [data-edge-hovered='true']) &`]: {
|
|
106
|
+
visibility: 'visible',
|
|
107
|
+
transition: 'fill-opacity 150ms ease-out, stroke 150ms ease-out, stroke-width 150ms ease-out',
|
|
108
|
+
transitionDelay: '50ms',
|
|
101
109
|
fillOpacity: 1,
|
|
102
|
-
strokeWidth:
|
|
110
|
+
strokeWidth: 5
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
})
|
|
@@ -122,12 +130,12 @@ export const cssEdgePath = style({
|
|
|
122
130
|
[`:where([data-edge-hovered='true']) &`]: {
|
|
123
131
|
animationName: strokeKeyframes,
|
|
124
132
|
animationDelay: '350ms',
|
|
125
|
-
transition: '
|
|
133
|
+
transition: 'stroke,stroke-width 130ms ease-out'
|
|
126
134
|
},
|
|
127
135
|
[`:where(${isSelected}, [data-edge-active='true']) &`]: {
|
|
128
136
|
animationName: strokeKeyframes,
|
|
129
137
|
animationDelay: '0ms',
|
|
130
|
-
transition: '
|
|
138
|
+
transition: 'stroke,stroke-width 130ms ease-out'
|
|
131
139
|
},
|
|
132
140
|
[`:where([data-edge-dir='back']) &`]: {
|
|
133
141
|
animationDirection: 'reverse'
|
|
@@ -154,7 +162,7 @@ export const edgeLabel = style({
|
|
|
154
162
|
borderRadius: '3px',
|
|
155
163
|
transform: varTranslate,
|
|
156
164
|
vars: {
|
|
157
|
-
[varTranslate]: `translate(${fallbackVar(varLabelX, '50%')}, ${fallbackVar(varLabelY, '50%')})`
|
|
165
|
+
[varTranslate]: `translate(${fallbackVar(varLabelX, '-50%')}, ${fallbackVar(varLabelY, '-50%')})`
|
|
158
166
|
},
|
|
159
167
|
selectors: {
|
|
160
168
|
'&[data-edge-hovered="true"]': {
|
|
@@ -162,8 +170,11 @@ export const edgeLabel = style({
|
|
|
162
170
|
transform: `${varTranslate} scale(1.1)`
|
|
163
171
|
},
|
|
164
172
|
[`&:is(${dimmed})`]: {
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
opacity: 0.8,
|
|
174
|
+
transition: 'opacity 600ms ease-in-out, filter 600ms ease-in-out',
|
|
175
|
+
transitionDelay: '200ms',
|
|
176
|
+
filter: 'grayscale(0.85) blur(2px)',
|
|
177
|
+
willChange: 'opacity, filter'
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
180
|
})
|
|
@@ -5,7 +5,10 @@ import type { InternalXYFlowNode } from '../types'
|
|
|
5
5
|
|
|
6
6
|
// this helper function returns the intersection point
|
|
7
7
|
// of the line between the center of the intersectionNode and the target node
|
|
8
|
-
export function getNodeIntersectionFromCenterToPoint(
|
|
8
|
+
export function getNodeIntersectionFromCenterToPoint(
|
|
9
|
+
intersectionNode: InternalXYFlowNode,
|
|
10
|
+
{ x: x1, y: y1 }: XYPosition
|
|
11
|
+
) {
|
|
9
12
|
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
|
10
13
|
const {
|
|
11
14
|
width: intersectionNodeWidth,
|
|
@@ -30,7 +33,7 @@ export function getNodeIntersectionFromCenterToPoint(intersectionNode: InternalX
|
|
|
30
33
|
const x = w * (xx3 + yy3) + x2
|
|
31
34
|
const y = h * (-xx3 + yy3) + y2
|
|
32
35
|
|
|
33
|
-
return
|
|
36
|
+
return { x, y }
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
// this helper function returns the intersection point
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { invariant, isAncestor, nonNullable } from '@likec4/core'
|
|
2
2
|
import { Expression, Expression as Expr, Operator, Solver, Strength, Variable } from '@lume/kiwi'
|
|
3
|
+
import { useDebouncedCallback } from '@react-hookz/web'
|
|
3
4
|
import type { InternalNode, ReactFlowProps, XYPosition } from '@xyflow/react'
|
|
4
5
|
import { getNodeDimensions } from '@xyflow/system'
|
|
5
6
|
import { useMemo, useRef } from 'react'
|
|
6
7
|
import { isNullish } from 'remeda'
|
|
7
8
|
import { useDiagramStoreApi } from '../../state'
|
|
8
9
|
import { type XYFlowInstance, XYFlowNode } from '../types'
|
|
10
|
+
import { isSamePoint } from '../utils'
|
|
9
11
|
import { useXYStoreApi, type XYStoreApi } from './useXYFlow'
|
|
10
12
|
|
|
11
13
|
abstract class Rect {
|
|
@@ -273,8 +275,17 @@ export function useLayoutConstraints(): LayoutConstraints {
|
|
|
273
275
|
const xyflowApi = useXYStoreApi()
|
|
274
276
|
const solverRef = useRef<ReturnType<typeof createLayoutConstraints>>()
|
|
275
277
|
|
|
278
|
+
const dragStartedAt = useRef({
|
|
279
|
+
x: 0,
|
|
280
|
+
y: 0
|
|
281
|
+
})
|
|
282
|
+
|
|
276
283
|
return useMemo((): LayoutConstraints => ({
|
|
277
|
-
onNodeDragStart: (
|
|
284
|
+
onNodeDragStart: (event, xynode) => {
|
|
285
|
+
dragStartedAt.current = {
|
|
286
|
+
x: event.clientX,
|
|
287
|
+
y: event.clientY
|
|
288
|
+
}
|
|
278
289
|
const { xyflow } = diagramApi.getState()
|
|
279
290
|
solverRef.current = createLayoutConstraints(xyflow, xyflowApi, xynode.id)
|
|
280
291
|
},
|
|
@@ -282,8 +293,11 @@ export function useLayoutConstraints(): LayoutConstraints {
|
|
|
282
293
|
invariant(solverRef.current, 'solverRef.current should be defined')
|
|
283
294
|
solverRef.current?.onNodeDrag(xynode)
|
|
284
295
|
},
|
|
285
|
-
onNodeDragStop: (
|
|
296
|
+
onNodeDragStop: (event, _xynode) => {
|
|
297
|
+
if (!isSamePoint(dragStartedAt.current, { x: event.clientX, y: event.clientY })) {
|
|
298
|
+
diagramApi.getState().triggerSaveManualLayout(xyflowApi)
|
|
299
|
+
}
|
|
286
300
|
solverRef.current = undefined
|
|
287
301
|
}
|
|
288
|
-
}), [xyflowApi])
|
|
302
|
+
}), [xyflowApi, diagramApi])
|
|
289
303
|
}
|