mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge branch 'master' of https://github.com/excalidraw/excalidraw
# Conflicts: # packages/math/package.json # yarn.lock
This commit is contained in:
commit
671865a141
396 changed files with 8688 additions and 4581 deletions
|
@ -48,3 +48,6 @@ UNWEjuqNMi/lwAErS9fFa2oJlWyT8U7zzv/5kQREkxZI6y9v0AF3qcbsy2731FnD
|
||||||
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
|
||||||
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
|
||||||
HQIDAQAB'
|
HQIDAQAB'
|
||||||
|
|
||||||
|
# set to true in .env.development.local to disable the prevent unload dialog
|
||||||
|
VITE_APP_DISABLE_PREVENT_UNLOAD=
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
All `props` are _optional_.
|
All `props` are _optional_.
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
||||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
||||||
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
||||||
|
@ -13,7 +13,7 @@ All `props` are _optional_.
|
||||||
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
|
||||||
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
|
||||||
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
||||||
| [`generateLinkForSelection`](#generateLinkForSelection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
| [`generateLinkForSelection`](#generatelinkforselection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
|
||||||
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
|
||||||
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
|
||||||
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
|
||||||
|
@ -29,7 +29,7 @@ All `props` are _optional_.
|
||||||
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
|
||||||
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
|
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
|
||||||
### Storing custom data on Excalidraw elements
|
### Storing custom data on Excalidraw elements
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:preview": "yarn build && vite preview --port 5002",
|
"preview": "vite preview --port 5002",
|
||||||
|
"build:preview": "yarn build && yarn preview",
|
||||||
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
"build:package": "yarn workspace @excalidraw/excalidraw run build:esm"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,6 @@ import {
|
||||||
THEME,
|
THEME,
|
||||||
TITLE_TIMEOUT,
|
TITLE_TIMEOUT,
|
||||||
VERSION_TIMEOUT,
|
VERSION_TIMEOUT,
|
||||||
} from "@excalidraw/excalidraw/constants";
|
|
||||||
import polyfill from "@excalidraw/excalidraw/polyfill";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
|
||||||
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import {
|
|
||||||
debounce,
|
debounce,
|
||||||
getVersion,
|
getVersion,
|
||||||
getFrame,
|
getFrame,
|
||||||
|
@ -36,7 +29,14 @@ import {
|
||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
isRunningInIframe,
|
isRunningInIframe,
|
||||||
} from "@excalidraw/excalidraw/utils";
|
isDevEnv,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
import polyfill from "@excalidraw/excalidraw/polyfill";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
|
||||||
|
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
XBrandIcon,
|
XBrandIcon,
|
||||||
|
@ -47,10 +47,10 @@ import {
|
||||||
share,
|
share,
|
||||||
youtubeIcon,
|
youtubeIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import { isElementLink } from "@excalidraw/excalidraw/element/elementLink";
|
import { isElementLink } from "@excalidraw/element/elementLink";
|
||||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
parseLibraryTokensFromUrl,
|
parseLibraryTokensFromUrl,
|
||||||
|
@ -63,7 +63,7 @@ import type {
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
|
@ -71,8 +71,8 @@ import type {
|
||||||
ExcalidrawInitialDataState,
|
ExcalidrawInitialDataState,
|
||||||
UIAppState,
|
UIAppState,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { ResolutionType } from "@excalidraw/excalidraw/utility-types";
|
import type { ResolutionType } from "@excalidraw/common/utility-types";
|
||||||
import type { ResolvablePromise } from "@excalidraw/excalidraw/utils";
|
import type { ResolvablePromise } from "@excalidraw/common/utils";
|
||||||
|
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import {
|
import {
|
||||||
|
@ -383,7 +383,7 @@ const ExcalidrawWrapper = () => {
|
||||||
const [, forceRefresh] = useState(false);
|
const [, forceRefresh] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (import.meta.env.DEV) {
|
if (isDevEnv()) {
|
||||||
const debugState = loadSavedDebugState();
|
const debugState = loadSavedDebugState();
|
||||||
|
|
||||||
if (debugState.enabled && !window.visualDebug) {
|
if (debugState.enabled && !window.visualDebug) {
|
||||||
|
@ -608,7 +608,13 @@ const ExcalidrawWrapper = () => {
|
||||||
excalidrawAPI.getSceneElements(),
|
excalidrawAPI.getSceneElements(),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||||
preventUnload(event);
|
preventUnload(event);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { Stats } from "@excalidraw/excalidraw";
|
import { Stats } from "@excalidraw/excalidraw";
|
||||||
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
|
||||||
import { DEFAULT_VERSION } from "@excalidraw/excalidraw/constants";
|
import {
|
||||||
|
DEFAULT_VERSION,
|
||||||
|
debounce,
|
||||||
|
getVersion,
|
||||||
|
nFormatter,
|
||||||
|
} from "@excalidraw/common";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
import { debounce, getVersion, nFormatter } from "@excalidraw/excalidraw/utils";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useLayoutEffect, useRef } from "react";
|
||||||
import type {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
|
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "./app_constants";
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
|
|
@ -6,28 +6,29 @@ import {
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
|
||||||
import { APP_NAME, ENV, EVENT } from "@excalidraw/excalidraw/constants";
|
import { APP_NAME, EVENT } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
IDLE_THRESHOLD,
|
IDLE_THRESHOLD,
|
||||||
ACTIVE_THRESHOLD,
|
ACTIVE_THRESHOLD,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
} from "@excalidraw/excalidraw/constants";
|
|
||||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
|
||||||
import { getVisibleSceneBounds } from "@excalidraw/excalidraw/element/bounds";
|
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
|
||||||
import {
|
|
||||||
isImageElement,
|
|
||||||
isInitializedImageElement,
|
|
||||||
} from "@excalidraw/excalidraw/element/typeChecks";
|
|
||||||
import { AbortError } from "@excalidraw/excalidraw/errors";
|
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
|
||||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
|
||||||
import {
|
|
||||||
assertNever,
|
assertNever,
|
||||||
|
isDevEnv,
|
||||||
|
isTestEnv,
|
||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
throttleRAF,
|
throttleRAF,
|
||||||
} from "@excalidraw/excalidraw/utils";
|
} from "@excalidraw/common";
|
||||||
|
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||||
|
import { getVisibleSceneBounds } from "@excalidraw/element/bounds";
|
||||||
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
|
import {
|
||||||
|
isImageElement,
|
||||||
|
isInitializedImageElement,
|
||||||
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
import { AbortError } from "@excalidraw/excalidraw/errors";
|
||||||
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
||||||
|
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { PureComponent } from "react";
|
import { PureComponent } from "react";
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ import type {
|
||||||
FileId,
|
FileId,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
|
@ -49,7 +50,7 @@ import type {
|
||||||
Collaborator,
|
Collaborator,
|
||||||
Gesture,
|
Gesture,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { Mutable, ValueOf } from "@excalidraw/excalidraw/utility-types";
|
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { appJotaiStore, atom } from "../app-jotai";
|
import { appJotaiStore, atom } from "../app-jotai";
|
||||||
import {
|
import {
|
||||||
|
@ -240,7 +241,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||||
|
|
||||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (isTestEnv() || isDevEnv()) {
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
Object.defineProperties(window, {
|
Object.defineProperties(window, {
|
||||||
collab: {
|
collab: {
|
||||||
|
@ -300,7 +301,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||||
// the purpose is to run in immediately after user decides to stay
|
// the purpose is to run in immediately after user decides to stay
|
||||||
this.saveCollabRoomToFirebase(syncableElements);
|
this.saveCollabRoomToFirebase(syncableElements);
|
||||||
|
|
||||||
|
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
|
||||||
preventUnload(event);
|
preventUnload(event);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1013,7 +1020,7 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (isTestEnv() || isDevEnv()) {
|
||||||
window.collab = window.collab || ({} as Window["collab"]);
|
window.collab = window.collab || ({} as Window["collab"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
|
||||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
|
import type { UserIdleState } from "@excalidraw/common";
|
||||||
import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
OnUserFollowedPayload,
|
OnUserFollowedPayload,
|
||||||
SocketId,
|
SocketId,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
TTDDialog,
|
TTDDialog,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
|
import { getDataURL } from "@excalidraw/excalidraw/data/blob";
|
||||||
import { safelyParseJSON } from "@excalidraw/excalidraw/utils";
|
import { safelyParseJSON } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,9 @@ import {
|
||||||
import { MainMenu } from "@excalidraw/excalidraw/index";
|
import { MainMenu } from "@excalidraw/excalidraw/index";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
import { isDevEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { LanguageList } from "../app-language/LanguageList";
|
import { LanguageList } from "../app-language/LanguageList";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
@ -57,7 +59,7 @@ export const AppMainMenu: React.FC<{
|
||||||
>
|
>
|
||||||
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
|
||||||
</MainMenu.ItemLink>
|
</MainMenu.ItemLink>
|
||||||
{import.meta.env.DEV && (
|
{isDevEnv() && (
|
||||||
<MainMenu.Item
|
<MainMenu.Item
|
||||||
icon={eyeIcon}
|
icon={eyeIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
|
import { loginIcon } from "@excalidraw/excalidraw/components/icons";
|
||||||
import { POINTER_EVENTS } from "@excalidraw/excalidraw/constants";
|
import { POINTER_EVENTS } from "@excalidraw/common";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
|
import { WelcomeScreen } from "@excalidraw/excalidraw/index";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
|
@ -8,20 +8,21 @@ import {
|
||||||
getNormalizedCanvasDimensions,
|
getNormalizedCanvasDimensions,
|
||||||
} from "@excalidraw/excalidraw/renderer/helpers";
|
} from "@excalidraw/excalidraw/renderer/helpers";
|
||||||
import { type AppState } from "@excalidraw/excalidraw/types";
|
import { type AppState } from "@excalidraw/excalidraw/types";
|
||||||
import { throttleRAF } from "@excalidraw/excalidraw/utils";
|
import { throttleRAF } from "@excalidraw/common";
|
||||||
import { useCallback, useImperativeHandle, useRef } from "react";
|
import { useCallback, useImperativeHandle, useRef } from "react";
|
||||||
|
|
||||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isLineSegment,
|
isLineSegment,
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LineSegment,
|
type LineSegment,
|
||||||
} from "../../packages/math";
|
} from "@excalidraw/math";
|
||||||
import { isCurve } from "../../packages/math/curve";
|
import { isCurve } from "@excalidraw/math/curve";
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
|
||||||
|
|
||||||
import type { Curve } from "../../packages/math";
|
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
||||||
|
|
||||||
|
import type { Curve } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
const renderLine = (
|
const renderLine = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
|
import React from "react";
|
||||||
|
import { uploadBytes, ref } from "firebase/storage";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
import { Card } from "@excalidraw/excalidraw/components/Card";
|
import { Card } from "@excalidraw/excalidraw/components/Card";
|
||||||
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
|
||||||
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
|
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
|
||||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
|
import { MIME_TYPES, getFrame } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
encryptData,
|
encryptData,
|
||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
import { getFrame } from "@excalidraw/excalidraw/utils";
|
|
||||||
import { uploadBytes, ref } from "firebase/storage";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { THEME } from "@excalidraw/excalidraw/constants";
|
import { THEME } from "@excalidraw/common";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
// https://github.com/tholman/github-corners
|
||||||
export const GitHubCorner = React.memo(
|
export const GitHubCorner = React.memo(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||||
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
||||||
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -9,7 +9,7 @@ import type {
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
FileId,
|
FileId,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
|
|
|
@ -14,9 +14,9 @@ import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
|
||||||
import {
|
import {
|
||||||
CANVAS_SEARCH_TAB,
|
CANVAS_SEARCH_TAB,
|
||||||
DEFAULT_SIDEBAR,
|
DEFAULT_SIDEBAR,
|
||||||
} from "@excalidraw/excalidraw/constants";
|
debounce,
|
||||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
|
} from "@excalidraw/common";
|
||||||
import { debounce } from "@excalidraw/excalidraw/utils";
|
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
createStore,
|
createStore,
|
||||||
entries,
|
entries,
|
||||||
|
@ -29,16 +29,13 @@ import {
|
||||||
|
|
||||||
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
|
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
|
||||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||||
import type {
|
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
|
||||||
ExcalidrawElement,
|
|
||||||
FileId,
|
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { MaybePromise } from "@excalidraw/excalidraw/utility-types";
|
import type { MaybePromise } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { reconcileElements } from "@excalidraw/excalidraw";
|
import { reconcileElements } from "@excalidraw/excalidraw";
|
||||||
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
|
import { MIME_TYPES } from "@excalidraw/common";
|
||||||
import { decompressData } from "@excalidraw/excalidraw/data/encode";
|
import { decompressData } from "@excalidraw/excalidraw/data/encode";
|
||||||
import {
|
import {
|
||||||
encryptData,
|
encryptData,
|
||||||
decryptData,
|
decryptData,
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
|
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
|
||||||
import { getSceneVersion } from "@excalidraw/excalidraw/element";
|
import { getSceneVersion } from "@excalidraw/element";
|
||||||
import { initializeApp } from "firebase/app";
|
import { initializeApp } from "firebase/app";
|
||||||
import {
|
import {
|
||||||
getFirestore,
|
getFirestore,
|
||||||
|
@ -22,7 +22,7 @@ import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
|
|
|
@ -9,26 +9,26 @@ import {
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||||
import { restore } from "@excalidraw/excalidraw/data/restore";
|
import { restore } from "@excalidraw/excalidraw/data/restore";
|
||||||
import { isInvisiblySmallElement } from "@excalidraw/excalidraw/element/sizeHelpers";
|
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
|
||||||
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
import { bytesToHexString } from "@excalidraw/excalidraw/utils";
|
import { bytesToHexString } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
|
import type { UserIdleState } from "@excalidraw/common";
|
||||||
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
|
||||||
import type { SceneBounds } from "@excalidraw/excalidraw/element/bounds";
|
import type { SceneBounds } from "@excalidraw/element/bounds";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "@excalidraw/excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
SocketId,
|
SocketId,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
import type { MakeBrand } from "@excalidraw/excalidraw/utility-types";
|
import type { MakeBrand } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DELETED_ELEMENT_TIMEOUT,
|
DELETED_ELEMENT_TIMEOUT,
|
||||||
|
|
|
@ -2,9 +2,9 @@ import {
|
||||||
clearAppStateForLocalStorage,
|
clearAppStateForLocalStorage,
|
||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
} from "@excalidraw/excalidraw/appState";
|
} from "@excalidraw/excalidraw/appState";
|
||||||
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
|
import { clearElementsForLocalStorage } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
|
@ -15,8 +15,7 @@ import {
|
||||||
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
|
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
|
||||||
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
|
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
import { KEYS } from "@excalidraw/excalidraw/keys";
|
import { KEYS, getFrame } from "@excalidraw/common";
|
||||||
import { getFrame } from "@excalidraw/excalidraw/utils";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { atom, useAtom, useAtomValue } from "../app-jotai";
|
import { atom, useAtom, useAtomValue } from "../app-jotai";
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
createRedoAction,
|
createRedoAction,
|
||||||
createUndoAction,
|
createUndoAction,
|
||||||
} from "@excalidraw/excalidraw/actions/actionHistory";
|
} from "@excalidraw/excalidraw/actions/actionHistory";
|
||||||
import { syncInvalidIndices } from "@excalidraw/excalidraw/fractionalIndex";
|
import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { THEME } from "@excalidraw/excalidraw";
|
import { THEME } from "@excalidraw/excalidraw";
|
||||||
import { EVENT } from "@excalidraw/excalidraw/constants";
|
import { EVENT, CODES, KEYS } from "@excalidraw/common";
|
||||||
import { CODES, KEYS } from "@excalidraw/excalidraw/keys";
|
|
||||||
import { useEffect, useLayoutEffect, useState } from "react";
|
import { useEffect, useLayoutEffect, useState } from "react";
|
||||||
|
|
||||||
import type { Theme } from "@excalidraw/excalidraw/element/types";
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "./app_constants";
|
import { STORAGE_KEYS } from "./app_constants";
|
||||||
|
|
||||||
|
|
|
@ -23,29 +23,57 @@ export default defineConfig(({ mode }) => {
|
||||||
envDir: "../",
|
envDir: "../",
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: [
|
alias: [
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/common$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/common/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/common\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/common/src/$1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/element$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/element/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/element\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/element/src/$1"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/excalidraw$/,
|
find: /^@excalidraw\/excalidraw$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"),
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/excalidraw/index.tsx",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
find: /^@excalidraw\/excalidraw\/(.*?)/,
|
||||||
replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
|
replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
find: /^@excalidraw\/utils$/,
|
|
||||||
replacement: path.resolve(__dirname, "../packages/utils/index.ts"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
find: /^@excalidraw\/utils\/(.*?)/,
|
|
||||||
replacement: path.resolve(__dirname, "../packages/utils/$1"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/math$/,
|
find: /^@excalidraw\/math$/,
|
||||||
replacement: path.resolve(__dirname, "../packages/math/index.ts"),
|
replacement: path.resolve(__dirname, "../packages/math/src/index.ts"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
find: /^@excalidraw\/math\/(.*?)/,
|
find: /^@excalidraw\/math\/(.*?)/,
|
||||||
replacement: path.resolve(__dirname, "../packages/math/$1"),
|
replacement: path.resolve(__dirname, "../packages/math/src/$1"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/utils$/,
|
||||||
|
replacement: path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/utils/src/index.ts",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@excalidraw\/utils\/(.*?)/,
|
||||||
|
replacement: path.resolve(__dirname, "../packages/utils/src/$1"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -197,7 +225,7 @@ export default defineConfig(({ mode }) => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
start_url: "/",
|
start_url: "/",
|
||||||
id:"excalidraw",
|
id: "excalidraw",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
theme_color: "#121212",
|
theme_color: "#121212",
|
||||||
background_color: "#ffffff",
|
background_color: "#ffffff",
|
||||||
|
|
|
@ -4,9 +4,7 @@
|
||||||
"packageManager": "yarn@1.22.22",
|
"packageManager": "yarn@1.22.22",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"excalidraw-app",
|
"excalidraw-app",
|
||||||
"packages/excalidraw",
|
"packages/*",
|
||||||
"packages/utils",
|
|
||||||
"packages/math",
|
|
||||||
"examples/*"
|
"examples/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
3
packages/common/.eslintrc.json
Normal file
3
packages/common/.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": ["../eslintrc.base.json"]
|
||||||
|
}
|
19
packages/common/README.md
Normal file
19
packages/common/README.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# @excalidraw/common
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @excalidraw/common
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @excalidraw/common
|
||||||
|
```
|
||||||
|
|
||||||
|
With PNPM, similarly install the package with this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @excalidraw/common
|
||||||
|
```
|
3
packages/common/global.d.ts
vendored
Normal file
3
packages/common/global.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
import "@excalidraw/excalidraw/global";
|
||||||
|
import "@excalidraw/excalidraw/css";
|
56
packages/common/package.json
Normal file
56
packages/common/package.json
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"name": "@excalidraw/common",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist/types/common/src/index.d.ts",
|
||||||
|
"main": "./dist/prod/index.js",
|
||||||
|
"module": "./dist/prod/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/types/common/src/index.d.ts",
|
||||||
|
"development": "./dist/dev/index.js",
|
||||||
|
"production": "./dist/prod/index.js",
|
||||||
|
"default": "./dist/prod/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./../common/dist/types/common/src/*.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*"
|
||||||
|
],
|
||||||
|
"description": "Excalidraw common functions, constants, etc.",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"excalidraw",
|
||||||
|
"excalidraw-utils"
|
||||||
|
],
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all",
|
||||||
|
"not safari < 12",
|
||||||
|
"not kaios <= 2.5",
|
||||||
|
"not edge < 79",
|
||||||
|
"not chrome < 70",
|
||||||
|
"not and_uc < 13",
|
||||||
|
"not samsung < 10"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
|
"scripts": {
|
||||||
|
"gen:types": "rm -rf types && tsc",
|
||||||
|
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export default class BinaryHeap<T> {
|
export class BinaryHeap<T> {
|
||||||
private content: T[] = [];
|
private content: T[] = [];
|
||||||
|
|
||||||
constructor(private scoreFunction: (node: T) => number) {}
|
constructor(private scoreFunction: (node: T) => number) {}
|
|
@ -2,6 +2,8 @@ import oc from "open-color";
|
||||||
|
|
||||||
import type { Merge } from "./utility-types";
|
import type { Merge } from "./utility-types";
|
||||||
|
|
||||||
|
export const COLOR_OUTLINE_CONTRAST_THRESHOLD = 240;
|
||||||
|
|
||||||
// FIXME can't put to utils.ts rn because of circular dependency
|
// FIXME can't put to utils.ts rn because of circular dependency
|
||||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||||
source: R,
|
source: R,
|
|
@ -1,7 +1,10 @@
|
||||||
import { COLOR_PALETTE } from "./colors";
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
FontFamilyValues,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
import { COLOR_PALETTE } from "./colors";
|
||||||
import type { AppProps, AppState } from "./types";
|
|
||||||
|
|
||||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||||
export const isWindows = /^Win/.test(navigator.platform);
|
export const isWindows = /^Win/.test(navigator.platform);
|
||||||
|
@ -416,6 +419,7 @@ export const LIBRARY_DISABLED_TYPES = new Set([
|
||||||
// use these constants to easily identify reference sites
|
// use these constants to easily identify reference sites
|
||||||
export const TOOL_TYPE = {
|
export const TOOL_TYPE = {
|
||||||
selection: "selection",
|
selection: "selection",
|
||||||
|
lasso: "lasso",
|
||||||
rectangle: "rectangle",
|
rectangle: "rectangle",
|
||||||
diamond: "diamond",
|
diamond: "diamond",
|
||||||
ellipse: "ellipse",
|
ellipse: "ellipse",
|
|
@ -1,12 +1,9 @@
|
||||||
import {
|
import type {
|
||||||
FreedrawIcon,
|
ExcalidrawTextElement,
|
||||||
FontFamilyNormalIcon,
|
FontFamilyValues,
|
||||||
FontFamilyHeadingIcon,
|
} from "@excalidraw/element/types";
|
||||||
FontFamilyCodeIcon,
|
|
||||||
} from "../components/icons";
|
|
||||||
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "../constants";
|
|
||||||
|
|
||||||
import type { JSX } from "react";
|
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "./constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates font metrics with additional font metadata.
|
* Encapsulates font metrics with additional font metadata.
|
||||||
|
@ -23,8 +20,6 @@ export interface FontMetadata {
|
||||||
/** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */
|
/** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */
|
||||||
lineHeight: number;
|
lineHeight: number;
|
||||||
};
|
};
|
||||||
/** element to be displayed as an icon */
|
|
||||||
icon?: JSX.Element;
|
|
||||||
/** flag to indicate a deprecated font */
|
/** flag to indicate a deprecated font */
|
||||||
deprecated?: true;
|
deprecated?: true;
|
||||||
/** flag to indicate a server-side only font */
|
/** flag to indicate a server-side only font */
|
||||||
|
@ -43,7 +38,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
descender: -374,
|
descender: -374,
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
icon: FreedrawIcon,
|
|
||||||
},
|
},
|
||||||
[FONT_FAMILY.Nunito]: {
|
[FONT_FAMILY.Nunito]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
|
@ -52,7 +46,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
descender: -353,
|
descender: -353,
|
||||||
lineHeight: 1.35,
|
lineHeight: 1.35,
|
||||||
},
|
},
|
||||||
icon: FontFamilyNormalIcon,
|
|
||||||
},
|
},
|
||||||
[FONT_FAMILY["Lilita One"]]: {
|
[FONT_FAMILY["Lilita One"]]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
|
@ -61,7 +54,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
descender: -220,
|
descender: -220,
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.15,
|
||||||
},
|
},
|
||||||
icon: FontFamilyHeadingIcon,
|
|
||||||
},
|
},
|
||||||
[FONT_FAMILY["Comic Shanns"]]: {
|
[FONT_FAMILY["Comic Shanns"]]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
|
@ -70,7 +62,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
descender: -250,
|
descender: -250,
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
icon: FontFamilyCodeIcon,
|
|
||||||
},
|
},
|
||||||
[FONT_FAMILY.Virgil]: {
|
[FONT_FAMILY.Virgil]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
|
@ -79,7 +70,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
descender: -374,
|
descender: -374,
|
||||||
lineHeight: 1.25,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
icon: FreedrawIcon,
|
|
||||||
deprecated: true,
|
deprecated: true,
|
||||||
},
|
},
|
||||||
[FONT_FAMILY.Helvetica]: {
|
[FONT_FAMILY.Helvetica]: {
|
||||||
|
@ -89,7 +79,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
descender: -471,
|
descender: -471,
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.15,
|
||||||
},
|
},
|
||||||
icon: FontFamilyNormalIcon,
|
|
||||||
deprecated: true,
|
deprecated: true,
|
||||||
local: true,
|
local: true,
|
||||||
},
|
},
|
||||||
|
@ -100,7 +89,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
descender: -480,
|
descender: -480,
|
||||||
lineHeight: 1.2,
|
lineHeight: 1.2,
|
||||||
},
|
},
|
||||||
icon: FontFamilyCodeIcon,
|
|
||||||
deprecated: true,
|
deprecated: true,
|
||||||
},
|
},
|
||||||
[FONT_FAMILY["Liberation Sans"]]: {
|
[FONT_FAMILY["Liberation Sans"]]: {
|
||||||
|
@ -149,3 +137,34 @@ export const GOOGLE_FONTS_RANGES = {
|
||||||
|
|
||||||
/** local protocol to skip the local font from registering or inlining */
|
/** local protocol to skip the local font from registering or inlining */
|
||||||
export const LOCAL_FONT_PROTOCOL = "local:";
|
export const LOCAL_FONT_PROTOCOL = "local:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates vertical offset for a text with alphabetic baseline.
|
||||||
|
*/
|
||||||
|
export const getVerticalOffset = (
|
||||||
|
fontFamily: ExcalidrawTextElement["fontFamily"],
|
||||||
|
fontSize: ExcalidrawTextElement["fontSize"],
|
||||||
|
lineHeightPx: number,
|
||||||
|
) => {
|
||||||
|
const { unitsPerEm, ascender, descender } =
|
||||||
|
FONT_METADATA[fontFamily]?.metrics ||
|
||||||
|
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
|
||||||
|
|
||||||
|
const fontSizeEm = fontSize / unitsPerEm;
|
||||||
|
const lineGap =
|
||||||
|
(lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender) / 2;
|
||||||
|
|
||||||
|
const verticalOffset = fontSizeEm * ascender + lineGap;
|
||||||
|
return verticalOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets line height for a selected family.
|
||||||
|
*/
|
||||||
|
export const getLineHeight = (fontFamily: FontFamilyValues) => {
|
||||||
|
const { lineHeight } =
|
||||||
|
FONT_METADATA[fontFamily]?.metrics ||
|
||||||
|
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
|
||||||
|
|
||||||
|
return lineHeight as ExcalidrawTextElement["lineHeight"];
|
||||||
|
};
|
11
packages/common/src/index.ts
Normal file
11
packages/common/src/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export * from "./binary-heap";
|
||||||
|
export * from "./colors";
|
||||||
|
export * from "./constants";
|
||||||
|
export * from "./font-metadata";
|
||||||
|
export * from "./queue";
|
||||||
|
export * from "./keys";
|
||||||
|
export * from "./points";
|
||||||
|
export * from "./promise-pool";
|
||||||
|
export * from "./random";
|
||||||
|
export * from "./url";
|
||||||
|
export * from "./utils";
|
|
@ -4,6 +4,8 @@ import {
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type { NullableGridSize } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
export const getSizeFromPoints = (
|
export const getSizeFromPoints = (
|
||||||
points: readonly (GlobalPoint | LocalPoint)[],
|
points: readonly (GlobalPoint | LocalPoint)[],
|
||||||
) => {
|
) => {
|
||||||
|
@ -61,3 +63,18 @@ export const rescalePoints = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
|
||||||
return nextPoints;
|
return nextPoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Rounding this point causes some shake when free drawing
|
||||||
|
export const getGridPoint = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
gridSize: NullableGridSize,
|
||||||
|
): [number, number] => {
|
||||||
|
if (gridSize) {
|
||||||
|
return [
|
||||||
|
Math.round(x / gridSize) * gridSize,
|
||||||
|
Math.round(y / gridSize) * gridSize,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [x, y];
|
||||||
|
};
|
50
packages/common/src/promise-pool.ts
Normal file
50
packages/common/src/promise-pool.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import Pool from "es6-promise-pool";
|
||||||
|
|
||||||
|
// extending the missing types
|
||||||
|
// relying on the [Index, T] to keep a correct order
|
||||||
|
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
|
||||||
|
addEventListener: (
|
||||||
|
type: "fulfilled",
|
||||||
|
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||||
|
) => (event: { data: { result: [Index, T] } }) => void;
|
||||||
|
removeEventListener: (
|
||||||
|
type: "fulfilled",
|
||||||
|
listener: (event: { data: { result: [Index, T] } }) => void,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PromisePool<T> {
|
||||||
|
private readonly pool: TPromisePool<T>;
|
||||||
|
private readonly entries: Record<number, T> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
source: IterableIterator<Promise<void | readonly [number, T]>>,
|
||||||
|
concurrency: number,
|
||||||
|
) {
|
||||||
|
this.pool = new Pool(
|
||||||
|
source as unknown as () => void | PromiseLike<[number, T][]>,
|
||||||
|
concurrency,
|
||||||
|
) as TPromisePool<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public all() {
|
||||||
|
const listener = (event: { data: { result: void | [number, T] } }) => {
|
||||||
|
if (event.data.result) {
|
||||||
|
// by default pool does not return the results, so we are gathering them manually
|
||||||
|
// with the correct call order (represented by the index in the tuple)
|
||||||
|
const [index, value] = event.data.result;
|
||||||
|
this.entries[index] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pool.addEventListener("fulfilled", listener);
|
||||||
|
|
||||||
|
return this.pool.start().then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.pool.removeEventListener("fulfilled", listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.values(this.entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import { promiseTry, resolvablePromise } from "./utils";
|
import { promiseTry, resolvablePromise } from ".";
|
||||||
|
|
||||||
|
import type { ResolvablePromise } from ".";
|
||||||
|
|
||||||
import type { MaybePromise } from "./utility-types";
|
import type { MaybePromise } from "./utility-types";
|
||||||
import type { ResolvablePromise } from "./utils";
|
|
||||||
|
|
||||||
type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>;
|
type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { sanitizeUrl } from "@braintree/sanitize-url";
|
import { sanitizeUrl } from "@braintree/sanitize-url";
|
||||||
|
|
||||||
import { escapeDoubleQuotes } from "../utils";
|
import { escapeDoubleQuotes } from "./utils";
|
||||||
|
|
||||||
export const normalizeLink = (link: string) => {
|
export const normalizeLink = (link: string) => {
|
||||||
link = link.trim();
|
link = link.trim();
|
|
@ -1,30 +1,34 @@
|
||||||
import { average } from "@excalidraw/math";
|
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||||
import Pool from "es6-promise-pool";
|
|
||||||
|
|
||||||
import { COLOR_PALETTE } from "./colors";
|
|
||||||
import {
|
|
||||||
DEFAULT_VERSION,
|
|
||||||
FONT_FAMILY,
|
|
||||||
getFontFamilyFallbacks,
|
|
||||||
isDarwin,
|
|
||||||
WINDOWS_EMOJI_FALLBACK_FONT,
|
|
||||||
} from "./constants";
|
|
||||||
|
|
||||||
import type { EVENT } from "./constants";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
FontString,
|
FontString,
|
||||||
} from "./element/types";
|
ExcalidrawElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ActiveTool,
|
ActiveTool,
|
||||||
AppState,
|
AppState,
|
||||||
ToolType,
|
ToolType,
|
||||||
UnsubscribeCallback,
|
UnsubscribeCallback,
|
||||||
Zoom,
|
Zoom,
|
||||||
} from "./types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { COLOR_PALETTE } from "./colors";
|
||||||
|
import {
|
||||||
|
DEFAULT_VERSION,
|
||||||
|
ENV,
|
||||||
|
FONT_FAMILY,
|
||||||
|
getFontFamilyFallbacks,
|
||||||
|
isDarwin,
|
||||||
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||||
|
} from "./constants";
|
||||||
|
|
||||||
import type { MaybePromise, ResolutionType } from "./utility-types";
|
import type { MaybePromise, ResolutionType } from "./utility-types";
|
||||||
|
|
||||||
|
import type { EVENT } from "./constants";
|
||||||
|
|
||||||
let mockDateTime: string | null = null;
|
let mockDateTime: string | null = null;
|
||||||
|
|
||||||
export const setDateTimeForTests = (dateTime: string) => {
|
export const setDateTimeForTests = (dateTime: string) => {
|
||||||
|
@ -169,7 +173,7 @@ export const throttleRAF = <T extends any[]>(
|
||||||
};
|
};
|
||||||
|
|
||||||
const ret = (...args: T) => {
|
const ret = (...args: T) => {
|
||||||
if (import.meta.env.MODE === "test") {
|
if (isTestEnv()) {
|
||||||
fn(...args);
|
fn(...args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -382,7 +386,7 @@ export const updateActiveTool = (
|
||||||
type: ToolType;
|
type: ToolType;
|
||||||
}
|
}
|
||||||
| { type: "custom"; customType: string }
|
| { type: "custom"; customType: string }
|
||||||
) & { locked?: boolean }) & {
|
) & { locked?: boolean; fromSelection?: boolean }) & {
|
||||||
lastActiveToolBeforeEraser?: ActiveTool | null;
|
lastActiveToolBeforeEraser?: ActiveTool | null;
|
||||||
},
|
},
|
||||||
): AppState["activeTool"] => {
|
): AppState["activeTool"] => {
|
||||||
|
@ -404,6 +408,7 @@ export const updateActiveTool = (
|
||||||
type: data.type,
|
type: data.type,
|
||||||
customType: null,
|
customType: null,
|
||||||
locked: data.locked ?? appState.activeTool.locked,
|
locked: data.locked ?? appState.activeTool.locked,
|
||||||
|
fromSelection: data.fromSelection ?? false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -730,9 +735,9 @@ export const arrayToList = <T>(array: readonly T[]): Node<T>[] =>
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as Node<T>[]);
|
}, [] as Node<T>[]);
|
||||||
|
|
||||||
export const isTestEnv = () => import.meta.env.MODE === "test";
|
export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
||||||
|
|
||||||
export const isDevEnv = () => import.meta.env.MODE === "development";
|
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
||||||
|
|
||||||
export const isServerEnv = () =>
|
export const isServerEnv = () =>
|
||||||
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
||||||
|
@ -1186,54 +1191,6 @@ export const safelyParseJSON = (json: string): Record<string, any> | null => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// extending the missing types
|
|
||||||
// relying on the [Index, T] to keep a correct order
|
|
||||||
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
|
|
||||||
addEventListener: (
|
|
||||||
type: "fulfilled",
|
|
||||||
listener: (event: { data: { result: [Index, T] } }) => void,
|
|
||||||
) => (event: { data: { result: [Index, T] } }) => void;
|
|
||||||
removeEventListener: (
|
|
||||||
type: "fulfilled",
|
|
||||||
listener: (event: { data: { result: [Index, T] } }) => void,
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PromisePool<T> {
|
|
||||||
private readonly pool: TPromisePool<T>;
|
|
||||||
private readonly entries: Record<number, T> = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
source: IterableIterator<Promise<void | readonly [number, T]>>,
|
|
||||||
concurrency: number,
|
|
||||||
) {
|
|
||||||
this.pool = new Pool(
|
|
||||||
source as unknown as () => void | PromiseLike<[number, T][]>,
|
|
||||||
concurrency,
|
|
||||||
) as TPromisePool<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
public all() {
|
|
||||||
const listener = (event: { data: { result: void | [number, T] } }) => {
|
|
||||||
if (event.data.result) {
|
|
||||||
// by default pool does not return the results, so we are gathering them manually
|
|
||||||
// with the correct call order (represented by the index in the tuple)
|
|
||||||
const [index, value] = event.data.result;
|
|
||||||
this.entries[index] = value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pool.addEventListener("fulfilled", listener);
|
|
||||||
|
|
||||||
return this.pool.start().then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.pool.removeEventListener("fulfilled", listener);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.values(this.entries);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* use when you need to render unsafe string as HTML attribute, but MAKE SURE
|
* use when you need to render unsafe string as HTML attribute, but MAKE SURE
|
||||||
|
@ -1245,3 +1202,17 @@ export const escapeDoubleQuotes = (str: string) => {
|
||||||
|
|
||||||
export const castArray = <T>(value: T | T[]): T[] =>
|
export const castArray = <T>(value: T | T[]): T[] =>
|
||||||
Array.isArray(value) ? value : [value];
|
Array.isArray(value) ? value : [value];
|
||||||
|
|
||||||
|
export const elementCenterPoint = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
xOffset: number = 0,
|
||||||
|
yOffset: number = 0,
|
||||||
|
) => {
|
||||||
|
const { x, y, width, height } = element;
|
||||||
|
|
||||||
|
const centerXPoint = x + width / 2 + xOffset;
|
||||||
|
|
||||||
|
const centerYPoint = y + height / 2 + yOffset;
|
||||||
|
|
||||||
|
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { KEYS, matchKey } from "./keys";
|
import { KEYS, matchKey } from "../src/keys";
|
||||||
|
|
||||||
describe("key matcher", async () => {
|
describe("key matcher", async () => {
|
||||||
it("should not match unexpected key", async () => {
|
it("should not match unexpected key", async () => {
|
|
@ -1,4 +1,4 @@
|
||||||
import { Queue } from "./queue";
|
import { Queue } from "../src/queue";
|
||||||
|
|
||||||
describe("Queue", () => {
|
describe("Queue", () => {
|
||||||
const calls: any[] = [];
|
const calls: any[] = [];
|
|
@ -1,4 +1,4 @@
|
||||||
import { normalizeLink } from "./url";
|
import { normalizeLink } from "../src/url";
|
||||||
|
|
||||||
describe("normalizeLink", () => {
|
describe("normalizeLink", () => {
|
||||||
// NOTE not an extensive XSS test suite, just to check if we're not
|
// NOTE not an extensive XSS test suite, just to check if we're not
|
8
packages/common/tsconfig.json
Normal file
8
packages/common/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/types"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "global.d.ts"],
|
||||||
|
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
|
||||||
|
}
|
3
packages/element/.eslintrc.json
Normal file
3
packages/element/.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": ["../eslintrc.base.json"]
|
||||||
|
}
|
19
packages/element/README.md
Normal file
19
packages/element/README.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# @excalidraw/element
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @excalidraw/element
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer Yarn over npm, use this command to install the Excalidraw utils package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @excalidraw/element
|
||||||
|
```
|
||||||
|
|
||||||
|
With PNPM, similarly install the package with this command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @excalidraw/element
|
||||||
|
```
|
3
packages/element/global.d.ts
vendored
Normal file
3
packages/element/global.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
import "@excalidraw/excalidraw/global";
|
||||||
|
import "@excalidraw/excalidraw/css";
|
56
packages/element/package.json
Normal file
56
packages/element/package.json
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"name": "@excalidraw/element",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"types": "./dist/types/element/src/index.d.ts",
|
||||||
|
"main": "./dist/prod/index.js",
|
||||||
|
"module": "./dist/prod/index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/types/element/src/index.d.ts",
|
||||||
|
"development": "./dist/dev/index.js",
|
||||||
|
"production": "./dist/prod/index.js",
|
||||||
|
"default": "./dist/prod/index.js"
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
"types": "./../element/dist/types/element/src/*.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*"
|
||||||
|
],
|
||||||
|
"description": "Excalidraw elements-related logic",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"excalidraw",
|
||||||
|
"excalidraw-utils"
|
||||||
|
],
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all",
|
||||||
|
"not safari < 12",
|
||||||
|
"not kaios <= 2.5",
|
||||||
|
"not edge < 79",
|
||||||
|
"not chrome < 70",
|
||||||
|
"not and_uc < 13",
|
||||||
|
"not samsung < 10"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
|
"scripts": {
|
||||||
|
"gen:types": "rm -rf types && tsc",
|
||||||
|
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,26 @@
|
||||||
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
|
|
||||||
import { ROUGHNESS } from "../constants";
|
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
||||||
import { getDiamondPoints, getArrowheadPoints } from "../element";
|
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||||
import { headingForPointIsHorizontal } from "../element/heading";
|
|
||||||
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||||
|
import type { ElementShapes } from "@excalidraw/excalidraw/scene/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
isIframeElement,
|
isIframeElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "../element/typeChecks";
|
} from "./typeChecks";
|
||||||
import { generateFreeDrawShape } from "../renderer/renderElement";
|
import { getCornerRadius, isPathALoop } from "./shapes";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { headingForPointIsHorizontal } from "./heading";
|
||||||
import { isTransparent, assertNever } from "../utils";
|
|
||||||
|
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
|
import { generateFreeDrawShape } from "./renderElement";
|
||||||
|
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
@ -23,9 +28,8 @@ import type {
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
} from "../element/types";
|
} from "./types";
|
||||||
import type { EmbedsValidationStatus } from "../types";
|
|
||||||
import type { ElementShapes } from "./types";
|
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
@ -511,7 +515,10 @@ export const _generateElementShape = (
|
||||||
|
|
||||||
if (isPathALoop(element.points)) {
|
if (isPathALoop(element.points)) {
|
||||||
// generate rough polygon to fill freedraw shape
|
// generate rough polygon to fill freedraw shape
|
||||||
const simplifiedPoints = simplify(element.points, 0.75);
|
const simplifiedPoints = simplify(
|
||||||
|
element.points as Mutable<LocalPoint[]>,
|
||||||
|
0.75,
|
||||||
|
);
|
||||||
shape = generator.curve(simplifiedPoints as [number, number][], {
|
shape = generator.curve(simplifiedPoints as [number, number][], {
|
||||||
...generateRoughOptions(element),
|
...generateRoughOptions(element),
|
||||||
stroke: "none",
|
stroke: "none",
|
|
@ -1,16 +1,22 @@
|
||||||
import { RoughGenerator } from "roughjs/bin/generator";
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
import { COLOR_PALETTE } from "../colors";
|
import { COLOR_PALETTE } from "@excalidraw/common";
|
||||||
import { elementWithCanvasCache } from "../renderer/renderElement";
|
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
EmbedsValidationStatus,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import type {
|
||||||
|
ElementShape,
|
||||||
|
ElementShapes,
|
||||||
|
} from "@excalidraw/excalidraw/scene/types";
|
||||||
|
|
||||||
import { _generateElementShape } from "./Shape";
|
import { _generateElementShape } from "./Shape";
|
||||||
|
|
||||||
import type {
|
import { elementWithCanvasCache } from "./renderElement";
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawSelectionElement,
|
import type { ExcalidrawElement, ExcalidrawSelectionElement } from "./types";
|
||||||
} from "../element/types";
|
|
||||||
import type { AppState, EmbedsValidationStatus } from "../types";
|
|
||||||
import type { ElementShape, ElementShapes } from "./types";
|
|
||||||
import type { Drawable } from "roughjs/bin/core";
|
import type { Drawable } from "roughjs/bin/core";
|
||||||
|
|
||||||
export class ShapeCache {
|
export class ShapeCache {
|
|
@ -1,11 +1,12 @@
|
||||||
import { updateBoundElements } from "./element/binding";
|
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
import { getCommonBoundingBox } from "./element/bounds";
|
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { updateBoundElements } from "./binding";
|
||||||
|
import { getCommonBoundingBox } from "./bounds";
|
||||||
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
import type { BoundingBox } from "./element/bounds";
|
import type { BoundingBox } from "./bounds";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||||
import type Scene from "./scene/Scene";
|
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
position: "start" | "center" | "end";
|
position: "start" | "center" | "end";
|
|
@ -1,3 +1,14 @@
|
||||||
|
import {
|
||||||
|
KEYS,
|
||||||
|
arrayToMap,
|
||||||
|
isBindingFallthroughEnabled,
|
||||||
|
tupleToCoors,
|
||||||
|
invariant,
|
||||||
|
isDevEnv,
|
||||||
|
isTestEnv,
|
||||||
|
elementCenterPoint,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
lineSegment,
|
lineSegment,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
|
@ -13,20 +24,18 @@ import {
|
||||||
vectorCross,
|
vectorCross,
|
||||||
pointsEqual,
|
pointsEqual,
|
||||||
lineSegmentIntersectionPoints,
|
lineSegmentIntersectionPoints,
|
||||||
round,
|
|
||||||
PRECISION,
|
PRECISION,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { isPointOnShape } from "@excalidraw/utils/collision";
|
import { isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import { KEYS } from "../keys";
|
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
|
|
||||||
import {
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
arrayToMap,
|
|
||||||
isBindingFallthroughEnabled,
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
tupleToCoors,
|
|
||||||
} from "../utils";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCenterForBounds,
|
getCenterForBounds,
|
||||||
|
@ -36,11 +45,8 @@ import {
|
||||||
import { intersectElementWithLineSegment } from "./collision";
|
import { intersectElementWithLineSegment } from "./collision";
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToBindableElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
compareHeading,
|
|
||||||
HEADING_DOWN,
|
|
||||||
HEADING_RIGHT,
|
|
||||||
HEADING_UP,
|
|
||||||
headingForPointFromElement,
|
headingForPointFromElement,
|
||||||
|
headingIsHorizontal,
|
||||||
vectorToHeading,
|
vectorToHeading,
|
||||||
type Heading,
|
type Heading,
|
||||||
} from "./heading";
|
} from "./heading";
|
||||||
|
@ -60,6 +66,9 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
||||||
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
import type {
|
import type {
|
||||||
|
@ -79,8 +88,6 @@ import type {
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type Scene from "../scene/Scene";
|
|
||||||
import type { AppState } from "../types";
|
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
| NonDeleted<ExcalidrawBindableElement>
|
| NonDeleted<ExcalidrawBindableElement>
|
||||||
|
@ -837,14 +844,19 @@ export const updateBoundElements = (
|
||||||
}> => update !== null,
|
}> => update !== null,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(element, updates, {
|
LinearElementEditor.movePoints(
|
||||||
|
element,
|
||||||
|
updates,
|
||||||
|
{
|
||||||
...(changedElement.id === element.startBinding?.elementId
|
...(changedElement.id === element.startBinding?.elementId
|
||||||
? { startBinding: bindings.startBinding }
|
? { startBinding: bindings.startBinding }
|
||||||
: {}),
|
: {}),
|
||||||
...(changedElement.id === element.endBinding?.elementId
|
...(changedElement.id === element.endBinding?.elementId
|
||||||
? { endBinding: bindings.endBinding }
|
? { endBinding: bindings.endBinding }
|
||||||
: {}),
|
: {}),
|
||||||
});
|
},
|
||||||
|
elementsMap as NonDeletedSceneElementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !boundText.isDeleted) {
|
if (boundText && !boundText.isDeleted) {
|
||||||
|
@ -893,13 +905,7 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
|
|
||||||
if (!distance) {
|
if (!distance) {
|
||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
vectorFromPoint(
|
vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
||||||
p,
|
|
||||||
pointFrom<GlobalPoint>(
|
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -925,113 +931,111 @@ const getDistanceForBinding = (
|
||||||
|
|
||||||
export const bindPointToSnapToElementOutline = (
|
export const bindPointToSnapToElementOutline = (
|
||||||
arrow: ExcalidrawElbowArrowElement,
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
bindableElement: ExcalidrawBindableElement | undefined,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const aabb = bindableElement && aabbForElement(bindableElement);
|
if (isDevEnv() || isTestEnv()) {
|
||||||
|
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
||||||
|
}
|
||||||
|
|
||||||
|
const aabb = aabbForElement(bindableElement);
|
||||||
const localP =
|
const localP =
|
||||||
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
|
||||||
const globalP = pointFrom<GlobalPoint>(
|
const globalP = pointFrom<GlobalPoint>(
|
||||||
arrow.x + localP[0],
|
arrow.x + localP[0],
|
||||||
arrow.y + localP[1],
|
arrow.y + localP[1],
|
||||||
);
|
);
|
||||||
const p = isRectanguloidElement(bindableElement)
|
const edgePoint = isRectanguloidElement(bindableElement)
|
||||||
? avoidRectangularCorner(bindableElement, globalP)
|
? avoidRectangularCorner(bindableElement, globalP)
|
||||||
: globalP;
|
: globalP;
|
||||||
|
const elbowed = isElbowArrow(arrow);
|
||||||
if (bindableElement && aabb) {
|
|
||||||
const center = getCenterForBounds(aabb);
|
const center = getCenterForBounds(aabb);
|
||||||
|
const adjacentPointIdx = startOrEnd === "start" ? 1 : arrow.points.length - 2;
|
||||||
const intersection = intersectElementWithLineSegment(
|
const adjacentPoint = pointRotateRads(
|
||||||
bindableElement,
|
pointFrom<GlobalPoint>(
|
||||||
lineSegment(
|
arrow.x + arrow.points[adjacentPointIdx][0],
|
||||||
center,
|
arrow.y + arrow.points[adjacentPointIdx][1],
|
||||||
pointFromVector(
|
|
||||||
vectorScale(
|
|
||||||
vectorNormalize(vectorFromPoint(p, center)),
|
|
||||||
Math.max(bindableElement.width, bindableElement.height),
|
|
||||||
),
|
),
|
||||||
center,
|
center,
|
||||||
|
arrow.angle ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let intersection: GlobalPoint | null = null;
|
||||||
|
if (elbowed) {
|
||||||
|
const isHorizontal = headingIsHorizontal(
|
||||||
|
headingForPointFromElement(bindableElement, aabb, globalP),
|
||||||
|
);
|
||||||
|
const otherPoint = pointFrom<GlobalPoint>(
|
||||||
|
isHorizontal ? center[0] : edgePoint[0],
|
||||||
|
!isHorizontal ? center[1] : edgePoint[1],
|
||||||
|
);
|
||||||
|
intersection = intersectElementWithLineSegment(
|
||||||
|
bindableElement,
|
||||||
|
lineSegment(
|
||||||
|
otherPoint,
|
||||||
|
pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorNormalize(vectorFromPoint(edgePoint, otherPoint)),
|
||||||
|
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||||
|
),
|
||||||
|
otherPoint,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)[0];
|
)[0];
|
||||||
const currentDistance = pointDistance(p, center);
|
} else {
|
||||||
const fullDistance = Math.max(
|
intersection = intersectElementWithLineSegment(
|
||||||
pointDistance(intersection ?? p, center),
|
bindableElement,
|
||||||
PRECISION,
|
lineSegment(
|
||||||
);
|
adjacentPoint,
|
||||||
const ratio = round(currentDistance / fullDistance, 6);
|
pointFromVector(
|
||||||
|
vectorScale(
|
||||||
switch (true) {
|
vectorNormalize(vectorFromPoint(edgePoint, adjacentPoint)),
|
||||||
case ratio > 0.9:
|
pointDistance(edgePoint, adjacentPoint) +
|
||||||
if (
|
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||||
currentDistance - fullDistance > FIXED_BINDING_DISTANCE ||
|
),
|
||||||
// Too close to determine vector from intersection to p
|
adjacentPoint,
|
||||||
pointDistanceSq(p, intersection) < PRECISION
|
),
|
||||||
) {
|
),
|
||||||
return p;
|
FIXED_BINDING_DISTANCE,
|
||||||
|
).sort(
|
||||||
|
(g, h) =>
|
||||||
|
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
|
||||||
|
)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!intersection ||
|
||||||
|
// Too close to determine vector from intersection to edgePoint
|
||||||
|
pointDistanceSq(edgePoint, intersection) < PRECISION
|
||||||
|
) {
|
||||||
|
return edgePoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elbowed) {
|
||||||
|
const scalar =
|
||||||
|
pointDistanceSq(edgePoint, center) -
|
||||||
|
pointDistanceSq(intersection, center) >
|
||||||
|
0
|
||||||
|
? FIXED_BINDING_DISTANCE
|
||||||
|
: -FIXED_BINDING_DISTANCE;
|
||||||
|
|
||||||
return pointFromVector(
|
return pointFromVector(
|
||||||
vectorScale(
|
vectorScale(
|
||||||
vectorNormalize(vectorFromPoint(p, intersection ?? center)),
|
vectorNormalize(vectorFromPoint(edgePoint, intersection)),
|
||||||
ratio > 1 ? FIXED_BINDING_DISTANCE : -FIXED_BINDING_DISTANCE,
|
scalar,
|
||||||
),
|
),
|
||||||
intersection ?? center,
|
intersection,
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return headingToMidBindPoint(p, bindableElement, aabb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p;
|
|
||||||
};
|
|
||||||
|
|
||||||
const headingToMidBindPoint = (
|
|
||||||
p: GlobalPoint,
|
|
||||||
bindableElement: ExcalidrawBindableElement,
|
|
||||||
aabb: Bounds,
|
|
||||||
): GlobalPoint => {
|
|
||||||
const center = getCenterForBounds(aabb);
|
|
||||||
const heading = vectorToHeading(vectorFromPoint(p, center));
|
|
||||||
|
|
||||||
switch (true) {
|
|
||||||
case compareHeading(heading, HEADING_UP):
|
|
||||||
return pointRotateRads(
|
|
||||||
pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
|
|
||||||
center,
|
|
||||||
bindableElement.angle,
|
|
||||||
);
|
|
||||||
case compareHeading(heading, HEADING_RIGHT):
|
|
||||||
return pointRotateRads(
|
|
||||||
pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
|
|
||||||
center,
|
|
||||||
bindableElement.angle,
|
|
||||||
);
|
|
||||||
case compareHeading(heading, HEADING_DOWN):
|
|
||||||
return pointRotateRads(
|
|
||||||
pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
|
|
||||||
center,
|
|
||||||
bindableElement.angle,
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return pointRotateRads(
|
|
||||||
pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
|
|
||||||
center,
|
|
||||||
bindableElement.angle,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return edgePoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const avoidRectangularCorner = (
|
export const avoidRectangularCorner = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||||
|
@ -1128,10 +1132,9 @@ export const snapToMid = (
|
||||||
tolerance: number = 0.05,
|
tolerance: number = 0.05,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const center = pointFrom<GlobalPoint>(
|
|
||||||
x + width / 2 - 0.1,
|
const center = elementCenterPoint(element, -0.1, -0.1);
|
||||||
y + height / 2 - 0.1,
|
|
||||||
);
|
|
||||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||||
|
|
||||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||||
|
@ -1216,10 +1219,7 @@ const updateBoundPoint = (
|
||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
const globalMidPoint = elementCenterPoint(bindableElement);
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
);
|
|
||||||
const global = pointFrom<GlobalPoint>(
|
const global = pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||||
|
@ -1263,10 +1263,7 @@ const updateBoundPoint = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(bindableElement);
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
);
|
|
||||||
const interceptorLength =
|
const interceptorLength =
|
||||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||||
pointDistance(adjacentPoint, center) +
|
pointDistance(adjacentPoint, center) +
|
||||||
|
@ -1411,107 +1408,265 @@ const getLinearElementEdgeCoors = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// We need to:
|
export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
// 1: Update elements not selected to point to duplicated elements
|
newElements: ExcalidrawElement[],
|
||||||
// 2: Update duplicated elements to point to other duplicated elements
|
|
||||||
export const fixBindingsAfterDuplication = (
|
|
||||||
sceneElements: readonly ExcalidrawElement[],
|
|
||||||
oldElements: readonly ExcalidrawElement[],
|
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
// There are three copying mechanisms: Copy-paste, duplication and alt-drag.
|
duplicatedElementsMap: NonDeletedSceneElementsMap,
|
||||||
// Only when alt-dragging the new "duplicates" act as the "old", while
|
) => {
|
||||||
// the "old" elements act as the "new copy" - essentially working reverse
|
for (const element of newElements) {
|
||||||
// to the other two.
|
if ("boundElements" in element && element.boundElements) {
|
||||||
duplicatesServeAsOld?: "duplicatesServeAsOld" | undefined,
|
Object.assign(element, {
|
||||||
): void => {
|
boundElements: element.boundElements.reduce(
|
||||||
// First collect all the binding/bindable elements, so we only update
|
|
||||||
// each once, regardless of whether they were duplicated or not.
|
|
||||||
const allBoundElementIds: Set<ExcalidrawElement["id"]> = new Set();
|
|
||||||
const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set();
|
|
||||||
const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld";
|
|
||||||
const duplicateIdToOldId = new Map(
|
|
||||||
[...oldIdToDuplicatedId].map(([key, value]) => [value, key]),
|
|
||||||
);
|
|
||||||
oldElements.forEach((oldElement) => {
|
|
||||||
const { boundElements } = oldElement;
|
|
||||||
if (boundElements != null && boundElements.length > 0) {
|
|
||||||
boundElements.forEach((boundElement) => {
|
|
||||||
if (shouldReverseRoles && !oldIdToDuplicatedId.has(boundElement.id)) {
|
|
||||||
allBoundElementIds.add(boundElement.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
allBindableElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!);
|
|
||||||
}
|
|
||||||
if (isBindingElement(oldElement)) {
|
|
||||||
if (oldElement.startBinding != null) {
|
|
||||||
const { elementId } = oldElement.startBinding;
|
|
||||||
if (shouldReverseRoles && !oldIdToDuplicatedId.has(elementId)) {
|
|
||||||
allBindableElementIds.add(elementId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldElement.endBinding != null) {
|
|
||||||
const { elementId } = oldElement.endBinding;
|
|
||||||
if (shouldReverseRoles && !oldIdToDuplicatedId.has(elementId)) {
|
|
||||||
allBindableElementIds.add(elementId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldElement.startBinding != null || oldElement.endBinding != null) {
|
|
||||||
allBoundElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the linear elements
|
|
||||||
(
|
(
|
||||||
sceneElements.filter(({ id }) =>
|
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
||||||
allBoundElementIds.has(id),
|
binding,
|
||||||
) as ExcalidrawLinearElement[]
|
) => {
|
||||||
).forEach((element) => {
|
const newBindingId = oldIdToDuplicatedId.get(binding.id);
|
||||||
const { startBinding, endBinding } = element;
|
if (newBindingId) {
|
||||||
mutateElement(element, {
|
acc.push({ ...binding, id: newBindingId });
|
||||||
startBinding: newBindingAfterDuplication(
|
|
||||||
startBinding,
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
),
|
|
||||||
endBinding: newBindingAfterDuplication(endBinding, oldIdToDuplicatedId),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the bindable shapes
|
|
||||||
sceneElements
|
|
||||||
.filter(({ id }) => allBindableElementIds.has(id))
|
|
||||||
.forEach((bindableElement) => {
|
|
||||||
const oldElementId = duplicateIdToOldId.get(bindableElement.id);
|
|
||||||
const boundElements = sceneElements.find(
|
|
||||||
({ id }) => id === oldElementId,
|
|
||||||
)?.boundElements;
|
|
||||||
|
|
||||||
if (boundElements && boundElements.length > 0) {
|
|
||||||
mutateElement(bindableElement, {
|
|
||||||
boundElements: boundElements.map((boundElement) =>
|
|
||||||
oldIdToDuplicatedId.has(boundElement.id)
|
|
||||||
? {
|
|
||||||
id: oldIdToDuplicatedId.get(boundElement.id)!,
|
|
||||||
type: boundElement.type,
|
|
||||||
}
|
}
|
||||||
: boundElement,
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("containerId" in element && element.containerId) {
|
||||||
|
Object.assign(element, {
|
||||||
|
containerId: oldIdToDuplicatedId.get(element.containerId) ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("endBinding" in element && element.endBinding) {
|
||||||
|
const newEndBindingId = oldIdToDuplicatedId.get(
|
||||||
|
element.endBinding.elementId,
|
||||||
|
);
|
||||||
|
Object.assign(element, {
|
||||||
|
endBinding: newEndBindingId
|
||||||
|
? {
|
||||||
|
...element.endBinding,
|
||||||
|
elementId: newEndBindingId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ("startBinding" in element && element.startBinding) {
|
||||||
|
const newEndBindingId = oldIdToDuplicatedId.get(
|
||||||
|
element.startBinding.elementId,
|
||||||
|
);
|
||||||
|
Object.assign(element, {
|
||||||
|
startBinding: newEndBindingId
|
||||||
|
? {
|
||||||
|
...element.startBinding,
|
||||||
|
elementId: newEndBindingId,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isElbowArrow(element)) {
|
||||||
|
Object.assign(
|
||||||
|
element,
|
||||||
|
updateElbowArrowPoints(element, duplicatedElementsMap, {
|
||||||
|
points: [
|
||||||
|
element.points[0],
|
||||||
|
element.points[element.points.length - 1],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fixReversedBindingsForBindables = (
|
||||||
|
original: ExcalidrawBindableElement,
|
||||||
|
duplicate: ExcalidrawBindableElement,
|
||||||
|
originalElements: Map<string, ExcalidrawElement>,
|
||||||
|
elementsWithClones: ExcalidrawElement[],
|
||||||
|
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
|
) => {
|
||||||
|
original.boundElements?.forEach((binding, idx) => {
|
||||||
|
if (binding.type !== "arrow") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldArrow = elementsWithClones.find((el) => el.id === binding.id);
|
||||||
|
|
||||||
|
if (!isBindingElement(oldArrow)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalElements.has(binding.id)) {
|
||||||
|
// Linked arrow is in the selection, so find the duplicate pair
|
||||||
|
const newArrowId = oldIdToDuplicatedId.get(binding.id) ?? binding.id;
|
||||||
|
const newArrow = elementsWithClones.find(
|
||||||
|
(el) => el.id === newArrowId,
|
||||||
|
)! as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
mutateElement(newArrow, {
|
||||||
|
startBinding:
|
||||||
|
oldArrow.startBinding?.elementId === binding.id
|
||||||
|
? {
|
||||||
|
...oldArrow.startBinding,
|
||||||
|
elementId: duplicate.id,
|
||||||
|
}
|
||||||
|
: newArrow.startBinding,
|
||||||
|
endBinding:
|
||||||
|
oldArrow.endBinding?.elementId === binding.id
|
||||||
|
? {
|
||||||
|
...oldArrow.endBinding,
|
||||||
|
elementId: duplicate.id,
|
||||||
|
}
|
||||||
|
: newArrow.endBinding,
|
||||||
|
});
|
||||||
|
mutateElement(duplicate, {
|
||||||
|
boundElements: [
|
||||||
|
...(duplicate.boundElements ?? []).filter(
|
||||||
|
(el) => el.id !== binding.id && el.id !== newArrowId,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
id: newArrowId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Linked arrow is outside the selection,
|
||||||
|
// so we move the binding to the duplicate
|
||||||
|
mutateElement(oldArrow, {
|
||||||
|
startBinding:
|
||||||
|
oldArrow.startBinding?.elementId === original.id
|
||||||
|
? {
|
||||||
|
...oldArrow.startBinding,
|
||||||
|
elementId: duplicate.id,
|
||||||
|
}
|
||||||
|
: oldArrow.startBinding,
|
||||||
|
endBinding:
|
||||||
|
oldArrow.endBinding?.elementId === original.id
|
||||||
|
? {
|
||||||
|
...oldArrow.endBinding,
|
||||||
|
elementId: duplicate.id,
|
||||||
|
}
|
||||||
|
: oldArrow.endBinding,
|
||||||
|
});
|
||||||
|
mutateElement(duplicate, {
|
||||||
|
boundElements: [
|
||||||
|
...(duplicate.boundElements ?? []),
|
||||||
|
{
|
||||||
|
type: "arrow",
|
||||||
|
id: oldArrow.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
mutateElement(original, {
|
||||||
|
boundElements:
|
||||||
|
original.boundElements?.filter((_, i) => i !== idx) ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const newBindingAfterDuplication = (
|
const fixReversedBindingsForArrows = (
|
||||||
binding: PointBinding | null,
|
original: ExcalidrawArrowElement,
|
||||||
|
duplicate: ExcalidrawArrowElement,
|
||||||
|
originalElements: Map<string, ExcalidrawElement>,
|
||||||
|
bindingProp: "startBinding" | "endBinding",
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
): PointBinding | null => {
|
elementsWithClones: ExcalidrawElement[],
|
||||||
if (binding == null) {
|
) => {
|
||||||
return null;
|
const oldBindableId = original[bindingProp]?.elementId;
|
||||||
|
|
||||||
|
if (oldBindableId) {
|
||||||
|
if (originalElements.has(oldBindableId)) {
|
||||||
|
// Linked element is in the selection
|
||||||
|
const newBindableId =
|
||||||
|
oldIdToDuplicatedId.get(oldBindableId) ?? oldBindableId;
|
||||||
|
const newBindable = elementsWithClones.find(
|
||||||
|
(el) => el.id === newBindableId,
|
||||||
|
) as ExcalidrawBindableElement;
|
||||||
|
mutateElement(duplicate, {
|
||||||
|
[bindingProp]: {
|
||||||
|
...original[bindingProp],
|
||||||
|
elementId: newBindableId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mutateElement(newBindable, {
|
||||||
|
boundElements: [
|
||||||
|
...(newBindable.boundElements ?? []).filter(
|
||||||
|
(el) => el.id !== original.id && el.id !== duplicate.id,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: duplicate.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Linked element is outside the selection
|
||||||
|
const originalBindable = elementsWithClones.find(
|
||||||
|
(el) => el.id === oldBindableId,
|
||||||
|
);
|
||||||
|
if (originalBindable) {
|
||||||
|
mutateElement(duplicate, {
|
||||||
|
[bindingProp]: original[bindingProp],
|
||||||
|
});
|
||||||
|
mutateElement(original, {
|
||||||
|
[bindingProp]: null,
|
||||||
|
});
|
||||||
|
mutateElement(originalBindable, {
|
||||||
|
boundElements: [
|
||||||
|
...(originalBindable.boundElements?.filter(
|
||||||
|
(el) => el.id !== original.id,
|
||||||
|
) ?? []),
|
||||||
|
{
|
||||||
|
id: duplicate.id,
|
||||||
|
type: "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fixReversedBindings = (
|
||||||
|
originalElements: Map<string, ExcalidrawElement>,
|
||||||
|
elementsWithClones: ExcalidrawElement[],
|
||||||
|
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
|
) => {
|
||||||
|
for (const original of originalElements.values()) {
|
||||||
|
const duplicate = elementsWithClones.find(
|
||||||
|
(el) => el.id === oldIdToDuplicatedId.get(original.id),
|
||||||
|
)!;
|
||||||
|
|
||||||
|
if (isBindableElement(original) && isBindableElement(duplicate)) {
|
||||||
|
fixReversedBindingsForBindables(
|
||||||
|
original,
|
||||||
|
duplicate,
|
||||||
|
originalElements,
|
||||||
|
elementsWithClones,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
} else if (isArrowElement(original) && isArrowElement(duplicate)) {
|
||||||
|
fixReversedBindingsForArrows(
|
||||||
|
original,
|
||||||
|
duplicate,
|
||||||
|
originalElements,
|
||||||
|
"startBinding",
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
elementsWithClones,
|
||||||
|
);
|
||||||
|
fixReversedBindingsForArrows(
|
||||||
|
original,
|
||||||
|
duplicate,
|
||||||
|
originalElements,
|
||||||
|
"endBinding",
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
elementsWithClones,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
...binding,
|
|
||||||
elementId: oldIdToDuplicatedId.get(binding.elementId) ?? binding.elementId,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fixBindingsAfterDeletion = (
|
export const fixBindingsAfterDeletion = (
|
||||||
|
@ -1601,10 +1756,7 @@ const determineFocusDistance = (
|
||||||
// Another point on the line, in absolute coordinates (closer to element)
|
// Another point on the line, in absolute coordinates (closer to element)
|
||||||
b: GlobalPoint,
|
b: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pointsEqual(a, b)) {
|
if (pointsEqual(a, b)) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -1734,10 +1886,7 @@ const determineFocusPoint = (
|
||||||
focus: number,
|
focus: number,
|
||||||
adjacentPoint: GlobalPoint,
|
adjacentPoint: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (focus === 0) {
|
if (focus === 0) {
|
||||||
return center;
|
return center;
|
||||||
|
@ -2168,10 +2317,7 @@ export const getGlobalFixedPointForBindableElement = (
|
||||||
element.x + element.width * fixedX,
|
element.x + element.width * fixedX,
|
||||||
element.y + element.height * fixedY,
|
element.y + element.height * fixedY,
|
||||||
),
|
),
|
||||||
pointFrom<GlobalPoint>(
|
elementCenterPoint(element),
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
),
|
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,3 +1,7 @@
|
||||||
|
import rough from "roughjs/bin/rough";
|
||||||
|
|
||||||
|
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
|
@ -6,10 +10,13 @@ import {
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/geometry/shape";
|
|
||||||
import rough from "roughjs/bin/rough";
|
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
|
import { pointsOnBezierCurves } from "points-on-curve";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
Curve,
|
||||||
Degrees,
|
Degrees,
|
||||||
GlobalPoint,
|
GlobalPoint,
|
||||||
LineSegment,
|
LineSegment,
|
||||||
|
@ -17,11 +24,12 @@ import type {
|
||||||
Radians,
|
Radians,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { rescalePoints } from "../points";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
import { generateRoughOptions } from "../scene/Shape";
|
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
|
||||||
import { arrayToMap, invariant } from "../utils";
|
|
||||||
|
|
||||||
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { ShapeCache } from "./ShapeCache";
|
||||||
|
import { generateRoughOptions } from "./Shape";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import {
|
import {
|
||||||
|
@ -32,8 +40,13 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import { getElementShape } from "./shapes";
|
||||||
import type { Mutable } from "../utility-types";
|
|
||||||
|
import {
|
||||||
|
deconstructDiamondElement,
|
||||||
|
deconstructRectanguloidElement,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
@ -42,6 +55,8 @@ import type {
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
@ -251,50 +266,82 @@ export const getElementAbsoluteCoords = (
|
||||||
* that can be used for visual collision detection (useful for frames)
|
* that can be used for visual collision detection (useful for frames)
|
||||||
* as opposed to bounding box collision detection
|
* as opposed to bounding box collision detection
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Given an element, return the line segments that make up the element.
|
||||||
|
*
|
||||||
|
* Uses helpers from /math
|
||||||
|
*/
|
||||||
export const getElementLineSegments = (
|
export const getElementLineSegments = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): LineSegment<GlobalPoint>[] => {
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const shape = getElementShape(element, elementsMap);
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
const center = pointFrom<GlobalPoint>(cx, cy);
|
||||||
|
|
||||||
const center: GlobalPoint = pointFrom(cx, cy);
|
if (shape.type === "polycurve") {
|
||||||
|
const curves = shape.data;
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
const points = curves
|
||||||
const segments: LineSegment<GlobalPoint>[] = [];
|
.map((curve) => pointsOnBezierCurves(curve, 10))
|
||||||
|
.flat();
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
while (i < element.points.length - 1) {
|
while (i < points.length - 1) {
|
||||||
segments.push(
|
segments.push(
|
||||||
lineSegment(
|
lineSegment(
|
||||||
pointRotateRads(
|
pointFrom(points[i][0], points[i][1]),
|
||||||
pointFrom(
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||||
element.points[i][0] + element.x,
|
|
||||||
element.points[i][1] + element.y,
|
|
||||||
),
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
pointRotateRads(
|
|
||||||
pointFrom(
|
|
||||||
element.points[i + 1][0] + element.x,
|
|
||||||
element.points[i + 1][1] + element.y,
|
|
||||||
),
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
|
} else if (shape.type === "polyline") {
|
||||||
|
return shape.data as LineSegment<GlobalPoint>[];
|
||||||
|
} else if (_isRectanguloidElement(element)) {
|
||||||
|
const [sides, corners] = deconstructRectanguloidElement(element);
|
||||||
|
const cornerSegments: LineSegment<GlobalPoint>[] = corners
|
||||||
|
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||||
|
.flat();
|
||||||
|
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||||
|
return [...rotatedSides, ...cornerSegments];
|
||||||
|
} else if (element.type === "diamond") {
|
||||||
|
const [sides, corners] = deconstructDiamondElement(element);
|
||||||
|
const cornerSegments = corners
|
||||||
|
.map((corner) => getSegmentsOnCurve(corner, center, element.angle))
|
||||||
|
.flat();
|
||||||
|
const rotatedSides = getRotatedSides(sides, center, element.angle);
|
||||||
|
|
||||||
|
return [...rotatedSides, ...cornerSegments];
|
||||||
|
} else if (shape.type === "polygon") {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
const container = getContainerElement(element, elementsMap);
|
||||||
|
if (container && isLinearElement(container)) {
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [
|
||||||
|
lineSegment(pointFrom(x1, y1), pointFrom(x2, y1)),
|
||||||
|
lineSegment(pointFrom(x2, y1), pointFrom(x2, y2)),
|
||||||
|
lineSegment(pointFrom(x2, y2), pointFrom(x1, y2)),
|
||||||
|
lineSegment(pointFrom(x1, y2), pointFrom(x1, y1)),
|
||||||
|
];
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [nw, ne, sw, se, n, s, w, e] = (
|
const points = shape.data as GlobalPoint[];
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
segments.push(lineSegment(points[i], points[i + 1]));
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
} else if (shape.type === "ellipse") {
|
||||||
|
return getSegmentsOnEllipse(element as ExcalidrawEllipseElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [nw, ne, sw, se, , , w, e] = (
|
||||||
[
|
[
|
||||||
[x1, y1],
|
[x1, y1],
|
||||||
[x2, y1],
|
[x2, y1],
|
||||||
|
@ -307,28 +354,6 @@ export const getElementLineSegments = (
|
||||||
] as GlobalPoint[]
|
] as GlobalPoint[]
|
||||||
).map((point) => pointRotateRads(point, center, element.angle));
|
).map((point) => pointRotateRads(point, center, element.angle));
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
|
||||||
return [
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.type === "ellipse") {
|
|
||||||
return [
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
lineSegment(nw, ne),
|
lineSegment(nw, ne),
|
||||||
lineSegment(sw, se),
|
lineSegment(sw, se),
|
||||||
|
@ -341,6 +366,94 @@ export const getElementLineSegments = (
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _isRectanguloidElement = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawRectanguloidElement => {
|
||||||
|
return (
|
||||||
|
element != null &&
|
||||||
|
(element.type === "rectangle" ||
|
||||||
|
element.type === "image" ||
|
||||||
|
element.type === "iframe" ||
|
||||||
|
element.type === "embeddable" ||
|
||||||
|
element.type === "frame" ||
|
||||||
|
element.type === "magicframe" ||
|
||||||
|
(element.type === "text" && !element.containerId))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRotatedSides = (
|
||||||
|
sides: LineSegment<GlobalPoint>[],
|
||||||
|
center: GlobalPoint,
|
||||||
|
angle: Radians,
|
||||||
|
) => {
|
||||||
|
return sides.map((side) => {
|
||||||
|
return lineSegment(
|
||||||
|
pointRotateRads<GlobalPoint>(side[0], center, angle),
|
||||||
|
pointRotateRads<GlobalPoint>(side[1], center, angle),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSegmentsOnCurve = (
|
||||||
|
curve: Curve<GlobalPoint>,
|
||||||
|
center: GlobalPoint,
|
||||||
|
angle: Radians,
|
||||||
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const points = pointsOnBezierCurves(curve, 10);
|
||||||
|
let i = 0;
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
while (i < points.length - 1) {
|
||||||
|
segments.push(
|
||||||
|
lineSegment(
|
||||||
|
pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(points[i][0], points[i][1]),
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
),
|
||||||
|
pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom(points[i + 1][0], points[i + 1][1]),
|
||||||
|
center,
|
||||||
|
angle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSegmentsOnEllipse = (
|
||||||
|
ellipse: ExcalidrawEllipseElement,
|
||||||
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const center = pointFrom<GlobalPoint>(
|
||||||
|
ellipse.x + ellipse.width / 2,
|
||||||
|
ellipse.y + ellipse.height / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
const a = ellipse.width / 2;
|
||||||
|
const b = ellipse.height / 2;
|
||||||
|
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
const points: GlobalPoint[] = [];
|
||||||
|
const n = 90;
|
||||||
|
const deltaT = (Math.PI * 2) / n;
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const t = i * deltaT;
|
||||||
|
const x = center[0] + a * Math.cos(t);
|
||||||
|
const y = center[1] + b * Math.sin(t);
|
||||||
|
points.push(pointRotateRads(pointFrom(x, y), center, ellipse.angle));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
segments.push(lineSegment(points[i], points[i + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push(lineSegment(points[points.length - 1], points[0]));
|
||||||
|
return segments;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
||||||
*
|
*
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
|
@ -8,12 +9,14 @@ import {
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointsEqual,
|
pointsEqual,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ellipse,
|
ellipse,
|
||||||
ellipseLineIntersectionPoints,
|
ellipseLineIntersectionPoints,
|
||||||
} from "@excalidraw/math/ellipse";
|
} from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
import { getPolygonShape } from "@excalidraw/utils/geometry/shape";
|
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GlobalPoint,
|
GlobalPoint,
|
||||||
|
@ -22,11 +25,10 @@ import type {
|
||||||
Polygon,
|
Polygon,
|
||||||
Radians,
|
Radians,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import type { GeometricShape } from "@excalidraw/utils/geometry/shape";
|
|
||||||
|
|
||||||
import { getBoundTextShape, isPathALoop } from "../shapes";
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
import { isTransparent } from "../utils";
|
|
||||||
|
|
||||||
|
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||||
import { getElementBounds } from "./bounds";
|
import { getElementBounds } from "./bounds";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
|
@ -47,7 +49,6 @@ import type {
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { FrameNameBounds } from "../types";
|
|
||||||
|
|
||||||
export const shouldTestInside = (element: ExcalidrawElement) => {
|
export const shouldTestInside = (element: ExcalidrawElement) => {
|
||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
|
@ -188,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||||
|
@ -209,10 +207,9 @@ const intersectRectanguloidWithLineSegment = (
|
||||||
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[
|
|
||||||
// Test intersection against the sides, keep only the valid
|
// Test intersection against the sides, keep only the valid
|
||||||
// intersection points and rotate them back to scene space
|
// intersection points and rotate them back to scene space
|
||||||
...sides
|
sides
|
||||||
.map((s) =>
|
.map((s) =>
|
||||||
lineSegmentIntersectionPoints(
|
lineSegmentIntersectionPoints(
|
||||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||||
|
@ -220,17 +217,18 @@ const intersectRectanguloidWithLineSegment = (
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.filter((x) => x != null)
|
.filter((x) => x != null)
|
||||||
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)),
|
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle))
|
||||||
// Test intersection against the corners which are cubic bezier curves,
|
// Test intersection against the corners which are cubic bezier curves,
|
||||||
// keep only the valid intersection points and rotate them back to scene
|
// keep only the valid intersection points and rotate them back to scene
|
||||||
// space
|
// space
|
||||||
...corners
|
.concat(
|
||||||
|
corners
|
||||||
.flatMap((t) =>
|
.flatMap((t) =>
|
||||||
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
||||||
)
|
)
|
||||||
.filter((i) => i != null)
|
.filter((i) => i != null)
|
||||||
.map((j) => pointRotateRads(j, center, element.angle)),
|
.map((j) => pointRotateRads(j, center, element.angle)),
|
||||||
]
|
)
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
.filter(
|
.filter(
|
||||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
||||||
|
@ -250,10 +248,7 @@ const intersectDiamondWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
|
@ -263,8 +258,7 @@ const intersectDiamondWithLineSegment = (
|
||||||
const [sides, curves] = deconstructDiamondElement(element, offset);
|
const [sides, curves] = deconstructDiamondElement(element, offset);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[
|
sides
|
||||||
...sides
|
|
||||||
.map((s) =>
|
.map((s) =>
|
||||||
lineSegmentIntersectionPoints(
|
lineSegmentIntersectionPoints(
|
||||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
||||||
|
@ -273,15 +267,16 @@ const intersectDiamondWithLineSegment = (
|
||||||
)
|
)
|
||||||
.filter((p): p is GlobalPoint => p != null)
|
.filter((p): p is GlobalPoint => p != null)
|
||||||
// Rotate back intersection points
|
// Rotate back intersection points
|
||||||
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)),
|
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle))
|
||||||
...curves
|
.concat(
|
||||||
|
curves
|
||||||
.flatMap((p) =>
|
.flatMap((p) =>
|
||||||
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
||||||
)
|
)
|
||||||
.filter((p) => p != null)
|
.filter((p) => p != null)
|
||||||
// Rotate back intersection points
|
// Rotate back intersection points
|
||||||
.map((p) => pointRotateRads(p, center, element.angle)),
|
.map((p) => pointRotateRads(p, center, element.angle)),
|
||||||
]
|
)
|
||||||
// Remove duplicates
|
// Remove duplicates
|
||||||
.filter(
|
.filter(
|
||||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
||||||
|
@ -301,10 +296,7 @@ const intersectEllipseWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ElementOrToolType } from "../types";
|
import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
export const hasBackground = (type: ElementOrToolType) =>
|
export const hasBackground = (type: ElementOrToolType) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
|
@ -14,6 +14,8 @@ import {
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { type Point } from "points-on-curve";
|
import { type Point } from "points-on-curve";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
|
@ -61,7 +63,7 @@ export const cropElement = (
|
||||||
|
|
||||||
const rotatedPointer = pointRotateRads(
|
const rotatedPointer = pointRotateRads(
|
||||||
pointFrom(pointerX, pointerY),
|
pointFrom(pointerX, pointerY),
|
||||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
elementCenterPoint(element),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import {
|
import {
|
||||||
curvePointDistance,
|
curvePointDistance,
|
||||||
distanceToLineSegment,
|
distanceToLineSegment,
|
||||||
pointFrom,
|
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -52,10 +54,7 @@ const distanceToRectanguloidElement = (
|
||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
) => {
|
) => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
@ -83,10 +82,7 @@ const distanceToDiamondElement = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
|
@ -114,10 +110,7 @@ const distanceToEllipseElement = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
return ellipseDistanceFromPoint(
|
return ellipseDistanceFromPoint(
|
||||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||||
pointRotateRads(p, center, -element.angle as Radians),
|
pointRotateRads(p, center, -element.angle as Radians),
|
|
@ -1,8 +1,9 @@
|
||||||
import { getCommonBoundingBox } from "./element/bounds";
|
import { getCommonBoundingBox } from "./bounds";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./mutateElement";
|
||||||
|
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
export interface Distribution {
|
export interface Distribution {
|
||||||
space: "between";
|
space: "between";
|
|
@ -1,6 +1,19 @@
|
||||||
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
|
import {
|
||||||
import { getGridPoint } from "../snapping";
|
TEXT_AUTOWRAP_THRESHOLD,
|
||||||
import { getFontString } from "../utils";
|
getGridPoint,
|
||||||
|
getFontString,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
NormalizedZoomValue,
|
||||||
|
NullableGridSize,
|
||||||
|
PointerDownState,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
|
|
||||||
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
|
@ -17,14 +30,7 @@ import {
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { ExcalidrawElement, NonDeletedExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
import type Scene from "../scene/Scene";
|
|
||||||
import type {
|
|
||||||
AppState,
|
|
||||||
NormalizedZoomValue,
|
|
||||||
NullableGridSize,
|
|
||||||
PointerDownState,
|
|
||||||
} from "../types";
|
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
496
packages/element/src/duplicate.ts
Normal file
496
packages/element/src/duplicate.ts
Normal file
|
@ -0,0 +1,496 @@
|
||||||
|
import {
|
||||||
|
ORIG_ID,
|
||||||
|
randomId,
|
||||||
|
randomInteger,
|
||||||
|
arrayToMap,
|
||||||
|
castArray,
|
||||||
|
findLastIndex,
|
||||||
|
getUpdatedTimestamp,
|
||||||
|
isTestEnv,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getElementsInGroup,
|
||||||
|
getNewGroupIdsForDuplication,
|
||||||
|
getSelectedGroupForElement,
|
||||||
|
} from "./groups";
|
||||||
|
|
||||||
|
import {
|
||||||
|
bindElementsToFramesAfterDuplication,
|
||||||
|
getFrameChildren,
|
||||||
|
} from "./frame";
|
||||||
|
|
||||||
|
import { normalizeElementOrder } from "./sortElements";
|
||||||
|
|
||||||
|
import { bumpVersion } from "./mutateElement";
|
||||||
|
|
||||||
|
import {
|
||||||
|
hasBoundTextElement,
|
||||||
|
isBoundToContainer,
|
||||||
|
isFrameLikeElement,
|
||||||
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
|
|
||||||
|
import {
|
||||||
|
fixDuplicatedBindingsAfterDuplication,
|
||||||
|
fixReversedBindings,
|
||||||
|
} from "./binding";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElement,
|
||||||
|
GroupId,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate an element, often used in the alt-drag operation.
|
||||||
|
* Note that this method has gotten a bit complicated since the
|
||||||
|
* introduction of gruoping/ungrouping elements.
|
||||||
|
* @param editingGroupId The current group being edited. The new
|
||||||
|
* element will inherit this group and its
|
||||||
|
* parents.
|
||||||
|
* @param groupIdMapForOperation A Map that maps old group IDs to
|
||||||
|
* duplicated ones. If you are duplicating
|
||||||
|
* multiple elements at once, share this map
|
||||||
|
* amongst all of them
|
||||||
|
* @param element Element to duplicate
|
||||||
|
* @param overrides Any element properties to override
|
||||||
|
*/
|
||||||
|
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||||
|
editingGroupId: AppState["editingGroupId"],
|
||||||
|
groupIdMapForOperation: Map<GroupId, GroupId>,
|
||||||
|
element: TElement,
|
||||||
|
overrides?: Partial<TElement>,
|
||||||
|
randomizeSeed?: boolean,
|
||||||
|
): Readonly<TElement> => {
|
||||||
|
let copy = deepCopyElement(element);
|
||||||
|
|
||||||
|
if (isTestEnv()) {
|
||||||
|
__test__defineOrigId(copy, element.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.id = randomId();
|
||||||
|
copy.updated = getUpdatedTimestamp();
|
||||||
|
if (randomizeSeed) {
|
||||||
|
copy.seed = randomInteger();
|
||||||
|
bumpVersion(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.groupIds = getNewGroupIdsForDuplication(
|
||||||
|
copy.groupIds,
|
||||||
|
editingGroupId,
|
||||||
|
(groupId) => {
|
||||||
|
if (!groupIdMapForOperation.has(groupId)) {
|
||||||
|
groupIdMapForOperation.set(groupId, randomId());
|
||||||
|
}
|
||||||
|
return groupIdMapForOperation.get(groupId)!;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (overrides) {
|
||||||
|
copy = Object.assign(copy, overrides);
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const duplicateElements = (
|
||||||
|
opts: {
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
|
randomizeSeed?: boolean;
|
||||||
|
overrides?: (
|
||||||
|
originalElement: ExcalidrawElement,
|
||||||
|
) => Partial<ExcalidrawElement>;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* Duplicates all elements in array.
|
||||||
|
*
|
||||||
|
* Use this when programmaticaly duplicating elements, without direct
|
||||||
|
* user interaction.
|
||||||
|
*/
|
||||||
|
type: "everything";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* Duplicates specified elements and inserts them back into the array
|
||||||
|
* in specified order.
|
||||||
|
*
|
||||||
|
* Use this when duplicating Scene elements, during user interaction
|
||||||
|
* such as alt-drag or on duplicate action.
|
||||||
|
*/
|
||||||
|
type: "in-place";
|
||||||
|
idsOfElementsToDuplicate: Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement
|
||||||
|
>;
|
||||||
|
appState: {
|
||||||
|
editingGroupId: AppState["editingGroupId"];
|
||||||
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* If true, duplicated elements are inserted _before_ specified
|
||||||
|
* elements. Case: alt-dragging elements to duplicate them.
|
||||||
|
*
|
||||||
|
* TODO: remove this once (if) we stop replacing the original element
|
||||||
|
* with the duplicated one in the scene array.
|
||||||
|
*/
|
||||||
|
reverseOrder: boolean;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
) => {
|
||||||
|
let { elements } = opts;
|
||||||
|
|
||||||
|
const appState =
|
||||||
|
"appState" in opts
|
||||||
|
? opts.appState
|
||||||
|
: ({
|
||||||
|
editingGroupId: null,
|
||||||
|
selectedGroupIds: {},
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
const reverseOrder = opts.type === "in-place" ? opts.reverseOrder : false;
|
||||||
|
|
||||||
|
// Ids of elements that have already been processed so we don't push them
|
||||||
|
// into the array twice if we end up backtracking when retrieving
|
||||||
|
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||||
|
// cases such as a group containing deleted elements which were not selected).
|
||||||
|
//
|
||||||
|
// This is not enough to prevent duplicates, so we do a second loop afterwards
|
||||||
|
// to remove them.
|
||||||
|
//
|
||||||
|
// For convenience we mark even the newly created ones even though we don't
|
||||||
|
// loop over them.
|
||||||
|
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||||
|
const groupIdMap = new Map();
|
||||||
|
const newElements: ExcalidrawElement[] = [];
|
||||||
|
const oldElements: ExcalidrawElement[] = [];
|
||||||
|
const oldIdToDuplicatedId = new Map();
|
||||||
|
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
||||||
|
const elementsMap = arrayToMap(elements) as ElementsMap;
|
||||||
|
const _idsOfElementsToDuplicate =
|
||||||
|
opts.type === "in-place"
|
||||||
|
? opts.idsOfElementsToDuplicate
|
||||||
|
: new Map(elements.map((el) => [el.id, el]));
|
||||||
|
|
||||||
|
// For sanity
|
||||||
|
if (opts.type === "in-place") {
|
||||||
|
for (const groupId of Object.keys(opts.appState.selectedGroupIds)) {
|
||||||
|
elements
|
||||||
|
.filter((el) => el.groupIds?.includes(groupId))
|
||||||
|
.forEach((el) => _idsOfElementsToDuplicate.set(el.id, el));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elements = normalizeElementOrder(elements);
|
||||||
|
|
||||||
|
const elementsWithClones: ExcalidrawElement[] = elements.slice();
|
||||||
|
|
||||||
|
// helper functions
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Used for the heavy lifing of copying a single element, a group of elements
|
||||||
|
// an element with bound text etc.
|
||||||
|
const copyElements = <T extends ExcalidrawElement | ExcalidrawElement[]>(
|
||||||
|
element: T,
|
||||||
|
): T extends ExcalidrawElement[]
|
||||||
|
? ExcalidrawElement[]
|
||||||
|
: ExcalidrawElement | null => {
|
||||||
|
const elements = castArray(element);
|
||||||
|
|
||||||
|
const _newElements = elements.reduce(
|
||||||
|
(acc: ExcalidrawElement[], element) => {
|
||||||
|
if (processedIds.has(element.id)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedIds.set(element.id, true);
|
||||||
|
|
||||||
|
const newElement = duplicateElement(
|
||||||
|
appState.editingGroupId,
|
||||||
|
groupIdMap,
|
||||||
|
element,
|
||||||
|
opts.overrides?.(element),
|
||||||
|
opts.randomizeSeed,
|
||||||
|
);
|
||||||
|
|
||||||
|
processedIds.set(newElement.id, true);
|
||||||
|
|
||||||
|
duplicatedElementsMap.set(newElement.id, newElement);
|
||||||
|
oldIdToDuplicatedId.set(element.id, newElement.id);
|
||||||
|
|
||||||
|
oldElements.push(element);
|
||||||
|
newElements.push(newElement);
|
||||||
|
|
||||||
|
acc.push(newElement);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
Array.isArray(element) ? _newElements : _newElements[0] || null
|
||||||
|
) as T extends ExcalidrawElement[]
|
||||||
|
? ExcalidrawElement[]
|
||||||
|
: ExcalidrawElement | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to position cloned elements in the Z-order the product needs it
|
||||||
|
const insertBeforeOrAfterIndex = (
|
||||||
|
index: number,
|
||||||
|
elements: ExcalidrawElement | null | ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
if (!elements) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reverseOrder && index < 1) {
|
||||||
|
elementsWithClones.unshift(...castArray(elements));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reverseOrder && index > elementsWithClones.length - 1) {
|
||||||
|
elementsWithClones.push(...castArray(elements));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
elementsWithClones.splice(
|
||||||
|
index + (reverseOrder ? 0 : 1),
|
||||||
|
0,
|
||||||
|
...castArray(elements),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const frameIdsToDuplicate = new Set(
|
||||||
|
elements
|
||||||
|
.filter(
|
||||||
|
(el) => _idsOfElementsToDuplicate.has(el.id) && isFrameLikeElement(el),
|
||||||
|
)
|
||||||
|
.map((el) => el.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
if (processedIds.has(element.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_idsOfElementsToDuplicate.has(element.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// groups
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const groupId = getSelectedGroupForElement(appState, element);
|
||||||
|
if (groupId) {
|
||||||
|
const groupElements = getElementsInGroup(elements, groupId).flatMap(
|
||||||
|
(element) =>
|
||||||
|
isFrameLikeElement(element)
|
||||||
|
? [...getFrameChildren(elements, element.id), element]
|
||||||
|
: [element],
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetIndex = reverseOrder
|
||||||
|
? elementsWithClones.findIndex((el) => {
|
||||||
|
return el.groupIds?.includes(groupId);
|
||||||
|
})
|
||||||
|
: findLastIndex(elementsWithClones, (el) => {
|
||||||
|
return el.groupIds?.includes(groupId);
|
||||||
|
});
|
||||||
|
|
||||||
|
insertBeforeOrAfterIndex(targetIndex, copyElements(groupElements));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// frame duplication
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if (element.frameId && frameIdsToDuplicate.has(element.frameId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFrameLikeElement(element)) {
|
||||||
|
const frameId = element.id;
|
||||||
|
|
||||||
|
const frameChildren = getFrameChildren(elements, frameId);
|
||||||
|
|
||||||
|
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||||
|
return el.frameId === frameId || el.id === frameId;
|
||||||
|
});
|
||||||
|
|
||||||
|
insertBeforeOrAfterIndex(
|
||||||
|
targetIndex,
|
||||||
|
copyElements([...frameChildren, element]),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// text container
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if (hasBoundTextElement(element)) {
|
||||||
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
|
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||||
|
return (
|
||||||
|
el.id === element.id ||
|
||||||
|
("containerId" in el && el.containerId === element.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (boundTextElement) {
|
||||||
|
insertBeforeOrAfterIndex(
|
||||||
|
targetIndex + (reverseOrder ? -1 : 0),
|
||||||
|
copyElements([element, boundTextElement]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
insertBeforeOrAfterIndex(targetIndex, copyElements(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBoundToContainer(element)) {
|
||||||
|
const container = getContainerElement(element, elementsMap);
|
||||||
|
|
||||||
|
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||||
|
return el.id === element.id || el.id === container?.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
insertBeforeOrAfterIndex(
|
||||||
|
targetIndex,
|
||||||
|
copyElements([container, element]),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
insertBeforeOrAfterIndex(targetIndex, copyElements(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// default duplication (regular elements)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
insertBeforeOrAfterIndex(
|
||||||
|
findLastIndex(elementsWithClones, (el) => el.id === element.id),
|
||||||
|
copyElements(element),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fixDuplicatedBindingsAfterDuplication(
|
||||||
|
newElements,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
duplicatedElementsMap as NonDeletedSceneElementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (reverseOrder) {
|
||||||
|
fixReversedBindings(
|
||||||
|
_idsOfElementsToDuplicate,
|
||||||
|
elementsWithClones,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindElementsToFramesAfterDuplication(
|
||||||
|
elementsWithClones,
|
||||||
|
oldElements,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
newElements,
|
||||||
|
elementsWithClones,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simplified deep clone for the purpose of cloning ExcalidrawElement.
|
||||||
|
//
|
||||||
|
// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
|
||||||
|
// Typed arrays and other non-null objects.
|
||||||
|
//
|
||||||
|
// Adapted from https://github.com/lukeed/klona
|
||||||
|
//
|
||||||
|
// The reason for `deepCopyElement()` wrapper is type safety (only allow
|
||||||
|
// passing ExcalidrawElement as the top-level argument).
|
||||||
|
const _deepCopyElement = (val: any, depth: number = 0) => {
|
||||||
|
// only clone non-primitives
|
||||||
|
if (val == null || typeof val !== "object") {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectType = Object.prototype.toString.call(val);
|
||||||
|
|
||||||
|
if (objectType === "[object Object]") {
|
||||||
|
const tmp =
|
||||||
|
typeof val.constructor === "function"
|
||||||
|
? Object.create(Object.getPrototypeOf(val))
|
||||||
|
: {};
|
||||||
|
for (const key in val) {
|
||||||
|
if (val.hasOwnProperty(key)) {
|
||||||
|
// don't copy non-serializable objects like these caches. They'll be
|
||||||
|
// populated when the element is rendered.
|
||||||
|
if (depth === 0 && (key === "shape" || key === "canvas")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tmp[key] = _deepCopyElement(val[key], depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
let k = val.length;
|
||||||
|
const arr = new Array(k);
|
||||||
|
while (k--) {
|
||||||
|
arr[k] = _deepCopyElement(val[k], depth + 1);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're not cloning non-array & non-plain-object objects because we
|
||||||
|
// don't support them on excalidraw elements yet. If we do, we need to make
|
||||||
|
// sure we start cloning them, so let's warn about it.
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
if (
|
||||||
|
objectType !== "[object Object]" &&
|
||||||
|
objectType !== "[object Array]" &&
|
||||||
|
objectType.startsWith("[object ")
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or
|
||||||
|
* any value. The purpose is to to break object references for immutability
|
||||||
|
* reasons, whenever we want to keep the original element, but ensure it's not
|
||||||
|
* mutated.
|
||||||
|
*
|
||||||
|
* Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
|
||||||
|
* Typed arrays and other non-null objects.
|
||||||
|
*/
|
||||||
|
export const deepCopyElement = <T extends ExcalidrawElement>(
|
||||||
|
val: T,
|
||||||
|
): Mutable<T> => {
|
||||||
|
return _deepCopyElement(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
const __test__defineOrigId = (clonedObj: object, origId: string) => {
|
||||||
|
Object.defineProperty(clonedObj, ORIG_ID, {
|
||||||
|
value: origId,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
};
|
|
@ -13,10 +13,16 @@ import {
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import BinaryHeap from "../binaryheap";
|
import {
|
||||||
import { getSizeFromPoints } from "../points";
|
BinaryHeap,
|
||||||
import { aabbForElement, pointInsideBounds } from "../shapes";
|
invariant,
|
||||||
import { invariant, isAnyTrue, tupleToCoors } from "../utils";
|
isAnyTrue,
|
||||||
|
tupleToCoors,
|
||||||
|
getSizeFromPoints,
|
||||||
|
isDevEnv,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bindPointToSnapToElementOutline,
|
bindPointToSnapToElementOutline,
|
||||||
|
@ -47,6 +53,8 @@ import {
|
||||||
type SceneElementsMap,
|
type SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { Heading } from "./heading";
|
import type { Heading } from "./heading";
|
||||||
import type {
|
import type {
|
||||||
|
@ -57,7 +65,6 @@ import type {
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { AppState } from "../types";
|
|
||||||
|
|
||||||
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||||
|
|
||||||
|
@ -238,16 +245,6 @@ const handleSegmentRenormalization = (
|
||||||
nextPoints.map((p) =>
|
nextPoints.map((p) =>
|
||||||
pointFrom<LocalPoint>(p[0] - arrow.x, p[1] - arrow.y),
|
pointFrom<LocalPoint>(p[0] - arrow.x, p[1] - arrow.y),
|
||||||
),
|
),
|
||||||
arrow.startBinding &&
|
|
||||||
getBindableElementForId(
|
|
||||||
arrow.startBinding.elementId,
|
|
||||||
elementsMap,
|
|
||||||
),
|
|
||||||
arrow.endBinding &&
|
|
||||||
getBindableElementForId(
|
|
||||||
arrow.endBinding.elementId,
|
|
||||||
elementsMap,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
) ?? [],
|
) ?? [],
|
||||||
),
|
),
|
||||||
|
@ -258,7 +255,7 @@ const handleSegmentRenormalization = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
import.meta.env.DEV &&
|
isDevEnv() &&
|
||||||
invariant(
|
invariant(
|
||||||
validateElbowPoints(nextPoints),
|
validateElbowPoints(nextPoints),
|
||||||
"Invalid elbow points with fixed segments",
|
"Invalid elbow points with fixed segments",
|
||||||
|
@ -341,9 +338,6 @@ const handleSegmentRelease = (
|
||||||
y,
|
y,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
startBinding &&
|
|
||||||
getBindableElementForId(startBinding.elementId, elementsMap),
|
|
||||||
endBinding && getBindableElementForId(endBinding.elementId, elementsMap),
|
|
||||||
{ isDragging: false },
|
{ isDragging: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -983,6 +977,8 @@ export const updateElbowArrowPoints = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||||
|
|
||||||
const updatedPoints: readonly LocalPoint[] = updates.points
|
const updatedPoints: readonly LocalPoint[] = updates.points
|
||||||
? updates.points && updates.points.length === 2
|
? updates.points && updates.points.length === 2
|
||||||
? arrow.points.map((p, idx) =>
|
? arrow.points.map((p, idx) =>
|
||||||
|
@ -995,7 +991,7 @@ export const updateElbowArrowPoints = (
|
||||||
: updates.points.slice()
|
: updates.points.slice()
|
||||||
: arrow.points.slice();
|
: arrow.points.slice();
|
||||||
|
|
||||||
// 0. During all element replacement in the scene, we just need to renormalize
|
// During all element replacement in the scene, we just need to renormalize
|
||||||
// the arrow
|
// the arrow
|
||||||
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
|
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
|
||||||
const {
|
const {
|
||||||
|
@ -1016,11 +1012,12 @@ export const updateElbowArrowPoints = (
|
||||||
getBindableElementForId(startBinding.elementId, elementsMap);
|
getBindableElementForId(startBinding.elementId, elementsMap);
|
||||||
const endElement =
|
const endElement =
|
||||||
endBinding && getBindableElementForId(endBinding.elementId, elementsMap);
|
endBinding && getBindableElementForId(endBinding.elementId, elementsMap);
|
||||||
|
const areUpdatedPointsValid = validateElbowPoints(updatedPoints);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(startBinding && !startElement) ||
|
(startBinding && !startElement && areUpdatedPointsValid) ||
|
||||||
(endBinding && !endElement) ||
|
(endBinding && !endElement && areUpdatedPointsValid) ||
|
||||||
(elementsMap.size === 0 && validateElbowPoints(updatedPoints)) ||
|
(elementsMap.size === 0 && areUpdatedPointsValid) ||
|
||||||
(Object.keys(restOfTheUpdates).length === 0 &&
|
(Object.keys(restOfTheUpdates).length === 0 &&
|
||||||
(startElement?.id !== startBinding?.elementId ||
|
(startElement?.id !== startBinding?.elementId ||
|
||||||
endElement?.id !== endBinding?.elementId))
|
endElement?.id !== endBinding?.elementId))
|
||||||
|
@ -1055,12 +1052,22 @@ export const updateElbowArrowPoints = (
|
||||||
},
|
},
|
||||||
elementsMap,
|
elementsMap,
|
||||||
updatedPoints,
|
updatedPoints,
|
||||||
startElement,
|
|
||||||
endElement,
|
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
// 0. During all element replacement in the scene, we just need to renormalize
|
||||||
|
// the arrow
|
||||||
|
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
|
||||||
|
if (elementsMap.size === 0 && areUpdatedPointsValid) {
|
||||||
|
return normalizeArrowElementUpdate(
|
||||||
|
updatedPoints.map((p) =>
|
||||||
|
pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1]),
|
||||||
|
),
|
||||||
|
arrow.fixedSegments,
|
||||||
|
arrow.startIsSpecial,
|
||||||
|
arrow.endIsSpecial,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
////
|
////
|
||||||
// 1. Renormalize the arrow
|
// 1. Renormalize the arrow
|
||||||
|
@ -1084,7 +1091,7 @@ export const updateElbowArrowPoints = (
|
||||||
arrow.points[i] ?? pointFrom<LocalPoint>(Infinity, Infinity),
|
arrow.points[i] ?? pointFrom<LocalPoint>(Infinity, Infinity),
|
||||||
),
|
),
|
||||||
) &&
|
) &&
|
||||||
validateElbowPoints(updatedPoints)
|
areUpdatedPointsValid
|
||||||
) {
|
) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -1195,8 +1202,6 @@ const getElbowArrowData = (
|
||||||
},
|
},
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
nextPoints: readonly LocalPoint[],
|
nextPoints: readonly LocalPoint[],
|
||||||
startElement: ExcalidrawBindableElement | null,
|
|
||||||
endElement: ExcalidrawBindableElement | null,
|
|
||||||
options?: {
|
options?: {
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
zoom?: AppState["zoom"];
|
zoom?: AppState["zoom"];
|
||||||
|
@ -1211,8 +1216,8 @@ const getElbowArrowData = (
|
||||||
GlobalPoint
|
GlobalPoint
|
||||||
>(nextPoints[nextPoints.length - 1], vector(arrow.x, arrow.y));
|
>(nextPoints[nextPoints.length - 1], vector(arrow.x, arrow.y));
|
||||||
|
|
||||||
let hoveredStartElement = startElement;
|
let hoveredStartElement = null;
|
||||||
let hoveredEndElement = endElement;
|
let hoveredEndElement = null;
|
||||||
if (options?.isDragging) {
|
if (options?.isDragging) {
|
||||||
const elements = Array.from(elementsMap.values());
|
const elements = Array.from(elementsMap.values());
|
||||||
hoveredStartElement =
|
hoveredStartElement =
|
||||||
|
@ -1221,39 +1226,47 @@ const getElbowArrowData = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
options?.zoom,
|
options?.zoom,
|
||||||
) || startElement;
|
) || null;
|
||||||
hoveredEndElement =
|
hoveredEndElement =
|
||||||
getHoveredElement(
|
getHoveredElement(
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
elements,
|
elements,
|
||||||
options?.zoom,
|
options?.zoom,
|
||||||
) || endElement;
|
) || null;
|
||||||
|
} else {
|
||||||
|
hoveredStartElement = arrow.startBinding
|
||||||
|
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
|
||||||
|
null
|
||||||
|
: null;
|
||||||
|
hoveredEndElement = arrow.endBinding
|
||||||
|
? getBindableElementForId(arrow.endBinding.elementId, elementsMap) || null
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startGlobalPoint = getGlobalPoint(
|
const startGlobalPoint = getGlobalPoint(
|
||||||
{
|
{
|
||||||
...arrow,
|
...arrow,
|
||||||
|
type: "arrow",
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
points: nextPoints,
|
points: nextPoints,
|
||||||
} as ExcalidrawElbowArrowElement,
|
} as ExcalidrawElbowArrowElement,
|
||||||
"start",
|
"start",
|
||||||
arrow.startBinding?.fixedPoint,
|
arrow.startBinding?.fixedPoint,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
startElement,
|
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
const endGlobalPoint = getGlobalPoint(
|
const endGlobalPoint = getGlobalPoint(
|
||||||
{
|
{
|
||||||
...arrow,
|
...arrow,
|
||||||
|
type: "arrow",
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
points: nextPoints,
|
points: nextPoints,
|
||||||
} as ExcalidrawElbowArrowElement,
|
} as ExcalidrawElbowArrowElement,
|
||||||
"end",
|
"end",
|
||||||
arrow.endBinding?.fixedPoint,
|
arrow.endBinding?.fixedPoint,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
endElement,
|
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
|
@ -2199,36 +2212,35 @@ const getGlobalPoint = (
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
fixedPointRatio: [number, number] | undefined | null,
|
fixedPointRatio: [number, number] | undefined | null,
|
||||||
initialPoint: GlobalPoint,
|
initialPoint: GlobalPoint,
|
||||||
boundElement?: ExcalidrawBindableElement | null,
|
element?: ExcalidrawBindableElement | null,
|
||||||
hoveredElement?: ExcalidrawBindableElement | null,
|
|
||||||
isDragging?: boolean,
|
isDragging?: boolean,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
if (hoveredElement) {
|
if (element) {
|
||||||
const snapPoint = bindPointToSnapToElementOutline(
|
const snapPoint = bindPointToSnapToElementOutline(
|
||||||
arrow,
|
arrow,
|
||||||
hoveredElement,
|
element,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
);
|
);
|
||||||
|
|
||||||
return snapToMid(hoveredElement, snapPoint);
|
return snapToMid(element, snapPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
return initialPoint;
|
return initialPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boundElement) {
|
if (element) {
|
||||||
const fixedGlobalPoint = getGlobalFixedPointForBindableElement(
|
const fixedGlobalPoint = getGlobalFixedPointForBindableElement(
|
||||||
fixedPointRatio || [0, 0],
|
fixedPointRatio || [0, 0],
|
||||||
boundElement,
|
element,
|
||||||
);
|
);
|
||||||
|
|
||||||
// NOTE: Resize scales the binding position point too, so we need to update it
|
// NOTE: Resize scales the binding position point too, so we need to update it
|
||||||
return Math.abs(
|
return Math.abs(
|
||||||
distanceToBindableElement(boundElement, fixedGlobalPoint) -
|
distanceToBindableElement(element, fixedGlobalPoint) -
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
) > 0.01
|
) > 0.01
|
||||||
? bindPointToSnapToElementOutline(arrow, boundElement, startOrEnd)
|
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
||||||
: fixedGlobalPoint;
|
: fixedGlobalPoint;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,12 @@
|
||||||
* Create and link between shapes.
|
* Create and link between shapes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ELEMENT_LINK_KEY } from "../constants";
|
import { ELEMENT_LINK_KEY, normalizeLink } from "@excalidraw/common";
|
||||||
import { normalizeLink } from "../data/url";
|
|
||||||
import { elementsAreInSameGroup } from "../groups";
|
import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { elementsAreInSameGroup } from "./groups";
|
||||||
|
|
||||||
import type { AppProps, AppState } from "../types";
|
|
||||||
import type { ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
export const defaultGetElementLinkFromSelection: Exclude<
|
export const defaultGetElementLinkFromSelection: Exclude<
|
|
@ -1,15 +1,17 @@
|
||||||
import { register } from "../actions/register";
|
import {
|
||||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
FONT_FAMILY,
|
||||||
import { setCursorForShape } from "../cursor";
|
VERTICAL_ALIGN,
|
||||||
import { CaptureUpdateAction } from "../store";
|
escapeDoubleQuotes,
|
||||||
import { escapeDoubleQuotes, getFontString, updateActiveTool } from "../utils";
|
getFontString,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { ExcalidrawProps } from "@excalidraw/excalidraw/types";
|
||||||
|
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { newTextElement } from "./newElement";
|
import { newTextElement } from "./newElement";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import { isIframeElement } from "./typeChecks";
|
import { isIframeElement } from "./typeChecks";
|
||||||
|
|
||||||
import type { ExcalidrawProps } from "../types";
|
|
||||||
import type { MarkRequired } from "../utility-types";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
|
@ -319,34 +321,6 @@ export const createPlaceholderEmbeddableLabel = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionSetEmbeddableAsActiveTool = register({
|
|
||||||
name: "setEmbeddableAsActiveTool",
|
|
||||||
trackEvent: { category: "toolbar" },
|
|
||||||
target: "Tool",
|
|
||||||
label: "toolBar.embeddable",
|
|
||||||
perform: (elements, appState, _, app) => {
|
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
|
||||||
type: "embeddable",
|
|
||||||
});
|
|
||||||
|
|
||||||
setCursorForShape(app.canvas, {
|
|
||||||
...appState,
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
elements,
|
|
||||||
appState: {
|
|
||||||
...appState,
|
|
||||||
activeTool: updateActiveTool(appState, {
|
|
||||||
type: "embeddable",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const matchHostname = (
|
const matchHostname = (
|
||||||
url: string,
|
url: string,
|
||||||
/** using a Set assumes it already contains normalized bare domains */
|
/** using a Set assumes it already contains normalized bare domains */
|
|
@ -1,9 +1,11 @@
|
||||||
import { pointFrom, type LocalPoint } from "@excalidraw/math";
|
import { KEYS, invariant, toBrandedType } from "@excalidraw/common";
|
||||||
|
|
||||||
import { elementOverlapsWithFrame, elementsAreInFrameBounds } from "../frame";
|
import { type GlobalPoint, pointFrom, type LocalPoint } from "@excalidraw/math";
|
||||||
import { KEYS } from "../keys";
|
|
||||||
import { aabbForElement } from "../shapes";
|
import type {
|
||||||
import { invariant, toBrandedType } from "../utils";
|
AppState,
|
||||||
|
PendingExcalidrawElements,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { bindLinearElement } from "./binding";
|
import { bindLinearElement } from "./binding";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
@ -19,6 +21,8 @@ import {
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { newArrowElement, newElement } from "./newElement";
|
import { newArrowElement, newElement } from "./newElement";
|
||||||
|
import { aabbForElement } from "./shapes";
|
||||||
|
import { elementsAreInFrameBounds, elementOverlapsWithFrame } from "./frame";
|
||||||
import {
|
import {
|
||||||
isBindableElement,
|
isBindableElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
@ -35,8 +39,6 @@ import {
|
||||||
type OrderedExcalidrawElement,
|
type OrderedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { AppState, PendingExcalidrawElements } from "../types";
|
|
||||||
|
|
||||||
type LinkDirection = "up" | "right" | "down" | "left";
|
type LinkDirection = "up" | "right" | "down" | "left";
|
||||||
|
|
||||||
const VERTICAL_OFFSET = 100;
|
const VERTICAL_OFFSET = 100;
|
||||||
|
@ -94,7 +96,7 @@ const getNodeRelatives = (
|
||||||
const heading = headingForPointFromElement(node, aabbForElement(node), [
|
const heading = headingForPointFromElement(node, aabbForElement(node), [
|
||||||
edgePoint[0] + el.x,
|
edgePoint[0] + el.x,
|
||||||
edgePoint[1] + el.y,
|
edgePoint[1] + el.y,
|
||||||
] as Readonly<LocalPoint>);
|
] as Readonly<GlobalPoint>);
|
||||||
|
|
||||||
acc.push({
|
acc.push({
|
||||||
relative,
|
relative,
|
|
@ -1,16 +1,20 @@
|
||||||
import { generateNKeysBetween } from "fractional-indexing";
|
import { generateNKeysBetween } from "fractional-indexing";
|
||||||
|
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
|
||||||
import { hasBoundTextElement } from "./element/typeChecks";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { InvalidFractionalIndexError } from "./errors";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { arrayToMap } from "./utils";
|
import { hasBoundTextElement } from "./typeChecks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./types";
|
||||||
|
|
||||||
|
export class InvalidFractionalIndexError extends Error {
|
||||||
|
public code = "ELEMENT_HAS_INVALID_INDEX" as const;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Envisioned relation between array order and fractional indices:
|
* Envisioned relation between array order and fractional indices:
|
|
@ -1,24 +1,33 @@
|
||||||
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
|
import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
|
||||||
import {
|
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
||||||
doLineSegmentsIntersect,
|
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
||||||
elementsOverlappingBBox,
|
|
||||||
} from "@excalidraw/utils";
|
import type { ExcalidrawElementsIncludingDeleted } from "@excalidraw/excalidraw/scene/Scene";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
StaticCanvasAppState,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import type { ReadonlySetLike } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { getElementsWithinSelection, getSelectedElements } from "./selection";
|
||||||
|
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getElementLineSegments,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
isTextElement,
|
} from "./bounds";
|
||||||
} from "./element";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getElementLineSegments } from "./element/bounds";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
isFrameElement,
|
||||||
getContainerElement,
|
isFrameLikeElement,
|
||||||
} from "./element/textElement";
|
isTextElement,
|
||||||
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
} from "./typeChecks";
|
||||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
|
||||||
import { arrayToMap } from "./utils";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
@ -27,14 +36,7 @@ import type {
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
|
||||||
import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
|
||||||
import type {
|
|
||||||
AppClassProperties,
|
|
||||||
AppState,
|
|
||||||
StaticCanvasAppState,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { ReadonlySetLike } from "./utility-types";
|
|
||||||
|
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
|
@ -1,6 +1,13 @@
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import type {
|
||||||
import { getSelectedElements } from "./scene";
|
AppClassProperties,
|
||||||
import { makeNextSelectedElementIds } from "./scene/selection";
|
AppState,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import { getBoundTextElement } from "./textElement";
|
||||||
|
|
||||||
|
import { makeNextSelectedElementIds, getSelectedElements } from "./selection";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GroupId,
|
GroupId,
|
||||||
|
@ -9,13 +16,7 @@ import type {
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
} from "./element/types";
|
|
||||||
import type {
|
|
||||||
AppClassProperties,
|
|
||||||
AppState,
|
|
||||||
InteractiveCanvasAppState,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Mutable } from "./utility-types";
|
|
||||||
|
|
||||||
export const selectGroup = (
|
export const selectGroup = (
|
||||||
groupId: GroupId,
|
groupId: GroupId,
|
||||||
|
@ -214,7 +215,10 @@ export const isSelectedViaGroup = (
|
||||||
) => getSelectedGroupForElement(appState, element) != null;
|
) => getSelectedGroupForElement(appState, element) != null;
|
||||||
|
|
||||||
export const getSelectedGroupForElement = (
|
export const getSelectedGroupForElement = (
|
||||||
appState: InteractiveCanvasAppState,
|
appState: Pick<
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
"editingGroupId" | "selectedGroupIds"
|
||||||
|
>,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
) =>
|
) =>
|
||||||
element.groupIds
|
element.groupIds
|
||||||
|
@ -294,24 +298,6 @@ export const getSelectedGroupIdForElement = (
|
||||||
selectedGroupIds: { [groupId: string]: boolean },
|
selectedGroupIds: { [groupId: string]: boolean },
|
||||||
) => element.groupIds.find((groupId) => selectedGroupIds[groupId]);
|
) => element.groupIds.find((groupId) => selectedGroupIds[groupId]);
|
||||||
|
|
||||||
export const getNewGroupIdsForDuplication = (
|
|
||||||
groupIds: ExcalidrawElement["groupIds"],
|
|
||||||
editingGroupId: AppState["editingGroupId"],
|
|
||||||
mapper: (groupId: GroupId) => GroupId,
|
|
||||||
) => {
|
|
||||||
const copy = [...groupIds];
|
|
||||||
const positionOfEditingGroupId = editingGroupId
|
|
||||||
? groupIds.indexOf(editingGroupId)
|
|
||||||
: -1;
|
|
||||||
const endIndex =
|
|
||||||
positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
|
|
||||||
for (let index = 0; index < endIndex; index++) {
|
|
||||||
copy[index] = mapper(copy[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return copy;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addToGroup = (
|
export const addToGroup = (
|
||||||
prevGroupIds: ExcalidrawElement["groupIds"],
|
prevGroupIds: ExcalidrawElement["groupIds"],
|
||||||
newGroupId: GroupId,
|
newGroupId: GroupId,
|
||||||
|
@ -398,3 +384,21 @@ export const elementsAreInSameGroup = (
|
||||||
export const isInGroup = (element: NonDeletedExcalidrawElement) => {
|
export const isInGroup = (element: NonDeletedExcalidrawElement) => {
|
||||||
return element.groupIds.length > 0;
|
return element.groupIds.length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getNewGroupIdsForDuplication = (
|
||||||
|
groupIds: ExcalidrawElement["groupIds"],
|
||||||
|
editingGroupId: AppState["editingGroupId"],
|
||||||
|
mapper: (groupId: GroupId) => GroupId,
|
||||||
|
) => {
|
||||||
|
const copy = [...groupIds];
|
||||||
|
const positionOfEditingGroupId = editingGroupId
|
||||||
|
? groupIds.indexOf(editingGroupId)
|
||||||
|
: -1;
|
||||||
|
const endIndex =
|
||||||
|
positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
|
||||||
|
for (let index = 0; index < endIndex; index++) {
|
||||||
|
copy[index] = mapper(copy[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
};
|
282
packages/element/src/heading.ts
Normal file
282
packages/element/src/heading.ts
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
pointFrom,
|
||||||
|
pointFromVector,
|
||||||
|
pointRotateRads,
|
||||||
|
pointScaleFromOrigin,
|
||||||
|
pointsEqual,
|
||||||
|
triangleIncludesPoint,
|
||||||
|
vectorCross,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorScale,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
LocalPoint,
|
||||||
|
GlobalPoint,
|
||||||
|
Triangle,
|
||||||
|
Vector,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { getCenterForBounds, type Bounds } from "./bounds";
|
||||||
|
|
||||||
|
import type { ExcalidrawBindableElement } from "./types";
|
||||||
|
|
||||||
|
export const HEADING_RIGHT = [1, 0] as Heading;
|
||||||
|
export const HEADING_DOWN = [0, 1] as Heading;
|
||||||
|
export const HEADING_LEFT = [-1, 0] as Heading;
|
||||||
|
export const HEADING_UP = [0, -1] as Heading;
|
||||||
|
export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
|
||||||
|
|
||||||
|
export const vectorToHeading = (vec: Vector): Heading => {
|
||||||
|
const [x, y] = vec;
|
||||||
|
const absX = Math.abs(x);
|
||||||
|
const absY = Math.abs(y);
|
||||||
|
if (x > absY) {
|
||||||
|
return HEADING_RIGHT;
|
||||||
|
} else if (x <= -absY) {
|
||||||
|
return HEADING_LEFT;
|
||||||
|
} else if (y > absX) {
|
||||||
|
return HEADING_DOWN;
|
||||||
|
}
|
||||||
|
return HEADING_UP;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const headingForPoint = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p: P,
|
||||||
|
o: P,
|
||||||
|
) => vectorToHeading(vectorFromPoint<P>(p, o));
|
||||||
|
|
||||||
|
export const headingForPointIsHorizontal = <P extends GlobalPoint | LocalPoint>(
|
||||||
|
p: P,
|
||||||
|
o: P,
|
||||||
|
) => headingIsHorizontal(headingForPoint<P>(p, o));
|
||||||
|
|
||||||
|
export const compareHeading = (a: Heading, b: Heading) =>
|
||||||
|
a[0] === b[0] && a[1] === b[1];
|
||||||
|
|
||||||
|
export const headingIsHorizontal = (a: Heading) =>
|
||||||
|
compareHeading(a, HEADING_RIGHT) || compareHeading(a, HEADING_LEFT);
|
||||||
|
|
||||||
|
export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
|
||||||
|
|
||||||
|
const headingForPointFromDiamondElement = (
|
||||||
|
element: Readonly<ExcalidrawBindableElement>,
|
||||||
|
aabb: Readonly<Bounds>,
|
||||||
|
point: Readonly<GlobalPoint>,
|
||||||
|
): Heading => {
|
||||||
|
const midPoint = getCenterForBounds(aabb);
|
||||||
|
|
||||||
|
if (isDevEnv() || isTestEnv()) {
|
||||||
|
invariant(
|
||||||
|
element.width > 0 && element.height > 0,
|
||||||
|
"Diamond element has no width or height",
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!pointsEqual(midPoint, point),
|
||||||
|
"The point is too close to the element mid point to determine heading",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHRINK = 0.95; // Rounded elements tolerance
|
||||||
|
const top = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(element.x + element.width / 2, element.y),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
),
|
||||||
|
SHRINK,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
const right = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
),
|
||||||
|
SHRINK,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
const bottom = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
),
|
||||||
|
SHRINK,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
const left = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorFromPoint(
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(element.x, element.y + element.height / 2),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
),
|
||||||
|
SHRINK,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Corners
|
||||||
|
if (
|
||||||
|
vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, right)) <=
|
||||||
|
0 &&
|
||||||
|
vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, left)) > 0
|
||||||
|
) {
|
||||||
|
return headingForPoint(top, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, right),
|
||||||
|
vectorFromPoint(right, bottom),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(vectorFromPoint(point, right), vectorFromPoint(right, top)) > 0
|
||||||
|
) {
|
||||||
|
return headingForPoint(right, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, bottom),
|
||||||
|
vectorFromPoint(bottom, left),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, bottom),
|
||||||
|
vectorFromPoint(bottom, right),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
return headingForPoint(bottom, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, top)) <=
|
||||||
|
0 &&
|
||||||
|
vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, bottom)) > 0
|
||||||
|
) {
|
||||||
|
return headingForPoint(left, midPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sides
|
||||||
|
if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(top, midPoint),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(right, midPoint),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
const p = element.width > element.height ? top : right;
|
||||||
|
return headingForPoint(p, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(right, midPoint),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(bottom, midPoint),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
const p = element.width > element.height ? bottom : right;
|
||||||
|
return headingForPoint(p, midPoint);
|
||||||
|
} else if (
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(bottom, midPoint),
|
||||||
|
) <= 0 &&
|
||||||
|
vectorCross(
|
||||||
|
vectorFromPoint(point, midPoint),
|
||||||
|
vectorFromPoint(left, midPoint),
|
||||||
|
) > 0
|
||||||
|
) {
|
||||||
|
const p = element.width > element.height ? bottom : left;
|
||||||
|
return headingForPoint(p, midPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = element.width > element.height ? top : left;
|
||||||
|
return headingForPoint(p, midPoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gets the heading for the point by creating a bounding box around the rotated
|
||||||
|
// close fitting bounding box, then creating 4 search cones around the center of
|
||||||
|
// the external bbox.
|
||||||
|
export const headingForPointFromElement = <Point extends GlobalPoint>(
|
||||||
|
element: Readonly<ExcalidrawBindableElement>,
|
||||||
|
aabb: Readonly<Bounds>,
|
||||||
|
p: Readonly<Point>,
|
||||||
|
): Heading => {
|
||||||
|
const SEARCH_CONE_MULTIPLIER = 2;
|
||||||
|
|
||||||
|
const midPoint = getCenterForBounds(aabb);
|
||||||
|
|
||||||
|
if (element.type === "diamond") {
|
||||||
|
return headingForPointFromDiamondElement(element, aabb, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
const topLeft = pointScaleFromOrigin(
|
||||||
|
pointFrom(aabb[0], aabb[1]),
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
) as Point;
|
||||||
|
const topRight = pointScaleFromOrigin(
|
||||||
|
pointFrom(aabb[2], aabb[1]),
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
) as Point;
|
||||||
|
const bottomLeft = pointScaleFromOrigin(
|
||||||
|
pointFrom(aabb[0], aabb[3]),
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
) as Point;
|
||||||
|
const bottomRight = pointScaleFromOrigin(
|
||||||
|
pointFrom(aabb[2], aabb[3]),
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
) as Point;
|
||||||
|
|
||||||
|
return triangleIncludesPoint<Point>(
|
||||||
|
[topLeft, topRight, midPoint] as Triangle<Point>,
|
||||||
|
p,
|
||||||
|
)
|
||||||
|
? HEADING_UP
|
||||||
|
: triangleIncludesPoint<Point>(
|
||||||
|
[topRight, bottomRight, midPoint] as Triangle<Point>,
|
||||||
|
p,
|
||||||
|
)
|
||||||
|
? HEADING_RIGHT
|
||||||
|
: triangleIncludesPoint<Point>(
|
||||||
|
[bottomRight, bottomLeft, midPoint] as Triangle<Point>,
|
||||||
|
p,
|
||||||
|
)
|
||||||
|
? HEADING_DOWN
|
||||||
|
: HEADING_LEFT;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const flipHeading = (h: Heading): Heading =>
|
||||||
|
[
|
||||||
|
h[0] === 0 ? 0 : h[0] > 0 ? -1 : 1,
|
||||||
|
h[1] === 0 ? 0 : h[1] > 0 ? -1 : 1,
|
||||||
|
] as Heading;
|
|
@ -2,11 +2,16 @@
|
||||||
// ExcalidrawImageElement & related helpers
|
// ExcalidrawImageElement & related helpers
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
import { MIME_TYPES, SVG_NS } from "../constants";
|
import { MIME_TYPES, SVG_NS } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AppClassProperties,
|
||||||
|
DataURL,
|
||||||
|
BinaryFiles,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { isInitializedImageElement } from "./typeChecks";
|
import { isInitializedImageElement } from "./typeChecks";
|
||||||
|
|
||||||
import type { AppClassProperties, DataURL, BinaryFiles } from "../types";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
|
@ -7,56 +7,6 @@ import type {
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export {
|
|
||||||
newElement,
|
|
||||||
newTextElement,
|
|
||||||
refreshTextDimensions,
|
|
||||||
newLinearElement,
|
|
||||||
newArrowElement,
|
|
||||||
newImageElement,
|
|
||||||
duplicateElement,
|
|
||||||
} from "./newElement";
|
|
||||||
export {
|
|
||||||
getElementAbsoluteCoords,
|
|
||||||
getElementBounds,
|
|
||||||
getCommonBounds,
|
|
||||||
getDiamondPoints,
|
|
||||||
getArrowheadPoints,
|
|
||||||
getClosestElementBounds,
|
|
||||||
} from "./bounds";
|
|
||||||
|
|
||||||
export {
|
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
|
||||||
getTransformHandlesFromCoords,
|
|
||||||
getTransformHandles,
|
|
||||||
} from "./transformHandles";
|
|
||||||
export {
|
|
||||||
resizeTest,
|
|
||||||
getCursorForResizingElement,
|
|
||||||
getElementWithTransformHandleType,
|
|
||||||
getTransformHandleTypeFromCoords,
|
|
||||||
} from "./resizeTest";
|
|
||||||
export {
|
|
||||||
transformElements,
|
|
||||||
getResizeOffsetXY,
|
|
||||||
getResizeArrowDirection,
|
|
||||||
} from "./resizeElements";
|
|
||||||
export {
|
|
||||||
dragSelectedElements,
|
|
||||||
getDragOffsetXY,
|
|
||||||
dragNewElement,
|
|
||||||
} from "./dragElements";
|
|
||||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
|
||||||
export { redrawTextBoundingBox, getTextFromElements } from "./textElement";
|
|
||||||
export {
|
|
||||||
getPerfectElementSize,
|
|
||||||
getLockedLinearCursorAlignSize,
|
|
||||||
isInvisiblySmallElement,
|
|
||||||
resizePerfectLineForNWHandler,
|
|
||||||
getNormalizedDimensions,
|
|
||||||
} from "./sizeHelpers";
|
|
||||||
export { showSelectedShapeActions } from "./showSelectedShapeActions";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated unsafe, use hashElementsVersion instead
|
* @deprecated unsafe, use hashElementsVersion instead
|
||||||
*/
|
*/
|
|
@ -8,31 +8,52 @@ import {
|
||||||
pointDistance,
|
pointDistance,
|
||||||
vectorFromPoint,
|
vectorFromPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { getCurvePathOps } from "@excalidraw/utils/geometry/shape";
|
|
||||||
|
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DRAGGING_THRESHOLD,
|
||||||
|
KEYS,
|
||||||
|
shouldRotateWithDiscreteAngle,
|
||||||
|
getGridPoint,
|
||||||
|
invariant,
|
||||||
|
tupleToCoors,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
|
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
|
|
||||||
|
import type { Store } from "@excalidraw/excalidraw/store";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import { DRAGGING_THRESHOLD } from "../constants";
|
import type {
|
||||||
import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
|
AppState,
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
PointerCoords,
|
||||||
import {
|
InteractiveCanvasAppState,
|
||||||
getBezierCurveLength,
|
AppClassProperties,
|
||||||
getBezierXY,
|
NullableGridSize,
|
||||||
getControlPointsForBezierCurve,
|
Zoom,
|
||||||
isPathALoop,
|
} from "@excalidraw/excalidraw/types";
|
||||||
mapIntervalToBezierT,
|
|
||||||
} from "../shapes";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
import { getGridPoint } from "../snapping";
|
|
||||||
import { invariant, tupleToCoors } from "../utils";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { getElementPointsCoords, getMinMaxXYFromCurvePathOps } from "./bounds";
|
import {
|
||||||
|
getElementAbsoluteCoords,
|
||||||
|
getElementPointsCoords,
|
||||||
|
getMinMaxXYFromCurvePathOps,
|
||||||
|
} from "./bounds";
|
||||||
|
|
||||||
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
|
@ -40,7 +61,17 @@ import {
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
import { ShapeCache } from "./ShapeCache";
|
||||||
|
|
||||||
|
import {
|
||||||
|
isPathALoop,
|
||||||
|
getBezierCurveLength,
|
||||||
|
getControlPointsForBezierCurve,
|
||||||
|
mapIntervalToBezierT,
|
||||||
|
getBezierXY,
|
||||||
|
} from "./shapes";
|
||||||
|
|
||||||
|
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
|
@ -57,17 +88,6 @@ import type {
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type Scene from "../scene/Scene";
|
|
||||||
import type { Store } from "../store";
|
|
||||||
import type {
|
|
||||||
AppState,
|
|
||||||
PointerCoords,
|
|
||||||
InteractiveCanvasAppState,
|
|
||||||
AppClassProperties,
|
|
||||||
NullableGridSize,
|
|
||||||
Zoom,
|
|
||||||
} from "../types";
|
|
||||||
import type { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
|
@ -113,6 +133,7 @@ export class LinearElementEditor {
|
||||||
};
|
};
|
||||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||||
console.error("Linear element is not normalized", Error().stack);
|
console.error("Linear element is not normalized", Error().stack);
|
||||||
|
LinearElementEditor.normalizePoints(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedPointsIndices = null;
|
this.selectedPointsIndices = null;
|
||||||
|
@ -232,15 +253,15 @@ export class LinearElementEditor {
|
||||||
) => void,
|
) => void,
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): boolean {
|
): LinearElementEditor | null {
|
||||||
if (!linearElementEditor) {
|
if (!linearElementEditor) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
const { elementId } = linearElementEditor;
|
const { elementId } = linearElementEditor;
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -248,24 +269,18 @@ export class LinearElementEditor {
|
||||||
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
||||||
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
||||||
) {
|
) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedPointsIndices = isElbowArrow(element)
|
const selectedPointsIndices = isElbowArrow(element)
|
||||||
? linearElementEditor.selectedPointsIndices
|
? [
|
||||||
?.reduce(
|
!!linearElementEditor.selectedPointsIndices?.includes(0)
|
||||||
(startEnd, index) =>
|
? 0
|
||||||
(index === 0
|
: undefined,
|
||||||
? [0, startEnd[1]]
|
!!linearElementEditor.selectedPointsIndices?.find((idx) => idx > 0)
|
||||||
: [startEnd[0], element.points.length - 1]) as [
|
? element.points.length - 1
|
||||||
boolean | number,
|
: undefined,
|
||||||
boolean | number,
|
].filter((idx): idx is number => idx !== undefined)
|
||||||
],
|
|
||||||
[false, false] as [number | boolean, number | boolean],
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
(idx: number | boolean): idx is number => typeof idx === "number",
|
|
||||||
)
|
|
||||||
: linearElementEditor.selectedPointsIndices;
|
: linearElementEditor.selectedPointsIndices;
|
||||||
const lastClickedPoint = isElbowArrow(element)
|
const lastClickedPoint = isElbowArrow(element)
|
||||||
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
||||||
|
@ -274,9 +289,7 @@ export class LinearElementEditor {
|
||||||
: linearElementEditor.pointerDownState.lastClickedPoint;
|
: linearElementEditor.pointerDownState.lastClickedPoint;
|
||||||
|
|
||||||
// point that's being dragged (out of all selected points)
|
// point that's being dragged (out of all selected points)
|
||||||
const draggingPoint = element.points[lastClickedPoint] as
|
const draggingPoint = element.points[lastClickedPoint];
|
||||||
| [number, number]
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (selectedPointsIndices && draggingPoint) {
|
if (selectedPointsIndices && draggingPoint) {
|
||||||
if (
|
if (
|
||||||
|
@ -384,10 +397,28 @@ export class LinearElementEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return {
|
||||||
|
...linearElementEditor,
|
||||||
|
selectedPointsIndices,
|
||||||
|
segmentMidPointHoveredCoords:
|
||||||
|
lastClickedPoint !== 0 &&
|
||||||
|
lastClickedPoint !== element.points.length - 1
|
||||||
|
? this.getPointGlobalCoordinates(
|
||||||
|
element,
|
||||||
|
draggingPoint,
|
||||||
|
elementsMap,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
hoverPointIndex:
|
||||||
|
lastClickedPoint === 0 ||
|
||||||
|
lastClickedPoint === element.points.length - 1
|
||||||
|
? lastClickedPoint
|
||||||
|
: -1,
|
||||||
|
isDragging: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static handlePointerUp(
|
static handlePointerUp(
|
||||||
|
@ -1264,6 +1295,7 @@ export class LinearElementEditor {
|
||||||
startBinding?: PointBinding | null;
|
startBinding?: PointBinding | null;
|
||||||
endBinding?: PointBinding | null;
|
endBinding?: PointBinding | null;
|
||||||
},
|
},
|
||||||
|
sceneElementsMap?: NonDeletedSceneElementsMap,
|
||||||
) {
|
) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
|
@ -1307,6 +1339,7 @@ export class LinearElementEditor {
|
||||||
dragging || targetPoint.isDragging === true,
|
dragging || targetPoint.isDragging === true,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
sceneElementsMap,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1420,6 +1453,7 @@ export class LinearElementEditor {
|
||||||
options?: {
|
options?: {
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
zoom?: AppState["zoom"];
|
zoom?: AppState["zoom"];
|
||||||
|
sceneElementsMap?: NonDeletedSceneElementsMap;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
|
@ -1445,9 +1479,28 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
updates.points = Array.from(nextPoints);
|
updates.points = Array.from(nextPoints);
|
||||||
|
|
||||||
|
if (!options?.sceneElementsMap || Scene.getScene(element)) {
|
||||||
mutateElement(element, updates, true, {
|
mutateElement(element, updates, true, {
|
||||||
isDragging: options?.isDragging,
|
isDragging: options?.isDragging,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// The element is not in the scene, so we need to use the provided
|
||||||
|
// scene map.
|
||||||
|
Object.assign(element, {
|
||||||
|
...updates,
|
||||||
|
angle: 0 as Radians,
|
||||||
|
|
||||||
|
...updateElbowArrowPoints(
|
||||||
|
element,
|
||||||
|
options.sceneElementsMap,
|
||||||
|
updates,
|
||||||
|
{
|
||||||
|
isDragging: options?.isDragging,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
bumpVersion(element);
|
||||||
} else {
|
} else {
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
const prevCoords = getElementPointsCoords(element, element.points);
|
const prevCoords = getElementPointsCoords(element, element.points);
|
|
@ -1,16 +1,24 @@
|
||||||
|
import {
|
||||||
|
getSizeFromPoints,
|
||||||
|
randomInteger,
|
||||||
|
getUpdatedTimestamp,
|
||||||
|
toBrandedType,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
|
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getSizeFromPoints } from "../points";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
import { randomInteger } from "../random";
|
|
||||||
import Scene from "../scene/Scene";
|
import { ShapeCache } from "./ShapeCache";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
|
||||||
import { getUpdatedTimestamp, toBrandedType } from "../utils";
|
|
||||||
|
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
import { isElbowArrow } from "./typeChecks";
|
import { isElbowArrow } from "./typeChecks";
|
||||||
|
|
||||||
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types";
|
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types";
|
||||||
import type { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
|
@ -1,32 +1,30 @@
|
||||||
import type { Radians } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_PROPS,
|
DEFAULT_ELEMENT_PROPS,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_TEXT_ALIGN,
|
DEFAULT_TEXT_ALIGN,
|
||||||
DEFAULT_VERTICAL_ALIGN,
|
DEFAULT_VERTICAL_ALIGN,
|
||||||
ORIG_ID,
|
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
randomInteger,
|
||||||
import { getLineHeight } from "../fonts";
|
randomId,
|
||||||
import { getNewGroupIdsForDuplication } from "../groups";
|
|
||||||
import { randomInteger, randomId } from "../random";
|
|
||||||
import {
|
|
||||||
arrayToMap,
|
|
||||||
getFontString,
|
getFontString,
|
||||||
getUpdatedTimestamp,
|
getUpdatedTimestamp,
|
||||||
isTestEnv,
|
getLineHeight,
|
||||||
} from "../utils";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import type { Radians } from "@excalidraw/math";
|
||||||
import { bumpVersion, newElementWith } from "./mutateElement";
|
|
||||||
|
import type { MarkOptional, Merge } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getElementAbsoluteCoords,
|
||||||
|
getResizedElementAbsoluteCoords,
|
||||||
|
} from "./bounds";
|
||||||
|
import { newElementWith } from "./mutateElement";
|
||||||
import { getBoundTextMaxWidth } from "./textElement";
|
import { getBoundTextMaxWidth } from "./textElement";
|
||||||
import { normalizeText, measureText } from "./textMeasurements";
|
import { normalizeText, measureText } from "./textMeasurements";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from ".";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
|
@ -35,7 +33,6 @@ import type {
|
||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
GroupId,
|
|
||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
|
@ -50,8 +47,6 @@ import type {
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { AppState } from "../types";
|
|
||||||
import type { MarkOptional, Merge, Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
|
@ -538,260 +533,3 @@ export const newImageElement = (
|
||||||
crop: opts.crop ?? null,
|
crop: opts.crop ?? null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Simplified deep clone for the purpose of cloning ExcalidrawElement.
|
|
||||||
//
|
|
||||||
// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
|
|
||||||
// Typed arrays and other non-null objects.
|
|
||||||
//
|
|
||||||
// Adapted from https://github.com/lukeed/klona
|
|
||||||
//
|
|
||||||
// The reason for `deepCopyElement()` wrapper is type safety (only allow
|
|
||||||
// passing ExcalidrawElement as the top-level argument).
|
|
||||||
const _deepCopyElement = (val: any, depth: number = 0) => {
|
|
||||||
// only clone non-primitives
|
|
||||||
if (val == null || typeof val !== "object") {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
const objectType = Object.prototype.toString.call(val);
|
|
||||||
|
|
||||||
if (objectType === "[object Object]") {
|
|
||||||
const tmp =
|
|
||||||
typeof val.constructor === "function"
|
|
||||||
? Object.create(Object.getPrototypeOf(val))
|
|
||||||
: {};
|
|
||||||
for (const key in val) {
|
|
||||||
if (val.hasOwnProperty(key)) {
|
|
||||||
// don't copy non-serializable objects like these caches. They'll be
|
|
||||||
// populated when the element is rendered.
|
|
||||||
if (depth === 0 && (key === "shape" || key === "canvas")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tmp[key] = _deepCopyElement(val[key], depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
let k = val.length;
|
|
||||||
const arr = new Array(k);
|
|
||||||
while (k--) {
|
|
||||||
arr[k] = _deepCopyElement(val[k], depth + 1);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we're not cloning non-array & non-plain-object objects because we
|
|
||||||
// don't support them on excalidraw elements yet. If we do, we need to make
|
|
||||||
// sure we start cloning them, so let's warn about it.
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
if (
|
|
||||||
objectType !== "[object Object]" &&
|
|
||||||
objectType !== "[object Array]" &&
|
|
||||||
objectType.startsWith("[object ")
|
|
||||||
) {
|
|
||||||
console.warn(
|
|
||||||
`_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return val;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or
|
|
||||||
* any value. The purpose is to to break object references for immutability
|
|
||||||
* reasons, whenever we want to keep the original element, but ensure it's not
|
|
||||||
* mutated.
|
|
||||||
*
|
|
||||||
* Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
|
|
||||||
* Typed arrays and other non-null objects.
|
|
||||||
*/
|
|
||||||
export const deepCopyElement = <T extends ExcalidrawElement>(
|
|
||||||
val: T,
|
|
||||||
): Mutable<T> => {
|
|
||||||
return _deepCopyElement(val);
|
|
||||||
};
|
|
||||||
|
|
||||||
const __test__defineOrigId = (clonedObj: object, origId: string) => {
|
|
||||||
Object.defineProperty(clonedObj, ORIG_ID, {
|
|
||||||
value: origId,
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* utility wrapper to generate new id.
|
|
||||||
*/
|
|
||||||
const regenerateId = () => {
|
|
||||||
return randomId();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Duplicate an element, often used in the alt-drag operation.
|
|
||||||
* Note that this method has gotten a bit complicated since the
|
|
||||||
* introduction of gruoping/ungrouping elements.
|
|
||||||
* @param editingGroupId The current group being edited. The new
|
|
||||||
* element will inherit this group and its
|
|
||||||
* parents.
|
|
||||||
* @param groupIdMapForOperation A Map that maps old group IDs to
|
|
||||||
* duplicated ones. If you are duplicating
|
|
||||||
* multiple elements at once, share this map
|
|
||||||
* amongst all of them
|
|
||||||
* @param element Element to duplicate
|
|
||||||
* @param overrides Any element properties to override
|
|
||||||
*/
|
|
||||||
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
|
||||||
editingGroupId: AppState["editingGroupId"],
|
|
||||||
groupIdMapForOperation: Map<GroupId, GroupId>,
|
|
||||||
element: TElement,
|
|
||||||
overrides?: Partial<TElement>,
|
|
||||||
): Readonly<TElement> => {
|
|
||||||
let copy = deepCopyElement(element);
|
|
||||||
|
|
||||||
if (isTestEnv()) {
|
|
||||||
__test__defineOrigId(copy, element.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
copy.id = regenerateId();
|
|
||||||
copy.boundElements = null;
|
|
||||||
copy.updated = getUpdatedTimestamp();
|
|
||||||
copy.seed = randomInteger();
|
|
||||||
copy.groupIds = getNewGroupIdsForDuplication(
|
|
||||||
copy.groupIds,
|
|
||||||
editingGroupId,
|
|
||||||
(groupId) => {
|
|
||||||
if (!groupIdMapForOperation.has(groupId)) {
|
|
||||||
groupIdMapForOperation.set(groupId, regenerateId());
|
|
||||||
}
|
|
||||||
return groupIdMapForOperation.get(groupId)!;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (overrides) {
|
|
||||||
copy = Object.assign(copy, overrides);
|
|
||||||
}
|
|
||||||
return copy;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clones elements, regenerating their ids (including bindings) and group ids.
|
|
||||||
*
|
|
||||||
* If bindings don't exist in the elements array, they are removed. Therefore,
|
|
||||||
* it's advised to supply the whole elements array, or sets of elements that
|
|
||||||
* are encapsulated (such as library items), if the purpose is to retain
|
|
||||||
* bindings to the cloned elements intact.
|
|
||||||
*
|
|
||||||
* NOTE by default does not randomize or regenerate anything except the id.
|
|
||||||
*/
|
|
||||||
export const duplicateElements = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
opts?: {
|
|
||||||
/** NOTE also updates version flags and `updated` */
|
|
||||||
randomizeSeed: boolean;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const clonedElements: ExcalidrawElement[] = [];
|
|
||||||
|
|
||||||
const origElementsMap = arrayToMap(elements);
|
|
||||||
|
|
||||||
// used for for migrating old ids to new ids
|
|
||||||
const elementNewIdsMap = new Map<
|
|
||||||
/* orig */ ExcalidrawElement["id"],
|
|
||||||
/* new */ ExcalidrawElement["id"]
|
|
||||||
>();
|
|
||||||
|
|
||||||
const maybeGetNewId = (id: ExcalidrawElement["id"]) => {
|
|
||||||
// if we've already migrated the element id, return the new one directly
|
|
||||||
if (elementNewIdsMap.has(id)) {
|
|
||||||
return elementNewIdsMap.get(id)!;
|
|
||||||
}
|
|
||||||
// if we haven't migrated the element id, but an old element with the same
|
|
||||||
// id exists, generate a new id for it and return it
|
|
||||||
if (origElementsMap.has(id)) {
|
|
||||||
const newId = regenerateId();
|
|
||||||
elementNewIdsMap.set(id, newId);
|
|
||||||
return newId;
|
|
||||||
}
|
|
||||||
// if old element doesn't exist, return null to mark it for removal
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupNewIdsMap = new Map</* orig */ GroupId, /* new */ GroupId>();
|
|
||||||
|
|
||||||
for (const element of elements) {
|
|
||||||
const clonedElement: Mutable<ExcalidrawElement> = _deepCopyElement(element);
|
|
||||||
|
|
||||||
clonedElement.id = maybeGetNewId(element.id)!;
|
|
||||||
if (isTestEnv()) {
|
|
||||||
__test__defineOrigId(clonedElement, element.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts?.randomizeSeed) {
|
|
||||||
clonedElement.seed = randomInteger();
|
|
||||||
bumpVersion(clonedElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clonedElement.groupIds) {
|
|
||||||
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
|
|
||||||
if (!groupNewIdsMap.has(groupId)) {
|
|
||||||
groupNewIdsMap.set(groupId, regenerateId());
|
|
||||||
}
|
|
||||||
return groupNewIdsMap.get(groupId)!;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("containerId" in clonedElement && clonedElement.containerId) {
|
|
||||||
const newContainerId = maybeGetNewId(clonedElement.containerId);
|
|
||||||
clonedElement.containerId = newContainerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("boundElements" in clonedElement && clonedElement.boundElements) {
|
|
||||||
clonedElement.boundElements = clonedElement.boundElements.reduce(
|
|
||||||
(
|
|
||||||
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
|
||||||
binding,
|
|
||||||
) => {
|
|
||||||
const newBindingId = maybeGetNewId(binding.id);
|
|
||||||
if (newBindingId) {
|
|
||||||
acc.push({ ...binding, id: newBindingId });
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("endBinding" in clonedElement && clonedElement.endBinding) {
|
|
||||||
const newEndBindingId = maybeGetNewId(clonedElement.endBinding.elementId);
|
|
||||||
clonedElement.endBinding = newEndBindingId
|
|
||||||
? {
|
|
||||||
...clonedElement.endBinding,
|
|
||||||
elementId: newEndBindingId,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
if ("startBinding" in clonedElement && clonedElement.startBinding) {
|
|
||||||
const newEndBindingId = maybeGetNewId(
|
|
||||||
clonedElement.startBinding.elementId,
|
|
||||||
);
|
|
||||||
clonedElement.startBinding = newEndBindingId
|
|
||||||
? {
|
|
||||||
...clonedElement.startBinding,
|
|
||||||
elementId: newEndBindingId,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clonedElement.frameId) {
|
|
||||||
clonedElement.frameId = maybeGetNewId(clonedElement.frameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
clonedElements.push(clonedElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return clonedElements;
|
|
||||||
};
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { isRightAngleRads } from "@excalidraw/math";
|
|
||||||
import { getStroke } from "perfect-freehand";
|
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
import { getStroke } from "perfect-freehand";
|
||||||
|
|
||||||
|
import { isRightAngleRads } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getDefaultAppState } from "../appState";
|
|
||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
DEFAULT_REDUCED_GLOBAL_ALPHA,
|
DEFAULT_REDUCED_GLOBAL_ALPHA,
|
||||||
|
@ -10,18 +10,39 @@ import {
|
||||||
FRAME_STYLE,
|
FRAME_STYLE,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
THEME,
|
THEME,
|
||||||
} from "../constants";
|
distance,
|
||||||
import { getElementAbsoluteCoords } from "../element/bounds";
|
getFontString,
|
||||||
import { getUncroppedImageElement } from "../element/cropElement";
|
isRTL,
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
getVerticalOffset,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AppState,
|
||||||
|
StaticCanvasAppState,
|
||||||
|
Zoom,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
ElementsPendingErasure,
|
||||||
|
PendingExcalidrawElements,
|
||||||
|
NormalizedZoomValue,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
StaticCanvasRenderConfig,
|
||||||
|
RenderableElementsMap,
|
||||||
|
InteractiveCanvasRenderConfig,
|
||||||
|
} from "@excalidraw/excalidraw/scene/types";
|
||||||
|
|
||||||
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
|
import { getUncroppedImageElement } from "./cropElement";
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCoords,
|
getContainerCoords,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getBoundTextMaxHeight,
|
getBoundTextMaxHeight,
|
||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "../element/textElement";
|
} from "./textElement";
|
||||||
import { getLineHeightInPx } from "../element/textMeasurements";
|
import { getLineHeightInPx } from "./textMeasurements";
|
||||||
import {
|
import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
|
@ -31,12 +52,11 @@ import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isMagicFrameElement,
|
isMagicFrameElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
} from "../element/typeChecks";
|
} from "./typeChecks";
|
||||||
import { getVerticalOffset } from "../fonts";
|
import { getContainingFrame } from "./frame";
|
||||||
import { getContainingFrame } from "../frame";
|
import { getCornerRadius } from "./shapes";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
|
||||||
import { getCornerRadius } from "../shapes";
|
import { ShapeCache } from "./ShapeCache";
|
||||||
import { distance, getFontString, isRTL } from "../utils";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
@ -48,20 +68,8 @@ import type {
|
||||||
ExcalidrawFrameLikeElement,
|
ExcalidrawFrameLikeElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
} from "../element/types";
|
} from "./types";
|
||||||
import type {
|
|
||||||
StaticCanvasRenderConfig,
|
|
||||||
RenderableElementsMap,
|
|
||||||
InteractiveCanvasRenderConfig,
|
|
||||||
} from "../scene/types";
|
|
||||||
import type {
|
|
||||||
AppState,
|
|
||||||
StaticCanvasAppState,
|
|
||||||
Zoom,
|
|
||||||
InteractiveCanvasAppState,
|
|
||||||
ElementsPendingErasure,
|
|
||||||
PendingExcalidrawElements,
|
|
||||||
} from "../types";
|
|
||||||
import type { StrokeOptions } from "perfect-freehand";
|
import type { StrokeOptions } from "perfect-freehand";
|
||||||
import type { RoughCanvas } from "roughjs/bin/canvas";
|
import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
|
|
||||||
|
@ -72,8 +80,6 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
export const IMAGE_INVERT_FILTER =
|
export const IMAGE_INVERT_FILTER =
|
||||||
"invert(100%) hue-rotate(180deg) saturate(1.25)";
|
"invert(100%) hue-rotate(180deg) saturate(1.25)";
|
||||||
|
|
||||||
const defaultAppState = getDefaultAppState();
|
|
||||||
|
|
||||||
const isPendingImageElement = (
|
const isPendingImageElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
@ -533,7 +539,11 @@ const generateElementWithCanvas = (
|
||||||
renderConfig: StaticCanvasRenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const zoom: Zoom = renderConfig ? appState.zoom : defaultAppState.zoom;
|
const zoom: Zoom = renderConfig
|
||||||
|
? appState.zoom
|
||||||
|
: {
|
||||||
|
value: 1 as NormalizedZoomValue,
|
||||||
|
};
|
||||||
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
||||||
const shouldRegenerateBecauseZoom =
|
const shouldRegenerateBecauseZoom =
|
||||||
prevElementWithCanvas &&
|
prevElementWithCanvas &&
|
|
@ -8,12 +8,20 @@ import {
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MIN_FONT_SIZE,
|
||||||
|
SHIFT_LOCKING_ANGLE,
|
||||||
|
rescalePoints,
|
||||||
|
getFontString,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { GlobalPoint } from "@excalidraw/math";
|
import type { GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
|
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
import { isInGroup } from "../groups";
|
|
||||||
import { rescalePoints } from "../points";
|
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
||||||
import { getFontString } from "../utils";
|
|
||||||
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
|
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
|
||||||
import {
|
import {
|
||||||
|
@ -50,6 +58,8 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import { isInGroup } from "./groups";
|
||||||
|
|
||||||
import type { BoundingBox } from "./bounds";
|
import type { BoundingBox } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
|
@ -67,9 +77,6 @@ import type {
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type Scene from "../scene/Scene";
|
|
||||||
import type { PointerDownState } from "../types";
|
|
||||||
import type { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
// Returns true when transform (resizing/rotation) happened
|
// Returns true when transform (resizing/rotation) happened
|
||||||
export const transformElements = (
|
export const transformElements = (
|
|
@ -5,9 +5,11 @@ import {
|
||||||
type Radians,
|
type Radians,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { SIDE_RESIZING_THRESHOLD } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
|
import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { SIDE_RESIZING_THRESHOLD } from "../constants";
|
import type { AppState, Device, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
|
@ -18,7 +20,6 @@ import {
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { isImageElement, isLinearElement } from "./typeChecks";
|
import { isImageElement, isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
import type { AppState, Device, Zoom } from "../types";
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
TransformHandleType,
|
TransformHandleType,
|
|
@ -1,20 +1,25 @@
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
import { isShallowEqual } from "@excalidraw/common";
|
||||||
import { isElementInViewport } from "../element/sizeHelpers";
|
|
||||||
import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks";
|
import type {
|
||||||
|
AppState,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||||
|
import { isElementInViewport } from "./sizeHelpers";
|
||||||
|
import { isBoundToContainer, isFrameLikeElement } from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
getContainingFrame,
|
getContainingFrame,
|
||||||
getFrameChildren,
|
getFrameChildren,
|
||||||
} from "../frame";
|
} from "./frame";
|
||||||
import { isShallowEqual } from "../utils";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "./types";
|
||||||
import type { AppState, InteractiveCanvasAppState } from "../types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frames and their containing elements are not to be selected at the same time.
|
* Frames and their containing elements are not to be selected at the same time.
|
|
@ -1,3 +1,11 @@
|
||||||
|
import {
|
||||||
|
DEFAULT_ADAPTIVE_RADIUS,
|
||||||
|
DEFAULT_PROPORTIONAL_RADIUS,
|
||||||
|
LINE_CONFIRM_THRESHOLD,
|
||||||
|
ROUNDNESS,
|
||||||
|
invariant,
|
||||||
|
elementCenterPoint,
|
||||||
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
|
@ -16,131 +24,26 @@ import {
|
||||||
getFreedrawShape,
|
getFreedrawShape,
|
||||||
getPolygonShape,
|
getPolygonShape,
|
||||||
type GeometricShape,
|
type GeometricShape,
|
||||||
} from "@excalidraw/utils/geometry/shape";
|
} from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
import {
|
import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
ArrowIcon,
|
|
||||||
DiamondIcon,
|
import { shouldTestInside } from "./collision";
|
||||||
EllipseIcon,
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
EraserIcon,
|
import { getBoundTextElement } from "./textElement";
|
||||||
FreedrawIcon,
|
import { ShapeCache } from "./ShapeCache";
|
||||||
ImageIcon,
|
|
||||||
LineIcon,
|
import { getElementAbsoluteCoords, type Bounds } from "./bounds";
|
||||||
RectangleIcon,
|
|
||||||
SelectionIcon,
|
|
||||||
TextIcon,
|
|
||||||
} from "./components/icons";
|
|
||||||
import {
|
|
||||||
DEFAULT_ADAPTIVE_RADIUS,
|
|
||||||
DEFAULT_PROPORTIONAL_RADIUS,
|
|
||||||
LINE_CONFIRM_THRESHOLD,
|
|
||||||
ROUNDNESS,
|
|
||||||
} from "./constants";
|
|
||||||
import { getElementAbsoluteCoords } from "./element";
|
|
||||||
import { shouldTestInside } from "./element/collision";
|
|
||||||
import { LinearElementEditor } from "./element/linearElementEditor";
|
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
|
||||||
import { KEYS } from "./keys";
|
|
||||||
import { ShapeCache } from "./scene/ShapeCache";
|
|
||||||
import { invariant } from "./utils";
|
|
||||||
|
|
||||||
import type { Bounds } from "./element/bounds";
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "./element/types";
|
} from "./types";
|
||||||
import type { NormalizedZoomValue, Zoom } from "./types";
|
|
||||||
|
|
||||||
export const SHAPES = [
|
|
||||||
{
|
|
||||||
icon: SelectionIcon,
|
|
||||||
value: "selection",
|
|
||||||
key: KEYS.V,
|
|
||||||
numericKey: KEYS["1"],
|
|
||||||
fillable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: RectangleIcon,
|
|
||||||
value: "rectangle",
|
|
||||||
key: KEYS.R,
|
|
||||||
numericKey: KEYS["2"],
|
|
||||||
fillable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: DiamondIcon,
|
|
||||||
value: "diamond",
|
|
||||||
key: KEYS.D,
|
|
||||||
numericKey: KEYS["3"],
|
|
||||||
fillable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: EllipseIcon,
|
|
||||||
value: "ellipse",
|
|
||||||
key: KEYS.O,
|
|
||||||
numericKey: KEYS["4"],
|
|
||||||
fillable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: ArrowIcon,
|
|
||||||
value: "arrow",
|
|
||||||
key: KEYS.A,
|
|
||||||
numericKey: KEYS["5"],
|
|
||||||
fillable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: LineIcon,
|
|
||||||
value: "line",
|
|
||||||
key: KEYS.L,
|
|
||||||
numericKey: KEYS["6"],
|
|
||||||
fillable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: FreedrawIcon,
|
|
||||||
value: "freedraw",
|
|
||||||
key: [KEYS.P, KEYS.X],
|
|
||||||
numericKey: KEYS["7"],
|
|
||||||
fillable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: TextIcon,
|
|
||||||
value: "text",
|
|
||||||
key: KEYS.T,
|
|
||||||
numericKey: KEYS["8"],
|
|
||||||
fillable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: ImageIcon,
|
|
||||||
value: "image",
|
|
||||||
key: null,
|
|
||||||
numericKey: KEYS["9"],
|
|
||||||
fillable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: EraserIcon,
|
|
||||||
value: "eraser",
|
|
||||||
key: KEYS.E,
|
|
||||||
numericKey: KEYS["0"],
|
|
||||||
fillable: false,
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const findShapeByKey = (key: string) => {
|
|
||||||
const shape = SHAPES.find((shape, index) => {
|
|
||||||
return (
|
|
||||||
(shape.numericKey != null && key === shape.numericKey.toString()) ||
|
|
||||||
(shape.key &&
|
|
||||||
(typeof shape.key === "string"
|
|
||||||
? shape.key === key
|
|
||||||
: (shape.key as readonly string[]).includes(key)))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return shape?.value || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the pure geometric shape of an excalidraw element
|
* get the pure geometric shape of an excalidraw elementw
|
||||||
* which is then used for hit detection
|
* which is then used for hit detection
|
||||||
*/
|
*/
|
||||||
export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
|
@ -395,7 +298,7 @@ export const aabbForElement = (
|
||||||
midY: element.y + element.height / 2,
|
midY: element.y + element.height / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const center = pointFrom(bbox.midX, bbox.midY);
|
const center = elementCenterPoint(element);
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(bbox.minX, bbox.minY),
|
pointFrom(bbox.minX, bbox.minY),
|
||||||
center,
|
center,
|
|
@ -1,6 +1,7 @@
|
||||||
import { getSelectedElements } from "../scene";
|
import type { UIAppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import { getSelectedElements } from "./selection";
|
||||||
|
|
||||||
import type { UIAppState } from "../types";
|
|
||||||
import type { NonDeletedExcalidrawElement } from "./types";
|
import type { NonDeletedExcalidrawElement } from "./types";
|
||||||
|
|
||||||
export const showSelectedShapeActions = (
|
export const showSelectedShapeActions = (
|
||||||
|
@ -13,6 +14,7 @@ export const showSelectedShapeActions = (
|
||||||
((appState.activeTool.type !== "custom" &&
|
((appState.activeTool.type !== "custom" &&
|
||||||
(appState.editingTextElement ||
|
(appState.editingTextElement ||
|
||||||
(appState.activeTool.type !== "selection" &&
|
(appState.activeTool.type !== "selection" &&
|
||||||
|
appState.activeTool.type !== "lasso" &&
|
||||||
appState.activeTool.type !== "eraser" &&
|
appState.activeTool.type !== "eraser" &&
|
||||||
appState.activeTool.type !== "hand" &&
|
appState.activeTool.type !== "hand" &&
|
||||||
appState.activeTool.type !== "laser"))) ||
|
appState.activeTool.type !== "laser"))) ||
|
|
@ -1,12 +1,15 @@
|
||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import {
|
||||||
import { viewportCoordsToSceneCoords } from "../utils";
|
SHIFT_LOCKING_ANGLE,
|
||||||
|
viewportCoordsToSceneCoords,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||||
import type { AppState, Offsets, Zoom } from "../types";
|
|
||||||
|
|
||||||
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
|
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
|
||||||
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
|
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
|
|
@ -1,4 +1,4 @@
|
||||||
import { arrayToMapWithIndex } from "../utils";
|
import { arrayToMapWithIndex } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
|
@ -5,8 +5,12 @@ import {
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
getFontString,
|
||||||
import { getFontString, arrayToMap } from "../utils";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
|
@ -16,9 +20,11 @@ import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { measureText } from "./textMeasurements";
|
import { measureText } from "./textMeasurements";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
import {
|
||||||
|
isBoundToContainer,
|
||||||
import { isTextElement } from ".";
|
isArrowElement,
|
||||||
|
isTextElement,
|
||||||
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import type {
|
import type {
|
||||||
|
@ -30,8 +36,6 @@ import type {
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { AppState } from "../types";
|
|
||||||
import type { ExtractSetType } from "../utility-types";
|
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
|
@ -112,48 +116,6 @@ export const redrawTextBoundingBox = (
|
||||||
mutateElement(textElement, boundTextUpdates, informMutation);
|
mutateElement(textElement, boundTextUpdates, informMutation);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bindTextToShapeAfterDuplication = (
|
|
||||||
newElements: ExcalidrawElement[],
|
|
||||||
oldElements: ExcalidrawElement[],
|
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
|
||||||
): void => {
|
|
||||||
const newElementsMap = arrayToMap(newElements) as Map<
|
|
||||||
ExcalidrawElement["id"],
|
|
||||||
ExcalidrawElement
|
|
||||||
>;
|
|
||||||
oldElements.forEach((element) => {
|
|
||||||
const newElementId = oldIdToDuplicatedId.get(element.id) as string;
|
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
|
||||||
|
|
||||||
if (boundTextElementId) {
|
|
||||||
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
|
|
||||||
if (newTextElementId) {
|
|
||||||
const newContainer = newElementsMap.get(newElementId);
|
|
||||||
if (newContainer) {
|
|
||||||
mutateElement(newContainer, {
|
|
||||||
boundElements: (element.boundElements || [])
|
|
||||||
.filter(
|
|
||||||
(boundElement) =>
|
|
||||||
boundElement.id !== newTextElementId &&
|
|
||||||
boundElement.id !== boundTextElementId,
|
|
||||||
)
|
|
||||||
.concat({
|
|
||||||
type: "text",
|
|
||||||
id: newTextElementId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const newTextElement = newElementsMap.get(newTextElementId);
|
|
||||||
if (newTextElement && isTextElement(newTextElement)) {
|
|
||||||
mutateElement(newTextElement, {
|
|
||||||
containerId: newContainer ? newElementId : null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
container: NonDeletedExcalidrawElement,
|
container: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
|
@ -2,8 +2,10 @@ import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
} from "../constants";
|
getFontString,
|
||||||
import { getFontString, isTestEnv, normalizeEOL } from "../utils";
|
isTestEnv,
|
||||||
|
normalizeEOL,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { FontString, ExcalidrawTextElement } from "./types";
|
import type { FontString, ExcalidrawTextElement } from "./types";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ENV } from "../constants";
|
import { isDevEnv, isTestEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
import { charWidth, getLineWidth } from "./textMeasurements";
|
import { charWidth, getLineWidth } from "./textMeasurements";
|
||||||
|
|
||||||
|
@ -562,7 +562,7 @@ const isSingleCharacter = (maybeSingleCharacter: string) => {
|
||||||
* Invariant for the word wrapping algorithm.
|
* Invariant for the word wrapping algorithm.
|
||||||
*/
|
*/
|
||||||
const satisfiesWordInvariant = (word: string) => {
|
const satisfiesWordInvariant = (word: string) => {
|
||||||
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
|
if (isTestEnv() || isDevEnv()) {
|
||||||
if (/\s/.test(word)) {
|
if (/\s/.test(word)) {
|
||||||
throw new Error("Word should not contain any whitespaces!");
|
throw new Error("Word should not contain any whitespaces!");
|
||||||
}
|
}
|
|
@ -1,12 +1,18 @@
|
||||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
isAndroid,
|
isAndroid,
|
||||||
isIOS,
|
isIOS,
|
||||||
} from "../constants";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Device,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
Zoom,
|
||||||
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
|
@ -16,7 +22,6 @@ import {
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
|
@ -1,8 +1,9 @@
|
||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS, assertNever } from "@excalidraw/common";
|
||||||
import { assertNever } from "../utils";
|
|
||||||
|
import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
import type { MarkNonNullable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { ElementOrToolType } from "../types";
|
|
||||||
import type { MarkNonNullable } from "../utility-types";
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
|
@ -6,13 +6,14 @@ import type {
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
THEME,
|
THEME,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
MakeBrand,
|
MakeBrand,
|
||||||
MarkNonNullable,
|
MarkNonNullable,
|
||||||
Merge,
|
Merge,
|
||||||
ValueOf,
|
ValueOf,
|
||||||
} from "../utility-types";
|
} from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
|
|
@ -10,11 +10,13 @@ import {
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
import type { Curve, LineSegment } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCornerRadius } from "../shapes";
|
import { getCornerRadius } from "./shapes";
|
||||||
|
|
||||||
import { getDiamondPoints } from ".";
|
import { getDiamondPoints } from "./bounds";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
|
@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
|
||||||
return [sides, []];
|
return [sides, []];
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const r = rectangle(
|
const r = rectangle(
|
||||||
pointFrom(element.x, element.y),
|
pointFrom(element.x, element.y),
|
||||||
|
@ -254,10 +253,7 @@ export function deconstructDiamondElement(
|
||||||
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||||
pointFrom(element.x + topX, element.y + topY),
|
pointFrom(element.x + topX, element.y + topY),
|
|
@ -1,15 +1,18 @@
|
||||||
import { isFrameLikeElement } from "./element/typeChecks";
|
import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
||||||
import { syncMovedIndices } from "./fractionalIndex";
|
|
||||||
import { getElementsInGroup } from "./groups";
|
|
||||||
import { getSelectedElements } from "./scene";
|
|
||||||
import Scene from "./scene/Scene";
|
|
||||||
import { arrayToMap, findIndex, findLastIndex } from "./utils";
|
|
||||||
|
|
||||||
import type {
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawFrameLikeElement,
|
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
} from "./element/types";
|
|
||||||
import type { AppState } from "./types";
|
import { isFrameLikeElement } from "./typeChecks";
|
||||||
|
|
||||||
|
import { getElementsInGroup } from "./groups";
|
||||||
|
|
||||||
|
import { syncMovedIndices } from "./fractionalIndex";
|
||||||
|
|
||||||
|
import { getSelectedElements } from "./selection";
|
||||||
|
|
||||||
|
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
||||||
|
|
||||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||||
return element.frameId === frameId || element.id === frameId;
|
return element.frameId === frameId || element.id === frameId;
|
||||||
|
@ -79,11 +82,11 @@ const getTargetIndexAccountingForBinding = (
|
||||||
nextElement: ExcalidrawElement,
|
nextElement: ExcalidrawElement,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
if ("containerId" in nextElement && nextElement.containerId) {
|
if ("containerId" in nextElement && nextElement.containerId) {
|
||||||
const containerElement = Scene.getScene(nextElement)!.getElement(
|
// TODO: why not to get the container from the nextElements?
|
||||||
nextElement.containerId,
|
const containerElement = scene.getElement(nextElement.containerId);
|
||||||
);
|
|
||||||
if (containerElement) {
|
if (containerElement) {
|
||||||
return direction === "left"
|
return direction === "left"
|
||||||
? Math.min(
|
? Math.min(
|
||||||
|
@ -100,8 +103,7 @@ const getTargetIndexAccountingForBinding = (
|
||||||
(binding) => binding.type !== "arrow",
|
(binding) => binding.type !== "arrow",
|
||||||
)?.id;
|
)?.id;
|
||||||
if (boundElementId) {
|
if (boundElementId) {
|
||||||
const boundTextElement =
|
const boundTextElement = scene.getElement(boundElementId);
|
||||||
Scene.getScene(nextElement)!.getElement(boundElementId);
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
return direction === "left"
|
return direction === "left"
|
||||||
? Math.min(
|
? Math.min(
|
||||||
|
@ -151,6 +153,7 @@ const getTargetIndex = (
|
||||||
* If whole frame (including all children) is being moved, supply `null`.
|
* If whole frame (including all children) is being moved, supply `null`.
|
||||||
*/
|
*/
|
||||||
containingFrame: ExcalidrawFrameLikeElement["id"] | null,
|
containingFrame: ExcalidrawFrameLikeElement["id"] | null,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
const sourceElement = elements[boundaryIndex];
|
const sourceElement = elements[boundaryIndex];
|
||||||
|
|
||||||
|
@ -190,8 +193,12 @@ const getTargetIndex = (
|
||||||
sourceElement?.groupIds.join("") === nextElement?.groupIds.join("")
|
sourceElement?.groupIds.join("") === nextElement?.groupIds.join("")
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
|
getTargetIndexAccountingForBinding(
|
||||||
candidateIndex
|
nextElement,
|
||||||
|
elements,
|
||||||
|
direction,
|
||||||
|
scene,
|
||||||
|
) ?? candidateIndex
|
||||||
);
|
);
|
||||||
} else if (!nextElement?.groupIds.includes(appState.editingGroupId)) {
|
} else if (!nextElement?.groupIds.includes(appState.editingGroupId)) {
|
||||||
// candidate element is outside current editing group → prevent
|
// candidate element is outside current editing group → prevent
|
||||||
|
@ -214,8 +221,12 @@ const getTargetIndex = (
|
||||||
|
|
||||||
if (!nextElement.groupIds.length) {
|
if (!nextElement.groupIds.length) {
|
||||||
return (
|
return (
|
||||||
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
|
getTargetIndexAccountingForBinding(
|
||||||
candidateIndex
|
nextElement,
|
||||||
|
elements,
|
||||||
|
direction,
|
||||||
|
scene,
|
||||||
|
) ?? candidateIndex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,6 +266,7 @@ const shiftElementsByOne = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: "left" | "right",
|
direction: "left" | "right",
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
const indicesToMove = getIndicesToMove(elements, appState);
|
const indicesToMove = getIndicesToMove(elements, appState);
|
||||||
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
|
||||||
|
@ -289,6 +301,7 @@ const shiftElementsByOne = (
|
||||||
boundaryIndex,
|
boundaryIndex,
|
||||||
direction,
|
direction,
|
||||||
containingFrame,
|
containingFrame,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (targetIndex === -1 || boundaryIndex === targetIndex) {
|
if (targetIndex === -1 || boundaryIndex === targetIndex) {
|
||||||
|
@ -502,15 +515,17 @@ function shiftElementsAccountingForFrames(
|
||||||
export const moveOneLeft = (
|
export const moveOneLeft = (
|
||||||
allElements: readonly ExcalidrawElement[],
|
allElements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
return shiftElementsByOne(allElements, appState, "left");
|
return shiftElementsByOne(allElements, appState, "left", scene);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moveOneRight = (
|
export const moveOneRight = (
|
||||||
allElements: readonly ExcalidrawElement[],
|
allElements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
return shiftElementsByOne(allElements, appState, "right");
|
return shiftElementsByOne(allElements, appState, "right", scene);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moveAllLeft = (
|
export const moveAllLeft = (
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import { KEYS } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
actionAlignVerticallyCentered,
|
actionAlignVerticallyCentered,
|
||||||
|
@ -8,14 +8,17 @@ import {
|
||||||
actionAlignBottom,
|
actionAlignBottom,
|
||||||
actionAlignLeft,
|
actionAlignLeft,
|
||||||
actionAlignRight,
|
actionAlignRight,
|
||||||
} from "../actions";
|
} from "@excalidraw/excalidraw/actions";
|
||||||
import { defaultLang, setLanguage } from "../i18n";
|
import { defaultLang, setLanguage } from "@excalidraw/excalidraw/i18n";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import { KEYS } from "../keys";
|
|
||||||
|
|
||||||
import { API } from "./helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
import { act, unmountComponent, render } from "./test-utils";
|
import {
|
||||||
|
act,
|
||||||
|
unmountComponent,
|
||||||
|
render,
|
||||||
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
|
import { KEYS, arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "@excalidraw/excalidraw/actions/actionBoundText";
|
||||||
import { getTransformHandles } from "../element/transformHandles";
|
|
||||||
import { Excalidraw, isLinearElement } from "../index";
|
|
||||||
import { KEYS } from "../keys";
|
|
||||||
import { arrayToMap } from "../utils";
|
|
||||||
|
|
||||||
import { API } from "./helpers/api";
|
import { Excalidraw, isLinearElement } from "@excalidraw/excalidraw";
|
||||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
|
||||||
import { fireEvent, render } from "./test-utils";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import { getTransformHandles } from "../src/transformHandles";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { arrayToMap, ROUNDNESS } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { ROUNDNESS } from "../constants";
|
import { getElementAbsoluteCoords, getElementBounds } from "../src/bounds";
|
||||||
import { arrayToMap } from "../utils";
|
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
import type { ExcalidrawElement, ExcalidrawLinearElement } from "../src/types";
|
||||||
|
|
||||||
import type { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
|
|
||||||
|
|
||||||
const _ce = ({
|
const _ce = ({
|
||||||
x,
|
x,
|
732
packages/element/tests/duplicate.test.tsx
Normal file
732
packages/element/tests/duplicate.test.tsx
Normal file
|
@ -0,0 +1,732 @@
|
||||||
|
import React from "react";
|
||||||
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import {
|
||||||
|
FONT_FAMILY,
|
||||||
|
ORIG_ID,
|
||||||
|
ROUNDNESS,
|
||||||
|
isPrimitive,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||||
|
|
||||||
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
|
import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
|
||||||
|
import {
|
||||||
|
act,
|
||||||
|
assertElements,
|
||||||
|
getCloneByOrigId,
|
||||||
|
render,
|
||||||
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { mutateElement } from "../src/mutateElement";
|
||||||
|
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
||||||
|
|
||||||
|
import type { ExcalidrawLinearElement } from "../src/types";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
const assertCloneObjects = (source: any, clone: any) => {
|
||||||
|
for (const key in clone) {
|
||||||
|
if (clone.hasOwnProperty(key) && !isPrimitive(clone[key])) {
|
||||||
|
expect(clone[key]).not.toBe(source[key]);
|
||||||
|
if (source[key]) {
|
||||||
|
assertCloneObjects(source[key], clone[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("duplicating single elements", () => {
|
||||||
|
it("clones arrow element", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
strokeColor: "#000000",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
fillStyle: "hachure",
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
element.__proto__ = { hello: "world" };
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
||||||
|
});
|
||||||
|
|
||||||
|
const copy = duplicateElement(null, new Map(), element, undefined, true);
|
||||||
|
|
||||||
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
|
// assert we clone the object's prototype
|
||||||
|
// @ts-ignore
|
||||||
|
expect(copy.__proto__).toEqual({ hello: "world" });
|
||||||
|
expect(copy.hasOwnProperty("hello")).toBe(false);
|
||||||
|
|
||||||
|
expect(copy.points).not.toBe(element.points);
|
||||||
|
expect(copy).not.toHaveProperty("shape");
|
||||||
|
expect(copy.id).not.toBe(element.id);
|
||||||
|
expect(typeof copy.id).toBe("string");
|
||||||
|
expect(copy.seed).not.toBe(element.seed);
|
||||||
|
expect(typeof copy.seed).toBe("number");
|
||||||
|
expect(copy).toEqual({
|
||||||
|
...element,
|
||||||
|
id: copy.id,
|
||||||
|
seed: copy.seed,
|
||||||
|
version: copy.version,
|
||||||
|
versionNonce: copy.versionNonce,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clones text element", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
strokeColor: "#000000",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
fillStyle: "hachure",
|
||||||
|
strokeWidth: 1,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roundness: null,
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
text: "hello",
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: FONT_FAMILY.Virgil,
|
||||||
|
textAlign: "left",
|
||||||
|
verticalAlign: "top",
|
||||||
|
});
|
||||||
|
|
||||||
|
const copy = duplicateElement(null, new Map(), element);
|
||||||
|
|
||||||
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
|
expect(copy).not.toHaveProperty("points");
|
||||||
|
expect(copy).not.toHaveProperty("shape");
|
||||||
|
expect(copy.id).not.toBe(element.id);
|
||||||
|
expect(typeof copy.id).toBe("string");
|
||||||
|
expect(typeof copy.seed).toBe("number");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("duplicating multiple elements", () => {
|
||||||
|
it("duplicateElements should clone bindings", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rectangle1",
|
||||||
|
boundElements: [
|
||||||
|
{ id: "arrow1", type: "arrow" },
|
||||||
|
{ id: "arrow2", type: "arrow" },
|
||||||
|
{ id: "text1", type: "text" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const text1 = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "text1",
|
||||||
|
containerId: "rectangle1",
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow1 = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startBinding: {
|
||||||
|
elementId: "rectangle1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow2 = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow2",
|
||||||
|
endBinding: {
|
||||||
|
elementId: "rectangle1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
|
},
|
||||||
|
boundElements: [{ id: "text2", type: "text" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const text2 = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "text2",
|
||||||
|
containerId: "arrow2",
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
||||||
|
const { newElements: clonedElements } = duplicateElements({
|
||||||
|
type: "everything",
|
||||||
|
elements: origElements,
|
||||||
|
});
|
||||||
|
|
||||||
|
// generic id in-equality checks
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
expect(origElements.map((e) => e.type)).toEqual(
|
||||||
|
clonedElements.map((e) => e.type),
|
||||||
|
);
|
||||||
|
origElements.forEach((origElement, idx) => {
|
||||||
|
const clonedElement = clonedElements[idx];
|
||||||
|
expect(origElement).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.not.stringMatching(clonedElement.id),
|
||||||
|
type: clonedElement.type,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if ("containerId" in origElement) {
|
||||||
|
expect(origElement.containerId).not.toBe(
|
||||||
|
(clonedElement as any).containerId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("endBinding" in origElement) {
|
||||||
|
if (origElement.endBinding) {
|
||||||
|
expect(origElement.endBinding.elementId).not.toBe(
|
||||||
|
(clonedElement as any).endBinding?.elementId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect((clonedElement as any).endBinding).toBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("startBinding" in origElement) {
|
||||||
|
if (origElement.startBinding) {
|
||||||
|
expect(origElement.startBinding.elementId).not.toBe(
|
||||||
|
(clonedElement as any).startBinding?.elementId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
expect((clonedElement as any).startBinding).toBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const clonedArrows = clonedElements.filter(
|
||||||
|
(e) => e.type === "arrow",
|
||||||
|
) as ExcalidrawLinearElement[];
|
||||||
|
|
||||||
|
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
|
||||||
|
clonedElements as any as typeof origElements;
|
||||||
|
|
||||||
|
expect(clonedText1.containerId).toBe(clonedRectangle.id);
|
||||||
|
expect(
|
||||||
|
clonedRectangle.boundElements!.find((e) => e.id === clonedText1.id),
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: clonedText1.id,
|
||||||
|
type: clonedText1.type,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(clonedRectangle.type).toBe("rectangle");
|
||||||
|
|
||||||
|
clonedArrows.forEach((arrow) => {
|
||||||
|
expect(
|
||||||
|
clonedRectangle.boundElements!.find((e) => e.id === arrow.id),
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: arrow.id,
|
||||||
|
type: arrow.type,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (arrow.endBinding) {
|
||||||
|
expect(arrow.endBinding.elementId).toBe(clonedRectangle.id);
|
||||||
|
}
|
||||||
|
if (arrow.startBinding) {
|
||||||
|
expect(arrow.startBinding.elementId).toBe(clonedRectangle.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(clonedArrow2.boundElements).toEqual([
|
||||||
|
{ type: "text", id: clonedArrowLabel.id },
|
||||||
|
]);
|
||||||
|
expect(clonedArrowLabel.containerId).toBe(clonedArrow2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove id references of elements that aren't found", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rectangle1",
|
||||||
|
boundElements: [
|
||||||
|
// should keep
|
||||||
|
{ id: "arrow1", type: "arrow" },
|
||||||
|
// should drop
|
||||||
|
{ id: "arrow-not-exists", type: "arrow" },
|
||||||
|
// should drop
|
||||||
|
{ id: "text-not-exists", type: "text" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow1 = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startBinding: {
|
||||||
|
elementId: "rectangle1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const text1 = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
id: "text1",
|
||||||
|
containerId: "rectangle-not-exists",
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow2 = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow2",
|
||||||
|
startBinding: {
|
||||||
|
elementId: "rectangle1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: "rectangle-not-exists",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow3 = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow3",
|
||||||
|
startBinding: {
|
||||||
|
elementId: "rectangle-not-exists",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: "rectangle1",
|
||||||
|
focus: 0.2,
|
||||||
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
||||||
|
const { newElements: clonedElements } = duplicateElements({
|
||||||
|
type: "everything",
|
||||||
|
elements: origElements,
|
||||||
|
}) as any as { newElements: typeof origElements };
|
||||||
|
|
||||||
|
const [
|
||||||
|
clonedRectangle,
|
||||||
|
clonedText1,
|
||||||
|
clonedArrow1,
|
||||||
|
clonedArrow2,
|
||||||
|
clonedArrow3,
|
||||||
|
] = clonedElements;
|
||||||
|
|
||||||
|
expect(clonedRectangle.boundElements).toEqual([
|
||||||
|
{ id: clonedArrow1.id, type: "arrow" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(clonedText1.containerId).toBe(null);
|
||||||
|
|
||||||
|
expect(clonedArrow2.startBinding).toEqual({
|
||||||
|
...arrow2.startBinding,
|
||||||
|
elementId: clonedRectangle.id,
|
||||||
|
});
|
||||||
|
expect(clonedArrow2.endBinding).toBe(null);
|
||||||
|
expect(clonedArrow3.startBinding).toBe(null);
|
||||||
|
expect(clonedArrow3.endBinding).toEqual({
|
||||||
|
...arrow3.endBinding,
|
||||||
|
elementId: clonedRectangle.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("should duplicate all group ids", () => {
|
||||||
|
it("should regenerate all group ids and keep them consistent across elements", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["g1"],
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["g2", "g1"],
|
||||||
|
});
|
||||||
|
const rectangle3 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["g2", "g1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
||||||
|
const { newElements: clonedElements } = duplicateElements({
|
||||||
|
type: "everything",
|
||||||
|
elements: origElements,
|
||||||
|
}) as any as { newElements: typeof origElements };
|
||||||
|
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
||||||
|
clonedElements;
|
||||||
|
|
||||||
|
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
||||||
|
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
|
||||||
|
expect(rectangle2.groupIds[1]).not.toBe(clonedRectangle2.groupIds[1]);
|
||||||
|
|
||||||
|
expect(clonedRectangle1.groupIds[0]).toBe(clonedRectangle2.groupIds[1]);
|
||||||
|
expect(clonedRectangle2.groupIds[0]).toBe(clonedRectangle3.groupIds[0]);
|
||||||
|
expect(clonedRectangle2.groupIds[1]).toBe(clonedRectangle3.groupIds[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep and regenerate ids of groups even if invalid", () => {
|
||||||
|
// lone element shouldn't be able to be grouped with itself,
|
||||||
|
// but hard to check against in a performant way so we ignore it
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["g1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
newElements: [clonedRectangle1],
|
||||||
|
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
||||||
|
|
||||||
|
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
||||||
|
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("duplication z-order", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplication z order with Cmd+D for the lowest z-ordered element should be +1 for the clone", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
});
|
||||||
|
const rectangle3 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 20,
|
||||||
|
y: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2, rectangle3]);
|
||||||
|
API.setSelectedElements([rectangle1]);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ id: rectangle3.id },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplication z order with Cmd+D for the highest z-ordered element should be +1 for the clone", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
});
|
||||||
|
const rectangle3 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 20,
|
||||||
|
y: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2, rectangle3]);
|
||||||
|
API.setSelectedElements([rectangle3]);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ id: rectangle3.id },
|
||||||
|
{ [ORIG_ID]: rectangle3.id, selected: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
});
|
||||||
|
const rectangle3 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 20,
|
||||||
|
y: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2, rectangle3]);
|
||||||
|
|
||||||
|
mouse.select(rectangle1);
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle1.x + 5, rectangle1.y + 5);
|
||||||
|
mouse.up(rectangle1.x + 5, rectangle1.y + 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ [ORIG_ID]: rectangle1.id },
|
||||||
|
{ id: rectangle1.id, selected: true },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ id: rectangle3.id },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplication z order with alt+drag for the highest z-ordered element should be +1 for the clone", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
});
|
||||||
|
const rectangle3 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 20,
|
||||||
|
y: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2, rectangle3]);
|
||||||
|
|
||||||
|
mouse.select(rectangle3);
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle3.x + 5, rectangle3.y + 5);
|
||||||
|
mouse.up(rectangle3.x + 5, rectangle3.y + 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ [ORIG_ID]: rectangle3.id },
|
||||||
|
{ id: rectangle3.id, selected: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
});
|
||||||
|
const rectangle3 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 20,
|
||||||
|
y: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2, rectangle3]);
|
||||||
|
|
||||||
|
mouse.select(rectangle1);
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle1.x + 5, rectangle1.y + 5);
|
||||||
|
mouse.up(rectangle1.x + 5, rectangle1.y + 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ [ORIG_ID]: rectangle1.id },
|
||||||
|
{ id: rectangle1.id, selected: true },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ id: rectangle3.id },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplication z order with alt+drag with grouped elements should consider the group together when determining z-index", () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
const rectangle3 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 20,
|
||||||
|
y: 20,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2, rectangle3]);
|
||||||
|
|
||||||
|
mouse.select(rectangle1);
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle1.x + 5, rectangle1.y + 5);
|
||||||
|
mouse.up(rectangle1.x + 15, rectangle1.y + 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ [ORIG_ID]: rectangle1.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id },
|
||||||
|
{ [ORIG_ID]: rectangle3.id },
|
||||||
|
{ id: rectangle1.id, selected: true },
|
||||||
|
{ id: rectangle2.id, selected: true },
|
||||||
|
{ id: rectangle3.id, selected: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reverse-duplicating text container (in-order)", async () => {
|
||||||
|
const [rectangle, text] = API.createTextContainer();
|
||||||
|
API.setElements([rectangle, text]);
|
||||||
|
API.setSelectedElements([rectangle, text]);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||||
|
mouse.up(rectangle.x + 15, rectangle.y + 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ [ORIG_ID]: rectangle.id },
|
||||||
|
{
|
||||||
|
[ORIG_ID]: text.id,
|
||||||
|
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||||
|
},
|
||||||
|
{ id: rectangle.id, selected: true },
|
||||||
|
{ id: text.id, containerId: rectangle.id, selected: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reverse-duplicating text container (out-of-order)", async () => {
|
||||||
|
const [rectangle, text] = API.createTextContainer();
|
||||||
|
API.setElements([text, rectangle]);
|
||||||
|
API.setSelectedElements([rectangle, text]);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||||
|
mouse.up(rectangle.x + 15, rectangle.y + 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ [ORIG_ID]: rectangle.id },
|
||||||
|
{
|
||||||
|
[ORIG_ID]: text.id,
|
||||||
|
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||||
|
},
|
||||||
|
{ id: rectangle.id, selected: true },
|
||||||
|
{ id: text.id, containerId: rectangle.id, selected: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reverse-duplicating labeled arrows (in-order)", async () => {
|
||||||
|
const [arrow, text] = API.createLabeledArrow();
|
||||||
|
|
||||||
|
API.setElements([arrow, text]);
|
||||||
|
API.setSelectedElements([arrow, text]);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||||
|
mouse.up(arrow.x + 15, arrow.y + 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ [ORIG_ID]: arrow.id },
|
||||||
|
{
|
||||||
|
[ORIG_ID]: text.id,
|
||||||
|
containerId: getCloneByOrigId(arrow.id)?.id,
|
||||||
|
},
|
||||||
|
{ id: arrow.id, selected: true },
|
||||||
|
{ id: text.id, containerId: arrow.id, selected: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reverse-duplicating labeled arrows (out-of-order)", async () => {
|
||||||
|
const [arrow, text] = API.createLabeledArrow();
|
||||||
|
|
||||||
|
API.setElements([text, arrow]);
|
||||||
|
API.setSelectedElements([arrow, text]);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||||
|
mouse.up(arrow.x + 15, arrow.y + 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ [ORIG_ID]: arrow.id },
|
||||||
|
{
|
||||||
|
[ORIG_ID]: text.id,
|
||||||
|
containerId: getCloneByOrigId(arrow.id)?.id,
|
||||||
|
},
|
||||||
|
{ id: arrow.id, selected: true },
|
||||||
|
{ id: text.id, containerId: arrow.id, selected: true },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reverse-duplicating bindable element with bound arrow should keep the arrow on the duplicate", () => {
|
||||||
|
const rect = UI.createElement("rectangle", {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
x: -100,
|
||||||
|
y: 50,
|
||||||
|
width: 95,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(5, 5);
|
||||||
|
mouse.up(15, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.h.elements).toHaveLength(3);
|
||||||
|
|
||||||
|
const newRect = window.h.elements[0];
|
||||||
|
|
||||||
|
expect(arrow.endBinding?.elementId).toBe(newRect.id);
|
||||||
|
expect(newRect.boundElements?.[0]?.id).toBe(arrow.id);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,31 +1,33 @@
|
||||||
|
import { ARROW_TYPE } from "@excalidraw/common";
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import React from "react";
|
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||||
|
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
||||||
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
||||||
|
|
||||||
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
|
||||||
import "../../utils/test-utils";
|
|
||||||
import { actionSelectAll } from "../actions";
|
|
||||||
import { actionDuplicateSelection } from "../actions/actionDuplicateSelection";
|
|
||||||
import { ARROW_TYPE } from "../constants";
|
|
||||||
import { Excalidraw, mutateElement } from "../index";
|
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { API } from "../tests/helpers/api";
|
|
||||||
import { Pointer, UI } from "../tests/helpers/ui";
|
|
||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
GlobalTestState,
|
GlobalTestState,
|
||||||
queryByTestId,
|
queryByTestId,
|
||||||
render,
|
render,
|
||||||
} from "../tests/test-utils";
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import { bindLinearElement } from "./binding";
|
import "@excalidraw/utils/test-utils";
|
||||||
|
|
||||||
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { bindLinearElement } from "../src/binding";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "../src/types";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
@ -358,7 +360,7 @@ describe("elbow arrow ui", () => {
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps arrow shape when only the bound arrow is duplicated", async () => {
|
it("changes arrow shape to unbind variant if only the connected elbow arrow is duplicated", async () => {
|
||||||
UI.createElement("rectangle", {
|
UI.createElement("rectangle", {
|
||||||
x: -150,
|
x: -150,
|
||||||
y: -150,
|
y: -150,
|
||||||
|
@ -404,8 +406,8 @@ describe("elbow arrow ui", () => {
|
||||||
expect(duplicatedArrow.elbowed).toBe(true);
|
expect(duplicatedArrow.elbowed).toBe(true);
|
||||||
expect(duplicatedArrow.points).toEqual([
|
expect(duplicatedArrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[45, 0],
|
[0, 100],
|
||||||
[45, 200],
|
[90, 100],
|
||||||
[90, 200],
|
[90, 200],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
|
@ -1,9 +1,13 @@
|
||||||
import { Excalidraw } from "../index";
|
import { KEYS, reseed } from "@excalidraw/common";
|
||||||
import { KEYS } from "../keys";
|
|
||||||
import { reseed } from "../random";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import { API } from "../tests/helpers/api";
|
|
||||||
import { UI, Keyboard, Pointer } from "../tests/helpers/ui";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { render, unmountComponent } from "../tests/test-utils";
|
import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
unmountComponent,
|
||||||
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
unmountComponent();
|
unmountComponent();
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
/* eslint-disable no-lone-blocks */
|
/* eslint-disable no-lone-blocks */
|
||||||
import { generateKeyBetween } from "fractional-indexing";
|
import { generateKeyBetween } from "fractional-indexing";
|
||||||
|
|
||||||
import { deepCopyElement } from "../element/newElement";
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
import { InvalidFractionalIndexError } from "../errors";
|
|
||||||
import {
|
import {
|
||||||
syncInvalidIndices,
|
syncInvalidIndices,
|
||||||
syncMovedIndices,
|
syncMovedIndices,
|
||||||
validateFractionalIndices,
|
validateFractionalIndices,
|
||||||
} from "../fractionalIndex";
|
} from "@excalidraw/element/fractionalIndex";
|
||||||
import { arrayToMap } from "../utils";
|
|
||||||
|
|
||||||
import { API } from "./helpers/api";
|
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||||
|
|
||||||
import type { ExcalidrawElement, FractionalIndex } from "../element/types";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
FractionalIndex,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import { InvalidFractionalIndexError } from "../src/fractionalIndex";
|
||||||
|
|
||||||
describe("sync invalid indices with array order", () => {
|
describe("sync invalid indices with array order", () => {
|
||||||
describe("should NOT sync empty array", () => {
|
describe("should NOT sync empty array", () => {
|
|
@ -1,10 +1,16 @@
|
||||||
import { API } from "./tests/helpers/api";
|
import {
|
||||||
import { Keyboard, Pointer } from "./tests/helpers/ui";
|
convertToExcalidrawElements,
|
||||||
import { getCloneByOrigId, render } from "./tests/test-utils";
|
Excalidraw,
|
||||||
|
} from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import { convertToExcalidrawElements, Excalidraw } from "./index";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
import {
|
||||||
|
getCloneByOrigId,
|
||||||
|
render,
|
||||||
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "./element/types";
|
import type { ExcalidrawElement } from "../src/types";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
|
@ -1,28 +1,33 @@
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import React from "react";
|
|
||||||
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
import {
|
||||||
|
KEYS,
|
||||||
|
getSizeFromPoints,
|
||||||
|
reseed,
|
||||||
|
arrayToMap,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
import {
|
||||||
|
render,
|
||||||
|
unmountComponent,
|
||||||
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getElementPointsCoords } from "../element/bounds";
|
import { isLinearElement } from "../src/typeChecks";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { resizeSingleElement } from "../src/resizeElements";
|
||||||
import { resizeSingleElement } from "../element/resizeElements";
|
import { LinearElementEditor } from "../src/linearElementEditor";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { getElementPointsCoords } from "../src/bounds";
|
||||||
import { Excalidraw } from "../index";
|
|
||||||
import { KEYS } from "../keys";
|
|
||||||
import { getSizeFromPoints } from "../points";
|
|
||||||
import { reseed } from "../random";
|
|
||||||
import { arrayToMap } from "../utils";
|
|
||||||
|
|
||||||
import { API } from "./helpers/api";
|
import type { Bounds } from "../src/bounds";
|
||||||
import { UI, Keyboard, Pointer } from "./helpers/ui";
|
|
||||||
import { render, unmountComponent } from "./test-utils";
|
|
||||||
|
|
||||||
import type { Bounds } from "../element/bounds";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
} from "../element/types";
|
} from "../src/types";
|
||||||
|
|
||||||
unmountComponent();
|
unmountComponent();
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue