diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index 862b51282..60fc83d03 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { useEffect, useReducer, useRef, useState } from "react"; import clsx from "clsx"; import type { ActionManager } from "../actions/manager"; @@ -29,7 +23,10 @@ import { import { canvasToBlob } from "../data/blob"; import { nativeFileSystemSupported } from "../data/filesystem"; -import { NonDeletedExcalidrawElement } from "../element/types"; +import { + ExcalidrawElement, + NonDeletedExcalidrawElement, +} from "../element/types"; import { t } from "../i18n"; import { getSelectedElements, isSomeElementSelected } from "../scene"; import { exportToCanvas, getScaleToFit } from "../packages/utils"; @@ -52,6 +49,7 @@ import { } from "../utils"; import { getFancyBackgroundPadding } from "../scene/fancyBackground"; import { Select } from "./Select"; +import { Bounds } from "../element/bounds"; const supportsContextFilters = "filter" in document.createElement("canvas").getContext("2d")!; @@ -83,6 +81,113 @@ type ImageExportModalProps = { onExportImage: AppClassProperties["onExportImage"]; }; +type State = { + projectName: string; + someElementIsSelected: boolean; + exportSelected: boolean; + exportedElements: + | readonly NonDeletedExcalidrawElement[] + | ExcalidrawElement[]; + elementsBounds: Bounds; + exportWithBackground: boolean; + exportBackgroundImage: keyof typeof FANCY_BACKGROUND_IMAGES; + exportDarkMode: boolean; + embedScene: boolean; + exportScale: number; + exportBaseScale: number; + renderError: Error | null; +}; + +type Action = + | { type: "SET_PROJECT_NAME"; projectName: string } + | { + type: "SET_EXPORT_SELECTED"; + exportSelected: boolean; + exportedElements: readonly NonDeletedExcalidrawElement[]; + } + | { + type: "SET_EXPORTED_ELEMENTS"; + exportedElements: readonly NonDeletedExcalidrawElement[]; + } + | { type: "SET_EXPORT_WITH_BACKGROUND"; exportWithBackground: boolean } + | { + type: "SET_EXPORT_BACKGROUND_IMAGE"; + exportBackgroundImage: keyof typeof FANCY_BACKGROUND_IMAGES; + } + | { type: "SET_EXPORT_DARK_MODE"; exportDarkMode: boolean } + | { type: "SET_EMBED_SCENE"; embedScene: boolean } + | { type: "SET_EXPORT_SCALE"; exportScale: number } + | { type: "SET_ALL_SCALES"; exportScale: number } + | { type: "SET_RENDER_ERROR"; renderError: Error | null }; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "SET_PROJECT_NAME": + return { + ...state, + projectName: action.projectName, + }; + case "SET_EXPORT_SELECTED": + return { + ...state, + exportSelected: action.exportSelected, + exportedElements: action.exportedElements, + }; + case "SET_EXPORTED_ELEMENTS": + return { + ...state, + exportedElements: action.exportedElements, + elementsBounds: getCommonBounds(action.exportedElements), + }; + case "SET_EXPORT_WITH_BACKGROUND": + return { + ...state, + exportWithBackground: action.exportWithBackground, + }; + case "SET_EXPORT_BACKGROUND_IMAGE": + return { + ...state, + exportBackgroundImage: action.exportBackgroundImage, + }; + case "SET_EXPORT_DARK_MODE": + return { ...state, exportDarkMode: action.exportDarkMode }; + case "SET_EMBED_SCENE": + return { ...state, embedScene: action.embedScene }; + case "SET_EXPORT_SCALE": + return { ...state, exportScale: action.exportScale }; + case "SET_ALL_SCALES": + return { + ...state, + exportScale: action.exportScale, + exportBaseScale: action.exportScale, + }; + default: + return state; + } +}; + +const createInitialState = ({ + appState, + elements, +}: Pick): State => { + const someElementIsSelected = isSomeElementSelected(elements, appState); + + return { + projectName: appState.name, + someElementIsSelected: isSomeElementSelected(elements, appState), + exportedElements: elements, + elementsBounds: getCommonBounds(elements), + exportSelected: someElementIsSelected, + exportWithBackground: appState.exportBackground, + exportBackgroundImage: appState.fancyBackgroundImageKey, + exportDarkMode: appState.exportWithDarkMode, + embedScene: appState.exportEmbedScene, + exportScale: appState.exportScale, + exportBaseScale: appState.exportScale, + renderError: null, + }; +}; + const ImageExportModal = ({ appState, elements, @@ -90,61 +195,38 @@ const ImageExportModal = ({ actionManager, onExportImage, }: ImageExportModalProps) => { - const appProps = useAppProps(); - const [projectName, setProjectName] = useState(appState.name); - - const someElementIsSelected = isSomeElementSelected(elements, appState); - - const [exportSelected, setExportSelected] = useState(someElementIsSelected); - const [exportWithBackground, setExportWithBackground] = useState( - appState.exportBackground, + const [state, dispatch] = useReducer( + reducer, + { appState, elements }, + createInitialState, ); - const [exportBackgroundImage, setExportBackgroundImage] = useState< - keyof typeof FANCY_BACKGROUND_IMAGES - >(appState.fancyBackgroundImageKey); - const [exportDarkMode, setExportDarkMode] = useState( - appState.exportWithDarkMode, - ); - const [embedScene, setEmbedScene] = useState(appState.exportEmbedScene); - const [exportScale, setExportScale] = useState(appState.exportScale); - const [exportBaseScale, setExportBaseScale] = useState(appState.exportScale); - - const previewRef = useRef(null); - const [renderError, setRenderError] = useState(null); - - const exportedElements = useMemo( - () => - exportSelected + useEffect(() => { + dispatch({ + type: "SET_EXPORTED_ELEMENTS", + exportedElements: state.exportSelected ? getSelectedElements(elements, appState, { includeBoundTextElement: true, includeElementsInFrames: true, }) : elements, - [exportSelected, elements], - ); + }); + }, [elements, appState, state.exportSelected]); - const updateAllScales = useCallback( - (scale: number) => { - actionManager.executeAction(actionChangeExportScale, "ui", scale); - setExportScale(scale); - setExportBaseScale(scale); - }, - [actionManager, setExportScale, setExportBaseScale], - ); + const appProps = useAppProps(); + + const previewRef = useRef(null); + const [renderError, setRenderError] = useState(null); // Upscale exported image when is smaller than preview useEffect(() => { - if ( - exportedElements.length > 0 && - exportWithBackground && - exportBackgroundImage !== "solid" - ) { + let scale = defaultExportScale; + if (state.exportWithBackground && state.exportBackgroundImage !== "solid") { const previewNode = previewRef.current; if (!previewNode) { return; } - const [minX, minY, maxX, maxY] = getCommonBounds(exportedElements); + const [minX, minY, maxX, maxY] = state.elementsBounds; const maxWidth = previewNode.offsetWidth; const maxHeight = previewNode.offsetHeight; @@ -153,7 +235,7 @@ const ImageExportModal = ({ true, ); - const scale = + const upscaledScale = Math.floor( (getScaleToFit( { @@ -166,23 +248,25 @@ const ImageExportModal = ({ 100, ) / 100; - if (scale > 1) { - if (scale !== exportBaseScale) { - updateAllScales(scale); - } - } else { - updateAllScales(defaultExportScale); + if (upscaledScale > 1) { + scale = upscaledScale; } - } else if (exportBaseScale !== defaultExportScale) { - updateAllScales(defaultExportScale); + } + + if (scale !== state.exportBaseScale) { + dispatch({ + type: "SET_ALL_SCALES", + exportScale: scale > 1 ? scale : defaultExportScale, + }); + actionManager.executeAction(actionChangeExportScale, "ui", scale); } }, [ - exportBackgroundImage, - exportWithBackground, - exportBaseScale, - updateAllScales, - exportedElements, - exportSelected, + state.elementsBounds, + state.exportSelected, + state.exportWithBackground, + state.exportBackgroundImage, + state.exportBaseScale, + actionManager, ]); useEffect(() => { @@ -199,7 +283,7 @@ const ImageExportModal = ({ return; } exportToCanvas({ - elements: exportedElements, + elements: state.exportedElements, appState, files, exportPadding: DEFAULT_EXPORT_PADDING, @@ -222,7 +306,7 @@ const ImageExportModal = ({ appState.exportBackground, appState.fancyBackgroundImageKey, files, - exportedElements, + state.exportedElements, ]); return ( @@ -248,12 +332,15 @@ const ImageExportModal = ({ { - setProjectName(event.target.value); + dispatch({ + type: "SET_PROJECT_NAME", + projectName: event.target.value, + }); actionManager.executeAction( actionChangeProjectName, "ui", @@ -263,16 +350,25 @@ const ImageExportModal = ({ /> )} - {someElementIsSelected && ( + {state.someElementIsSelected && ( { - setExportSelected(checked); + dispatch({ + type: "SET_EXPORT_SELECTED", + exportSelected: checked, + exportedElements: checked + ? getSelectedElements(elements, appState, { + includeBoundTextElement: true, + includeElementsInFrames: true, + }) + : elements, + }); }} /> @@ -282,13 +378,16 @@ const ImageExportModal = ({ name="exportBackgroundSwitch" multipleInputs > - {exportWithBackground && ( + {state.exportWithBackground && (