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 <luzar.david@gmail.com>
This commit is contained in:
parent
ded0222e8d
commit
3ea07076ad
@ -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) {}
|
||||||
|
@ -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,
|
||||||
|
@ -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
src/data/__snapshots__/transform.test.ts.snap
Normal file
2032
src/data/__snapshots__/transform.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
@ -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",
|
fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle,
|
||||||
strokeWidth: element.strokeWidth || 1,
|
strokeWidth: element.strokeWidth || DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||||
strokeStyle: element.strokeStyle ?? "solid",
|
strokeStyle: element.strokeStyle ?? DEFAULT_ELEMENT_PROPS.strokeStyle,
|
||||||
roughness: element.roughness ?? 1,
|
roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness,
|
||||||
opacity: element.opacity == null ? 100 : element.opacity,
|
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,
|
strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||||
backgroundColor: element.backgroundColor || COLOR_PALETTE.transparent,
|
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
src/data/transform.test.ts
Normal file
706
src/data/transform.test.ts
Normal file
@ -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
src/data/transform.ts
Normal file
561
src/data/transform.ts
Normal file
@ -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();
|
||||||
|
};
|
@ -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",
|
||||||
|
@ -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;
|
startArrowhead?: Arrowhead | null;
|
||||||
endArrowhead: 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,
|
startArrowhead: opts.startArrowhead || null,
|
||||||
endArrowhead: opts.endArrowhead,
|
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,
|
||||||
) => {
|
) => {
|
||||||
|
@ -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) =>
|
export const isValidTextContainer = (element: {
|
||||||
VALID_CONTAINER_TYPES.has(element.type);
|
type: ExcalidrawElement["type"];
|
||||||
|
}) => VALID_CONTAINER_TYPES.has(element.type);
|
||||||
|
|
||||||
export const computeContainerDimensionForBoundText = (
|
export const computeContainerDimensionForBoundText = (
|
||||||
dimension: number,
|
dimension: number,
|
||||||
|
@ -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,
|
id: "rect-1",
|
||||||
versionNonce: 361174001,
|
|
||||||
isDeleted: false,
|
|
||||||
id: "oDVXy8D6rom3H1-LLH2-f",
|
|
||||||
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: {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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";
|
||||||
|
@ -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
src/utils.ts
13
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);
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user