mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-12-09 05:55:00 +01:00
add transform to the element package
This commit is contained in:
2720
packages/element/src/__tests__/__snapshots__/transform.test.ts.snap
Normal file
2720
packages/element/src/__tests__/__snapshots__/transform.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
967
packages/element/src/__tests__/transform.test.ts
Normal file
967
packages/element/src/__tests__/transform.test.ts
Normal file
@@ -0,0 +1,967 @@
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { convertToExcalidrawElements } from "../transform";
|
||||
|
||||
import type { ExcalidrawArrowElement } from "../types";
|
||||
|
||||
import type { ExcalidrawElementSkeleton } from "../types";
|
||||
|
||||
const opts = { regenerateIds: false };
|
||||
|
||||
describe("Test Transform", () => {
|
||||
it("should generate id unless opts.regenerateIds is set to false explicitly", () => {
|
||||
const elements = [
|
||||
{
|
||||
type: "rectangle",
|
||||
x: 100,
|
||||
y: 100,
|
||||
id: "rect-1",
|
||||
},
|
||||
];
|
||||
let data = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
);
|
||||
expect(data.length).toBe(1);
|
||||
expect(data[0].id).toBe("id0");
|
||||
|
||||
data = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
expect(data[0].id).toBe("rect-1");
|
||||
});
|
||||
|
||||
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[],
|
||||
opts,
|
||||
).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[],
|
||||
opts,
|
||||
).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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(4);
|
||||
|
||||
excalidrawElements.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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(12);
|
||||
|
||||
excalidrawElements.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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(8);
|
||||
|
||||
excalidrawElements.forEach((ele) => {
|
||||
expect(ele).toMatchSnapshot({
|
||||
seed: expect.any(Number),
|
||||
versionNonce: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Frames", () => {
|
||||
const elements: ExcalidrawElementSkeleton[] = [
|
||||
{
|
||||
type: "rectangle",
|
||||
x: 10,
|
||||
y: 10,
|
||||
strokeWidth: 2,
|
||||
id: "1",
|
||||
},
|
||||
{
|
||||
type: "diamond",
|
||||
x: 120,
|
||||
y: 20,
|
||||
backgroundColor: "#fff3bf",
|
||||
strokeWidth: 2,
|
||||
label: {
|
||||
text: "HELLO EXCALIDRAW",
|
||||
strokeColor: "#099268",
|
||||
fontSize: 30,
|
||||
},
|
||||
id: "2",
|
||||
},
|
||||
];
|
||||
|
||||
it("should transform frames and update frame ids when regenerated", () => {
|
||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||
...elements,
|
||||
{
|
||||
type: "frame",
|
||||
children: ["1", "2"],
|
||||
name: "My frame",
|
||||
},
|
||||
];
|
||||
const excalidrawElements = convertToExcalidrawElements(
|
||||
elementsSkeleton,
|
||||
opts,
|
||||
);
|
||||
expect(excalidrawElements.length).toBe(4);
|
||||
|
||||
excalidrawElements.forEach((ele) => {
|
||||
expect(ele).toMatchObject({
|
||||
seed: expect.any(Number),
|
||||
versionNonce: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should consider user defined frame dimensions over calculated when provided", () => {
|
||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||
...elements,
|
||||
{
|
||||
type: "frame",
|
||||
children: ["1", "2"],
|
||||
name: "My frame",
|
||||
width: 800,
|
||||
height: 100,
|
||||
},
|
||||
];
|
||||
const excalidrawElements = convertToExcalidrawElements(
|
||||
elementsSkeleton,
|
||||
opts,
|
||||
);
|
||||
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
||||
expect(frame.width).toBe(800);
|
||||
expect(frame.height).toBe(100);
|
||||
});
|
||||
|
||||
it("should consider user defined frame coordinates calculated when provided", () => {
|
||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||
...elements,
|
||||
{
|
||||
type: "frame",
|
||||
children: ["1", "2"],
|
||||
name: "My frame",
|
||||
x: 100,
|
||||
y: 300,
|
||||
},
|
||||
];
|
||||
const excalidrawElements = convertToExcalidrawElements(
|
||||
elementsSkeleton,
|
||||
opts,
|
||||
);
|
||||
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
|
||||
expect(frame.x).toBe(100);
|
||||
expect(frame.y).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(4);
|
||||
const [arrow, text, rectangle, ellipse] = excalidrawElements;
|
||||
expect(arrow).toMatchObject({
|
||||
type: "arrow",
|
||||
x: 255.5,
|
||||
y: 239,
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
startBinding: {
|
||||
elementId: rectangle.id,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: ellipse.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(text).toMatchObject({
|
||||
x: 240,
|
||||
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: 355,
|
||||
y: 189,
|
||||
type: "ellipse",
|
||||
boundElements: [
|
||||
{
|
||||
id: arrow.id,
|
||||
type: "arrow",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
excalidrawElements.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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(4);
|
||||
const [arrow, text1, text2, text3] = excalidrawElements;
|
||||
|
||||
expect(arrow).toMatchObject({
|
||||
type: "arrow",
|
||||
x: 255.5,
|
||||
y: 239,
|
||||
boundElements: [{ id: text1.id, type: "text" }],
|
||||
startBinding: {
|
||||
elementId: text2.id,
|
||||
},
|
||||
endBinding: {
|
||||
elementId: text3.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(text1).toMatchObject({
|
||||
x: 240,
|
||||
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: 355,
|
||||
y: 226.5,
|
||||
type: "text",
|
||||
boundElements: [
|
||||
{
|
||||
id: arrow.id,
|
||||
type: "arrow",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
excalidrawElements.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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(5);
|
||||
|
||||
excalidrawElements.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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(4);
|
||||
|
||||
excalidrawElements.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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(4);
|
||||
const [, , arrow, text] = excalidrawElements;
|
||||
expect(arrow).toMatchObject({
|
||||
type: "arrow",
|
||||
x: 255.5,
|
||||
y: 239,
|
||||
boundElements: [
|
||||
{
|
||||
id: text.id,
|
||||
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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
expect(excalidrawElements.length).toBe(2);
|
||||
const [arrow, rect] = excalidrawElements;
|
||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||
elementId: "rect-1",
|
||||
fixedPoint: [-2.05, 0.5001],
|
||||
mode: "orbit",
|
||||
});
|
||||
expect(rect.boundElements).toStrictEqual([
|
||||
{
|
||||
id: arrow.id,
|
||||
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 excalidrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excalidrawElements.length).toBe(1);
|
||||
expect(excalidrawElements[0]).toMatchSnapshot({
|
||||
seed: expect.any(Number),
|
||||
versionNonce: expect.any(Number),
|
||||
});
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Duplicate id found for rect-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("should contains customData if provided", () => {
|
||||
const rawData = [
|
||||
{
|
||||
type: "rectangle",
|
||||
x: 100,
|
||||
y: 100,
|
||||
customData: { createdBy: "user01" },
|
||||
},
|
||||
];
|
||||
const convertedElements = convertToExcalidrawElements(
|
||||
rawData as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
expect(convertedElements[0].customData).toStrictEqual({
|
||||
createdBy: "user01",
|
||||
});
|
||||
});
|
||||
|
||||
it("should transform the elements correctly when linear elements have single point", () => {
|
||||
const elements: ExcalidrawElementSkeleton[] = [
|
||||
{
|
||||
id: "B",
|
||||
type: "rectangle",
|
||||
groupIds: ["subgraph_group_B"],
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 166.03125,
|
||||
height: 163,
|
||||
label: {
|
||||
groupIds: ["subgraph_group_B"],
|
||||
text: "B",
|
||||
fontSize: 20,
|
||||
verticalAlign: "top",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "A",
|
||||
type: "rectangle",
|
||||
groupIds: ["subgraph_group_A"],
|
||||
x: 364.546875,
|
||||
y: 0,
|
||||
width: 120.265625,
|
||||
height: 114,
|
||||
label: {
|
||||
groupIds: ["subgraph_group_A"],
|
||||
text: "A",
|
||||
fontSize: 20,
|
||||
verticalAlign: "top",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Alice",
|
||||
type: "rectangle",
|
||||
groupIds: ["subgraph_group_A"],
|
||||
x: 389.546875,
|
||||
y: 35,
|
||||
width: 70.265625,
|
||||
height: 44,
|
||||
strokeWidth: 2,
|
||||
label: {
|
||||
groupIds: ["subgraph_group_A"],
|
||||
text: "Alice",
|
||||
fontSize: 20,
|
||||
},
|
||||
link: null,
|
||||
},
|
||||
{
|
||||
id: "Bob",
|
||||
type: "rectangle",
|
||||
groupIds: ["subgraph_group_B"],
|
||||
x: 54.76953125,
|
||||
y: 35,
|
||||
width: 56.4921875,
|
||||
height: 44,
|
||||
strokeWidth: 2,
|
||||
label: {
|
||||
groupIds: ["subgraph_group_B"],
|
||||
text: "Bob",
|
||||
fontSize: 20,
|
||||
},
|
||||
link: null,
|
||||
},
|
||||
{
|
||||
id: "Bob_Alice",
|
||||
type: "arrow",
|
||||
groupIds: [],
|
||||
x: 111.262,
|
||||
y: 57,
|
||||
strokeWidth: 2,
|
||||
points: [pointFrom(0, 0), pointFrom(272.985, 0)],
|
||||
label: {
|
||||
text: "How are you?",
|
||||
fontSize: 20,
|
||||
groupIds: [],
|
||||
},
|
||||
roundness: {
|
||||
type: 2,
|
||||
},
|
||||
start: {
|
||||
id: "Bob",
|
||||
},
|
||||
end: {
|
||||
id: "Alice",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Bob_B",
|
||||
type: "arrow",
|
||||
groupIds: [],
|
||||
x: 77.017,
|
||||
y: 79,
|
||||
strokeWidth: 2,
|
||||
points: [pointFrom(0, 0)],
|
||||
label: {
|
||||
text: "Friendship",
|
||||
fontSize: 20,
|
||||
groupIds: [],
|
||||
},
|
||||
roundness: {
|
||||
type: 2,
|
||||
},
|
||||
start: {
|
||||
id: "Bob",
|
||||
},
|
||||
end: {
|
||||
id: "B",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const excalidrawElements = convertToExcalidrawElements(elements, opts);
|
||||
expect(excalidrawElements.length).toBe(12);
|
||||
excalidrawElements.forEach((ele) => {
|
||||
expect(ele).toMatchSnapshot({
|
||||
seed: expect.any(Number),
|
||||
versionNonce: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -92,6 +92,7 @@ export * from "./store";
|
||||
export * from "./textElement";
|
||||
export * from "./textMeasurements";
|
||||
export * from "./textWrapping";
|
||||
export * from "./transform";
|
||||
export * from "./transformHandles";
|
||||
export * from "./typeChecks";
|
||||
export * from "./utils";
|
||||
|
||||
654
packages/element/src/transform.ts
Normal file
654
packages/element/src/transform.ts
Normal file
@@ -0,0 +1,654 @@
|
||||
import { pointFrom, type LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
TEXT_ALIGN,
|
||||
VERTICAL_ALIGN,
|
||||
getSizeFromPoints,
|
||||
randomId,
|
||||
arrayToMap,
|
||||
assertNever,
|
||||
cloneJSON,
|
||||
getFontString,
|
||||
isDevEnv,
|
||||
toBrandedType,
|
||||
getLineHeight,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { bindBindingElement } from "./binding";
|
||||
import {
|
||||
newArrowElement,
|
||||
newElement,
|
||||
newFrameElement,
|
||||
newImageElement,
|
||||
newLinearElement,
|
||||
newMagicFrameElement,
|
||||
newTextElement,
|
||||
type ElementConstructorOpts,
|
||||
} from "./newElement";
|
||||
import { measureText, normalizeText } from "./textMeasurements";
|
||||
import { isArrowElement } from "./typeChecks";
|
||||
|
||||
import { syncInvalidIndices } from "./fractionalIndex";
|
||||
|
||||
import { redrawTextBoundingBox } from "./textElement";
|
||||
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
||||
import { getCommonBounds } from "./bounds";
|
||||
|
||||
import { Scene } from "./Scene";
|
||||
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawElementSkeleton,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
ValidLinearElement,
|
||||
} from "./types";
|
||||
|
||||
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
||||
width: 100,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
const DEFAULT_DIMENSION = 100;
|
||||
|
||||
const bindTextToContainer = (
|
||||
container: ExcalidrawElement,
|
||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||
scene: Scene,
|
||||
) => {
|
||||
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, scene);
|
||||
|
||||
return [container, textElement] as const;
|
||||
};
|
||||
|
||||
const bindLinearElementToElement = (
|
||||
linearElement: ExcalidrawArrowElement,
|
||||
start: ValidLinearElement["start"],
|
||||
end: ValidLinearElement["end"],
|
||||
elementStore: ElementStore,
|
||||
scene: Scene,
|
||||
): {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bindBindingElement(
|
||||
linearElement,
|
||||
startBoundElement as ExcalidrawBindableElement,
|
||||
"orbit",
|
||||
"start",
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bindBindingElement(
|
||||
linearElement,
|
||||
endBoundElement as ExcalidrawBindableElement,
|
||||
"orbit",
|
||||
"end",
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Safe check to early return for single point
|
||||
if (linearElement.points.length < 2) {
|
||||
return {
|
||||
linearElement,
|
||||
startBoundElement,
|
||||
endBoundElement,
|
||||
};
|
||||
}
|
||||
|
||||
// Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.
|
||||
const endPointIndex = linearElement.points.length - 1;
|
||||
const delta = 0.5;
|
||||
|
||||
const newPoints = cloneJSON<readonly LocalPoint[]>(linearElement.points);
|
||||
|
||||
// left to right so shift the arrow towards right
|
||||
if (
|
||||
linearElement.points[endPointIndex][0] >
|
||||
linearElement.points[endPointIndex - 1][0]
|
||||
) {
|
||||
newPoints[0][0] = delta;
|
||||
newPoints[endPointIndex][0] -= delta;
|
||||
}
|
||||
|
||||
// right to left so shift the arrow towards left
|
||||
if (
|
||||
linearElement.points[endPointIndex][0] <
|
||||
linearElement.points[endPointIndex - 1][0]
|
||||
) {
|
||||
newPoints[0][0] = -delta;
|
||||
newPoints[endPointIndex][0] += delta;
|
||||
}
|
||||
// top to bottom so shift the arrow towards top
|
||||
if (
|
||||
linearElement.points[endPointIndex][1] >
|
||||
linearElement.points[endPointIndex - 1][1]
|
||||
) {
|
||||
newPoints[0][1] = delta;
|
||||
newPoints[endPointIndex][1] -= delta;
|
||||
}
|
||||
|
||||
// bottom to top so shift the arrow towards bottom
|
||||
if (
|
||||
linearElement.points[endPointIndex][1] <
|
||||
linearElement.points[endPointIndex - 1][1]
|
||||
) {
|
||||
newPoints[0][1] = -delta;
|
||||
newPoints[endPointIndex][1] += delta;
|
||||
}
|
||||
|
||||
Object.assign(
|
||||
linearElement,
|
||||
LinearElementEditor.getNormalizeElementPointsAndCoords({
|
||||
...linearElement,
|
||||
points: newPoints,
|
||||
}),
|
||||
);
|
||||
|
||||
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 syncInvalidIndices(Array.from(this.excalidrawElements.values()));
|
||||
};
|
||||
|
||||
getElementsMap = () => {
|
||||
return toBrandedType<NonDeletedSceneElementsMap>(
|
||||
arrayToMap(this.getElements()),
|
||||
);
|
||||
};
|
||||
|
||||
getElement = (id: string) => {
|
||||
return this.excalidrawElements.get(id);
|
||||
};
|
||||
}
|
||||
|
||||
export const convertToExcalidrawElements = (
|
||||
elementsSkeleton: ExcalidrawElementSkeleton[] | null,
|
||||
opts?: { regenerateIds: boolean },
|
||||
) => {
|
||||
if (!elementsSkeleton) {
|
||||
return [];
|
||||
}
|
||||
const elements = cloneJSON(elementsSkeleton);
|
||||
const elementStore = new ElementStore();
|
||||
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
||||
const oldToNewElementIdMap = new Map<string, string>();
|
||||
|
||||
// Create individual elements
|
||||
for (const element of elements) {
|
||||
let excalidrawElement: ExcalidrawElement;
|
||||
const originalId = element.id;
|
||||
if (opts?.regenerateIds !== false) {
|
||||
Object.assign(element, { id: randomId() });
|
||||
}
|
||||
|
||||
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: [pointFrom(0, 0), pointFrom(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 = newArrowElement({
|
||||
width,
|
||||
height,
|
||||
endArrowhead: "arrow",
|
||||
points: [pointFrom(0, 0), pointFrom(width, height)],
|
||||
...element,
|
||||
type: "arrow",
|
||||
});
|
||||
|
||||
Object.assign(
|
||||
excalidrawElement,
|
||||
getSizeFromPoints(excalidrawElement.points),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "text": {
|
||||
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
|
||||
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
|
||||
const lineHeight = element?.lineHeight || getLineHeight(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 "frame": {
|
||||
excalidrawElement = newFrameElement({
|
||||
x: 0,
|
||||
y: 0,
|
||||
...element,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "magicframe": {
|
||||
excalidrawElement = newMagicFrameElement({
|
||||
x: 0,
|
||||
y: 0,
|
||||
...element,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "freedraw":
|
||||
case "iframe":
|
||||
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);
|
||||
if (originalId) {
|
||||
oldToNewElementIdMap.set(originalId, excalidrawElement.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const elementsMap = elementStore.getElementsMap();
|
||||
// we don't have a real scene, so we just use a temp scene to query and mutate elements
|
||||
const scene = new Scene(elementsMap);
|
||||
|
||||
// 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,
|
||||
scene,
|
||||
);
|
||||
elementStore.add(container);
|
||||
elementStore.add(text);
|
||||
|
||||
if (isArrowElement(container)) {
|
||||
const originalStart =
|
||||
element.type === "arrow" ? element?.start : undefined;
|
||||
const originalEnd =
|
||||
element.type === "arrow" ? element?.end : undefined;
|
||||
if (originalStart && originalStart.id) {
|
||||
const newStartId = oldToNewElementIdMap.get(originalStart.id);
|
||||
if (newStartId) {
|
||||
Object.assign(originalStart, { id: newStartId });
|
||||
}
|
||||
}
|
||||
if (originalEnd && originalEnd.id) {
|
||||
const newEndId = oldToNewElementIdMap.get(originalEnd.id);
|
||||
if (newEndId) {
|
||||
Object.assign(originalEnd, { id: newEndId });
|
||||
}
|
||||
}
|
||||
const { linearElement, startBoundElement, endBoundElement } =
|
||||
bindLinearElementToElement(
|
||||
container,
|
||||
originalStart,
|
||||
originalEnd,
|
||||
elementStore,
|
||||
scene,
|
||||
);
|
||||
container = linearElement;
|
||||
elementStore.add(linearElement);
|
||||
elementStore.add(startBoundElement);
|
||||
elementStore.add(endBoundElement);
|
||||
}
|
||||
} else {
|
||||
switch (element.type) {
|
||||
case "arrow": {
|
||||
const { start, end } = element;
|
||||
if (start && start.id) {
|
||||
const newStartId = oldToNewElementIdMap.get(start.id);
|
||||
Object.assign(start, { id: newStartId });
|
||||
}
|
||||
if (end && end.id) {
|
||||
const newEndId = oldToNewElementIdMap.get(end.id);
|
||||
Object.assign(end, { id: newEndId });
|
||||
}
|
||||
const { linearElement, startBoundElement, endBoundElement } =
|
||||
bindLinearElementToElement(
|
||||
excalidrawElement as ExcalidrawArrowElement,
|
||||
start,
|
||||
end,
|
||||
elementStore,
|
||||
scene,
|
||||
);
|
||||
|
||||
elementStore.add(linearElement);
|
||||
elementStore.add(startBoundElement);
|
||||
elementStore.add(endBoundElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once all the excalidraw elements are created, we can add frames since we
|
||||
// need to calculate coordinates and dimensions of frame which is possible after all
|
||||
// frame children are processed.
|
||||
for (const [id, element] of elementsWithIds) {
|
||||
if (element.type !== "frame" && element.type !== "magicframe") {
|
||||
continue;
|
||||
}
|
||||
const frame = elementStore.getElement(id);
|
||||
|
||||
if (!frame) {
|
||||
throw new Error(`Excalidraw element with id ${id} doesn't exist`);
|
||||
}
|
||||
const childrenElements: ExcalidrawElement[] = [];
|
||||
|
||||
element.children.forEach((id) => {
|
||||
const newElementId = oldToNewElementIdMap.get(id);
|
||||
if (!newElementId) {
|
||||
throw new Error(`Element with ${id} wasn't mapped correctly`);
|
||||
}
|
||||
|
||||
const elementInFrame = elementStore.getElement(newElementId);
|
||||
if (!elementInFrame) {
|
||||
throw new Error(`Frame element with id ${newElementId} doesn't exist`);
|
||||
}
|
||||
Object.assign(elementInFrame, { frameId: frame.id });
|
||||
|
||||
elementInFrame?.boundElements?.forEach((boundElement) => {
|
||||
const ele = elementStore.getElement(boundElement.id);
|
||||
if (!ele) {
|
||||
throw new Error(
|
||||
`Bound element with id ${boundElement.id} doesn't exist`,
|
||||
);
|
||||
}
|
||||
Object.assign(ele, { frameId: frame.id });
|
||||
childrenElements.push(ele);
|
||||
});
|
||||
|
||||
childrenElements.push(elementInFrame);
|
||||
});
|
||||
|
||||
let [minX, minY, maxX, maxY] = getCommonBounds(childrenElements);
|
||||
|
||||
const PADDING = 10;
|
||||
minX = minX - PADDING;
|
||||
minY = minY - PADDING;
|
||||
maxX = maxX + PADDING;
|
||||
maxY = maxY + PADDING;
|
||||
|
||||
const frameX = frame?.x || minX;
|
||||
const frameY = frame?.y || minY;
|
||||
const frameWidth = frame?.width || maxX - minX;
|
||||
const frameHeight = frame?.height || maxY - minY;
|
||||
|
||||
Object.assign(frame, {
|
||||
x: frameX,
|
||||
y: frameY,
|
||||
width: frameWidth,
|
||||
height: frameHeight,
|
||||
});
|
||||
if (
|
||||
isDevEnv() &&
|
||||
element.children.length &&
|
||||
(frame?.x || frame?.y || frame?.width || frame?.height)
|
||||
) {
|
||||
console.info(
|
||||
"User provided frame attributes are being considered, if you find this inaccurate, please remove any of the attributes - x, y, width and height so frame coordinates and dimensions are calculated automatically",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return elementStore.getElements();
|
||||
};
|
||||
@@ -11,10 +11,13 @@ import type {
|
||||
import type {
|
||||
MakeBrand,
|
||||
MarkNonNullable,
|
||||
MarkOptional,
|
||||
Merge,
|
||||
ValueOf,
|
||||
} from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { ElementConstructorOpts } from "./newElement";
|
||||
|
||||
export type ChartType = "bar" | "line";
|
||||
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
|
||||
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
|
||||
@@ -433,3 +436,149 @@ export type ExcalidrawLinearElementSubType =
|
||||
export type ConvertibleGenericTypes = "rectangle" | "diamond" | "ellipse";
|
||||
export type ConvertibleLinearTypes = ExcalidrawLinearElementSubType;
|
||||
export type ConvertibleTypes = ConvertibleGenericTypes | ConvertibleLinearTypes;
|
||||
|
||||
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"
|
||||
| "magicframe"
|
||||
| "embeddable"
|
||||
| "iframe"
|
||||
>;
|
||||
id?: ExcalidrawGenericElement["id"];
|
||||
}
|
||||
| {
|
||||
id: ExcalidrawGenericElement["id"];
|
||||
type?: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
| "image"
|
||||
| "text"
|
||||
| "frame"
|
||||
| "magicframe"
|
||||
| "embeddable"
|
||||
| "iframe"
|
||||
>;
|
||||
}
|
||||
)
|
||||
| ((
|
||||
| {
|
||||
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"
|
||||
| "magicframe"
|
||||
| "embeddable"
|
||||
| "iframe"
|
||||
>;
|
||||
id?: ExcalidrawGenericElement["id"];
|
||||
}
|
||||
| {
|
||||
id: ExcalidrawGenericElement["id"];
|
||||
type?: Exclude<
|
||||
ExcalidrawBindableElement["type"],
|
||||
| "image"
|
||||
| "text"
|
||||
| "frame"
|
||||
| "magicframe"
|
||||
| "embeddable"
|
||||
| "iframe"
|
||||
>;
|
||||
}
|
||||
)
|
||||
| ((
|
||||
| {
|
||||
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>,
|
||||
ExcalidrawIframeLikeElement | ExcalidrawFreeDrawElement
|
||||
>
|
||||
| ({
|
||||
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>)
|
||||
| ({
|
||||
type: "frame";
|
||||
children: readonly ExcalidrawElement["id"][];
|
||||
name?: string;
|
||||
} & Partial<ExcalidrawFrameElement>)
|
||||
| ({
|
||||
type: "magicframe";
|
||||
children: readonly ExcalidrawElement["id"][];
|
||||
name?: string;
|
||||
} & Partial<ExcalidrawMagicFrameElement>);
|
||||
|
||||
Reference in New Issue
Block a user