feat: support adding multiple library items on canvas (#5116)
This commit is contained in:
parent
cad6097d60
commit
d2cc76e52e
@ -3,7 +3,7 @@ import {
|
|||||||
DistributeVerticallyIcon,
|
DistributeVerticallyIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { distributeElements, Distribution } from "../disitrubte";
|
import { distributeElements, Distribution } from "../distribute";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
@ -74,7 +74,7 @@ import {
|
|||||||
ZOOM_STEP,
|
ZOOM_STEP,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { loadFromBlob } from "../data";
|
import { loadFromBlob } from "../data";
|
||||||
import Library from "../data/library";
|
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||||
import { restore, restoreElements } from "../data/restore";
|
import { restore, restoreElements } from "../data/restore";
|
||||||
import {
|
import {
|
||||||
dragNewElement,
|
dragNewElement,
|
||||||
@ -232,6 +232,7 @@ import {
|
|||||||
isSupportedImageFile,
|
isSupportedImageFile,
|
||||||
loadSceneOrLibraryFromBlob,
|
loadSceneOrLibraryFromBlob,
|
||||||
normalizeFile,
|
normalizeFile,
|
||||||
|
parseLibraryJSON,
|
||||||
resizeImageFile,
|
resizeImageFile,
|
||||||
SVGStringToFile,
|
SVGStringToFile,
|
||||||
} from "../data/blob";
|
} from "../data/blob";
|
||||||
@ -5212,13 +5213,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryShapes = event.dataTransfer.getData(MIME_TYPES.excalidrawlib);
|
const libraryJSON = event.dataTransfer.getData(MIME_TYPES.excalidrawlib);
|
||||||
if (libraryShapes !== "") {
|
if (libraryJSON && typeof libraryJSON === "string") {
|
||||||
this.addElementsFromPasteOrLibrary({
|
try {
|
||||||
elements: JSON.parse(libraryShapes),
|
const libraryItems = parseLibraryJSON(libraryJSON);
|
||||||
position: event,
|
this.addElementsFromPasteOrLibrary({
|
||||||
files: null,
|
elements: distributeLibraryItemsOnSquareGrid(libraryItems),
|
||||||
});
|
position: event,
|
||||||
|
files: null,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
this.setState({ errorMessage: error.message });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ import { HelpDialog } from "./HelpDialog";
|
|||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import Library from "../data/library";
|
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||||
import { JSONExportDialog } from "./JSONExportDialog";
|
import { JSONExportDialog } from "./JSONExportDialog";
|
||||||
import { LibraryButton } from "./LibraryButton";
|
import { LibraryButton } from "./LibraryButton";
|
||||||
import { isImageFileHandle } from "../data/blob";
|
import { isImageFileHandle } from "../data/blob";
|
||||||
@ -277,7 +277,9 @@ const LayerUI = ({
|
|||||||
<LibraryMenu
|
<LibraryMenu
|
||||||
pendingElements={getSelectedElements(elements, appState, true)}
|
pendingElements={getSelectedElements(elements, appState, true)}
|
||||||
onClose={closeLibrary}
|
onClose={closeLibrary}
|
||||||
onInsertShape={onInsertElements}
|
onInsertLibraryItems={(libraryItems) => {
|
||||||
|
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
|
||||||
|
}}
|
||||||
onAddToLibrary={deselectItems}
|
onAddToLibrary={deselectItems}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
@ -76,7 +76,7 @@ const LibraryMenuWrapper = forwardRef<
|
|||||||
|
|
||||||
export const LibraryMenu = ({
|
export const LibraryMenu = ({
|
||||||
onClose,
|
onClose,
|
||||||
onInsertShape,
|
onInsertLibraryItems,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
theme,
|
theme,
|
||||||
@ -90,7 +90,7 @@ export const LibraryMenu = ({
|
|||||||
}: {
|
}: {
|
||||||
pendingElements: LibraryItem["elements"];
|
pendingElements: LibraryItem["elements"];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onInsertShape: (elements: LibraryItem["elements"]) => void;
|
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||||
onAddToLibrary: () => void;
|
onAddToLibrary: () => void;
|
||||||
theme: AppState["theme"];
|
theme: AppState["theme"];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
@ -270,7 +270,7 @@ export const LibraryMenu = ({
|
|||||||
onAddToLibrary={(elements) =>
|
onAddToLibrary={(elements) =>
|
||||||
addToLibrary(elements, libraryItemsData.libraryItems)
|
addToLibrary(elements, libraryItemsData.libraryItems)
|
||||||
}
|
}
|
||||||
onInsertShape={onInsertShape}
|
onInsertLibraryItems={onInsertLibraryItems}
|
||||||
pendingElements={pendingElements}
|
pendingElements={pendingElements}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { chunk } from "lodash";
|
import { chunk } from "lodash";
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { saveLibraryAsJSON } from "../data/json";
|
import { saveLibraryAsJSON, serializeLibraryAsJSON } from "../data/json";
|
||||||
import Library from "../data/library";
|
import Library from "../data/library";
|
||||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@ -21,7 +21,7 @@ import { ToolButton } from "./ToolButton";
|
|||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
import "./LibraryMenuItems.scss";
|
import "./LibraryMenuItems.scss";
|
||||||
import { VERSIONS } from "../constants";
|
import { MIME_TYPES, VERSIONS } from "../constants";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
import { fileOpen } from "../data/filesystem";
|
import { fileOpen } from "../data/filesystem";
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ const LibraryMenuItems = ({
|
|||||||
libraryItems,
|
libraryItems,
|
||||||
onRemoveFromLibrary,
|
onRemoveFromLibrary,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
onInsertShape,
|
onInsertLibraryItems,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
theme,
|
theme,
|
||||||
setAppState,
|
setAppState,
|
||||||
@ -47,7 +47,7 @@ const LibraryMenuItems = ({
|
|||||||
libraryItems: LibraryItems;
|
libraryItems: LibraryItems;
|
||||||
pendingElements: LibraryItem["elements"];
|
pendingElements: LibraryItem["elements"];
|
||||||
onRemoveFromLibrary: () => void;
|
onRemoveFromLibrary: () => void;
|
||||||
onInsertShape: (elements: LibraryItem["elements"]) => void;
|
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
|
||||||
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
|
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
|
||||||
theme: AppState["theme"];
|
theme: AppState["theme"];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
@ -252,6 +252,18 @@ const LibraryMenuItems = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getInsertedElements = (id: string) => {
|
||||||
|
let targetElements;
|
||||||
|
if (selectedItems.includes(id)) {
|
||||||
|
targetElements = libraryItems.filter((item) =>
|
||||||
|
selectedItems.includes(item.id),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
targetElements = libraryItems.filter((item) => item.id === id);
|
||||||
|
}
|
||||||
|
return targetElements;
|
||||||
|
};
|
||||||
|
|
||||||
const createLibraryItemCompo = (params: {
|
const createLibraryItemCompo = (params: {
|
||||||
item:
|
item:
|
||||||
| LibraryItem
|
| LibraryItem
|
||||||
@ -273,6 +285,12 @@ const LibraryMenuItems = ({
|
|||||||
id={params.item?.id || null}
|
id={params.item?.id || null}
|
||||||
selected={!!params.item?.id && selectedItems.includes(params.item.id)}
|
selected={!!params.item?.id && selectedItems.includes(params.item.id)}
|
||||||
onToggle={onItemSelectToggle}
|
onToggle={onItemSelectToggle}
|
||||||
|
onDrag={(id, event) => {
|
||||||
|
event.dataTransfer.setData(
|
||||||
|
MIME_TYPES.excalidrawlib,
|
||||||
|
serializeLibraryAsJSON(getInsertedElements(id)),
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
);
|
);
|
||||||
@ -291,7 +309,7 @@ const LibraryMenuItems = ({
|
|||||||
if (item.id) {
|
if (item.id) {
|
||||||
return createLibraryItemCompo({
|
return createLibraryItemCompo({
|
||||||
item,
|
item,
|
||||||
onClick: () => onInsertShape(item.elements),
|
onClick: () => onInsertLibraryItems(getInsertedElements(item.id)),
|
||||||
key: item.id,
|
key: item.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { MIME_TYPES } from "../constants";
|
|
||||||
import { useDeviceType } from "../components/App";
|
import { useDeviceType } from "../components/App";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
import { BinaryFiles, LibraryItem } from "../types";
|
import { BinaryFiles, LibraryItem } from "../types";
|
||||||
@ -29,6 +28,7 @@ export const LibraryUnit = ({
|
|||||||
onClick,
|
onClick,
|
||||||
selected,
|
selected,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
onDrag,
|
||||||
}: {
|
}: {
|
||||||
id: LibraryItem["id"] | /** for pending item */ null;
|
id: LibraryItem["id"] | /** for pending item */ null;
|
||||||
elements?: LibraryItem["elements"];
|
elements?: LibraryItem["elements"];
|
||||||
@ -37,6 +37,7 @@ export const LibraryUnit = ({
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onToggle: (id: string, event: React.MouseEvent) => void;
|
onToggle: (id: string, event: React.MouseEvent) => void;
|
||||||
|
onDrag: (id: string, event: React.DragEvent) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -99,11 +100,12 @@ export const LibraryUnit = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onDragStart={(event) => {
|
onDragStart={(event) => {
|
||||||
|
if (!id) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
event.dataTransfer.setData(
|
onDrag(id, event);
|
||||||
MIME_TYPES.excalidrawlib,
|
|
||||||
JSON.stringify(elements),
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{adder}
|
{adder}
|
||||||
|
@ -191,12 +191,11 @@ export const loadFromBlob = async (
|
|||||||
return ret.data;
|
return ret.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadLibraryFromBlob = async (
|
export const parseLibraryJSON = (
|
||||||
blob: Blob,
|
json: string,
|
||||||
defaultStatus: LibraryItem["status"] = "unpublished",
|
defaultStatus: LibraryItem["status"] = "unpublished",
|
||||||
) => {
|
) => {
|
||||||
const contents = await parseFileContents(blob);
|
const data: ImportedLibraryData | undefined = JSON.parse(json);
|
||||||
const data: ImportedLibraryData | undefined = JSON.parse(contents);
|
|
||||||
if (!isValidLibrary(data)) {
|
if (!isValidLibrary(data)) {
|
||||||
throw new Error("Invalid library");
|
throw new Error("Invalid library");
|
||||||
}
|
}
|
||||||
@ -204,6 +203,13 @@ export const loadLibraryFromBlob = async (
|
|||||||
return restoreLibraryItems(libraryItems, defaultStatus);
|
return restoreLibraryItems(libraryItems, defaultStatus);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const loadLibraryFromBlob = async (
|
||||||
|
blob: Blob,
|
||||||
|
defaultStatus: LibraryItem["status"] = "unpublished",
|
||||||
|
) => {
|
||||||
|
return parseLibraryJSON(await parseFileContents(blob), defaultStatus);
|
||||||
|
};
|
||||||
|
|
||||||
export const canvasToBlob = async (
|
export const canvasToBlob = async (
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
): Promise<Blob> => {
|
): Promise<Blob> => {
|
||||||
|
@ -9,6 +9,8 @@ import { restoreLibraryItems } from "./restore";
|
|||||||
import type App from "../components/App";
|
import type App from "../components/App";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { jotaiStore } from "../jotai";
|
import { jotaiStore } from "../jotai";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
import { AbortError } from "../errors";
|
import { AbortError } from "../errors";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
@ -242,6 +244,98 @@ class Library {
|
|||||||
|
|
||||||
export default Library;
|
export default Library;
|
||||||
|
|
||||||
|
export const distributeLibraryItemsOnSquareGrid = (
|
||||||
|
libraryItems: LibraryItems,
|
||||||
|
) => {
|
||||||
|
const PADDING = 50;
|
||||||
|
const ITEMS_PER_ROW = Math.ceil(Math.sqrt(libraryItems.length));
|
||||||
|
|
||||||
|
const resElements: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
const getMaxHeightPerRow = (row: number) => {
|
||||||
|
const maxHeight = libraryItems
|
||||||
|
.slice(row * ITEMS_PER_ROW, row * ITEMS_PER_ROW + ITEMS_PER_ROW)
|
||||||
|
.reduce((acc, item) => {
|
||||||
|
const { height } = getCommonBoundingBox(item.elements);
|
||||||
|
return Math.max(acc, height);
|
||||||
|
}, 0);
|
||||||
|
return maxHeight;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaxWidthPerCol = (targetCol: number) => {
|
||||||
|
let index = 0;
|
||||||
|
let currCol = 0;
|
||||||
|
let maxWidth = 0;
|
||||||
|
for (const item of libraryItems) {
|
||||||
|
if (index % ITEMS_PER_ROW === 0) {
|
||||||
|
currCol = 0;
|
||||||
|
}
|
||||||
|
if (currCol === targetCol) {
|
||||||
|
const { width } = getCommonBoundingBox(item.elements);
|
||||||
|
maxWidth = Math.max(maxWidth, width);
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
currCol++;
|
||||||
|
}
|
||||||
|
return maxWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
let colOffsetX = 0;
|
||||||
|
let rowOffsetY = 0;
|
||||||
|
|
||||||
|
let maxHeightCurrRow = 0;
|
||||||
|
let maxWidthCurrCol = 0;
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let col = 0;
|
||||||
|
let row = 0;
|
||||||
|
|
||||||
|
for (const item of libraryItems) {
|
||||||
|
if (index && index % ITEMS_PER_ROW === 0) {
|
||||||
|
rowOffsetY += maxHeightCurrRow + PADDING;
|
||||||
|
colOffsetX = 0;
|
||||||
|
col = 0;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (col === 0) {
|
||||||
|
maxHeightCurrRow = getMaxHeightPerRow(row);
|
||||||
|
}
|
||||||
|
maxWidthCurrCol = getMaxWidthPerCol(col);
|
||||||
|
|
||||||
|
const { minX, minY, width, height } = getCommonBoundingBox(item.elements);
|
||||||
|
const offsetCenterX = (maxWidthCurrCol - width) / 2;
|
||||||
|
const offsetCenterY = (maxHeightCurrRow - height) / 2;
|
||||||
|
resElements.push(
|
||||||
|
// eslint-disable-next-line no-loop-func
|
||||||
|
...item.elements.map((element) => ({
|
||||||
|
...element,
|
||||||
|
x:
|
||||||
|
element.x +
|
||||||
|
// offset for column
|
||||||
|
colOffsetX +
|
||||||
|
// offset to center in given square grid
|
||||||
|
offsetCenterX -
|
||||||
|
// subtract minX so that given item starts at 0 coord
|
||||||
|
minX,
|
||||||
|
y:
|
||||||
|
element.y +
|
||||||
|
// offset for row
|
||||||
|
rowOffsetY +
|
||||||
|
// offset to center in given square grid
|
||||||
|
offsetCenterY -
|
||||||
|
// subtract minY so that given item starts at 0 coord
|
||||||
|
minY,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
colOffsetX += maxWidthCurrCol + PADDING;
|
||||||
|
index++;
|
||||||
|
col++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resElements;
|
||||||
|
};
|
||||||
|
|
||||||
export const parseLibraryTokensFromUrl = () => {
|
export const parseLibraryTokensFromUrl = () => {
|
||||||
const libraryUrl =
|
const libraryUrl =
|
||||||
// current
|
// current
|
||||||
|
@ -59,9 +59,10 @@ export class API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static createElement = <
|
static createElement = <
|
||||||
T extends Exclude<ExcalidrawElement["type"], "selection">,
|
T extends Exclude<ExcalidrawElement["type"], "selection"> = "rectangle",
|
||||||
>({
|
>({
|
||||||
type,
|
// @ts-ignore
|
||||||
|
type = "rectangle",
|
||||||
id,
|
id,
|
||||||
x = 0,
|
x = 0,
|
||||||
y = x,
|
y = x,
|
||||||
@ -71,7 +72,7 @@ export class API {
|
|||||||
groupIds = [],
|
groupIds = [],
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
type: T;
|
type?: T;
|
||||||
x?: number;
|
x?: number;
|
||||||
y?: number;
|
y?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
@ -2,8 +2,12 @@ import { render, waitFor } from "./test-utils";
|
|||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { MIME_TYPES } from "../constants";
|
import { MIME_TYPES } from "../constants";
|
||||||
import { LibraryItem } from "../types";
|
import { LibraryItem, LibraryItems } from "../types";
|
||||||
import { UI } from "./helpers/ui";
|
import { UI } from "./helpers/ui";
|
||||||
|
import { serializeLibraryAsJSON } from "../data/json";
|
||||||
|
import { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||||
|
import { ExcalidrawGenericElement } from "../element/types";
|
||||||
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -37,7 +41,7 @@ describe("library", () => {
|
|||||||
await API.readFile("./fixtures/fixture_library.excalidrawlib", "utf8"),
|
await API.readFile("./fixtures/fixture_library.excalidrawlib", "utf8"),
|
||||||
).library[0];
|
).library[0];
|
||||||
await API.drop(
|
await API.drop(
|
||||||
new Blob([JSON.stringify(libraryItems)], {
|
new Blob([serializeLibraryAsJSON([libraryItems])], {
|
||||||
type: MIME_TYPES.excalidrawlib,
|
type: MIME_TYPES.excalidrawlib,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -53,7 +57,7 @@ describe("library", () => {
|
|||||||
await API.readFile("./fixtures/fixture_library.excalidrawlib", "utf8"),
|
await API.readFile("./fixtures/fixture_library.excalidrawlib", "utf8"),
|
||||||
).library[0];
|
).library[0];
|
||||||
await API.drop(
|
await API.drop(
|
||||||
new Blob([JSON.stringify(libraryItems)], {
|
new Blob([serializeLibraryAsJSON([libraryItems])], {
|
||||||
type: MIME_TYPES.excalidrawlib,
|
type: MIME_TYPES.excalidrawlib,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -63,3 +67,111 @@ describe("library", () => {
|
|||||||
expect(h.state.activeTool.type).toBe("selection");
|
expect(h.state.activeTool.type).toBe("selection");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("distributeLibraryItemsOnSquareGrid()", () => {
|
||||||
|
it("should distribute items on a grid", async () => {
|
||||||
|
const createLibraryItem = (
|
||||||
|
elements: ExcalidrawGenericElement[],
|
||||||
|
): LibraryItem => {
|
||||||
|
return {
|
||||||
|
id: `id-${Date.now()}`,
|
||||||
|
elements,
|
||||||
|
status: "unpublished",
|
||||||
|
created: Date.now(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const PADDING = 50;
|
||||||
|
|
||||||
|
const el1 = API.createElement({
|
||||||
|
id: "id1",
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const el2 = API.createElement({
|
||||||
|
id: "id2",
|
||||||
|
width: 100,
|
||||||
|
height: 80,
|
||||||
|
x: -100,
|
||||||
|
y: -50,
|
||||||
|
});
|
||||||
|
|
||||||
|
const el3 = API.createElement({
|
||||||
|
id: "id3",
|
||||||
|
width: 40,
|
||||||
|
height: 50,
|
||||||
|
x: -100,
|
||||||
|
y: -50,
|
||||||
|
});
|
||||||
|
|
||||||
|
const el4 = API.createElement({
|
||||||
|
id: "id4",
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const el5 = API.createElement({
|
||||||
|
id: "id5",
|
||||||
|
width: 70,
|
||||||
|
height: 100,
|
||||||
|
x: 40,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const libraryItems: LibraryItems = [
|
||||||
|
createLibraryItem([el1]),
|
||||||
|
createLibraryItem([el2]),
|
||||||
|
createLibraryItem([el3]),
|
||||||
|
createLibraryItem([el4, el5]),
|
||||||
|
];
|
||||||
|
|
||||||
|
const distributed = distributeLibraryItemsOnSquareGrid(libraryItems);
|
||||||
|
// assert the returned library items are flattened to elements
|
||||||
|
expect(distributed.length).toEqual(
|
||||||
|
libraryItems.map((x) => x.elements).flat().length,
|
||||||
|
);
|
||||||
|
expect(distributed).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: el1.id,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: el2.id,
|
||||||
|
x:
|
||||||
|
el1.width +
|
||||||
|
PADDING +
|
||||||
|
(getCommonBoundingBox([el4, el5]).width - el2.width) / 2,
|
||||||
|
y: Math.abs(el1.height - el2.height) / 2,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: el3.id,
|
||||||
|
x: Math.abs(el1.width - el3.width) / 2,
|
||||||
|
y:
|
||||||
|
Math.max(el1.height, el2.height) +
|
||||||
|
PADDING +
|
||||||
|
Math.abs(el3.height - Math.max(el4.height, el5.height)) / 2,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: el4.id,
|
||||||
|
x: Math.max(el1.width, el2.width) + PADDING,
|
||||||
|
y: Math.max(el1.height, el2.height) + PADDING,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: el5.id,
|
||||||
|
x: Math.max(el1.width, el2.width) + PADDING + Math.abs(el5.x - el4.x),
|
||||||
|
y:
|
||||||
|
Math.max(el1.height, el2.height) +
|
||||||
|
PADDING +
|
||||||
|
Math.abs(el5.y - el4.y),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user