@tldiagram/core-ui 1.87.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/App.d.ts +1 -0
- package/dist/api/client.d.ts +143 -0
- package/dist/api/transport-vscode.d.ts +8 -0
- package/dist/api/transport.d.ts +1 -0
- package/dist/components/CodePreviewPanel-vscode.d.ts +7 -0
- package/dist/components/CodePreviewPanel.d.ts +9 -0
- package/dist/components/ConfirmDialog.d.ts +12 -0
- package/dist/components/ConnectorPanel.d.ts +21 -0
- package/dist/components/ContextBoundaryElement.d.ts +11 -0
- package/dist/components/ContextNeighborElement.d.ts +29 -0
- package/dist/components/ContextStraightConnector.d.ts +4 -0
- package/dist/components/CrossBranchControls.d.ts +9 -0
- package/dist/components/DependenciesOnboarding.d.ts +5 -0
- package/dist/components/DrawingCanvas.d.ts +39 -0
- package/dist/components/ElementLibrary-vscode.d.ts +7 -0
- package/dist/components/ElementLibrary.d.ts +22 -0
- package/dist/components/ElementNode.d.ts +36 -0
- package/dist/components/ElementPanel.d.ts +25 -0
- package/dist/components/ExploreOnboarding.d.ts +5 -0
- package/dist/components/ExplorePageOnboarding.d.ts +5 -0
- package/dist/components/ExportModal.d.ts +16 -0
- package/dist/components/FloatingEdge.d.ts +9 -0
- package/dist/components/GitSourceLinker.d.ts +8 -0
- package/dist/components/HeaderContext.d.ts +16 -0
- package/dist/components/Icons.d.ts +95 -0
- package/dist/components/ImportModal.d.ts +10 -0
- package/dist/components/InlineElementAdder.d.ts +17 -0
- package/dist/components/LayoutSection.d.ts +7 -0
- package/dist/components/LocalSourceLinker.d.ts +8 -0
- package/dist/components/MiniZoomOnboarding.d.ts +5 -0
- package/dist/components/NavBreadcrumb.d.ts +6 -0
- package/dist/components/NodeBody.d.ts +12 -0
- package/dist/components/NodeContainer.d.ts +8 -0
- package/dist/components/NodeHoverCard.d.ts +10 -0
- package/dist/components/PanelHeader.d.ts +8 -0
- package/dist/components/PanelUI.d.ts +3 -0
- package/dist/components/ProxyConnectorEdge.d.ts +4 -0
- package/dist/components/ProxyConnectorPanel.d.ts +9 -0
- package/dist/components/SafeBackground.d.ts +13 -0
- package/dist/components/ScrollIndicatorWrapper.d.ts +8 -0
- package/dist/components/SetChildModal.d.ts +10 -0
- package/dist/components/SetParentModal.d.ts +10 -0
- package/dist/components/SlidingPanel.d.ts +16 -0
- package/dist/components/TagUpsert.d.ts +8 -0
- package/dist/components/TopMenuBar.d.ts +8 -0
- package/dist/components/ViewBezierConnector.d.ts +4 -0
- package/dist/components/ViewDrawMenu.d.ts +22 -0
- package/dist/components/ViewEditorEdgeLabelLayout.d.ts +16 -0
- package/dist/components/ViewEditorOnboarding.d.ts +5 -0
- package/dist/components/ViewExplorer/TagManager/ColorPicker.d.ts +7 -0
- package/dist/components/ViewExplorer/TagManager/GroupNamingPopover.d.ts +10 -0
- package/dist/components/ViewExplorer/TagManager/LayerItem.d.ts +27 -0
- package/dist/components/ViewExplorer/TagManager/TagItem.d.ts +25 -0
- package/dist/components/ViewExplorer/TagManager/index.d.ts +21 -0
- package/dist/components/ViewExplorer/ViewNavigator.d.ts +11 -0
- package/dist/components/ViewExplorer/ViewSearch.d.ts +8 -0
- package/dist/components/ViewExplorer/ViewTree.d.ts +18 -0
- package/dist/components/ViewExplorer/index.d.ts +31 -0
- package/dist/components/ViewExplorer/types.d.ts +11 -0
- package/dist/components/ViewExplorer/utils.d.ts +6 -0
- package/dist/components/ViewExplorer-vscode.d.ts +6 -0
- package/dist/components/ViewFloatingMenu-vscode.d.ts +27 -0
- package/dist/components/ViewFloatingMenu.d.ts +39 -0
- package/dist/components/ViewGridNode.d.ts +29 -0
- package/dist/components/ViewHeaderButton.d.ts +11 -0
- package/dist/components/ViewPanel.d.ts +18 -0
- package/dist/components/ViewsGridOnboarding.d.ts +5 -0
- package/dist/components/ZUI/ZUICanvas.d.ts +18 -0
- package/dist/components/ZUI/index.d.ts +2 -0
- package/dist/components/ZUI/layout.d.ts +18 -0
- package/dist/components/ZUI/proxy.d.ts +25 -0
- package/dist/components/ZUI/renderer.d.ts +30 -0
- package/dist/components/ZUI/types.d.ts +140 -0
- package/dist/components/ZUI/useZUIInteraction.d.ts +21 -0
- package/dist/config/runtime-vscode.d.ts +22 -0
- package/dist/config/runtime.d.ts +5 -0
- package/dist/constants/colors.d.ts +27 -0
- package/dist/constants/diagramColors.d.ts +1 -0
- package/dist/context/ThemeContext.d.ts +27 -0
- package/dist/crossBranch/graph.d.ts +13 -0
- package/dist/crossBranch/resolve.d.ts +22 -0
- package/dist/crossBranch/settings.d.ts +6 -0
- package/dist/crossBranch/store.d.ts +11 -0
- package/dist/crossBranch/types.d.ts +96 -0
- package/dist/demo/DemoPage.d.ts +9 -0
- package/dist/demo/seed.d.ts +9 -0
- package/dist/demo/store.d.ts +137 -0
- package/dist/demo/viewEditor.d.ts +26 -0
- package/dist/favicon.svg +35 -0
- package/dist/hooks/useSafeFitView.d.ts +16 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +19966 -0
- package/dist/lib/vscodeBridge-vscode.d.ts +13 -0
- package/dist/lib/vscodeBridge.d.ts +5 -0
- package/dist/logo-120.png +0 -0
- package/dist/logo-bw.png +0 -0
- package/dist/logo-bw.svg +15 -0
- package/dist/logo-text.svg +51 -0
- package/dist/logo.svg +35 -0
- package/dist/pages/AppearanceSettings.d.ts +3 -0
- package/dist/pages/Dependencies.d.ts +1 -0
- package/dist/pages/InfiniteZoom.d.ts +7 -0
- package/dist/pages/Settings.d.ts +7 -0
- package/dist/pages/ViewEditor/components/EditorMenus.d.ts +24 -0
- package/dist/pages/ViewEditor/components/EditorOverlays.d.ts +30 -0
- package/dist/pages/ViewEditor/components/EmptyCanvasState.d.ts +7 -0
- package/dist/pages/ViewEditor/context.d.ts +13 -0
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +201 -0
- package/dist/pages/ViewEditor/hooks/useDrawingEngine.d.ts +40 -0
- package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +20 -0
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +74 -0
- package/dist/pages/ViewEditor/index.d.ts +8 -0
- package/dist/pages/ViewEditor/utils.d.ts +14 -0
- package/dist/pages/Views.d.ts +6 -0
- package/dist/pages/ViewsGrid.d.ts +6 -0
- package/dist/pkg/importer/mermaid.d.ts +7 -0
- package/dist/pkg/importer/mermaid.test.d.ts +1 -0
- package/dist/platform/PlatformContext.d.ts +6 -0
- package/dist/platform/context.d.ts +3 -0
- package/dist/platform/local.d.ts +2 -0
- package/dist/platform/types.d.ts +17 -0
- package/dist/slots.d.ts +67 -0
- package/dist/theme.d.ts +2 -0
- package/dist/types/index.d.ts +193 -0
- package/dist/types/vscode-messages.d.ts +60 -0
- package/dist/utils/edgeDistribution.d.ts +34 -0
- package/dist/utils/githubApi.d.ts +4 -0
- package/dist/utils/githubCache.d.ts +17 -0
- package/dist/utils/ids.d.ts +2 -0
- package/dist/utils/technologyCatalog.d.ts +15 -0
- package/dist/utils/toast.d.ts +15 -0
- package/dist/utils/treesitter.d.ts +13 -0
- package/dist/utils/url.d.ts +12 -0
- package/package.json +159 -0
- package/src/App.tsx +141 -0
- package/src/api/client.ts +618 -0
- package/src/api/transport-vscode.ts +28 -0
- package/src/api/transport.ts +7 -0
- package/src/assets/logo-mark.svg +31 -0
- package/src/assets/logo-wordmark.svg +22 -0
- package/src/assets/logo.svg +35 -0
- package/src/components/CodePreviewPanel-vscode.tsx +85 -0
- package/src/components/CodePreviewPanel.tsx +384 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/ConnectorPanel.tsx +403 -0
- package/src/components/ContextBoundaryElement.tsx +35 -0
- package/src/components/ContextNeighborElement.tsx +282 -0
- package/src/components/ContextStraightConnector.tsx +144 -0
- package/src/components/CrossBranchControls.tsx +105 -0
- package/src/components/DependenciesOnboarding.tsx +427 -0
- package/src/components/DrawingCanvas.tsx +391 -0
- package/src/components/ElementLibrary-vscode.tsx +9 -0
- package/src/components/ElementLibrary.tsx +512 -0
- package/src/components/ElementNode.tsx +1033 -0
- package/src/components/ElementPanel.tsx +928 -0
- package/src/components/ExploreOnboarding.tsx +347 -0
- package/src/components/ExplorePageOnboarding.tsx +383 -0
- package/src/components/ExportModal.tsx +132 -0
- package/src/components/FloatingEdge.tsx +115 -0
- package/src/components/GitSourceLinker.tsx +1053 -0
- package/src/components/HeaderContext.tsx +30 -0
- package/src/components/Icons.tsx +245 -0
- package/src/components/ImportModal.tsx +219 -0
- package/src/components/InlineElementAdder.tsx +216 -0
- package/src/components/LayoutSection.tsx +624 -0
- package/src/components/LocalSourceLinker.tsx +330 -0
- package/src/components/MiniZoomOnboarding.tsx +78 -0
- package/src/components/NavBreadcrumb.tsx +24 -0
- package/src/components/NodeBody.tsx +89 -0
- package/src/components/NodeContainer.tsx +58 -0
- package/src/components/NodeHoverCard.tsx +135 -0
- package/src/components/PanelHeader.tsx +36 -0
- package/src/components/PanelUI.tsx +24 -0
- package/src/components/ProxyConnectorEdge.tsx +169 -0
- package/src/components/ProxyConnectorPanel.tsx +130 -0
- package/src/components/SafeBackground.tsx +19 -0
- package/src/components/ScrollIndicatorWrapper.tsx +117 -0
- package/src/components/SetChildModal.tsx +191 -0
- package/src/components/SetParentModal.tsx +187 -0
- package/src/components/SlidingPanel.tsx +114 -0
- package/src/components/TagUpsert.tsx +142 -0
- package/src/components/TopMenuBar.tsx +380 -0
- package/src/components/ViewBezierConnector.tsx +143 -0
- package/src/components/ViewDrawMenu.tsx +270 -0
- package/src/components/ViewEditorEdgeLabelLayout.ts +189 -0
- package/src/components/ViewEditorOnboarding.tsx +445 -0
- package/src/components/ViewExplorer/TagManager/ColorPicker.tsx +49 -0
- package/src/components/ViewExplorer/TagManager/GroupNamingPopover.tsx +96 -0
- package/src/components/ViewExplorer/TagManager/LayerItem.tsx +228 -0
- package/src/components/ViewExplorer/TagManager/TagItem.tsx +242 -0
- package/src/components/ViewExplorer/TagManager/index.tsx +418 -0
- package/src/components/ViewExplorer/ViewNavigator.tsx +121 -0
- package/src/components/ViewExplorer/ViewSearch.tsx +33 -0
- package/src/components/ViewExplorer/ViewTree.tsx +98 -0
- package/src/components/ViewExplorer/index.tsx +384 -0
- package/src/components/ViewExplorer/types.ts +13 -0
- package/src/components/ViewExplorer/utils.ts +56 -0
- package/src/components/ViewExplorer-vscode.tsx +8 -0
- package/src/components/ViewFloatingMenu-vscode.tsx +248 -0
- package/src/components/ViewFloatingMenu.tsx +379 -0
- package/src/components/ViewGridNode.tsx +451 -0
- package/src/components/ViewHeaderButton.tsx +60 -0
- package/src/components/ViewPanel.tsx +162 -0
- package/src/components/ViewsGridOnboarding.tsx +400 -0
- package/src/components/ZUI/ZUICanvas.tsx +853 -0
- package/src/components/ZUI/index.ts +3 -0
- package/src/components/ZUI/layout.ts +323 -0
- package/src/components/ZUI/proxy.ts +278 -0
- package/src/components/ZUI/renderer.ts +1189 -0
- package/src/components/ZUI/types.ts +150 -0
- package/src/components/ZUI/useZUIInteraction.ts +720 -0
- package/src/config/runtime-vscode.ts +46 -0
- package/src/config/runtime.ts +30 -0
- package/src/constants/colors.ts +80 -0
- package/src/constants/diagramColors.ts +9 -0
- package/src/context/ThemeContext.tsx +158 -0
- package/src/crossBranch/graph.ts +207 -0
- package/src/crossBranch/resolve.ts +643 -0
- package/src/crossBranch/settings.ts +59 -0
- package/src/crossBranch/store.ts +71 -0
- package/src/crossBranch/types.ts +102 -0
- package/src/demo/DemoPage.tsx +184 -0
- package/src/demo/seed.ts +67 -0
- package/src/demo/store.ts +536 -0
- package/src/demo/viewEditor.ts +110 -0
- package/src/hooks/useSafeFitView.ts +60 -0
- package/src/index.css +309 -0
- package/src/index.ts +184 -0
- package/src/kafka-ss.png +0 -0
- package/src/lib/vscodeBridge-vscode.ts +27 -0
- package/src/lib/vscodeBridge.ts +7 -0
- package/src/main.tsx +46 -0
- package/src/pages/AppearanceSettings.tsx +135 -0
- package/src/pages/Dependencies.tsx +926 -0
- package/src/pages/InfiniteZoom.tsx +404 -0
- package/src/pages/Settings.tsx +91 -0
- package/src/pages/ViewEditor/EDGE_DISTRIBUTION.md +64 -0
- package/src/pages/ViewEditor/components/EditorMenus.tsx +112 -0
- package/src/pages/ViewEditor/components/EditorOverlays.tsx +172 -0
- package/src/pages/ViewEditor/components/EmptyCanvasState.tsx +42 -0
- package/src/pages/ViewEditor/context.tsx +21 -0
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +1349 -0
- package/src/pages/ViewEditor/hooks/useDrawingEngine.ts +127 -0
- package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +501 -0
- package/src/pages/ViewEditor/hooks/useViewData.ts +491 -0
- package/src/pages/ViewEditor/index.tsx +1366 -0
- package/src/pages/ViewEditor/utils.ts +88 -0
- package/src/pages/Views.tsx +171 -0
- package/src/pages/ViewsGrid.tsx +1310 -0
- package/src/pkg/importer/mermaid.test.ts +141 -0
- package/src/pkg/importer/mermaid.ts +76 -0
- package/src/platform/PlatformContext.tsx +17 -0
- package/src/platform/context.ts +9 -0
- package/src/platform/local.tsx +15 -0
- package/src/platform/types.ts +19 -0
- package/src/slots.ts +92 -0
- package/src/styles/editor-panels.css +66 -0
- package/src/styles/theme.css +56 -0
- package/src/theme.ts +336 -0
- package/src/types/index.ts +234 -0
- package/src/types/offline-ambient.d.ts +14 -0
- package/src/types/vscode-messages.ts +32 -0
- package/src/utils/edgeDistribution.ts +103 -0
- package/src/utils/githubApi.ts +121 -0
- package/src/utils/githubCache.ts +108 -0
- package/src/utils/ids.ts +9 -0
- package/src/utils/technologyCatalog.ts +143 -0
- package/src/utils/toast.ts +100 -0
- package/src/utils/treesitter.ts +147 -0
- package/src/utils/url.ts +72 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import type { DrawingPath } from '../../../components/DrawingCanvas'
|
|
3
|
+
import { DRAWING_COLORS } from '../../../constants/colors'
|
|
4
|
+
|
|
5
|
+
export function useDrawingEngine(viewId: number | null) {
|
|
6
|
+
const [drawingMode, setDrawingMode] = useState(false)
|
|
7
|
+
const [drawingVisible, setDrawingVisible] = useState(true)
|
|
8
|
+
const [drawingPaths, setDrawingPaths] = useState<DrawingPath[]>([])
|
|
9
|
+
const [drawingTool, setDrawingTool] = useState<'pencil' | 'eraser' | 'text' | 'select'>('pencil')
|
|
10
|
+
const [drawingColor, setDrawingColor] = useState(DRAWING_COLORS[0])
|
|
11
|
+
const [drawingWidth, setDrawingWidth] = useState(3)
|
|
12
|
+
const [textEditorState, setTextEditorState] = useState<{
|
|
13
|
+
canvasX: number; canvasY: number; flowX: number; flowY: number
|
|
14
|
+
} | null>(null)
|
|
15
|
+
|
|
16
|
+
const drawingHistoryRef = useRef<DrawingPath[][]>([])
|
|
17
|
+
const drawingRedoStackRef = useRef<DrawingPath[][]>([])
|
|
18
|
+
|
|
19
|
+
const lastViewIdRef = useRef<number | null>(null)
|
|
20
|
+
|
|
21
|
+
// Reset drawing state only when viewId actually changes to a new value
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (lastViewIdRef.current !== viewId) {
|
|
24
|
+
setDrawingPaths([])
|
|
25
|
+
setDrawingMode(false)
|
|
26
|
+
setTextEditorState(null)
|
|
27
|
+
lastViewIdRef.current = viewId
|
|
28
|
+
}
|
|
29
|
+
}, [viewId])
|
|
30
|
+
|
|
31
|
+
const handleUndo = useCallback(() => {
|
|
32
|
+
if (drawingHistoryRef.current.length === 0) return
|
|
33
|
+
const prevState = drawingHistoryRef.current.pop()!
|
|
34
|
+
setDrawingPaths((current) => {
|
|
35
|
+
drawingRedoStackRef.current.push([...current])
|
|
36
|
+
return prevState
|
|
37
|
+
})
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const handleRedo = useCallback(() => {
|
|
41
|
+
if (drawingRedoStackRef.current.length === 0) return
|
|
42
|
+
const nextState = drawingRedoStackRef.current.pop()!
|
|
43
|
+
setDrawingPaths((current) => {
|
|
44
|
+
drawingHistoryRef.current.push([...current])
|
|
45
|
+
return nextState
|
|
46
|
+
})
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
const commitDrawingText = useCallback(
|
|
50
|
+
(value: string, state: { canvasX: number; canvasY: number; flowX: number; flowY: number }) => {
|
|
51
|
+
setTextEditorState(null)
|
|
52
|
+
if (!value.trim()) return
|
|
53
|
+
const path: DrawingPath = {
|
|
54
|
+
id: `text-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
55
|
+
points: [{ x: state.flowX, y: state.flowY }],
|
|
56
|
+
color: drawingColor,
|
|
57
|
+
width: drawingWidth,
|
|
58
|
+
text: value,
|
|
59
|
+
fontSize: Math.max(14, drawingWidth * 5),
|
|
60
|
+
}
|
|
61
|
+
setDrawingPaths((prev) => {
|
|
62
|
+
drawingHistoryRef.current.push([...prev])
|
|
63
|
+
drawingRedoStackRef.current = []
|
|
64
|
+
return [...prev, path]
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
[drawingColor, drawingWidth],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const onPathComplete = useCallback(
|
|
71
|
+
(path: DrawingPath) => {
|
|
72
|
+
setDrawingPaths((prev) => {
|
|
73
|
+
drawingHistoryRef.current.push([...prev])
|
|
74
|
+
drawingRedoStackRef.current = []
|
|
75
|
+
return [...prev, path]
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
[],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const onPathDelete = useCallback(
|
|
82
|
+
(pathId: string) => {
|
|
83
|
+
setDrawingPaths((prev) => {
|
|
84
|
+
drawingHistoryRef.current.push([...prev])
|
|
85
|
+
drawingRedoStackRef.current = []
|
|
86
|
+
return prev.filter((p) => p.id !== pathId)
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
[],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const onPathUpdate = useCallback(
|
|
93
|
+
(path: DrawingPath) => {
|
|
94
|
+
setDrawingPaths((prev) => {
|
|
95
|
+
drawingHistoryRef.current.push([...prev])
|
|
96
|
+
drawingRedoStackRef.current = []
|
|
97
|
+
return prev.map(p => p.id === path.id ? path : p)
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
[],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
drawingMode,
|
|
105
|
+
setDrawingMode,
|
|
106
|
+
drawingVisible,
|
|
107
|
+
setDrawingVisible,
|
|
108
|
+
drawingPaths,
|
|
109
|
+
setDrawingPaths,
|
|
110
|
+
drawingTool,
|
|
111
|
+
setDrawingTool,
|
|
112
|
+
drawingColor,
|
|
113
|
+
setDrawingColor,
|
|
114
|
+
drawingWidth,
|
|
115
|
+
setDrawingWidth,
|
|
116
|
+
textEditorState,
|
|
117
|
+
setTextEditorState,
|
|
118
|
+
drawingHistoryRef,
|
|
119
|
+
drawingRedoStackRef,
|
|
120
|
+
handleUndo,
|
|
121
|
+
handleRedo,
|
|
122
|
+
commitDrawingText,
|
|
123
|
+
onPathComplete,
|
|
124
|
+
onPathDelete,
|
|
125
|
+
onPathUpdate,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { type Edge as RFEdge, type Node as RFNode } from 'reactflow'
|
|
3
|
+
import type { PlacedElement } from '../../../types'
|
|
4
|
+
import type { CrossBranchContextSettings, ProxyConnectorDetails, WorkspaceGraphSnapshot } from '../../../crossBranch/types'
|
|
5
|
+
import { resolveViewProxyGraph } from '../../../crossBranch/resolve'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
snapshot: WorkspaceGraphSnapshot | null
|
|
9
|
+
settings: CrossBranchContextSettings
|
|
10
|
+
viewId: number | null
|
|
11
|
+
viewElements: PlacedElement[]
|
|
12
|
+
rfNodes: RFNode[]
|
|
13
|
+
stableOnNavigateToView: (id: number) => void
|
|
14
|
+
onSelectProxyDetails: (details: ProxyConnectorDetails) => void
|
|
15
|
+
expandedAncestorGroups: Set<string>
|
|
16
|
+
onToggleAncestorGroup: (anchorId: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ContextSide = 'top' | 'bottom' | 'left' | 'right'
|
|
20
|
+
|
|
21
|
+
const CONTEXT_NODE_W = 200
|
|
22
|
+
const CONTEXT_NODE_H = 100
|
|
23
|
+
const CONTEXT_NODE_HALF_W = CONTEXT_NODE_W / 2
|
|
24
|
+
const CONTEXT_NODE_HALF_H = CONTEXT_NODE_H / 2
|
|
25
|
+
const HORIZONTAL_STACK_SPACING = 128
|
|
26
|
+
const VERTICAL_STACK_SPACING = 74
|
|
27
|
+
const HORIZONTAL_BOUNDARY_CLEARANCE = 72
|
|
28
|
+
const VERTICAL_BOUNDARY_CLEARANCE = 44
|
|
29
|
+
// Extra space to clear the chevron button when a cluster is expanded.
|
|
30
|
+
// The chevron sits at 75% of the node's layout dimension (scale-0.5 visual edge) + 6px offset.
|
|
31
|
+
// For right side: visual right ≈ 150px, chevron ~36px wide → children need ~80px extra.
|
|
32
|
+
// For bottom side: visual bottom ≈ 75px, chevron ~14px tall → children need ~30px extra.
|
|
33
|
+
const CHEVRON_H_CLEARANCE = 30
|
|
34
|
+
const CHEVRON_V_CLEARANCE = 10
|
|
35
|
+
const SIDE_CLUSTER_THRESHOLD = 90
|
|
36
|
+
const TOP_BOTTOM_CLUSTER_THRESHOLD = 140
|
|
37
|
+
|
|
38
|
+
function stableAngleFromId(id: string) {
|
|
39
|
+
let hash = 0
|
|
40
|
+
for (let i = 0; i < id.length; i += 1) hash = ((hash << 5) - hash) + id.charCodeAt(i)
|
|
41
|
+
return ((hash % 360) * Math.PI) / 180
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function averageAngles(angles: number[]): number {
|
|
45
|
+
if (angles.length === 0) return 0
|
|
46
|
+
const sum = angles.reduce((acc, angle) => ({
|
|
47
|
+
x: acc.x + Math.cos(angle),
|
|
48
|
+
y: acc.y + Math.sin(angle),
|
|
49
|
+
}), { x: 0, y: 0 })
|
|
50
|
+
if (sum.x === 0 && sum.y === 0) return angles[0]
|
|
51
|
+
return Math.atan2(sum.y, sum.x)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function classifySide(angle: number): ContextSide {
|
|
55
|
+
const dx = Math.cos(angle)
|
|
56
|
+
const dy = Math.sin(angle)
|
|
57
|
+
if (Math.abs(dx) > Math.abs(dy)) return dx < 0 ? 'left' : 'right'
|
|
58
|
+
return dy < 0 ? 'top' : 'bottom'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isAncestorContextNode(
|
|
62
|
+
snapshot: WorkspaceGraphSnapshot,
|
|
63
|
+
ancestor: { anchorElementId: number },
|
|
64
|
+
descendant: { placementViewId: number | null },
|
|
65
|
+
): boolean {
|
|
66
|
+
const ownedViewId = snapshot.childViewIdByOwnerElementId[ancestor.anchorElementId]
|
|
67
|
+
if (ownedViewId == null || descendant.placementViewId == null) return false
|
|
68
|
+
return descendant.placementViewId === ownedViewId ||
|
|
69
|
+
(snapshot.descendantsByViewId[ownedViewId]?.includes(descendant.placementViewId) ?? false)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useViewContextNeighbours({
|
|
73
|
+
snapshot,
|
|
74
|
+
settings,
|
|
75
|
+
viewId,
|
|
76
|
+
viewElements,
|
|
77
|
+
rfNodes,
|
|
78
|
+
stableOnNavigateToView,
|
|
79
|
+
onSelectProxyDetails,
|
|
80
|
+
expandedAncestorGroups,
|
|
81
|
+
onToggleAncestorGroup,
|
|
82
|
+
}: Props) {
|
|
83
|
+
return useMemo(() => {
|
|
84
|
+
if (!snapshot || viewId == null || !settings.enabled) {
|
|
85
|
+
return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey: {} as Record<string, ProxyConnectorDetails> }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { proxyNodes, proxyConnectors, proxyConnectorDetailsByKey } = resolveViewProxyGraph(snapshot, viewId, viewElements, settings)
|
|
89
|
+
if (proxyNodes.length === 0 && proxyConnectors.length === 0) {
|
|
90
|
+
return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const mainNodes = rfNodes.filter((node) => node.type === 'elementNode')
|
|
94
|
+
if (mainNodes.length === 0) {
|
|
95
|
+
return { contextNodes: [] as RFNode[], contextConnectors: [] as RFEdge[], proxyConnectorDetailsByKey }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let minX = Infinity
|
|
99
|
+
let minY = Infinity
|
|
100
|
+
let maxX = -Infinity
|
|
101
|
+
let maxY = -Infinity
|
|
102
|
+
for (const node of mainNodes) {
|
|
103
|
+
const width = node.width ?? 200
|
|
104
|
+
const height = node.height ?? 90
|
|
105
|
+
minX = Math.min(minX, node.position.x)
|
|
106
|
+
minY = Math.min(minY, node.position.y)
|
|
107
|
+
maxX = Math.max(maxX, node.position.x + width)
|
|
108
|
+
maxY = Math.max(maxY, node.position.y + height)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const centerX = (minX + maxX) / 2
|
|
112
|
+
const centerY = (minY + maxY) / 2
|
|
113
|
+
const boundaryW = maxX - minX
|
|
114
|
+
const boundaryH = maxY - minY
|
|
115
|
+
const totalInset = 100
|
|
116
|
+
const padding = 180
|
|
117
|
+
const radiusX = boundaryW / 2 + padding
|
|
118
|
+
const radiusY = boundaryH / 2 + padding
|
|
119
|
+
const boundaryLeft = minX - totalInset
|
|
120
|
+
const boundaryRight = maxX + totalInset
|
|
121
|
+
const boundaryTop = minY - totalInset
|
|
122
|
+
const boundaryBottom = maxY + totalInset
|
|
123
|
+
|
|
124
|
+
const livePositions = new Map(rfNodes.map((node) => [node.id, node.position] as const))
|
|
125
|
+
const currentViewPositions = new Map(viewElements.map((element) => {
|
|
126
|
+
const live = livePositions.get(String(element.element_id))
|
|
127
|
+
return [
|
|
128
|
+
element.element_id,
|
|
129
|
+
live ? { ...element, position_x: live.x, position_y: live.y } : element,
|
|
130
|
+
] as const
|
|
131
|
+
}))
|
|
132
|
+
const proxyNodeDetailsById = new Map(
|
|
133
|
+
proxyNodes.map((proxyNode) => {
|
|
134
|
+
const connectors = proxyConnectors
|
|
135
|
+
.filter((connector) => connector.sourceAnchorId === proxyNode.id || connector.targetAnchorId === proxyNode.id)
|
|
136
|
+
.flatMap((connector) => connector.details.connectors)
|
|
137
|
+
|
|
138
|
+
const ownerViews = new Map<number, string>()
|
|
139
|
+
connectors.forEach((leaf) => ownerViews.set(leaf.ownerViewId, leaf.ownerViewName))
|
|
140
|
+
|
|
141
|
+
const details: ProxyConnectorDetails = {
|
|
142
|
+
key: `node:${proxyNode.id}`,
|
|
143
|
+
label: 'Merged branch context',
|
|
144
|
+
count: connectors.length,
|
|
145
|
+
sourceAnchorId: proxyNode.id,
|
|
146
|
+
targetAnchorId: proxyNode.id,
|
|
147
|
+
sourceAnchorName: proxyNode.name,
|
|
148
|
+
targetAnchorName: 'Multiple connections',
|
|
149
|
+
ownerViewIds: Array.from(ownerViews.keys()),
|
|
150
|
+
ownerViewNames: Array.from(ownerViews.values()),
|
|
151
|
+
connectors,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return [proxyNode.id, details] as const
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const ContextBoundaryElement: RFNode = {
|
|
159
|
+
id: 'context-boundary',
|
|
160
|
+
type: 'ContextBoundaryElement',
|
|
161
|
+
position: { x: minX - totalInset, y: minY - totalInset },
|
|
162
|
+
selectable: false,
|
|
163
|
+
draggable: false,
|
|
164
|
+
connectable: false,
|
|
165
|
+
zIndex: 1,
|
|
166
|
+
data: {
|
|
167
|
+
width: boundaryW + totalInset * 2,
|
|
168
|
+
height: boundaryH + totalInset * 2,
|
|
169
|
+
parentName: snapshot.viewById[viewId]?.name ?? 'Current view',
|
|
170
|
+
onNavigateToDiagram: () => { /* intentionally read-only */ },
|
|
171
|
+
},
|
|
172
|
+
style: {
|
|
173
|
+
pointerEvents: 'none',
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const provisionalContextLayouts = proxyNodes.map((contextNode) => {
|
|
178
|
+
const relatedAngles = proxyConnectors
|
|
179
|
+
.filter((connector) => connector.sourceAnchorId === contextNode.id || connector.targetAnchorId === contextNode.id)
|
|
180
|
+
.flatMap((connector) => {
|
|
181
|
+
const leafAngles = connector.details.connectors.map((leaf) => {
|
|
182
|
+
const external = leaf.source.externalToView ? leaf.source : leaf.target
|
|
183
|
+
if (external.anchorElementId !== contextNode.anchorElementId) return null
|
|
184
|
+
const internal = leaf.source.externalToView ? leaf.target : leaf.source
|
|
185
|
+
|
|
186
|
+
if (external.anchorViewId === viewId) {
|
|
187
|
+
const externalPos = currentViewPositions.get(external.anchorElementId)
|
|
188
|
+
const internalPos = currentViewPositions.get(internal.anchorElementId)
|
|
189
|
+
if (externalPos && internalPos) {
|
|
190
|
+
return Math.atan2(externalPos.position_y - internalPos.position_y, externalPos.position_x - internalPos.position_x)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (external.commonAncestorViewId != null && external.currentBranchElementId != null) {
|
|
195
|
+
const commonAncestorPositions = snapshot.placementsByViewId[external.commonAncestorViewId] ?? []
|
|
196
|
+
const currentBranchPlacement = commonAncestorPositions.find((placement) => placement.element_id === external.currentBranchElementId)
|
|
197
|
+
const externalPlacement = commonAncestorPositions.find((placement) => placement.element_id === external.anchorElementId)
|
|
198
|
+
if (currentBranchPlacement && externalPlacement) {
|
|
199
|
+
return Math.atan2(
|
|
200
|
+
externalPlacement.position_y - currentBranchPlacement.position_y,
|
|
201
|
+
externalPlacement.position_x - currentBranchPlacement.position_x,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const internalPos = currentViewPositions.get(internal.anchorElementId)
|
|
207
|
+
if (internalPos) {
|
|
208
|
+
return Math.atan2(internalPos.position_y - centerY, internalPos.position_x - centerX) + Math.PI
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
return leafAngles.filter((angle): angle is number => angle != null)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
let angle = relatedAngles.length > 0 ? averageAngles(relatedAngles) : stableAngleFromId(contextNode.id)
|
|
218
|
+
const isDescendant = contextNode.placementViewId != null && (snapshot.descendantsByViewId[viewId]?.includes(contextNode.placementViewId) ?? false)
|
|
219
|
+
|
|
220
|
+
if (isDescendant) {
|
|
221
|
+
angle = -Math.PI / 2
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const side = isDescendant ? 'top' : classifySide(angle)
|
|
225
|
+
|
|
226
|
+
const centerPosX = centerX + Math.cos(angle) * radiusX
|
|
227
|
+
const centerPosY = centerY + Math.sin(angle) * radiusY
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
contextNode,
|
|
231
|
+
angle,
|
|
232
|
+
side,
|
|
233
|
+
centerX: centerPosX,
|
|
234
|
+
centerY: centerPosY,
|
|
235
|
+
position: {
|
|
236
|
+
x: centerPosX - CONTEXT_NODE_HALF_W,
|
|
237
|
+
y: centerPosY - CONTEXT_NODE_HALF_H,
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const layoutsBySide: Record<ContextSide, typeof provisionalContextLayouts> = {
|
|
243
|
+
top: [],
|
|
244
|
+
bottom: [],
|
|
245
|
+
left: [],
|
|
246
|
+
right: [],
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
provisionalContextLayouts.forEach((layout) => {
|
|
250
|
+
layoutsBySide[layout.side].push(layout)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
let clusterCounter = 0
|
|
254
|
+
const stackedLayouts = (['top', 'bottom', 'left', 'right'] as const).flatMap((side) => {
|
|
255
|
+
const sideLayouts = [...layoutsBySide[side]].sort((left, right) => {
|
|
256
|
+
const leftCoord = side === 'left' || side === 'right' ? left.centerY : left.centerX
|
|
257
|
+
const rightCoord = side === 'left' || side === 'right' ? right.centerY : right.centerX
|
|
258
|
+
return leftCoord - rightCoord
|
|
259
|
+
})
|
|
260
|
+
if (sideLayouts.length === 0) return []
|
|
261
|
+
|
|
262
|
+
const threshold = side === 'left' || side === 'right' ? SIDE_CLUSTER_THRESHOLD : TOP_BOTTOM_CLUSTER_THRESHOLD
|
|
263
|
+
const clusters: typeof sideLayouts[] = []
|
|
264
|
+
|
|
265
|
+
for (const layout of sideLayouts) {
|
|
266
|
+
const coord = side === 'left' || side === 'right' ? layout.centerY : layout.centerX
|
|
267
|
+
const cluster = clusters[clusters.length - 1]
|
|
268
|
+
if (!cluster) {
|
|
269
|
+
clusters.push([layout])
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
const last = cluster[cluster.length - 1]
|
|
273
|
+
const lastCoord = side === 'left' || side === 'right' ? last.centerY : last.centerX
|
|
274
|
+
if (Math.abs(coord - lastCoord) <= threshold) cluster.push(layout)
|
|
275
|
+
else clusters.push([layout])
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return clusters.flatMap((cluster) => {
|
|
279
|
+
const clusterId = `${side}-${clusterCounter++}`
|
|
280
|
+
const ordered = [...cluster].sort((left, right) => {
|
|
281
|
+
if (left.contextNode.sortLevel !== right.contextNode.sortLevel) {
|
|
282
|
+
return left.contextNode.sortLevel - right.contextNode.sortLevel
|
|
283
|
+
}
|
|
284
|
+
return left.contextNode.name.localeCompare(right.contextNode.name)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const anchorX = cluster.reduce((sum, layout) => sum + layout.centerX, 0) / cluster.length
|
|
288
|
+
const anchorY = cluster.reduce((sum, layout) => sum + layout.centerY, 0) / cluster.length
|
|
289
|
+
const clusterExpanded = ordered.length > 1 && expandedAncestorGroups.has(ordered[0].contextNode.id)
|
|
290
|
+
// A cluster is collapsible only when the lowest-sortLevel node genuinely owns a view
|
|
291
|
+
// that contains at least one other node's placement view. Siblings that merely happen
|
|
292
|
+
// to be close in angle but are NOT in an ancestor-descendant view relationship
|
|
293
|
+
// are laid out along the boundary edge instead of stacking away from it.
|
|
294
|
+
const hasAncestorDescendant = ordered.length > 1 &&
|
|
295
|
+
ordered.slice(1).some((layout) => isAncestorContextNode(snapshot, ordered[0].contextNode, layout.contextNode))
|
|
296
|
+
|
|
297
|
+
if (side === 'top') {
|
|
298
|
+
if (!hasAncestorDescendant) {
|
|
299
|
+
const startX = anchorX - ((ordered.length - 1) * HORIZONTAL_STACK_SPACING) / 2
|
|
300
|
+
return ordered.map((layout, index) => ({
|
|
301
|
+
...layout,
|
|
302
|
+
clusterId,
|
|
303
|
+
position: {
|
|
304
|
+
x: startX + index * HORIZONTAL_STACK_SPACING - CONTEXT_NODE_HALF_W,
|
|
305
|
+
y: boundaryTop - VERTICAL_BOUNDARY_CLEARANCE - CONTEXT_NODE_HALF_H,
|
|
306
|
+
},
|
|
307
|
+
}))
|
|
308
|
+
}
|
|
309
|
+
const startY = boundaryTop - VERTICAL_BOUNDARY_CLEARANCE - (ordered.length - 1) * VERTICAL_STACK_SPACING
|
|
310
|
+
return ordered.map((layout, index) => ({
|
|
311
|
+
...layout,
|
|
312
|
+
clusterId,
|
|
313
|
+
position: {
|
|
314
|
+
x: anchorX - CONTEXT_NODE_HALF_W,
|
|
315
|
+
y: startY + index * VERTICAL_STACK_SPACING - CONTEXT_NODE_HALF_H,
|
|
316
|
+
},
|
|
317
|
+
}))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (side === 'bottom') {
|
|
321
|
+
if (!hasAncestorDescendant) {
|
|
322
|
+
const startX = anchorX - ((ordered.length - 1) * HORIZONTAL_STACK_SPACING) / 2
|
|
323
|
+
return ordered.map((layout, index) => ({
|
|
324
|
+
...layout,
|
|
325
|
+
clusterId,
|
|
326
|
+
position: {
|
|
327
|
+
x: startX + index * HORIZONTAL_STACK_SPACING - CONTEXT_NODE_HALF_W,
|
|
328
|
+
y: boundaryBottom + VERTICAL_BOUNDARY_CLEARANCE - CONTEXT_NODE_HALF_H,
|
|
329
|
+
},
|
|
330
|
+
}))
|
|
331
|
+
}
|
|
332
|
+
const startY = boundaryBottom + VERTICAL_BOUNDARY_CLEARANCE
|
|
333
|
+
return ordered.map((layout, index) => ({
|
|
334
|
+
...layout,
|
|
335
|
+
clusterId,
|
|
336
|
+
position: {
|
|
337
|
+
x: anchorX - CONTEXT_NODE_HALF_W,
|
|
338
|
+
y: startY + index * VERTICAL_STACK_SPACING + (index > 0 && clusterExpanded ? CHEVRON_V_CLEARANCE : 0) - CONTEXT_NODE_HALF_H,
|
|
339
|
+
},
|
|
340
|
+
}))
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (side === 'left') {
|
|
344
|
+
if (!hasAncestorDescendant) {
|
|
345
|
+
const startY = anchorY - ((ordered.length - 1) * VERTICAL_STACK_SPACING) / 2
|
|
346
|
+
return ordered.map((layout, index) => ({
|
|
347
|
+
...layout,
|
|
348
|
+
clusterId,
|
|
349
|
+
position: {
|
|
350
|
+
x: boundaryLeft - HORIZONTAL_BOUNDARY_CLEARANCE - CONTEXT_NODE_HALF_W,
|
|
351
|
+
y: startY + index * VERTICAL_STACK_SPACING - CONTEXT_NODE_HALF_H,
|
|
352
|
+
},
|
|
353
|
+
}))
|
|
354
|
+
}
|
|
355
|
+
const startX = boundaryLeft - HORIZONTAL_BOUNDARY_CLEARANCE - (ordered.length - 1) * HORIZONTAL_STACK_SPACING
|
|
356
|
+
return ordered.map((layout, index) => ({
|
|
357
|
+
...layout,
|
|
358
|
+
clusterId,
|
|
359
|
+
position: {
|
|
360
|
+
x: startX + index * HORIZONTAL_STACK_SPACING - CONTEXT_NODE_HALF_W,
|
|
361
|
+
y: anchorY - CONTEXT_NODE_HALF_H,
|
|
362
|
+
},
|
|
363
|
+
}))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// right side
|
|
367
|
+
if (!hasAncestorDescendant) {
|
|
368
|
+
const startY = anchorY - ((ordered.length - 1) * VERTICAL_STACK_SPACING) / 2
|
|
369
|
+
return ordered.map((layout, index) => ({
|
|
370
|
+
...layout,
|
|
371
|
+
clusterId,
|
|
372
|
+
position: {
|
|
373
|
+
x: boundaryRight + HORIZONTAL_BOUNDARY_CLEARANCE - CONTEXT_NODE_HALF_W,
|
|
374
|
+
y: startY + index * VERTICAL_STACK_SPACING - CONTEXT_NODE_HALF_H,
|
|
375
|
+
},
|
|
376
|
+
}))
|
|
377
|
+
}
|
|
378
|
+
const startX = boundaryRight + HORIZONTAL_BOUNDARY_CLEARANCE
|
|
379
|
+
return ordered.map((layout, index) => ({
|
|
380
|
+
...layout,
|
|
381
|
+
clusterId,
|
|
382
|
+
position: {
|
|
383
|
+
x: startX + index * HORIZONTAL_STACK_SPACING + (index > 0 && clusterExpanded ? CHEVRON_H_CLEARANCE : 0) - CONTEXT_NODE_HALF_W,
|
|
384
|
+
y: anchorY - CONTEXT_NODE_HALF_H,
|
|
385
|
+
},
|
|
386
|
+
}))
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
// Group nodes that the layout already places in the same spatial cluster.
|
|
391
|
+
// Within each cluster, the node with the lowest sortLevel (closest ancestor) is the anchor;
|
|
392
|
+
// all others are children that collapse behind it.
|
|
393
|
+
const clusterGroups = new Map<string, typeof stackedLayouts>()
|
|
394
|
+
for (const layout of stackedLayouts) {
|
|
395
|
+
const group = clusterGroups.get(layout.clusterId) ?? []
|
|
396
|
+
group.push(layout)
|
|
397
|
+
clusterGroups.set(layout.clusterId, group)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const childToAnchorId = new Map<string, string>()
|
|
401
|
+
const anchorGroupChildCount = new Map<string, number>()
|
|
402
|
+
|
|
403
|
+
for (const [, clusterLayouts] of clusterGroups) {
|
|
404
|
+
if (clusterLayouts.length < 2) continue
|
|
405
|
+
const [anchor, ...rest] = clusterLayouts
|
|
406
|
+
// Only collapse nodes whose placement view is actually inside the view tree rooted
|
|
407
|
+
// at the anchor element's child view. Siblings that merely cluster spatially are not collapsed.
|
|
408
|
+
const children = rest.filter((layout) =>
|
|
409
|
+
isAncestorContextNode(snapshot, anchor.contextNode, layout.contextNode),
|
|
410
|
+
)
|
|
411
|
+
if (children.length === 0) continue
|
|
412
|
+
anchorGroupChildCount.set(anchor.contextNode.id, children.length)
|
|
413
|
+
for (const child of children) {
|
|
414
|
+
childToAnchorId.set(child.contextNode.id, anchor.contextNode.id)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const contextNodes: RFNode[] = stackedLayouts
|
|
419
|
+
.filter(({ contextNode }) => {
|
|
420
|
+
const anchorId = childToAnchorId.get(contextNode.id)
|
|
421
|
+
if (anchorId == null) return true
|
|
422
|
+
return expandedAncestorGroups.has(anchorId)
|
|
423
|
+
})
|
|
424
|
+
.map(({ contextNode, position, side }) => {
|
|
425
|
+
const isGroupAnchor = anchorGroupChildCount.has(contextNode.id)
|
|
426
|
+
const groupChildCount = anchorGroupChildCount.get(contextNode.id) ?? 0
|
|
427
|
+
const isGroupExpanded = expandedAncestorGroups.has(contextNode.id)
|
|
428
|
+
return {
|
|
429
|
+
id: contextNode.id,
|
|
430
|
+
type: 'contextNeighborNode',
|
|
431
|
+
position,
|
|
432
|
+
selectable: false,
|
|
433
|
+
draggable: false,
|
|
434
|
+
connectable: false,
|
|
435
|
+
zIndex: isGroupAnchor && isGroupExpanded ? 8 : 6,
|
|
436
|
+
data: {
|
|
437
|
+
element_id: contextNode.anchorElementId,
|
|
438
|
+
name: contextNode.name,
|
|
439
|
+
kind: contextNode.kind,
|
|
440
|
+
description: contextNode.description,
|
|
441
|
+
technology: contextNode.technology,
|
|
442
|
+
logo_url: contextNode.logoUrl,
|
|
443
|
+
technology_connectors: contextNode.technologyConnectors,
|
|
444
|
+
ownerViewIds: contextNode.ownerViewIds,
|
|
445
|
+
ownerViewNames: contextNode.ownerViewNames,
|
|
446
|
+
commonAncestorViewId: contextNode.commonAncestorViewId,
|
|
447
|
+
commonAncestorViewName: contextNode.commonAncestorViewName,
|
|
448
|
+
connectorCount: contextNode.connectorCount,
|
|
449
|
+
onNavigateToView: stableOnNavigateToView,
|
|
450
|
+
onSelectDetails: proxyNodeDetailsById.get(contextNode.id)
|
|
451
|
+
? () => onSelectProxyDetails(proxyNodeDetailsById.get(contextNode.id) as ProxyConnectorDetails)
|
|
452
|
+
: undefined,
|
|
453
|
+
isGroupAnchor,
|
|
454
|
+
groupChildCount,
|
|
455
|
+
isGroupExpanded,
|
|
456
|
+
onToggleGroup: isGroupAnchor ? () => onToggleAncestorGroup(contextNode.id) : undefined,
|
|
457
|
+
side,
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const seenCollapsedPairs = new Set<string>()
|
|
463
|
+
const contextConnectors: RFEdge[] = proxyConnectors.flatMap((connector) => {
|
|
464
|
+
let sourceId = connector.sourceAnchorId
|
|
465
|
+
let targetId = connector.targetAnchorId
|
|
466
|
+
|
|
467
|
+
const sourceAnchor = childToAnchorId.get(sourceId)
|
|
468
|
+
if (sourceAnchor != null && !expandedAncestorGroups.has(sourceAnchor)) sourceId = sourceAnchor
|
|
469
|
+
|
|
470
|
+
const targetAnchor = childToAnchorId.get(targetId)
|
|
471
|
+
if (targetAnchor != null && !expandedAncestorGroups.has(targetAnchor)) targetId = targetAnchor
|
|
472
|
+
|
|
473
|
+
if (sourceId === targetId) return []
|
|
474
|
+
|
|
475
|
+
const pairKey = `${sourceId}::${targetId}`
|
|
476
|
+
if (seenCollapsedPairs.has(pairKey)) return []
|
|
477
|
+
seenCollapsedPairs.add(pairKey)
|
|
478
|
+
|
|
479
|
+
return [{
|
|
480
|
+
id: `proxy:${connector.key}`,
|
|
481
|
+
source: sourceId,
|
|
482
|
+
target: targetId,
|
|
483
|
+
type: 'proxyConnectorEdge',
|
|
484
|
+
animated: false,
|
|
485
|
+
selectable: true,
|
|
486
|
+
updatable: false,
|
|
487
|
+
data: {
|
|
488
|
+
isProxy: true,
|
|
489
|
+
proxyKey: connector.key,
|
|
490
|
+
details: connector.details,
|
|
491
|
+
},
|
|
492
|
+
style: {
|
|
493
|
+
stroke: 'rgba(255, 255, 255, 0.2)',
|
|
494
|
+
strokeWidth: 2,
|
|
495
|
+
},
|
|
496
|
+
}]
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
return { contextNodes: [ContextBoundaryElement, ...contextNodes], contextConnectors, proxyConnectorDetailsByKey }
|
|
500
|
+
}, [snapshot, settings, viewId, viewElements, rfNodes, stableOnNavigateToView, onSelectProxyDetails, expandedAncestorGroups, onToggleAncestorGroup])
|
|
501
|
+
}
|