Compare commits

...

5 Commits

Author SHA1 Message Date
pomdtr
8984d8f19a
don't clean export options on load 2022-06-29 10:23:48 +00:00
Achille Lacoin
b80706cd4a
Update appState.ts 2022-06-28 17:53:01 +02:00
pomdtr
cf34cbdd30
fix export.test.tsx 2022-06-28 08:16:14 +00:00
pomdtr
6ead3ff839
fix export snapshot 2022-06-28 08:12:38 +00:00
pomdtr
d7f0d4ee21
[feat] serialize export options when embedding scene in an image 2022-06-27 20:31:05 +00:00
9 changed files with 251 additions and 98 deletions

View File

@ -101,90 +101,228 @@ const APP_STATE_STORAGE_CONF = (<
Values extends { Values extends {
/** whether to keep when storing to browser storage (localStorage/IDB) */ /** whether to keep when storing to browser storage (localStorage/IDB) */
browser: boolean; browser: boolean;
/** whether to keep when exporting to file/database */ /** whether to keep when exporting to a text file */
export: boolean; text: boolean;
/** whether to keep when exporting to an image file */
image: boolean;
/** server (shareLink/collab/...) */ /** server (shareLink/collab/...) */
server: boolean; server: boolean;
}, },
T extends Record<keyof AppState, Values>, T extends Record<keyof AppState, Values>,
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) => >(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
config)({ config)({
theme: { browser: true, export: false, server: false }, theme: { browser: true, text: false, image: false, server: false },
collaborators: { browser: false, export: false, server: false }, collaborators: { browser: false, text: false, image: false, server: false },
currentChartType: { browser: true, export: false, server: false }, currentChartType: { browser: true, text: false, image: false, server: false },
currentItemBackgroundColor: { browser: true, export: false, server: false }, currentItemBackgroundColor: {
currentItemEndArrowhead: { browser: true, export: false, server: false },
currentItemFillStyle: { browser: true, export: false, server: false },
currentItemFontFamily: { browser: true, export: false, server: false },
currentItemFontSize: { browser: true, export: false, server: false },
currentItemLinearStrokeSharpness: {
browser: true, browser: true,
export: false, text: false,
image: false,
server: false,
},
currentItemEndArrowhead: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFillStyle: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFontFamily: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFontSize: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemLinearStrokeSharpness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemOpacity: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemRoughness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStartArrowhead: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeColor: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeSharpness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeStyle: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeWidth: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemTextAlign: {
browser: true,
text: false,
image: false,
server: false,
},
cursorButton: { browser: true, text: false, image: false, server: false },
draggingElement: { browser: false, text: false, image: false, server: false },
editingElement: { browser: false, text: false, image: false, server: false },
editingGroupId: { browser: true, text: false, image: false, server: false },
editingLinearElement: {
browser: false,
text: false,
image: false,
server: false,
},
activeTool: { browser: true, text: false, image: false, server: false },
penMode: { browser: true, text: false, image: false, server: false },
penDetected: { browser: true, text: false, image: false, server: false },
errorMessage: { browser: false, text: false, image: false, server: false },
exportBackground: { browser: true, text: false, image: true, server: false },
exportEmbedScene: { browser: true, text: false, image: true, server: false },
exportScale: { browser: true, text: false, image: true, server: false },
exportWithDarkMode: {
browser: true,
text: false,
image: true,
server: false,
},
fileHandle: { browser: false, text: false, image: false, server: false },
gridSize: { browser: true, text: true, image: true, server: true },
height: { browser: false, text: false, image: false, server: false },
isBindingEnabled: {
browser: false,
text: false,
image: false,
server: false,
},
isLibraryOpen: { browser: true, text: false, image: false, server: false },
isLibraryMenuDocked: {
browser: true,
text: false,
image: false,
server: false,
},
isLoading: { browser: false, text: false, image: false, server: false },
isResizing: { browser: false, text: false, image: false, server: false },
isRotating: { browser: false, text: false, image: false, server: false },
lastPointerDownWith: {
browser: true,
text: false,
image: false,
server: false,
},
multiElement: { browser: false, text: false, image: false, server: false },
name: { browser: true, text: false, image: false, server: false },
offsetLeft: { browser: false, text: false, image: false, server: false },
offsetTop: { browser: false, text: false, image: false, server: false },
openMenu: { browser: true, text: false, image: false, server: false },
openPopup: { browser: false, text: false, image: false, server: false },
pasteDialog: { browser: false, text: false, image: false, server: false },
previousSelectedElementIds: {
browser: true,
text: false,
image: false,
server: false,
},
resizingElement: { browser: false, text: false, image: false, server: false },
scrolledOutside: { browser: true, text: false, image: false, server: false },
scrollX: { browser: true, text: false, image: false, server: false },
scrollY: { browser: true, text: false, image: false, server: false },
selectedElementIds: {
browser: true,
text: false,
image: false,
server: false,
},
selectedGroupIds: { browser: true, text: false, image: false, server: false },
selectionElement: {
browser: false,
text: false,
image: false,
server: false,
},
shouldCacheIgnoreZoom: {
browser: true,
text: false,
image: false,
server: false,
},
showHelpDialog: { browser: false, text: false, image: false, server: false },
showStats: { browser: true, text: false, image: false, server: false },
startBoundElement: {
browser: false,
text: false,
image: false,
server: false,
},
suggestedBindings: {
browser: false,
text: false,
image: false,
server: false,
},
toastMessage: { browser: false, text: false, image: false, server: false },
viewBackgroundColor: {
browser: true,
text: true,
image: true,
server: true,
},
width: { browser: false, text: false, image: false, server: false },
zenModeEnabled: { browser: true, text: false, image: false, server: false },
zoom: { browser: true, text: false, image: false, server: false },
viewModeEnabled: { browser: false, text: false, image: false, server: false },
pendingImageElementId: {
browser: false,
text: false,
image: false,
server: false,
},
showHyperlinkPopup: {
browser: false,
text: false,
image: false,
server: false, server: false,
}, },
currentItemOpacity: { browser: true, export: false, server: false },
currentItemRoughness: { browser: true, export: false, server: false },
currentItemStartArrowhead: { browser: true, export: false, server: false },
currentItemStrokeColor: { browser: true, export: false, server: false },
currentItemStrokeSharpness: { browser: true, export: false, server: false },
currentItemStrokeStyle: { browser: true, export: false, server: false },
currentItemStrokeWidth: { browser: true, export: false, server: false },
currentItemTextAlign: { browser: true, export: false, server: false },
cursorButton: { browser: true, export: false, server: false },
draggingElement: { browser: false, export: false, server: false },
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
activeTool: { browser: true, export: false, server: false },
penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false },
exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false },
exportScale: { browser: true, export: false, server: false },
exportWithDarkMode: { browser: true, export: false, server: false },
fileHandle: { browser: false, export: false, server: false },
gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false },
isLibraryOpen: { browser: true, export: false, server: false },
isLibraryMenuDocked: { browser: true, export: false, server: false },
isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false },
lastPointerDownWith: { browser: true, export: false, server: false },
multiElement: { browser: false, export: false, server: false },
name: { browser: true, export: false, server: false },
offsetLeft: { browser: false, export: false, server: false },
offsetTop: { browser: false, export: false, server: false },
openMenu: { browser: true, export: false, server: false },
openPopup: { browser: false, export: false, server: false },
pasteDialog: { browser: false, export: false, server: false },
previousSelectedElementIds: { browser: true, export: false, server: false },
resizingElement: { browser: false, export: false, server: false },
scrolledOutside: { browser: true, export: false, server: false },
scrollX: { browser: true, export: false, server: false },
scrollY: { browser: true, export: false, server: false },
selectedElementIds: { browser: true, export: false, server: false },
selectedGroupIds: { browser: true, export: false, server: false },
selectionElement: { browser: false, export: false, server: false },
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
showHelpDialog: { browser: false, export: false, server: false },
showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false },
toastMessage: { browser: false, export: false, server: false },
viewBackgroundColor: { browser: true, export: true, server: true },
width: { browser: false, export: false, server: false },
zenModeEnabled: { browser: true, export: false, server: false },
zoom: { browser: true, export: false, server: false },
viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false },
}); });
const _clearAppStateForStorage = < const _clearAppStateForStorage = <
ExportType extends "export" | "browser" | "server", ExportType extends "image" | "text" | "browser" | "server",
>( >(
appState: Partial<AppState>, appState: Partial<AppState>,
exportType: ExportType, exportType: ExportType,
@ -211,8 +349,12 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "browser"); return _clearAppStateForStorage(appState, "browser");
}; };
export const cleanAppStateForExport = (appState: Partial<AppState>) => { export const cleanAppStateForTextExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "export"); return _clearAppStateForStorage(appState, "text");
};
export const cleanAppStateForImageExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "image");
}; };
export const clearAppStateForDatabase = (appState: Partial<AppState>) => { export const clearAppStateForDatabase = (appState: Partial<AppState>) => {

View File

@ -1,5 +1,5 @@
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { cleanAppStateForExport } from "../appState"; import { cleanAppStateForImageExport } from "../appState";
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants"; import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element"; import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types"; import { ExcalidrawElement, FileId } from "../element/types";
@ -143,7 +143,7 @@ export const loadSceneOrLibraryFromBlob = async (
appState: { appState: {
theme: localAppState?.theme, theme: localAppState?.theme,
fileHandle: fileHandle || blob.handle || null, fileHandle: fileHandle || blob.handle || null,
...cleanAppStateForExport(data.appState || {}), ...cleanAppStateForImageExport(data.appState || {}),
...(localAppState ...(localAppState
? calculateScrollCenter( ? calculateScrollCenter(
data.elements || [], data.elements || [],

View File

@ -82,7 +82,7 @@ export const exportCanvas = async (
await import(/* webpackChunkName: "image" */ "./image") await import(/* webpackChunkName: "image" */ "./image")
).encodePngMetadata({ ).encodePngMetadata({
blob, blob,
metadata: serializeAsJSON(elements, appState, files, "local"), metadata: serializeAsJSON(elements, appState, files, "image"),
}); });
} }

View File

@ -1,5 +1,9 @@
import { fileOpen, fileSave } from "./filesystem"; import { fileOpen, fileSave } from "./filesystem";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState"; import {
cleanAppStateForImageExport,
cleanAppStateForTextExport,
clearAppStateForDatabase,
} from "../appState";
import { import {
EXPORT_DATA_TYPES, EXPORT_DATA_TYPES,
EXPORT_SOURCE, EXPORT_SOURCE,
@ -43,25 +47,32 @@ export const serializeAsJSON = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Partial<AppState>, appState: Partial<AppState>,
files: BinaryFiles, files: BinaryFiles,
type: "local" | "database", destination: "text" | "image" | "database",
): string => { ): string => {
const cleanAppState = () => {
switch (destination) {
case "database":
return clearAppStateForDatabase(appState);
case "text":
return cleanAppStateForTextExport(appState);
case "image":
return cleanAppStateForImageExport(appState);
}
};
const data: ExportedDataState = { const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw, type: EXPORT_DATA_TYPES.excalidraw,
version: VERSIONS.excalidraw, version: VERSIONS.excalidraw,
source: EXPORT_SOURCE, source: EXPORT_SOURCE,
elements: elements:
type === "local" destination === "database"
? clearElementsForExport(elements) ? clearElementsForDatabase(elements)
: clearElementsForDatabase(elements), : clearElementsForExport(elements),
appState: appState: cleanAppState(),
type === "local"
? cleanAppStateForExport(appState)
: clearAppStateForDatabase(appState),
files: files:
type === "local" destination === "database"
? filterOutDeletedFiles(elements, files) ? // will be stripped from JSON
: // will be stripped from JSON undefined
undefined, : filterOutDeletedFiles(elements, files),
}; };
return JSON.stringify(data, null, 2); return JSON.stringify(data, null, 2);
@ -72,7 +83,7 @@ export const saveAsJSON = async (
appState: AppState, appState: AppState,
files: BinaryFiles, files: BinaryFiles,
) => { ) => {
const serialized = serializeAsJSON(elements, appState, files, "local"); const serialized = serializeAsJSON(elements, appState, files, "text");
const blob = new Blob([serialized], { const blob = new Blob([serialized], {
type: MIME_TYPES.excalidraw, type: MIME_TYPES.excalidraw,
}); });

View File

@ -5,7 +5,7 @@ import {
LibraryItems, LibraryItems,
LibraryItems_anyVersion, LibraryItems_anyVersion,
} from "../types"; } from "../types";
import type { cleanAppStateForExport } from "../appState"; import type { cleanAppStateForTextExport } from "../appState";
import { VERSIONS } from "../constants"; import { VERSIONS } from "../constants";
export interface ExportedDataState { export interface ExportedDataState {
@ -13,7 +13,7 @@ export interface ExportedDataState {
version: number; version: number;
source: string; source: string;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
appState: ReturnType<typeof cleanAppStateForExport>; appState: ReturnType<typeof cleanAppStateForTextExport>;
files: BinaryFiles | undefined; files: BinaryFiles | undefined;
} }

View File

@ -131,7 +131,7 @@ export const exportToBlob = async (
opts.elements, opts.elements,
opts.appState, opts.appState,
opts.files || {}, opts.files || {},
"local", "image",
), ),
}); });
} }

View File

@ -96,7 +96,7 @@ export const exportToSvg = async (
metadata = await ( metadata = await (
await import(/* webpackChunkName: "image" */ "../../src/data/image") await import(/* webpackChunkName: "image" */ "../../src/data/image")
).encodeSvgMetadata({ ).encodeSvgMetadata({
text: serializeAsJSON(elements, appState, files || {}, "local"), text: serializeAsJSON(elements, appState, files || {}, "image"),
}); });
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);

View File

@ -45,7 +45,7 @@ describe("export", () => {
const pngBlob = await API.loadFile("./fixtures/smiley.png"); const pngBlob = await API.loadFile("./fixtures/smiley.png");
const pngBlobEmbedded = await encodePngMetadata({ const pngBlobEmbedded = await encodePngMetadata({
blob: pngBlob, blob: pngBlob,
metadata: serializeAsJSON(testElements, h.state, {}, "local"), metadata: serializeAsJSON(testElements, h.state, {}, "image"),
}); });
API.drop(pngBlobEmbedded); API.drop(pngBlobEmbedded);
@ -58,7 +58,7 @@ describe("export", () => {
it("test encoding/decoding scene for SVG export", async () => { it("test encoding/decoding scene for SVG export", async () => {
const encoded = await encodeSvgMetadata({ const encoded = await encodeSvgMetadata({
text: serializeAsJSON(testElements, h.state, {}, "local"), text: serializeAsJSON(testElements, h.state, {}, "image"),
}); });
const decoded = JSON.parse(await decodeSvgMetadata({ svg: encoded })); const decoded = JSON.parse(await decodeSvgMetadata({ svg: encoded }));
expect(decoded.elements).toEqual([ expect(decoded.elements).toEqual([

View File

@ -93,7 +93,7 @@ exports[`exportToSvg with elements that have a link 1`] = `
exports[`exportToSvg with exportEmbedScene 1`] = ` exports[`exportToSvg with exportEmbedScene 1`] = `
" "
<!-- svg-source:excalidraw --> <!-- svg-source:excalidraw -->
<!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1SPW/CMFx1MDAxMN35XHUwMDE1kbtcIpGk4aNstFRVpapcdTAwMWRcdTAwMTiQWnUw8YVYMbaxXHUwMDFkPoT477VccsRtxNpcclx1MDAwZpbu+b278907dKJcYpm9XHUwMDA0NI5cdTAwMTDscswoUXiLulx1MDAwZd+A0lRw+5T6WIta5Z5ZXHUwMDFhI8e9XHUwMDFlXHUwMDEzVlBcbm1OfGCwXHUwMDAybrRlfNk4ilx1MDAwZf62L5Q41Wau1lx1MDAxZpOiopyk63w1fJtOXj691JN2lpMlWVx1MDAxM+9d4fthXHUwMDEzbykxpcWSOG6wXHUwMDEy6LI0LVx1MDAxMPMlc21cdTAwMDZEXHUwMDFiJSp4XHUwMDEyTCjXyF3sTyi9wHm1VKLmJHCSPsaLXCJwXG7K2Mzs2WlcdTAwMDA4L2tcdTAwMDWoVWF+abGFNzot7ICDypZcXJZcdTAwMWO0/qNcdTAwMTFcdTAwMTLn1Oxbv3L9yVfip/vdzl9iJc95kHbBr85cdTAwMDCIT5Ulg/7wIVx1MDAxZTUvYb9JXHUwMDFht9F3wf2uk2Q0iuMsXHUwMDFkXHUwMDBlXHUwMDFhXHUwMDA21VO7auPTXHUwMDE2mGlcYnN0I3xcdTAwMGU24DVjzWMtXHQ+icJXXHUwMDE55VWbZ11VXcl9cSmheCU4QVx1MDAxZT92b0a7XHUwMDE57X+MXHUwMDA2jFGp4Ww0e/thICzlzNj8lnKyXHUwMDFk2lDYPl5ZbOGP03ubusWCa/Zw7Fx1MDAxY39cdTAwMDCLqmbvIn0=<!-- payload-end --> <!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1UPW/CMFx1MDAxMN35XHUwMDE1kbtcIpFQvspcdTAwMDZcdTAwMDVVlap2YKjUqoOJL8SKsY3t8CHEf69tQlxcXCLWbmSwdM/v3p3vnnJsRVx1MDAxMTJcdTAwMDdcdGhcdTAwMWMh2KeYUaLwXHUwMDBltVx1MDAxZL5cdTAwMDWlqeD2qutjLUqVemZujFx1MDAxY3c6TNiEXFxoc+ZcdTAwMDODNXCjLePbxlF09Ke9ocRlbT/V5mOSXHUwMDE1lJPuJl1cdTAwMGbfZpOXL5/qSXvL6SW9Oj64wo/DOt5RYnKLJXFcXGM50FVuXHUwMDFhIOYr5tpcZog2Slx1MDAxNPAsmFCukYfYf6H0XHUwMDEyp8VKiZKTwEn6XHUwMDE4L7PAyShjXHUwMDBic2DnXHUwMDAx4DQvXHUwMDE1oEaFz0uLXHK8ztPCXHUwMDBlOGTZkqucg9ZXOULilJpD41WuP/lK/HR/mvo5VrLSQdpcdTAwMDV/Olx1MDAwMyBeqpdcZvrDp3hU34T9Jt24ib5cdTAwMGLud50ko1FcdTAwMWP3usNBzaB6ZldtvGyGmYYwRzfCebBcdTAwMDEvXHUwMDE5qy9LSfA5KTyVUV40edZVxVxy7YtLXHTFa8FcdPL4qX032t1o/2M0YIxKXHKV0ezph4GwlFx1MDAwYmP1LeVsO/vflEKZab3aa0W0pbCb3th75r9qfpXIfL1cdTAwMDSySIE7eaNKaFVcdTAwMWV3rlx1MDAwMPfS46l1+lx1MDAwNa05enEifQ==<!-- payload-end -->
<defs> <defs>
<style> <style>
@font-face { @font-face {