mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2026-04-23 08:31:16 -04:00
feat: Added SVG export, fixes #211
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "fossflow-monorepo",
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fossflow-monorepo",
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.2",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -18835,7 +18835,7 @@
|
||||
}
|
||||
},
|
||||
"packages/fossflow-app": {
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.2",
|
||||
"dependencies": {
|
||||
"@isoflow/isopacks": "^0.0.10",
|
||||
"fossflow": "*",
|
||||
@@ -18883,7 +18883,7 @@
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
|
||||
},
|
||||
"packages/fossflow-backend": {
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.2",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.6.1",
|
||||
@@ -18896,7 +18896,7 @@
|
||||
},
|
||||
"packages/fossflow-lib": {
|
||||
"name": "fossflow",
|
||||
"version": "1.9.1",
|
||||
"version": "1.9.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import {
|
||||
exportAsImage,
|
||||
exportAsSVG,
|
||||
downloadFile as downloadFileUtil,
|
||||
base64ToBlob,
|
||||
generateGenericFilename,
|
||||
@@ -57,6 +58,7 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
const [dragStart, setDragStart] = useState<Coords | null>(null);
|
||||
const currentView = useUiStateStore((state) => state.view);
|
||||
const [imageData, setImageData] = React.useState<string>();
|
||||
const [svgData, setSvgData] = useState<string>();
|
||||
const [croppedImageData, setCroppedImageData] = useState<string>();
|
||||
const [exportError, setExportError] = useState(false);
|
||||
const { getUnprojectedBounds } = useDiagramUtils();
|
||||
@@ -100,7 +102,7 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
customVars.customPalette.diagramBg
|
||||
);
|
||||
|
||||
const exportImage = useCallback(() => {
|
||||
const exportImage = useCallback(async () => {
|
||||
if (!containerRef.current || isExporting.current) {
|
||||
return;
|
||||
}
|
||||
@@ -113,16 +115,23 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
height: bounds.height
|
||||
};
|
||||
|
||||
exportAsImage(containerRef.current as HTMLDivElement, containerSize, exportScale, transparentBackground ? 'transparent' : backgroundColor)
|
||||
.then((data) => {
|
||||
setImageData(data);
|
||||
isExporting.current = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setExportError(true);
|
||||
isExporting.current = false;
|
||||
});
|
||||
const bgColor = transparentBackground ? 'transparent' : backgroundColor;
|
||||
|
||||
try {
|
||||
// Export both PNG and SVG in parallel
|
||||
const [pngData, svgDataResult] = await Promise.all([
|
||||
exportAsImage(containerRef.current as HTMLDivElement, containerSize, exportScale, bgColor),
|
||||
exportAsSVG(containerRef.current as HTMLDivElement, containerSize, bgColor)
|
||||
]);
|
||||
|
||||
setImageData(pngData);
|
||||
setSvgData(svgDataResult);
|
||||
isExporting.current = false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setExportError(true);
|
||||
isExporting.current = false;
|
||||
}
|
||||
}, [bounds, exportScale, transparentBackground, backgroundColor]);
|
||||
|
||||
// Crop the image based on selected area
|
||||
@@ -381,6 +390,7 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
useEffect(() => {
|
||||
if (!cropToContent) {
|
||||
setImageData(undefined);
|
||||
setSvgData(undefined);
|
||||
setExportError(false);
|
||||
isExporting.current = false;
|
||||
const timer = setTimeout(() => {
|
||||
@@ -411,6 +421,20 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
downloadFileUtil(data, generateGenericFilename('png'));
|
||||
}, [imageData, croppedImageData]);
|
||||
|
||||
const downloadSvgFile = useCallback(async () => {
|
||||
if (!svgData) return;
|
||||
|
||||
try {
|
||||
// Fetch the data URL as a blob to handle encoding properly
|
||||
const response = await fetch(svgData);
|
||||
const blob = await response.blob();
|
||||
downloadFileUtil(blob, generateGenericFilename('svg'));
|
||||
} catch (error) {
|
||||
console.error('SVG download failed:', error);
|
||||
setExportError(true);
|
||||
}
|
||||
}, [svgData]);
|
||||
|
||||
const displayImage = croppedImageData || imageData;
|
||||
|
||||
return (
|
||||
@@ -680,7 +704,14 @@ export const ExportImageDialog = ({ onClose, quality = 1.5 }: Props) => {
|
||||
<Button variant="text" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={downloadSvgFile}
|
||||
disabled={!svgData || (cropToContent && isInCropMode && !croppedImageData)}
|
||||
>
|
||||
Download as SVG
|
||||
</Button>
|
||||
<Button
|
||||
onClick={downloadFile}
|
||||
disabled={cropToContent && isInCropMode && !croppedImageData}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Model, Size } from '../types';
|
||||
import { icons as availableIcons } from '../examples/initialData';
|
||||
|
||||
export const generateGenericFilename = (extension: string) => {
|
||||
return `isoflow-export-${new Date().toISOString()}.${extension}`;
|
||||
return `fossflow-export-${new Date().toISOString()}.${extension}`;
|
||||
};
|
||||
|
||||
export const base64ToBlob = (
|
||||
@@ -213,3 +213,34 @@ export const exportAsImage = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const exportAsSVG = async (
|
||||
el: HTMLDivElement,
|
||||
size?: Size,
|
||||
bgcolor: string = '#ffffff'
|
||||
) => {
|
||||
const width = size ? size.width : el.clientWidth;
|
||||
const height = size ? size.height : el.clientHeight;
|
||||
|
||||
const options = {
|
||||
width,
|
||||
height,
|
||||
cacheBust: true,
|
||||
bgcolor,
|
||||
quality: 1.0
|
||||
};
|
||||
|
||||
try {
|
||||
const svgData = await domtoimage.toSvg(el, options);
|
||||
return svgData;
|
||||
} catch (error) {
|
||||
console.error('SVG export failed, trying fallback method:', error);
|
||||
// Fallback: try with minimal options
|
||||
return await domtoimage.toSvg(el, {
|
||||
width,
|
||||
height,
|
||||
cacheBust: true,
|
||||
bgcolor
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user