From 3ea07076ada5cc9a441919de2313c82de7ec006c Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 9 Aug 2023 16:41:15 +0530 Subject: [PATCH] feat: support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically (#6546) * feat: support creating text containers programatically * fix * fix * fix * fix * update api to use label * fix api and support individual shapes and text element * update test case in package example * support creating arrows and line * support labelled arrows * add in package example * fix alignment * better types * fix * keep element as is unless we support prog api * fix tests * fix lint * ignore * support arrow bindings via start and end in api * fix lint * fix coords * support id as well for elements * preserve bindings if present and fix testcases * preserve bindings for labelled arrows * support ids, clean up code and move the api related stuff to transform.ts * allow multiple arrows to bind to single element * fix singular elements * fix single text element, unique id and tests * fix lint * fix * support binding arrow to text element * fix creation of regular text * use same stroke color as parent for text containers and height 0 for linear element by default * fix types * fix * remove more ts ignore * remove ts ignore * remove * Add coverage script * Add tests * fix tests * make type optional when id present * remove type when id provided in tests * Add more tests * tweak * let host call convertToExcalidrawElements when using programmatic API * remove convertToExcalidrawElements call from restore * lint * update snaps * Add new type excalidraw-api/clipboard for programmatic api * cleanup * rename tweak * tweak * make image attributes optional and better ts check * support image via programmatic API * fix lint * more types * make fileId mandatory for image and export convertToExcalidrawElements * fix * small tweaks * update snaps * fix * use Object.assign instead of mutateElement * lint * preserve z-index by pushing all elements first and then add bindings * instantiate instead of closure for storing elements * use element API to create regular text, diamond, ellipse and rectangle * fix snaps * udpdate api * ts fixes * make `convertToExcalidrawElements` more typesafe * update snaps * refactor the approach so that order of elements doesn't matter * Revert "update snaps" This reverts commit 621dfadccfea975a1f77223f506dce9d260f91fd. * review fixes * rename ExcalidrawProgrammaticElement -> ExcalidrawELementSkeleton * Add tests * give preference to first element when duplicate ids found * use console.error --------- Co-authored-by: dwelle --- src/clipboard.ts | 5 + src/components/App.tsx | 14 +- src/constants.ts | 1 + src/data/__snapshots__/transform.test.ts.snap | 2032 +++++++++++++++++ src/data/restore.ts | 21 +- src/data/transform.test.ts | 706 ++++++ src/data/transform.ts | 561 +++++ src/element/binding.ts | 2 +- src/element/newElement.ts | 14 +- src/element/textElement.ts | 16 +- src/packages/excalidraw/example/App.tsx | 36 +- .../{initialData.js => initialData.tsx} | 467 +--- src/packages/excalidraw/index.tsx | 1 + src/tests/data/restore.test.ts | 5 +- src/utils.ts | 13 + 15 files changed, 3440 insertions(+), 454 deletions(-) create mode 100644 src/data/__snapshots__/transform.test.ts.snap create mode 100644 src/data/transform.test.ts create mode 100644 src/data/transform.ts rename src/packages/excalidraw/example/{initialData.js => initialData.tsx} (71%) diff --git a/src/clipboard.ts b/src/clipboard.ts index c13829b6..c0deb93a 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -24,6 +24,7 @@ export interface ClipboardData { files?: BinaryFiles; text?: string; errorMessage?: string; + programmaticAPI?: boolean; } let CLIPBOARD = ""; @@ -48,6 +49,7 @@ const clipboardContainsElements = ( [ EXPORT_DATA_TYPES.excalidraw, EXPORT_DATA_TYPES.excalidrawClipboard, + EXPORT_DATA_TYPES.excalidrawClipboardWithAPI, ].includes(contents?.type) && Array.isArray(contents.elements) ) { @@ -191,6 +193,8 @@ export const parseClipboard = async ( try { const systemClipboardData = JSON.parse(systemClipboard); + const programmaticAPI = + systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI; if (clipboardContainsElements(systemClipboardData)) { return { elements: systemClipboardData.elements, @@ -198,6 +202,7 @@ export const parseClipboard = async ( text: isPlainPaste ? JSON.stringify(systemClipboardData.elements, null, 2) : undefined, + programmaticAPI, }; } } catch (e) {} diff --git a/src/components/App.tsx b/src/components/App.tsx index e0847bf8..901b9836 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -346,6 +346,10 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; import { actionWrapTextInContainer } from "../actions/actionBoundText"; import BraveMeasureTextError from "./BraveMeasureTextError"; import { activeEyeDropperAtom } from "./EyeDropper"; +import { + ExcalidrawElementSkeleton, + convertToExcalidrawElements, +} from "../data/transform"; import { ValueOf } from "../utility-types"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; @@ -2231,7 +2235,6 @@ class App extends React.Component { let file = event?.clipboardData?.files[0]; const data = await parseClipboard(event, isPlainPaste); - if (!file && data.text && !isPlainPaste) { const string = data.text.trim(); if (string.startsWith("")) { @@ -2286,9 +2289,16 @@ class App extends React.Component { }, }); } else if (data.elements) { + const elements = ( + data.programmaticAPI + ? convertToExcalidrawElements( + data.elements as ExcalidrawElementSkeleton[], + ) + : data.elements + ) as readonly ExcalidrawElement[]; // TODO remove formatting from elements if isPlainPaste this.addElementsFromPasteOrLibrary({ - elements: data.elements, + elements, files: data.files || null, position: "cursor", retainSeed: isPlainPaste, diff --git a/src/constants.ts b/src/constants.ts index 6ef98af1..46ae25b4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -164,6 +164,7 @@ export const EXPORT_DATA_TYPES = { excalidraw: "excalidraw", excalidrawClipboard: "excalidraw/clipboard", excalidrawLibrary: "excalidrawlib", + excalidrawClipboardWithAPI: "excalidraw-api/clipboard", } as const; export const EXPORT_SOURCE = diff --git a/src/data/__snapshots__/transform.test.ts.snap b/src/data/__snapshots__/transform.test.ts.snap new file mode 100644 index 00000000..24503a94 --- /dev/null +++ b/src/data/__snapshots__/transform.test.ts.snap @@ -0,0 +1,2032 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "#d8f5a2", + "boundElements": [ + { + "id": "id40", + "type": "arrow", + }, + { + "id": "id41", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 300, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#66a80f", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 630, + "y": 316, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id41", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#9c36b5", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "diamond", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 140, + "x": 96, + "y": 400, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "ellipse-1", + "focus": -0.008153707962747813, + "gap": 1, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 35, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 395, + 35, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id42", + "focus": -0.08139534883720931, + "gap": 1, + }, + "strokeColor": "#1864ab", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 395, + "x": 247, + "y": 420, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "ellipse-1", + "focus": 0.10666666666666667, + "gap": 3.834326468444573, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 400, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "diamond-1", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#e67700", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 400, + "x": 227, + "y": 450, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id40", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 300, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": -53, + "y": 270, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id43", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HEYYYYY", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HEYYYYY", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 70, + "x": 185, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id43", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "Whats up ?", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "Whats up ?", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 100, + "x": 560, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id44", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "text-2", + "focus": 0, + "gap": 5, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "text-1", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 255, + "y": 239, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id43", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 340, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id33", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id35", + "focus": 0, + "gap": 1, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id34", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 255, + "y": 239, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id32", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 340, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id32", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 100, + "x": 155, + "y": 189, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id32", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 100, + "x": 555, + "y": 189, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id37", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id39", + "focus": 0, + "gap": 1, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id38", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 255, + "y": 239, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id36", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 340, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id36", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HEYYYYY", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HEYYYYY", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 70, + "x": 185, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id36", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "WHATS UP ?", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "WHATS UP ?", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 100, + "x": 555, + "y": 226.5, +} +`; + +exports[`Test Transform > should not allow duplicate ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 200, + "id": "rect-1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 300, + "y": 100, +} +`; + +exports[`Test Transform > should transform linear elements 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 20, +} +`; + +exports[`Test Transform > should transform linear elements 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "triangle", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": "dot", + "startBinding": null, + "strokeColor": "#1971c2", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 450, + "y": 20, +} +`; + +exports[`Test Transform > should transform linear elements 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "line", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 60, +} +`; + +exports[`Test Transform > should transform linear elements 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#2f9e44", + "strokeStyle": "dotted", + "strokeWidth": 2, + "type": "line", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 450, + "y": 60, +} +`; + +exports[`Test Transform > should transform regular shapes 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 100, + "y": 100, +} +`; + +exports[`Test Transform > should transform regular shapes 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 100, + "y": 250, +} +`; + +exports[`Test Transform > should transform regular shapes 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "diamond", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 100, + "y": 400, +} +`; + +exports[`Test Transform > should transform regular shapes 4`] = ` +{ + "angle": 0, + "backgroundColor": "#c0eb75", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 200, + "x": 300, + "y": 100, +} +`; + +exports[`Test Transform > should transform regular shapes 5`] = ` +{ + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "dotted", + "strokeWidth": 2, + "type": "ellipse", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 200, + "x": 300, + "y": 250, +} +`; + +exports[`Test Transform > should transform regular shapes 6`] = ` +{ + "angle": 0, + "backgroundColor": "#a5d8ff", + "boundElements": null, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1971c2", + "strokeStyle": "dashed", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 200, + "x": 300, + "y": 400, +} +`; + +exports[`Test Transform > should transform text element 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HELLO WORLD!", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 1, + "versionNonce": Any, + "verticalAlign": "top", + "width": 120, + "x": 100, + "y": 100, +} +`; + +exports[`Test Transform > should transform text element 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "STYLED HELLO WORLD!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#5f3dc4", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "STYLED HELLO WORLD!", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 1, + "versionNonce": Any, + "verticalAlign": "top", + "width": 190, + "x": 100, + "y": 150, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id28", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 100, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id29", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 200, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id30", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 130, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1098ad", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 300, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id31", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 130, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1098ad", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 400, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id24", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "LABELED ARROW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "LABELED ARROW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 185, + "y": 87.5, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 6`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id25", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "STYLED LABELED ARROW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#099268", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "STYLED LABELED ARROW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 200, + "x": 150, + "y": 187.5, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 7`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id26", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ANOTHER STYLED LABELLED ARROW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1098ad", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "ANOTHER STYLED +LABELLED ARROW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 150, + "x": 175, + "y": 275, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 8`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id27", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ANOTHER STYLED LABELLED ARROW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#099268", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "ANOTHER STYLED +LABELLED ARROW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 150, + "x": 175, + "y": 375, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id18", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 35, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 250, + "x": 100, + "y": 100, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id19", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 85, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 200, + "x": 500, + "y": 100, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id20", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 170, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "diamond", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 280, + "x": 100, + "y": 150, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 4`] = ` +{ + "angle": 0, + "backgroundColor": "#fff3bf", + "boundElements": [ + { + "id": "id21", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 120, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 400, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id22", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 85, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 200, + "x": 500, + "y": 300, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 6`] = ` +{ + "angle": 0, + "backgroundColor": "#ffec99", + "boundElements": [ + { + "id": "id23", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 120, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#f08c00", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 200, + "x": 500, + "y": 500, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 7`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id12", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "RECTANGLE TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "RECTANGLE TEXT CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 240, + "x": 105, + "y": 105, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 8`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id13", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ELLIPSE TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "ELLIPSE TEXT +CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 534.7893218813452, + "y": 117.44796179957173, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 9`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id14", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 75, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "DIAMOND +TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "DIAMOND +TEXT +CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 90, + "x": 195, + "y": 197.5, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 10`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id15", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "STYLED DIAMOND TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#099268", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "STYLED DIAMOND +TEXT CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 140, + "x": 180, + "y": 435, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 11`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id16", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 75, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "TOP LEFT ALIGNED +RECTANGLE TEXT +CONTAINER", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 170, + "x": 505, + "y": 305, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 12`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id17", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 75, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "STYLED ELLIPSE TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "STYLED +ELLIPSE TEXT +CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 534.7893218813452, + "y": 522.5735931288071, +} +`; diff --git a/src/data/restore.ts b/src/data/restore.ts index 63f1e33c..9316cfe4 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -29,6 +29,7 @@ import { FONT_FAMILY, ROUNDNESS, DEFAULT_SIDEBAR, + DEFAULT_ELEMENT_PROPS, } from "../constants"; import { getDefaultAppState } from "../appState"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -41,7 +42,6 @@ import { getDefaultLineHeight, measureBaseline, } from "../element/textElement"; -import { COLOR_PALETTE } from "../colors"; import { normalizeLink } from "./url"; type RestoredAppState = Omit< @@ -122,16 +122,18 @@ const restoreElementWithProperties = < versionNonce: element.versionNonce ?? 0, isDeleted: element.isDeleted ?? false, id: element.id || randomId(), - fillStyle: element.fillStyle || "hachure", - strokeWidth: element.strokeWidth || 1, - strokeStyle: element.strokeStyle ?? "solid", - roughness: element.roughness ?? 1, - opacity: element.opacity == null ? 100 : element.opacity, + fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle, + strokeWidth: element.strokeWidth || DEFAULT_ELEMENT_PROPS.strokeWidth, + strokeStyle: element.strokeStyle ?? DEFAULT_ELEMENT_PROPS.strokeStyle, + roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness, + opacity: + element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity, angle: element.angle || 0, x: extra.x ?? element.x ?? 0, y: extra.y ?? element.y ?? 0, - strokeColor: element.strokeColor || COLOR_PALETTE.black, - backgroundColor: element.backgroundColor || COLOR_PALETTE.transparent, + strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor, + backgroundColor: + element.backgroundColor || DEFAULT_ELEMENT_PROPS.backgroundColor, width: element.width || 0, height: element.height || 0, seed: element.seed ?? 1, @@ -246,7 +248,6 @@ const restoreElement = ( startArrowhead = null, endArrowhead = element.type === "arrow" ? "arrow" : null, } = element; - let x = element.x; let y = element.y; let points = // migrate old arrow model to new one @@ -410,7 +411,6 @@ export const restoreElements = ( ): ExcalidrawElement[] => { // used to detect duplicate top-level element ids const existingIds = new Set(); - const localElementsMap = localElements ? arrayToMap(localElements) : null; const restoredElements = (elements || []).reduce((elements, element) => { // filtering out selection, which is legacy, no longer kept in elements, @@ -429,6 +429,7 @@ export const restoreElements = ( migratedElement = { ...migratedElement, id: randomId() }; } existingIds.add(migratedElement.id); + elements.push(migratedElement); } } diff --git a/src/data/transform.test.ts b/src/data/transform.test.ts new file mode 100644 index 00000000..03d65bea --- /dev/null +++ b/src/data/transform.test.ts @@ -0,0 +1,706 @@ +import { vi } from "vitest"; +import { + ExcalidrawElementSkeleton, + convertToExcalidrawElements, +} from "./transform"; +import { ExcalidrawArrowElement } from "../element/types"; + +describe("Test Transform", () => { + it("should transform regular shapes", () => { + const elements = [ + { + type: "rectangle", + x: 100, + y: 100, + }, + { + type: "ellipse", + x: 100, + y: 250, + }, + { + type: "diamond", + x: 100, + y: 400, + }, + { + type: "rectangle", + x: 300, + y: 100, + width: 200, + height: 100, + backgroundColor: "#c0eb75", + strokeWidth: 2, + }, + { + type: "ellipse", + x: 300, + y: 250, + width: 200, + height: 100, + backgroundColor: "#ffc9c9", + strokeStyle: "dotted", + fillStyle: "solid", + strokeWidth: 2, + }, + { + type: "diamond", + x: 300, + y: 400, + width: 200, + height: 100, + backgroundColor: "#a5d8ff", + strokeColor: "#1971c2", + strokeStyle: "dashed", + fillStyle: "cross-hatch", + strokeWidth: 2, + }, + ]; + + convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ).forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should transform text element", () => { + const elements = [ + { + type: "text", + x: 100, + y: 100, + text: "HELLO WORLD!", + }, + { + type: "text", + x: 100, + y: 150, + text: "STYLED HELLO WORLD!", + fontSize: 20, + strokeColor: "#5f3dc4", + }, + ]; + convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ).forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should transform linear elements", () => { + const elements = [ + { + type: "arrow", + x: 100, + y: 20, + }, + { + type: "arrow", + x: 450, + y: 20, + startArrowhead: "dot", + endArrowhead: "triangle", + strokeColor: "#1971c2", + strokeWidth: 2, + }, + { + type: "line", + x: 100, + y: 60, + }, + { + type: "line", + x: 450, + y: 60, + strokeColor: "#2f9e44", + strokeWidth: 2, + strokeStyle: "dotted", + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should transform to text containers when label provided", () => { + const elements = [ + { + type: "rectangle", + x: 100, + y: 100, + label: { + text: "RECTANGLE TEXT CONTAINER", + }, + }, + { + type: "ellipse", + x: 500, + y: 100, + width: 200, + label: { + text: "ELLIPSE TEXT CONTAINER", + }, + }, + { + type: "diamond", + x: 100, + y: 150, + width: 280, + label: { + text: "DIAMOND\nTEXT CONTAINER", + }, + }, + { + type: "diamond", + x: 100, + y: 400, + width: 300, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "STYLED DIAMOND TEXT CONTAINER", + strokeColor: "#099268", + fontSize: 20, + }, + }, + { + type: "rectangle", + x: 500, + y: 300, + width: 200, + strokeColor: "#c2255c", + label: { + text: "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER", + textAlign: "left", + verticalAlign: "top", + fontSize: 20, + }, + }, + { + type: "ellipse", + x: 500, + y: 500, + strokeColor: "#f08c00", + backgroundColor: "#ffec99", + width: 200, + label: { + text: "STYLED ELLIPSE TEXT CONTAINER", + strokeColor: "#c2255c", + }, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(12); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should transform to labelled arrows when label provided for arrows", () => { + const elements = [ + { + type: "arrow", + x: 100, + y: 100, + label: { + text: "LABELED ARROW", + }, + }, + { + type: "arrow", + x: 100, + y: 200, + label: { + text: "STYLED LABELED ARROW", + strokeColor: "#099268", + fontSize: 20, + }, + }, + { + type: "arrow", + x: 100, + y: 300, + strokeColor: "#1098ad", + strokeWidth: 2, + label: { + text: "ANOTHER STYLED LABELLED ARROW", + }, + }, + { + type: "arrow", + x: 100, + y: 400, + strokeColor: "#1098ad", + strokeWidth: 2, + label: { + text: "ANOTHER STYLED LABELLED ARROW", + strokeColor: "#099268", + }, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(8); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + describe("Test arrow bindings", () => { + it("should bind arrows to shapes when start / end provided without ids", () => { + const elements = [ + { + type: "arrow", + x: 255, + y: 239, + label: { + text: "HELLO WORLD!!", + }, + start: { + type: "rectangle", + }, + end: { + type: "ellipse", + }, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + const [arrow, text, rectangle, ellipse] = excaldrawElements; + expect(arrow).toMatchObject({ + type: "arrow", + x: 255, + y: 239, + boundElements: [{ id: text.id, type: "text" }], + startBinding: { + elementId: rectangle.id, + focus: 0, + gap: 1, + }, + endBinding: { + elementId: ellipse.id, + focus: 0, + }, + }); + + expect(text).toMatchObject({ + x: 340, + y: 226.5, + type: "text", + text: "HELLO WORLD!!", + containerId: arrow.id, + }); + + expect(rectangle).toMatchObject({ + x: 155, + y: 189, + type: "rectangle", + boundElements: [ + { + id: arrow.id, + type: "arrow", + }, + ], + }); + + expect(ellipse).toMatchObject({ + x: 555, + y: 189, + type: "ellipse", + boundElements: [ + { + id: arrow.id, + type: "arrow", + }, + ], + }); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should bind arrows to text when start / end provided without ids", () => { + const elements = [ + { + type: "arrow", + x: 255, + y: 239, + label: { + text: "HELLO WORLD!!", + }, + start: { + type: "text", + text: "HEYYYYY", + }, + end: { + type: "text", + text: "WHATS UP ?", + }, + }, + ]; + + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + + const [arrow, text1, text2, text3] = excaldrawElements; + + expect(arrow).toMatchObject({ + type: "arrow", + x: 255, + y: 239, + boundElements: [{ id: text1.id, type: "text" }], + startBinding: { + elementId: text2.id, + focus: 0, + gap: 1, + }, + endBinding: { + elementId: text3.id, + focus: 0, + }, + }); + + expect(text1).toMatchObject({ + x: 340, + y: 226.5, + type: "text", + text: "HELLO WORLD!!", + containerId: arrow.id, + }); + + expect(text2).toMatchObject({ + x: 185, + y: 226.5, + type: "text", + boundElements: [ + { + id: arrow.id, + type: "arrow", + }, + ], + }); + + expect(text3).toMatchObject({ + x: 555, + y: 226.5, + type: "text", + boundElements: [ + { + id: arrow.id, + type: "arrow", + }, + ], + }); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should bind arrows to existing shapes when start / end provided with ids", () => { + const elements = [ + { + type: "ellipse", + id: "ellipse-1", + strokeColor: "#66a80f", + x: 630, + y: 316, + width: 300, + height: 300, + backgroundColor: "#d8f5a2", + }, + { + type: "diamond", + id: "diamond-1", + strokeColor: "#9c36b5", + width: 140, + x: 96, + y: 400, + }, + { + type: "arrow", + x: 247, + y: 420, + width: 395, + height: 35, + strokeColor: "#1864ab", + start: { + type: "rectangle", + width: 300, + height: 300, + }, + end: { + id: "ellipse-1", + }, + }, + { + type: "arrow", + x: 227, + y: 450, + width: 400, + strokeColor: "#e67700", + start: { + id: "diamond-1", + }, + end: { + id: "ellipse-1", + }, + }, + ]; + + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(5); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should bind arrows to existing text elements when start / end provided with ids", () => { + const elements = [ + { + x: 100, + y: 239, + type: "text", + text: "HEYYYYY", + id: "text-1", + strokeColor: "#c2255c", + }, + { + type: "text", + id: "text-2", + x: 560, + y: 239, + text: "Whats up ?", + }, + { + type: "arrow", + x: 255, + y: 239, + label: { + text: "HELLO WORLD!!", + }, + start: { + id: "text-1", + }, + end: { + id: "text-2", + }, + }, + ]; + + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should bind arrows to existing elements if ids are correct", () => { + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementationOnce(() => void 0); + const elements = [ + { + x: 100, + y: 239, + type: "text", + text: "HEYYYYY", + id: "text-1", + strokeColor: "#c2255c", + }, + { + type: "rectangle", + x: 560, + y: 139, + id: "rect-1", + width: 100, + height: 200, + backgroundColor: "#bac8ff", + }, + { + type: "arrow", + x: 255, + y: 239, + label: { + text: "HELLO WORLD!!", + }, + start: { + id: "text-13", + }, + end: { + id: "rect-11", + }, + }, + ]; + + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + const [, , arrow] = excaldrawElements; + expect(arrow).toMatchObject({ + type: "arrow", + x: 255, + y: 239, + boundElements: [ + { + id: "id46", + type: "text", + }, + ], + startBinding: null, + endBinding: null, + }); + expect(consoleErrorSpy).toHaveBeenCalledTimes(2); + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 1, + "No element for start binding with id text-13 found", + ); + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 2, + "No element for end binding with id rect-11 found", + ); + }); + + it("should bind when ids referenced before the element data", () => { + const elements = [ + { + type: "arrow", + x: 255, + y: 239, + end: { + id: "rect-1", + }, + }, + { + type: "rectangle", + x: 560, + y: 139, + id: "rect-1", + width: 100, + height: 200, + backgroundColor: "#bac8ff", + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + expect(excaldrawElements.length).toBe(2); + const [arrow, rect] = excaldrawElements; + expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({ + elementId: "rect-1", + focus: 0, + gap: 5, + }); + expect(rect.boundElements).toStrictEqual([ + { + id: "id47", + type: "arrow", + }, + ]); + }); + }); + + it("should not allow duplicate ids", () => { + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementationOnce(() => void 0); + const elements = [ + { + type: "rectangle", + x: 300, + y: 100, + id: "rect-1", + width: 100, + height: 200, + }, + + { + type: "rectangle", + x: 100, + y: 200, + id: "rect-1", + width: 100, + height: 200, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(1); + expect(excaldrawElements[0]).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Duplicate id found for rect-1", + ); + }); +}); diff --git a/src/data/transform.ts b/src/data/transform.ts new file mode 100644 index 00000000..bf541195 --- /dev/null +++ b/src/data/transform.ts @@ -0,0 +1,561 @@ +import { + DEFAULT_FONT_FAMILY, + DEFAULT_FONT_SIZE, + TEXT_ALIGN, + VERTICAL_ALIGN, +} from "../constants"; +import { + newElement, + newLinearElement, + redrawTextBoundingBox, +} from "../element"; +import { bindLinearElement } from "../element/binding"; +import { + ElementConstructorOpts, + newImageElement, + newTextElement, +} from "../element/newElement"; +import { + getDefaultLineHeight, + measureText, + normalizeText, +} from "../element/textElement"; +import { + ExcalidrawArrowElement, + ExcalidrawBindableElement, + ExcalidrawElement, + ExcalidrawEmbeddableElement, + ExcalidrawFrameElement, + ExcalidrawFreeDrawElement, + ExcalidrawGenericElement, + ExcalidrawImageElement, + ExcalidrawLinearElement, + ExcalidrawSelectionElement, + ExcalidrawTextElement, + FileId, + FontFamilyValues, + TextAlign, + VerticalAlign, +} from "../element/types"; +import { MarkOptional } from "../utility-types"; +import { assertNever, getFontString } from "../utils"; + +export type ValidLinearElement = { + type: "arrow" | "line"; + x: number; + y: number; + label?: { + text: string; + fontSize?: number; + fontFamily?: FontFamilyValues; + textAlign?: TextAlign; + verticalAlign?: VerticalAlign; + } & MarkOptional; + end?: + | ( + | ( + | { + type: Exclude< + ExcalidrawBindableElement["type"], + "image" | "text" | "frame" | "embeddable" + >; + id?: ExcalidrawGenericElement["id"]; + } + | { + id: ExcalidrawGenericElement["id"]; + type?: Exclude< + ExcalidrawBindableElement["type"], + "image" | "text" | "frame" | "embeddable" + >; + } + ) + | (( + | { + type: "text"; + text: string; + } + | { + type?: "text"; + id: ExcalidrawTextElement["id"]; + text: string; + } + ) & + Partial) + ) & + MarkOptional; + start?: + | ( + | ( + | { + type: Exclude< + ExcalidrawBindableElement["type"], + "image" | "text" | "frame" | "embeddable" + >; + id?: ExcalidrawGenericElement["id"]; + } + | { + id: ExcalidrawGenericElement["id"]; + type?: Exclude< + ExcalidrawBindableElement["type"], + "image" | "text" | "frame" | "embeddable" + >; + } + ) + | (( + | { + type: "text"; + text: string; + } + | { + type?: "text"; + id: ExcalidrawTextElement["id"]; + text: string; + } + ) & + Partial) + ) & + MarkOptional; +} & Partial; + +export type ValidContainer = + | { + type: Exclude; + id?: ExcalidrawGenericElement["id"]; + label?: { + text: string; + fontSize?: number; + fontFamily?: FontFamilyValues; + textAlign?: TextAlign; + verticalAlign?: VerticalAlign; + } & MarkOptional; + } & ElementConstructorOpts; + +export type ExcalidrawElementSkeleton = + | Extract< + Exclude, + | ExcalidrawEmbeddableElement + | ExcalidrawFreeDrawElement + | ExcalidrawFrameElement + > + | ({ + type: Extract; + x: number; + y: number; + } & Partial) + | ValidContainer + | ValidLinearElement + | ({ + type: "text"; + text: string; + x: number; + y: number; + id?: ExcalidrawTextElement["id"]; + } & Partial) + | ({ + type: Extract; + x: number; + y: number; + fileId: FileId; + } & Partial); + +const DEFAULT_LINEAR_ELEMENT_PROPS = { + width: 300, + height: 0, +}; + +const DEFAULT_DIMENSION = 100; + +const bindTextToContainer = ( + container: ExcalidrawElement, + textProps: { text: string } & MarkOptional, +) => { + const textElement: ExcalidrawTextElement = newTextElement({ + x: 0, + y: 0, + textAlign: TEXT_ALIGN.CENTER, + verticalAlign: VERTICAL_ALIGN.MIDDLE, + ...textProps, + containerId: container.id, + strokeColor: textProps.strokeColor || container.strokeColor, + }); + + Object.assign(container, { + boundElements: (container.boundElements || []).concat({ + type: "text", + id: textElement.id, + }), + }); + + redrawTextBoundingBox(textElement, container); + return [container, textElement] as const; +}; + +const bindLinearElementToElement = ( + linearElement: ExcalidrawArrowElement, + start: ValidLinearElement["start"], + end: ValidLinearElement["end"], + elementStore: ElementStore, +): { + linearElement: ExcalidrawLinearElement; + startBoundElement?: ExcalidrawElement; + endBoundElement?: ExcalidrawElement; +} => { + let startBoundElement; + let endBoundElement; + + Object.assign(linearElement, { + startBinding: linearElement?.startBinding || null, + endBinding: linearElement.endBinding || null, + }); + + if (start) { + const width = start?.width ?? DEFAULT_DIMENSION; + const height = start?.height ?? DEFAULT_DIMENSION; + + let existingElement; + if (start.id) { + existingElement = elementStore.getElement(start.id); + if (!existingElement) { + console.error(`No element for start binding with id ${start.id} found`); + } + } + + const startX = start.x || linearElement.x - width; + const startY = start.y || linearElement.y - height / 2; + const startType = existingElement ? existingElement.type : start.type; + + if (startType) { + if (startType === "text") { + let text = ""; + if (existingElement && existingElement.type === "text") { + text = existingElement.text; + } else if (start.type === "text") { + text = start.text; + } + if (!text) { + console.error( + `No text found for start binding text element for ${linearElement.id}`, + ); + } + startBoundElement = newTextElement({ + x: startX, + y: startY, + type: "text", + ...existingElement, + ...start, + text, + }); + // to position the text correctly when coordinates not provided + Object.assign(startBoundElement, { + x: start.x || linearElement.x - startBoundElement.width, + y: start.y || linearElement.y - startBoundElement.height / 2, + }); + } else { + switch (startType) { + case "rectangle": + case "ellipse": + case "diamond": { + startBoundElement = newElement({ + x: startX, + y: startY, + width, + height, + ...existingElement, + ...start, + type: startType, + }); + break; + } + default: { + assertNever( + linearElement as never, + `Unhandled element start type "${start.type}"`, + true, + ); + } + } + } + + bindLinearElement( + linearElement, + startBoundElement as ExcalidrawBindableElement, + "start", + ); + } + } + if (end) { + const height = end?.height ?? DEFAULT_DIMENSION; + const width = end?.width ?? DEFAULT_DIMENSION; + + let existingElement; + if (end.id) { + existingElement = elementStore.getElement(end.id); + if (!existingElement) { + console.error(`No element for end binding with id ${end.id} found`); + } + } + const endX = end.x || linearElement.x + linearElement.width; + const endY = end.y || linearElement.y - height / 2; + const endType = existingElement ? existingElement.type : end.type; + + if (endType) { + if (endType === "text") { + let text = ""; + if (existingElement && existingElement.type === "text") { + text = existingElement.text; + } else if (end.type === "text") { + text = end.text; + } + + if (!text) { + console.error( + `No text found for end binding text element for ${linearElement.id}`, + ); + } + endBoundElement = newTextElement({ + x: endX, + y: endY, + type: "text", + ...existingElement, + ...end, + text, + }); + // to position the text correctly when coordinates not provided + Object.assign(endBoundElement, { + y: end.y || linearElement.y - endBoundElement.height / 2, + }); + } else { + switch (endType) { + case "rectangle": + case "ellipse": + case "diamond": { + endBoundElement = newElement({ + x: endX, + y: endY, + width, + height, + ...existingElement, + ...end, + type: endType, + }); + break; + } + default: { + assertNever( + linearElement as never, + `Unhandled element end type "${endType}"`, + true, + ); + } + } + } + + bindLinearElement( + linearElement, + endBoundElement as ExcalidrawBindableElement, + "end", + ); + } + } + return { + linearElement, + startBoundElement, + endBoundElement, + }; +}; + +class ElementStore { + excalidrawElements = new Map(); + + add = (ele?: ExcalidrawElement) => { + if (!ele) { + return; + } + + this.excalidrawElements.set(ele.id, ele); + }; + getElements = () => { + return Array.from(this.excalidrawElements.values()); + }; + + getElement = (id: string) => { + return this.excalidrawElements.get(id); + }; +} + +export const convertToExcalidrawElements = ( + elements: ExcalidrawElementSkeleton[] | null, +) => { + if (!elements) { + return []; + } + + const elementStore = new ElementStore(); + const elementsWithIds = new Map(); + + // Create individual elements + for (const element of elements) { + let excalidrawElement: ExcalidrawElement; + switch (element.type) { + case "rectangle": + case "ellipse": + case "diamond": { + const width = + element?.label?.text && element.width === undefined + ? 0 + : element?.width || DEFAULT_DIMENSION; + const height = + element?.label?.text && element.height === undefined + ? 0 + : element?.height || DEFAULT_DIMENSION; + excalidrawElement = newElement({ + ...element, + width, + height, + }); + + break; + } + case "line": { + const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width; + const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height; + excalidrawElement = newLinearElement({ + width, + height, + points: [ + [0, 0], + [width, height], + ], + ...element, + }); + + break; + } + case "arrow": { + const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width; + const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height; + excalidrawElement = newLinearElement({ + width, + height, + endArrowhead: "arrow", + points: [ + [0, 0], + [width, height], + ], + ...element, + }); + break; + } + case "text": { + const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY; + const fontSize = element?.fontSize || DEFAULT_FONT_SIZE; + const lineHeight = + element?.lineHeight || getDefaultLineHeight(fontFamily); + const text = element.text ?? ""; + const normalizedText = normalizeText(text); + const metrics = measureText( + normalizedText, + getFontString({ fontFamily, fontSize }), + lineHeight, + ); + + excalidrawElement = newTextElement({ + width: metrics.width, + height: metrics.height, + fontFamily, + fontSize, + ...element, + }); + break; + } + case "image": { + excalidrawElement = newImageElement({ + width: element?.width || DEFAULT_DIMENSION, + height: element?.height || DEFAULT_DIMENSION, + ...element, + }); + + break; + } + case "freedraw": + case "frame": + case "embeddable": { + excalidrawElement = element; + break; + } + + default: { + excalidrawElement = element; + assertNever( + element, + `Unhandled element type "${(element as any).type}"`, + true, + ); + } + } + const existingElement = elementStore.getElement(excalidrawElement.id); + if (existingElement) { + console.error(`Duplicate id found for ${excalidrawElement.id}`); + } else { + elementStore.add(excalidrawElement); + elementsWithIds.set(excalidrawElement.id, element); + } + } + + // Add labels and arrow bindings + for (const [id, element] of elementsWithIds) { + const excalidrawElement = elementStore.getElement(id)!; + + switch (element.type) { + case "rectangle": + case "ellipse": + case "diamond": + case "arrow": { + if (element.label?.text) { + let [container, text] = bindTextToContainer( + excalidrawElement, + element?.label, + ); + elementStore.add(container); + elementStore.add(text); + + if (container.type === "arrow") { + const originalStart = + element.type === "arrow" ? element?.start : undefined; + const originalEnd = + element.type === "arrow" ? element?.end : undefined; + const { linearElement, startBoundElement, endBoundElement } = + bindLinearElementToElement( + container as ExcalidrawArrowElement, + originalStart, + originalEnd, + elementStore, + ); + container = linearElement; + elementStore.add(linearElement); + elementStore.add(startBoundElement); + elementStore.add(endBoundElement); + } + } else { + switch (element.type) { + case "arrow": { + const { linearElement, startBoundElement, endBoundElement } = + bindLinearElementToElement( + excalidrawElement as ExcalidrawArrowElement, + element.start, + element.end, + elementStore, + ); + elementStore.add(linearElement); + elementStore.add(startBoundElement); + elementStore.add(endBoundElement); + break; + } + } + } + break; + } + } + } + return elementStore.getElements(); +}; diff --git a/src/element/binding.ts b/src/element/binding.ts index b175f14e..bc997759 100644 --- a/src/element/binding.ts +++ b/src/element/binding.ts @@ -190,7 +190,7 @@ export const maybeBindLinearElement = ( } }; -const bindLinearElement = ( +export const bindLinearElement = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", diff --git a/src/element/newElement.ts b/src/element/newElement.ts index cb5657f1..bbc98030 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -46,7 +46,7 @@ import { } from "../constants"; import { MarkOptional, Merge, Mutable } from "../utility-types"; -type ElementConstructorOpts = MarkOptional< +export type ElementConstructorOpts = MarkOptional< Omit, | "width" | "height" @@ -187,7 +187,7 @@ export const newTextElement = ( fontFamily?: FontFamilyValues; textAlign?: TextAlign; verticalAlign?: VerticalAlign; - containerId?: ExcalidrawTextContainer["id"]; + containerId?: ExcalidrawTextContainer["id"] | null; lineHeight?: ExcalidrawTextElement["lineHeight"]; strokeWidth?: ExcalidrawTextElement["strokeWidth"]; } & ElementConstructorOpts, @@ -361,8 +361,8 @@ export const newFreeDrawElement = ( export const newLinearElement = ( opts: { type: ExcalidrawLinearElement["type"]; - startArrowhead: Arrowhead | null; - endArrowhead: Arrowhead | null; + startArrowhead?: Arrowhead | null; + endArrowhead?: Arrowhead | null; points?: ExcalidrawLinearElement["points"]; } & ElementConstructorOpts, ): NonDeleted => { @@ -372,8 +372,8 @@ export const newLinearElement = ( lastCommittedPoint: null, startBinding: null, endBinding: null, - startArrowhead: opts.startArrowhead, - endArrowhead: opts.endArrowhead, + startArrowhead: opts.startArrowhead || null, + endArrowhead: opts.endArrowhead || null, }; }; @@ -477,7 +477,7 @@ export const deepCopyElement = ( * utility wrapper to generate new id. In test env it reuses the old + postfix * for test assertions. */ -const regenerateId = ( +export const regenerateId = ( /** supply null if no previous id exists */ previousId: string | null, ) => { diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 61813d2b..ce80d0eb 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -89,16 +89,23 @@ export const redrawTextBoundingBox = ( container, textElement as ExcalidrawTextElementWithContainer, ); + const maxContainerWidth = getBoundTextMaxWidth(container); - let nextHeight = container.height; if (metrics.height > maxContainerHeight) { - nextHeight = computeContainerDimensionForBoundText( + const nextHeight = computeContainerDimensionForBoundText( metrics.height, container.type, ); mutateElement(container, { height: nextHeight }); updateOriginalContainerCache(container.id, nextHeight); } + if (metrics.width > maxContainerWidth) { + const nextWidth = computeContainerDimensionForBoundText( + metrics.width, + container.type, + ); + mutateElement(container, { width: nextWidth }); + } const updatedTextElement = { ...textElement, ...boundTextUpdates, @@ -859,8 +866,9 @@ const VALID_CONTAINER_TYPES = new Set([ "arrow", ]); -export const isValidTextContainer = (element: ExcalidrawElement) => - VALID_CONTAINER_TYPES.has(element.type); +export const isValidTextContainer = (element: { + type: ExcalidrawElement["type"]; +}) => VALID_CONTAINER_TYPES.has(element.type); export const computeContainerDimensionForBoundText = ( dimension: number, diff --git a/src/packages/excalidraw/example/App.tsx b/src/packages/excalidraw/example/App.tsx index 1a5c6b9e..15744427 100644 --- a/src/packages/excalidraw/example/App.tsx +++ b/src/packages/excalidraw/example/App.tsx @@ -75,6 +75,7 @@ const { WelcomeScreen, MainMenu, LiveCollaborationTrigger, + convertToExcalidrawElements, } = window.ExcalidrawLib; const COMMENT_ICON_DIMENSION = 32; @@ -140,7 +141,10 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { ]; //@ts-ignore - initialStatePromiseRef.current.promise.resolve(initialData); + initialStatePromiseRef.current.promise.resolve({ + ...initialData, + elements: convertToExcalidrawElements(initialData.elements), + }); excalidrawAPI.addFiles(imagesArray); }; }; @@ -184,38 +188,40 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { const updateScene = () => { const sceneData = { elements: restoreElements( - [ + convertToExcalidrawElements([ { type: "rectangle", - version: 141, - versionNonce: 361174001, - isDeleted: false, - id: "oDVXy8D6rom3H1-LLH2-f", + id: "rect-1", fillStyle: "hachure", strokeWidth: 1, strokeStyle: "solid", roughness: 1, - opacity: 100, angle: 0, x: 100.50390625, y: 93.67578125, strokeColor: "#c92a2a", - backgroundColor: "transparent", width: 186.47265625, height: 141.9765625, seed: 1968410350, - groupIds: [], - frameId: null, - boundElements: null, - locked: false, - link: null, - updated: 1, roundness: { type: ROUNDNESS.ADAPTIVE_RADIUS, value: 32, }, }, - ], + { + type: "arrow", + x: 300, + y: 150, + start: { id: "rect-1" }, + end: { type: "ellipse" }, + }, + { + type: "text", + x: 300, + y: 100, + text: "HELLO WORLD!", + }, + ]), null, ), appState: { diff --git a/src/packages/excalidraw/example/initialData.js b/src/packages/excalidraw/example/initialData.tsx similarity index 71% rename from src/packages/excalidraw/example/initialData.js rename to src/packages/excalidraw/example/initialData.tsx index 37b0755d..92c7205d 100644 --- a/src/packages/excalidraw/example/initialData.js +++ b/src/packages/excalidraw/example/initialData.tsx @@ -1,107 +1,58 @@ +import { ExcalidrawElementSkeleton } from "../../../data/transform"; +import { FileId } from "../../../element/types"; + +const elements: ExcalidrawElementSkeleton[] = [ + { + type: "rectangle", + x: 10, + y: 10, + strokeWidth: 2, + }, + { + type: "diamond", + x: 120, + y: 20, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "HELLO EXCALIDRAW", + strokeColor: "#099268", + fontSize: 30, + }, + }, + { + type: "arrow", + x: 100, + y: 200, + label: { text: "HELLO WORLD!!" }, + start: { type: "rectangle" }, + end: { type: "ellipse" }, + }, + { + type: "image", + x: 606.1042326312408, + y: 153.57729779411773, + width: 230, + height: 230, + fileId: "rocket" as FileId, + }, +]; export default { - elements: [ - { - type: "rectangle", - version: 141, - versionNonce: 361174001, - isDeleted: false, - id: "oDVXy8D6rom3H1-LLH2-f", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, - x: 100.50390625, - y: 93.67578125, - strokeColor: "#000000", - backgroundColor: "transparent", - width: 186.47265625, - height: 141.9765625, - seed: 1968410350, - groupIds: [], - frameId: null, - }, - { - id: "-xMIs_0jIFqvpx-R9UnaG", - type: "ellipse", - x: 300.5703125, - y: 190.69140625, - width: 198.21875, - height: 129.51171875, - angle: 0, - strokeColor: "#000000", - backgroundColor: "transparent", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - groupIds: [], - frameId: null, - seed: 957947807, - version: 47, - versionNonce: 1128618623, - isDeleted: false, - }, - { - fileId: "rocket", - type: "image", - x: 606.1042326312408, - y: 153.57729779411773, - width: 231.30325348751828, - height: 231.64340533088227, - angle: 0, - strokeColor: "transparent", - backgroundColor: "transparent", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - groupIds: [], - frameId: null, - strokeSharpness: "round", - seed: 707269846, - version: 143, - versionNonce: 2028982666, - isDeleted: false, - boundElements: null, - updated: 1644914782403, - link: null, - status: "pending", - scale: [1, 1], - }, - ], + elements, appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 }, scrollToContent: true, libraryItems: [ [ { type: "line", - version: 1699, - versionNonce: 1813275999, - isDeleted: false, - id: "1OMHrnYMU3LJ3w3IaXU_R", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, + x: 209.72304760646858, y: 338.83587294718825, strokeColor: "#881fa3", backgroundColor: "#be4bdb", width: 116.42036295658873, height: 103.65107323746608, - seed: 1445523839, - groupIds: [], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], - startBinding: null, - endBinding: null, points: [ [-92.28090097254909, 7.105427357601002e-15], [-154.72281841151394, 19.199290805487394], @@ -111,37 +62,21 @@ export default { [-39.037226329125524, 21.285677238400705], [-92.28090097254909, 7.105427357601002e-15], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, ], [ { type: "line", - version: 3901, - versionNonce: 540959103, - isDeleted: false, - id: "b-rwW8s76ztV_uTu1SHq1", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, x: -249.48446738689245, y: 374.851387389359, strokeColor: "#0a11d3", backgroundColor: "#228be6", width: 88.21658171083376, height: 113.8575037534261, - seed: 1513238033, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, - strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, points: [ [-0.22814350714115691, -43.414939319563715], [0.06274947619197979, 42.63794490105306], @@ -163,16 +98,9 @@ export default { [-0.2758413461535838, -43.46664538034193], [-0.22814350714115691, -43.414939319563715], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 1635, - versionNonce: 1383184881, - isDeleted: false, - id: "3CMZYj34FwjhgPB7jUC3f", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -187,11 +115,8 @@ export default { height: 9.797916664247975, seed: 683951089, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, -2.1538602707609424], [2.326538897826852, 1.751753055375216], @@ -202,16 +127,9 @@ export default { [85.2899738827162, 1.3342483900732343], [88.30808627974527, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 1722, - versionNonce: 303290783, - isDeleted: false, - id: "DX3fUhBWtlJwYyrBDhebG", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -226,11 +144,8 @@ export default { height: 9.797916664247975, seed: 1817746897, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, -2.1538602707609424], [2.326538897826852, 1.751753055375216], @@ -241,16 +156,9 @@ export default { [85.2899738827162, 1.3342483900732343], [88.30808627974527, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "ellipse", - version: 4738, - versionNonce: 753357777, - isDeleted: false, - id: "a-Snvp2FgqDYqSLylF44S", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -265,16 +173,11 @@ export default { height: 17.72670397681366, seed: 1409727409, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "sharp", boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], }, { type: "ellipse", - version: 109, - versionNonce: 1992641983, - isDeleted: false, - id: "7-6c-JFuB2yGoNQRgb2WM", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -289,16 +192,10 @@ export default { height: 13.941904362416096, seed: 1073094033, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 158, - versionNonce: 1028567473, - isDeleted: false, - id: "150XitJtlKDhTPRCyzv56", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -313,16 +210,10 @@ export default { height: 13.941904362416096, seed: 526271345, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 212, - versionNonce: 158547423, - isDeleted: false, - id: "cmwAR3NBl1VqvSorrQN2W", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -337,18 +228,12 @@ export default { height: 13.941904362416096, seed: 243707217, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, ], [ { type: "diamond", - version: 659, - versionNonce: 1294871039, - isDeleted: false, - id: "aDDArXRjZugwyEawdhCeZ", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -363,16 +248,10 @@ export default { height: 36.77344700318558, seed: 511870335, groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "diamond", - version: 700, - versionNonce: 60864881, - isDeleted: false, - id: "Hzx8zkeyDs3YicO2Tdv6G", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -387,16 +266,10 @@ export default { height: 36.77344700318558, seed: 1283079231, groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "diamond", - version: 780, - versionNonce: 251040287, - isDeleted: false, - id: "PNzYhT295VNCT5EXmqvmw", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -411,16 +284,10 @@ export default { height: 36.77344700318558, seed: 996251633, groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "diamond", - version: 822, - versionNonce: 1862951761, - isDeleted: false, - id: "jiMMAhQF3__7bF-obgXc0", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -435,18 +302,12 @@ export default { height: 36.77344700318558, seed: 1764842481, groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, ], [ { type: "line", - version: 4766, - versionNonce: 2003030321, - isDeleted: false, - id: "BXfdLRoPYZ9MIumzzoA9-", fillStyle: "hachure", strokeWidth: 1, strokeStyle: "solid", @@ -461,11 +322,8 @@ export default { height: 154.56722543646003, seed: 1424381745, groupIds: ["HSrtfEf-CssQTf160Fb6R"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [-0.24755378372925183, -40.169554027464216], [-0.07503751055611152, 76.6515171914404], @@ -487,9 +345,6 @@ export default { [-0.2758413461535838, -40.23974757720194], [-0.24755378372925183, -40.169554027464216], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", @@ -511,11 +366,8 @@ export default { height: 12.698053371678215, seed: 726657713, groupIds: ["HSrtfEf-CssQTf160Fb6R"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, -2.0205717204386002], [1.3361877396713384, 3.0410845646550486], @@ -526,16 +378,9 @@ export default { [48.98410145879092, 2.500000505196364], [50.7174766392476, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 2538, - versionNonce: 1913946897, - isDeleted: false, - id: "VIuxhGjvYUBniitomEkKm", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -550,11 +395,8 @@ export default { height: 10.178760037658167, seed: 1977326481, groupIds: ["HSrtfEf-CssQTf160Fb6R"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, -2.136356936862347], [1.332367676378171, 1.9210669226078037], @@ -565,16 +407,9 @@ export default { [48.84405948536458, 1.4873339211608216], [50.57247907260371, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "ellipse", - version: 5503, - versionNonce: 1236644479, - isDeleted: false, - id: "1acGiqpJjntE3sr1JVnBP", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -589,7 +424,6 @@ export default { height: 22.797152568995934, seed: 1774660383, groupIds: ["HSrtfEf-CssQTf160Fb6R"], - frameId: null, strokeSharpness: "sharp", boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], }, @@ -597,10 +431,6 @@ export default { [ { type: "rectangle", - version: 4270, - versionNonce: 309922463, - isDeleted: false, - id: "SqGRpNqls7OV1QB2Eq-0m", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", @@ -615,7 +445,6 @@ export default { height: 107.25081879410921, seed: 371096063, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "sharp", boundElementIds: [ "CFu0B4Mw_1wC1Hbgx8Fs0", @@ -625,10 +454,6 @@ export default { }, { type: "rectangle", - version: 4319, - versionNonce: 896119505, - isDeleted: false, - id: "fayss6b_GPh6LK1x4iX-q", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", @@ -643,7 +468,6 @@ export default { height: 107.25081879410921, seed: 685932433, groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "sharp", boundElementIds: [ "CFu0B4Mw_1wC1Hbgx8Fs0", @@ -653,10 +477,6 @@ export default { }, { type: "rectangle", - version: 4417, - versionNonce: 1968987839, - isDeleted: false, - id: "HgAnv2rwYhUpLiJiZAXv-", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", @@ -671,7 +491,6 @@ export default { height: 107.25081879410921, seed: 58634943, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "sharp", boundElementIds: [ "CFu0B4Mw_1wC1Hbgx8Fs0", @@ -681,10 +500,6 @@ export default { }, { type: "draw", - version: 3541, - versionNonce: 1680683185, - isDeleted: false, - id: "12aO-Bs9HdALZN_-tuQTr", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -699,24 +514,16 @@ export default { height: 3.249953844290203, seed: 1673003743, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0.6014697828497827], [40.42449133807562, 0.7588628355182573], [46.57983585730082, -2.491091008771946], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3567, - versionNonce: 620768991, - isDeleted: false, - id: "Ck_Y0EVPh_fsY0qoRnGiD", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -731,24 +538,16 @@ export default { height: 2.8032978840147194, seed: 1821527807, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [16.832548902953302, -2.8032978840147194], [45.567415680676426, -0.3275477042019195], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3592, - versionNonce: 1300624017, - isDeleted: false, - id: "a_7IZapEuD918VW1P8Ss_", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -763,25 +562,17 @@ export default { height: 4.280657518731036, seed: 1485707039, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [26.41225578429045, -0.2552319773002338], [37.62000339651456, 2.3153712935189787], [48.33668263438425, -1.9652862252120569], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3629, - versionNonce: 737475327, - isDeleted: false, - id: "8io6FVNdFOLsQ266W8Lni", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -796,9 +587,8 @@ export default { height: 2.9096445412231735, seed: 1042012991, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [10.166093050596771, -1.166642430373031], @@ -806,16 +596,9 @@ export default { [46.26079588567538, 0.6125567455206506], [54.40694982784246, -2.297087795702523], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3594, - versionNonce: 1982560369, - isDeleted: false, - id: "LJI5kY6tg7UFAjPV3fKL-", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -830,24 +613,16 @@ export default { height: 2.4757501798128, seed: 295443295, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [18.193786115221407, -0.5912874140789839], [46.92865289294453, 1.884462765733816], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3609, - versionNonce: 1857766175, - isDeleted: false, - id: "zCrZOHW-q8YWKLw6ltKxX", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -862,27 +637,19 @@ export default { height: 2.4757501798128, seed: 1734301567, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [8.093938105125233, 1.4279702913643746], [18.193786115221407, -0.5912874140789839], [46.92865289294453, 1.884462765733816], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, ], [ { type: "rectangle", - version: 676, - versionNonce: 1841530687, - isDeleted: false, - id: "XOD3vRhtoLWoxC9wF9Sk8", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -897,16 +664,10 @@ export default { height: 76.53703389977764, seed: 106569279, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "line", - version: 462, - versionNonce: 1737150513, - isDeleted: false, - id: "WBkTga1PjKzYK-tcGjnjZ", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -921,25 +682,15 @@ export default { height: 0, seed: 73916127, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, 0], [128.84193229844433, 0], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "ellipse", - version: 282, - versionNonce: 1198409567, - isDeleted: false, - id: "FHX0ZsIzUUfYPJqrZ8Lso", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -954,16 +705,10 @@ export default { height: 5.001953125, seed: 387857791, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 327, - versionNonce: 1661182481, - isDeleted: false, - id: "ugVRR0f_uDOjrllO10yAs", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -978,16 +723,10 @@ export default { height: 5.001953125, seed: 1486370207, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 385, - versionNonce: 2047607679, - isDeleted: false, - id: "SBzNA0Sn-ou4QGxotj0SB", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1002,16 +741,10 @@ export default { height: 5.001953125, seed: 610150847, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 664, - versionNonce: 2135373809, - isDeleted: false, - id: "VKcfbELTVlyJ90m0bGsj0", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1026,35 +759,21 @@ export default { height: 42.72020253937572, seed: 144280593, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "draw", - version: 1281, - versionNonce: 1708997535, - isDeleted: false, - id: "zWrJVrKnkF5K8iXNxi9Aa", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", - roughness: 0, - opacity: 100, - angle: 0, x: -530.327851842306, y: 378.9357912947449, strokeColor: "#087f5b", backgroundColor: "#40c057", width: 28.226201983883442, height: 24.44112284281997, - seed: 29167967, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, points: [ [4.907524351775825, 2.043055712211473], [3.0769604829149455, 1.6284171290602836], @@ -1079,16 +798,9 @@ export default { [4.669824267311791, 1.1200945145694894], [4.907524351775825, 2.043055712211473], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 701, - versionNonce: 1583157713, - isDeleted: false, - id: "LX6kTl9A8K36ld2MEV4tI", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1103,25 +815,15 @@ export default { height: 0, seed: 1443027377, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, 0], [42.095115772272244, 0], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 2908, - versionNonce: 252866495, - isDeleted: false, - id: "SHmV_QtcwxIE-peI_QOX1", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1134,13 +836,8 @@ export default { backgroundColor: "#99bcff", width: 29.31860660384862, height: 5.711199931375845, - seed: 244310513, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, points: [ [0, -2.341683327443203], [0.7724193963150375, -0.06510358900749044], @@ -1151,16 +848,9 @@ export default { [28.316582284417855, -0.3084668090492442], [29.31860660384862, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "ellipse", - version: 725, - versionNonce: 1969008561, - isDeleted: false, - id: "PKRg6SqIetkWIgRqBAnDY", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1175,22 +865,14 @@ export default { height: 44.82230388130942, seed: 683572113, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "line", - version: 3113, - versionNonce: 533471199, - isDeleted: false, - id: "HrelUAgvfxi_4v8MyL_iT", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", - roughness: 0, opacity: 90, - angle: 0, x: -544.828148539078, y: 402.0199316371545, strokeColor: "#000000", @@ -1199,11 +881,8 @@ export default { height: 5.896061363392446, seed: 318798801, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, 0], [4.103544916365185, -4.322122351104391], @@ -1213,18 +892,11 @@ export default { [28.316582284417855, -2.0990281379671547], [29.31860660384862, 0.2709794602754383], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, ], [ { type: "rectangle", - version: 685, - versionNonce: 706399231, - isDeleted: false, - id: "dba8s5bDYEnF20oGn2a8b", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1239,16 +911,10 @@ export default { height: 108.30428902193904, seed: 1914896753, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "rectangle", - version: 835, - versionNonce: 851916657, - isDeleted: false, - id: "3HxCT4mFZF-jJ6m9pyOCt", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1263,16 +929,10 @@ export default { height: 82.83278895375764, seed: 1306468145, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 881, - versionNonce: 704574495, - isDeleted: false, - id: "xX9mcMHy_0Bn-D0UAMyCc", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1287,16 +947,10 @@ export default { height: 11.427824006438863, seed: 93422161, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "rectangle", - version: 528, - versionNonce: 816914769, - isDeleted: false, - id: "h60d2h6UPYkopTlW_XEs4", fillStyle: "cross-hatch", strokeWidth: 1, strokeStyle: "solid", @@ -1311,22 +965,13 @@ export default { height: 19.889460471185775, seed: 11646495, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "rectangle", - version: 570, - versionNonce: 1198069823, - isDeleted: false, - id: "bZbx28BjXM33JV1UezMcH", fillStyle: "cross-hatch", strokeWidth: 1, strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, x: -698.7169501405845, y: 384.7822247024333, strokeColor: "#000000", @@ -1335,9 +980,7 @@ export default { height: 19.889460471185775, seed: 291717649, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, ], ], diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index c417cdfb..901785f1 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -253,3 +253,4 @@ export { LiveCollaborationTrigger }; export { DefaultSidebar } from "../../components/DefaultSidebar"; export { normalizeLink } from "../../data/url"; +export { convertToExcalidrawElements } from "../../data/transform"; diff --git a/src/tests/data/restore.test.ts b/src/tests/data/restore.test.ts index e8ac49ed..0019b0e8 100644 --- a/src/tests/data/restore.test.ts +++ b/src/tests/data/restore.test.ts @@ -140,9 +140,8 @@ describe("restoreElements", () => { expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) }); }); - it("when arrow element has defined endArrowHead", () => { + it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is null', () => { const arrowElement = API.createElement({ type: "arrow" }); - const restoredElements = restore.restoreElements([arrowElement], null); const restoredArrow = restoredElements[0] as ExcalidrawLinearElement; @@ -150,7 +149,7 @@ describe("restoreElements", () => { expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead); }); - it("when arrow element has undefined endArrowHead", () => { + it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is undefined', () => { const arrowElement = API.createElement({ type: "arrow" }); Object.defineProperty(arrowElement, "endArrowhead", { get: vi.fn(() => undefined), diff --git a/src/utils.ts b/src/utils.ts index e14413ea..407fecd2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -914,3 +914,16 @@ export const isOnlyExportingSingleFrame = ( ) ); }; + +export const assertNever = ( + value: never, + message: string, + softAssert?: boolean, +): never => { + if (softAssert) { + console.error(message); + return value; + } + + throw new Error(message); +};