@likec4/diagram 1.6.1 → 1.7.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 +10 -11
- package/dist/hooks/useUpdateEffect.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +572 -464
- package/dist/state/DiagramContext.d.ts +746 -181
- package/dist/state/diagram-to-xyflow.d.ts +10 -0
- package/dist/state/diagramStore.d.ts +782 -202
- package/dist/style.css +273 -275
- package/dist/types.d.ts +20 -0
- package/dist/xyflow/EdgeMarkers.d.ts +1 -0
- package/dist/xyflow/FitviewOnDiagramChange.d.ts +1 -1
- package/dist/xyflow/XYFlow.d.ts +1 -4
- package/dist/xyflow/XYFlowEvents.d.ts +41 -12
- package/dist/xyflow/XYFlowInner.d.ts +0 -1
- package/dist/xyflow/edges/RelationshipEdge.d.ts +3 -3
- package/dist/xyflow/hooks/index.d.ts +0 -1
- package/dist/xyflow/hooks/useXYFlow.d.ts +27 -27
- package/dist/xyflow/types.d.ts +11 -19
- package/dist/xyflow/utils.d.ts +1 -0
- package/package.json +6 -6
- package/src/LikeC4Diagram.props.ts +17 -13
- package/src/LikeC4Diagram.tsx +62 -96
- package/src/globals.css.ts +10 -6
- package/src/hooks/useUpdateEffect.ts +16 -1
- package/src/index.ts +1 -0
- package/src/state/DiagramContext.tsx +19 -27
- package/src/{xyflow → state}/diagram-to-xyflow.ts +19 -18
- package/src/state/diagramStore.ts +404 -192
- package/src/types.ts +22 -0
- package/src/ui/BackwardForwardPanel.tsx +24 -7
- package/src/xyflow/EdgeMarkers.tsx +2 -0
- package/src/xyflow/FitviewOnDiagramChange.tsx +42 -75
- package/src/xyflow/SelectEdgesOnNodeFocus.tsx +1 -1
- package/src/xyflow/XYFlow.tsx +57 -32
- package/src/xyflow/XYFlowEvents.tsx +58 -10
- package/src/xyflow/XYFlowInner.tsx +8 -8
- package/src/xyflow/edges/RelationshipEdge.tsx +43 -29
- package/src/xyflow/edges/edges.css.ts +3 -3
- package/src/xyflow/edges/utils.ts +0 -1
- package/src/xyflow/hooks/index.ts +0 -1
- package/src/xyflow/hooks/useLayoutConstraints.ts +42 -65
- package/src/xyflow/nodes/element/ElementNode.tsx +0 -4
- package/src/xyflow/nodes/element/element.css.ts +8 -6
- package/src/xyflow/types.ts +14 -32
- package/src/xyflow/utils.ts +4 -0
- package/dist/xyflow/SyncWithDiagram.d.ts +0 -4
- package/dist/xyflow/diagram-to-xyflow.d.ts +0 -7
- package/src/xyflow/SyncWithDiagram.tsx +0 -85
|
@@ -7,18 +7,18 @@ import {
|
|
|
7
7
|
type RelationshipArrowType
|
|
8
8
|
} from '@likec4/core'
|
|
9
9
|
import { Box } from '@mantine/core'
|
|
10
|
-
import {
|
|
10
|
+
import { useDebouncedEffect } from '@react-hookz/web'
|
|
11
11
|
import { assignInlineVars } from '@vanilla-extract/dynamic'
|
|
12
12
|
import type { EdgeProps, XYPosition } from '@xyflow/react'
|
|
13
13
|
import { EdgeLabelRenderer } from '@xyflow/react'
|
|
14
14
|
import clsx from 'clsx'
|
|
15
15
|
import { curveCatmullRomOpen, line as d3line } from 'd3-shape'
|
|
16
16
|
import { deepEqual, deepEqual as eq } from 'fast-equals'
|
|
17
|
-
import { memo, type PointerEvent as ReactPointerEvent, useRef, useState } from 'react'
|
|
17
|
+
import { memo, type PointerEvent as ReactPointerEvent, useEffect, useRef, useState } from 'react'
|
|
18
18
|
import { first, hasAtLeast, isArray, isTruthy, last } from 'remeda'
|
|
19
19
|
import { useDiagramState, useDiagramStoreApi } from '../../state/useDiagramStore'
|
|
20
20
|
import { ZIndexes } from '../const'
|
|
21
|
-
import { EdgeMarkers } from '../EdgeMarkers'
|
|
21
|
+
import { EdgeMarkers, type EdgeMarkerType } from '../EdgeMarkers'
|
|
22
22
|
import { useXYStoreApi } from '../hooks/useXYFlow'
|
|
23
23
|
import { type RelationshipData, type XYFlowEdge } from '../types'
|
|
24
24
|
import { bezierControlPoints } from '../utils'
|
|
@@ -56,7 +56,7 @@ import { getNodeIntersectionFromCenterToPoint } from './utils'
|
|
|
56
56
|
// return path + segment
|
|
57
57
|
// }
|
|
58
58
|
|
|
59
|
-
const toMarker = (arrowType?: RelationshipArrowType) => {
|
|
59
|
+
const toMarker = (arrowType?: RelationshipArrowType): EdgeMarkerType | undefined => {
|
|
60
60
|
if (!arrowType || arrowType === 'none') {
|
|
61
61
|
return undefined
|
|
62
62
|
}
|
|
@@ -124,6 +124,7 @@ const isEqualProps = (prev: EdgeProps<XYFlowEdge>, next: EdgeProps<XYFlowEdge>)
|
|
|
124
124
|
&& isSame(prev.targetX, next.targetX)
|
|
125
125
|
&& isSame(prev.targetY, next.targetY)
|
|
126
126
|
&& eq(prev.data.stepNum, next.data.stepNum)
|
|
127
|
+
&& eq(prev.data.label, next.data.label)
|
|
127
128
|
&& sameControlPoints(prev.data.controlPoints, next.data.controlPoints)
|
|
128
129
|
&& eq(prev.data.edge, next.data.edge)
|
|
129
130
|
)
|
|
@@ -149,8 +150,7 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
149
150
|
const [isControlPointDragging, setIsControlPointDragging] = useState(false)
|
|
150
151
|
const diagramStore = useDiagramStoreApi()
|
|
151
152
|
const xyflowStore = useXYStoreApi()
|
|
152
|
-
const { isActive,
|
|
153
|
-
isEditable: s.readonly !== true,
|
|
153
|
+
const { isActive, isEdgePathEditable, isHovered, isDimmed } = useDiagramState(s => ({
|
|
154
154
|
isEdgePathEditable: s.readonly !== true && s.experimentalEdgeEditing === true,
|
|
155
155
|
isActive: s.focusedNodeId === source || s.focusedNodeId === target
|
|
156
156
|
|| (s.activeDynamicViewStep !== null && s.activeDynamicViewStep === data.stepNum),
|
|
@@ -161,10 +161,9 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
161
161
|
const sourceNode = nonNullable(nodeLookup.get(source)!, `source node ${source} not found`)
|
|
162
162
|
const targetNode = nonNullable(nodeLookup.get(target)!, `target node ${target} not found`)
|
|
163
163
|
|
|
164
|
-
const isModified =
|
|
165
|
-
|| isTruthy(data.controlPoints)
|
|
164
|
+
const isModified = isTruthy(data.controlPoints)
|
|
166
165
|
|| !isSamePoint(sourceNode.internals.positionAbsolute, sourceNode.data.element.position)
|
|
167
|
-
|| !isSamePoint(targetNode.internals.positionAbsolute, targetNode.data.element.position)
|
|
166
|
+
|| !isSamePoint(targetNode.internals.positionAbsolute, targetNode.data.element.position)
|
|
168
167
|
|
|
169
168
|
const {
|
|
170
169
|
label,
|
|
@@ -172,6 +171,7 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
172
171
|
points: diagramEdgePoints,
|
|
173
172
|
line = 'dashed',
|
|
174
173
|
color = 'gray',
|
|
174
|
+
labelBBox,
|
|
175
175
|
...diagramEdge
|
|
176
176
|
}
|
|
177
177
|
} = data
|
|
@@ -189,12 +189,12 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
189
189
|
strokeDasharray = '8,10'
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
let labelX =
|
|
193
|
-
labelY =
|
|
192
|
+
let labelX = labelBBox?.x ?? 0,
|
|
193
|
+
labelY = labelBBox?.y ?? 0
|
|
194
194
|
|
|
195
195
|
const [labelPos, setLabelPosition] = useState<XYPosition>({
|
|
196
|
-
x: labelX,
|
|
197
|
-
y: labelY
|
|
196
|
+
x: label?.bbox.x ?? labelX,
|
|
197
|
+
y: label?.bbox.y ?? labelY
|
|
198
198
|
})
|
|
199
199
|
|
|
200
200
|
let edgePath: string
|
|
@@ -224,16 +224,10 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
224
224
|
edgePath = nonNullable(curve(points))
|
|
225
225
|
} else {
|
|
226
226
|
edgePath = bezierPath(diagramEdgePoints)
|
|
227
|
-
// if (diagramEdge.tailArrowPoint) {
|
|
228
|
-
// edgePath = `M ${diagramEdge.tailArrowPoint[0]},${diagramEdge.tailArrowPoint[1]} L ${edgePath.substring(1)}`
|
|
229
|
-
// }
|
|
230
|
-
// if (diagramEdge.headArrowPoint) {
|
|
231
|
-
// edgePath += ` L ${diagramEdge.headArrowPoint[0]},${diagramEdge.headArrowPoint[1]}`
|
|
232
|
-
// }
|
|
233
227
|
}
|
|
234
228
|
|
|
235
229
|
const svgPathRef = useRef<SVGPathElement>(null)
|
|
236
|
-
|
|
230
|
+
useEffect(() => {
|
|
237
231
|
const path = svgPathRef.current
|
|
238
232
|
if (!path) return
|
|
239
233
|
const dompoint = path.getPointAtLength(path.getTotalLength() * 0.5)
|
|
@@ -244,6 +238,26 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
244
238
|
setLabelPosition(current => isSamePoint(current, point) ? current : point)
|
|
245
239
|
}, [edgePath])
|
|
246
240
|
|
|
241
|
+
useDebouncedEffect(
|
|
242
|
+
() => {
|
|
243
|
+
if (!label || label.bbox.x === labelPos.x && label.bbox.y === labelPos.y) {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
diagramStore.getState().xyflow.updateEdgeData(id, {
|
|
247
|
+
label: {
|
|
248
|
+
...label,
|
|
249
|
+
bbox: {
|
|
250
|
+
...label.bbox,
|
|
251
|
+
...labelPos
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
},
|
|
256
|
+
[labelPos],
|
|
257
|
+
100,
|
|
258
|
+
300
|
|
259
|
+
)
|
|
260
|
+
|
|
247
261
|
const onControlPointerDown = (index: number, e: ReactPointerEvent<SVGCircleElement>) => {
|
|
248
262
|
const { domNode } = xyflowStore.getState()
|
|
249
263
|
if (!domNode || e.pointerType !== 'mouse') {
|
|
@@ -263,15 +277,13 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
263
277
|
if (e.button !== 0) {
|
|
264
278
|
return
|
|
265
279
|
}
|
|
280
|
+
const wasCanceled = diagramStore.getState().cancelSaveManualLayout()
|
|
266
281
|
e.stopPropagation()
|
|
267
282
|
let hasMoved = false
|
|
268
283
|
let pointer = { x: e.clientX, y: e.clientY }
|
|
269
284
|
const onPointerMove = (e: PointerEvent) => {
|
|
270
285
|
if (!isSamePoint(pointer, [e.clientX, e.clientY])) {
|
|
271
286
|
setIsControlPointDragging(true)
|
|
272
|
-
if (!hasMoved) {
|
|
273
|
-
diagramStore.getState().cancelSaveManualLayout()
|
|
274
|
-
}
|
|
275
287
|
hasMoved = true
|
|
276
288
|
pointer = { x: e.clientX, y: e.clientY }
|
|
277
289
|
const { x, y } = xyflow.screenToFlowPosition(pointer, { snapToGrid: false })
|
|
@@ -284,12 +296,12 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
284
296
|
}
|
|
285
297
|
}
|
|
286
298
|
const onPointerUp = () => {
|
|
287
|
-
setIsControlPointDragging(false)
|
|
288
299
|
domNode.removeEventListener('pointermove', onPointerMove)
|
|
289
300
|
domNode.removeEventListener('pointerup', onPointerUp)
|
|
290
|
-
if (hasMoved) {
|
|
291
|
-
diagramStore.getState().
|
|
301
|
+
if (hasMoved || wasCanceled) {
|
|
302
|
+
diagramStore.getState().scheduleSaveManualLayout()
|
|
292
303
|
}
|
|
304
|
+
setIsControlPointDragging(false)
|
|
293
305
|
}
|
|
294
306
|
domNode.addEventListener('pointermove', onPointerMove)
|
|
295
307
|
domNode.addEventListener('pointerup', onPointerUp, { once: true })
|
|
@@ -304,13 +316,15 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
304
316
|
const MarkerStart = markerStartName ? EdgeMarkers[markerStartName] : null
|
|
305
317
|
const MarkerEnd = markerEndName ? EdgeMarkers[markerEndName] : null
|
|
306
318
|
|
|
319
|
+
const labelZIndex = 1 + (isHovered ? ZIndexes.Element : (edgeLookup.get(id)!.zIndex ?? ZIndexes.Edge))
|
|
320
|
+
|
|
307
321
|
return (
|
|
308
322
|
<g
|
|
309
|
-
className={clsx(
|
|
323
|
+
className={clsx(
|
|
310
324
|
edgesCss.container,
|
|
311
325
|
isDimmed && edgesCss.dimmed,
|
|
312
326
|
isControlPointDragging && edgesCss.controlDragging
|
|
313
|
-
|
|
327
|
+
)}
|
|
314
328
|
data-likec4-color={color}
|
|
315
329
|
data-edge-dir={diagramEdge.dir}
|
|
316
330
|
data-edge-active={isActive}
|
|
@@ -375,7 +389,7 @@ export const RelationshipEdge = /* @__PURE__ */ memo<EdgeProps<XYFlowEdge>>(func
|
|
|
375
389
|
selected: selected ?? false,
|
|
376
390
|
stepNum: data.stepNum,
|
|
377
391
|
label: data.label,
|
|
378
|
-
zIndex:
|
|
392
|
+
zIndex: labelZIndex,
|
|
379
393
|
isHovered,
|
|
380
394
|
isActive,
|
|
381
395
|
isStepEdge
|
|
@@ -97,7 +97,7 @@ export const controlPoint = style({
|
|
|
97
97
|
stroke: xyvars.edge.stroke,
|
|
98
98
|
fillOpacity: 0.75,
|
|
99
99
|
strokeWidth: 1,
|
|
100
|
-
cursor: '
|
|
100
|
+
cursor: 'grab',
|
|
101
101
|
pointerEvents: 'auto',
|
|
102
102
|
visibility: 'hidden',
|
|
103
103
|
':hover': {
|
|
@@ -117,11 +117,11 @@ export const controlPoint = style({
|
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
export const controlDragging = style({
|
|
120
|
-
cursor: '
|
|
120
|
+
cursor: 'grabbing'
|
|
121
121
|
})
|
|
122
122
|
|
|
123
123
|
globalStyle(`${controlDragging} *`, {
|
|
124
|
-
cursor: '
|
|
124
|
+
cursor: 'grabbing !important'
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
const strokeKeyframes = keyframes({
|
|
@@ -1,13 +1,12 @@
|
|
|
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 type { InternalNode, ReactFlowProps, XYPosition } from '@xyflow/react'
|
|
3
|
+
import type { InternalNode, NodeChange, ReactFlowProps, XYPosition } from '@xyflow/react'
|
|
4
4
|
import { getNodeDimensions } from '@xyflow/system'
|
|
5
5
|
import { useMemo, useRef } from 'react'
|
|
6
6
|
import { isNullish } from 'remeda'
|
|
7
7
|
import { useDiagramStoreApi } from '../../state/useDiagramStore'
|
|
8
|
-
import type {
|
|
9
|
-
import {
|
|
10
|
-
import { useXYStoreApi, type XYStoreApi } from './useXYFlow'
|
|
8
|
+
import type { XYFlowNode } from '../types'
|
|
9
|
+
import { type XYStoreApi } from './useXYFlow'
|
|
11
10
|
|
|
12
11
|
abstract class Rect {
|
|
13
12
|
id!: string
|
|
@@ -81,28 +80,28 @@ class Compound extends Rect {
|
|
|
81
80
|
)
|
|
82
81
|
const { weak } = Strength
|
|
83
82
|
this.solver.createConstraint(leftPadding, Operator.Ge, 40)
|
|
84
|
-
this.solver.createConstraint(leftPadding, Operator.Eq,
|
|
83
|
+
this.solver.createConstraint(leftPadding, Operator.Eq, 41, weak)
|
|
85
84
|
|
|
86
85
|
const topPadding = new Expr(
|
|
87
86
|
rect.minY,
|
|
88
87
|
[-1, this.minY]
|
|
89
88
|
)
|
|
90
|
-
this.solver.createConstraint(topPadding, Operator.Ge,
|
|
91
|
-
this.solver.createConstraint(topPadding, Operator.Eq,
|
|
89
|
+
this.solver.createConstraint(topPadding, Operator.Ge, 54)
|
|
90
|
+
this.solver.createConstraint(topPadding, Operator.Eq, 55, weak)
|
|
92
91
|
|
|
93
92
|
const rightPadding = new Expr(
|
|
94
93
|
this.maxX,
|
|
95
94
|
[-1, rect.maxX]
|
|
96
95
|
)
|
|
97
96
|
this.solver.createConstraint(rightPadding, Operator.Ge, 40)
|
|
98
|
-
this.solver.createConstraint(rightPadding, Operator.Eq,
|
|
97
|
+
this.solver.createConstraint(rightPadding, Operator.Eq, 41, weak)
|
|
99
98
|
|
|
100
99
|
const bottomPadding = new Expr(
|
|
101
100
|
this.maxY,
|
|
102
101
|
[-1, rect.maxY]
|
|
103
102
|
)
|
|
104
103
|
this.solver.createConstraint(bottomPadding, Operator.Ge, 40)
|
|
105
|
-
this.solver.createConstraint(bottomPadding, Operator.Eq,
|
|
104
|
+
this.solver.createConstraint(bottomPadding, Operator.Eq, 41, weak)
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
107
|
|
|
@@ -164,7 +163,6 @@ class Leaf extends Rect {
|
|
|
164
163
|
}
|
|
165
164
|
|
|
166
165
|
function createLayoutConstraints(
|
|
167
|
-
xyflow: XYFlowInstance,
|
|
168
166
|
xyflowApi: XYStoreApi,
|
|
169
167
|
draggingNodeId: string
|
|
170
168
|
) {
|
|
@@ -206,40 +204,29 @@ function createLayoutConstraints(
|
|
|
206
204
|
solver.updateVariables()
|
|
207
205
|
solver.maxIterations = 1000
|
|
208
206
|
|
|
207
|
+
const rectsToUpdate = [...rects.values()].filter(r => r.id !== draggingNodeId)
|
|
208
|
+
|
|
209
209
|
function updateXYFlowNodes() {
|
|
210
210
|
solver.updateVariables()
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
position: rect.position,
|
|
228
|
-
width: dimensions.width,
|
|
229
|
-
height: dimensions.height,
|
|
230
|
-
measured: dimensions
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (dimensions.width !== n.width || dimensions.height !== n.height) {
|
|
234
|
-
return {
|
|
235
|
-
...n,
|
|
236
|
-
width: dimensions.width,
|
|
237
|
-
height: dimensions.height,
|
|
238
|
-
measured: dimensions
|
|
239
|
-
}
|
|
211
|
+
xyflowApi.getState().triggerNodeChanges(
|
|
212
|
+
rectsToUpdate.reduce((acc, r) => {
|
|
213
|
+
acc.push({
|
|
214
|
+
id: r.id,
|
|
215
|
+
type: 'position',
|
|
216
|
+
dragging: false,
|
|
217
|
+
position: r.position,
|
|
218
|
+
positionAbsolute: r.positionAbsolute
|
|
219
|
+
})
|
|
220
|
+
if (r instanceof Compound) {
|
|
221
|
+
acc.push({
|
|
222
|
+
id: r.id,
|
|
223
|
+
type: 'dimensions',
|
|
224
|
+
setAttributes: true,
|
|
225
|
+
dimensions: r.dimensions
|
|
226
|
+
})
|
|
240
227
|
}
|
|
241
|
-
return
|
|
242
|
-
})
|
|
228
|
+
return acc
|
|
229
|
+
}, [] as NodeChange<XYFlowNode>[])
|
|
243
230
|
)
|
|
244
231
|
}
|
|
245
232
|
|
|
@@ -249,13 +236,16 @@ function createLayoutConstraints(
|
|
|
249
236
|
* Move the editing node to the given position.
|
|
250
237
|
*/
|
|
251
238
|
function onNodeDrag(xynode: XYFlowNode) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
solver.suggestValue(rect.minY, Math.ceil(pos.y))
|
|
239
|
+
if (rectsToUpdate.length === 0) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
256
242
|
animationFrameId ??= requestAnimationFrame(() => {
|
|
257
|
-
updateXYFlowNodes()
|
|
258
243
|
animationFrameId = null
|
|
244
|
+
const pos = nodeLookup.get(xynode.id)!.internals.positionAbsolute
|
|
245
|
+
const rect = nonNullable(rects.get(xynode.id))
|
|
246
|
+
solver.suggestValue(rect.minX, Math.ceil(pos.x))
|
|
247
|
+
solver.suggestValue(rect.minY, Math.ceil(pos.y))
|
|
248
|
+
updateXYFlowNodes()
|
|
259
249
|
})
|
|
260
250
|
}
|
|
261
251
|
|
|
@@ -271,33 +261,20 @@ type LayoutConstraints = Required<Pick<ReactFlowProps<XYFlowNode>, 'onNodeDragSt
|
|
|
271
261
|
*/
|
|
272
262
|
export function useLayoutConstraints(): LayoutConstraints {
|
|
273
263
|
const diagramApi = useDiagramStoreApi()
|
|
274
|
-
const xyflowApi = useXYStoreApi()
|
|
275
264
|
const solverRef = useRef<ReturnType<typeof createLayoutConstraints>>()
|
|
276
|
-
|
|
277
|
-
const dragStartedAt = useRef({
|
|
278
|
-
x: 0,
|
|
279
|
-
y: 0
|
|
280
|
-
})
|
|
281
|
-
|
|
282
265
|
return useMemo((): LayoutConstraints => ({
|
|
283
|
-
onNodeDragStart: (
|
|
284
|
-
|
|
285
|
-
x: event.clientX,
|
|
286
|
-
y: event.clientY
|
|
287
|
-
}
|
|
288
|
-
const { xyflow, cancelSaveManualLayout } = diagramApi.getState()
|
|
266
|
+
onNodeDragStart: (_event, xynode) => {
|
|
267
|
+
const { cancelSaveManualLayout, xystore } = diagramApi.getState()
|
|
289
268
|
cancelSaveManualLayout()
|
|
290
|
-
solverRef.current = createLayoutConstraints(
|
|
269
|
+
solverRef.current = createLayoutConstraints(xystore, xynode.id)
|
|
291
270
|
},
|
|
292
271
|
onNodeDrag: (_event, xynode) => {
|
|
293
272
|
invariant(solverRef.current, 'solverRef.current should be defined')
|
|
294
273
|
solverRef.current?.onNodeDrag(xynode)
|
|
295
274
|
},
|
|
296
|
-
onNodeDragStop: (
|
|
297
|
-
|
|
298
|
-
diagramApi.getState().triggerSaveManualLayout()
|
|
299
|
-
}
|
|
275
|
+
onNodeDragStop: (_event, _xynode) => {
|
|
276
|
+
diagramApi.getState().scheduleSaveManualLayout()
|
|
300
277
|
solverRef.current = undefined
|
|
301
278
|
}
|
|
302
|
-
}), [
|
|
279
|
+
}), [diagramApi])
|
|
303
280
|
}
|
|
@@ -96,10 +96,6 @@ export const ElementNodeMemo = memo<ElementNodeProps>(function ElementNode({
|
|
|
96
96
|
isHovercards: s.showElementLinks,
|
|
97
97
|
hasOnNavigateTo: !!s.onNavigateTo
|
|
98
98
|
}))
|
|
99
|
-
// const diagramState = useDiagramState()
|
|
100
|
-
// const isNodeInteractive = diagramState.isNodeInteractive
|
|
101
|
-
// const isHovercards = diagramState.disableHovercards !== true
|
|
102
|
-
// const isNavigable = diagramState.hasOnNavigateTo && !!element.navigateTo
|
|
103
99
|
const isNavigable = hasOnNavigateTo && !!element.navigateTo
|
|
104
100
|
|
|
105
101
|
const w = toDomPrecision(width ?? element.width)
|
|
@@ -197,14 +197,15 @@ export const elementDataContainer = style({
|
|
|
197
197
|
paddingTop: 32,
|
|
198
198
|
paddingBottom: 28
|
|
199
199
|
},
|
|
200
|
-
[`&:is(${hasIcon})`]: {
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
// [`&:is(${hasIcon})`]: {
|
|
201
|
+
// paddingLeft: 40,
|
|
202
|
+
// paddingRight: 20
|
|
203
|
+
// },
|
|
203
204
|
[`${container}:not(:is([data-likec4-shape="queue"],[data-likec4-shape="mobile"])) &:is(${hasIcon})`]: {
|
|
204
|
-
paddingLeft:
|
|
205
|
+
paddingLeft: 24,
|
|
206
|
+
paddingRight: 18
|
|
205
207
|
},
|
|
206
208
|
[`&:has(${description}, ${technology})`]: {
|
|
207
|
-
alignItems: 'flex-start',
|
|
208
209
|
gap: rem(16),
|
|
209
210
|
vars: {
|
|
210
211
|
[iconSize]: '60px'
|
|
@@ -247,7 +248,8 @@ export const elementIcon = style({
|
|
|
247
248
|
display: 'flex',
|
|
248
249
|
alignItems: 'center',
|
|
249
250
|
justifyContent: 'center',
|
|
250
|
-
mixBlendMode: 'hard-light'
|
|
251
|
+
mixBlendMode: 'hard-light',
|
|
252
|
+
alignSelf: 'flex-start'
|
|
251
253
|
})
|
|
252
254
|
globalStyle(`${elementIcon} svg, ${elementIcon} img`, {
|
|
253
255
|
width: '100%',
|
package/src/xyflow/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BBox, DiagramEdge, DiagramNode, Fqn,
|
|
1
|
+
import type { BBox, DiagramEdge, DiagramNode, Fqn, XYPoint } from '@likec4/core'
|
|
2
2
|
import type {
|
|
3
3
|
Edge,
|
|
4
4
|
EdgeMouseHandler,
|
|
@@ -13,38 +13,23 @@ import type {
|
|
|
13
13
|
import { isNode } from '@xyflow/react'
|
|
14
14
|
import type { MouseEvent as ReactMouseEvent } from 'react'
|
|
15
15
|
import { isTruthy } from 'remeda'
|
|
16
|
-
import type {
|
|
16
|
+
import type { Simplify } from 'type-fest'
|
|
17
17
|
|
|
18
|
-
export type
|
|
18
|
+
export type ElementXYFlowNode = Node<{
|
|
19
19
|
fqn: Fqn
|
|
20
20
|
element: DiagramNode
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type TypedXYFlowNode<D extends Record<string, unknown>, T extends string> = SetReadonly<
|
|
24
|
-
SetRequired<Node<D, T>, 'type'>,
|
|
25
|
-
'id' | 'type'
|
|
26
|
-
>
|
|
27
|
-
type TypedXYFlowEdge<D extends Record<string, unknown>, T extends string> = SetReadonly<
|
|
28
|
-
SetRequired<Edge<D, T>, 'type' | 'data'>,
|
|
29
|
-
'id' | 'type'
|
|
30
|
-
>
|
|
31
|
-
|
|
32
|
-
export type ElementXYFlowNode = TypedXYFlowNode<ElementNodeData, 'element'>
|
|
21
|
+
}, 'element'>
|
|
33
22
|
|
|
34
|
-
export type
|
|
23
|
+
export type CompoundXYFlowNode = Node<{
|
|
35
24
|
fqn: Fqn
|
|
36
25
|
element: DiagramNode
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type CompoundXYFlowNode = TypedXYFlowNode<CompoundNodeData, 'compound'>
|
|
26
|
+
}, 'compound'>
|
|
40
27
|
|
|
41
28
|
export type XYFlowNode = ElementXYFlowNode | CompoundXYFlowNode
|
|
42
29
|
|
|
43
30
|
export type InternalXYFlowNode = InternalNode<XYFlowNode>
|
|
44
31
|
|
|
45
32
|
export namespace XYFlowNode {
|
|
46
|
-
export type Data = ElementNodeData | CompoundNodeData
|
|
47
|
-
|
|
48
33
|
export function isCompound(node: unknown): node is CompoundXYFlowNode {
|
|
49
34
|
return isNode(node) && node.type === 'compound'
|
|
50
35
|
}
|
|
@@ -57,12 +42,8 @@ export namespace XYFlowNode {
|
|
|
57
42
|
|
|
58
43
|
export type RelationshipData = {
|
|
59
44
|
edge: DiagramEdge
|
|
60
|
-
// to replace edges on view change
|
|
61
|
-
// viewId: ViewID
|
|
62
45
|
// if set - edge was changed by user
|
|
63
|
-
controlPoints:
|
|
64
|
-
type: 'bezier' | 'poly'
|
|
65
|
-
// if dynamic view
|
|
46
|
+
controlPoints: XYPoint[] | null
|
|
66
47
|
stepNum: number | null
|
|
67
48
|
label: null | {
|
|
68
49
|
bbox: BBox
|
|
@@ -70,7 +51,13 @@ export type RelationshipData = {
|
|
|
70
51
|
}
|
|
71
52
|
}
|
|
72
53
|
|
|
73
|
-
export type RelationshipEdge =
|
|
54
|
+
export type RelationshipEdge = Simplify<
|
|
55
|
+
Edge<RelationshipData, 'relationship'> & {
|
|
56
|
+
type: 'relationship'
|
|
57
|
+
// Make field required
|
|
58
|
+
data: RelationshipData
|
|
59
|
+
}
|
|
60
|
+
>
|
|
74
61
|
|
|
75
62
|
export type XYFlowEdge = RelationshipEdge
|
|
76
63
|
|
|
@@ -83,11 +70,6 @@ export namespace XYFlowEdge {
|
|
|
83
70
|
export type XYFlowInstance = ReactFlowInstance<XYFlowNode, XYFlowEdge>
|
|
84
71
|
export type XYFlowState = ReactFlowState<XYFlowNode, XYFlowEdge>
|
|
85
72
|
|
|
86
|
-
export type XYFlowData = {
|
|
87
|
-
nodes: XYFlowNode[]
|
|
88
|
-
edges: XYFlowEdge[]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
73
|
export type XYFlowEventHandlers = {
|
|
92
74
|
onPanelClick: (event: ReactMouseEvent) => void
|
|
93
75
|
onNodeContextMenu: NodeMouseHandler<XYFlowNode>
|
package/src/xyflow/utils.ts
CHANGED
|
@@ -55,3 +55,7 @@ export function isSamePoint(a: XYPosition | Point, b: XYPosition | Point) {
|
|
|
55
55
|
const [bx, by] = isArray(b) ? b : [b.x, b.y]
|
|
56
56
|
return isClose(ax, bx) && isClose(ay, by)
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
export function distanceBetweenPoints(a: XYPosition, b: XYPosition) {
|
|
60
|
+
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
|
|
61
|
+
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { DiagramView } from '@likec4/core/types';
|
|
2
|
-
import { XYFlowData } from '../xyflow/types';
|
|
3
|
-
|
|
4
|
-
export declare function diagramViewToXYFlowData(view: Pick<DiagramView, 'id' | 'nodes' | 'edges' | '__' | 'manualLayout'>, opts: {
|
|
5
|
-
draggable: boolean;
|
|
6
|
-
selectable: boolean;
|
|
7
|
-
}): XYFlowData;
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { deepEqual as eq } from 'fast-equals'
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react'
|
|
4
|
-
import { omit } from 'remeda'
|
|
5
|
-
import { useDiagramState, useDiagramStoreApi } from '../state/useDiagramStore'
|
|
6
|
-
import { diagramViewToXYFlowData } from './diagram-to-xyflow'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Syncs the diagram state with the XYFlow instance
|
|
10
|
-
*/
|
|
11
|
-
export function SyncWithDiagram() {
|
|
12
|
-
const xyflowSynced = useDiagramState(s => s.xyflowSynced)
|
|
13
|
-
const diagramStoreApi = useDiagramStoreApi()
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
if (xyflowSynced) {
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
const {
|
|
20
|
-
view,
|
|
21
|
-
nodesDraggable,
|
|
22
|
-
nodesSelectable,
|
|
23
|
-
focusedNodeId,
|
|
24
|
-
xystore
|
|
25
|
-
} = diagramStoreApi.getState()
|
|
26
|
-
const updates = diagramViewToXYFlowData(view, {
|
|
27
|
-
draggable: nodesDraggable,
|
|
28
|
-
selectable: nodesSelectable || focusedNodeId !== null
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const {
|
|
32
|
-
nodeLookup,
|
|
33
|
-
edgeLookup,
|
|
34
|
-
setNodes,
|
|
35
|
-
setEdges
|
|
36
|
-
} = xystore.getState()
|
|
37
|
-
|
|
38
|
-
setNodes(updates.nodes.map(update => {
|
|
39
|
-
const existing = nodeLookup.get(update.id)?.internals.userNode
|
|
40
|
-
if (existing && existing.type === update.type && eq(existing.parentId, update.parentId)) {
|
|
41
|
-
if (eq(existing.data.element, update.data.element)) {
|
|
42
|
-
return existing
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
...existing,
|
|
46
|
-
...update
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return update
|
|
50
|
-
}))
|
|
51
|
-
|
|
52
|
-
setEdges(updates.edges.map(update => {
|
|
53
|
-
const existing = edgeLookup.get(update.id)
|
|
54
|
-
if (existing) {
|
|
55
|
-
if (
|
|
56
|
-
eq(existing.data.controlPoints, update.data.controlPoints)
|
|
57
|
-
&& eq(existing.markerStart, update.markerStart)
|
|
58
|
-
&& eq(existing.markerEnd, update.markerEnd)
|
|
59
|
-
&& eq(existing.data.edge, update.data.edge)
|
|
60
|
-
) {
|
|
61
|
-
return existing
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
...omit(existing, ['data', 'markerStart', 'markerEnd']),
|
|
65
|
-
...update,
|
|
66
|
-
data: {
|
|
67
|
-
...existing.data,
|
|
68
|
-
...update.data
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return update
|
|
73
|
-
}))
|
|
74
|
-
|
|
75
|
-
diagramStoreApi.setState(
|
|
76
|
-
{
|
|
77
|
-
xyflowSynced: true
|
|
78
|
-
},
|
|
79
|
-
false,
|
|
80
|
-
'xyflowSynced'
|
|
81
|
-
)
|
|
82
|
-
}, [xyflowSynced, diagramStoreApi])
|
|
83
|
-
|
|
84
|
-
return null
|
|
85
|
-
}
|