@tldiagram/core-ui 1.94.5 → 1.95.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/components/ZUI/ZUICanvas.d.ts +5 -0
- package/dist/components/ZUI/index.d.ts +1 -1
- package/dist/index.js +4164 -4062
- package/dist/pages/InfiniteZoom.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/ZUI/ZUICanvas.tsx +140 -1
- package/src/components/ZUI/index.ts +1 -1
- package/src/pages/InfiniteZoom.tsx +23 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type ZUICameraFrame } from '../components/ZUI';
|
|
1
2
|
interface Props {
|
|
2
3
|
sharedToken?: string;
|
|
3
4
|
shareSlot?: React.ReactNode;
|
|
4
5
|
}
|
|
5
6
|
export interface InfiniteZoomHandle {
|
|
6
7
|
focusDiagram(viewId: number): boolean;
|
|
8
|
+
setCameraFrame(frame: ZUICameraFrame): boolean;
|
|
7
9
|
}
|
|
8
10
|
declare const InfiniteZoom: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<InfiniteZoomHandle>>;
|
|
9
11
|
export default InfiniteZoom;
|
package/package.json
CHANGED
|
@@ -38,6 +38,12 @@ import { buildVisibleProxyConnectors, collectVisibleNodeAnchors, drawVisibleProx
|
|
|
38
38
|
export interface ZUICanvasHandle {
|
|
39
39
|
fitView(): void
|
|
40
40
|
focusDiagram(viewId: number): boolean
|
|
41
|
+
setCameraFrame(frame: ZUICameraFrame): boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ZUICameraFrame {
|
|
45
|
+
profile: 'detail-to-overview'
|
|
46
|
+
progress: number
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
interface Props {
|
|
@@ -238,6 +244,81 @@ function easeOutQuart(t: number): number {
|
|
|
238
244
|
return 1 - Math.pow(1 - t, 4)
|
|
239
245
|
}
|
|
240
246
|
|
|
247
|
+
function clamp01(value: number): number {
|
|
248
|
+
return Math.max(0, Math.min(1, value))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function fitWorldRect(
|
|
252
|
+
rect: { x: number; y: number; w: number; h: number },
|
|
253
|
+
canvasW: number,
|
|
254
|
+
canvasH: number,
|
|
255
|
+
maxZoom: number,
|
|
256
|
+
padding: number,
|
|
257
|
+
): ZUIViewState | null {
|
|
258
|
+
const bboxW = Math.max(1, rect.w)
|
|
259
|
+
const bboxH = Math.max(1, rect.h)
|
|
260
|
+
const zoom = Math.min(
|
|
261
|
+
(canvasW * (1 - padding * 2)) / bboxW,
|
|
262
|
+
(canvasH * (1 - padding * 2)) / bboxH,
|
|
263
|
+
maxZoom,
|
|
264
|
+
)
|
|
265
|
+
if (!Number.isFinite(zoom) || zoom <= 0) return null
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
x: (canvasW - bboxW * zoom) / 2 - rect.x * zoom,
|
|
269
|
+
y: (canvasH - bboxH * zoom) / 2 - rect.y * zoom,
|
|
270
|
+
zoom,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function findFirstExpandableNode(groups: DiagramGroupLayout[]): PathItem | null {
|
|
275
|
+
for (const group of groups) {
|
|
276
|
+
const found = findFirstExpandableNodeInTree(group.nodes, 0, 0, 1, 0, 0)
|
|
277
|
+
if (found) return found
|
|
278
|
+
}
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function findFirstExpandableNodeInTree(
|
|
283
|
+
nodes: DiagramGroupLayout['nodes'],
|
|
284
|
+
parentAbsX: number,
|
|
285
|
+
parentAbsY: number,
|
|
286
|
+
parentAbsScale: number,
|
|
287
|
+
parentChildOffsetX: number,
|
|
288
|
+
parentChildOffsetY: number,
|
|
289
|
+
): PathItem | null {
|
|
290
|
+
for (const node of nodes) {
|
|
291
|
+
const absX = parentAbsX + (node.worldX - parentChildOffsetX) * parentAbsScale
|
|
292
|
+
const absY = parentAbsY + (node.worldY - parentChildOffsetY) * parentAbsScale
|
|
293
|
+
const absW = node.worldW * parentAbsScale
|
|
294
|
+
const absH = node.worldH * parentAbsScale
|
|
295
|
+
|
|
296
|
+
if (node.children.length > 0) {
|
|
297
|
+
return {
|
|
298
|
+
id: node.id,
|
|
299
|
+
label: node.linkedDiagramLabel || node.label,
|
|
300
|
+
type: 'node',
|
|
301
|
+
isCircular: node.isCircular,
|
|
302
|
+
absX,
|
|
303
|
+
absY,
|
|
304
|
+
absW,
|
|
305
|
+
absH,
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const found = findFirstExpandableNodeInTree(
|
|
310
|
+
node.children,
|
|
311
|
+
absX,
|
|
312
|
+
absY,
|
|
313
|
+
parentAbsScale * node.childScale,
|
|
314
|
+
node.childOffsetX,
|
|
315
|
+
node.childOffsetY,
|
|
316
|
+
)
|
|
317
|
+
if (found) return found
|
|
318
|
+
}
|
|
319
|
+
return null
|
|
320
|
+
}
|
|
321
|
+
|
|
241
322
|
export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({ data, onReady, onZoom, onPan, highlightedTags, highlightColor, hiddenTags, crossBranchSettings, hoverLocked = false }, ref) {
|
|
242
323
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
243
324
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -445,6 +526,63 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
445
526
|
return true
|
|
446
527
|
}, [isMobileLayout, layout.groups, maxZoom, setHoveredItem, setViewState, viewStateRef])
|
|
447
528
|
|
|
529
|
+
const setCameraFrame = useCallback((frame: ZUICameraFrame) => {
|
|
530
|
+
if (frame.profile !== 'detail-to-overview') return false
|
|
531
|
+
|
|
532
|
+
const el = containerRef.current
|
|
533
|
+
if (!el) return false
|
|
534
|
+
|
|
535
|
+
const canvasW = el.offsetWidth
|
|
536
|
+
const canvasH = el.offsetHeight
|
|
537
|
+
if (canvasW === 0 || canvasH === 0) return false
|
|
538
|
+
|
|
539
|
+
const detailTarget = findFirstExpandableNode(layout.groups)
|
|
540
|
+
const overviewTarget = layout.groups[0]
|
|
541
|
+
if (!detailTarget || !overviewTarget) return false
|
|
542
|
+
|
|
543
|
+
const detail = fitWorldRect(
|
|
544
|
+
{
|
|
545
|
+
x: detailTarget.absX,
|
|
546
|
+
y: detailTarget.absY,
|
|
547
|
+
w: detailTarget.absW,
|
|
548
|
+
h: detailTarget.absH,
|
|
549
|
+
},
|
|
550
|
+
canvasW,
|
|
551
|
+
canvasH,
|
|
552
|
+
maxZoom,
|
|
553
|
+
0.28,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
const overview = fitWorldRect(
|
|
557
|
+
{
|
|
558
|
+
x: overviewTarget.worldX,
|
|
559
|
+
y: overviewTarget.worldY,
|
|
560
|
+
w: overviewTarget.worldW,
|
|
561
|
+
h: overviewTarget.worldH,
|
|
562
|
+
},
|
|
563
|
+
canvasW,
|
|
564
|
+
canvasH,
|
|
565
|
+
maxZoom,
|
|
566
|
+
0.18,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
if (!detail || !overview) return false
|
|
570
|
+
|
|
571
|
+
if (cameraTransitionRef.current !== null) {
|
|
572
|
+
cancelAnimationFrame(cameraTransitionRef.current)
|
|
573
|
+
cameraTransitionRef.current = null
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
setHoveredItem(null, true)
|
|
577
|
+
const t = easeOutQuart(clamp01(frame.progress))
|
|
578
|
+
setViewState({
|
|
579
|
+
x: detail.x + (overview.x - detail.x) * t,
|
|
580
|
+
y: detail.y + (overview.y - detail.y) * t,
|
|
581
|
+
zoom: detail.zoom + (overview.zoom - detail.zoom) * t,
|
|
582
|
+
})
|
|
583
|
+
return true
|
|
584
|
+
}, [layout.groups, maxZoom, setHoveredItem, setViewState])
|
|
585
|
+
|
|
448
586
|
useEffect(() => {
|
|
449
587
|
return () => {
|
|
450
588
|
if (cameraTransitionRef.current !== null) {
|
|
@@ -482,8 +620,9 @@ export const ZUICanvas = forwardRef<ZUICanvasHandle, Props>(function ZUICanvas({
|
|
|
482
620
|
fitView(el.offsetWidth, el.offsetHeight, layout.bbox)
|
|
483
621
|
},
|
|
484
622
|
focusDiagram,
|
|
623
|
+
setCameraFrame,
|
|
485
624
|
}),
|
|
486
|
-
[fitView, focusDiagram, layout.bbox, setHoveredItem],
|
|
625
|
+
[fitView, focusDiagram, layout.bbox, setCameraFrame, setHoveredItem],
|
|
487
626
|
)
|
|
488
627
|
|
|
489
628
|
// ── RAF render loop ──────────────────────────────────────────────
|
|
@@ -24,7 +24,7 @@ import { FitViewIcon as FitViewSvg, TagsIcon, EyeIcon, EyeOffIcon, FocusIcon as
|
|
|
24
24
|
import ExploreOnboarding from '../components/ExploreOnboarding'
|
|
25
25
|
import ExplorePageOnboarding from '../components/ExplorePageOnboarding'
|
|
26
26
|
import MiniZoomOnboarding from '../components/MiniZoomOnboarding'
|
|
27
|
-
import { ZUICanvas, type ZUICanvasHandle } from '../components/ZUI'
|
|
27
|
+
import { ZUICanvas, type ZUICameraFrame, type ZUICanvasHandle } from '../components/ZUI'
|
|
28
28
|
import { useCrossBranchContextSettings } from '../crossBranch/settings'
|
|
29
29
|
import { primeWorkspaceGraphSnapshot } from '../crossBranch/store'
|
|
30
30
|
|
|
@@ -36,6 +36,7 @@ interface Props {
|
|
|
36
36
|
|
|
37
37
|
export interface InfiniteZoomHandle {
|
|
38
38
|
focusDiagram(viewId: number): boolean
|
|
39
|
+
setCameraFrame(frame: ZUICameraFrame): boolean
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const MINI_ONBOARDING_KEY = 'shared_zoom_onboarding_dismissed'
|
|
@@ -62,6 +63,9 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
62
63
|
focusDiagram(viewId: number) {
|
|
63
64
|
return zuiRef.current?.focusDiagram(viewId) ?? false
|
|
64
65
|
},
|
|
66
|
+
setCameraFrame(frame: ZUICameraFrame) {
|
|
67
|
+
return zuiRef.current?.setCameraFrame(frame) ?? false
|
|
68
|
+
},
|
|
65
69
|
}), [])
|
|
66
70
|
|
|
67
71
|
// ── No data or No content ────────────────────────────────────────
|
|
@@ -173,6 +177,24 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
173
177
|
setCanvasReady(true)
|
|
174
178
|
}, [])
|
|
175
179
|
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!sharedToken) return
|
|
182
|
+
|
|
183
|
+
const handleMessage = (event: MessageEvent) => {
|
|
184
|
+
const data = event.data as { type?: unknown; progress?: unknown; profile?: unknown } | null
|
|
185
|
+
if (!data || data.type !== 'tldiagram-zui-camera') return
|
|
186
|
+
if (data.profile !== 'detail-to-overview') return
|
|
187
|
+
|
|
188
|
+
const progress = Number(data.progress)
|
|
189
|
+
if (!Number.isFinite(progress)) return
|
|
190
|
+
|
|
191
|
+
zuiRef.current?.setCameraFrame({ profile: 'detail-to-overview', progress })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
window.addEventListener('message', handleMessage)
|
|
195
|
+
return () => window.removeEventListener('message', handleMessage)
|
|
196
|
+
}, [sharedToken])
|
|
197
|
+
|
|
176
198
|
if (!loading && (!data || (data.tree ?? []).length === 0 || !hasPlacements)) {
|
|
177
199
|
const noDiagrams = !data || (data.tree ?? []).length === 0
|
|
178
200
|
return (
|