@tldiagram/core-ui 1.95.1 → 2.0.1
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/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 +4 -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 +16529 -14030
- 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/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 +216 -122
- 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 +41 -19
- 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
|
|
|
@@ -55,6 +67,8 @@ interface Props {
|
|
|
55
67
|
highlightedTags?: string[]
|
|
56
68
|
highlightColor?: string
|
|
57
69
|
hiddenTags?: string[]
|
|
70
|
+
versionPreview?: WorkspaceVersionPreview | null
|
|
71
|
+
versionFollowTarget?: WorkspaceVersionFollowTarget | null
|
|
58
72
|
crossBranchSettings: CrossBranchContextSettings
|
|
59
73
|
hoverLocked?: boolean
|
|
60
74
|
}
|
|
@@ -71,6 +85,26 @@ interface PathItem {
|
|
|
71
85
|
absH: number
|
|
72
86
|
}
|
|
73
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
|
+
|
|
74
108
|
function getPathAt(
|
|
75
109
|
view: ZUIViewState,
|
|
76
110
|
groups: DiagramGroupLayout[],
|
|
@@ -80,8 +114,8 @@ function getPathAt(
|
|
|
80
114
|
if (canvasW === 0 || canvasH === 0) return []
|
|
81
115
|
|
|
82
116
|
// World center of the screen
|
|
83
|
-
const worldCenterX = (canvasW / 2
|
|
84
|
-
const worldCenterY = (canvasH / 2
|
|
117
|
+
const worldCenterX = screenToWorldX(canvasW / 2, view)
|
|
118
|
+
const worldCenterY = screenToWorldY(canvasH / 2, view)
|
|
85
119
|
const thresholds = getExpandThresholds(canvasW)
|
|
86
120
|
|
|
87
121
|
for (const group of groups) {
|
|
@@ -177,70 +211,6 @@ function getPathAt(
|
|
|
177
211
|
return []
|
|
178
212
|
}
|
|
179
213
|
|
|
180
|
-
function findDiagramFocusTarget(groups: DiagramGroupLayout[], viewId: number): PathItem | null {
|
|
181
|
-
for (const group of groups) {
|
|
182
|
-
if (group.diagramId === viewId) {
|
|
183
|
-
return {
|
|
184
|
-
id: `g-${group.diagramId}`,
|
|
185
|
-
label: group.label,
|
|
186
|
-
type: 'group',
|
|
187
|
-
absX: group.worldX,
|
|
188
|
-
absY: group.worldY,
|
|
189
|
-
absW: group.worldW,
|
|
190
|
-
absH: group.worldH,
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const found = findLinkedDiagramInNodes(viewId, group.nodes, 0, 0, 1, 0, 0)
|
|
195
|
-
if (found) return found
|
|
196
|
-
}
|
|
197
|
-
return null
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function findLinkedDiagramInNodes(
|
|
201
|
-
viewId: number,
|
|
202
|
-
nodes: DiagramGroupLayout['nodes'],
|
|
203
|
-
parentAbsX: number,
|
|
204
|
-
parentAbsY: number,
|
|
205
|
-
parentAbsScale: number,
|
|
206
|
-
parentChildOffsetX: number,
|
|
207
|
-
parentChildOffsetY: number,
|
|
208
|
-
): PathItem | null {
|
|
209
|
-
for (const node of nodes) {
|
|
210
|
-
const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
|
|
211
|
-
const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
|
|
212
|
-
const absW = node.worldW * parentAbsScale
|
|
213
|
-
const absH = node.worldH * parentAbsScale
|
|
214
|
-
|
|
215
|
-
if (node.linkedDiagramId === viewId) {
|
|
216
|
-
return {
|
|
217
|
-
id: node.id,
|
|
218
|
-
label: node.linkedDiagramLabel || node.label,
|
|
219
|
-
type: 'node',
|
|
220
|
-
isCircular: node.isCircular,
|
|
221
|
-
absX,
|
|
222
|
-
absY,
|
|
223
|
-
absW,
|
|
224
|
-
absH,
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (node.children.length > 0) {
|
|
229
|
-
const found = findLinkedDiagramInNodes(
|
|
230
|
-
viewId,
|
|
231
|
-
node.children,
|
|
232
|
-
absX,
|
|
233
|
-
absY,
|
|
234
|
-
parentAbsScale * node.childScale,
|
|
235
|
-
node.childOffsetX,
|
|
236
|
-
node.childOffsetY,
|
|
237
|
-
)
|
|
238
|
-
if (found) return found
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return null
|
|
242
|
-
}
|
|
243
|
-
|
|
244
214
|
function easeOutQuart(t: number): number {
|
|
245
215
|
return 1 - Math.pow(1 - t, 4)
|
|
246
216
|
}
|
|
@@ -320,25 +290,27 @@ function findFirstExpandableNodeInTree(
|
|
|
320
290
|
return null
|
|
321
291
|
}
|
|
322
292
|
|
|
323
|
-
export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, initialCameraFrame, 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) {
|
|
324
294
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
325
295
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
326
296
|
const cameraTransitionRef = useRef<number | null>(null)
|
|
327
297
|
const [initialized, setInitialized] = useState(false)
|
|
328
298
|
const [containerSize, setContainerSize] = useState({ w: 0, h: 0 })
|
|
329
299
|
const isMobileLayout = useBreakpointValue({ base: true, md: false }) ?? false
|
|
300
|
+
const debugViewport = useMemo(() => typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debugZuiCamera'), [])
|
|
330
301
|
|
|
331
302
|
// ── Layout ──────────────────────────────────────────────────────
|
|
332
303
|
const layout = useMemo(() => computeLayout(data), [data])
|
|
333
304
|
const workspaceSnapshot = useMemo(() => buildWorkspaceGraphSnapshot(data), [data])
|
|
334
|
-
// Holds the
|
|
335
|
-
//
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
const resolveHoveredProxyItem = useCallback((worldX: number, worldY: number, view: ZUIViewState
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}, [])
|
|
342
314
|
|
|
343
315
|
// ── Interaction ─────────────────────────────────────────────────
|
|
344
316
|
const { viewState, viewStateRef, setViewState, fitView, maxZoom, hoveredItem, setHoveredItem, setHoverLocked } = useZUIInteraction(
|
|
@@ -352,42 +324,100 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
352
324
|
resolveHoveredProxyItem,
|
|
353
325
|
)
|
|
354
326
|
|
|
355
|
-
// 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.
|
|
356
329
|
const anchors = useMemo(() =>
|
|
357
|
-
collectVisibleNodeAnchors(layout.groups, viewState, containerSize.w || 1, hiddenTags),
|
|
358
|
-
[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],
|
|
359
332
|
)
|
|
360
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
|
+
|
|
361
373
|
// A stable string key encoding which element→nodeId pairs are currently visible.
|
|
362
374
|
// This only changes when nodes cross zoom-expansion thresholds not on every pan pixel.
|
|
363
375
|
const visibleElementSig = useMemo(() =>
|
|
364
376
|
Array.from(anchors.visibleAnchors.entries())
|
|
365
377
|
.sort(([a], [b]) => a - b)
|
|
366
|
-
.map(([id, anchor]) => `${id}:${anchor.nodeId}`)
|
|
378
|
+
.map(([id, anchor]) => `${id}:${anchor.nodeId}:${anchor.renderAlpha >= (crossBranchSettings.minConnectorAnchorAlpha ?? DEFAULT_MIN_CONNECTOR_ANCHOR_ALPHA) ? 1 : 0}`)
|
|
367
379
|
.join(','),
|
|
368
|
-
[anchors.visibleAnchors],
|
|
380
|
+
[anchors.visibleAnchors, crossBranchSettings.minConnectorAnchorAlpha],
|
|
369
381
|
)
|
|
370
|
-
|
|
371
|
-
|
|
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.
|
|
372
394
|
const proxyConnectors = useMemo(() => {
|
|
373
395
|
const resolved = buildVisibleProxyConnectors(workspaceSnapshot, anchors.visibleAnchors, crossBranchSettings)
|
|
374
|
-
proxyConnectorsRef.current = resolved
|
|
375
396
|
return resolved
|
|
376
397
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
377
|
-
}, [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
|
|
378
404
|
|
|
379
405
|
const visibleProxyState = useMemo(() => ({
|
|
380
406
|
...anchors,
|
|
381
|
-
proxyConnectors,
|
|
407
|
+
proxyConnectors: proxyConnectors.connectors,
|
|
408
|
+
hiddenProxyBadges: proxyConnectors.hiddenBadges,
|
|
382
409
|
}), [anchors, proxyConnectors])
|
|
383
410
|
|
|
384
411
|
const visibleProxyStateRef = useRef(visibleProxyState)
|
|
385
412
|
visibleProxyStateRef.current = visibleProxyState
|
|
386
413
|
|
|
387
414
|
const labelBgRef = useRef('#171923')
|
|
415
|
+
const accentRef = useRef('#63b3ed')
|
|
388
416
|
useEffect(() => {
|
|
389
417
|
const update = () => {
|
|
390
|
-
|
|
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'
|
|
391
421
|
needsRedrawRef.current = true
|
|
392
422
|
}
|
|
393
423
|
update()
|
|
@@ -415,8 +445,8 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
415
445
|
absY = g.worldY + g.diagramY - absH
|
|
416
446
|
}
|
|
417
447
|
|
|
418
|
-
const sx = absX
|
|
419
|
-
const sy = absY
|
|
448
|
+
const sx = worldToScreenX(absX, viewState)
|
|
449
|
+
const sy = worldToScreenY(absY, viewState)
|
|
420
450
|
const sw = absW * viewState.zoom
|
|
421
451
|
const sh = absH * viewState.zoom
|
|
422
452
|
|
|
@@ -473,36 +503,13 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
473
503
|
setViewState({ x, y, zoom })
|
|
474
504
|
}, [containerSize, maxZoom, setViewState, setHoveredItem])
|
|
475
505
|
|
|
476
|
-
const
|
|
477
|
-
const el = containerRef.current
|
|
478
|
-
const target = findDiagramFocusTarget(layout.groups, viewId)
|
|
479
|
-
if (!el || !target) return false
|
|
480
|
-
|
|
481
|
-
const canvasW = el.offsetWidth
|
|
482
|
-
const canvasH = el.offsetHeight
|
|
483
|
-
if (canvasW === 0 || canvasH === 0) return false
|
|
484
|
-
|
|
485
|
-
setHoveredItem(null, true)
|
|
486
|
-
|
|
487
|
-
const padding = isMobileLayout ? 0.18 : 0.16
|
|
488
|
-
const bboxW = Math.max(1, target.absW)
|
|
489
|
-
const bboxH = Math.max(1, target.absH)
|
|
490
|
-
const zoom = Math.min(
|
|
491
|
-
(canvasW * (1 - padding * 2)) / bboxW,
|
|
492
|
-
(canvasH * (1 - padding * 2)) / bboxH,
|
|
493
|
-
maxZoom,
|
|
494
|
-
)
|
|
495
|
-
|
|
496
|
-
const x = (canvasW - bboxW * zoom) / 2 - target.absX * zoom
|
|
497
|
-
const y = (canvasH - bboxH * zoom) / 2 - target.absY * zoom
|
|
498
|
-
|
|
506
|
+
const animateToViewport = useCallback((to: ZUIViewState) => {
|
|
499
507
|
if (cameraTransitionRef.current !== null) {
|
|
500
508
|
cancelAnimationFrame(cameraTransitionRef.current)
|
|
501
509
|
cameraTransitionRef.current = null
|
|
502
510
|
}
|
|
503
511
|
|
|
504
|
-
const from = viewStateRef.current
|
|
505
|
-
const to = { x, y, zoom }
|
|
512
|
+
const from = rawCameraView(viewStateRef.current)
|
|
506
513
|
const duration = 520
|
|
507
514
|
const startedAt = performance.now()
|
|
508
515
|
|
|
@@ -524,8 +531,41 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
524
531
|
}
|
|
525
532
|
|
|
526
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)
|
|
550
|
+
return true
|
|
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)
|
|
527
567
|
return true
|
|
528
|
-
}, [isMobileLayout, layout.groups, maxZoom, setHoveredItem
|
|
568
|
+
}, [animateToViewport, isMobileLayout, layout.groups, maxZoom, setHoveredItem])
|
|
529
569
|
|
|
530
570
|
const setCameraFrame = useCallback((frame: ZUICameraFrame) => {
|
|
531
571
|
if (frame.profile !== 'detail-to-overview') return false
|
|
@@ -625,9 +665,10 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
625
665
|
fitView(el.offsetWidth, el.offsetHeight, layout.bbox)
|
|
626
666
|
},
|
|
627
667
|
focusDiagram,
|
|
668
|
+
focusElement,
|
|
628
669
|
setCameraFrame,
|
|
629
670
|
}),
|
|
630
|
-
[fitView, focusDiagram, layout.bbox, setCameraFrame, setHoveredItem],
|
|
671
|
+
[fitView, focusDiagram, focusElement, layout.bbox, setCameraFrame, setHoveredItem],
|
|
631
672
|
)
|
|
632
673
|
|
|
633
674
|
// ── RAF render loop ──────────────────────────────────────────────
|
|
@@ -719,14 +760,29 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
719
760
|
ctx.save()
|
|
720
761
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
721
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
|
+
)
|
|
722
769
|
ctx.save()
|
|
723
|
-
ctx.translate(
|
|
724
|
-
ctx.scale(
|
|
770
|
+
ctx.translate(cameraRebase.view.x, cameraRebase.view.y)
|
|
771
|
+
ctx.scale(cameraRebase.view.zoom, cameraRebase.view.zoom)
|
|
725
772
|
drawVisibleProxyConnectors(
|
|
726
773
|
ctx,
|
|
727
774
|
visibleProxyStateRef.current.proxyConnectors,
|
|
728
|
-
|
|
729
|
-
|
|
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,
|
|
730
786
|
labelBgRef.current,
|
|
731
787
|
occupiedLabelRects,
|
|
732
788
|
)
|
|
@@ -761,6 +817,30 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
761
817
|
needsRedrawRef.current = true
|
|
762
818
|
}, [hiddenTags])
|
|
763
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
|
+
|
|
764
844
|
useEffect(() => {
|
|
765
845
|
setHoverLocked(hoverLocked)
|
|
766
846
|
}, [hoverLocked, setHoverLocked])
|
|
@@ -771,6 +851,7 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
771
851
|
setRendererHighlightedTags(new Set())
|
|
772
852
|
setRendererHighlightColor('')
|
|
773
853
|
setRendererHiddenTags(new Set())
|
|
854
|
+
setRendererVersionDiff(new Map(), new Map())
|
|
774
855
|
}
|
|
775
856
|
}, [])
|
|
776
857
|
|
|
@@ -957,6 +1038,19 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
957
1038
|
</Text>
|
|
958
1039
|
<Text fontSize="xs" color="gray.400">{hoveredItem.data.details.label}</Text>
|
|
959
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>
|
|
960
1054
|
<Divider borderColor="whiteAlpha.200" />
|
|
961
1055
|
<VStack align="stretch" spacing={2} width="full">
|
|
962
1056
|
{hoveredItem.data.details.ownerViewIds.map((ownerViewId, index) => (
|