@tldiagram/core-ui 1.95.0 → 2.0.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/api/client.d.ts +184 -3
- package/dist/components/ConnectorPanel.d.ts +5 -1
- package/dist/components/CrossBranchControls.d.ts +4 -3
- package/dist/components/ElementNode.d.ts +5 -0
- package/dist/components/ElementPanel.d.ts +6 -1
- package/dist/components/LayoutSection.d.ts +2 -1
- package/dist/components/MergeDialog.d.ts +16 -0
- package/dist/components/MiniZoomOnboarding.d.ts +2 -1
- package/dist/components/NodeContainer.d.ts +2 -0
- package/dist/components/ProxyConnectorPanel.d.ts +4 -1
- package/dist/components/ViewExplorer/index.d.ts +1 -1
- package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
- package/dist/components/ViewFloatingMenu.d.ts +8 -1
- package/dist/components/ViewGridNode.d.ts +3 -0
- package/dist/components/ViewPanel.d.ts +2 -1
- package/dist/components/WorkspacePanel.d.ts +2 -0
- package/dist/components/ZUI/ZUICanvas.d.ts +5 -0
- package/dist/components/ZUI/focus.d.ts +32 -0
- package/dist/components/ZUI/focus.test.d.ts +1 -0
- package/dist/components/ZUI/layout.d.ts +2 -2
- package/dist/components/ZUI/proxy.d.ts +20 -4
- package/dist/components/ZUI/renderer.d.ts +35 -1
- package/dist/components/ZUI/types.d.ts +6 -0
- package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
- package/dist/context/WorkspaceVersionContext.d.ts +49 -0
- package/dist/crossBranch/resolve.d.ts +39 -2
- package/dist/crossBranch/resolve.test.d.ts +1 -0
- package/dist/crossBranch/settings.d.ts +6 -1
- package/dist/crossBranch/types.d.ts +8 -0
- package/dist/hooks/useElementSearch.d.ts +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14597 -12083
- package/dist/pages/InfiniteZoom.d.ts +1 -0
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
- package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
- package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
- package/dist/pages/viewsJumpSearch.d.ts +22 -0
- package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
- package/dist/store/useStore.d.ts +3 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/elementIcon.d.ts +2 -0
- package/dist/utils/elementIcon.test.d.ts +1 -0
- package/dist/utils/sourceEditor.d.ts +7 -0
- package/dist/utils/watchDiffSummary.d.ts +34 -0
- package/package.json +2 -2
- package/src/App.tsx +12 -8
- package/src/api/client.ts +488 -26
- package/src/components/CodePreviewPanel.tsx +90 -16
- package/src/components/ConnectorPanel.tsx +34 -3
- package/src/components/ContextNeighborElement.tsx +2 -5
- package/src/components/CrossBranchControls.tsx +46 -17
- package/src/components/ElementNode.tsx +98 -47
- package/src/components/ElementPanel.tsx +62 -25
- package/src/components/InlineElementAdder.tsx +8 -3
- package/src/components/LayoutSection.tsx +4 -1
- package/src/components/MergeDialog.tsx +269 -0
- package/src/components/MiniZoomOnboarding.tsx +29 -22
- package/src/components/NodeContainer.tsx +55 -17
- package/src/components/ProxyConnectorPanel.tsx +58 -16
- package/src/components/ViewBezierConnector.tsx +116 -21
- package/src/components/ViewExplorer/index.tsx +1 -1
- package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
- package/src/components/ViewFloatingMenu.tsx +110 -1
- package/src/components/ViewGridNode.tsx +59 -8
- package/src/components/ViewPanel.tsx +3 -2
- package/src/components/WorkspacePanel.tsx +938 -0
- package/src/components/ZUI/ZUICanvas.tsx +226 -127
- package/src/components/ZUI/focus.test.ts +534 -0
- package/src/components/ZUI/focus.ts +293 -0
- package/src/components/ZUI/layout.ts +7 -11
- package/src/components/ZUI/proxy.ts +470 -114
- package/src/components/ZUI/renderer.ts +510 -134
- package/src/components/ZUI/types.ts +6 -0
- package/src/components/ZUI/useZUIInteraction.ts +66 -29
- package/src/context/WorkspaceVersionContext.tsx +126 -0
- package/src/crossBranch/resolve.test.ts +342 -0
- package/src/crossBranch/resolve.ts +368 -68
- package/src/crossBranch/settings.ts +49 -3
- package/src/crossBranch/types.ts +9 -0
- package/src/hooks/useElementSearch.ts +45 -0
- package/src/index.css +11 -0
- package/src/index.ts +7 -0
- package/src/pages/AppearanceSettings.tsx +24 -1
- package/src/pages/Dependencies.tsx +231 -65
- package/src/pages/InfiniteZoom.tsx +76 -27
- package/src/pages/Settings.tsx +1 -1
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
- package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
- package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
- package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
- package/src/pages/ViewEditor/index.tsx +549 -59
- package/src/pages/Views.tsx +112 -41
- package/src/pages/ViewsGrid.tsx +332 -113
- package/src/pages/viewsJumpSearch.test.ts +193 -0
- package/src/pages/viewsJumpSearch.ts +111 -0
- package/src/store/useStore.ts +58 -0
- package/src/types/index.ts +10 -0
- package/src/utils/elementIcon.test.ts +28 -0
- package/src/utils/elementIcon.ts +20 -0
- package/src/utils/sourceEditor.ts +46 -0
- package/src/utils/watchDiffSummary.ts +159 -0
|
@@ -27,17 +27,29 @@ import { Link as RouterLink } from 'react-router-dom'
|
|
|
27
27
|
import { ExternalLinkIcon } from '@chakra-ui/icons'
|
|
28
28
|
import type { ExploreData } from '../../types'
|
|
29
29
|
import { computeLayout } from './layout'
|
|
30
|
-
import { renderFrame, getExpandThresholds, setOnImageLoadCallback, setHighlightedTags as setRendererHighlightedTags, setHiddenTags as setRendererHiddenTags, setHighlightColor as setRendererHighlightColor } from './renderer'
|
|
30
|
+
import { renderFrame, getExpandThresholds, getCameraRebase, rawCameraView, screenToWorldX, screenToWorldY, worldToScreenX, worldToScreenY, setOnImageLoadCallback, setHighlightedTags as setRendererHighlightedTags, setHiddenTags as setRendererHiddenTags, setHighlightColor as setRendererHighlightColor, setVersionDiff as setRendererVersionDiff } from './renderer'
|
|
31
31
|
import { useZUIInteraction } from './useZUIInteraction'
|
|
32
32
|
import type { DiagramGroupLayout, ZUIViewState } from './types'
|
|
33
|
+
import { findDiagramFocusTarget, findElementFocusTarget, viewportForDiagramFocusTarget, viewportForElementFocusTarget } from './focus'
|
|
33
34
|
import { buildWorkspaceGraphSnapshot } from '../../crossBranch/graph'
|
|
34
35
|
import type { CrossBranchContextSettings } from '../../crossBranch/types'
|
|
35
|
-
import
|
|
36
|
-
import {
|
|
36
|
+
import { DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA } from '../../crossBranch/settings'
|
|
37
|
+
import type { WorkspaceVersionFollowTarget, WorkspaceVersionPreview } from '../../context/WorkspaceVersionContext'
|
|
38
|
+
import {
|
|
39
|
+
buildProxyConnectorSpatialIndex,
|
|
40
|
+
buildVisibleProxyConnectors,
|
|
41
|
+
collectVisibleNodeAnchors,
|
|
42
|
+
drawVisibleDirectProxyBadges,
|
|
43
|
+
drawVisibleProxyConnectors,
|
|
44
|
+
findHoveredProxyConnector,
|
|
45
|
+
type ProxyConnectorSpatialIndex,
|
|
46
|
+
type VisibleNodeAnchor,
|
|
47
|
+
} from './proxy'
|
|
37
48
|
|
|
38
49
|
export interface ZUICanvasHandle {
|
|
39
50
|
fitView(): void
|
|
40
51
|
focusDiagram(viewId: number): boolean
|
|
52
|
+
focusElement(viewId: number, elementId: number): boolean
|
|
41
53
|
setCameraFrame(frame: ZUICameraFrame): boolean
|
|
42
54
|
}
|
|
43
55
|
|
|
@@ -51,9 +63,12 @@ interface Props {
|
|
|
51
63
|
onReady?: () => void
|
|
52
64
|
onZoom?: () => void
|
|
53
65
|
onPan?: () => void
|
|
66
|
+
initialCameraFrame?: ZUICameraFrame
|
|
54
67
|
highlightedTags?: string[]
|
|
55
68
|
highlightColor?: string
|
|
56
69
|
hiddenTags?: string[]
|
|
70
|
+
versionPreview?: WorkspaceVersionPreview | null
|
|
71
|
+
versionFollowTarget?: WorkspaceVersionFollowTarget | null
|
|
57
72
|
crossBranchSettings: CrossBranchContextSettings
|
|
58
73
|
hoverLocked?: boolean
|
|
59
74
|
}
|
|
@@ -70,6 +85,26 @@ interface PathItem {
|
|
|
70
85
|
absH: number
|
|
71
86
|
}
|
|
72
87
|
|
|
88
|
+
function rebaseVisibleNodeAnchors(
|
|
89
|
+
anchors: Map<string, VisibleNodeAnchor>,
|
|
90
|
+
originX: number,
|
|
91
|
+
originY: number,
|
|
92
|
+
): Map<string, VisibleNodeAnchor> {
|
|
93
|
+
const rebased = new Map<string, VisibleNodeAnchor>()
|
|
94
|
+
for (const [nodeId, anchor] of anchors) {
|
|
95
|
+
rebased.set(nodeId, {
|
|
96
|
+
...anchor,
|
|
97
|
+
worldX: anchor.worldX - originX,
|
|
98
|
+
worldY: anchor.worldY - originY,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
return rebased
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function anchorViewForZoom(zoom: number): ZUIViewState {
|
|
105
|
+
return { x: 0, y: 0, zoom: Math.max(0.0001, zoom) }
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
function getPathAt(
|
|
74
109
|
view: ZUIViewState,
|
|
75
110
|
groups: DiagramGroupLayout[],
|
|
@@ -79,8 +114,8 @@ function getPathAt(
|
|
|
79
114
|
if (canvasW === 0 || canvasH === 0) return []
|
|
80
115
|
|
|
81
116
|
// World center of the screen
|
|
82
|
-
const worldCenterX = (canvasW / 2
|
|
83
|
-
const worldCenterY = (canvasH / 2
|
|
117
|
+
const worldCenterX = screenToWorldX(canvasW / 2, view)
|
|
118
|
+
const worldCenterY = screenToWorldY(canvasH / 2, view)
|
|
84
119
|
const thresholds = getExpandThresholds(canvasW)
|
|
85
120
|
|
|
86
121
|
for (const group of groups) {
|
|
@@ -176,70 +211,6 @@ function getPathAt(
|
|
|
176
211
|
return []
|
|
177
212
|
}
|
|
178
213
|
|
|
179
|
-
function findDiagramFocusTarget(groups: DiagramGroupLayout[], viewId: number): PathItem | null {
|
|
180
|
-
for (const group of groups) {
|
|
181
|
-
if (group.diagramId === viewId) {
|
|
182
|
-
return {
|
|
183
|
-
id: `g-${group.diagramId}`,
|
|
184
|
-
label: group.label,
|
|
185
|
-
type: 'group',
|
|
186
|
-
absX: group.worldX,
|
|
187
|
-
absY: group.worldY,
|
|
188
|
-
absW: group.worldW,
|
|
189
|
-
absH: group.worldH,
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const found = findLinkedDiagramInNodes(viewId, group.nodes, 0, 0, 1, 0, 0)
|
|
194
|
-
if (found) return found
|
|
195
|
-
}
|
|
196
|
-
return null
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function findLinkedDiagramInNodes(
|
|
200
|
-
viewId: number,
|
|
201
|
-
nodes: DiagramGroupLayout['nodes'],
|
|
202
|
-
parentAbsX: number,
|
|
203
|
-
parentAbsY: number,
|
|
204
|
-
parentAbsScale: number,
|
|
205
|
-
parentChildOffsetX: number,
|
|
206
|
-
parentChildOffsetY: number,
|
|
207
|
-
): PathItem | null {
|
|
208
|
-
for (const node of nodes) {
|
|
209
|
-
const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
|
|
210
|
-
const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
|
|
211
|
-
const absW = node.worldW * parentAbsScale
|
|
212
|
-
const absH = node.worldH * parentAbsScale
|
|
213
|
-
|
|
214
|
-
if (node.linkedDiagramId === viewId) {
|
|
215
|
-
return {
|
|
216
|
-
id: node.id,
|
|
217
|
-
label: node.linkedDiagramLabel || node.label,
|
|
218
|
-
type: 'node',
|
|
219
|
-
isCircular: node.isCircular,
|
|
220
|
-
absX,
|
|
221
|
-
absY,
|
|
222
|
-
absW,
|
|
223
|
-
absH,
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (node.children.length > 0) {
|
|
228
|
-
const found = findLinkedDiagramInNodes(
|
|
229
|
-
viewId,
|
|
230
|
-
node.children,
|
|
231
|
-
absX,
|
|
232
|
-
absY,
|
|
233
|
-
parentAbsScale * node.childScale,
|
|
234
|
-
node.childOffsetX,
|
|
235
|
-
node.childOffsetY,
|
|
236
|
-
)
|
|
237
|
-
if (found) return found
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return null
|
|
241
|
-
}
|
|
242
|
-
|
|
243
214
|
function easeOutQuart(t: number): number {
|
|
244
215
|
return 1 - Math.pow(1 - t, 4)
|
|
245
216
|
}
|
|
@@ -319,25 +290,27 @@ function findFirstExpandableNodeInTree(
|
|
|
319
290
|
return null
|
|
320
291
|
}
|
|
321
292
|
|
|
322
|
-
export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, highlightedTags, highlightColor, hiddenTags, crossBranchSettings, hoverLocked = false }, ref) {
|
|
293
|
+
export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, initialCameraFrame, highlightedTags, highlightColor, hiddenTags, versionPreview, versionFollowTarget, crossBranchSettings, hoverLocked = false }, ref) {
|
|
323
294
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
324
295
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
325
296
|
const cameraTransitionRef = useRef<number | null>(null)
|
|
326
297
|
const [initialized, setInitialized] = useState(false)
|
|
327
298
|
const [containerSize, setContainerSize] = useState({ w: 0, h: 0 })
|
|
328
299
|
const isMobileLayout = useBreakpointValue({ base: true, md: false }) ?? false
|
|
300
|
+
const debugViewport = useMemo(() => typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugZuiCamera'), [])
|
|
329
301
|
|
|
330
302
|
// ── Layout ──────────────────────────────────────────────────────
|
|
331
303
|
const layout = useMemo(() => computeLayout(data), [data])
|
|
332
304
|
const workspaceSnapshot = useMemo(() => buildWorkspaceGraphSnapshot(data), [data])
|
|
333
|
-
// Holds the
|
|
334
|
-
//
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
const resolveHoveredProxyItem = useCallback((worldX: number, worldY: number, view: ZUIViewState
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
305
|
+
// Holds the latest proxy hover index so mousemove can query it without
|
|
306
|
+
// rebuilding anchors or connector geometry.
|
|
307
|
+
const proxyHoverIndexRef = useRef<ProxyConnectorSpatialIndex | null>(null)
|
|
308
|
+
|
|
309
|
+
const resolveHoveredProxyItem = useCallback((worldX: number, worldY: number, view: ZUIViewState) => {
|
|
310
|
+
const index = proxyHoverIndexRef.current
|
|
311
|
+
if (!index) return null
|
|
312
|
+
return findHoveredProxyConnector(worldX, worldY, index, view)
|
|
313
|
+
}, [])
|
|
341
314
|
|
|
342
315
|
// ── Interaction ─────────────────────────────────────────────────
|
|
343
316
|
const { viewState, viewStateRef, setViewState, fitView, maxZoom, hoveredItem, setHoveredItem, setHoverLocked } = useZUIInteraction(
|
|
@@ -351,42 +324,100 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
351
324
|
resolveHoveredProxyItem,
|
|
352
325
|
)
|
|
353
326
|
|
|
354
|
-
// Anchor positions
|
|
327
|
+
// Anchor positions are zoom-dependent, but not pan-dependent. Keeping pan out
|
|
328
|
+
// of this memo avoids re-walking the ZUI tree during drag/trackpad movement.
|
|
355
329
|
const anchors = useMemo(() =>
|
|
356
|
-
collectVisibleNodeAnchors(layout.groups, viewState, containerSize.w || 1, hiddenTags),
|
|
357
|
-
[layout.groups, viewState, containerSize.w, hiddenTags],
|
|
330
|
+
collectVisibleNodeAnchors(layout.groups, anchorViewForZoom(viewState.zoom), containerSize.w || 1, hiddenTags),
|
|
331
|
+
[layout.groups, viewState.zoom, containerSize.w, hiddenTags],
|
|
358
332
|
)
|
|
359
333
|
|
|
334
|
+
const viewportBounds = useMemo(() => {
|
|
335
|
+
const zoom = Math.max(0.0001, viewState.zoom)
|
|
336
|
+
const stableView = { ...viewState, zoom }
|
|
337
|
+
const minX = screenToWorldX(0, stableView)
|
|
338
|
+
const minY = screenToWorldY(0, stableView)
|
|
339
|
+
const maxX = screenToWorldX(containerSize.w, stableView)
|
|
340
|
+
const maxY = screenToWorldY(containerSize.h, stableView)
|
|
341
|
+
return {
|
|
342
|
+
minX,
|
|
343
|
+
minY,
|
|
344
|
+
maxX,
|
|
345
|
+
maxY,
|
|
346
|
+
centerX: (minX + maxX) / 2,
|
|
347
|
+
centerY: (minY + maxY) / 2,
|
|
348
|
+
}
|
|
349
|
+
}, [containerSize.h, containerSize.w, viewState])
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
if (!debugViewport) return
|
|
353
|
+
const cameraRebase = getCameraRebase(viewState, containerSize.w, containerSize.h)
|
|
354
|
+
console.debug('[ZUICanvas] viewport', {
|
|
355
|
+
x: viewState.x,
|
|
356
|
+
y: viewState.y,
|
|
357
|
+
zoom: viewState.zoom,
|
|
358
|
+
width: containerSize.w,
|
|
359
|
+
height: containerSize.h,
|
|
360
|
+
minX: viewportBounds.minX,
|
|
361
|
+
minY: viewportBounds.minY,
|
|
362
|
+
maxX: viewportBounds.maxX,
|
|
363
|
+
maxY: viewportBounds.maxY,
|
|
364
|
+
centerX: viewportBounds.centerX,
|
|
365
|
+
centerY: viewportBounds.centerY,
|
|
366
|
+
renderX: cameraRebase.view.x,
|
|
367
|
+
renderY: cameraRebase.view.y,
|
|
368
|
+
renderOriginX: cameraRebase.originX,
|
|
369
|
+
renderOriginY: cameraRebase.originY,
|
|
370
|
+
})
|
|
371
|
+
}, [containerSize.h, containerSize.w, debugViewport, viewState, viewportBounds])
|
|
372
|
+
|
|
360
373
|
// A stable string key encoding which element→nodeId pairs are currently visible.
|
|
361
374
|
// This only changes when nodes cross zoom-expansion thresholds not on every pan pixel.
|
|
362
375
|
const visibleElementSig = useMemo(() =>
|
|
363
376
|
Array.from(anchors.visibleAnchors.entries())
|
|
364
377
|
.sort(([a], [b]) => a - b)
|
|
365
|
-
.map(([id, anchor]) => `${id}:${anchor.nodeId}`)
|
|
378
|
+
.map(([id, anchor]) => `${id}:${anchor.nodeId}:${anchor.renderAlpha >= (crossBranchSettings.minConnectorAnchorAlpha ?? DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA) ? 1 : 0}`)
|
|
366
379
|
.join(','),
|
|
367
|
-
[anchors.visibleAnchors],
|
|
380
|
+
[anchors.visibleAnchors, crossBranchSettings.minConnectorAnchorAlpha],
|
|
368
381
|
)
|
|
369
|
-
|
|
370
|
-
|
|
382
|
+
const proxySettingsSig = [
|
|
383
|
+
crossBranchSettings.enabled,
|
|
384
|
+
crossBranchSettings.depth,
|
|
385
|
+
crossBranchSettings.connectorBudget,
|
|
386
|
+
crossBranchSettings.connectorPriority,
|
|
387
|
+
crossBranchSettings.minConnectorAnchorAlpha ?? '',
|
|
388
|
+
crossBranchSettings.maxProxyConnectorGroups ?? '',
|
|
389
|
+
].join(':')
|
|
390
|
+
|
|
391
|
+
// Connector topology follows visible anchor identity, not camera position.
|
|
392
|
+
// Continuous pan/zoom renders reuse the previous topology until zoom changes
|
|
393
|
+
// which elements have visible/eligible anchors.
|
|
371
394
|
const proxyConnectors = useMemo(() => {
|
|
372
395
|
const resolved = buildVisibleProxyConnectors(workspaceSnapshot, anchors.visibleAnchors, crossBranchSettings)
|
|
373
|
-
proxyConnectorsRef.current = resolved
|
|
374
396
|
return resolved
|
|
375
397
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
376
|
-
}, [workspaceSnapshot, visibleElementSig,
|
|
398
|
+
}, [workspaceSnapshot, visibleElementSig, proxySettingsSig])
|
|
399
|
+
|
|
400
|
+
const proxyHoverIndex = useMemo(() => (
|
|
401
|
+
buildProxyConnectorSpatialIndex(proxyConnectors.connectors, anchors.byNodeId)
|
|
402
|
+
), [proxyConnectors.connectors, anchors.byNodeId])
|
|
403
|
+
proxyHoverIndexRef.current = proxyHoverIndex
|
|
377
404
|
|
|
378
405
|
const visibleProxyState = useMemo(() => ({
|
|
379
406
|
...anchors,
|
|
380
|
-
proxyConnectors,
|
|
407
|
+
proxyConnectors: proxyConnectors.connectors,
|
|
408
|
+
hiddenProxyBadges: proxyConnectors.hiddenBadges,
|
|
381
409
|
}), [anchors, proxyConnectors])
|
|
382
410
|
|
|
383
411
|
const visibleProxyStateRef = useRef(visibleProxyState)
|
|
384
412
|
visibleProxyStateRef.current = visibleProxyState
|
|
385
413
|
|
|
386
414
|
const labelBgRef = useRef('#171923')
|
|
415
|
+
const accentRef = useRef('#63b3ed')
|
|
387
416
|
useEffect(() => {
|
|
388
417
|
const update = () => {
|
|
389
|
-
|
|
418
|
+
const styles = getComputedStyle(document.documentElement)
|
|
419
|
+
labelBgRef.current = styles.getPropertyValue('--chakra-colors-gray-900').trim() || '#171923'
|
|
420
|
+
accentRef.current = styles.getPropertyValue('--accent').trim() || '#63b3ed'
|
|
390
421
|
needsRedrawRef.current = true
|
|
391
422
|
}
|
|
392
423
|
update()
|
|
@@ -414,8 +445,8 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
414
445
|
absY = g.worldY + g.diagramY - absH
|
|
415
446
|
}
|
|
416
447
|
|
|
417
|
-
const sx = absX
|
|
418
|
-
const sy = absY
|
|
448
|
+
const sx = worldToScreenX(absX, viewState)
|
|
449
|
+
const sy = worldToScreenY(absY, viewState)
|
|
419
450
|
const sw = absW * viewState.zoom
|
|
420
451
|
const sh = absH * viewState.zoom
|
|
421
452
|
|
|
@@ -472,36 +503,13 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
472
503
|
setViewState({ x, y, zoom })
|
|
473
504
|
}, [containerSize, maxZoom, setViewState, setHoveredItem])
|
|
474
505
|
|
|
475
|
-
const
|
|
476
|
-
const el = containerRef.current
|
|
477
|
-
const target = findDiagramFocusTarget(layout.groups, viewId)
|
|
478
|
-
if (!el || !target) return false
|
|
479
|
-
|
|
480
|
-
const canvasW = el.offsetWidth
|
|
481
|
-
const canvasH = el.offsetHeight
|
|
482
|
-
if (canvasW === 0 || canvasH === 0) return false
|
|
483
|
-
|
|
484
|
-
setHoveredItem(null, true)
|
|
485
|
-
|
|
486
|
-
const padding = isMobileLayout ? 0.18 : 0.16
|
|
487
|
-
const bboxW = Math.max(1, target.absW)
|
|
488
|
-
const bboxH = Math.max(1, target.absH)
|
|
489
|
-
const zoom = Math.min(
|
|
490
|
-
(canvasW * (1 - padding * 2)) / bboxW,
|
|
491
|
-
(canvasH * (1 - padding * 2)) / bboxH,
|
|
492
|
-
maxZoom,
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
const x = (canvasW - bboxW * zoom) / 2 - target.absX * zoom
|
|
496
|
-
const y = (canvasH - bboxH * zoom) / 2 - target.absY * zoom
|
|
497
|
-
|
|
506
|
+
const animateToViewport = useCallback((to: ZUIViewState) => {
|
|
498
507
|
if (cameraTransitionRef.current !== null) {
|
|
499
508
|
cancelAnimationFrame(cameraTransitionRef.current)
|
|
500
509
|
cameraTransitionRef.current = null
|
|
501
510
|
}
|
|
502
511
|
|
|
503
|
-
const from = viewStateRef.current
|
|
504
|
-
const to = { x, y, zoom }
|
|
512
|
+
const from = rawCameraView(viewStateRef.current)
|
|
505
513
|
const duration = 520
|
|
506
514
|
const startedAt = performance.now()
|
|
507
515
|
|
|
@@ -523,8 +531,41 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
523
531
|
}
|
|
524
532
|
|
|
525
533
|
cameraTransitionRef.current = requestAnimationFrame(step)
|
|
534
|
+
}, [setViewState, viewStateRef])
|
|
535
|
+
|
|
536
|
+
const focusDiagram = useCallback((viewId: number) => {
|
|
537
|
+
const el = containerRef.current
|
|
538
|
+
const target = findDiagramFocusTarget(layout.groups, viewId)
|
|
539
|
+
if (!el || !target) return false
|
|
540
|
+
|
|
541
|
+
const canvasW = el.offsetWidth
|
|
542
|
+
const canvasH = el.offsetHeight
|
|
543
|
+
if (canvasW === 0 || canvasH === 0) return false
|
|
544
|
+
|
|
545
|
+
const to = viewportForDiagramFocusTarget(target, canvasW, canvasH, maxZoom, isMobileLayout)
|
|
546
|
+
if (!to) return false
|
|
547
|
+
|
|
548
|
+
setHoveredItem(null, true)
|
|
549
|
+
animateToViewport(to)
|
|
526
550
|
return true
|
|
527
|
-
}, [isMobileLayout, layout.groups, maxZoom, setHoveredItem
|
|
551
|
+
}, [animateToViewport, isMobileLayout, layout.groups, maxZoom, setHoveredItem])
|
|
552
|
+
|
|
553
|
+
const focusElement = useCallback((viewId: number, elementId: number) => {
|
|
554
|
+
const el = containerRef.current
|
|
555
|
+
const target = findElementFocusTarget(layout.groups, viewId, elementId)
|
|
556
|
+
if (!el || !target) return false
|
|
557
|
+
|
|
558
|
+
const canvasW = el.offsetWidth
|
|
559
|
+
const canvasH = el.offsetHeight
|
|
560
|
+
if (canvasW === 0 || canvasH === 0) return false
|
|
561
|
+
|
|
562
|
+
const to = viewportForElementFocusTarget(target, canvasW, canvasH, maxZoom, isMobileLayout)
|
|
563
|
+
if (!to) return false
|
|
564
|
+
|
|
565
|
+
setHoveredItem(null, true)
|
|
566
|
+
animateToViewport(to)
|
|
567
|
+
return true
|
|
568
|
+
}, [animateToViewport, isMobileLayout, layout.groups, maxZoom, setHoveredItem])
|
|
528
569
|
|
|
529
570
|
const setCameraFrame = useCallback((frame: ZUICameraFrame) => {
|
|
530
571
|
if (frame.profile !== 'detail-to-overview') return false
|
|
@@ -583,6 +624,11 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
583
624
|
return true
|
|
584
625
|
}, [layout.groups, maxZoom, setHoveredItem, setViewState])
|
|
585
626
|
|
|
627
|
+
const fitInitialView = useCallback((w: number, h: number) => {
|
|
628
|
+
if (initialCameraFrame && setCameraFrame(initialCameraFrame)) return
|
|
629
|
+
fitView(w, h, layout.bbox)
|
|
630
|
+
}, [fitView, initialCameraFrame, layout.bbox, setCameraFrame])
|
|
631
|
+
|
|
586
632
|
useEffect(() => {
|
|
587
633
|
return () => {
|
|
588
634
|
if (cameraTransitionRef.current !== null) {
|
|
@@ -600,14 +646,13 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
600
646
|
// Only set as initialized if we have valid dimensions
|
|
601
647
|
if (w > 0 && h > 0) {
|
|
602
648
|
setContainerSize({ w, h })
|
|
603
|
-
|
|
649
|
+
fitInitialView(w, h)
|
|
604
650
|
if (!initialized) {
|
|
605
651
|
setInitialized(true)
|
|
606
652
|
onReady?.()
|
|
607
653
|
}
|
|
608
654
|
}
|
|
609
|
-
|
|
610
|
-
}, [layout, initialized, onReady])
|
|
655
|
+
}, [initialized, onReady, fitInitialView])
|
|
611
656
|
|
|
612
657
|
// ── Expose fitView to parent ─────────────────────────────────────
|
|
613
658
|
useImperativeHandle(
|
|
@@ -620,9 +665,10 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
620
665
|
fitView(el.offsetWidth, el.offsetHeight, layout.bbox)
|
|
621
666
|
},
|
|
622
667
|
focusDiagram,
|
|
668
|
+
focusElement,
|
|
623
669
|
setCameraFrame,
|
|
624
670
|
}),
|
|
625
|
-
[fitView, focusDiagram, layout.bbox, setCameraFrame, setHoveredItem],
|
|
671
|
+
[fitView, focusDiagram, focusElement, layout.bbox, setCameraFrame, setHoveredItem],
|
|
626
672
|
)
|
|
627
673
|
|
|
628
674
|
// ── RAF render loop ──────────────────────────────────────────────
|
|
@@ -670,7 +716,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
670
716
|
|
|
671
717
|
// Trigger initialization if it hasn't happened yet
|
|
672
718
|
if (!initialized && w > 0 && h > 0) {
|
|
673
|
-
|
|
719
|
+
fitInitialView(w, h)
|
|
674
720
|
setInitialized(true)
|
|
675
721
|
onReady?.()
|
|
676
722
|
}
|
|
@@ -680,7 +726,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
680
726
|
ro.observe(container)
|
|
681
727
|
resize()
|
|
682
728
|
return () => ro.disconnect()
|
|
683
|
-
}, [initialized,
|
|
729
|
+
}, [initialized, fitInitialView, onReady])
|
|
684
730
|
|
|
685
731
|
useEffect(() => {
|
|
686
732
|
if (!initialized) return // Don't start loop until initialized
|
|
@@ -714,14 +760,29 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
714
760
|
ctx.save()
|
|
715
761
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
716
762
|
const occupiedLabelRects = renderFrame(ctx, layout.groups, currentView, w, h)
|
|
763
|
+
const cameraRebase = getCameraRebase(currentView, w, h)
|
|
764
|
+
const rebasedProxyAnchors = rebaseVisibleNodeAnchors(
|
|
765
|
+
visibleProxyStateRef.current.byNodeId,
|
|
766
|
+
cameraRebase.originX,
|
|
767
|
+
cameraRebase.originY,
|
|
768
|
+
)
|
|
717
769
|
ctx.save()
|
|
718
|
-
ctx.translate(
|
|
719
|
-
ctx.scale(
|
|
770
|
+
ctx.translate(cameraRebase.view.x, cameraRebase.view.y)
|
|
771
|
+
ctx.scale(cameraRebase.view.zoom, cameraRebase.view.zoom)
|
|
720
772
|
drawVisibleProxyConnectors(
|
|
721
773
|
ctx,
|
|
722
774
|
visibleProxyStateRef.current.proxyConnectors,
|
|
723
|
-
|
|
724
|
-
|
|
775
|
+
rebasedProxyAnchors,
|
|
776
|
+
cameraRebase.view.zoom,
|
|
777
|
+
labelBgRef.current,
|
|
778
|
+
accentRef.current,
|
|
779
|
+
occupiedLabelRects,
|
|
780
|
+
)
|
|
781
|
+
drawVisibleDirectProxyBadges(
|
|
782
|
+
ctx,
|
|
783
|
+
visibleProxyStateRef.current.hiddenProxyBadges,
|
|
784
|
+
rebasedProxyAnchors,
|
|
785
|
+
cameraRebase.view.zoom,
|
|
725
786
|
labelBgRef.current,
|
|
726
787
|
occupiedLabelRects,
|
|
727
788
|
)
|
|
@@ -756,6 +817,30 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
756
817
|
needsRedrawRef.current = true
|
|
757
818
|
}, [hiddenTags])
|
|
758
819
|
|
|
820
|
+
useEffect(() => {
|
|
821
|
+
const pulsedElementChanges = new Map<number, string>()
|
|
822
|
+
const pulsedElementLineDeltas = new Map<number, { added: number; removed: number }>()
|
|
823
|
+
if (versionFollowTarget?.resourceType === 'element' && versionFollowTarget.resourceId) {
|
|
824
|
+
const change = versionFollowTarget.changeType ?? versionPreview?.elementChanges.get(versionFollowTarget.resourceId)
|
|
825
|
+
if (change) pulsedElementChanges.set(versionFollowTarget.resourceId, change)
|
|
826
|
+
}
|
|
827
|
+
setRendererVersionDiff(
|
|
828
|
+
pulsedElementChanges,
|
|
829
|
+
versionPreview?.connectorChanges ?? new Map(),
|
|
830
|
+
versionPreview?.elementLineDeltas ?? pulsedElementLineDeltas,
|
|
831
|
+
)
|
|
832
|
+
needsRedrawRef.current = true
|
|
833
|
+
}, [versionPreview, versionFollowTarget])
|
|
834
|
+
|
|
835
|
+
useEffect(() => {
|
|
836
|
+
if (!initialized || !versionFollowTarget?.viewId) return
|
|
837
|
+
if (versionFollowTarget.resourceType === 'element' && versionFollowTarget.resourceId) {
|
|
838
|
+
focusElement(versionFollowTarget.viewId, versionFollowTarget.resourceId)
|
|
839
|
+
return
|
|
840
|
+
}
|
|
841
|
+
focusDiagram(versionFollowTarget.viewId)
|
|
842
|
+
}, [focusDiagram, focusElement, initialized, versionFollowTarget?.resourceId, versionFollowTarget?.resourceType, versionFollowTarget?.token, versionFollowTarget?.viewId])
|
|
843
|
+
|
|
759
844
|
useEffect(() => {
|
|
760
845
|
setHoverLocked(hoverLocked)
|
|
761
846
|
}, [hoverLocked, setHoverLocked])
|
|
@@ -766,6 +851,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
766
851
|
setRendererHighlightedTags(new Set())
|
|
767
852
|
setRendererHighlightColor('')
|
|
768
853
|
setRendererHiddenTags(new Set())
|
|
854
|
+
setRendererVersionDiff(new Map(), new Map())
|
|
769
855
|
}
|
|
770
856
|
}, [])
|
|
771
857
|
|
|
@@ -952,6 +1038,19 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
952
1038
|
</Text>
|
|
953
1039
|
<Text fontSize="xs" color="gray.400">{hoveredItem.data.details.label}</Text>
|
|
954
1040
|
</VStack>
|
|
1041
|
+
<VStack align="start" spacing={1} width="full">
|
|
1042
|
+
<Text color="gray.400" fontSize="2xs" fontWeight="600" letterSpacing="wider">UNDERLYING PATHS</Text>
|
|
1043
|
+
{hoveredItem.data.details.connectors.slice(0, 4).map((leaf, index) => (
|
|
1044
|
+
<Text key={`${leaf.connector.id}-${index}`} fontSize="xs" color="gray.200">
|
|
1045
|
+
{leaf.source.actualElementName} → {leaf.target.actualElementName}
|
|
1046
|
+
</Text>
|
|
1047
|
+
))}
|
|
1048
|
+
{hoveredItem.data.details.connectors.length > 4 && (
|
|
1049
|
+
<Text fontSize="xs" color="gray.500">
|
|
1050
|
+
+{hoveredItem.data.details.connectors.length - 4} more
|
|
1051
|
+
</Text>
|
|
1052
|
+
)}
|
|
1053
|
+
</VStack>
|
|
955
1054
|
<Divider borderColor="whiteAlpha.200" />
|
|
956
1055
|
<VStack align="stretch" spacing={2} width="full">
|
|
957
1056
|
{hoveredItem.data.details.ownerViewIds.map((ownerViewId, index) => (
|