diff --git a/src/data/blob.ts b/src/data/blob.ts index 3aa8fe4093..4a9e1ac26c 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -135,23 +135,20 @@ export const loadSceneOrLibraryFromBlob = async ( try { const data = JSON.parse(contents); if (isValidExcalidrawData(data)) { + const excaldrawElements = convertToExcalidrawElements( + data.elements || [], + ); return { type: MIME_TYPES.excalidraw, data: restore( { - elements: clearElementsForExport( - convertToExcalidrawElements(data.elements || []), - ), + elements: clearElementsForExport(excaldrawElements), appState: { theme: localAppState?.theme, fileHandle: fileHandle || blob.handle || null, ...cleanAppStateForExport(data.appState || {}), ...(localAppState - ? calculateScrollCenter( - convertToExcalidrawElements(data.elements || []), - localAppState, - null, - ) + ? calculateScrollCenter(excaldrawElements, localAppState, null) : {}), }, files: data.files, diff --git a/src/data/transform.ts b/src/data/transform.ts index 49835764f2..335abdce04 100644 --- a/src/data/transform.ts +++ b/src/data/transform.ts @@ -1,4 +1,9 @@ -import { TEXT_ALIGN, VERTICAL_ALIGN } from "../constants"; +import { + DEFAULT_FONT_FAMILY, + DEFAULT_FONT_SIZE, + TEXT_ALIGN, + VERTICAL_ALIGN, +} from "../constants"; import { newElement, newLinearElement, @@ -6,8 +11,17 @@ import { } from "../element"; import { bindLinearElement } from "../element/binding"; import { mutateElement } from "../element/mutateElement"; -import { ElementConstructorOpts, newTextElement } from "../element/newElement"; -import { VALID_CONTAINER_TYPES } from "../element/textElement"; +import { + ElementConstructorOpts, + newTextElement, + regenerateId, +} from "../element/newElement"; +import { + VALID_CONTAINER_TYPES, + getDefaultLineHeight, + measureText, + normalizeText, +} from "../element/textElement"; import { ExcalidrawBindableElement, ExcalidrawElement, @@ -18,8 +32,8 @@ import { TextAlign, VerticalAlign, } from "../element/types"; -import { randomId } from "../random"; import { MarkOptional } from "../utility-types"; +import { getFontString } from "../utils"; import { ImportedDataState } from "./types"; export const ELEMENTS_SUPPORTING_PROGRAMMATIC_API = [ @@ -212,14 +226,20 @@ const excalidrawElements = (() => { }; const clear = () => { res.length = 0; + elementMap.clear(); }; const get = () => { return res; }; + const hasElementWithId = (id: string) => { + const index = elementMap.get(id); + return index !== undefined && index >= 0; + }; return { push, clear, get, + hasElementWithId, }; })(); @@ -234,6 +254,15 @@ export const convertToExcalidrawElements = ( if (!element) { return; } + + let elementId = element.id || regenerateId(null); + + // To make sure every element has a unique id + while (excalidrawElements.hasElementWithId(elementId)) { + elementId = regenerateId(elementId); + } + const elementWithid = { ...element, id: elementId }; + if (!ELEMENTS_SUPPORTING_PROGRAMMATIC_API.includes(element.type)) { excalidrawElements.push(element as ExcalidrawElement); @@ -242,7 +271,7 @@ export const convertToExcalidrawElements = ( //@ts-ignore if (VALID_CONTAINER_TYPES.has(element.type) && element?.label?.text) { //@ts-ignore - let [container, text] = bindTextToContainer(element, element.label); + let [container, text] = bindTextToContainer(elementWithid, element.label); excalidrawElements.push(container); excalidrawElements.push(text); @@ -263,15 +292,28 @@ export const convertToExcalidrawElements = ( } else { let excalidrawElement; if (element.type === "text") { - excalidrawElement = newTextElement({ - ...element, - }); + 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 = { + width: metrics.width, + height: metrics.height, + ...elementWithid, + }; - excalidrawElements.push(excalidrawElement); + excalidrawElements.push(excalidrawElement as ExcalidrawTextElement); } else if (element.type === "arrow" || element.type === "line") { const { linearElement, startBoundElement, endBoundElement } = //@ts-ignore - bindLinearElementToElement(element); + bindLinearElementToElement(elementWithid); excalidrawElements.push(linearElement); excalidrawElements.push(startBoundElement); excalidrawElements.push(endBoundElement); @@ -287,8 +329,7 @@ export const convertToExcalidrawElements = ( } } else { excalidrawElement = { - ...element, - id: element.id || randomId(), + ...elementWithid, width: element?.width || (ELEMENTS_SUPPORTING_PROGRAMMATIC_API.includes(element.type) diff --git a/src/data/types.ts b/src/data/types.ts index c06001068b..3a8fd20def 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -18,7 +18,6 @@ import type { cleanAppStateForExport } from "../appState"; import { VERSIONS } from "../constants"; import { MarkOptional } from "../utility-types"; import { ElementConstructorOpts } from "../element/newElement"; -import { ELEMENTS_SUPPORTING_PROGRAMMATIC_API } from "./transform"; export interface ExportedDataState { type: string; @@ -62,7 +61,7 @@ export interface ImportedDataState { type: "text"; text: string; id?: ExcalidrawTextElement["id"]; - } & ElementConstructorOpts) + } & Partial) | ({ type: ExcalidrawLinearElement["type"]; x: number; diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 804705c3d5..4e6a67b7fb 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -471,7 +471,7 @@ export const deepCopyElement = ( * utility wrapper to generate new id. In test env it reuses the old + postfix * for test assertions. */ -const regenerateId = ( +export const regenerateId = ( /** supply null if no previous id exists */ previousId: string | null, ) => {