Compare commits

...

18 commits

Author SHA1 Message Date
みけCAT
e73265c561
Merge 490c370c74 into dff69e9191 2025-04-13 18:42:43 +00:00
jhanma17dev
dff69e9191
chore: Element center point util (#9298)
All checks were successful
Tests / test (push) Successful in 5m4s
2025-04-09 17:04:51 +02:00
Ryan Di
6fc85022ae
fix: lasso selection issues (#9353)
Some checks failed
Tests / test (push) Successful in 7m13s
Auto release excalidraw next / Auto-release-excalidraw-next (push) Failing after 1m45s
Build Docker image / build-docker (push) Failing after 6s
Cancel previous runs / cancel (push) Failing after 1s
Publish Docker / publish-docker (push) Failing after 20s
New Sentry production release / sentry (push) Failing after 2m5s
* revert stroke slicing hack for knot

* fix incorrect closing of path

* nonzero enclosure

* lint
2025-04-08 00:50:52 +10:00
Márk Tolmács
e48b63a0ae
fix: Rounded diamond edge elbow arrow U route (#9349)
Some checks failed
Tests / test (push) Successful in 4m54s
Auto release excalidraw next / Auto-release-excalidraw-next (push) Successful in 2m15s
Build Docker image / build-docker (push) Failing after 8s
Cancel previous runs / cancel (push) Failing after 2s
Publish Docker / publish-docker (push) Failing after 27s
New Sentry production release / sentry (push) Failing after 2m58s
2025-04-07 10:43:07 +02:00
David Luzar
c2caf78e95
fix: deselected hit element being duplicated + incorrect re-seeding (#9333)
* fix: deselected hit element being duplicated + incorrect re-seeding

* snapshots

* Fix alt-drag binding

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Add alt-drag bound arrow test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
2025-04-07 10:41:31 +02:00
Ryan Di
ce267aa0d3
feat: lasso selection (#9169)
* lasso without 'real' shape detection

* select a single linear el

* improve ux

* feed segments to worker

* simplify path threshold adaptive to zoom

* add a tiny threshold for checks

* refactor code

* lasso tests

* fix: ts

* do not capture lasso tool

* try worker-loader in next config

* update config

* refactor

* lint

* feat: show active tool when using "more tools"

* keep lasso if selected from toolbar

* fix incorrect checks for resetting to selection

* shift for additive selection

* bound text related fixes

* lint

* keep alt toggled lasso selection if shift pressed

* fix regression

* fix 'dead' lassos

* lint

* use workerpool and polyfill

* fix worker bundled with window related code

* refactor

* add file extension for worker constructor error

* another attempt at constructor error

* attempt at build issue

* attempt with dynamic import

* test not importing from math

* narrow down imports

* Reusing existing workers infrastructure (fallback to the main thread, type-safety)

* Points on curve inside the shared chunk

* Give up on experimental code splitting

* Remove potentially unnecessary optimisation

* Removing workers as the complexit is much worse, while perf. does not seem to be much better

* fix selecting text containers and containing frames together

* render fill directly from animated trail

* do not re-render static when setting selected element ids in lasso

* remove unnecessary property

* tweak trail animation

* slice points to remove notch

* always start alt-lasso from initial point

* revert build & worker changes (unused)

* remove `lasso` from `hasStrokeColor`

* label change

* remove unused props

* remove unsafe optimization

* snaps

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Co-authored-by: Marcel Mraz <marcel@excalidraw.com>
2025-04-07 16:44:25 +10:00
Narek Malkhasyan
6e47fadb59
feat: add container to multiple text elements (#9348)
All checks were successful
Tests / test (push) Successful in 4m55s
2025-04-07 00:57:27 +02:00
Márk Tolmács
b3d5ba0567
fix: Linear element is not normalized (#9347)
All checks were successful
Tests / test (push) Successful in 5m4s
* Fix #9292
2025-04-06 13:41:11 +02:00
Panagiotis Papadopoulos
c79e892e55
chore: bump @radix-ui/react-tabs version to 1.1.3 (#9329)
All checks were successful
Tests / test (push) Successful in 6m2s
* chore: bump @radix-ui/react-tabs version to 1.1.3

bumped the version to latest stable that includes
react ^19 as peerDepenecy.
This fixes peerDependency issues, as reported in #9253

* redeploy

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-04-02 16:23:15 +02:00
David Luzar
57a9e301d4
feat: tweak color swatch, and button bgs (#9330)
* feat: tweak color swatch, and button bgs

* snapshots
2025-04-02 14:36:13 +02:00
David Luzar
7c58477382
feat: tweak properties panel styling (#9322)
All checks were successful
Tests / test (push) Successful in 4m43s
2025-03-30 19:20:13 +02:00
David Luzar
83fac6d0db
feat: tweak stats panel input styles (#9321) 2025-03-30 19:00:31 +02:00
David Luzar
f2e8404c7b
feat: allow to disable preventUnload in dev (#9319)
All checks were successful
Tests / test (push) Successful in 4m36s
* feat: allow to disable preventUnload in dev

* add template
2025-03-29 19:42:33 +01:00
David Luzar
d797c2e210
fix: strip legacy attrs on element restore (#9318) 2025-03-29 19:31:16 +01:00
Marcel Mraz
0cd5a259ae
fix: incorrect type imports (#9308)
All checks were successful
Tests / test (push) Successful in 4m39s
2025-03-27 12:00:12 +01:00
Marcel Mraz
432a46ef9e
refactor: separate elements logic into a standalone package (#9285)
Some checks failed
Auto release excalidraw next / Auto-release-excalidraw-next (push) Failing after 2m36s
Build Docker image / build-docker (push) Failing after 6s
Cancel previous runs / cancel (push) Failing after 1s
Publish Docker / publish-docker (push) Failing after 31s
New Sentry production release / sentry (push) Failing after 2m3s
2025-03-26 15:24:59 +01:00
Márk Tolmács
a18f059188
fix: Reduce allocations in collision detection (#9299)
Reduce allocations
2025-03-26 15:10:43 +01:00
みけCAT
490c370c74
add MainMenu.Separator to the documentation 2024-01-11 19:19:32 +09:00
395 changed files with 7093 additions and 3179 deletions

View file

@ -48,3 +48,6 @@ UNWEjuqNMi/lwAErS9fFa2oJlWyT8U7zzv/5kQREkxZI6y9v0AF3qcbsy2731FnD
s9ChJvOUW9toIab2gsIdrKW8ZNpu084ZFVKb6LNjvIXI1Se4oMTHeszXzNptzlot
kdxxjOoaQMAyfljFSot1F1FlU6MQlag7UnFGvFjRHN1JI5q4K+n3a67DX+TMyRqS
HQIDAQAB'
# set to true in .env.development.local to disable the prevent unload dialog
VITE_APP_DISABLE_PREVENT_UNLOAD=

View file

@ -167,3 +167,27 @@ function App() {
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `children ` | `React.ReactNode` | Yes | - | The content of the `Menu Group` |
### MainMenu.Separator
To put a horizontal line between items, you can use `MainMenu.Separator`.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Item onSelect={() => window.alert("Item1")}>
Item1
</MainMenu.Item>
<MainMenu.Separator />
<MainMenu.Item onSelect={() => window.alert("Item2")}>
Item 2
</MainMenu.Item>
</MainMenu>
</Excalidraw>
</div>
);
}
```

View file

@ -15,7 +15,8 @@
"scripts": {
"start": "vite",
"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"
}
}

View file

@ -22,13 +22,6 @@ import {
THEME,
TITLE_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,
getVersion,
getFrame,
@ -37,7 +30,13 @@ import {
resolvablePromise,
isRunningInIframe,
isDevEnv,
} from "@excalidraw/excalidraw/utils";
} 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 {
GithubIcon,
XBrandIcon,
@ -48,10 +47,10 @@ import {
share,
youtubeIcon,
} 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 { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
import { newElementWith } from "@excalidraw/element/mutateElement";
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
import clsx from "clsx";
import {
parseLibraryTokensFromUrl,
@ -64,7 +63,7 @@ import type {
FileId,
NonDeletedExcalidrawElement,
OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
} from "@excalidraw/element/types";
import type {
AppState,
ExcalidrawImperativeAPI,
@ -72,8 +71,8 @@ import type {
ExcalidrawInitialDataState,
UIAppState,
} from "@excalidraw/excalidraw/types";
import type { ResolutionType } from "@excalidraw/excalidraw/utility-types";
import type { ResolvablePromise } from "@excalidraw/excalidraw/utils";
import type { ResolutionType } from "@excalidraw/common/utility-types";
import type { ResolvablePromise } from "@excalidraw/common/utils";
import CustomStats from "./CustomStats";
import {
@ -609,7 +608,13 @@ const ExcalidrawWrapper = () => {
excalidrawAPI.getSceneElements(),
)
) {
preventUnload(event);
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
preventUnload(event);
} else {
console.warn(
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
);
}
}
};
window.addEventListener(EVENT.BEFORE_UNLOAD, unloadHandler);

View file

@ -1,11 +1,15 @@
import { Stats } from "@excalidraw/excalidraw";
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 { debounce, getVersion, nFormatter } from "@excalidraw/excalidraw/utils";
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 {

View file

@ -5,7 +5,7 @@ import { useLayoutEffect, useRef } from "react";
import type {
FileId,
OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
} from "@excalidraw/element/types";
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
import { STORAGE_KEYS } from "./app_constants";

View file

@ -6,30 +6,29 @@ import {
reconcileElements,
} from "@excalidraw/excalidraw";
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
import { APP_NAME, EVENT } from "@excalidraw/excalidraw/constants";
import { APP_NAME, EVENT } from "@excalidraw/common";
import {
IDLE_THRESHOLD,
ACTIVE_THRESHOLD,
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,
isDevEnv,
isTestEnv,
preventUnload,
resolvablePromise,
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 { PureComponent } from "react";
@ -43,7 +42,7 @@ import type {
FileId,
InitializedExcalidrawImageElement,
OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
} from "@excalidraw/element/types";
import type {
BinaryFileData,
ExcalidrawImperativeAPI,
@ -51,7 +50,7 @@ import type {
Collaborator,
Gesture,
} 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 {
@ -302,7 +301,13 @@ class Collab extends PureComponent<CollabProps, CollabState> {
// the purpose is to run in immediately after user decides to stay
this.saveCollabRoomToFirebase(syncableElements);
preventUnload(event);
if (import.meta.env.VITE_APP_DISABLE_PREVENT_UNLOAD !== "true") {
preventUnload(event);
} else {
console.warn(
"preventing unload disabled (VITE_APP_DISABLE_PREVENT_UNLOAD)",
);
}
}
});

View file

@ -1,11 +1,11 @@
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
import { trackEvent } from "@excalidraw/excalidraw/analytics";
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 type { UserIdleState } from "@excalidraw/excalidraw/constants";
import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import type { UserIdleState } from "@excalidraw/common";
import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
import type {
OnUserFollowedPayload,
SocketId,

View file

@ -6,7 +6,7 @@ import {
TTDDialog,
} from "@excalidraw/excalidraw";
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";

View file

@ -6,9 +6,9 @@ import {
import { MainMenu } from "@excalidraw/excalidraw/index";
import React from "react";
import { isDevEnv } from "@excalidraw/excalidraw/utils";
import { isDevEnv } from "@excalidraw/common";
import type { Theme } from "@excalidraw/excalidraw/element/types";
import type { Theme } from "@excalidraw/element/types";
import { LanguageList } from "../app-language/LanguageList";
import { isExcalidrawPlusSignedUser } from "../app_constants";

View file

@ -1,5 +1,5 @@
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 { WelcomeScreen } from "@excalidraw/excalidraw/index";
import React from "react";

View file

@ -8,20 +8,21 @@ import {
getNormalizedCanvasDimensions,
} from "@excalidraw/excalidraw/renderer/helpers";
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 type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
import {
isLineSegment,
type GlobalPoint,
type LineSegment,
} from "../../packages/math";
import { isCurve } from "../../packages/math/curve";
import { STORAGE_KEYS } from "../app_constants";
} from "@excalidraw/math";
import { isCurve } from "@excalidraw/math/curve";
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 = (
context: CanvasRenderingContext2D,

View file

@ -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 { Card } from "@excalidraw/excalidraw/components/Card";
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
import { MIME_TYPES, getFrame } from "@excalidraw/common";
import {
encryptData,
generateEncryptionKey,
} from "@excalidraw/excalidraw/data/encryption";
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 { getFrame } from "@excalidraw/excalidraw/utils";
import { uploadBytes, ref } from "firebase/storage";
import { nanoid } from "nanoid";
import React from "react";
import type {
FileId,
NonDeletedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
} from "@excalidraw/element/types";
import type {
AppState,
BinaryFileData,

View file

@ -1,8 +1,8 @@
import { THEME } from "@excalidraw/excalidraw/constants";
import { THEME } from "@excalidraw/common";
import oc from "open-color";
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
export const GitHubCorner = React.memo(

View file

@ -1,7 +1,7 @@
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
import { compressData } from "@excalidraw/excalidraw/data/encode";
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
import { newElementWith } from "@excalidraw/element/mutateElement";
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
import { t } from "@excalidraw/excalidraw/i18n";
import type {
@ -9,7 +9,7 @@ import type {
ExcalidrawImageElement,
FileId,
InitializedExcalidrawImageElement,
} from "@excalidraw/excalidraw/element/types";
} from "@excalidraw/element/types";
import type {
BinaryFileData,
BinaryFileMetadata,

View file

@ -14,9 +14,9 @@ import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
import {
CANVAS_SEARCH_TAB,
DEFAULT_SIDEBAR,
} from "@excalidraw/excalidraw/constants";
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element";
import { debounce } from "@excalidraw/excalidraw/utils";
debounce,
} from "@excalidraw/common";
import { clearElementsForLocalStorage } from "@excalidraw/element";
import {
createStore,
entries,
@ -29,16 +29,13 @@ import {
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
import type {
ExcalidrawElement,
FileId,
} from "@excalidraw/excalidraw/element/types";
import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
import type {
AppState,
BinaryFileData,
BinaryFiles,
} 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";

View file

@ -1,12 +1,12 @@
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 {
encryptData,
decryptData,
} from "@excalidraw/excalidraw/data/encryption";
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
import { getSceneVersion } from "@excalidraw/excalidraw/element";
import { getSceneVersion } from "@excalidraw/element";
import { initializeApp } from "firebase/app";
import {
getFirestore,
@ -22,7 +22,7 @@ import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
} from "@excalidraw/element/types";
import type {
AppState,
BinaryFileData,

View file

@ -9,26 +9,26 @@ import {
} from "@excalidraw/excalidraw/data/encryption";
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
import { restore } from "@excalidraw/excalidraw/data/restore";
import { isInvisiblySmallElement } from "@excalidraw/excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
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 { SceneBounds } from "@excalidraw/excalidraw/element/bounds";
import type { SceneBounds } from "@excalidraw/element/bounds";
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
} from "@excalidraw/element/types";
import type {
AppState,
BinaryFileData,
BinaryFiles,
SocketId,
} from "@excalidraw/excalidraw/types";
import type { MakeBrand } from "@excalidraw/excalidraw/utility-types";
import type { MakeBrand } from "@excalidraw/common/utility-types";
import {
DELETED_ELEMENT_TIMEOUT,

View file

@ -2,9 +2,9 @@ import {
clearAppStateForLocalStorage,
getDefaultAppState,
} 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 { STORAGE_KEYS } from "../app_constants";

View file

@ -15,8 +15,7 @@ import {
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
import { useI18n } from "@excalidraw/excalidraw/i18n";
import { KEYS } from "@excalidraw/excalidraw/keys";
import { getFrame } from "@excalidraw/excalidraw/utils";
import { KEYS, getFrame } from "@excalidraw/common";
import { useEffect, useRef, useState } from "react";
import { atom, useAtom, useAtomValue } from "../app-jotai";

View file

@ -3,7 +3,7 @@ import {
createRedoAction,
createUndoAction,
} 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 { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
import { vi } from "vitest";

View file

@ -1,9 +1,8 @@
import { THEME } from "@excalidraw/excalidraw";
import { EVENT } from "@excalidraw/excalidraw/constants";
import { CODES, KEYS } from "@excalidraw/excalidraw/keys";
import { EVENT, CODES, KEYS } from "@excalidraw/common";
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";

View file

@ -23,29 +23,57 @@ export default defineConfig(({ mode }) => {
envDir: "../",
resolve: {
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$/,
replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"),
replacement: path.resolve(
__dirname,
"../packages/excalidraw/index.tsx",
),
},
{
find: /^@excalidraw\/excalidraw\/(.*?)/,
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$/,
replacement: path.resolve(__dirname, "../packages/math/index.ts"),
replacement: path.resolve(__dirname, "../packages/math/src/index.ts"),
},
{
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: "/",
id:"excalidraw",
id: "excalidraw",
display: "standalone",
theme_color: "#121212",
background_color: "#ffffff",

View file

@ -4,9 +4,7 @@
"packageManager": "yarn@1.22.22",
"workspaces": [
"excalidraw-app",
"packages/excalidraw",
"packages/utils",
"packages/math",
"packages/*",
"examples/*"
],
"devDependencies": {

View file

@ -0,0 +1,3 @@
{
"extends": ["../eslintrc.base.json"]
}

19
packages/common/README.md Normal file
View 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
View file

@ -0,0 +1,3 @@
/// <reference types="vite/client" />
import "@excalidraw/excalidraw/global";
import "@excalidraw/excalidraw/css";

View 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"
}
}

View file

@ -1,4 +1,4 @@
export default class BinaryHeap<T> {
export class BinaryHeap<T> {
private content: T[] = [];
constructor(private scoreFunction: (node: T) => number) {}

View file

@ -2,6 +2,8 @@ import oc from "open-color";
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
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
source: R,

View file

@ -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 type { AppProps, AppState } from "./types";
import { COLOR_PALETTE } from "./colors";
export const isDarwin = /Mac|iPod|iPhone|iPad/.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
export const TOOL_TYPE = {
selection: "selection",
lasso: "lasso",
rectangle: "rectangle",
diamond: "diamond",
ellipse: "ellipse",

View file

@ -1,12 +1,9 @@
import {
FreedrawIcon,
FontFamilyNormalIcon,
FontFamilyHeadingIcon,
FontFamilyCodeIcon,
} from "../components/icons";
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "../constants";
import type {
ExcalidrawTextElement,
FontFamilyValues,
} from "@excalidraw/element/types";
import type { JSX } from "react";
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "./constants";
/**
* 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 */
lineHeight: number;
};
/** element to be displayed as an icon */
icon?: JSX.Element;
/** flag to indicate a deprecated font */
deprecated?: true;
/** flag to indicate a server-side only font */
@ -43,7 +38,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -374,
lineHeight: 1.25,
},
icon: FreedrawIcon,
},
[FONT_FAMILY.Nunito]: {
metrics: {
@ -52,7 +46,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -353,
lineHeight: 1.35,
},
icon: FontFamilyNormalIcon,
},
[FONT_FAMILY["Lilita One"]]: {
metrics: {
@ -61,7 +54,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -220,
lineHeight: 1.15,
},
icon: FontFamilyHeadingIcon,
},
[FONT_FAMILY["Comic Shanns"]]: {
metrics: {
@ -70,7 +62,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -250,
lineHeight: 1.25,
},
icon: FontFamilyCodeIcon,
},
[FONT_FAMILY.Virgil]: {
metrics: {
@ -79,7 +70,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -374,
lineHeight: 1.25,
},
icon: FreedrawIcon,
deprecated: true,
},
[FONT_FAMILY.Helvetica]: {
@ -89,7 +79,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -471,
lineHeight: 1.15,
},
icon: FontFamilyNormalIcon,
deprecated: true,
local: true,
},
@ -100,7 +89,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -480,
lineHeight: 1.2,
},
icon: FontFamilyCodeIcon,
deprecated: true,
},
[FONT_FAMILY["Liberation Sans"]]: {
@ -149,3 +137,34 @@ export const GOOGLE_FONTS_RANGES = {
/** local protocol to skip the local font from registering or inlining */
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"];
};

View 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";

View file

@ -4,6 +4,8 @@ import {
type LocalPoint,
} from "@excalidraw/math";
import type { NullableGridSize } from "@excalidraw/excalidraw/types";
export const getSizeFromPoints = (
points: readonly (GlobalPoint | LocalPoint)[],
) => {
@ -61,3 +63,18 @@ export const rescalePoints = <Point extends GlobalPoint | LocalPoint>(
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];
};

View 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);
});
}
}

View file

@ -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 { ResolvablePromise } from "./utils";
type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>;

View file

@ -1,6 +1,6 @@
import { sanitizeUrl } from "@braintree/sanitize-url";
import { escapeDoubleQuotes } from "../utils";
import { escapeDoubleQuotes } from "./utils";
export const normalizeLink = (link: string) => {
link = link.trim();

View file

@ -1,30 +1,34 @@
import { average } from "@excalidraw/math";
import Pool from "es6-promise-pool";
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
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 {
ExcalidrawBindableElement,
FontFamilyValues,
FontString,
} from "./element/types";
ExcalidrawElement,
} from "@excalidraw/element/types";
import type {
ActiveTool,
AppState,
ToolType,
UnsubscribeCallback,
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 { EVENT } from "./constants";
let mockDateTime: string | null = null;
export const setDateTimeForTests = (dateTime: string) => {
@ -382,7 +386,7 @@ export const updateActiveTool = (
type: ToolType;
}
| { type: "custom"; customType: string }
) & { locked?: boolean }) & {
) & { locked?: boolean; fromSelection?: boolean }) & {
lastActiveToolBeforeEraser?: ActiveTool | null;
},
): AppState["activeTool"] => {
@ -404,6 +408,7 @@ export const updateActiveTool = (
type: data.type,
customType: null,
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;
}, [] 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 = () =>
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
@ -1186,54 +1191,6 @@ export const safelyParseJSON = (json: string): Record<string, any> | 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
@ -1245,3 +1202,17 @@ export const escapeDoubleQuotes = (str: string) => {
export const castArray = <T>(value: T | T[]): T[] =>
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);
};

View file

@ -1,4 +1,4 @@
import { KEYS, matchKey } from "./keys";
import { KEYS, matchKey } from "../src/keys";
describe("key matcher", async () => {
it("should not match unexpected key", async () => {

View file

@ -1,4 +1,4 @@
import { Queue } from "./queue";
import { Queue } from "../src/queue";
describe("Queue", () => {
const calls: any[] = [];

View file

@ -1,4 +1,4 @@
import { normalizeLink } from "./url";
import { normalizeLink } from "../src/url";
describe("normalizeLink", () => {
// NOTE not an extensive XSS test suite, just to check if we're not

View file

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/types"
},
"include": ["src/**/*", "global.d.ts"],
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
}

View file

@ -0,0 +1,3 @@
{
"extends": ["../eslintrc.base.json"]
}

View 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
View file

@ -0,0 +1,3 @@
/// <reference types="vite/client" />
import "@excalidraw/excalidraw/global";
import "@excalidraw/excalidraw/css";

View 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"
}
}

View file

@ -1,21 +1,26 @@
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
import { simplify } from "points-on-curve";
import { ROUGHNESS } from "../constants";
import { getDiamondPoints, getArrowheadPoints } from "../element";
import { headingForPointIsHorizontal } from "../element/heading";
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
import type { Mutable } from "@excalidraw/common/utility-types";
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
import type { ElementShapes } from "@excalidraw/excalidraw/scene/types";
import {
isElbowArrow,
isEmbeddableElement,
isIframeElement,
isIframeLikeElement,
isLinearElement,
} from "../element/typeChecks";
import { generateFreeDrawShape } from "../renderer/renderElement";
import { getCornerRadius, isPathALoop } from "../shapes";
import { isTransparent, assertNever } from "../utils";
} from "./typeChecks";
import { getCornerRadius, isPathALoop } from "./shapes";
import { headingForPointIsHorizontal } from "./heading";
import { canChangeRoundness } from "./comparisons";
import { generateFreeDrawShape } from "./renderElement";
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
import type {
ExcalidrawElement,
@ -23,9 +28,8 @@ import type {
ExcalidrawSelectionElement,
ExcalidrawLinearElement,
Arrowhead,
} from "../element/types";
import type { EmbedsValidationStatus } from "../types";
import type { ElementShapes } from "./types";
} from "./types";
import type { Drawable, Options } from "roughjs/bin/core";
import type { RoughGenerator } from "roughjs/bin/generator";
import type { Point as RoughPoint } from "roughjs/bin/geometry";
@ -511,7 +515,10 @@ export const _generateElementShape = (
if (isPathALoop(element.points)) {
// 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][], {
...generateRoughOptions(element),
stroke: "none",

View file

@ -1,16 +1,22 @@
import { RoughGenerator } from "roughjs/bin/generator";
import { COLOR_PALETTE } from "../colors";
import { elementWithCanvasCache } from "../renderer/renderElement";
import { COLOR_PALETTE } from "@excalidraw/common";
import type {
AppState,
EmbedsValidationStatus,
} from "@excalidraw/excalidraw/types";
import type {
ElementShape,
ElementShapes,
} from "@excalidraw/excalidraw/scene/types";
import { _generateElementShape } from "./Shape";
import type {
ExcalidrawElement,
ExcalidrawSelectionElement,
} from "../element/types";
import type { AppState, EmbedsValidationStatus } from "../types";
import type { ElementShape, ElementShapes } from "./types";
import { elementWithCanvasCache } from "./renderElement";
import type { ExcalidrawElement, ExcalidrawSelectionElement } from "./types";
import type { Drawable } from "roughjs/bin/core";
export class ShapeCache {

View file

@ -1,11 +1,12 @@
import { updateBoundElements } from "./element/binding";
import { getCommonBoundingBox } from "./element/bounds";
import { mutateElement } from "./element/mutateElement";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { updateBoundElements } from "./binding";
import { getCommonBoundingBox } from "./bounds";
import { mutateElement } from "./mutateElement";
import { getMaximumGroups } from "./groups";
import type { BoundingBox } from "./element/bounds";
import type { ElementsMap, ExcalidrawElement } from "./element/types";
import type Scene from "./scene/Scene";
import type { BoundingBox } from "./bounds";
import type { ElementsMap, ExcalidrawElement } from "./types";
export interface Alignment {
position: "start" | "center" | "end";

View file

@ -1,3 +1,14 @@
import {
KEYS,
arrayToMap,
isBindingFallthroughEnabled,
tupleToCoors,
invariant,
isDevEnv,
isTestEnv,
elementCenterPoint,
} from "@excalidraw/common";
import {
lineSegment,
pointFrom,
@ -15,20 +26,16 @@ import {
lineSegmentIntersectionPoints,
PRECISION,
} from "@excalidraw/math";
import { isPointOnShape } from "@excalidraw/utils/collision";
import type { LocalPoint, Radians } from "@excalidraw/math";
import { KEYS } from "../keys";
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
import {
arrayToMap,
invariant,
isBindingFallthroughEnabled,
isDevEnv,
isTestEnv,
tupleToCoors,
} from "../utils";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import type { AppState } from "@excalidraw/excalidraw/types";
import type { Mutable } from "@excalidraw/common/utility-types";
import {
getCenterForBounds,
@ -49,6 +56,7 @@ import { getBoundTextElement, handleBindTextResize } from "./textElement";
import {
isArrowElement,
isBindableElement,
isBindingElement,
isBoundToContainer,
isElbowArrow,
isFixedPointBinding,
@ -58,10 +66,9 @@ import {
isTextElement,
} from "./typeChecks";
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
import { updateElbowArrowPoints } from "./elbowArrow";
import type { Mutable } from "../utility-types";
import type { Bounds } from "./bounds";
import type { ElementUpdate } from "./mutateElement";
import type {
@ -81,8 +88,6 @@ import type {
SceneElementsMap,
FixedPointBinding,
} from "./types";
import type Scene from "../scene/Scene";
import type { AppState } from "../types";
export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement>
@ -900,13 +905,7 @@ export const getHeadingForElbowArrowSnap = (
if (!distance) {
return vectorToHeading(
vectorFromPoint(
p,
pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2,
),
),
vectorFromPoint(p, elementCenterPoint(bindableElement)),
);
}
@ -1036,10 +1035,7 @@ export const avoidRectangularCorner = (
element: ExcalidrawBindableElement,
p: GlobalPoint,
): GlobalPoint => {
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
@ -1136,10 +1132,9 @@ export const snapToMid = (
tolerance: number = 0.05,
): GlobalPoint => {
const { x, y, width, height, angle } = element;
const center = pointFrom<GlobalPoint>(
x + width / 2 - 0.1,
y + height / 2 - 0.1,
);
const center = elementCenterPoint(element, -0.1, -0.1);
const nonRotated = pointRotateRads(p, center, -angle as Radians);
// snap-to-center point is adaptive to element size, but we don't want to go
@ -1224,10 +1219,7 @@ const updateBoundPoint = (
startOrEnd === "startBinding" ? "start" : "end",
elementsMap,
).fixedPoint;
const globalMidPoint = pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2,
);
const globalMidPoint = elementCenterPoint(bindableElement);
const global = pointFrom<GlobalPoint>(
bindableElement.x + fixedPoint[0] * bindableElement.width,
bindableElement.y + fixedPoint[1] * bindableElement.height,
@ -1271,10 +1263,7 @@ const updateBoundPoint = (
elementsMap,
);
const center = pointFrom<GlobalPoint>(
bindableElement.x + bindableElement.width / 2,
bindableElement.y + bindableElement.height / 2,
);
const center = elementCenterPoint(bindableElement);
const interceptorLength =
pointDistance(adjacentPoint, edgePointAbsolute) +
pointDistance(adjacentPoint, center) +
@ -1419,7 +1408,7 @@ const getLinearElementEdgeCoors = (
);
};
export const fixBindingsAfterDuplication = (
export const fixDuplicatedBindingsAfterDuplication = (
newElements: ExcalidrawElement[],
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
duplicatedElementsMap: NonDeletedSceneElementsMap,
@ -1490,6 +1479,196 @@ export const fixBindingsAfterDuplication = (
}
};
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 fixReversedBindingsForArrows = (
original: ExcalidrawArrowElement,
duplicate: ExcalidrawArrowElement,
originalElements: Map<string, ExcalidrawElement>,
bindingProp: "startBinding" | "endBinding",
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
elementsWithClones: ExcalidrawElement[],
) => {
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,
);
}
}
};
export const fixBindingsAfterDeletion = (
sceneElements: readonly ExcalidrawElement[],
deletedElements: readonly ExcalidrawElement[],
@ -1577,10 +1756,7 @@ const determineFocusDistance = (
// Another point on the line, in absolute coordinates (closer to element)
b: GlobalPoint,
): number => {
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
if (pointsEqual(a, b)) {
return 0;
@ -1710,10 +1886,7 @@ const determineFocusPoint = (
focus: number,
adjacentPoint: GlobalPoint,
): GlobalPoint => {
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
if (focus === 0) {
return center;
@ -2144,10 +2317,7 @@ export const getGlobalFixedPointForBindableElement = (
element.x + element.width * fixedX,
element.y + element.height * fixedY,
),
pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
),
elementCenterPoint(element),
element.angle,
);
};

View file

@ -1,3 +1,7 @@
import rough from "roughjs/bin/rough";
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
import {
degreesToRadians,
lineSegment,
@ -6,10 +10,13 @@ import {
pointFromArray,
pointRotateRads,
} 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 {
Curve,
Degrees,
GlobalPoint,
LineSegment,
@ -17,11 +24,12 @@ import type {
Radians,
} from "@excalidraw/math";
import { rescalePoints } from "../points";
import { generateRoughOptions } from "../scene/Shape";
import { ShapeCache } from "../scene/ShapeCache";
import { arrayToMap, invariant } from "../utils";
import type { AppState } from "@excalidraw/excalidraw/types";
import type { Mutable } from "@excalidraw/common/utility-types";
import { ShapeCache } from "./ShapeCache";
import { generateRoughOptions } from "./Shape";
import { LinearElementEditor } from "./linearElementEditor";
import { getBoundTextElement, getContainerElement } from "./textElement";
import {
@ -32,8 +40,13 @@ import {
isTextElement,
} from "./typeChecks";
import type { AppState } from "../types";
import type { Mutable } from "../utility-types";
import { getElementShape } from "./shapes";
import {
deconstructDiamondElement,
deconstructRectanguloidElement,
} from "./utils";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
@ -42,6 +55,8 @@ import type {
NonDeleted,
ExcalidrawTextElementWithContainer,
ElementsMap,
ExcalidrawRectanguloidElement,
ExcalidrawEllipseElement,
} from "./types";
import type { Drawable, Op } from "roughjs/bin/core";
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)
* 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 = (
element: ExcalidrawElement,
elementsMap: ElementsMap,
): LineSegment<GlobalPoint>[] => {
const shape = getElementShape(element, elementsMap);
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
);
const center = pointFrom<GlobalPoint>(cx, cy);
const center: GlobalPoint = pointFrom(cx, cy);
if (isLinearElement(element) || isFreeDrawElement(element)) {
const segments: LineSegment<GlobalPoint>[] = [];
if (shape.type === "polycurve") {
const curves = shape.data;
const points = curves
.map((curve) => pointsOnBezierCurves(curve, 10))
.flat();
let i = 0;
while (i < element.points.length - 1) {
const segments: LineSegment<GlobalPoint>[] = [];
while (i < points.length - 1) {
segments.push(
lineSegment(
pointRotateRads(
pointFrom(
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,
),
pointFrom(points[i][0], points[i][1]),
pointFrom(points[i + 1][0], points[i + 1][1]),
),
);
i++;
}
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 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, n, s, w, e] = (
const [nw, ne, sw, se, , , w, e] = (
[
[x1, y1],
[x2, y1],
@ -307,28 +354,6 @@ export const getElementLineSegments = (
] as GlobalPoint[]
).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 [
lineSegment(nw, ne),
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.
*

View file

@ -1,3 +1,4 @@
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
import {
curveIntersectLineSegment,
isPointWithinBounds,
@ -8,12 +9,14 @@ import {
pointRotateRads,
pointsEqual,
} from "@excalidraw/math";
import {
ellipse,
ellipseLineIntersectionPoints,
} from "@excalidraw/math/ellipse";
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
import { getPolygonShape } from "@excalidraw/utils/geometry/shape";
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
import type {
GlobalPoint,
@ -22,11 +25,10 @@ import type {
Polygon,
Radians,
} from "@excalidraw/math";
import type { GeometricShape } from "@excalidraw/utils/geometry/shape";
import { getBoundTextShape, isPathALoop } from "../shapes";
import { isTransparent } from "../utils";
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
import { getBoundTextShape, isPathALoop } from "./shapes";
import { getElementBounds } from "./bounds";
import {
hasBoundTextElement,
@ -47,7 +49,6 @@ import type {
ExcalidrawRectangleElement,
ExcalidrawRectanguloidElement,
} from "./types";
import type { FrameNameBounds } from "../types";
export const shouldTestInside = (element: ExcalidrawElement) => {
if (element.type === "arrow") {
@ -188,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
l: LineSegment<GlobalPoint>,
offset: number = 0,
): GlobalPoint[] => {
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
// To emulate a rotated rectangle we rotate the point in the inverse angle
// instead. It's all the same distance-wise.
const rotatedA = pointRotateRads<GlobalPoint>(
@ -209,28 +207,28 @@ const intersectRectanguloidWithLineSegment = (
const [sides, corners] = deconstructRectanguloidElement(element, offset);
return (
[
// Test intersection against the sides, keep only the valid
// intersection points and rotate them back to scene space
...sides
.map((s) =>
lineSegmentIntersectionPoints(
lineSegment<GlobalPoint>(rotatedA, rotatedB),
s,
),
)
.filter((x) => x != null)
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)),
// Test intersection against the sides, keep only the valid
// intersection points and rotate them back to scene space
sides
.map((s) =>
lineSegmentIntersectionPoints(
lineSegment<GlobalPoint>(rotatedA, rotatedB),
s,
),
)
.filter((x) => x != null)
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle))
// Test intersection against the corners which are cubic bezier curves,
// keep only the valid intersection points and rotate them back to scene
// space
...corners
.flatMap((t) =>
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
)
.filter((i) => i != null)
.map((j) => pointRotateRads(j, center, element.angle)),
]
.concat(
corners
.flatMap((t) =>
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
)
.filter((i) => i != null)
.map((j) => pointRotateRads(j, center, element.angle)),
)
// Remove duplicates
.filter(
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
@ -250,10 +248,7 @@ const intersectDiamondWithLineSegment = (
l: LineSegment<GlobalPoint>,
offset: number = 0,
): GlobalPoint[] => {
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
// Rotate the point to the inverse direction to simulate the rotated diamond
// points. It's all the same distance-wise.
@ -263,25 +258,25 @@ const intersectDiamondWithLineSegment = (
const [sides, curves] = deconstructDiamondElement(element, offset);
return (
[
...sides
.map((s) =>
lineSegmentIntersectionPoints(
lineSegment<GlobalPoint>(rotatedA, rotatedB),
s,
),
)
.filter((p): p is GlobalPoint => p != null)
// Rotate back intersection points
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)),
...curves
.flatMap((p) =>
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
)
.filter((p) => p != null)
// Rotate back intersection points
.map((p) => pointRotateRads(p, center, element.angle)),
]
sides
.map((s) =>
lineSegmentIntersectionPoints(
lineSegment<GlobalPoint>(rotatedA, rotatedB),
s,
),
)
.filter((p): p is GlobalPoint => p != null)
// Rotate back intersection points
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle))
.concat(
curves
.flatMap((p) =>
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
)
.filter((p) => p != null)
// Rotate back intersection points
.map((p) => pointRotateRads(p, center, element.angle)),
)
// Remove duplicates
.filter(
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
@ -301,10 +296,7 @@ const intersectEllipseWithLineSegment = (
l: LineSegment<GlobalPoint>,
offset: number = 0,
): GlobalPoint[] => {
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);

View file

@ -1,4 +1,4 @@
import type { ElementOrToolType } from "../types";
import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
export const hasBackground = (type: ElementOrToolType) =>
type === "rectangle" ||

View file

@ -14,6 +14,8 @@ import {
} from "@excalidraw/math";
import { type Point } from "points-on-curve";
import { elementCenterPoint } from "@excalidraw/common";
import {
getElementAbsoluteCoords,
getResizedElementAbsoluteCoords,
@ -61,7 +63,7 @@ export const cropElement = (
const rotatedPointer = pointRotateRads(
pointFrom(pointerX, pointerY),
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
elementCenterPoint(element),
-element.angle as Radians,
);

View file

@ -1,11 +1,13 @@
import {
curvePointDistance,
distanceToLineSegment,
pointFrom,
pointRotateRads,
} from "@excalidraw/math";
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
import { elementCenterPoint } from "@excalidraw/common";
import type { GlobalPoint, Radians } from "@excalidraw/math";
import {
@ -52,10 +54,7 @@ const distanceToRectanguloidElement = (
element: ExcalidrawRectanguloidElement,
p: GlobalPoint,
) => {
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
// To emulate a rotated rectangle we rotate the point in the inverse angle
// instead. It's all the same distance-wise.
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
@ -83,10 +82,7 @@ const distanceToDiamondElement = (
element: ExcalidrawDiamondElement,
p: GlobalPoint,
): number => {
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
// Rotate the point to the inverse direction to simulate the rotated diamond
// points. It's all the same distance-wise.
@ -114,10 +110,7 @@ const distanceToEllipseElement = (
element: ExcalidrawEllipseElement,
p: GlobalPoint,
): number => {
const center = pointFrom(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
return ellipseDistanceFromPoint(
// Instead of rotating the ellipse, rotate the point to the inverse angle
pointRotateRads(p, center, -element.angle as Radians),

View file

@ -1,8 +1,9 @@
import { getCommonBoundingBox } from "./element/bounds";
import { newElementWith } from "./element/mutateElement";
import { getCommonBoundingBox } from "./bounds";
import { newElementWith } from "./mutateElement";
import { getMaximumGroups } from "./groups";
import type { ElementsMap, ExcalidrawElement } from "./element/types";
import type { ElementsMap, ExcalidrawElement } from "./types";
export interface Distribution {
space: "between";

View file

@ -1,6 +1,19 @@
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
import { getGridPoint } from "../snapping";
import { getFontString } from "../utils";
import {
TEXT_AUTOWRAP_THRESHOLD,
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 { getCommonBounds } from "./bounds";
@ -17,14 +30,7 @@ import {
} from "./typeChecks";
import type { Bounds } from "./bounds";
import type { ExcalidrawElement, NonDeletedExcalidrawElement } from "./types";
import type Scene from "../scene/Scene";
import type {
AppState,
NormalizedZoomValue,
NullableGridSize,
PointerDownState,
} from "../types";
import type { ExcalidrawElement } from "./types";
export const dragSelectedElements = (
pointerDownState: PointerDownState,

View file

@ -1,24 +1,28 @@
import { ORIG_ID } from "../constants";
import {
getElementsInGroup,
getNewGroupIdsForDuplication,
getSelectedGroupForElement,
} from "../groups";
import { randomId, randomInteger } from "../random";
import {
ORIG_ID,
randomId,
randomInteger,
arrayToMap,
castArray,
findLastIndex,
getUpdatedTimestamp,
isTestEnv,
} from "../utils";
} 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";
} from "./frame";
import { normalizeElementOrder } from "./sortElements";
@ -32,10 +36,10 @@ import {
import { getBoundTextElement, getContainerElement } from "./textElement";
import { fixBindingsAfterDuplication } from "./binding";
import type { AppState } from "../types";
import type { Mutable } from "../utility-types";
import {
fixDuplicatedBindingsAfterDuplication,
fixReversedBindings,
} from "./binding";
import type {
ElementsMap,
@ -380,12 +384,20 @@ export const duplicateElements = (
// ---------------------------------------------------------------------------
fixBindingsAfterDuplication(
fixDuplicatedBindingsAfterDuplication(
newElements,
oldIdToDuplicatedId,
duplicatedElementsMap as NonDeletedSceneElementsMap,
);
if (reverseOrder) {
fixReversedBindings(
_idsOfElementsToDuplicate,
elementsWithClones,
oldIdToDuplicatedId,
);
}
bindElementsToFramesAfterDuplication(
elementsWithClones,
oldElements,

View file

@ -13,10 +13,16 @@ import {
type LocalPoint,
} from "@excalidraw/math";
import BinaryHeap from "../binaryheap";
import { getSizeFromPoints } from "../points";
import { aabbForElement, pointInsideBounds } from "../shapes";
import { invariant, isAnyTrue, isDevEnv, tupleToCoors } from "../utils";
import {
BinaryHeap,
invariant,
isAnyTrue,
tupleToCoors,
getSizeFromPoints,
isDevEnv,
} from "@excalidraw/common";
import type { AppState } from "@excalidraw/excalidraw/types";
import {
bindPointToSnapToElementOutline,
@ -47,6 +53,8 @@ import {
type SceneElementsMap,
} from "./types";
import { aabbForElement, pointInsideBounds } from "./shapes";
import type { Bounds } from "./bounds";
import type { Heading } from "./heading";
import type {
@ -57,7 +65,6 @@ import type {
FixedSegment,
NonDeletedExcalidrawElement,
} from "./types";
import type { AppState } from "../types";
type GridAddress = [number, number] & { _brand: "gridaddress" };

View file

@ -2,11 +2,12 @@
* Create and link between shapes.
*/
import { ELEMENT_LINK_KEY } from "../constants";
import { normalizeLink } from "../data/url";
import { elementsAreInSameGroup } from "../groups";
import { ELEMENT_LINK_KEY, normalizeLink } from "@excalidraw/common";
import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
import { elementsAreInSameGroup } from "./groups";
import type { AppProps, AppState } from "../types";
import type { ExcalidrawElement } from "./types";
export const defaultGetElementLinkFromSelection: Exclude<

View file

@ -1,15 +1,17 @@
import { register } from "../actions/register";
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
import { setCursorForShape } from "../cursor";
import { CaptureUpdateAction } from "../store";
import { escapeDoubleQuotes, getFontString, updateActiveTool } from "../utils";
import {
FONT_FAMILY,
VERTICAL_ALIGN,
escapeDoubleQuotes,
getFontString,
} from "@excalidraw/common";
import type { ExcalidrawProps } from "@excalidraw/excalidraw/types";
import type { MarkRequired } from "@excalidraw/common/utility-types";
import { newTextElement } from "./newElement";
import { wrapText } from "./textWrapping";
import { isIframeElement } from "./typeChecks";
import type { ExcalidrawProps } from "../types";
import type { MarkRequired } from "../utility-types";
import type {
ExcalidrawElement,
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 = (
url: string,
/** using a Set assumes it already contains normalized bare domains */

View file

@ -1,9 +1,11 @@
import { KEYS, invariant, toBrandedType } from "@excalidraw/common";
import { type GlobalPoint, pointFrom, type LocalPoint } from "@excalidraw/math";
import { elementOverlapsWithFrame, elementsAreInFrameBounds } from "../frame";
import { KEYS } from "../keys";
import { aabbForElement } from "../shapes";
import { invariant, toBrandedType } from "../utils";
import type {
AppState,
PendingExcalidrawElements,
} from "@excalidraw/excalidraw/types";
import { bindLinearElement } from "./binding";
import { updateElbowArrowPoints } from "./elbowArrow";
@ -19,6 +21,8 @@ import {
import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement";
import { newArrowElement, newElement } from "./newElement";
import { aabbForElement } from "./shapes";
import { elementsAreInFrameBounds, elementOverlapsWithFrame } from "./frame";
import {
isBindableElement,
isElbowArrow,
@ -35,8 +39,6 @@ import {
type OrderedExcalidrawElement,
} from "./types";
import type { AppState, PendingExcalidrawElements } from "../types";
type LinkDirection = "up" | "right" | "down" | "left";
const VERTICAL_OFFSET = 100;

View file

@ -1,16 +1,20 @@
import { generateNKeysBetween } from "fractional-indexing";
import { mutateElement } from "./element/mutateElement";
import { getBoundTextElement } from "./element/textElement";
import { hasBoundTextElement } from "./element/typeChecks";
import { InvalidFractionalIndexError } from "./errors";
import { arrayToMap } from "./utils";
import { arrayToMap } from "@excalidraw/common";
import { mutateElement } from "./mutateElement";
import { getBoundTextElement } from "./textElement";
import { hasBoundTextElement } from "./typeChecks";
import type {
ExcalidrawElement,
FractionalIndex,
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:

View file

@ -1,24 +1,33 @@
import { arrayToMap } from "@excalidraw/common";
import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
import {
doLineSegmentsIntersect,
elementsOverlappingBBox,
} from "@excalidraw/utils";
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
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 {
getElementLineSegments,
getCommonBounds,
getElementAbsoluteCoords,
isTextElement,
} from "./element";
import { getElementLineSegments } from "./element/bounds";
import { mutateElement } from "./element/mutateElement";
} from "./bounds";
import { mutateElement } from "./mutateElement";
import { getBoundTextElement, getContainerElement } from "./textElement";
import {
getBoundTextElement,
getContainerElement,
} from "./element/textElement";
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { arrayToMap } from "./utils";
isFrameElement,
isFrameLikeElement,
isTextElement,
} from "./typeChecks";
import type {
ElementsMap,
@ -27,14 +36,7 @@ import type {
ExcalidrawFrameLikeElement,
NonDeleted,
NonDeletedExcalidrawElement,
} from "./element/types";
import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
import type {
AppClassProperties,
AppState,
StaticCanvasAppState,
} from "./types";
import type { ReadonlySetLike } from "./utility-types";
// --------------------------- Frame State ------------------------------------
export const bindElementsToFramesAfterDuplication = (

View file

@ -1,6 +1,13 @@
import { getBoundTextElement } from "./element/textElement";
import { getSelectedElements } from "./scene";
import { makeNextSelectedElementIds } from "./scene/selection";
import type {
AppClassProperties,
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 {
GroupId,
@ -9,13 +16,7 @@ import type {
NonDeletedExcalidrawElement,
ElementsMapOrArray,
ElementsMap,
} from "./element/types";
import type {
AppClassProperties,
AppState,
InteractiveCanvasAppState,
} from "./types";
import type { Mutable } from "./utility-types";
export const selectGroup = (
groupId: GroupId,
@ -297,24 +298,6 @@ export const getSelectedGroupIdForElement = (
selectedGroupIds: { [groupId: string]: boolean },
) => 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 = (
prevGroupIds: ExcalidrawElement["groupIds"],
newGroupId: GroupId,
@ -401,3 +384,21 @@ export const elementsAreInSameGroup = (
export const isInGroup = (element: NonDeletedExcalidrawElement) => {
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;
};

View 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;

View file

@ -2,11 +2,16 @@
// 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 type { AppClassProperties, DataURL, BinaryFiles } from "../types";
import type {
ExcalidrawElement,
FileId,

View file

@ -7,56 +7,6 @@ import type {
NonDeleted,
} from "./types";
export {
newElement,
newTextElement,
refreshTextDimensions,
newLinearElement,
newArrowElement,
newImageElement,
} from "./newElement";
export { duplicateElement } from "./duplicate";
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
*/

View file

@ -8,34 +8,50 @@ import {
pointDistance,
vectorFromPoint,
} 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 { DRAGGING_THRESHOLD } from "../constants";
import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
import { ShapeCache } from "../scene/ShapeCache";
import {
getBezierCurveLength,
getBezierXY,
getControlPointsForBezierCurve,
isPathALoop,
mapIntervalToBezierT,
} from "../shapes";
import { getGridPoint } from "../snapping";
import { invariant, tupleToCoors } from "../utils";
import type {
AppState,
PointerCoords,
InteractiveCanvasAppState,
AppClassProperties,
NullableGridSize,
Zoom,
} from "@excalidraw/excalidraw/types";
import Scene from "../scene/Scene";
import type { Mutable } from "@excalidraw/common/utility-types";
import {
bindOrUnbindLinearElement,
getHoveredElementForBinding,
isBindingEnabled,
} from "./binding";
import {
getElementAbsoluteCoords,
getElementPointsCoords,
getMinMaxXYFromCurvePathOps,
} from "./bounds";
import { updateElbowArrowPoints } from "./elbowArrow";
import { getElementPointsCoords, getMinMaxXYFromCurvePathOps } from "./bounds";
import { headingIsHorizontal, vectorToHeading } from "./heading";
import { bumpVersion, mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
@ -45,7 +61,17 @@ import {
isFixedPointBinding,
} 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 {
@ -62,17 +88,6 @@ import type {
FixedSegment,
ExcalidrawElbowArrowElement,
} from "./types";
import type { Store } from "../store";
import type {
AppState,
PointerCoords,
InteractiveCanvasAppState,
AppClassProperties,
NullableGridSize,
Zoom,
} from "../types";
import type { Mutable } from "../utility-types";
const editorMidPointsCache: {
version: number | null;
@ -118,6 +133,7 @@ export class LinearElementEditor {
};
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
console.error("Linear element is not normalized", Error().stack);
LinearElementEditor.normalizePoints(element);
}
this.selectedPointsIndices = null;

View file

@ -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 { getSizeFromPoints } from "../points";
import { randomInteger } from "../random";
import Scene from "../scene/Scene";
import { ShapeCache } from "../scene/ShapeCache";
import { getUpdatedTimestamp, toBrandedType } from "../utils";
import type { Mutable } from "@excalidraw/common/utility-types";
import { ShapeCache } from "./ShapeCache";
import { updateElbowArrowPoints } from "./elbowArrow";
import { isElbowArrow } from "./typeChecks";
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types";
import type { Mutable } from "../utility-types";
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>,

View file

@ -1,5 +1,3 @@
import type { Radians } from "@excalidraw/math";
import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
@ -7,20 +5,26 @@ import {
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import { getLineHeight } from "../fonts";
import { randomInteger, randomId } from "../random";
randomInteger,
randomId,
getFontString,
getUpdatedTimestamp,
getLineHeight,
} from "@excalidraw/common";
import { getFontString, getUpdatedTimestamp } from "../utils";
import type { Radians } from "@excalidraw/math";
import { getResizedElementAbsoluteCoords } from "./bounds";
import type { MarkOptional, Merge } from "@excalidraw/common/utility-types";
import {
getElementAbsoluteCoords,
getResizedElementAbsoluteCoords,
} from "./bounds";
import { newElementWith } from "./mutateElement";
import { getBoundTextMaxWidth } from "./textElement";
import { normalizeText, measureText } from "./textMeasurements";
import { wrapText } from "./textWrapping";
import { getElementAbsoluteCoords } from ".";
import type {
ExcalidrawElement,
ExcalidrawImageElement,
@ -43,7 +47,6 @@ import type {
FixedSegment,
ExcalidrawElbowArrowElement,
} from "./types";
import type { MarkOptional, Merge } from "../utility-types";
export type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,

View file

@ -1,8 +1,8 @@
import { isRightAngleRads } from "@excalidraw/math";
import { getStroke } from "perfect-freehand";
import rough from "roughjs/bin/rough";
import { getStroke } from "perfect-freehand";
import { isRightAngleRads } from "@excalidraw/math";
import { getDefaultAppState } from "../appState";
import {
BOUND_TEXT_PADDING,
DEFAULT_REDUCED_GLOBAL_ALPHA,
@ -10,18 +10,39 @@ import {
FRAME_STYLE,
MIME_TYPES,
THEME,
} from "../constants";
import { getElementAbsoluteCoords } from "../element/bounds";
import { getUncroppedImageElement } from "../element/cropElement";
import { LinearElementEditor } from "../element/linearElementEditor";
distance,
getFontString,
isRTL,
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 {
getBoundTextElement,
getContainerCoords,
getContainerElement,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
} from "../element/textElement";
import { getLineHeightInPx } from "../element/textMeasurements";
} from "./textElement";
import { getLineHeightInPx } from "./textMeasurements";
import {
isTextElement,
isLinearElement,
@ -31,12 +52,11 @@ import {
hasBoundTextElement,
isMagicFrameElement,
isImageElement,
} from "../element/typeChecks";
import { getVerticalOffset } from "../fonts";
import { getContainingFrame } from "../frame";
import { ShapeCache } from "../scene/ShapeCache";
import { getCornerRadius } from "../shapes";
import { distance, getFontString, isRTL } from "../utils";
} from "./typeChecks";
import { getContainingFrame } from "./frame";
import { getCornerRadius } from "./shapes";
import { ShapeCache } from "./ShapeCache";
import type {
ExcalidrawElement,
@ -48,20 +68,8 @@ import type {
ExcalidrawFrameLikeElement,
NonDeletedSceneElementsMap,
ElementsMap,
} from "../element/types";
import type {
StaticCanvasRenderConfig,
RenderableElementsMap,
InteractiveCanvasRenderConfig,
} from "../scene/types";
import type {
AppState,
StaticCanvasAppState,
Zoom,
InteractiveCanvasAppState,
ElementsPendingErasure,
PendingExcalidrawElements,
} from "../types";
} from "./types";
import type { StrokeOptions } from "perfect-freehand";
import type { RoughCanvas } from "roughjs/bin/canvas";
@ -72,8 +80,6 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
export const IMAGE_INVERT_FILTER =
"invert(100%) hue-rotate(180deg) saturate(1.25)";
const defaultAppState = getDefaultAppState();
const isPendingImageElement = (
element: ExcalidrawElement,
renderConfig: StaticCanvasRenderConfig,
@ -533,7 +539,11 @@ const generateElementWithCanvas = (
renderConfig: StaticCanvasRenderConfig,
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 shouldRegenerateBecauseZoom =
prevElementWithCanvas &&

View file

@ -8,12 +8,20 @@ import {
type LocalPoint,
} from "@excalidraw/math";
import {
MIN_FONT_SIZE,
SHIFT_LOCKING_ANGLE,
rescalePoints,
getFontString,
} from "@excalidraw/common";
import type { GlobalPoint } from "@excalidraw/math";
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
import { isInGroup } from "../groups";
import { rescalePoints } from "../points";
import { getFontString } from "../utils";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import type { PointerDownState } from "@excalidraw/excalidraw/types";
import type { Mutable } from "@excalidraw/common/utility-types";
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
import {
@ -50,6 +58,8 @@ import {
isTextElement,
} from "./typeChecks";
import { isInGroup } from "./groups";
import type { BoundingBox } from "./bounds";
import type {
MaybeTransformHandleType,
@ -67,9 +77,6 @@ import type {
SceneElementsMap,
ExcalidrawElbowArrowElement,
} 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
export const transformElements = (

View file

@ -5,9 +5,11 @@ import {
type Radians,
} from "@excalidraw/math";
import { SIDE_RESIZING_THRESHOLD } from "@excalidraw/common";
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 {
@ -18,7 +20,6 @@ import {
} from "./transformHandles";
import { isImageElement, isLinearElement } from "./typeChecks";
import type { AppState, Device, Zoom } from "../types";
import type { Bounds } from "./bounds";
import type {
TransformHandleType,

View file

@ -1,20 +1,25 @@
import { getElementAbsoluteCoords, getElementBounds } from "../element";
import { isElementInViewport } from "../element/sizeHelpers";
import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks";
import { isShallowEqual } from "@excalidraw/common";
import type {
AppState,
InteractiveCanvasAppState,
} from "@excalidraw/excalidraw/types";
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
import { isElementInViewport } from "./sizeHelpers";
import { isBoundToContainer, isFrameLikeElement } from "./typeChecks";
import {
elementOverlapsWithFrame,
getContainingFrame,
getFrameChildren,
} from "../frame";
import { isShallowEqual } from "../utils";
} from "./frame";
import type {
ElementsMap,
ElementsMapOrArray,
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "../element/types";
import type { AppState, InteractiveCanvasAppState } from "../types";
} from "./types";
/**
* Frames and their containing elements are not to be selected at the same time.

View file

@ -1,3 +1,11 @@
import {
DEFAULT_ADAPTIVE_RADIUS,
DEFAULT_PROPORTIONAL_RADIUS,
LINE_CONFIRM_THRESHOLD,
ROUNDNESS,
invariant,
elementCenterPoint,
} from "@excalidraw/common";
import {
isPoint,
pointFrom,
@ -16,131 +24,26 @@ import {
getFreedrawShape,
getPolygonShape,
type GeometricShape,
} from "@excalidraw/utils/geometry/shape";
} from "@excalidraw/utils/shape";
import {
ArrowIcon,
DiamondIcon,
EllipseIcon,
EraserIcon,
FreedrawIcon,
ImageIcon,
LineIcon,
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 { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types";
import { shouldTestInside } from "./collision";
import { LinearElementEditor } from "./linearElementEditor";
import { getBoundTextElement } from "./textElement";
import { ShapeCache } from "./ShapeCache";
import { getElementAbsoluteCoords, type Bounds } from "./bounds";
import type { Bounds } from "./element/bounds";
import type {
ElementsMap,
ExcalidrawElement,
ExcalidrawLinearElement,
NonDeleted,
} from "./element/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;
};
} from "./types";
/**
* 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
*/
export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
@ -395,7 +298,7 @@ export const aabbForElement = (
midY: element.y + element.height / 2,
};
const center = pointFrom(bbox.midX, bbox.midY);
const center = elementCenterPoint(element);
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(bbox.minX, bbox.minY),
center,

View file

@ -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";
export const showSelectedShapeActions = (
@ -13,6 +14,7 @@ export const showSelectedShapeActions = (
((appState.activeTool.type !== "custom" &&
(appState.editingTextElement ||
(appState.activeTool.type !== "selection" &&
appState.activeTool.type !== "lasso" &&
appState.activeTool.type !== "eraser" &&
appState.activeTool.type !== "hand" &&
appState.activeTool.type !== "laser"))) ||

View file

@ -1,12 +1,15 @@
import { SHIFT_LOCKING_ANGLE } from "../constants";
import { viewportCoordsToSceneCoords } from "../utils";
import {
SHIFT_LOCKING_ANGLE,
viewportCoordsToSceneCoords,
} from "@excalidraw/common";
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
import { getCommonBounds, getElementBounds } from "./bounds";
import { mutateElement } from "./mutateElement";
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
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
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'

View file

@ -1,4 +1,4 @@
import { arrayToMapWithIndex } from "../utils";
import { arrayToMapWithIndex } from "@excalidraw/common";
import type { ExcalidrawElement } from "./types";

View file

@ -5,8 +5,12 @@ import {
DEFAULT_FONT_SIZE,
TEXT_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import { getFontString } from "../utils";
getFontString,
} from "@excalidraw/common";
import type { AppState } from "@excalidraw/excalidraw/types";
import type { ExtractSetType } from "@excalidraw/common/utility-types";
import {
resetOriginalContainerCache,
@ -16,9 +20,11 @@ import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement";
import { measureText } from "./textMeasurements";
import { wrapText } from "./textWrapping";
import { isBoundToContainer, isArrowElement } from "./typeChecks";
import { isTextElement } from ".";
import {
isBoundToContainer,
isArrowElement,
isTextElement,
} from "./typeChecks";
import type { MaybeTransformHandleType } from "./transformHandles";
import type {
@ -30,8 +36,6 @@ import type {
ExcalidrawTextElementWithContainer,
NonDeletedExcalidrawElement,
} from "./types";
import type { AppState } from "../types";
import type { ExtractSetType } from "../utility-types";
export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement,

View file

@ -2,8 +2,10 @@ import {
BOUND_TEXT_PADDING,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_FAMILY,
} from "../constants";
import { getFontString, isTestEnv, normalizeEOL } from "../utils";
getFontString,
isTestEnv,
normalizeEOL,
} from "@excalidraw/common";
import type { FontString, ExcalidrawTextElement } from "./types";

View file

@ -1,4 +1,4 @@
import { isDevEnv, isTestEnv } from "../utils";
import { isDevEnv, isTestEnv } from "@excalidraw/common";
import { charWidth, getLineWidth } from "./textMeasurements";

View file

@ -1,12 +1,18 @@
import { pointFrom, pointRotateRads } from "@excalidraw/math";
import type { Radians } from "@excalidraw/math";
import {
DEFAULT_TRANSFORM_HANDLE_SPACING,
isAndroid,
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 {
@ -16,7 +22,6 @@ import {
isLinearElement,
} from "./typeChecks";
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
import type { Bounds } from "./bounds";
import type {
ElementsMap,

View file

@ -1,8 +1,9 @@
import { ROUNDNESS } from "../constants";
import { assertNever } from "../utils";
import { ROUNDNESS, assertNever } from "@excalidraw/common";
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 {
ExcalidrawElement,

View file

@ -6,13 +6,14 @@ import type {
TEXT_ALIGN,
THEME,
VERTICAL_ALIGN,
} from "../constants";
} from "@excalidraw/common";
import type {
MakeBrand,
MarkNonNullable,
Merge,
ValueOf,
} from "../utility-types";
} from "@excalidraw/common/utility-types";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";

View file

@ -10,11 +10,13 @@ import {
type GlobalPoint,
} from "@excalidraw/math";
import { elementCenterPoint } from "@excalidraw/common";
import type { Curve, LineSegment } from "@excalidraw/math";
import { getCornerRadius } from "../shapes";
import { getCornerRadius } from "./shapes";
import { getDiamondPoints } from ".";
import { getDiamondPoints } from "./bounds";
import type {
ExcalidrawDiamondElement,
@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
return [sides, []];
}
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
const r = rectangle(
pointFrom(element.x, element.y),
@ -254,10 +253,7 @@ export function deconstructDiamondElement(
return [[topRight, bottomRight, bottomLeft, topLeft], []];
}
const center = pointFrom<GlobalPoint>(
element.x + element.width / 2,
element.y + element.height / 2,
);
const center = elementCenterPoint(element);
const [top, right, bottom, left]: GlobalPoint[] = [
pointFrom(element.x + topX, element.y + topY),

View file

@ -1,15 +1,18 @@
import { isFrameLikeElement } from "./element/typeChecks";
import { syncMovedIndices } from "./fractionalIndex";
import { getElementsInGroup } from "./groups";
import { getSelectedElements } from "./scene";
import Scene from "./scene/Scene";
import { arrayToMap, findIndex, findLastIndex } from "./utils";
import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
import type {
ExcalidrawElement,
ExcalidrawFrameLikeElement,
} from "./element/types";
import type { AppState } from "./types";
import type { AppState } from "@excalidraw/excalidraw/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
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) => {
return element.frameId === frameId || element.id === frameId;
@ -79,11 +82,11 @@ const getTargetIndexAccountingForBinding = (
nextElement: ExcalidrawElement,
elements: readonly ExcalidrawElement[],
direction: "left" | "right",
scene: Scene,
) => {
if ("containerId" in nextElement && nextElement.containerId) {
const containerElement = Scene.getScene(nextElement)!.getElement(
nextElement.containerId,
);
// TODO: why not to get the container from the nextElements?
const containerElement = scene.getElement(nextElement.containerId);
if (containerElement) {
return direction === "left"
? Math.min(
@ -100,8 +103,7 @@ const getTargetIndexAccountingForBinding = (
(binding) => binding.type !== "arrow",
)?.id;
if (boundElementId) {
const boundTextElement =
Scene.getScene(nextElement)!.getElement(boundElementId);
const boundTextElement = scene.getElement(boundElementId);
if (boundTextElement) {
return direction === "left"
? Math.min(
@ -151,6 +153,7 @@ const getTargetIndex = (
* If whole frame (including all children) is being moved, supply `null`.
*/
containingFrame: ExcalidrawFrameLikeElement["id"] | null,
scene: Scene,
) => {
const sourceElement = elements[boundaryIndex];
@ -190,8 +193,12 @@ const getTargetIndex = (
sourceElement?.groupIds.join("") === nextElement?.groupIds.join("")
) {
return (
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
candidateIndex
getTargetIndexAccountingForBinding(
nextElement,
elements,
direction,
scene,
) ?? candidateIndex
);
} else if (!nextElement?.groupIds.includes(appState.editingGroupId)) {
// candidate element is outside current editing group → prevent
@ -214,8 +221,12 @@ const getTargetIndex = (
if (!nextElement.groupIds.length) {
return (
getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
candidateIndex
getTargetIndexAccountingForBinding(
nextElement,
elements,
direction,
scene,
) ?? candidateIndex
);
}
@ -255,6 +266,7 @@ const shiftElementsByOne = (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
scene: Scene,
) => {
const indicesToMove = getIndicesToMove(elements, appState);
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
@ -289,6 +301,7 @@ const shiftElementsByOne = (
boundaryIndex,
direction,
containingFrame,
scene,
);
if (targetIndex === -1 || boundaryIndex === targetIndex) {
@ -502,15 +515,17 @@ function shiftElementsAccountingForFrames(
export const moveOneLeft = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
scene: Scene,
) => {
return shiftElementsByOne(allElements, appState, "left");
return shiftElementsByOne(allElements, appState, "left", scene);
};
export const moveOneRight = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
scene: Scene,
) => {
return shiftElementsByOne(allElements, appState, "right");
return shiftElementsByOne(allElements, appState, "right", scene);
};
export const moveAllLeft = (

View file

@ -1,4 +1,4 @@
import React from "react";
import { KEYS } from "@excalidraw/common";
import {
actionAlignVerticallyCentered,
@ -8,14 +8,17 @@ import {
actionAlignBottom,
actionAlignLeft,
actionAlignRight,
} from "../actions";
import { defaultLang, setLanguage } from "../i18n";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
} from "@excalidraw/excalidraw/actions";
import { defaultLang, setLanguage } from "@excalidraw/excalidraw/i18n";
import { Excalidraw } from "@excalidraw/excalidraw";
import { API } from "./helpers/api";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { act, unmountComponent, render } from "./test-utils";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
import {
act,
unmountComponent,
render,
} from "@excalidraw/excalidraw/tests/test-utils";
const mouse = new Pointer("mouse");

View file

@ -1,15 +1,16 @@
import { KEYS, arrayToMap } from "@excalidraw/common";
import { pointFrom } from "@excalidraw/math";
import React from "react";
import { actionWrapTextInContainer } from "../actions/actionBoundText";
import { getTransformHandles } from "../element/transformHandles";
import { Excalidraw, isLinearElement } from "../index";
import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
import { actionWrapTextInContainer } from "@excalidraw/excalidraw/actions/actionBoundText";
import { API } from "./helpers/api";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { fireEvent, render } from "./test-utils";
import { Excalidraw, isLinearElement } from "@excalidraw/excalidraw";
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;

View file

@ -1,13 +1,12 @@
import { pointFrom } from "@excalidraw/math";
import { arrayToMap, ROUNDNESS } from "@excalidraw/common";
import type { LocalPoint } from "@excalidraw/math";
import { ROUNDNESS } from "../constants";
import { arrayToMap } from "../utils";
import { getElementAbsoluteCoords, getElementBounds } from "../src/bounds";
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
import type { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
import type { ExcalidrawElement, ExcalidrawLinearElement } from "../src/types";
const _ce = ({
x,

View file

@ -1,28 +1,34 @@
import React from "react";
import { pointFrom } from "@excalidraw/math";
import type { LocalPoint } from "@excalidraw/math";
import {
FONT_FAMILY,
ORIG_ID,
ROUNDNESS,
isPrimitive,
} from "@excalidraw/common";
import { FONT_FAMILY, ORIG_ID, ROUNDNESS } from "../constants";
import { API } from "../tests/helpers/api";
import { isPrimitive } from "../utils";
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 "../tests/test-utils";
import { Excalidraw } from "..";
import { actionDuplicateSelection } from "../actions";
} from "@excalidraw/excalidraw/tests/test-utils";
import { Keyboard, Pointer } from "../tests/helpers/ui";
import type { LocalPoint } from "@excalidraw/math";
import { mutateElement } from "./mutateElement";
import { mutateElement } from "../src/mutateElement";
import { duplicateElement, duplicateElements } from "../src/duplicate";
import { duplicateElement, duplicateElements } from "./duplicate";
import type { ExcalidrawLinearElement } from "./types";
import type { ExcalidrawLinearElement } from "../src/types";
const { h } = window;
const mouse = new Pointer("mouse");
@ -693,4 +699,34 @@ describe("duplication z-order", () => {
{ 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);
});
});

View file

@ -1,31 +1,33 @@
import { ARROW_TYPE } from "@excalidraw/common";
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 {
act,
fireEvent,
GlobalTestState,
queryByTestId,
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 {
ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElbowArrowElement,
} from "./types";
} from "../src/types";
const { h } = window;

View file

@ -1,9 +1,13 @@
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { reseed } from "../random";
import { API } from "../tests/helpers/api";
import { UI, Keyboard, Pointer } from "../tests/helpers/ui";
import { render, unmountComponent } from "../tests/test-utils";
import { KEYS, reseed } from "@excalidraw/common";
import { Excalidraw } from "@excalidraw/excalidraw";
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";
unmountComponent();

View file

@ -1,19 +1,24 @@
/* eslint-disable no-lone-blocks */
import { generateKeyBetween } from "fractional-indexing";
import { InvalidFractionalIndexError } from "../errors";
import { arrayToMap } from "@excalidraw/common";
import {
syncInvalidIndices,
syncMovedIndices,
validateFractionalIndices,
} from "../fractionalIndex";
import { arrayToMap } from "../utils";
} from "@excalidraw/element/fractionalIndex";
import { deepCopyElement } from "../element/duplicate";
import { deepCopyElement } from "@excalidraw/element/duplicate";
import { API } from "./helpers/api";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import type { ExcalidrawElement, FractionalIndex } from "../element/types";
import type {
ExcalidrawElement,
FractionalIndex,
} from "@excalidraw/element/types";
import { InvalidFractionalIndexError } from "../src/fractionalIndex";
describe("sync invalid indices with array order", () => {
describe("should NOT sync empty array", () => {

View file

@ -1,10 +1,16 @@
import { API } from "./tests/helpers/api";
import { Keyboard, Pointer } from "./tests/helpers/ui";
import { getCloneByOrigId, render } from "./tests/test-utils";
import {
convertToExcalidrawElements,
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 mouse = new Pointer("mouse");

View file

@ -1,28 +1,33 @@
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 { getElementPointsCoords } from "../element/bounds";
import { LinearElementEditor } from "../element/linearElementEditor";
import { resizeSingleElement } from "../element/resizeElements";
import { isLinearElement } from "../element/typeChecks";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { getSizeFromPoints } from "../points";
import { reseed } from "../random";
import { arrayToMap } from "../utils";
import { isLinearElement } from "../src/typeChecks";
import { resizeSingleElement } from "../src/resizeElements";
import { LinearElementEditor } from "../src/linearElementEditor";
import { getElementPointsCoords } from "../src/bounds";
import { API } from "./helpers/api";
import { UI, Keyboard, Pointer } from "./helpers/ui";
import { render, unmountComponent } from "./test-utils";
import type { Bounds } from "../element/bounds";
import type { Bounds } from "../src/bounds";
import type {
ExcalidrawElbowArrowElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
} from "../element/types";
} from "../src/types";
unmountComponent();

Some files were not shown because too many files have changed in this diff Show more