mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-24 01:40:41 +02:00
Compare commits
3 Commits
master
...
mtolmacs/f
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8d359427d1 | ||
![]() |
06251ef8ed | ||
![]() |
54ca52e063 |
@@ -530,10 +530,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingRoomLinkData) {
|
if (!existingRoomLinkData) {
|
||||||
// when joining existing room, don't merge it with current scene data
|
|
||||||
this.excalidrawAPI.resetScene();
|
|
||||||
} else {
|
|
||||||
const elements = this.excalidrawAPI.getSceneElements().map((element) => {
|
const elements = this.excalidrawAPI.getSceneElements().map((element) => {
|
||||||
if (isImageElement(element) && element.status === "saved") {
|
if (isImageElement(element) && element.status === "saved") {
|
||||||
return newElementWith(element, { status: "pending" });
|
return newElementWith(element, { status: "pending" });
|
||||||
|
@@ -266,10 +266,7 @@ export const STRING_MIME_TYPES = {
|
|||||||
json: "application/json",
|
json: "application/json",
|
||||||
// excalidraw data
|
// excalidraw data
|
||||||
excalidraw: "application/vnd.excalidraw+json",
|
excalidraw: "application/vnd.excalidraw+json",
|
||||||
// LEGACY: fully-qualified library JSON data
|
|
||||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||||
// list of excalidraw library item ids
|
|
||||||
excalidrawlibIds: "application/vnd.excalidrawlib.ids+json",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const MIME_TYPES = {
|
export const MIME_TYPES = {
|
||||||
|
@@ -42,6 +42,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
|
isLineElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
@@ -321,19 +322,42 @@ export const getElementLineSegments = (
|
|||||||
|
|
||||||
if (shape.type === "polycurve") {
|
if (shape.type === "polycurve") {
|
||||||
const curves = shape.data;
|
const curves = shape.data;
|
||||||
const points = curves
|
const pointsOnCurves = curves.map((curve) =>
|
||||||
.map((curve) => pointsOnBezierCurves(curve, 10))
|
pointsOnBezierCurves(curve, 10),
|
||||||
.flat();
|
);
|
||||||
let i = 0;
|
|
||||||
const segments: LineSegment<GlobalPoint>[] = [];
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
while (i < points.length - 1) {
|
|
||||||
segments.push(
|
if (
|
||||||
lineSegment(
|
(isLineElement(element) && !element.polygon) ||
|
||||||
pointFrom(points[i][0], points[i][1]),
|
isArrowElement(element)
|
||||||
pointFrom(points[i + 1][0], points[i + 1][1]),
|
) {
|
||||||
),
|
for (const points of pointsOnCurves) {
|
||||||
);
|
let i = 0;
|
||||||
i++;
|
|
||||||
|
while (i < points.length - 1) {
|
||||||
|
segments.push(
|
||||||
|
lineSegment(
|
||||||
|
pointFrom(points[i][0], points[i][1]),
|
||||||
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const points = pointsOnCurves.flat();
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < points.length - 1) {
|
||||||
|
segments.push(
|
||||||
|
lineSegment(
|
||||||
|
pointFrom(points[i][0], points[i][1]),
|
||||||
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
|
@@ -433,8 +433,6 @@ import { findShapeByKey } from "./shapes";
|
|||||||
|
|
||||||
import UnlockPopup from "./UnlockPopup";
|
import UnlockPopup from "./UnlockPopup";
|
||||||
|
|
||||||
import type { ExcalidrawLibraryIds } from "../data/types";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
RenderInteractiveSceneCallback,
|
RenderInteractiveSceneCallback,
|
||||||
ScrollBars,
|
ScrollBars,
|
||||||
@@ -10547,44 +10545,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (imageFiles.length > 0 && this.isToolSupported("image")) {
|
if (imageFiles.length > 0 && this.isToolSupported("image")) {
|
||||||
return this.insertImages(imageFiles, sceneX, sceneY);
|
return this.insertImages(imageFiles, sceneX, sceneY);
|
||||||
}
|
}
|
||||||
const excalidrawLibrary_ids = dataTransferList.getData(
|
|
||||||
MIME_TYPES.excalidrawlibIds,
|
|
||||||
);
|
|
||||||
const excalidrawLibrary_data = dataTransferList.getData(
|
|
||||||
MIME_TYPES.excalidrawlib,
|
|
||||||
);
|
|
||||||
if (excalidrawLibrary_ids || excalidrawLibrary_data) {
|
|
||||||
try {
|
|
||||||
let libraryItems: LibraryItems | null = null;
|
|
||||||
if (excalidrawLibrary_ids) {
|
|
||||||
const { itemIds } = JSON.parse(
|
|
||||||
excalidrawLibrary_ids,
|
|
||||||
) as ExcalidrawLibraryIds;
|
|
||||||
const allLibraryItems = await this.library.getLatestLibrary();
|
|
||||||
libraryItems = allLibraryItems.filter((item) =>
|
|
||||||
itemIds.includes(item.id),
|
|
||||||
);
|
|
||||||
// legacy library dataTransfer format
|
|
||||||
} else if (excalidrawLibrary_data) {
|
|
||||||
libraryItems = parseLibraryJSON(excalidrawLibrary_data);
|
|
||||||
}
|
|
||||||
if (libraryItems?.length) {
|
|
||||||
libraryItems = libraryItems.map((item) => ({
|
|
||||||
...item,
|
|
||||||
// #6465
|
|
||||||
elements: duplicateElements({
|
|
||||||
type: "everything",
|
|
||||||
elements: item.elements,
|
|
||||||
randomizeSeed: true,
|
|
||||||
}).duplicatedElements,
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.addElementsFromPasteOrLibrary({
|
const libraryJSON = dataTransferList.getData(MIME_TYPES.excalidrawlib);
|
||||||
elements: distributeLibraryItemsOnSquareGrid(libraryItems),
|
if (libraryJSON && typeof libraryJSON === "string") {
|
||||||
position: event,
|
try {
|
||||||
files: null,
|
const libraryItems = parseLibraryJSON(libraryJSON);
|
||||||
});
|
this.addElementsFromPasteOrLibrary({
|
||||||
}
|
elements: distributeLibraryItemsOnSquareGrid(libraryItems),
|
||||||
|
position: event,
|
||||||
|
files: null,
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.setState({ errorMessage: error.message });
|
this.setState({ errorMessage: error.message });
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@ import { MIME_TYPES, arrayToMap } from "@excalidraw/common";
|
|||||||
|
|
||||||
import { duplicateElements } from "@excalidraw/element";
|
import { duplicateElements } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { serializeLibraryAsJSON } from "../data/json";
|
||||||
import { useLibraryCache } from "../hooks/useLibraryItemSvg";
|
import { useLibraryCache } from "../hooks/useLibraryItemSvg";
|
||||||
import { useScrollPosition } from "../hooks/useScrollPosition";
|
import { useScrollPosition } from "../hooks/useScrollPosition";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -26,8 +27,6 @@ import Stack from "./Stack";
|
|||||||
|
|
||||||
import "./LibraryMenuItems.scss";
|
import "./LibraryMenuItems.scss";
|
||||||
|
|
||||||
import type { ExcalidrawLibraryIds } from "../data/types";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
@@ -176,17 +175,12 @@ export default function LibraryMenuItems({
|
|||||||
|
|
||||||
const onItemDrag = useCallback(
|
const onItemDrag = useCallback(
|
||||||
(id: LibraryItem["id"], event: React.DragEvent) => {
|
(id: LibraryItem["id"], event: React.DragEvent) => {
|
||||||
// we want to serialize just the ids so the operation is fast and there's
|
|
||||||
// no race condition if people drop the library items on canvas too fast
|
|
||||||
const data: ExcalidrawLibraryIds = {
|
|
||||||
itemIds: selectedItems.includes(id) ? selectedItems : [id],
|
|
||||||
};
|
|
||||||
event.dataTransfer.setData(
|
event.dataTransfer.setData(
|
||||||
MIME_TYPES.excalidrawlibIds,
|
MIME_TYPES.excalidrawlib,
|
||||||
JSON.stringify(data),
|
serializeLibraryAsJSON(getInsertedElements(id)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[selectedItems],
|
[getInsertedElements],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isItemSelected = useCallback(
|
const isItemSelected = useCallback(
|
||||||
|
@@ -192,7 +192,6 @@ const createLibraryUpdate = (
|
|||||||
class Library {
|
class Library {
|
||||||
/** latest libraryItems */
|
/** latest libraryItems */
|
||||||
private currLibraryItems: LibraryItems = [];
|
private currLibraryItems: LibraryItems = [];
|
||||||
|
|
||||||
/** snapshot of library items since last onLibraryChange call */
|
/** snapshot of library items since last onLibraryChange call */
|
||||||
private prevLibraryItems = cloneLibraryItems(this.currLibraryItems);
|
private prevLibraryItems = cloneLibraryItems(this.currLibraryItems);
|
||||||
|
|
||||||
|
@@ -6,7 +6,6 @@ import type { cleanAppStateForExport } from "../appState";
|
|||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
LibraryItem,
|
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
LibraryItems_anyVersion,
|
LibraryItems_anyVersion,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
@@ -60,7 +59,3 @@ export interface ImportedLibraryData extends Partial<ExportedLibraryData> {
|
|||||||
/** @deprecated v1 */
|
/** @deprecated v1 */
|
||||||
library?: LibraryItems;
|
library?: LibraryItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExcalidrawLibraryIds = {
|
|
||||||
itemIds: LibraryItem["id"][];
|
|
||||||
};
|
|
||||||
|
@@ -2,10 +2,10 @@ import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
distanceToElement,
|
|
||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
|
getElementLineSegments,
|
||||||
getFreedrawOutlineAsSegments,
|
getFreedrawOutlineAsSegments,
|
||||||
getFreedrawOutlinePoints,
|
getFreedrawOutlinePoints,
|
||||||
intersectElementWithLineSegment,
|
intersectElementWithLineSegment,
|
||||||
@@ -265,19 +265,28 @@ const eraserTest = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} else if (
|
}
|
||||||
isArrowElement(element) ||
|
|
||||||
(isLineElement(element) && !element.polygon)
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
) {
|
|
||||||
|
if (isArrowElement(element) || (isLineElement(element) && !element.polygon)) {
|
||||||
const tolerance = Math.max(
|
const tolerance = Math.max(
|
||||||
element.strokeWidth,
|
element.strokeWidth,
|
||||||
(element.strokeWidth * 2) / zoom,
|
(element.strokeWidth * 2) / zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
return distanceToElement(element, elementsMap, lastPoint) <= tolerance;
|
// If the eraser movement is so fast that a large distance is covered
|
||||||
}
|
// between the last two points, the distanceToElement miss, so we test
|
||||||
|
// agaist each segment of the linear element
|
||||||
|
const segments = getElementLineSegments(element, elementsMap);
|
||||||
|
for (const seg of segments) {
|
||||||
|
if (lineSegmentsDistance(seg, pathSegment) <= tolerance) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
intersectElementWithLineSegment(element, elementsMap, pathSegment, 0, true)
|
intersectElementWithLineSegment(element, elementsMap, pathSegment, 0, true)
|
||||||
|
@@ -15,7 +15,7 @@ import { Excalidraw } from "../index";
|
|||||||
|
|
||||||
import { API } from "./helpers/api";
|
import { API } from "./helpers/api";
|
||||||
import { UI } from "./helpers/ui";
|
import { UI } from "./helpers/ui";
|
||||||
import { fireEvent, render, waitFor } from "./test-utils";
|
import { fireEvent, getCloneByOrigId, render, waitFor } from "./test-utils";
|
||||||
|
|
||||||
import type { LibraryItem, LibraryItems } from "../types";
|
import type { LibraryItem, LibraryItems } from "../types";
|
||||||
|
|
||||||
@@ -46,8 +46,52 @@ vi.mock("../data/filesystem.ts", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("library items inserting", () => {
|
describe("library", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
await act(() => {
|
||||||
|
return h.app.library.resetLibrary();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("import library via drag&drop", async () => {
|
||||||
|
expect(await h.app.library.getLatestLibrary()).toEqual([]);
|
||||||
|
await API.drop([
|
||||||
|
{
|
||||||
|
kind: "file",
|
||||||
|
type: MIME_TYPES.excalidrawlib,
|
||||||
|
file: await API.loadFile("./fixtures/fixture_library.excalidrawlib"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(await h.app.library.getLatestLibrary()).toEqual([
|
||||||
|
{
|
||||||
|
status: "unpublished",
|
||||||
|
elements: [expect.objectContaining({ id: "A" })],
|
||||||
|
id: "id0",
|
||||||
|
created: expect.any(Number),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: mocked to test logic, not actual drag&drop via UI
|
||||||
|
it("drop library item onto canvas", async () => {
|
||||||
|
expect(h.elements).toEqual([]);
|
||||||
|
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
|
||||||
|
await API.drop([
|
||||||
|
{
|
||||||
|
kind: "string",
|
||||||
|
value: serializeLibraryAsJSON(libraryItems),
|
||||||
|
type: MIME_TYPES.excalidrawlib,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should regenerate ids but retain bindings on library insert", async () => {
|
||||||
const rectangle = API.createElement({
|
const rectangle = API.createElement({
|
||||||
id: "rectangle1",
|
id: "rectangle1",
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
@@ -73,116 +117,45 @@ describe("library items inserting", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const libraryItems: LibraryItems = [
|
|
||||||
{
|
|
||||||
id: "libraryItem_id0",
|
|
||||||
status: "unpublished",
|
|
||||||
elements: [rectangle, text, arrow],
|
|
||||||
created: 0,
|
|
||||||
name: "test",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
await render(<Excalidraw initialData={{ libraryItems }} />);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await act(() => {
|
|
||||||
return h.app.library.resetLibrary();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should regenerate ids but retain bindings on library insert", async () => {
|
|
||||||
const libraryItems = await h.app.library.getLatestLibrary();
|
|
||||||
|
|
||||||
expect(libraryItems.length).toBe(1);
|
|
||||||
|
|
||||||
await API.drop([
|
await API.drop([
|
||||||
{
|
{
|
||||||
kind: "string",
|
kind: "string",
|
||||||
value: JSON.stringify({
|
value: serializeLibraryAsJSON([
|
||||||
itemIds: [libraryItems[0].id],
|
{
|
||||||
}),
|
id: "item1",
|
||||||
type: MIME_TYPES.excalidrawlibIds,
|
status: "published",
|
||||||
|
elements: [rectangle, text, arrow],
|
||||||
|
created: 1,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
type: MIME_TYPES.excalidrawlib,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const rectangle = h.elements.find((e) => e.type === "rectangle")!;
|
|
||||||
const text = h.elements.find((e) => e.type === "text")!;
|
|
||||||
const arrow = h.elements.find((e) => e.type === "arrow")!;
|
|
||||||
expect(h.elements).toEqual(
|
expect(h.elements).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: "rectangle",
|
[ORIG_ID]: "rectangle1",
|
||||||
id: expect.not.stringMatching("rectangle1"),
|
|
||||||
boundElements: expect.arrayContaining([
|
boundElements: expect.arrayContaining([
|
||||||
{ type: "text", id: text.id },
|
{ type: "text", id: getCloneByOrigId("text1").id },
|
||||||
{ type: "arrow", id: arrow.id },
|
{ type: "arrow", id: getCloneByOrigId("arrow1").id },
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: "text",
|
[ORIG_ID]: "text1",
|
||||||
id: expect.not.stringMatching("text1"),
|
containerId: getCloneByOrigId("rectangle1").id,
|
||||||
containerId: rectangle.id,
|
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: "arrow",
|
[ORIG_ID]: "arrow1",
|
||||||
id: expect.not.stringMatching("arrow1"),
|
|
||||||
endBinding: expect.objectContaining({
|
endBinding: expect.objectContaining({
|
||||||
elementId: rectangle.id,
|
elementId: getCloneByOrigId("rectangle1").id,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("library", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await render(<Excalidraw />);
|
|
||||||
await act(() => {
|
|
||||||
return h.app.library.resetLibrary();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("import library via drag&drop", async () => {
|
|
||||||
expect(await h.app.library.getLatestLibrary()).toEqual([]);
|
|
||||||
await API.drop([
|
|
||||||
{
|
|
||||||
kind: "file",
|
|
||||||
type: MIME_TYPES.excalidrawlib,
|
|
||||||
file: await API.loadFile("./fixtures/fixture_library.excalidrawlib"),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await waitFor(async () => {
|
|
||||||
expect(await h.app.library.getLatestLibrary()).toEqual([
|
|
||||||
{
|
|
||||||
status: "unpublished",
|
|
||||||
elements: [expect.objectContaining({ id: "A" })],
|
|
||||||
id: expect.any(String),
|
|
||||||
created: expect.any(Number),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: mocked to test logic, not actual drag&drop via UI
|
|
||||||
it("drop library item onto canvas", async () => {
|
|
||||||
expect(h.elements).toEqual([]);
|
|
||||||
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
|
|
||||||
await API.drop([
|
|
||||||
{
|
|
||||||
kind: "string",
|
|
||||||
value: serializeLibraryAsJSON(libraryItems),
|
|
||||||
type: MIME_TYPES.excalidrawlib,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(h.elements).toEqual([expect.objectContaining({ [ORIG_ID]: "A" })]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fix duplicate ids between items on insert", async () => {
|
it("should fix duplicate ids between items on insert", async () => {
|
||||||
// note, we're not testing for duplicate group ids and such because
|
// note, we're not testing for duplicate group ids and such because
|
||||||
|
Reference in New Issue
Block a user