返回 presentation-ai
diagram-fit.ts
1 "use client";
2
3 import React from "react";
4
5 const DIAGRAM_SCALE_EPSILON = 0.001;
6 const DIAGRAM_SIZE_EPSILON_PX = 0.5;
7
8 type DiagramAlignment = "left" | "center" | "right";
9
10 export function getDiagramFitFrameStyle(
11 alignment: DiagramAlignment = "center",
12 ) {
13 return {
14 left: alignment === "right" ? "auto" : 0,
15 marginLeft: alignment === "center" ? "auto" : undefined,
16 marginRight: alignment === "center" ? "auto" : undefined,
17 right: alignment === "right" ? 0 : "auto",
18 } satisfies React.CSSProperties;
19 }
20
21 export function useDiagramFitScale<TContainer extends HTMLElement>(
22 layoutWidth: number,
23 minLayoutHeight: number,
24 ) {
25 const containerRef = React.useRef<TContainer | null>(null);
26 const layoutRef = React.useRef<HTMLDivElement | null>(null);
27 const [scale, setScale] = React.useState(1);
28 const [layoutHeight, setLayoutHeight] = React.useState(minLayoutHeight);
29
30 React.useLayoutEffect(() => {
31 const container = containerRef.current;
32 if (!container) return;
33
34 let frame = 0;
35 const updateScale = () => {
36 cancelAnimationFrame(frame);
37 frame = requestAnimationFrame(() => {
38 const availableWidth = container.clientWidth;
39 const nextScale =
40 availableWidth > 0 ? Math.min(1, availableWidth / layoutWidth) : 1;
41
42 setScale((currentScale) =>
43 Math.abs(currentScale - nextScale) > DIAGRAM_SCALE_EPSILON
44 ? nextScale
45 : currentScale,
46 );
47 });
48 };
49
50 updateScale();
51
52 if (typeof ResizeObserver === "undefined") {
53 return () => cancelAnimationFrame(frame);
54 }
55
56 const observer = new ResizeObserver(updateScale);
57 observer.observe(container);
58
59 return () => {
60 cancelAnimationFrame(frame);
61 observer.disconnect();
62 };
63 }, [layoutWidth]);
64
65 React.useLayoutEffect(() => {
66 const layout = layoutRef.current;
67 if (!layout) return;
68
69 let frame = 0;
70 const updateHeight = () => {
71 cancelAnimationFrame(frame);
72 frame = requestAnimationFrame(() => {
73 const nextHeight = Math.max(layout.scrollHeight, minLayoutHeight);
74
75 setLayoutHeight((currentHeight) =>
76 Math.abs(currentHeight - nextHeight) > DIAGRAM_SIZE_EPSILON_PX
77 ? nextHeight
78 : currentHeight,
79 );
80 });
81 };
82
83 updateHeight();
84
85 if (typeof ResizeObserver === "undefined") {
86 return () => cancelAnimationFrame(frame);
87 }
88
89 const observer = new ResizeObserver(updateHeight);
90 observer.observe(layout);
91
92 return () => {
93 cancelAnimationFrame(frame);
94 observer.disconnect();
95 };
96 }, [minLayoutHeight]);
97
98 return {
99 containerRef,
100 layoutRef,
101 frameStyle: {
102 height: layoutHeight * scale,
103 maxWidth: "100%",
104 overflow: "visible",
105 position: "relative",
106 width: layoutWidth * scale,
107 } satisfies React.CSSProperties,
108 fitStyle: {
109 transform: `scale(${scale})`,
110 transformOrigin: "top left",
111 width: layoutWidth,
112 } satisfies React.CSSProperties,
113 };
114 }
115
115 lines TYPESCRIPT