SVG preview fitting size problem solved
This commit is contained in:
+63
-13
@@ -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>
|
||||||
|
|||||||
@@ -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') &&
|
||||||
|
|||||||
Reference in New Issue
Block a user