SVG preview fitting size problem solved

This commit is contained in:
2026-06-09 20:58:37 +08:00
parent fa0ebe899c
commit 7195dea7cd
2 changed files with 76 additions and 26 deletions
+63 -13
View File
@@ -2246,24 +2246,73 @@ Organization : OptiHK Limited
// Displays generated layout SVG previews with zoom and pan controls. // Displays generated layout SVG previews with zoom and pan controls.
const LayoutSvgPreview = ({ page }) => { 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( const previewBounds = useMemo(
() => page.layoutBounds || calculateLayoutBounds(page), () => page.layoutBounds || calculateLayoutBounds(page),
[page.layoutBounds, page.nodes, page.canvasSize] [page.layoutBounds, page.nodes, page.canvasSize]
); );
const normalizedScale = Math.min(800, Math.max(10, Number(layoutScale) || 100)); const minLayoutScale = 0.01;
const stageWidth = Math.max(1, previewBounds.width) * normalizedScale / 100; const maxLayoutScale = 800;
const stageHeight = Math.max(1, previewBounds.height) * normalizedScale / 100; 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) => { const updateScale = (value) => {
setLayoutScale(Math.min(800, Math.max(10, Number(value) || 100))); setLayoutScale(clampLayoutScale(value, fitScalePercent));
}; };
const handleWheel = (event) => { const handleWheel = (event) => {
event.preventDefault(); event.preventDefault();
const direction = event.deltaY > 0 ? -1 : 1; const direction = event.deltaY > 0 ? -1 : 1;
const step = event.shiftKey ? 5 : 15; 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 ( return (
@@ -2273,9 +2322,9 @@ Organization : OptiHK Limited
Scale Scale
<input <input
type="range" type="range"
min="10" min={minLayoutScale}
max="800" max={maxLayoutScale}
step="5" step="0.01"
value={normalizedScale} value={normalizedScale}
onChange={(event) => updateScale(event.target.value)} onChange={(event) => updateScale(event.target.value)}
aria-label="Layout SVG preview scale" aria-label="Layout SVG preview scale"
@@ -2284,9 +2333,9 @@ Organization : OptiHK Limited
<label> <label>
<input <input
type="number" type="number"
min="10" min={minLayoutScale}
max="800" max={maxLayoutScale}
step="5" step="0.01"
value={normalizedScale} value={normalizedScale}
onChange={(event) => updateScale(event.target.value)} onChange={(event) => updateScale(event.target.value)}
aria-label="Layout SVG preview scale percent" aria-label="Layout SVG preview scale percent"
@@ -2294,7 +2343,7 @@ Organization : OptiHK Limited
% %
</label> </label>
</div> </div>
<div className="layout-preview-canvas" onWheel={handleWheel}> <div className="layout-preview-canvas" ref={previewCanvasRef} onWheel={handleWheel}>
<div className="layout-preview-scroll-area"> <div className="layout-preview-scroll-area">
<div <div
className="layout-preview-stage" className="layout-preview-stage"
@@ -2304,6 +2353,7 @@ Organization : OptiHK Limited
className="layout-preview-image" className="layout-preview-image"
src={page.svgUrl} src={page.svgUrl}
alt={`${page.name} layout preview`} alt={`${page.name} layout preview`}
onLoad={handleSvgLoad}
style={{ objectFit: 'contain' }} style={{ objectFit: 'contain' }}
/> />
</div> </div>
+13 -13
View File
@@ -33,16 +33,6 @@ assert(
'Build Layout should use the backend svg_url response' 'Build Layout should use the backend svg_url response'
); );
assert( assert(
<<<<<<< HEAD
canvasHtml.includes('result.svg_ready && result.svg_url') &&
canvasHtml.includes('buildLayoutRequestRef') &&
canvasHtml.includes('buildLayoutBusyRef') &&
canvasHtml.includes("cache: 'no-store'"),
'Build Layout should wait for a ready, versioned SVG response and prevent stale duplicate preview updates'
);
assert(
=======
>>>>>>> jingwen_main
canvasHtml.includes('result.preview_error') && canvasHtml.includes('result.preview_error') &&
canvasHtml.includes('Preview skipped: '), canvasHtml.includes('Preview skipped: '),
'Build Layout should log when the backend saves YAML but skips SVG preview because the router stack is unavailable' 'Build Layout should log when the backend saves YAML but skips SVG preview because the router stack is unavailable'
@@ -60,8 +50,18 @@ assert(
'layout SVG preview should expose an editable scale value' 'layout SVG preview should expose an editable scale value'
); );
assert( assert(
canvasHtml.includes('objectFit: \'contain\''), canvasHtml.includes('objectFit: \'contain\'') &&
'100% layout preview scale should fit the full SVG within the screen' canvasHtml.includes('previewCanvasRef') &&
canvasHtml.includes('ResizeObserver') &&
canvasHtml.includes('svgSize') &&
canvasHtml.includes('naturalWidth') &&
canvasHtml.includes('naturalHeight') &&
canvasHtml.includes('const fitScalePercent = clampLayoutScale(Math.floor') &&
canvasHtml.includes('layoutScale ?? fitScalePercent') &&
canvasHtml.includes('const stageWidth = baseWidth * normalizedScale / 100') &&
!canvasHtml.includes('useState(100)') &&
!canvasHtml.includes('baseWidth * fitScale * normalizedScale / 100'),
'layout preview should default to the actual fitted SVG scale percent instead of redefining 100% as fit'
); );
assert( assert(
canvasHtml.includes('className="build-gds-btn"'), canvasHtml.includes('className="build-gds-btn"'),
@@ -354,7 +354,7 @@ assert(
canvasHtml.includes('layoutBounds') && canvasHtml.includes('layoutBounds') &&
canvasHtml.includes('stageWidth') && canvasHtml.includes('stageWidth') &&
canvasHtml.includes('stageHeight'), canvasHtml.includes('stageHeight'),
'layout preview should mouse-wheel zoom and size 100% from calculated box_size layout bounds' 'layout preview should mouse-wheel zoom from the actual calculated SVG scale'
); );
assert( assert(
canvasHtml.includes('reactFlowInstance.fitBounds') && canvasHtml.includes('reactFlowInstance.fitBounds') &&