Explorar el Código

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 <[email protected]>
Aakansha Doshi hace 1 año
padre
commit
3ea07076ad

+ 5 - 0
src/clipboard.ts

@@ -24,6 +24,7 @@ export interface ClipboardData {
   files?: BinaryFiles;
   files?: BinaryFiles;
   text?: string;
   text?: string;
   errorMessage?: string;
   errorMessage?: string;
+  programmaticAPI?: boolean;
 }
 }
 
 
 let CLIPBOARD = "";
 let CLIPBOARD = "";
@@ -48,6 +49,7 @@ const clipboardContainsElements = (
     [
     [
       EXPORT_DATA_TYPES.excalidraw,
       EXPORT_DATA_TYPES.excalidraw,
       EXPORT_DATA_TYPES.excalidrawClipboard,
       EXPORT_DATA_TYPES.excalidrawClipboard,
+      EXPORT_DATA_TYPES.excalidrawClipboardWithAPI,
     ].includes(contents?.type) &&
     ].includes(contents?.type) &&
     Array.isArray(contents.elements)
     Array.isArray(contents.elements)
   ) {
   ) {
@@ -191,6 +193,8 @@ export const parseClipboard = async (
 
 
   try {
   try {
     const systemClipboardData = JSON.parse(systemClipboard);
     const systemClipboardData = JSON.parse(systemClipboard);
+    const programmaticAPI =
+      systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
     if (clipboardContainsElements(systemClipboardData)) {
     if (clipboardContainsElements(systemClipboardData)) {
       return {
       return {
         elements: systemClipboardData.elements,
         elements: systemClipboardData.elements,
@@ -198,6 +202,7 @@ export const parseClipboard = async (
         text: isPlainPaste
         text: isPlainPaste
           ? JSON.stringify(systemClipboardData.elements, null, 2)
           ? JSON.stringify(systemClipboardData.elements, null, 2)
           : undefined,
           : undefined,
+        programmaticAPI,
       };
       };
     }
     }
   } catch (e) {}
   } catch (e) {}

+ 12 - 2
src/components/App.tsx

@@ -346,6 +346,10 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
 import { actionWrapTextInContainer } from "../actions/actionBoundText";
 import { actionWrapTextInContainer } from "../actions/actionBoundText";
 import BraveMeasureTextError from "./BraveMeasureTextError";
 import BraveMeasureTextError from "./BraveMeasureTextError";
 import { activeEyeDropperAtom } from "./EyeDropper";
 import { activeEyeDropperAtom } from "./EyeDropper";
+import {
+  ExcalidrawElementSkeleton,
+  convertToExcalidrawElements,
+} from "../data/transform";
 import { ValueOf } from "../utility-types";
 import { ValueOf } from "../utility-types";
 import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
 import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
 
 
@@ -2231,7 +2235,6 @@ class App extends React.Component<AppProps, AppState> {
       let file = event?.clipboardData?.files[0];
       let file = event?.clipboardData?.files[0];
 
 
       const data = await parseClipboard(event, isPlainPaste);
       const data = await parseClipboard(event, isPlainPaste);
-
       if (!file && data.text && !isPlainPaste) {
       if (!file && data.text && !isPlainPaste) {
         const string = data.text.trim();
         const string = data.text.trim();
         if (string.startsWith("<svg") && string.endsWith("</svg>")) {
         if (string.startsWith("<svg") && string.endsWith("</svg>")) {
@@ -2286,9 +2289,16 @@ class App extends React.Component<AppProps, AppState> {
           },
           },
         });
         });
       } else if (data.elements) {
       } 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
         // TODO remove formatting from elements if isPlainPaste
         this.addElementsFromPasteOrLibrary({
         this.addElementsFromPasteOrLibrary({
-          elements: data.elements,
+          elements,
           files: data.files || null,
           files: data.files || null,
           position: "cursor",
           position: "cursor",
           retainSeed: isPlainPaste,
           retainSeed: isPlainPaste,

+ 1 - 0
src/constants.ts

@@ -164,6 +164,7 @@ export const EXPORT_DATA_TYPES = {
   excalidraw: "excalidraw",
   excalidraw: "excalidraw",
   excalidrawClipboard: "excalidraw/clipboard",
   excalidrawClipboard: "excalidraw/clipboard",
   excalidrawLibrary: "excalidrawlib",
   excalidrawLibrary: "excalidrawlib",
+  excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
 } as const;
 } as const;
 
 
 export const EXPORT_SOURCE =
 export const EXPORT_SOURCE =

+ 2032 - 0
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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#66a80f",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "ellipse",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#9c36b5",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "diamond",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      395,
+      35,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "id42",
+    "focus": -0.08139534883720931,
+    "gap": 1,
+  },
+  "strokeColor": "#1864ab",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      400,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "diamond-1",
+    "focus": 0,
+    "gap": 1,
+  },
+  "strokeColor": "#e67700",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HEYYYYY",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#c2255c",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "HEYYYYY",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "Whats up ?",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "Whats up ?",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "text-1",
+    "focus": 0,
+    "gap": 1,
+  },
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO WORLD!!",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "HELLO WORLD!!",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "id34",
+    "focus": 0,
+    "gap": 1,
+  },
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO WORLD!!",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "HELLO WORLD!!",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "ellipse",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "id38",
+    "focus": 0,
+    "gap": 1,
+  },
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "arrow",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO WORLD!!",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "HELLO WORLD!!",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HEYYYYY",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "HEYYYYY",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "WHATS UP ?",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "WHATS UP ?",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": null,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "arrow",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": "dot",
+  "startBinding": null,
+  "strokeColor": "#1971c2",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": null,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "line",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": null,
+  "strokeColor": "#2f9e44",
+  "strokeStyle": "dotted",
+  "strokeWidth": 2,
+  "type": "line",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "ellipse",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "diamond",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "dotted",
+  "strokeWidth": 2,
+  "type": "ellipse",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1971c2",
+  "strokeStyle": "dashed",
+  "strokeWidth": 2,
+  "type": "diamond",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "HELLO WORLD!",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "HELLO WORLD!",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "STYLED HELLO WORLD!",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#5f3dc4",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "STYLED HELLO WORLD!",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": null,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "arrow",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": null,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "arrow",
+  "updated": 1,
+  "version": 1,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": null,
+  "strokeColor": "#1098ad",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      300,
+      0,
+    ],
+  ],
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "startArrowhead": null,
+  "startBinding": null,
+  "strokeColor": "#1098ad",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "LABELED ARROW",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "LABELED ARROW",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "STYLED LABELED ARROW",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#099268",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "STYLED LABELED ARROW",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "ANOTHER STYLED LABELLED ARROW",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1098ad",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "ANOTHER STYLED 
+LABELLED ARROW",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "ANOTHER STYLED LABELLED ARROW",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#099268",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "ANOTHER STYLED 
+LABELLED ARROW",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 3,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "ellipse",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "diamond",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "diamond",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#c2255c",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#f08c00",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "type": "ellipse",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "RECTANGLE TEXT CONTAINER",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "RECTANGLE TEXT CONTAINER",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "ELLIPSE TEXT CONTAINER",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "ELLIPSE TEXT 
+CONTAINER",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "DIAMOND
+TEXT CONTAINER",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "DIAMOND
+TEXT 
+CONTAINER",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "STYLED DIAMOND TEXT CONTAINER",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#099268",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "STYLED DIAMOND
+TEXT CONTAINER",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#c2255c",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "TOP LEFT ALIGNED 
+RECTANGLE TEXT 
+CONTAINER",
+  "textAlign": "left",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "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<String>,
+  "isDeleted": false,
+  "lineHeight": 1.25,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "originalText": "STYLED ELLIPSE TEXT CONTAINER",
+  "roughness": 1,
+  "roundness": null,
+  "seed": Any<Number>,
+  "strokeColor": "#c2255c",
+  "strokeStyle": "solid",
+  "strokeWidth": 1,
+  "text": "STYLED 
+ELLIPSE TEXT 
+CONTAINER",
+  "textAlign": "center",
+  "type": "text",
+  "updated": 1,
+  "version": 2,
+  "versionNonce": Any<Number>,
+  "verticalAlign": "middle",
+  "width": 130,
+  "x": 534.7893218813452,
+  "y": 522.5735931288071,
+}
+`;

+ 11 - 10
src/data/restore.ts

@@ -29,6 +29,7 @@ import {
   FONT_FAMILY,
   FONT_FAMILY,
   ROUNDNESS,
   ROUNDNESS,
   DEFAULT_SIDEBAR,
   DEFAULT_SIDEBAR,
+  DEFAULT_ELEMENT_PROPS,
 } from "../constants";
 } from "../constants";
 import { getDefaultAppState } from "../appState";
 import { getDefaultAppState } from "../appState";
 import { LinearElementEditor } from "../element/linearElementEditor";
 import { LinearElementEditor } from "../element/linearElementEditor";
@@ -41,7 +42,6 @@ import {
   getDefaultLineHeight,
   getDefaultLineHeight,
   measureBaseline,
   measureBaseline,
 } from "../element/textElement";
 } from "../element/textElement";
-import { COLOR_PALETTE } from "../colors";
 import { normalizeLink } from "./url";
 import { normalizeLink } from "./url";
 
 
 type RestoredAppState = Omit<
 type RestoredAppState = Omit<
@@ -122,16 +122,18 @@ const restoreElementWithProperties = <
     versionNonce: element.versionNonce ?? 0,
     versionNonce: element.versionNonce ?? 0,
     isDeleted: element.isDeleted ?? false,
     isDeleted: element.isDeleted ?? false,
     id: element.id || randomId(),
     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,
     angle: element.angle || 0,
     x: extra.x ?? element.x ?? 0,
     x: extra.x ?? element.x ?? 0,
     y: extra.y ?? element.y ?? 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,
     width: element.width || 0,
     height: element.height || 0,
     height: element.height || 0,
     seed: element.seed ?? 1,
     seed: element.seed ?? 1,
@@ -246,7 +248,6 @@ const restoreElement = (
         startArrowhead = null,
         startArrowhead = null,
         endArrowhead = element.type === "arrow" ? "arrow" : null,
         endArrowhead = element.type === "arrow" ? "arrow" : null,
       } = element;
       } = element;
-
       let x = element.x;
       let x = element.x;
       let y = element.y;
       let y = element.y;
       let points = // migrate old arrow model to new one
       let points = // migrate old arrow model to new one
@@ -410,7 +411,6 @@ export const restoreElements = (
 ): ExcalidrawElement[] => {
 ): ExcalidrawElement[] => {
   // used to detect duplicate top-level element ids
   // used to detect duplicate top-level element ids
   const existingIds = new Set<string>();
   const existingIds = new Set<string>();
-
   const localElementsMap = localElements ? arrayToMap(localElements) : null;
   const localElementsMap = localElements ? arrayToMap(localElements) : null;
   const restoredElements = (elements || []).reduce((elements, element) => {
   const restoredElements = (elements || []).reduce((elements, element) => {
     // filtering out selection, which is legacy, no longer kept in elements,
     // filtering out selection, which is legacy, no longer kept in elements,
@@ -429,6 +429,7 @@ export const restoreElements = (
           migratedElement = { ...migratedElement, id: randomId() };
           migratedElement = { ...migratedElement, id: randomId() };
         }
         }
         existingIds.add(migratedElement.id);
         existingIds.add(migratedElement.id);
+
         elements.push(migratedElement);
         elements.push(migratedElement);
       }
       }
     }
     }

+ 706 - 0
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",
+    );
+  });
+});

+ 561 - 0
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<ElementConstructorOpts, "x" | "y">;
+  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<ExcalidrawTextElement>)
+      ) &
+        MarkOptional<ElementConstructorOpts, "x" | "y">;
+  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<ExcalidrawTextElement>)
+      ) &
+        MarkOptional<ElementConstructorOpts, "x" | "y">;
+} & Partial<ExcalidrawLinearElement>;
+
+export type ValidContainer =
+  | {
+      type: Exclude<ExcalidrawGenericElement["type"], "selection">;
+      id?: ExcalidrawGenericElement["id"];
+      label?: {
+        text: string;
+        fontSize?: number;
+        fontFamily?: FontFamilyValues;
+        textAlign?: TextAlign;
+        verticalAlign?: VerticalAlign;
+      } & MarkOptional<ElementConstructorOpts, "x" | "y">;
+    } & ElementConstructorOpts;
+
+export type ExcalidrawElementSkeleton =
+  | Extract<
+      Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
+      | ExcalidrawEmbeddableElement
+      | ExcalidrawFreeDrawElement
+      | ExcalidrawFrameElement
+    >
+  | ({
+      type: Extract<ExcalidrawLinearElement["type"], "line">;
+      x: number;
+      y: number;
+    } & Partial<ExcalidrawLinearElement>)
+  | ValidContainer
+  | ValidLinearElement
+  | ({
+      type: "text";
+      text: string;
+      x: number;
+      y: number;
+      id?: ExcalidrawTextElement["id"];
+    } & Partial<ExcalidrawTextElement>)
+  | ({
+      type: Extract<ExcalidrawImageElement["type"], "image">;
+      x: number;
+      y: number;
+      fileId: FileId;
+    } & Partial<ExcalidrawImageElement>);
+
+const DEFAULT_LINEAR_ELEMENT_PROPS = {
+  width: 300,
+  height: 0,
+};
+
+const DEFAULT_DIMENSION = 100;
+
+const bindTextToContainer = (
+  container: ExcalidrawElement,
+  textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
+) => {
+  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<string, ExcalidrawElement>();
+
+  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<string, ExcalidrawElementSkeleton>();
+
+  // 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();
+};

+ 1 - 1
src/element/binding.ts

@@ -190,7 +190,7 @@ export const maybeBindLinearElement = (
   }
   }
 };
 };
 
 
-const bindLinearElement = (
+export const bindLinearElement = (
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   linearElement: NonDeleted<ExcalidrawLinearElement>,
   hoveredElement: ExcalidrawBindableElement,
   hoveredElement: ExcalidrawBindableElement,
   startOrEnd: "start" | "end",
   startOrEnd: "start" | "end",

+ 7 - 7
src/element/newElement.ts

@@ -46,7 +46,7 @@ import {
 } from "../constants";
 } from "../constants";
 import { MarkOptional, Merge, Mutable } from "../utility-types";
 import { MarkOptional, Merge, Mutable } from "../utility-types";
 
 
-type ElementConstructorOpts = MarkOptional<
+export type ElementConstructorOpts = MarkOptional<
   Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
   Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
   | "width"
   | "width"
   | "height"
   | "height"
@@ -187,7 +187,7 @@ export const newTextElement = (
     fontFamily?: FontFamilyValues;
     fontFamily?: FontFamilyValues;
     textAlign?: TextAlign;
     textAlign?: TextAlign;
     verticalAlign?: VerticalAlign;
     verticalAlign?: VerticalAlign;
-    containerId?: ExcalidrawTextContainer["id"];
+    containerId?: ExcalidrawTextContainer["id"] | null;
     lineHeight?: ExcalidrawTextElement["lineHeight"];
     lineHeight?: ExcalidrawTextElement["lineHeight"];
     strokeWidth?: ExcalidrawTextElement["strokeWidth"];
     strokeWidth?: ExcalidrawTextElement["strokeWidth"];
   } & ElementConstructorOpts,
   } & ElementConstructorOpts,
@@ -361,8 +361,8 @@ export const newFreeDrawElement = (
 export const newLinearElement = (
 export const newLinearElement = (
   opts: {
   opts: {
     type: ExcalidrawLinearElement["type"];
     type: ExcalidrawLinearElement["type"];
-    startArrowhead: Arrowhead | null;
-    endArrowhead: Arrowhead | null;
+    startArrowhead?: Arrowhead | null;
+    endArrowhead?: Arrowhead | null;
     points?: ExcalidrawLinearElement["points"];
     points?: ExcalidrawLinearElement["points"];
   } & ElementConstructorOpts,
   } & ElementConstructorOpts,
 ): NonDeleted<ExcalidrawLinearElement> => {
 ): NonDeleted<ExcalidrawLinearElement> => {
@@ -372,8 +372,8 @@ export const newLinearElement = (
     lastCommittedPoint: null,
     lastCommittedPoint: null,
     startBinding: null,
     startBinding: null,
     endBinding: null,
     endBinding: null,
-    startArrowhead: opts.startArrowhead,
-    endArrowhead: opts.endArrowhead,
+    startArrowhead: opts.startArrowhead || null,
+    endArrowhead: opts.endArrowhead || null,
   };
   };
 };
 };
 
 
@@ -477,7 +477,7 @@ export const deepCopyElement = <T extends ExcalidrawElement>(
  * utility wrapper to generate new id. In test env it reuses the old + postfix
  * utility wrapper to generate new id. In test env it reuses the old + postfix
  * for test assertions.
  * for test assertions.
  */
  */
-const regenerateId = (
+export const regenerateId = (
   /** supply null if no previous id exists */
   /** supply null if no previous id exists */
   previousId: string | null,
   previousId: string | null,
 ) => {
 ) => {

+ 12 - 4
src/element/textElement.ts

@@ -89,16 +89,23 @@ export const redrawTextBoundingBox = (
       container,
       container,
       textElement as ExcalidrawTextElementWithContainer,
       textElement as ExcalidrawTextElementWithContainer,
     );
     );
+    const maxContainerWidth = getBoundTextMaxWidth(container);
 
 
-    let nextHeight = container.height;
     if (metrics.height > maxContainerHeight) {
     if (metrics.height > maxContainerHeight) {
-      nextHeight = computeContainerDimensionForBoundText(
+      const nextHeight = computeContainerDimensionForBoundText(
         metrics.height,
         metrics.height,
         container.type,
         container.type,
       );
       );
       mutateElement(container, { height: nextHeight });
       mutateElement(container, { height: nextHeight });
       updateOriginalContainerCache(container.id, nextHeight);
       updateOriginalContainerCache(container.id, nextHeight);
     }
     }
+    if (metrics.width > maxContainerWidth) {
+      const nextWidth = computeContainerDimensionForBoundText(
+        metrics.width,
+        container.type,
+      );
+      mutateElement(container, { width: nextWidth });
+    }
     const updatedTextElement = {
     const updatedTextElement = {
       ...textElement,
       ...textElement,
       ...boundTextUpdates,
       ...boundTextUpdates,
@@ -859,8 +866,9 @@ const VALID_CONTAINER_TYPES = new Set([
   "arrow",
   "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 = (
 export const computeContainerDimensionForBoundText = (
   dimension: number,
   dimension: number,

+ 21 - 15
src/packages/excalidraw/example/App.tsx

@@ -75,6 +75,7 @@ const {
   WelcomeScreen,
   WelcomeScreen,
   MainMenu,
   MainMenu,
   LiveCollaborationTrigger,
   LiveCollaborationTrigger,
+  convertToExcalidrawElements,
 } = window.ExcalidrawLib;
 } = window.ExcalidrawLib;
 
 
 const COMMENT_ICON_DIMENSION = 32;
 const COMMENT_ICON_DIMENSION = 32;
@@ -140,7 +141,10 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
         ];
         ];
 
 
         //@ts-ignore
         //@ts-ignore
-        initialStatePromiseRef.current.promise.resolve(initialData);
+        initialStatePromiseRef.current.promise.resolve({
+          ...initialData,
+          elements: convertToExcalidrawElements(initialData.elements),
+        });
         excalidrawAPI.addFiles(imagesArray);
         excalidrawAPI.addFiles(imagesArray);
       };
       };
     };
     };
@@ -184,38 +188,40 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
   const updateScene = () => {
   const updateScene = () => {
     const sceneData = {
     const sceneData = {
       elements: restoreElements(
       elements: restoreElements(
-        [
+        convertToExcalidrawElements([
           {
           {
             type: "rectangle",
             type: "rectangle",
-            version: 141,
-            versionNonce: 361174001,
-            isDeleted: false,
-            id: "oDVXy8D6rom3H1-LLH2-f",
+            id: "rect-1",
             fillStyle: "hachure",
             fillStyle: "hachure",
             strokeWidth: 1,
             strokeWidth: 1,
             strokeStyle: "solid",
             strokeStyle: "solid",
             roughness: 1,
             roughness: 1,
-            opacity: 100,
             angle: 0,
             angle: 0,
             x: 100.50390625,
             x: 100.50390625,
             y: 93.67578125,
             y: 93.67578125,
             strokeColor: "#c92a2a",
             strokeColor: "#c92a2a",
-            backgroundColor: "transparent",
             width: 186.47265625,
             width: 186.47265625,
             height: 141.9765625,
             height: 141.9765625,
             seed: 1968410350,
             seed: 1968410350,
-            groupIds: [],
-            frameId: null,
-            boundElements: null,
-            locked: false,
-            link: null,
-            updated: 1,
             roundness: {
             roundness: {
               type: ROUNDNESS.ADAPTIVE_RADIUS,
               type: ROUNDNESS.ADAPTIVE_RADIUS,
               value: 32,
               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,
         null,
       ),
       ),
       appState: {
       appState: {

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 55 - 389
src/packages/excalidraw/example/initialData.tsx


+ 1 - 0
src/packages/excalidraw/index.tsx

@@ -253,3 +253,4 @@ export { LiveCollaborationTrigger };
 export { DefaultSidebar } from "../../components/DefaultSidebar";
 export { DefaultSidebar } from "../../components/DefaultSidebar";
 
 
 export { normalizeLink } from "../../data/url";
 export { normalizeLink } from "../../data/url";
+export { convertToExcalidrawElements } from "../../data/transform";

+ 2 - 3
src/tests/data/restore.test.ts

@@ -140,9 +140,8 @@ describe("restoreElements", () => {
     expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) });
     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 arrowElement = API.createElement({ type: "arrow" });
-
     const restoredElements = restore.restoreElements([arrowElement], null);
     const restoredElements = restore.restoreElements([arrowElement], null);
 
 
     const restoredArrow = restoredElements[0] as ExcalidrawLinearElement;
     const restoredArrow = restoredElements[0] as ExcalidrawLinearElement;
@@ -150,7 +149,7 @@ describe("restoreElements", () => {
     expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead);
     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" });
     const arrowElement = API.createElement({ type: "arrow" });
     Object.defineProperty(arrowElement, "endArrowhead", {
     Object.defineProperty(arrowElement, "endArrowhead", {
       get: vi.fn(() => undefined),
       get: vi.fn(() => undefined),

+ 13 - 0
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);
+};

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio