diff --git a/frontend/canvas.html b/frontend/canvas.html index 67fdcae..a3b6cca 100644 --- a/frontend/canvas.html +++ b/frontend/canvas.html @@ -2246,24 +2246,73 @@ Organization : OptiHK Limited // Displays generated layout SVG previews with zoom and pan controls. const LayoutSvgPreview = ({ page }) => { - const [layoutScale, setLayoutScale] = useState(100); + const [layoutScale, setLayoutScale] = useState(null); + const [previewViewport, setPreviewViewport] = useState({ width: 1, height: 1 }); + const [svgSize, setSvgSize] = useState(null); + const previewCanvasRef = useRef(null); const previewBounds = useMemo( () => page.layoutBounds || calculateLayoutBounds(page), [page.layoutBounds, page.nodes, page.canvasSize] ); - const normalizedScale = Math.min(800, Math.max(10, Number(layoutScale) || 100)); - const stageWidth = Math.max(1, previewBounds.width) * normalizedScale / 100; - const stageHeight = Math.max(1, previewBounds.height) * normalizedScale / 100; + const minLayoutScale = 0.01; + const maxLayoutScale = 800; + const scalePrecision = 100; + const clampLayoutScale = (value, fallback = 100) => { + const numericValue = Number(value); + const scale = Number.isFinite(numericValue) && numericValue > 0 ? numericValue : fallback; + return Number(Math.min(maxLayoutScale, Math.max(minLayoutScale, scale)).toFixed(2)); + }; + const baseWidth = Math.max(1, svgSize?.width || previewBounds.width); + const baseHeight = Math.max(1, svgSize?.height || previewBounds.height); + const availableWidth = Math.max(1, previewViewport.width); + const availableHeight = Math.max(1, previewViewport.height); + const rawFitScalePercent = Math.min(availableWidth / baseWidth, availableHeight / baseHeight) * 100; + const fitScalePercent = clampLayoutScale(Math.floor(rawFitScalePercent * scalePrecision) / scalePrecision); + const normalizedScale = clampLayoutScale(layoutScale ?? fitScalePercent, fitScalePercent); + const stageWidth = baseWidth * normalizedScale / 100; + const stageHeight = baseHeight * normalizedScale / 100; + + useEffect(() => { + const previewCanvas = previewCanvasRef.current; + if (!previewCanvas) return undefined; + + const measurePreviewViewport = () => { + const styles = window.getComputedStyle(previewCanvas); + const paddingX = (parseFloat(styles.paddingLeft) || 0) + (parseFloat(styles.paddingRight) || 0); + const paddingY = (parseFloat(styles.paddingTop) || 0) + (parseFloat(styles.paddingBottom) || 0); + setPreviewViewport({ + width: Math.max(1, previewCanvas.clientWidth - paddingX), + height: Math.max(1, previewCanvas.clientHeight - paddingY) + }); + }; + + measurePreviewViewport(); + if (typeof ResizeObserver === 'undefined') { + window.addEventListener('resize', measurePreviewViewport); + return () => window.removeEventListener('resize', measurePreviewViewport); + } + + const observer = new ResizeObserver(measurePreviewViewport); + observer.observe(previewCanvas); + return () => observer.disconnect(); + }, []); const updateScale = (value) => { - setLayoutScale(Math.min(800, Math.max(10, Number(value) || 100))); + setLayoutScale(clampLayoutScale(value, fitScalePercent)); }; const handleWheel = (event) => { event.preventDefault(); const direction = event.deltaY > 0 ? -1 : 1; const step = event.shiftKey ? 5 : 15; - setLayoutScale(current => Math.min(800, Math.max(10, (Number(current) || 100) + direction * step))); + setLayoutScale(current => clampLayoutScale((current ?? fitScalePercent) + direction * step, fitScalePercent)); + }; + + const handleSvgLoad = (event) => { + const image = event.currentTarget; + if (image.naturalWidth > 0 && image.naturalHeight > 0) { + setSvgSize({ width: image.naturalWidth, height: image.naturalHeight }); + } }; return ( @@ -2273,9 +2322,9 @@ Organization : OptiHK Limited Scale updateScale(event.target.value)} aria-label="Layout SVG preview scale" @@ -2284,9 +2333,9 @@ Organization : OptiHK Limited -