Merge remote-tracking branch 'upstream/master' into snap-to-shape

This commit is contained in:
Mathias Krafft 2025-03-28 11:22:44 +01:00
commit ae65fbd570
No known key found for this signature in database
GPG key ID: D99E394FA2319429
490 changed files with 7224 additions and 5197 deletions

View file

@ -1,6 +1,21 @@
{ {
"extends": ["@excalidraw/eslint-config", "react-app"], "extends": ["@excalidraw/eslint-config", "react-app"],
"rules": { "rules": {
"import/order": [
"warn",
{
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
"pathGroups": [
{
"pattern": "@excalidraw/**",
"group": "external",
"position": "after"
}
],
"newlines-between": "always-and-inside-groups",
"warnOnUnassignedImports": true
}
],
"import/no-anonymous-default-export": "off", "import/no-anonymous-default-export": "off",
"no-restricted-globals": "off", "no-restricted-globals": "off",
"@typescript-eslint/consistent-type-imports": [ "@typescript-eslint/consistent-type-imports": [

View file

@ -3,7 +3,7 @@
All `props` are _optional_. All `props` are _optional_.
| Name | Type | Default | Description | | Name | Type | Default | Description |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- |
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` &#124; `null` &#124; <code>Promise<object &#124; null></code> | `null` | The initial data with which app loads. | | [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` &#124; `null` &#124; <code>Promise<object &#124; null></code> | `null` | The initial data with which app loads. |
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered | | [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode | | [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
@ -13,7 +13,7 @@ All `props` are _optional_.
| [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. | | [`onScrollChange`](#onscrollchange) | `function` | \_ | This prop if passed gets triggered when scrolling the canvas. |
| [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene | | [`onPaste`](#onpaste) | `function` | \_ | Callback to be triggered if passed when something is pasted into the scene |
| [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. | | [`onLibraryChange`](#onlibrarychange) | `function` | \_ | The callback if supplied is triggered when the library is updated and receives the library items. |
| [`generateLinkForSelection`](#generateLinkForSelection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. | | [`generateLinkForSelection`](#generatelinkforselection) | `function` | \_ | Allows you to override `url` generation when linking to Excalidraw elements. |
| [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. | | [`onLinkOpen`](#onlinkopen) | `function` | \_ | The callback if supplied is triggered when any link is opened. |
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw | | [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner | | [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | \_ | Render function that renders custom UI in top right corner |
@ -29,7 +29,7 @@ All `props` are _optional_.
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. | | [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load | | [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas | | [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation | | [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean &#124; undefined)</code> | \_ | use for custom src url validation |
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` | | [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
### Storing custom data on Excalidraw elements ### Storing custom data on Excalidraw elements

View file

@ -1,5 +1,6 @@
import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
const FeatureList = [ const FeatureList = [

View file

@ -1,5 +1,6 @@
import React from "react";
import clsx from "clsx"; import clsx from "clsx";
import React from "react";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
type FeatureItem = { type FeatureItem = {

View file

@ -1,10 +1,11 @@
import React from "react";
import clsx from "clsx";
import Layout from "@theme/Layout";
import Link from "@docusaurus/Link"; import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import styles from "./index.module.css";
import HomepageFeatures from "@site/src/components/Homepage"; import HomepageFeatures from "@site/src/components/Homepage";
import Layout from "@theme/Layout";
import clsx from "clsx";
import React from "react";
import styles from "./index.module.css";
function HomepageHeader() { function HomepageHeader() {
const { siteConfig } = useDocusaurusContext(); const { siteConfig } = useDocusaurusContext();

View file

@ -1,6 +1,6 @@
// Import the original mapper // Import the original mapper
import MDXComponents from "@theme-original/MDXComponents";
import Highlight from "@site/src/components/Highlight"; import Highlight from "@site/src/components/Highlight";
import MDXComponents from "@theme-original/MDXComponents";
export default { export default {
// Re-use the default mapping // Re-use the default mapping

View file

@ -1,5 +1,6 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Script from "next/script"; import Script from "next/script";
import "../common.scss"; import "../common.scss";
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically // Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically

View file

@ -1,10 +1,11 @@
"use client"; "use client";
import * as excalidrawLib from "@excalidraw/excalidraw"; import * as excalidrawLib from "@excalidraw/excalidraw";
import { Excalidraw } from "@excalidraw/excalidraw"; import { Excalidraw } from "@excalidraw/excalidraw";
import App from "../../with-script-in-browser/components/ExampleApp";
import "@excalidraw/excalidraw/index.css"; import "@excalidraw/excalidraw/index.css";
import App from "../../with-script-in-browser/components/ExampleApp";
const ExcalidrawWrapper: React.FC = () => { const ExcalidrawWrapper: React.FC = () => {
return ( return (
<> <>

View file

@ -1,4 +1,5 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import "../common.scss"; import "../common.scss";
// Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically // Since client components get prerenderd on server as well hence importing the excalidraw stuff dynamically

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import type * as TExcalidraw from "@excalidraw/excalidraw"; import type * as TExcalidraw from "@excalidraw/excalidraw";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types"; import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";

View file

@ -1,3 +1,4 @@
import { nanoid } from "nanoid";
import React, { import React, {
useEffect, useEffect,
useState, useState,
@ -6,13 +7,24 @@ import React, {
Children, Children,
cloneElement, cloneElement,
} from "react"; } from "react";
import ExampleSidebar from "./sidebar/ExampleSidebar";
import type * as TExcalidraw from "@excalidraw/excalidraw"; import type * as TExcalidraw from "@excalidraw/excalidraw";
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
import type {
NonDeletedExcalidrawElement,
Theme,
} from "@excalidraw/excalidraw/element/types";
import type {
AppState,
BinaryFileData,
ExcalidrawImperativeAPI,
ExcalidrawInitialDataState,
Gesture,
LibraryItems,
PointerDownState as ExcalidrawPointerDownState,
} from "@excalidraw/excalidraw/types";
import { nanoid } from "nanoid"; import initialData from "../initialData";
import type { ResolvablePromise } from "../utils";
import { import {
resolvablePromise, resolvablePromise,
distance2d, distance2d,
@ -23,25 +35,12 @@ import {
import CustomFooter from "./CustomFooter"; import CustomFooter from "./CustomFooter";
import MobileFooter from "./MobileFooter"; import MobileFooter from "./MobileFooter";
import initialData from "../initialData"; import ExampleSidebar from "./sidebar/ExampleSidebar";
import type {
AppState,
BinaryFileData,
ExcalidrawImperativeAPI,
ExcalidrawInitialDataState,
Gesture,
LibraryItems,
PointerDownState as ExcalidrawPointerDownState,
} from "@excalidraw/excalidraw/types";
import type {
NonDeletedExcalidrawElement,
Theme,
} from "@excalidraw/excalidraw/element/types";
import type { ImportedLibraryData } from "@excalidraw/excalidraw/data/types";
import "./ExampleApp.scss"; import "./ExampleApp.scss";
import type { ResolvablePromise } from "../utils";
type Comment = { type Comment = {
x: number; x: number;
y: number; y: number;

View file

@ -1,7 +1,9 @@
import React from "react"; import React from "react";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
import CustomFooter from "./CustomFooter";
import type * as TExcalidraw from "@excalidraw/excalidraw"; import type * as TExcalidraw from "@excalidraw/excalidraw";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
import CustomFooter from "./CustomFooter";
const MobileFooter = ({ const MobileFooter = ({
excalidrawAPI, excalidrawAPI,

View file

@ -1,4 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import "./ExampleSidebar.scss"; import "./ExampleSidebar.scss";
export default function Sidebar({ children }: { children: React.ReactNode }) { export default function Sidebar({ children }: { children: React.ReactNode }) {

View file

@ -1,10 +1,11 @@
import App from "./components/ExampleApp";
import React, { StrictMode } from "react"; import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import "@excalidraw/excalidraw/index.css";
import type * as TExcalidraw from "@excalidraw/excalidraw"; import type * as TExcalidraw from "@excalidraw/excalidraw";
import "@excalidraw/excalidraw/index.css"; import App from "./components/ExampleApp";
declare global { declare global {
interface Window { interface Window {

View file

@ -1,6 +1,6 @@
import { unstable_batchedUpdates } from "react-dom";
import { fileOpen as _fileOpen } from "browser-fs-access";
import { MIME_TYPES } from "@excalidraw/excalidraw"; import { MIME_TYPES } from "@excalidraw/excalidraw";
import { fileOpen as _fileOpen } from "browser-fs-access";
import { unstable_batchedUpdates } from "react-dom";
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">; type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;

View file

@ -1,24 +1,3 @@
import polyfill from "@excalidraw/excalidraw/polyfill";
import { useCallback, useEffect, useRef, useState } from "react";
import { trackEvent } from "@excalidraw/excalidraw/analytics";
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
import { TopErrorBoundary } from "./components/TopErrorBoundary";
import {
APP_NAME,
EVENT,
THEME,
TITLE_TIMEOUT,
VERSION_TIMEOUT,
} from "@excalidraw/excalidraw/constants";
import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
import type {
FileId,
NonDeletedExcalidrawElement,
OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
import { t } from "@excalidraw/excalidraw/i18n";
import { import {
Excalidraw, Excalidraw,
LiveCollaborationTrigger, LiveCollaborationTrigger,
@ -26,15 +5,23 @@ import {
CaptureUpdateAction, CaptureUpdateAction,
reconcileElements, reconcileElements,
} from "@excalidraw/excalidraw"; } from "@excalidraw/excalidraw";
import type { import { trackEvent } from "@excalidraw/excalidraw/analytics";
AppState, import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
ExcalidrawImperativeAPI,
BinaryFiles,
ExcalidrawInitialDataState,
UIAppState,
} from "@excalidraw/excalidraw/types";
import type { ResolvablePromise } from "@excalidraw/excalidraw/utils";
import { import {
CommandPalette,
DEFAULT_CATEGORIES,
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
import Trans from "@excalidraw/excalidraw/components/Trans";
import {
APP_NAME,
EVENT,
THEME,
TITLE_TIMEOUT,
VERSION_TIMEOUT,
debounce, debounce,
getVersion, getVersion,
getFrame, getFrame,
@ -42,75 +29,14 @@ import {
preventUnload, preventUnload,
resolvablePromise, resolvablePromise,
isRunningInIframe, isRunningInIframe,
} from "@excalidraw/excalidraw/utils"; isDevEnv,
import { } from "@excalidraw/common";
FIREBASE_STORAGE_PREFIXES, import polyfill from "@excalidraw/excalidraw/polyfill";
isExcalidrawPlusSignedUser, import { useCallback, useEffect, useRef, useState } from "react";
STORAGE_KEYS, import { loadFromBlob } from "@excalidraw/excalidraw/data/blob";
SYNC_BROWSER_TABS_TIMEOUT, import { useCallbackRefState } from "@excalidraw/excalidraw/hooks/useCallbackRefState";
} from "./app_constants"; import { t } from "@excalidraw/excalidraw/i18n";
import type { CollabAPI } from "./collab/Collab";
import Collab, {
collabAPIAtom,
isCollaboratingAtom,
isOfflineAtom,
} from "./collab/Collab";
import {
exportToBackend,
getCollaborationLinkData,
isCollaborationLink,
loadScene,
} from "./data";
import {
importFromLocalStorage,
importUsernameFromLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
import {
ExportToExcalidrawPlus,
exportToExcalidrawPlus,
} from "./components/ExportToExcalidrawPlus";
import { updateStaleImageStatuses } from "./data/FileManager";
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
import { loadFilesFromFirebase } from "./data/firebase";
import {
LibraryIndexedDBAdapter,
LibraryLocalStorageMigrationAdapter,
LocalData,
} from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync";
import clsx from "clsx";
import {
parseLibraryTokensFromUrl,
useHandleLibrary,
} from "@excalidraw/excalidraw/data/library";
import { AppMainMenu } from "./components/AppMainMenu";
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
import { AppFooter } from "./components/AppFooter";
import {
Provider,
useAtom,
useAtomValue,
useAtomWithInitialValue,
appJotaiStore,
} from "./app-jotai";
import "./index.scss";
import type { ResolutionType } from "@excalidraw/excalidraw/utility-types";
import { ShareableLinkDialog } from "@excalidraw/excalidraw/components/ShareableLinkDialog";
import { openConfirmModal } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirmState";
import { OverwriteConfirmDialog } from "@excalidraw/excalidraw/components/OverwriteConfirm/OverwriteConfirm";
import Trans from "@excalidraw/excalidraw/components/Trans";
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
import {
CommandPalette,
DEFAULT_CATEGORIES,
} from "@excalidraw/excalidraw/components/CommandPalette/CommandPalette";
import { import {
GithubIcon, GithubIcon,
XBrandIcon, XBrandIcon,
@ -121,6 +47,83 @@ import {
share, share,
youtubeIcon, youtubeIcon,
} from "@excalidraw/excalidraw/components/icons"; } from "@excalidraw/excalidraw/components/icons";
import { isElementLink } from "@excalidraw/element/elementLink";
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
import { newElementWith } from "@excalidraw/element/mutateElement";
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
import clsx from "clsx";
import {
parseLibraryTokensFromUrl,
useHandleLibrary,
} from "@excalidraw/excalidraw/data/library";
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
import type { RestoredDataState } from "@excalidraw/excalidraw/data/restore";
import type {
FileId,
NonDeletedExcalidrawElement,
OrderedExcalidrawElement,
} from "@excalidraw/element/types";
import type {
AppState,
ExcalidrawImperativeAPI,
BinaryFiles,
ExcalidrawInitialDataState,
UIAppState,
} from "@excalidraw/excalidraw/types";
import type { ResolutionType } from "@excalidraw/common/utility-types";
import type { ResolvablePromise } from "@excalidraw/common/utils";
import CustomStats from "./CustomStats";
import {
Provider,
useAtom,
useAtomValue,
useAtomWithInitialValue,
appJotaiStore,
} from "./app-jotai";
import {
FIREBASE_STORAGE_PREFIXES,
isExcalidrawPlusSignedUser,
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
import Collab, {
collabAPIAtom,
isCollaboratingAtom,
isOfflineAtom,
} from "./collab/Collab";
import { AppFooter } from "./components/AppFooter";
import { AppMainMenu } from "./components/AppMainMenu";
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
import {
ExportToExcalidrawPlus,
exportToExcalidrawPlus,
} from "./components/ExportToExcalidrawPlus";
import { TopErrorBoundary } from "./components/TopErrorBoundary";
import {
exportToBackend,
getCollaborationLinkData,
isCollaborationLink,
loadScene,
} from "./data";
import { updateStaleImageStatuses } from "./data/FileManager";
import {
importFromLocalStorage,
importUsernameFromLocalStorage,
} from "./data/localStorage";
import { loadFilesFromFirebase } from "./data/firebase";
import {
LibraryIndexedDBAdapter,
LibraryLocalStorageMigrationAdapter,
LocalData,
} from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync";
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
import { useHandleAppTheme } from "./useHandleAppTheme"; import { useHandleAppTheme } from "./useHandleAppTheme";
import { getPreferredLanguage } from "./app-language/language-detector"; import { getPreferredLanguage } from "./app-language/language-detector";
import { useAppLangCode } from "./app-language/language-state"; import { useAppLangCode } from "./app-language/language-state";
@ -131,7 +134,10 @@ import DebugCanvas, {
} from "./components/DebugCanvas"; } from "./components/DebugCanvas";
import { AIComponents } from "./components/AI"; import { AIComponents } from "./components/AI";
import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport"; import { ExcalidrawPlusIframeExport } from "./ExcalidrawPlusIframeExport";
import { isElementLink } from "@excalidraw/excalidraw/element/elementLink";
import "./index.scss";
import type { CollabAPI } from "./collab/Collab";
polyfill(); polyfill();
@ -377,7 +383,7 @@ const ExcalidrawWrapper = () => {
const [, forceRefresh] = useState(false); const [, forceRefresh] = useState(false);
useEffect(() => { useEffect(() => {
if (import.meta.env.DEV) { if (isDevEnv()) {
const debugState = loadSavedDebugState(); const debugState = loadSavedDebugState();
if (debugState.enabled && !window.visualDebug) { if (debugState.enabled && !window.visualDebug) {

View file

@ -1,15 +1,21 @@
import { Stats } from "@excalidraw/excalidraw";
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
import {
DEFAULT_VERSION,
debounce,
getVersion,
nFormatter,
} from "@excalidraw/common";
import { t } from "@excalidraw/excalidraw/i18n";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { debounce, getVersion, nFormatter } from "@excalidraw/excalidraw/utils";
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
import type { UIAppState } from "@excalidraw/excalidraw/types";
import { import {
getElementsStorageSize, getElementsStorageSize,
getTotalStorageSize, getTotalStorageSize,
} from "./data/localStorage"; } from "./data/localStorage";
import { DEFAULT_VERSION } from "@excalidraw/excalidraw/constants";
import { t } from "@excalidraw/excalidraw/i18n";
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
import type { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import type { UIAppState } from "@excalidraw/excalidraw/types";
import { Stats } from "@excalidraw/excalidraw";
type StorageSizes = { scene: number; total: number }; type StorageSizes = { scene: number; total: number };

View file

@ -1,13 +1,15 @@
import { base64urlToString } from "@excalidraw/excalidraw/data/encode";
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
import { useLayoutEffect, useRef } from "react"; import { useLayoutEffect, useRef } from "react";
import { STORAGE_KEYS } from "./app_constants";
import { LocalData } from "./data/LocalData";
import type { import type {
FileId, FileId,
OrderedExcalidrawElement, OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types"; } from "@excalidraw/element/types";
import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types"; import type { AppState, BinaryFileData } from "@excalidraw/excalidraw/types";
import { ExcalidrawError } from "@excalidraw/excalidraw/errors";
import { base64urlToString } from "@excalidraw/excalidraw/data/encode"; import { STORAGE_KEYS } from "./app_constants";
import { LocalData } from "./data/LocalData";
const EVENT_REQUEST_SCENE = "REQUEST_SCENE"; const EVENT_REQUEST_SCENE = "REQUEST_SCENE";

View file

@ -1,6 +1,8 @@
import React from "react";
import { useI18n, languages } from "@excalidraw/excalidraw/i18n"; import { useI18n, languages } from "@excalidraw/excalidraw/i18n";
import React from "react";
import { useSetAtom } from "../app-jotai"; import { useSetAtom } from "../app-jotai";
import { appLangCodeAtom } from "./language-state"; import { appLangCodeAtom } from "./language-state";
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => { export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {

View file

@ -1,5 +1,5 @@
import LanguageDetector from "i18next-browser-languagedetector";
import { defaultLang, languages } from "@excalidraw/excalidraw"; import { defaultLang, languages } from "@excalidraw/excalidraw";
import LanguageDetector from "i18next-browser-languagedetector";
export const languageDetector = new LanguageDetector(); export const languageDetector = new LanguageDetector();

View file

@ -1,5 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { atom, useAtom } from "../app-jotai"; import { atom, useAtom } from "../app-jotai";
import { getPreferredLanguage, languageDetector } from "./language-detector"; import { getPreferredLanguage, languageDetector } from "./language-detector";
export const appLangCodeAtom = atom(getPreferredLanguage()); export const appLangCodeAtom = atom(getPreferredLanguage());

View file

@ -1,21 +1,3 @@
import throttle from "lodash.throttle";
import { PureComponent } from "react";
import type {
BinaryFileData,
ExcalidrawImperativeAPI,
SocketId,
Collaborator,
Gesture,
} from "@excalidraw/excalidraw/types";
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
import { APP_NAME, ENV, EVENT } from "@excalidraw/excalidraw/constants";
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
import type {
ExcalidrawElement,
FileId,
InitializedExcalidrawImageElement,
OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
import { import {
CaptureUpdateAction, CaptureUpdateAction,
getSceneVersion, getSceneVersion,
@ -23,12 +5,54 @@ import {
zoomToFitBounds, zoomToFitBounds,
reconcileElements, reconcileElements,
} from "@excalidraw/excalidraw"; } from "@excalidraw/excalidraw";
import { ErrorDialog } from "@excalidraw/excalidraw/components/ErrorDialog";
import { APP_NAME, EVENT } from "@excalidraw/common";
import { import {
IDLE_THRESHOLD,
ACTIVE_THRESHOLD,
UserIdleState,
assertNever, assertNever,
isDevEnv,
isTestEnv,
preventUnload, preventUnload,
resolvablePromise, resolvablePromise,
throttleRAF, throttleRAF,
} from "@excalidraw/excalidraw/utils"; } from "@excalidraw/common";
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
import { getVisibleSceneBounds } from "@excalidraw/element/bounds";
import { newElementWith } from "@excalidraw/element/mutateElement";
import {
isImageElement,
isInitializedImageElement,
} from "@excalidraw/element/typeChecks";
import { AbortError } from "@excalidraw/excalidraw/errors";
import { t } from "@excalidraw/excalidraw/i18n";
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
import throttle from "lodash.throttle";
import { PureComponent } from "react";
import type {
ReconciledExcalidrawElement,
RemoteExcalidrawElement,
} from "@excalidraw/excalidraw/data/reconcile";
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
import type {
ExcalidrawElement,
FileId,
InitializedExcalidrawImageElement,
OrderedExcalidrawElement,
} from "@excalidraw/element/types";
import type {
BinaryFileData,
ExcalidrawImperativeAPI,
SocketId,
Collaborator,
Gesture,
} from "@excalidraw/excalidraw/types";
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
import { appJotaiStore, atom } from "../app-jotai";
import { import {
CURSOR_SYNC_TIMEOUT, CURSOR_SYNC_TIMEOUT,
FILE_UPLOAD_MAX_BYTES, FILE_UPLOAD_MAX_BYTES,
@ -39,15 +63,17 @@ import {
SYNC_FULL_SCENE_INTERVAL_MS, SYNC_FULL_SCENE_INTERVAL_MS,
WS_EVENTS, WS_EVENTS,
} from "../app_constants"; } from "../app_constants";
import type {
SocketUpdateDataSource,
SyncableExcalidrawElement,
} from "../data";
import { import {
generateCollaborationLinkData, generateCollaborationLinkData,
getCollaborationLink, getCollaborationLink,
getSyncableElements, getSyncableElements,
} from "../data"; } from "../data";
import {
encodeFilesForUpload,
FileManager,
updateStaleImageStatuses,
} from "../data/FileManager";
import { LocalData } from "../data/LocalData";
import { import {
isSavedToFirebase, isSavedToFirebase,
loadFilesFromFirebase, loadFilesFromFirebase,
@ -59,36 +85,15 @@ import {
importUsernameFromLocalStorage, importUsernameFromLocalStorage,
saveUsernameToLocalStorage, saveUsernameToLocalStorage,
} from "../data/localStorage"; } from "../data/localStorage";
import Portal from "./Portal";
import { t } from "@excalidraw/excalidraw/i18n";
import {
IDLE_THRESHOLD,
ACTIVE_THRESHOLD,
UserIdleState,
} from "@excalidraw/excalidraw/constants";
import {
encodeFilesForUpload,
FileManager,
updateStaleImageStatuses,
} from "../data/FileManager";
import { AbortError } from "@excalidraw/excalidraw/errors";
import {
isImageElement,
isInitializedImageElement,
} from "@excalidraw/excalidraw/element/typeChecks";
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
import { resetBrowserStateVersions } from "../data/tabSync"; import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData";
import { appJotaiStore, atom } from "../app-jotai";
import type { Mutable, ValueOf } from "@excalidraw/excalidraw/utility-types";
import { getVisibleSceneBounds } from "@excalidraw/excalidraw/element/bounds";
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
import { collabErrorIndicatorAtom } from "./CollabError"; import { collabErrorIndicatorAtom } from "./CollabError";
import Portal from "./Portal";
import type { import type {
ReconciledExcalidrawElement, SocketUpdateDataSource,
RemoteExcalidrawElement, SyncableExcalidrawElement,
} from "@excalidraw/excalidraw/data/reconcile"; } from "../data";
export const collabAPIAtom = atom<CollabAPI | null>(null); export const collabAPIAtom = atom<CollabAPI | null>(null);
export const isCollaboratingAtom = atom(false); export const isCollaboratingAtom = atom(false);
@ -236,7 +241,7 @@ class Collab extends PureComponent<CollabProps, CollabState> {
appJotaiStore.set(collabAPIAtom, collabAPI); appJotaiStore.set(collabAPIAtom, collabAPI);
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) { if (isTestEnv() || isDevEnv()) {
window.collab = window.collab || ({} as Window["collab"]); window.collab = window.collab || ({} as Window["collab"]);
Object.defineProperties(window, { Object.defineProperties(window, {
collab: { collab: {
@ -1009,7 +1014,7 @@ declare global {
} }
} }
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) { if (isTestEnv() || isDevEnv()) {
window.collab = window.collab || ({} as Window["collab"]); window.collab = window.collab || ({} as Window["collab"]);
} }

View file

@ -2,6 +2,7 @@ import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
import { warning } from "@excalidraw/excalidraw/components/icons"; import { warning } from "@excalidraw/excalidraw/components/icons";
import clsx from "clsx"; import clsx from "clsx";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { atom } from "../app-jotai"; import { atom } from "../app-jotai";
import "./CollabError.scss"; import "./CollabError.scss";

View file

@ -1,25 +1,26 @@
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
import { trackEvent } from "@excalidraw/excalidraw/analytics";
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
import { newElementWith } from "@excalidraw/element/mutateElement";
import throttle from "lodash.throttle";
import type { UserIdleState } from "@excalidraw/common";
import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
import type {
OnUserFollowedPayload,
SocketId,
} from "@excalidraw/excalidraw/types";
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
import { isSyncableElement } from "../data";
import type { import type {
SocketUpdateData, SocketUpdateData,
SocketUpdateDataSource, SocketUpdateDataSource,
SyncableExcalidrawElement, SyncableExcalidrawElement,
} from "../data"; } from "../data";
import { isSyncableElement } from "../data";
import type { TCollabClass } from "./Collab"; import type { TCollabClass } from "./Collab";
import type { OrderedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import { WS_EVENTS, FILE_UPLOAD_TIMEOUT, WS_SUBTYPES } from "../app_constants";
import type {
OnUserFollowedPayload,
SocketId,
} from "@excalidraw/excalidraw/types";
import type { UserIdleState } from "@excalidraw/excalidraw/constants";
import { trackEvent } from "@excalidraw/excalidraw/analytics";
import throttle from "lodash.throttle";
import { newElementWith } from "@excalidraw/excalidraw/element/mutateElement";
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
import type { Socket } from "socket.io-client"; import type { Socket } from "socket.io-client";
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
class Portal { class Portal {
collab: TCollabClass; collab: TCollabClass;

View file

@ -1,4 +1,3 @@
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
import { import {
DiagramToCodePlugin, DiagramToCodePlugin,
exportToBlob, exportToBlob,
@ -7,7 +6,9 @@ import {
TTDDialog, TTDDialog,
} from "@excalidraw/excalidraw"; } from "@excalidraw/excalidraw";
import { getDataURL } from "@excalidraw/excalidraw/data/blob"; import { getDataURL } from "@excalidraw/excalidraw/data/blob";
import { safelyParseJSON } from "@excalidraw/excalidraw/utils"; import { safelyParseJSON } from "@excalidraw/common";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
export const AIComponents = ({ export const AIComponents = ({
excalidrawAPI, excalidrawAPI,

View file

@ -1,9 +1,11 @@
import React from "react";
import { Footer } from "@excalidraw/excalidraw/index"; import { Footer } from "@excalidraw/excalidraw/index";
import React from "react";
import { isExcalidrawPlusSignedUser } from "../app_constants";
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
import { EncryptedIcon } from "./EncryptedIcon"; import { EncryptedIcon } from "./EncryptedIcon";
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink"; import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
import { isExcalidrawPlusSignedUser } from "../app_constants";
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
export const AppFooter = React.memo( export const AppFooter = React.memo(
({ onChange }: { onChange: () => void }) => { ({ onChange }: { onChange: () => void }) => {

View file

@ -1,13 +1,18 @@
import React from "react";
import { import {
loginIcon, loginIcon,
ExcalLogo, ExcalLogo,
eyeIcon, eyeIcon,
} from "@excalidraw/excalidraw/components/icons"; } from "@excalidraw/excalidraw/components/icons";
import type { Theme } from "@excalidraw/excalidraw/element/types";
import { MainMenu } from "@excalidraw/excalidraw/index"; import { MainMenu } from "@excalidraw/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants"; import React from "react";
import { isDevEnv } from "@excalidraw/common";
import type { Theme } from "@excalidraw/element/types";
import { LanguageList } from "../app-language/LanguageList"; import { LanguageList } from "../app-language/LanguageList";
import { isExcalidrawPlusSignedUser } from "../app_constants";
import { saveDebugState } from "./DebugCanvas"; import { saveDebugState } from "./DebugCanvas";
export const AppMainMenu: React.FC<{ export const AppMainMenu: React.FC<{
@ -54,7 +59,7 @@ export const AppMainMenu: React.FC<{
> >
{isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"} {isExcalidrawPlusSignedUser ? "Sign in" : "Sign up"}
</MainMenu.ItemLink> </MainMenu.ItemLink>
{import.meta.env.DEV && ( {isDevEnv() && (
<MainMenu.Item <MainMenu.Item
icon={eyeIcon} icon={eyeIcon}
onClick={() => { onClick={() => {

View file

@ -1,9 +1,10 @@
import React from "react";
import { loginIcon } from "@excalidraw/excalidraw/components/icons"; import { loginIcon } from "@excalidraw/excalidraw/components/icons";
import { POINTER_EVENTS } from "@excalidraw/common";
import { useI18n } from "@excalidraw/excalidraw/i18n"; import { useI18n } from "@excalidraw/excalidraw/i18n";
import { WelcomeScreen } from "@excalidraw/excalidraw/index"; import { WelcomeScreen } from "@excalidraw/excalidraw/index";
import React from "react";
import { isExcalidrawPlusSignedUser } from "../app_constants"; import { isExcalidrawPlusSignedUser } from "../app_constants";
import { POINTER_EVENTS } from "@excalidraw/excalidraw/constants";
export const AppWelcomeScreen: React.FC<{ export const AppWelcomeScreen: React.FC<{
onCollabDialogOpen: () => any; onCollabDialogOpen: () => any;

View file

@ -1,24 +1,28 @@
import { useCallback, useImperativeHandle, useRef } from "react";
import { type AppState } from "@excalidraw/excalidraw/types";
import { throttleRAF } from "@excalidraw/excalidraw/utils";
import {
bootstrapCanvas,
getNormalizedCanvasDimensions,
} from "@excalidraw/excalidraw/renderer/helpers";
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
import { import {
ArrowheadArrowIcon, ArrowheadArrowIcon,
CloseIcon, CloseIcon,
TrashIcon, TrashIcon,
} from "@excalidraw/excalidraw/components/icons"; } from "@excalidraw/excalidraw/components/icons";
import { STORAGE_KEYS } from "../app_constants"; import {
import type { Curve } from "../../packages/math"; bootstrapCanvas,
getNormalizedCanvasDimensions,
} from "@excalidraw/excalidraw/renderer/helpers";
import { type AppState } from "@excalidraw/excalidraw/types";
import { throttleRAF } from "@excalidraw/common";
import { useCallback, useImperativeHandle, useRef } from "react";
import { import {
isLineSegment, isLineSegment,
type GlobalPoint, type GlobalPoint,
type LineSegment, type LineSegment,
} from "../../packages/math"; } from "@excalidraw/math";
import { isCurve } from "../../packages/math/curve"; import { isCurve } from "@excalidraw/math/curve";
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
import type { Curve } from "@excalidraw/math";
import { STORAGE_KEYS } from "../app_constants";
const renderLine = ( const renderLine = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,

View file

@ -1,5 +1,5 @@
import { shield } from "@excalidraw/excalidraw/components/icons";
import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip"; import { Tooltip } from "@excalidraw/excalidraw/components/Tooltip";
import { shield } from "@excalidraw/excalidraw/components/icons";
import { useI18n } from "@excalidraw/excalidraw/i18n"; import { useI18n } from "@excalidraw/excalidraw/i18n";
export const EncryptedIcon = () => { export const EncryptedIcon = () => {

View file

@ -1,31 +1,33 @@
import React from "react"; 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 { Card } from "@excalidraw/excalidraw/components/Card";
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton"; import { ToolButton } from "@excalidraw/excalidraw/components/ToolButton";
import { MIME_TYPES, getFrame } from "@excalidraw/common";
import {
encryptData,
generateEncryptionKey,
} from "@excalidraw/excalidraw/data/encryption";
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json"; import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase"; import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
import { useI18n } from "@excalidraw/excalidraw/i18n";
import type { import type {
FileId, FileId,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types"; } from "@excalidraw/element/types";
import type { import type {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFiles, BinaryFiles,
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
import { nanoid } from "nanoid";
import { useI18n } from "@excalidraw/excalidraw/i18n";
import {
encryptData,
generateEncryptionKey,
} from "@excalidraw/excalidraw/data/encryption";
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants"; import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
import { encodeFilesForUpload } from "../data/FileManager"; import { encodeFilesForUpload } from "../data/FileManager";
import { uploadBytes, ref } from "firebase/storage"; import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
import { MIME_TYPES } from "@excalidraw/excalidraw/constants";
import { trackEvent } from "@excalidraw/excalidraw/analytics";
import { getFrame } from "@excalidraw/excalidraw/utils";
import { ExcalidrawLogo } from "@excalidraw/excalidraw/components/ExcalidrawLogo";
export const exportToExcalidrawPlus = async ( export const exportToExcalidrawPlus = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],

View file

@ -1,7 +1,8 @@
import { THEME } from "@excalidraw/common";
import oc from "open-color"; import oc from "open-color";
import React from "react"; import React from "react";
import { THEME } from "@excalidraw/excalidraw/constants";
import type { Theme } from "@excalidraw/excalidraw/element/types"; import type { Theme } from "@excalidraw/element/types";
// https://github.com/tholman/github-corners // https://github.com/tholman/github-corners
export const GitHubCorner = React.memo( export const GitHubCorner = React.memo(

View file

@ -1,7 +1,7 @@
import React from "react";
import * as Sentry from "@sentry/browser";
import { t } from "@excalidraw/excalidraw/i18n";
import Trans from "@excalidraw/excalidraw/components/Trans"; import Trans from "@excalidraw/excalidraw/components/Trans";
import { t } from "@excalidraw/excalidraw/i18n";
import * as Sentry from "@sentry/browser";
import React from "react";
interface TopErrorBoundaryState { interface TopErrorBoundaryState {
hasError: boolean; hasError: boolean;

View file

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

View file

@ -10,6 +10,13 @@
* (localStorage, indexedDB). * (localStorage, indexedDB).
*/ */
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
import {
CANVAS_SEARCH_TAB,
DEFAULT_SIDEBAR,
debounce,
} from "@excalidraw/common";
import { clearElementsForLocalStorage } from "@excalidraw/element";
import { import {
createStore, createStore,
entries, entries,
@ -19,26 +26,19 @@ import {
setMany, setMany,
get, get,
} from "idb-keyval"; } from "idb-keyval";
import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
import {
CANVAS_SEARCH_TAB,
DEFAULT_SIDEBAR,
} from "@excalidraw/excalidraw/constants";
import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library"; import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types"; import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element"; import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
import type {
ExcalidrawElement,
FileId,
} from "@excalidraw/excalidraw/element/types";
import type { import type {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFiles, BinaryFiles,
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
import type { MaybePromise } from "@excalidraw/excalidraw/utility-types"; import type { MaybePromise } from "@excalidraw/common/utility-types";
import { debounce } from "@excalidraw/excalidraw/utils";
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants"; import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
import { FileManager } from "./FileManager"; import { FileManager } from "./FileManager";
import { Locker } from "./Locker"; import { Locker } from "./Locker";
import { updateBrowserStateVersion } from "./tabSync"; import { updateBrowserStateVersion } from "./tabSync";

View file

@ -1,27 +1,12 @@
import { reconcileElements } from "@excalidraw/excalidraw"; import { reconcileElements } from "@excalidraw/excalidraw";
import type { import { MIME_TYPES } from "@excalidraw/common";
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types";
import { getSceneVersion } from "@excalidraw/excalidraw/element";
import type Portal from "../collab/Portal";
import { restoreElements } from "@excalidraw/excalidraw/data/restore";
import type {
AppState,
BinaryFileData,
BinaryFileMetadata,
DataURL,
} from "@excalidraw/excalidraw/types";
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
import { decompressData } from "@excalidraw/excalidraw/data/encode"; import { decompressData } from "@excalidraw/excalidraw/data/encode";
import { import {
encryptData, encryptData,
decryptData, decryptData,
} from "@excalidraw/excalidraw/data/encryption"; } from "@excalidraw/excalidraw/data/encryption";
import { MIME_TYPES } from "@excalidraw/excalidraw/constants"; import { restoreElements } from "@excalidraw/excalidraw/data/restore";
import type { SyncableExcalidrawElement } from "."; import { getSceneVersion } from "@excalidraw/element";
import { getSyncableElements } from ".";
import { initializeApp } from "firebase/app"; import { initializeApp } from "firebase/app";
import { import {
getFirestore, getFirestore,
@ -31,8 +16,27 @@ import {
Bytes, Bytes,
} from "firebase/firestore"; } from "firebase/firestore";
import { getStorage, ref, uploadBytes } from "firebase/storage"; import { getStorage, ref, uploadBytes } from "firebase/storage";
import type { Socket } from "socket.io-client";
import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile"; import type { RemoteExcalidrawElement } from "@excalidraw/excalidraw/data/reconcile";
import type {
ExcalidrawElement,
FileId,
OrderedExcalidrawElement,
} from "@excalidraw/element/types";
import type {
AppState,
BinaryFileData,
BinaryFileMetadata,
DataURL,
} from "@excalidraw/excalidraw/types";
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
import { getSyncableElements } from ".";
import type { SyncableExcalidrawElement } from ".";
import type Portal from "../collab/Portal";
import type { Socket } from "socket.io-client";
// private // private
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View file

@ -9,34 +9,38 @@ import {
} from "@excalidraw/excalidraw/data/encryption"; } from "@excalidraw/excalidraw/data/encryption";
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json"; import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
import { restore } from "@excalidraw/excalidraw/data/restore"; import { restore } from "@excalidraw/excalidraw/data/restore";
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
import { t } from "@excalidraw/excalidraw/i18n";
import { bytesToHexString } from "@excalidraw/common";
import type { UserIdleState } from "@excalidraw/common";
import type { ImportedDataState } from "@excalidraw/excalidraw/data/types"; import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
import type { SceneBounds } from "@excalidraw/excalidraw/element/bounds"; import type { SceneBounds } from "@excalidraw/element/bounds";
import { isInvisiblySmallElement } from "@excalidraw/excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "@excalidraw/excalidraw/element/typeChecks";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
FileId, FileId,
OrderedExcalidrawElement, OrderedExcalidrawElement,
} from "@excalidraw/excalidraw/element/types"; } from "@excalidraw/element/types";
import { t } from "@excalidraw/excalidraw/i18n";
import type { import type {
AppState, AppState,
BinaryFileData, BinaryFileData,
BinaryFiles, BinaryFiles,
SocketId, SocketId,
} from "@excalidraw/excalidraw/types"; } from "@excalidraw/excalidraw/types";
import type { UserIdleState } from "@excalidraw/excalidraw/constants"; import type { MakeBrand } from "@excalidraw/common/utility-types";
import type { MakeBrand } from "@excalidraw/excalidraw/utility-types";
import { bytesToHexString } from "@excalidraw/excalidraw/utils";
import type { WS_SUBTYPES } from "../app_constants";
import { import {
DELETED_ELEMENT_TIMEOUT, DELETED_ELEMENT_TIMEOUT,
FILE_UPLOAD_MAX_BYTES, FILE_UPLOAD_MAX_BYTES,
ROOM_ID_BYTES, ROOM_ID_BYTES,
} from "../app_constants"; } from "../app_constants";
import { encodeFilesForUpload } from "./FileManager"; import { encodeFilesForUpload } from "./FileManager";
import { saveFilesToFirebase } from "./firebase"; import { saveFilesToFirebase } from "./firebase";
import type { WS_SUBTYPES } from "../app_constants";
export type SyncableExcalidrawElement = OrderedExcalidrawElement & export type SyncableExcalidrawElement = OrderedExcalidrawElement &
MakeBrand<"SyncableExcalidrawElement">; MakeBrand<"SyncableExcalidrawElement">;

View file

@ -1,10 +1,12 @@
import type { ExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import type { AppState } from "@excalidraw/excalidraw/types";
import { import {
clearAppStateForLocalStorage, clearAppStateForLocalStorage,
getDefaultAppState, getDefaultAppState,
} from "@excalidraw/excalidraw/appState"; } from "@excalidraw/excalidraw/appState";
import { clearElementsForLocalStorage } from "@excalidraw/excalidraw/element"; import { clearElementsForLocalStorage } from "@excalidraw/element";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { AppState } from "@excalidraw/excalidraw/types";
import { STORAGE_KEYS } from "../app_constants"; import { STORAGE_KEYS } from "../app_constants";
export const saveUsernameToLocalStorage = (username: string) => { export const saveUsernameToLocalStorage = (username: string) => {

View file

@ -1,9 +1,11 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import ExcalidrawApp from "./App";
import { registerSW } from "virtual:pwa-register"; import { registerSW } from "virtual:pwa-register";
import "../excalidraw-app/sentry"; import "../excalidraw-app/sentry";
import ExcalidrawApp from "./App";
window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA; window.__EXCALIDRAW_SHA__ = import.meta.env.VITE_APP_GIT_SHA;
const rootElement = document.getElementById("root")!; const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement); const root = createRoot(rootElement);

View file

@ -1,10 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
import { trackEvent } from "@excalidraw/excalidraw/analytics"; import { trackEvent } from "@excalidraw/excalidraw/analytics";
import { getFrame } from "@excalidraw/excalidraw/utils"; import { copyTextToSystemClipboard } from "@excalidraw/excalidraw/clipboard";
import { useI18n } from "@excalidraw/excalidraw/i18n";
import { KEYS } from "@excalidraw/excalidraw/keys";
import { Dialog } from "@excalidraw/excalidraw/components/Dialog"; import { Dialog } from "@excalidraw/excalidraw/components/Dialog";
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
import { TextField } from "@excalidraw/excalidraw/components/TextField";
import { import {
copyIcon, copyIcon,
LinkIcon, LinkIcon,
@ -14,16 +12,19 @@ import {
shareIOS, shareIOS,
shareWindows, shareWindows,
} from "@excalidraw/excalidraw/components/icons"; } from "@excalidraw/excalidraw/components/icons";
import { TextField } from "@excalidraw/excalidraw/components/TextField";
import { FilledButton } from "@excalidraw/excalidraw/components/FilledButton";
import type { CollabAPI } from "../collab/Collab";
import { activeRoomLinkAtom } from "../collab/Collab";
import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState"; import { useUIAppState } from "@excalidraw/excalidraw/context/ui-appState";
import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator"; import { useCopyStatus } from "@excalidraw/excalidraw/hooks/useCopiedIndicator";
import { useI18n } from "@excalidraw/excalidraw/i18n";
import { KEYS, getFrame } from "@excalidraw/common";
import { useEffect, useRef, useState } from "react";
import { atom, useAtom, useAtomValue } from "../app-jotai"; import { atom, useAtom, useAtomValue } from "../app-jotai";
import { activeRoomLinkAtom } from "../collab/Collab";
import "./ShareDialog.scss"; import "./ShareDialog.scss";
import type { CollabAPI } from "../collab/Collab";
type OnExportToBackend = () => void; type OnExportToBackend = () => void;
type ShareDialogType = "share" | "collaborationOnly"; type ShareDialogType = "share" | "collaborationOnly";

View file

@ -1,11 +1,11 @@
import ExcalidrawApp from "../App"; import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
import { import {
mockBoundingClientRect, mockBoundingClientRect,
render, render,
restoreOriginalGetBoundingClientRect, restoreOriginalGetBoundingClientRect,
} from "@excalidraw/excalidraw/tests/test-utils"; } from "@excalidraw/excalidraw/tests/test-utils";
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui"; import ExcalidrawApp from "../App";
describe("Test MobileMenu", () => { describe("Test MobileMenu", () => {
const { h } = window; const { h } = window;

View file

@ -1,13 +1,14 @@
import { vi } from "vitest"; import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw";
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
import ExcalidrawApp from "../App";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { syncInvalidIndices } from "@excalidraw/excalidraw/fractionalIndex";
import { import {
createRedoAction, createRedoAction,
createUndoAction, createUndoAction,
} from "@excalidraw/excalidraw/actions/actionHistory"; } from "@excalidraw/excalidraw/actions/actionHistory";
import { CaptureUpdateAction, newElementWith } from "@excalidraw/excalidraw"; 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";
import ExcalidrawApp from "../App";
const { h } = window; const { h } = window;

View file

@ -1,8 +1,9 @@
import { useEffect, useLayoutEffect, useState } from "react";
import { THEME } from "@excalidraw/excalidraw"; import { THEME } from "@excalidraw/excalidraw";
import { EVENT } from "@excalidraw/excalidraw/constants"; import { EVENT, CODES, KEYS } from "@excalidraw/common";
import type { Theme } from "@excalidraw/excalidraw/element/types"; import { useEffect, useLayoutEffect, useState } from "react";
import { CODES, KEYS } from "@excalidraw/excalidraw/keys";
import type { Theme } from "@excalidraw/element/types";
import { STORAGE_KEYS } from "./app_constants"; import { STORAGE_KEYS } from "./app_constants";
const getDarkThemeMediaQuery = (): MediaQueryList | undefined => const getDarkThemeMediaQuery = (): MediaQueryList | undefined =>

View file

@ -23,6 +23,22 @@ export default defineConfig(({ mode }) => {
envDir: "../", envDir: "../",
resolve: { resolve: {
alias: [ alias: [
{
find: /^@excalidraw\/common$/,
replacement: path.resolve(__dirname, "../packages/common/src/index.ts"),
},
{
find: /^@excalidraw\/common\/(.*?)/,
replacement: path.resolve(__dirname, "../packages/common/src/$1"),
},
{
find: /^@excalidraw\/element$/,
replacement: path.resolve(__dirname, "../packages/element/src/index.ts"),
},
{
find: /^@excalidraw\/element\/(.*?)/,
replacement: path.resolve(__dirname, "../packages/element/src/$1"),
},
{ {
find: /^@excalidraw\/excalidraw$/, find: /^@excalidraw\/excalidraw$/,
replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"), replacement: path.resolve(__dirname, "../packages/excalidraw/index.tsx"),
@ -31,21 +47,21 @@ export default defineConfig(({ mode }) => {
find: /^@excalidraw\/excalidraw\/(.*?)/, find: /^@excalidraw\/excalidraw\/(.*?)/,
replacement: path.resolve(__dirname, "../packages/excalidraw/$1"), replacement: path.resolve(__dirname, "../packages/excalidraw/$1"),
}, },
{
find: /^@excalidraw\/utils$/,
replacement: path.resolve(__dirname, "../packages/utils/index.ts"),
},
{
find: /^@excalidraw\/utils\/(.*?)/,
replacement: path.resolve(__dirname, "../packages/utils/$1"),
},
{ {
find: /^@excalidraw\/math$/, find: /^@excalidraw\/math$/,
replacement: path.resolve(__dirname, "../packages/math/index.ts"), replacement: path.resolve(__dirname, "../packages/math/src/index.ts"),
}, },
{ {
find: /^@excalidraw\/math\/(.*?)/, find: /^@excalidraw\/math\/(.*?)/,
replacement: path.resolve(__dirname, "../packages/math/$1"), replacement: path.resolve(__dirname, "../packages/math/src/$1"),
},
{
find: /^@excalidraw\/utils$/,
replacement: path.resolve(__dirname, "../packages/utils/src/index.ts"),
},
{
find: /^@excalidraw\/utils\/(.*?)/,
replacement: path.resolve(__dirname, "../packages/utils/src/$1"),
}, },
], ],
}, },

View file

@ -4,9 +4,7 @@
"packageManager": "yarn@1.22.22", "packageManager": "yarn@1.22.22",
"workspaces": [ "workspaces": [
"excalidraw-app", "excalidraw-app",
"packages/excalidraw", "packages/*",
"packages/utils",
"packages/math",
"examples/*" "examples/*"
], ],
"devDependencies": { "devDependencies": {
@ -26,6 +24,7 @@
"dotenv": "16.0.1", "dotenv": "16.0.1",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-config-react-app": "7.0.1", "eslint-config-react-app": "7.0.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
"http-server": "14.1.1", "http-server": "14.1.1",
"husky": "7.0.4", "husky": "7.0.4",

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[] = []; private content: T[] = [];
constructor(private scoreFunction: (node: T) => number) {} constructor(private scoreFunction: (node: T) => number) {}

View file

@ -1,4 +1,5 @@
import oc from "open-color"; import oc from "open-color";
import type { Merge } from "./utility-types"; import type { Merge } from "./utility-types";
// FIXME can't put to utils.ts rn because of circular dependency // FIXME can't put to utils.ts rn because of circular dependency

View file

@ -1,5 +1,9 @@
import type { AppProps, AppState } from "./types"; import type {
import type { ExcalidrawElement, FontFamilyValues } from "./element/types"; ExcalidrawElement,
FontFamilyValues,
} from "@excalidraw/element/types";
import type { AppProps, AppState } from "@excalidraw/excalidraw/types";
import { COLOR_PALETTE } from "./colors"; import { COLOR_PALETTE } from "./colors";
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform); export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);

View file

@ -1,11 +1,9 @@
import type { JSX } from "react"; import type {
import { ExcalidrawTextElement,
FreedrawIcon, FontFamilyValues,
FontFamilyNormalIcon, } from "@excalidraw/element/types";
FontFamilyHeadingIcon,
FontFamilyCodeIcon, import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "./constants";
} from "../components/icons";
import { FONT_FAMILY, FONT_FAMILY_FALLBACKS } from "../constants";
/** /**
* Encapsulates font metrics with additional font metadata. * Encapsulates font metrics with additional font metadata.
@ -22,8 +20,6 @@ export interface FontMetadata {
/** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */ /** harcoded unitless line-height, https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971 */
lineHeight: number; lineHeight: number;
}; };
/** element to be displayed as an icon */
icon?: JSX.Element;
/** flag to indicate a deprecated font */ /** flag to indicate a deprecated font */
deprecated?: true; deprecated?: true;
/** flag to indicate a server-side only font */ /** flag to indicate a server-side only font */
@ -42,7 +38,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -374, descender: -374,
lineHeight: 1.25, lineHeight: 1.25,
}, },
icon: FreedrawIcon,
}, },
[FONT_FAMILY.Nunito]: { [FONT_FAMILY.Nunito]: {
metrics: { metrics: {
@ -51,7 +46,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -353, descender: -353,
lineHeight: 1.35, lineHeight: 1.35,
}, },
icon: FontFamilyNormalIcon,
}, },
[FONT_FAMILY["Lilita One"]]: { [FONT_FAMILY["Lilita One"]]: {
metrics: { metrics: {
@ -60,7 +54,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -220, descender: -220,
lineHeight: 1.15, lineHeight: 1.15,
}, },
icon: FontFamilyHeadingIcon,
}, },
[FONT_FAMILY["Comic Shanns"]]: { [FONT_FAMILY["Comic Shanns"]]: {
metrics: { metrics: {
@ -69,7 +62,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -250, descender: -250,
lineHeight: 1.25, lineHeight: 1.25,
}, },
icon: FontFamilyCodeIcon,
}, },
[FONT_FAMILY.Virgil]: { [FONT_FAMILY.Virgil]: {
metrics: { metrics: {
@ -78,7 +70,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -374, descender: -374,
lineHeight: 1.25, lineHeight: 1.25,
}, },
icon: FreedrawIcon,
deprecated: true, deprecated: true,
}, },
[FONT_FAMILY.Helvetica]: { [FONT_FAMILY.Helvetica]: {
@ -88,7 +79,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -471, descender: -471,
lineHeight: 1.15, lineHeight: 1.15,
}, },
icon: FontFamilyNormalIcon,
deprecated: true, deprecated: true,
local: true, local: true,
}, },
@ -99,7 +89,6 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
descender: -480, descender: -480,
lineHeight: 1.2, lineHeight: 1.2,
}, },
icon: FontFamilyCodeIcon,
deprecated: true, deprecated: true,
}, },
[FONT_FAMILY["Liberation Sans"]]: { [FONT_FAMILY["Liberation Sans"]]: {
@ -148,3 +137,34 @@ export const GOOGLE_FONTS_RANGES = {
/** local protocol to skip the local font from registering or inlining */ /** local protocol to skip the local font from registering or inlining */
export const LOCAL_FONT_PROTOCOL = "local:"; export const LOCAL_FONT_PROTOCOL = "local:";
/**
* Calculates vertical offset for a text with alphabetic baseline.
*/
export const getVerticalOffset = (
fontFamily: ExcalidrawTextElement["fontFamily"],
fontSize: ExcalidrawTextElement["fontSize"],
lineHeightPx: number,
) => {
const { unitsPerEm, ascender, descender } =
FONT_METADATA[fontFamily]?.metrics ||
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
const fontSizeEm = fontSize / unitsPerEm;
const lineGap =
(lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender) / 2;
const verticalOffset = fontSizeEm * ascender + lineGap;
return verticalOffset;
};
/**
* Gets line height for a selected family.
*/
export const getLineHeight = (fontFamily: FontFamilyValues) => {
const { lineHeight } =
FONT_METADATA[fontFamily]?.metrics ||
FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
return lineHeight as ExcalidrawTextElement["lineHeight"];
};

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

@ -1,4 +1,5 @@
import { isDarwin } from "./constants"; import { isDarwin } from "./constants";
import type { ValueOf } from "./utility-types"; import type { ValueOf } from "./utility-types";
export const CODES = { export const CODES = {

View file

@ -4,6 +4,8 @@ import {
type LocalPoint, type LocalPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
import type { NullableGridSize } from "@excalidraw/excalidraw/types";
export const getSizeFromPoints = ( export const getSizeFromPoints = (
points: readonly (GlobalPoint | LocalPoint)[], points: readonly (GlobalPoint | LocalPoint)[],
) => { ) => {
@ -61,3 +63,18 @@ export const rescalePoints = <Point extends GlobalPoint | LocalPoint>(
return nextPoints; return nextPoints;
}; };
// TODO: Rounding this point causes some shake when free drawing
export const getGridPoint = (
x: number,
y: number,
gridSize: NullableGridSize,
): [number, number] => {
if (gridSize) {
return [
Math.round(x / gridSize) * gridSize,
Math.round(y / gridSize) * gridSize,
];
}
return [x, y];
};

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,6 +1,8 @@
import { promiseTry, resolvablePromise } from ".";
import type { ResolvablePromise } from ".";
import type { MaybePromise } from "./utility-types"; import type { MaybePromise } from "./utility-types";
import type { ResolvablePromise } from "./utils";
import { promiseTry, resolvablePromise } from "./utils";
type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>; type Job<T, TArgs extends unknown[]> = (...args: TArgs) => MaybePromise<T>;

View file

@ -1,5 +1,6 @@
import { Random } from "roughjs/bin/math";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { Random } from "roughjs/bin/math";
import { isTestEnv } from "./utils"; import { isTestEnv } from "./utils";
let random = new Random(Date.now()); let random = new Random(Date.now());

View file

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

View file

@ -1,28 +1,33 @@
import Pool from "es6-promise-pool";
import { average } from "@excalidraw/math"; import { average } from "@excalidraw/math";
import { COLOR_PALETTE } from "./colors";
import type { EVENT } from "./constants";
import {
DEFAULT_VERSION,
FONT_FAMILY,
getFontFamilyFallbacks,
isDarwin,
WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants";
import type { import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
FontFamilyValues, FontFamilyValues,
FontString, FontString,
} from "./element/types"; } from "@excalidraw/element/types";
import type { import type {
ActiveTool, ActiveTool,
AppState, AppState,
ToolType, ToolType,
UnsubscribeCallback, UnsubscribeCallback,
Zoom, Zoom,
} from "./types"; } from "@excalidraw/excalidraw/types";
import { COLOR_PALETTE } from "./colors";
import {
DEFAULT_VERSION,
ENV,
FONT_FAMILY,
getFontFamilyFallbacks,
isDarwin,
WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants";
import type { MaybePromise, ResolutionType } from "./utility-types"; import type { MaybePromise, ResolutionType } from "./utility-types";
import type { EVENT } from "./constants";
let mockDateTime: string | null = null; let mockDateTime: string | null = null;
export const setDateTimeForTests = (dateTime: string) => { export const setDateTimeForTests = (dateTime: string) => {
@ -167,7 +172,7 @@ export const throttleRAF = <T extends any[]>(
}; };
const ret = (...args: T) => { const ret = (...args: T) => {
if (import.meta.env.MODE === "test") { if (isTestEnv()) {
fn(...args); fn(...args);
return; return;
} }
@ -728,9 +733,9 @@ export const arrayToList = <T>(array: readonly T[]): Node<T>[] =>
return acc; return acc;
}, [] as Node<T>[]); }, [] as Node<T>[]);
export const isTestEnv = () => import.meta.env.MODE === "test"; export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
export const isDevEnv = () => import.meta.env.MODE === "development"; export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
export const isServerEnv = () => export const isServerEnv = () =>
typeof process !== "undefined" && !!process?.env?.NODE_ENV; typeof process !== "undefined" && !!process?.env?.NODE_ENV;
@ -1184,54 +1189,6 @@ export const safelyParseJSON = (json: string): Record<string, any> | null => {
return null; return null;
} }
}; };
// extending the missing types
// relying on the [Index, T] to keep a correct order
type TPromisePool<T, Index = number> = Pool<[Index, T][]> & {
addEventListener: (
type: "fulfilled",
listener: (event: { data: { result: [Index, T] } }) => void,
) => (event: { data: { result: [Index, T] } }) => void;
removeEventListener: (
type: "fulfilled",
listener: (event: { data: { result: [Index, T] } }) => void,
) => void;
};
export class PromisePool<T> {
private readonly pool: TPromisePool<T>;
private readonly entries: Record<number, T> = {};
constructor(
source: IterableIterator<Promise<void | readonly [number, T]>>,
concurrency: number,
) {
this.pool = new Pool(
source as unknown as () => void | PromiseLike<[number, T][]>,
concurrency,
) as TPromisePool<T>;
}
public all() {
const listener = (event: { data: { result: void | [number, T] } }) => {
if (event.data.result) {
// by default pool does not return the results, so we are gathering them manually
// with the correct call order (represented by the index in the tuple)
const [index, value] = event.data.result;
this.entries[index] = value;
}
};
this.pool.addEventListener("fulfilled", listener);
return this.pool.start().then(() => {
setTimeout(() => {
this.pool.removeEventListener("fulfilled", listener);
});
return Object.values(this.entries);
});
}
}
/** /**
* use when you need to render unsafe string as HTML attribute, but MAKE SURE * use when you need to render unsafe string as HTML attribute, but MAKE SURE

View file

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

View file

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

View file

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

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,31 +1,38 @@
import type { Point as RoughPoint } from "roughjs/bin/geometry";
import type { Drawable, Options } from "roughjs/bin/core";
import type { RoughGenerator } from "roughjs/bin/generator";
import { getDiamondPoints, getArrowheadPoints } from "../element";
import type { ElementShapes } from "./types";
import type {
ExcalidrawElement,
NonDeletedExcalidrawElement,
ExcalidrawSelectionElement,
ExcalidrawLinearElement,
Arrowhead,
} from "../element/types";
import { generateFreeDrawShape } from "../renderer/renderElement";
import { isTransparent, assertNever } from "../utils";
import { simplify } from "points-on-curve"; import { simplify } from "points-on-curve";
import { ROUGHNESS } from "../constants";
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
import { 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 { import {
isElbowArrow, isElbowArrow,
isEmbeddableElement, isEmbeddableElement,
isIframeElement, isIframeElement,
isIframeLikeElement, isIframeLikeElement,
isLinearElement, isLinearElement,
} from "../element/typeChecks"; } from "./typeChecks";
import { getCornerRadius, isPathALoop } from "./shapes";
import { headingForPointIsHorizontal } from "./heading";
import { canChangeRoundness } from "./comparisons"; import { canChangeRoundness } from "./comparisons";
import type { EmbedsValidationStatus } from "../types"; import { generateFreeDrawShape } from "./renderElement";
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math"; import { getArrowheadPoints, getDiamondPoints } from "./bounds";
import { getCornerRadius, isPathALoop } from "../shapes";
import { headingForPointIsHorizontal } from "../element/heading"; import type {
ExcalidrawElement,
NonDeletedExcalidrawElement,
ExcalidrawSelectionElement,
ExcalidrawLinearElement,
Arrowhead,
} 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";
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
@ -508,7 +515,10 @@ export const _generateElementShape = (
if (isPathALoop(element.points)) { if (isPathALoop(element.points)) {
// generate rough polygon to fill freedraw shape // generate rough polygon to fill freedraw shape
const simplifiedPoints = simplify(element.points, 0.75); const simplifiedPoints = simplify(
element.points as Mutable<LocalPoint[]>,
0.75,
);
shape = generator.curve(simplifiedPoints as [number, number][], { shape = generator.curve(simplifiedPoints as [number, number][], {
...generateRoughOptions(element), ...generateRoughOptions(element),
stroke: "none", stroke: "none",

View file

@ -1,14 +1,23 @@
import type { Drawable } from "roughjs/bin/core";
import { RoughGenerator } from "roughjs/bin/generator"; import { RoughGenerator } from "roughjs/bin/generator";
import { COLOR_PALETTE } from "@excalidraw/common";
import type { import type {
ExcalidrawElement, AppState,
ExcalidrawSelectionElement, EmbedsValidationStatus,
} from "../element/types"; } from "@excalidraw/excalidraw/types";
import { elementWithCanvasCache } from "../renderer/renderElement"; import type {
ElementShape,
ElementShapes,
} from "@excalidraw/excalidraw/scene/types";
import { _generateElementShape } from "./Shape"; import { _generateElementShape } from "./Shape";
import type { ElementShape, ElementShapes } from "./types";
import { COLOR_PALETTE } from "../colors"; import { elementWithCanvasCache } from "./renderElement";
import type { AppState, EmbedsValidationStatus } from "../types";
import type { ExcalidrawElement, ExcalidrawSelectionElement } from "./types";
import type { Drawable } from "roughjs/bin/core";
export class ShapeCache { export class ShapeCache {
private static rg = new RoughGenerator(); private static rg = new RoughGenerator();

View file

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

View file

@ -1,3 +1,74 @@
import {
KEYS,
arrayToMap,
isBindingFallthroughEnabled,
tupleToCoors,
invariant,
isDevEnv,
isTestEnv,
} from "@excalidraw/common";
import {
lineSegment,
pointFrom,
pointRotateRads,
type GlobalPoint,
vectorFromPoint,
pointDistanceSq,
clamp,
pointDistance,
pointFromVector,
vectorScale,
vectorNormalize,
vectorCross,
pointsEqual,
lineSegmentIntersectionPoints,
PRECISION,
} from "@excalidraw/math";
import { isPointOnShape } from "@excalidraw/utils/collision";
import type { LocalPoint, Radians } from "@excalidraw/math";
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,
getElementBounds,
doBoundsIntersect,
} from "./bounds";
import { intersectElementWithLineSegment } from "./collision";
import { distanceToBindableElement } from "./distance";
import {
headingForPointFromElement,
headingIsHorizontal,
vectorToHeading,
type Heading,
} from "./heading";
import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import {
isArrowElement,
isBindableElement,
isBoundToContainer,
isElbowArrow,
isFixedPointBinding,
isFrameLikeElement,
isLinearElement,
isRectanguloidElement,
isTextElement,
} from "./typeChecks";
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
import { updateElbowArrowPoints } from "./elbowArrow";
import type { Bounds } from "./bounds";
import type { ElementUpdate } from "./mutateElement";
import type { import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
@ -16,69 +87,6 @@ import type {
FixedPointBinding, FixedPointBinding,
} from "./types"; } from "./types";
import type { Bounds } from "./bounds";
import {
getCenterForBounds,
getElementBounds,
doBoundsIntersect,
} from "./bounds";
import type { AppState } from "../types";
import { isPointOnShape } from "@excalidraw/utils/collision";
import {
isArrowElement,
isBindableElement,
isBindingElement,
isBoundToContainer,
isElbowArrow,
isFixedPointBinding,
isFrameLikeElement,
isLinearElement,
isRectanguloidElement,
isTextElement,
} from "./typeChecks";
import type { ElementUpdate } from "./mutateElement";
import { mutateElement } from "./mutateElement";
import type Scene from "../scene/Scene";
import { LinearElementEditor } from "./linearElementEditor";
import {
arrayToMap,
isBindingFallthroughEnabled,
tupleToCoors,
} from "../utils";
import { KEYS } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
import {
compareHeading,
HEADING_DOWN,
HEADING_RIGHT,
HEADING_UP,
headingForPointFromElement,
vectorToHeading,
type Heading,
} from "./heading";
import type { LocalPoint, Radians } from "@excalidraw/math";
import {
lineSegment,
pointFrom,
pointRotateRads,
type GlobalPoint,
vectorFromPoint,
pointDistanceSq,
clamp,
pointDistance,
pointFromVector,
vectorScale,
vectorNormalize,
vectorCross,
pointsEqual,
lineSegmentIntersectionPoints,
round,
PRECISION,
} from "@excalidraw/math";
import { intersectElementWithLineSegment } from "./collision";
import { distanceToBindableElement } from "./distance";
export type SuggestedBinding = export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement> | NonDeleted<ExcalidrawBindableElement>
| SuggestedPointBinding; | SuggestedPointBinding;
@ -484,20 +492,8 @@ export const bindLinearElement = (
return; return;
} }
const binding: PointBinding | FixedPointBinding = { let binding: PointBinding | FixedPointBinding = {
elementId: hoveredElement.id, elementId: hoveredElement.id,
...(isElbowArrow(linearElement)
? {
...calculateFixedPointForElbowArrowBinding(
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
),
focus: 0,
gap: 0,
}
: {
...normalizePointBinding( ...normalizePointBinding(
calculateFocusAndGap( calculateFocusAndGap(
linearElement, linearElement,
@ -507,9 +503,20 @@ export const bindLinearElement = (
), ),
hoveredElement, hoveredElement,
), ),
}),
}; };
if (isElbowArrow(linearElement)) {
binding = {
...binding,
...calculateFixedPointForElbowArrowBinding(
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
),
};
}
mutateElement(linearElement, { mutateElement(linearElement, {
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding, [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
}); });
@ -835,14 +842,19 @@ export const updateBoundElements = (
}> => update !== null, }> => update !== null,
); );
LinearElementEditor.movePoints(element, updates, { LinearElementEditor.movePoints(
element,
updates,
{
...(changedElement.id === element.startBinding?.elementId ...(changedElement.id === element.startBinding?.elementId
? { startBinding: bindings.startBinding } ? { startBinding: bindings.startBinding }
: {}), : {}),
...(changedElement.id === element.endBinding?.elementId ...(changedElement.id === element.endBinding?.elementId
? { endBinding: bindings.endBinding } ? { endBinding: bindings.endBinding }
: {}), : {}),
}); },
elementsMap as NonDeletedSceneElementsMap,
);
const boundText = getBoundTextElement(element, elementsMap); const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !boundText.isDeleted) { if (boundText && !boundText.isDeleted) {
@ -923,103 +935,104 @@ const getDistanceForBinding = (
export const bindPointToSnapToElementOutline = ( export const bindPointToSnapToElementOutline = (
arrow: ExcalidrawElbowArrowElement, arrow: ExcalidrawElbowArrowElement,
bindableElement: ExcalidrawBindableElement | undefined, bindableElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
): GlobalPoint => { ): GlobalPoint => {
const aabb = bindableElement && aabbForElement(bindableElement); if (isDevEnv() || isTestEnv()) {
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
}
const aabb = aabbForElement(bindableElement);
const localP = const localP =
arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1]; arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1];
const globalP = pointFrom<GlobalPoint>( const globalP = pointFrom<GlobalPoint>(
arrow.x + localP[0], arrow.x + localP[0],
arrow.y + localP[1], arrow.y + localP[1],
); );
const p = isRectanguloidElement(bindableElement) const edgePoint = isRectanguloidElement(bindableElement)
? avoidRectangularCorner(bindableElement, globalP) ? avoidRectangularCorner(bindableElement, globalP)
: globalP; : globalP;
const elbowed = isElbowArrow(arrow);
if (bindableElement && aabb) {
const center = getCenterForBounds(aabb); const center = getCenterForBounds(aabb);
const adjacentPointIdx = startOrEnd === "start" ? 1 : arrow.points.length - 2;
const intersection = intersectElementWithLineSegment( const adjacentPoint = pointRotateRads(
bindableElement, pointFrom<GlobalPoint>(
lineSegment( arrow.x + arrow.points[adjacentPointIdx][0],
center, arrow.y + arrow.points[adjacentPointIdx][1],
pointFromVector(
vectorScale(
vectorNormalize(vectorFromPoint(p, center)),
Math.max(bindableElement.width, bindableElement.height),
), ),
center, center,
arrow.angle ?? 0,
);
let intersection: GlobalPoint | null = null;
if (elbowed) {
const isHorizontal = headingIsHorizontal(
headingForPointFromElement(bindableElement, aabb, globalP),
);
const otherPoint = pointFrom<GlobalPoint>(
isHorizontal ? center[0] : edgePoint[0],
!isHorizontal ? center[1] : edgePoint[1],
);
intersection = intersectElementWithLineSegment(
bindableElement,
lineSegment(
otherPoint,
pointFromVector(
vectorScale(
vectorNormalize(vectorFromPoint(edgePoint, otherPoint)),
Math.max(bindableElement.width, bindableElement.height) * 2,
),
otherPoint,
), ),
), ),
)[0]; )[0];
const currentDistance = pointDistance(p, center); } else {
const fullDistance = Math.max( intersection = intersectElementWithLineSegment(
pointDistance(intersection ?? p, center), bindableElement,
PRECISION, lineSegment(
); adjacentPoint,
const ratio = round(currentDistance / fullDistance, 6); pointFromVector(
vectorScale(
switch (true) { vectorNormalize(vectorFromPoint(edgePoint, adjacentPoint)),
case ratio > 0.9: pointDistance(edgePoint, adjacentPoint) +
if ( Math.max(bindableElement.width, bindableElement.height) * 2,
currentDistance - fullDistance > FIXED_BINDING_DISTANCE || ),
// Too close to determine vector from intersection to p adjacentPoint,
pointDistanceSq(p, intersection) < PRECISION ),
) { ),
return p; FIXED_BINDING_DISTANCE,
).sort(
(g, h) =>
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
)[0];
} }
if (
!intersection ||
// Too close to determine vector from intersection to edgePoint
pointDistanceSq(edgePoint, intersection) < PRECISION
) {
return edgePoint;
}
if (elbowed) {
const scalar =
pointDistanceSq(edgePoint, center) -
pointDistanceSq(intersection, center) >
0
? FIXED_BINDING_DISTANCE
: -FIXED_BINDING_DISTANCE;
return pointFromVector( return pointFromVector(
vectorScale( vectorScale(
vectorNormalize(vectorFromPoint(p, intersection ?? center)), vectorNormalize(vectorFromPoint(edgePoint, intersection)),
ratio > 1 ? FIXED_BINDING_DISTANCE : -FIXED_BINDING_DISTANCE, scalar,
), ),
intersection ?? center, intersection,
);
default:
return headingToMidBindPoint(p, bindableElement, aabb);
}
}
return p;
};
const headingToMidBindPoint = (
p: GlobalPoint,
bindableElement: ExcalidrawBindableElement,
aabb: Bounds,
): GlobalPoint => {
const center = getCenterForBounds(aabb);
const heading = vectorToHeading(vectorFromPoint(p, center));
switch (true) {
case compareHeading(heading, HEADING_UP):
return pointRotateRads(
pointFrom((aabb[0] + aabb[2]) / 2 + 0.1, aabb[1]),
center,
bindableElement.angle,
);
case compareHeading(heading, HEADING_RIGHT):
return pointRotateRads(
pointFrom(aabb[2], (aabb[1] + aabb[3]) / 2 + 0.1),
center,
bindableElement.angle,
);
case compareHeading(heading, HEADING_DOWN):
return pointRotateRads(
pointFrom((aabb[0] + aabb[2]) / 2 - 0.1, aabb[3]),
center,
bindableElement.angle,
);
default:
return pointRotateRads(
pointFrom(aabb[0], (aabb[1] + aabb[3]) / 2 - 0.1),
center,
bindableElement.angle,
); );
} }
return edgePoint;
}; };
export const avoidRectangularCorner = ( export const avoidRectangularCorner = (
@ -1269,13 +1282,16 @@ const updateBoundPoint = (
pointDistance(adjacentPoint, edgePointAbsolute) + pointDistance(adjacentPoint, edgePointAbsolute) +
pointDistance(adjacentPoint, center) + pointDistance(adjacentPoint, center) +
Math.max(bindableElement.width, bindableElement.height) * 2; Math.max(bindableElement.width, bindableElement.height) * 2;
const intersections = intersectElementWithLineSegment( const intersections = [
...intersectElementWithLineSegment(
bindableElement, bindableElement,
lineSegment<GlobalPoint>( lineSegment<GlobalPoint>(
adjacentPoint, adjacentPoint,
pointFromVector( pointFromVector(
vectorScale( vectorScale(
vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)), vectorNormalize(
vectorFromPoint(focusPointAbsolute, adjacentPoint),
),
interceptorLength, interceptorLength,
), ),
adjacentPoint, adjacentPoint,
@ -1285,23 +1301,16 @@ const updateBoundPoint = (
).sort( ).sort(
(g, h) => (g, h) =>
pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint), pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint),
); ),
// Fallback when arrow doesn't point to the shape
// debugClear(); pointFromVector(
// debugDrawPoint(intersections[0], { color: "red", permanent: true }); vectorScale(
// debugDrawLine( vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)),
// lineSegment<GlobalPoint>( pointDistance(adjacentPoint, edgePointAbsolute),
// adjacentPoint, ),
// pointFromVector( adjacentPoint,
// vectorScale( ),
// vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)), ];
// interceptorLength,
// ),
// adjacentPoint,
// ),
// ),
// { permanent: true, color: "green" },
// );
if (intersections.length > 1) { if (intersections.length > 1) {
// The adjacent point is outside the shape (+ gap) // The adjacent point is outside the shape (+ gap)
@ -1413,107 +1422,75 @@ const getLinearElementEdgeCoors = (
); );
}; };
// We need to:
// 1: Update elements not selected to point to duplicated elements
// 2: Update duplicated elements to point to other duplicated elements
export const fixBindingsAfterDuplication = ( export const fixBindingsAfterDuplication = (
sceneElements: readonly ExcalidrawElement[], newElements: ExcalidrawElement[],
oldElements: readonly ExcalidrawElement[],
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>, oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
// There are three copying mechanisms: Copy-paste, duplication and alt-drag. duplicatedElementsMap: NonDeletedSceneElementsMap,
// Only when alt-dragging the new "duplicates" act as the "old", while ) => {
// the "old" elements act as the "new copy" - essentially working reverse for (const element of newElements) {
// to the other two. if ("boundElements" in element && element.boundElements) {
duplicatesServeAsOld?: "duplicatesServeAsOld" | undefined, Object.assign(element, {
): void => { boundElements: element.boundElements.reduce(
// First collect all the binding/bindable elements, so we only update
// each once, regardless of whether they were duplicated or not.
const allBoundElementIds: Set<ExcalidrawElement["id"]> = new Set();
const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set();
const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld";
const duplicateIdToOldId = new Map(
[...oldIdToDuplicatedId].map(([key, value]) => [value, key]),
);
oldElements.forEach((oldElement) => {
const { boundElements } = oldElement;
if (boundElements != null && boundElements.length > 0) {
boundElements.forEach((boundElement) => {
if (shouldReverseRoles && !oldIdToDuplicatedId.has(boundElement.id)) {
allBoundElementIds.add(boundElement.id);
}
});
allBindableElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!);
}
if (isBindingElement(oldElement)) {
if (oldElement.startBinding != null) {
const { elementId } = oldElement.startBinding;
if (shouldReverseRoles && !oldIdToDuplicatedId.has(elementId)) {
allBindableElementIds.add(elementId);
}
}
if (oldElement.endBinding != null) {
const { elementId } = oldElement.endBinding;
if (shouldReverseRoles && !oldIdToDuplicatedId.has(elementId)) {
allBindableElementIds.add(elementId);
}
}
if (oldElement.startBinding != null || oldElement.endBinding != null) {
allBoundElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!);
}
}
});
// Update the linear elements
( (
sceneElements.filter(({ id }) => acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
allBoundElementIds.has(id), binding,
) as ExcalidrawLinearElement[] ) => {
).forEach((element) => { const newBindingId = oldIdToDuplicatedId.get(binding.id);
const { startBinding, endBinding } = element; if (newBindingId) {
mutateElement(element, { acc.push({ ...binding, id: newBindingId });
startBinding: newBindingAfterDuplication( }
startBinding, return acc;
oldIdToDuplicatedId, },
[],
), ),
endBinding: newBindingAfterDuplication(endBinding, oldIdToDuplicatedId),
}); });
}
if ("containerId" in element && element.containerId) {
Object.assign(element, {
containerId: oldIdToDuplicatedId.get(element.containerId) ?? null,
}); });
}
// Update the bindable shapes if ("endBinding" in element && element.endBinding) {
sceneElements const newEndBindingId = oldIdToDuplicatedId.get(
.filter(({ id }) => allBindableElementIds.has(id)) element.endBinding.elementId,
.forEach((bindableElement) => { );
const oldElementId = duplicateIdToOldId.get(bindableElement.id); Object.assign(element, {
const boundElements = sceneElements.find( endBinding: newEndBindingId
({ id }) => id === oldElementId,
)?.boundElements;
if (boundElements && boundElements.length > 0) {
mutateElement(bindableElement, {
boundElements: boundElements.map((boundElement) =>
oldIdToDuplicatedId.has(boundElement.id)
? { ? {
id: oldIdToDuplicatedId.get(boundElement.id)!, ...element.endBinding,
type: boundElement.type, elementId: newEndBindingId,
} }
: boundElement, : null,
),
}); });
} }
if ("startBinding" in element && element.startBinding) {
const newEndBindingId = oldIdToDuplicatedId.get(
element.startBinding.elementId,
);
Object.assign(element, {
startBinding: newEndBindingId
? {
...element.startBinding,
elementId: newEndBindingId,
}
: null,
}); });
}; }
const newBindingAfterDuplication = ( if (isElbowArrow(element)) {
binding: PointBinding | null, Object.assign(
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>, element,
): PointBinding | null => { updateElbowArrowPoints(element, duplicatedElementsMap, {
if (binding == null) { points: [
return null; element.points[0],
element.points[element.points.length - 1],
],
}),
);
}
} }
return {
...binding,
elementId: oldIdToDuplicatedId.get(binding.elementId) ?? binding.elementId,
};
}; };
export const fixBindingsAfterDeletion = ( export const fixBindingsAfterDeletion = (
@ -1724,21 +1701,6 @@ const determineFocusDistance = (
) )
.sort((g, h) => Math.abs(g) - Math.abs(h)); .sort((g, h) => Math.abs(g) - Math.abs(h));
// debugClear();
// [
// lineSegmentIntersectionPoints(rotatedInterceptor, interceptees[0]),
// lineSegmentIntersectionPoints(rotatedInterceptor, interceptees[1]),
// ]
// .filter((p): p is GlobalPoint => p !== null)
// .forEach((p) => debugDrawPoint(p, { color: "black", permanent: true }));
// debugDrawPoint(determineFocusPoint(element, ordered[0] ?? 0, rotatedA), {
// color: "red",
// permanent: true,
// });
// debugDrawLine(rotatedInterceptor, { color: "green", permanent: true });
// debugDrawLine(interceptees[0], { color: "red", permanent: true });
// debugDrawLine(interceptees[1], { color: "red", permanent: true });
const signedDistanceRatio = ordered[0] ?? 0; const signedDistanceRatio = ordered[0] ?? 0;
return signedDistanceRatio; return signedDistanceRatio;

View file

@ -1,3 +1,42 @@
import rough from "roughjs/bin/rough";
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
import {
degreesToRadians,
lineSegment,
pointFrom,
pointDistance,
pointFromArray,
pointRotateRads,
} from "@excalidraw/math";
import { getCurvePathOps } from "@excalidraw/utils/shape";
import type {
Degrees,
GlobalPoint,
LineSegment,
LocalPoint,
Radians,
} from "@excalidraw/math";
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 {
isArrowElement,
isBoundToContainer,
isFreeDrawElement,
isLinearElement,
isTextElement,
} from "./typeChecks";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
@ -7,40 +46,8 @@ import type {
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ElementsMap, ElementsMap,
} from "./types"; } from "./types";
import rough from "roughjs/bin/rough";
import type { Point as RoughPoint } from "roughjs/bin/geometry";
import type { Drawable, Op } from "roughjs/bin/core"; import type { Drawable, Op } from "roughjs/bin/core";
import type { AppState } from "../types"; import type { Point as RoughPoint } from "roughjs/bin/geometry";
import { generateRoughOptions } from "../scene/Shape";
import {
isArrowElement,
isBoundToContainer,
isFreeDrawElement,
isLinearElement,
isTextElement,
} from "./typeChecks";
import { rescalePoints } from "../points";
import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
import { ShapeCache } from "../scene/ShapeCache";
import { arrayToMap, invariant } from "../utils";
import type {
Degrees,
GlobalPoint,
LineSegment,
LocalPoint,
Radians,
} from "@excalidraw/math";
import {
degreesToRadians,
lineSegment,
pointFrom,
pointDistance,
pointFromArray,
pointRotateRads,
} from "@excalidraw/math";
import type { Mutable } from "../utility-types";
import { getCurvePathOps } from "@excalidraw/utils/geometry/shape";
export type RectangleBox = { export type RectangleBox = {
x: number; x: number;

View file

@ -1,31 +1,4 @@
import type { import { isTransparent } from "@excalidraw/common";
ElementsMap,
ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawEllipseElement,
ExcalidrawRectangleElement,
ExcalidrawRectanguloidElement,
} from "./types";
import { getElementBounds } from "./bounds";
import type { FrameNameBounds } from "../types";
import type { GeometricShape } from "@excalidraw/utils/geometry/shape";
import { getPolygonShape } from "@excalidraw/utils/geometry/shape";
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
import { isTransparent } from "../utils";
import {
hasBoundTextElement,
isIframeLikeElement,
isImageElement,
isTextElement,
} from "./typeChecks";
import { getBoundTextShape, isPathALoop } from "../shapes";
import type {
GlobalPoint,
LineSegment,
LocalPoint,
Polygon,
Radians,
} from "@excalidraw/math";
import { import {
curveIntersectLineSegment, curveIntersectLineSegment,
isPointWithinBounds, isPointWithinBounds,
@ -36,15 +9,49 @@ import {
pointRotateRads, pointRotateRads,
pointsEqual, pointsEqual,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { import {
ellipse, ellipse,
ellipseLineIntersectionPoints, ellipseLineIntersectionPoints,
} from "@excalidraw/math/ellipse"; } from "@excalidraw/math/ellipse";
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
import { getPolygonShape } from "@excalidraw/utils/shape";
import type {
GlobalPoint,
LineSegment,
LocalPoint,
Polygon,
Radians,
} from "@excalidraw/math";
import type { GeometricShape } from "@excalidraw/utils/shape";
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
import { getBoundTextShape, isPathALoop } from "./shapes";
import { getElementBounds } from "./bounds";
import {
hasBoundTextElement,
isIframeLikeElement,
isImageElement,
isTextElement,
} from "./typeChecks";
import { import {
deconstructDiamondElement, deconstructDiamondElement,
deconstructRectanguloidElement, deconstructRectanguloidElement,
} from "./utils"; } from "./utils";
import type {
ElementsMap,
ExcalidrawDiamondElement,
ExcalidrawElement,
ExcalidrawEllipseElement,
ExcalidrawRectangleElement,
ExcalidrawRectanguloidElement,
} from "./types";
export const shouldTestInside = (element: ExcalidrawElement) => { export const shouldTestInside = (element: ExcalidrawElement) => {
if (element.type === "arrow") { if (element.type === "arrow") {
return false; return false;
@ -205,10 +212,9 @@ const intersectRectanguloidWithLineSegment = (
const [sides, corners] = deconstructRectanguloidElement(element, offset); const [sides, corners] = deconstructRectanguloidElement(element, offset);
return ( return (
[
// Test intersection against the sides, keep only the valid // Test intersection against the sides, keep only the valid
// intersection points and rotate them back to scene space // intersection points and rotate them back to scene space
...sides sides
.map((s) => .map((s) =>
lineSegmentIntersectionPoints( lineSegmentIntersectionPoints(
lineSegment<GlobalPoint>(rotatedA, rotatedB), lineSegment<GlobalPoint>(rotatedA, rotatedB),
@ -216,17 +222,18 @@ const intersectRectanguloidWithLineSegment = (
), ),
) )
.filter((x) => x != null) .filter((x) => x != null)
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle)), .map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle))
// Test intersection against the corners which are cubic bezier curves, // Test intersection against the corners which are cubic bezier curves,
// keep only the valid intersection points and rotate them back to scene // keep only the valid intersection points and rotate them back to scene
// space // space
...corners .concat(
corners
.flatMap((t) => .flatMap((t) =>
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)), curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
) )
.filter((i) => i != null) .filter((i) => i != null)
.map((j) => pointRotateRads(j, center, element.angle)), .map((j) => pointRotateRads(j, center, element.angle)),
] )
// Remove duplicates // Remove duplicates
.filter( .filter(
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx, (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
@ -259,8 +266,7 @@ const intersectDiamondWithLineSegment = (
const [sides, curves] = deconstructDiamondElement(element, offset); const [sides, curves] = deconstructDiamondElement(element, offset);
return ( return (
[ sides
...sides
.map((s) => .map((s) =>
lineSegmentIntersectionPoints( lineSegmentIntersectionPoints(
lineSegment<GlobalPoint>(rotatedA, rotatedB), lineSegment<GlobalPoint>(rotatedA, rotatedB),
@ -269,15 +275,16 @@ const intersectDiamondWithLineSegment = (
) )
.filter((p): p is GlobalPoint => p != null) .filter((p): p is GlobalPoint => p != null)
// Rotate back intersection points // Rotate back intersection points
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle)), .map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle))
...curves .concat(
curves
.flatMap((p) => .flatMap((p) =>
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)), curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
) )
.filter((p) => p != null) .filter((p) => p != null)
// Rotate back intersection points // Rotate back intersection points
.map((p) => pointRotateRads(p, center, element.angle)), .map((p) => pointRotateRads(p, center, element.angle)),
] )
// Remove duplicates // Remove duplicates
.filter( .filter(
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx, (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,

View file

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

View file

@ -1,4 +1,3 @@
import { type Point } from "points-on-curve";
import { import {
type Radians, type Radians,
pointFrom, pointFrom,
@ -13,6 +12,13 @@ import {
clamp, clamp,
isCloseTo, isCloseTo,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { type Point } from "points-on-curve";
import {
getElementAbsoluteCoords,
getResizedElementAbsoluteCoords,
} from "./bounds";
import type { TransformHandleType } from "./transformHandles"; import type { TransformHandleType } from "./transformHandles";
import type { import type {
ElementsMap, ElementsMap,
@ -21,10 +27,6 @@ import type {
ImageCrop, ImageCrop,
NonDeleted, NonDeleted,
} from "./types"; } from "./types";
import {
getElementAbsoluteCoords,
getResizedElementAbsoluteCoords,
} from "./bounds";
export const MINIMAL_CROP_SIZE = 10; export const MINIMAL_CROP_SIZE = 10;

View file

@ -1,21 +1,25 @@
import type { GlobalPoint, Radians } from "@excalidraw/math";
import { import {
curvePointDistance, curvePointDistance,
distanceToLineSegment, distanceToLineSegment,
pointFrom, pointFrom,
pointRotateRads, pointRotateRads,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse"; import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
import type { GlobalPoint, Radians } from "@excalidraw/math";
import {
deconstructDiamondElement,
deconstructRectanguloidElement,
} from "./utils";
import type { import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawDiamondElement, ExcalidrawDiamondElement,
ExcalidrawEllipseElement, ExcalidrawEllipseElement,
ExcalidrawRectanguloidElement, ExcalidrawRectanguloidElement,
} from "./types"; } from "./types";
import {
deconstructDiamondElement,
deconstructRectanguloidElement,
} from "./utils";
export const distanceToBindableElement = ( export const distanceToBindableElement = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,

View file

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

View file

@ -1,17 +1,26 @@
import { updateBoundElements } from "./binding"; import {
import type { Bounds } from "./bounds"; TEXT_AUTOWRAP_THRESHOLD,
import { getCommonBounds } from "./bounds"; getGridPoint,
import { mutateElement } from "./mutateElement"; getFontString,
import { getPerfectElementSize } from "./sizeHelpers"; } from "@excalidraw/common";
import type { NonDeletedExcalidrawElement } from "./types";
import type { import type {
AppState, AppState,
NormalizedZoomValue, NormalizedZoomValue,
NullableGridSize, NullableGridSize,
PointerDownState, PointerDownState,
} from "../types"; } 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";
import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers";
import { getBoundTextElement } from "./textElement"; import { getBoundTextElement } from "./textElement";
import type Scene from "../scene/Scene"; import { getMinTextElementWidth } from "./textMeasurements";
import { import {
isArrowElement, isArrowElement,
isElbowArrow, isElbowArrow,
@ -19,10 +28,9 @@ import {
isImageElement, isImageElement,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { getFontString } from "../utils";
import { TEXT_AUTOWRAP_THRESHOLD } from "../constants"; import type { Bounds } from "./bounds";
import { getGridPoint } from "../snapping"; import type { ExcalidrawElement } from "./types";
import { getMinTextElementWidth } from "./textMeasurements";
export const dragSelectedElements = ( export const dragSelectedElements = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
@ -76,13 +84,20 @@ export const dragSelectedElements = (
} }
} }
const commonBounds = getCommonBounds( const origElements: ExcalidrawElement[] = [];
Array.from(elementsToUpdate).map(
(el) => pointerDownState.originalElements.get(el.id) ?? el, for (const element of elementsToUpdate) {
), const origElement = pointerDownState.originalElements.get(element.id);
); // if original element is not set (e.g. when you duplicate during a drag
// operation), exit to avoid undefined behavior
if (!origElement) {
return;
}
origElements.push(origElement);
}
const adjustedOffset = calculateOffset( const adjustedOffset = calculateOffset(
commonBounds, getCommonBounds(origElements),
offset, offset,
snapOffset, snapOffset,
gridSize, gridSize,

View file

@ -0,0 +1,485 @@
import {
ORIG_ID,
randomId,
randomInteger,
arrayToMap,
castArray,
findLastIndex,
getUpdatedTimestamp,
isTestEnv,
} from "@excalidraw/common";
import type { Mutable } from "@excalidraw/common/utility-types";
import type { AppState } from "@excalidraw/excalidraw/types";
import {
getElementsInGroup,
getNewGroupIdsForDuplication,
getSelectedGroupForElement,
} from "./groups";
import {
bindElementsToFramesAfterDuplication,
getFrameChildren,
} from "./frame";
import { normalizeElementOrder } from "./sortElements";
import { bumpVersion } from "./mutateElement";
import {
hasBoundTextElement,
isBoundToContainer,
isFrameLikeElement,
} from "./typeChecks";
import { getBoundTextElement, getContainerElement } from "./textElement";
import { fixBindingsAfterDuplication } from "./binding";
import type {
ElementsMap,
ExcalidrawElement,
GroupId,
NonDeletedSceneElementsMap,
} from "./types";
/**
* Duplicate an element, often used in the alt-drag operation.
* Note that this method has gotten a bit complicated since the
* introduction of gruoping/ungrouping elements.
* @param editingGroupId The current group being edited. The new
* element will inherit this group and its
* parents.
* @param groupIdMapForOperation A Map that maps old group IDs to
* duplicated ones. If you are duplicating
* multiple elements at once, share this map
* amongst all of them
* @param element Element to duplicate
* @param overrides Any element properties to override
*/
export const duplicateElement = <TElement extends ExcalidrawElement>(
editingGroupId: AppState["editingGroupId"],
groupIdMapForOperation: Map<GroupId, GroupId>,
element: TElement,
overrides?: Partial<TElement>,
randomizeSeed?: boolean,
): Readonly<TElement> => {
let copy = deepCopyElement(element);
if (isTestEnv()) {
__test__defineOrigId(copy, element.id);
}
copy.id = randomId();
copy.updated = getUpdatedTimestamp();
if (randomizeSeed) {
copy.seed = randomInteger();
bumpVersion(copy);
}
copy.groupIds = getNewGroupIdsForDuplication(
copy.groupIds,
editingGroupId,
(groupId) => {
if (!groupIdMapForOperation.has(groupId)) {
groupIdMapForOperation.set(groupId, randomId());
}
return groupIdMapForOperation.get(groupId)!;
},
);
if (overrides) {
copy = Object.assign(copy, overrides);
}
return copy;
};
export const duplicateElements = (
opts: {
elements: readonly ExcalidrawElement[];
randomizeSeed?: boolean;
overrides?: (
originalElement: ExcalidrawElement,
) => Partial<ExcalidrawElement>;
} & (
| {
/**
* Duplicates all elements in array.
*
* Use this when programmaticaly duplicating elements, without direct
* user interaction.
*/
type: "everything";
}
| {
/**
* Duplicates specified elements and inserts them back into the array
* in specified order.
*
* Use this when duplicating Scene elements, during user interaction
* such as alt-drag or on duplicate action.
*/
type: "in-place";
idsOfElementsToDuplicate: Map<
ExcalidrawElement["id"],
ExcalidrawElement
>;
appState: {
editingGroupId: AppState["editingGroupId"];
selectedGroupIds: AppState["selectedGroupIds"];
};
/**
* If true, duplicated elements are inserted _before_ specified
* elements. Case: alt-dragging elements to duplicate them.
*
* TODO: remove this once (if) we stop replacing the original element
* with the duplicated one in the scene array.
*/
reverseOrder: boolean;
}
),
) => {
let { elements } = opts;
const appState =
"appState" in opts
? opts.appState
: ({
editingGroupId: null,
selectedGroupIds: {},
} as const);
const reverseOrder = opts.type === "in-place" ? opts.reverseOrder : false;
// Ids of elements that have already been processed so we don't push them
// into the array twice if we end up backtracking when retrieving
// discontiguous group of elements (can happen due to a bug, or in edge
// cases such as a group containing deleted elements which were not selected).
//
// This is not enough to prevent duplicates, so we do a second loop afterwards
// to remove them.
//
// For convenience we mark even the newly created ones even though we don't
// loop over them.
const processedIds = new Map<ExcalidrawElement["id"], true>();
const groupIdMap = new Map();
const newElements: ExcalidrawElement[] = [];
const oldElements: ExcalidrawElement[] = [];
const oldIdToDuplicatedId = new Map();
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
const elementsMap = arrayToMap(elements) as ElementsMap;
const _idsOfElementsToDuplicate =
opts.type === "in-place"
? opts.idsOfElementsToDuplicate
: new Map(elements.map((el) => [el.id, el]));
// For sanity
if (opts.type === "in-place") {
for (const groupId of Object.keys(opts.appState.selectedGroupIds)) {
elements
.filter((el) => el.groupIds?.includes(groupId))
.forEach((el) => _idsOfElementsToDuplicate.set(el.id, el));
}
}
elements = normalizeElementOrder(elements);
const elementsWithClones: ExcalidrawElement[] = elements.slice();
// helper functions
// -------------------------------------------------------------------------
// Used for the heavy lifing of copying a single element, a group of elements
// an element with bound text etc.
const copyElements = <T extends ExcalidrawElement | ExcalidrawElement[]>(
element: T,
): T extends ExcalidrawElement[]
? ExcalidrawElement[]
: ExcalidrawElement | null => {
const elements = castArray(element);
const _newElements = elements.reduce(
(acc: ExcalidrawElement[], element) => {
if (processedIds.has(element.id)) {
return acc;
}
processedIds.set(element.id, true);
const newElement = duplicateElement(
appState.editingGroupId,
groupIdMap,
element,
opts.overrides?.(element),
opts.randomizeSeed,
);
processedIds.set(newElement.id, true);
duplicatedElementsMap.set(newElement.id, newElement);
oldIdToDuplicatedId.set(element.id, newElement.id);
oldElements.push(element);
newElements.push(newElement);
acc.push(newElement);
return acc;
},
[],
);
return (
Array.isArray(element) ? _newElements : _newElements[0] || null
) as T extends ExcalidrawElement[]
? ExcalidrawElement[]
: ExcalidrawElement | null;
};
// Helper to position cloned elements in the Z-order the product needs it
const insertBeforeOrAfterIndex = (
index: number,
elements: ExcalidrawElement | null | ExcalidrawElement[],
) => {
if (!elements) {
return;
}
if (reverseOrder && index < 1) {
elementsWithClones.unshift(...castArray(elements));
return;
}
if (!reverseOrder && index > elementsWithClones.length - 1) {
elementsWithClones.push(...castArray(elements));
return;
}
elementsWithClones.splice(
index + (reverseOrder ? 0 : 1),
0,
...castArray(elements),
);
};
const frameIdsToDuplicate = new Set(
elements
.filter(
(el) => _idsOfElementsToDuplicate.has(el.id) && isFrameLikeElement(el),
)
.map((el) => el.id),
);
for (const element of elements) {
if (processedIds.has(element.id)) {
continue;
}
if (!_idsOfElementsToDuplicate.has(element.id)) {
continue;
}
// groups
// -------------------------------------------------------------------------
const groupId = getSelectedGroupForElement(appState, element);
if (groupId) {
const groupElements = getElementsInGroup(elements, groupId).flatMap(
(element) =>
isFrameLikeElement(element)
? [...getFrameChildren(elements, element.id), element]
: [element],
);
const targetIndex = reverseOrder
? elementsWithClones.findIndex((el) => {
return el.groupIds?.includes(groupId);
})
: findLastIndex(elementsWithClones, (el) => {
return el.groupIds?.includes(groupId);
});
insertBeforeOrAfterIndex(targetIndex, copyElements(groupElements));
continue;
}
// frame duplication
// -------------------------------------------------------------------------
if (element.frameId && frameIdsToDuplicate.has(element.frameId)) {
continue;
}
if (isFrameLikeElement(element)) {
const frameId = element.id;
const frameChildren = getFrameChildren(elements, frameId);
const targetIndex = findLastIndex(elementsWithClones, (el) => {
return el.frameId === frameId || el.id === frameId;
});
insertBeforeOrAfterIndex(
targetIndex,
copyElements([...frameChildren, element]),
);
continue;
}
// text container
// -------------------------------------------------------------------------
if (hasBoundTextElement(element)) {
const boundTextElement = getBoundTextElement(element, elementsMap);
const targetIndex = findLastIndex(elementsWithClones, (el) => {
return (
el.id === element.id ||
("containerId" in el && el.containerId === element.id)
);
});
if (boundTextElement) {
insertBeforeOrAfterIndex(
targetIndex + (reverseOrder ? -1 : 0),
copyElements([element, boundTextElement]),
);
} else {
insertBeforeOrAfterIndex(targetIndex, copyElements(element));
}
continue;
}
if (isBoundToContainer(element)) {
const container = getContainerElement(element, elementsMap);
const targetIndex = findLastIndex(elementsWithClones, (el) => {
return el.id === element.id || el.id === container?.id;
});
if (container) {
insertBeforeOrAfterIndex(
targetIndex,
copyElements([container, element]),
);
} else {
insertBeforeOrAfterIndex(targetIndex, copyElements(element));
}
continue;
}
// default duplication (regular elements)
// -------------------------------------------------------------------------
insertBeforeOrAfterIndex(
findLastIndex(elementsWithClones, (el) => el.id === element.id),
copyElements(element),
);
}
// ---------------------------------------------------------------------------
fixBindingsAfterDuplication(
newElements,
oldIdToDuplicatedId,
duplicatedElementsMap as NonDeletedSceneElementsMap,
);
bindElementsToFramesAfterDuplication(
elementsWithClones,
oldElements,
oldIdToDuplicatedId,
);
return {
newElements,
elementsWithClones,
};
};
// Simplified deep clone for the purpose of cloning ExcalidrawElement.
//
// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
// Typed arrays and other non-null objects.
//
// Adapted from https://github.com/lukeed/klona
//
// The reason for `deepCopyElement()` wrapper is type safety (only allow
// passing ExcalidrawElement as the top-level argument).
const _deepCopyElement = (val: any, depth: number = 0) => {
// only clone non-primitives
if (val == null || typeof val !== "object") {
return val;
}
const objectType = Object.prototype.toString.call(val);
if (objectType === "[object Object]") {
const tmp =
typeof val.constructor === "function"
? Object.create(Object.getPrototypeOf(val))
: {};
for (const key in val) {
if (val.hasOwnProperty(key)) {
// don't copy non-serializable objects like these caches. They'll be
// populated when the element is rendered.
if (depth === 0 && (key === "shape" || key === "canvas")) {
continue;
}
tmp[key] = _deepCopyElement(val[key], depth + 1);
}
}
return tmp;
}
if (Array.isArray(val)) {
let k = val.length;
const arr = new Array(k);
while (k--) {
arr[k] = _deepCopyElement(val[k], depth + 1);
}
return arr;
}
// we're not cloning non-array & non-plain-object objects because we
// don't support them on excalidraw elements yet. If we do, we need to make
// sure we start cloning them, so let's warn about it.
if (import.meta.env.DEV) {
if (
objectType !== "[object Object]" &&
objectType !== "[object Array]" &&
objectType.startsWith("[object ")
) {
console.warn(
`_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`,
);
}
}
return val;
};
/**
* Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or
* any value. The purpose is to to break object references for immutability
* reasons, whenever we want to keep the original element, but ensure it's not
* mutated.
*
* Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
* Typed arrays and other non-null objects.
*/
export const deepCopyElement = <T extends ExcalidrawElement>(
val: T,
): Mutable<T> => {
return _deepCopyElement(val);
};
const __test__defineOrigId = (clonedObj: object, origId: string) => {
Object.defineProperty(clonedObj, ORIG_ID, {
value: origId,
writable: false,
enumerable: false,
});
};

View file

@ -12,11 +12,18 @@ import {
type GlobalPoint, type GlobalPoint,
type LocalPoint, type LocalPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
import BinaryHeap from "../binaryheap";
import { getSizeFromPoints } from "../points"; import {
import { aabbForElement, pointInsideBounds } from "../shapes"; BinaryHeap,
import { invariant, isAnyTrue, tupleToCoors } from "../utils"; invariant,
import type { AppState } from "../types"; isAnyTrue,
tupleToCoors,
getSizeFromPoints,
isDevEnv,
} from "@excalidraw/common";
import type { AppState } from "@excalidraw/excalidraw/types";
import { import {
bindPointToSnapToElementOutline, bindPointToSnapToElementOutline,
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
@ -25,8 +32,7 @@ import {
snapToMid, snapToMid,
getHoveredElementForBinding, getHoveredElementForBinding,
} from "./binding"; } from "./binding";
import type { Bounds } from "./bounds"; import { distanceToBindableElement } from "./distance";
import type { Heading } from "./heading";
import { import {
compareHeading, compareHeading,
flipHeading, flipHeading,
@ -46,6 +52,11 @@ import {
type NonDeletedSceneElementsMap, type NonDeletedSceneElementsMap,
type SceneElementsMap, type SceneElementsMap,
} from "./types"; } from "./types";
import { aabbForElement, pointInsideBounds } from "./shapes";
import type { Bounds } from "./bounds";
import type { Heading } from "./heading";
import type { import type {
Arrowhead, Arrowhead,
ElementsMap, ElementsMap,
@ -54,7 +65,6 @@ import type {
FixedSegment, FixedSegment,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./types"; } from "./types";
import { distanceToBindableElement } from "./distance";
type GridAddress = [number, number] & { _brand: "gridaddress" }; type GridAddress = [number, number] & { _brand: "gridaddress" };
@ -235,16 +245,6 @@ const handleSegmentRenormalization = (
nextPoints.map((p) => nextPoints.map((p) =>
pointFrom<LocalPoint>(p[0] - arrow.x, p[1] - arrow.y), pointFrom<LocalPoint>(p[0] - arrow.x, p[1] - arrow.y),
), ),
arrow.startBinding &&
getBindableElementForId(
arrow.startBinding.elementId,
elementsMap,
),
arrow.endBinding &&
getBindableElementForId(
arrow.endBinding.elementId,
elementsMap,
),
), ),
) ?? [], ) ?? [],
), ),
@ -255,7 +255,7 @@ const handleSegmentRenormalization = (
); );
} }
import.meta.env.DEV && isDevEnv() &&
invariant( invariant(
validateElbowPoints(nextPoints), validateElbowPoints(nextPoints),
"Invalid elbow points with fixed segments", "Invalid elbow points with fixed segments",
@ -338,9 +338,6 @@ const handleSegmentRelease = (
y, y,
), ),
], ],
startBinding &&
getBindableElementForId(startBinding.elementId, elementsMap),
endBinding && getBindableElementForId(endBinding.elementId, elementsMap),
{ isDragging: false }, { isDragging: false },
); );
@ -980,6 +977,8 @@ export const updateElbowArrowPoints = (
); );
} }
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
const updatedPoints: readonly LocalPoint[] = updates.points const updatedPoints: readonly LocalPoint[] = updates.points
? updates.points && updates.points.length === 2 ? updates.points && updates.points.length === 2
? arrow.points.map((p, idx) => ? arrow.points.map((p, idx) =>
@ -992,26 +991,36 @@ export const updateElbowArrowPoints = (
: updates.points.slice() : updates.points.slice()
: arrow.points.slice(); : arrow.points.slice();
// 0. During all element replacement in the scene, we just need to renormalize // During all element replacement in the scene, we just need to renormalize
// the arrow // the arrow
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed // TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
const {
startBinding: updatedStartBinding,
endBinding: updatedEndBinding,
...restOfTheUpdates
} = updates;
const startBinding = const startBinding =
typeof updates.startBinding !== "undefined" typeof updatedStartBinding !== "undefined"
? updates.startBinding ? updatedStartBinding
: arrow.startBinding; : arrow.startBinding;
const endBinding = const endBinding =
typeof updates.endBinding !== "undefined" typeof updatedEndBinding !== "undefined"
? updates.endBinding ? updatedEndBinding
: arrow.endBinding; : arrow.endBinding;
const startElement = const startElement =
startBinding && startBinding &&
getBindableElementForId(startBinding.elementId, elementsMap); getBindableElementForId(startBinding.elementId, elementsMap);
const endElement = const endElement =
endBinding && getBindableElementForId(endBinding.elementId, elementsMap); endBinding && getBindableElementForId(endBinding.elementId, elementsMap);
const areUpdatedPointsValid = validateElbowPoints(updatedPoints);
if ( if (
(elementsMap.size === 0 && validateElbowPoints(updatedPoints)) || (startBinding && !startElement && areUpdatedPointsValid) ||
startElement?.id !== startBinding?.elementId || (endBinding && !endElement && areUpdatedPointsValid) ||
endElement?.id !== endBinding?.elementId (elementsMap.size === 0 && areUpdatedPointsValid) ||
(Object.keys(restOfTheUpdates).length === 0 &&
(startElement?.id !== startBinding?.elementId ||
endElement?.id !== endBinding?.elementId))
) { ) {
return normalizeArrowElementUpdate( return normalizeArrowElementUpdate(
updatedPoints.map((p) => updatedPoints.map((p) =>
@ -1043,12 +1052,22 @@ export const updateElbowArrowPoints = (
}, },
elementsMap, elementsMap,
updatedPoints, updatedPoints,
startElement,
endElement,
options, options,
); );
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? []; // 0. During all element replacement in the scene, we just need to renormalize
// the arrow
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
if (elementsMap.size === 0 && areUpdatedPointsValid) {
return normalizeArrowElementUpdate(
updatedPoints.map((p) =>
pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1]),
),
arrow.fixedSegments,
arrow.startIsSpecial,
arrow.endIsSpecial,
);
}
//// ////
// 1. Renormalize the arrow // 1. Renormalize the arrow
@ -1071,7 +1090,8 @@ export const updateElbowArrowPoints = (
p, p,
arrow.points[i] ?? pointFrom<LocalPoint>(Infinity, Infinity), arrow.points[i] ?? pointFrom<LocalPoint>(Infinity, Infinity),
), ),
) ) &&
areUpdatedPointsValid
) { ) {
return {}; return {};
} }
@ -1182,8 +1202,6 @@ const getElbowArrowData = (
}, },
elementsMap: NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
nextPoints: readonly LocalPoint[], nextPoints: readonly LocalPoint[],
startElement: ExcalidrawBindableElement | null,
endElement: ExcalidrawBindableElement | null,
options?: { options?: {
isDragging?: boolean; isDragging?: boolean;
zoom?: AppState["zoom"]; zoom?: AppState["zoom"];
@ -1198,8 +1216,8 @@ const getElbowArrowData = (
GlobalPoint GlobalPoint
>(nextPoints[nextPoints.length - 1], vector(arrow.x, arrow.y)); >(nextPoints[nextPoints.length - 1], vector(arrow.x, arrow.y));
let hoveredStartElement = startElement; let hoveredStartElement = null;
let hoveredEndElement = endElement; let hoveredEndElement = null;
if (options?.isDragging) { if (options?.isDragging) {
const elements = Array.from(elementsMap.values()); const elements = Array.from(elementsMap.values());
hoveredStartElement = hoveredStartElement =
@ -1208,39 +1226,47 @@ const getElbowArrowData = (
elementsMap, elementsMap,
elements, elements,
options?.zoom, options?.zoom,
) || startElement; ) || null;
hoveredEndElement = hoveredEndElement =
getHoveredElement( getHoveredElement(
origEndGlobalPoint, origEndGlobalPoint,
elementsMap, elementsMap,
elements, elements,
options?.zoom, options?.zoom,
) || endElement; ) || null;
} else {
hoveredStartElement = arrow.startBinding
? getBindableElementForId(arrow.startBinding.elementId, elementsMap) ||
null
: null;
hoveredEndElement = arrow.endBinding
? getBindableElementForId(arrow.endBinding.elementId, elementsMap) || null
: null;
} }
const startGlobalPoint = getGlobalPoint( const startGlobalPoint = getGlobalPoint(
{ {
...arrow, ...arrow,
type: "arrow",
elbowed: true, elbowed: true,
points: nextPoints, points: nextPoints,
} as ExcalidrawElbowArrowElement, } as ExcalidrawElbowArrowElement,
"start", "start",
arrow.startBinding?.fixedPoint, arrow.startBinding?.fixedPoint,
origStartGlobalPoint, origStartGlobalPoint,
startElement,
hoveredStartElement, hoveredStartElement,
options?.isDragging, options?.isDragging,
); );
const endGlobalPoint = getGlobalPoint( const endGlobalPoint = getGlobalPoint(
{ {
...arrow, ...arrow,
type: "arrow",
elbowed: true, elbowed: true,
points: nextPoints, points: nextPoints,
} as ExcalidrawElbowArrowElement, } as ExcalidrawElbowArrowElement,
"end", "end",
arrow.endBinding?.fixedPoint, arrow.endBinding?.fixedPoint,
origEndGlobalPoint, origEndGlobalPoint,
endElement,
hoveredEndElement, hoveredEndElement,
options?.isDragging, options?.isDragging,
); );
@ -2186,36 +2212,35 @@ const getGlobalPoint = (
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
fixedPointRatio: [number, number] | undefined | null, fixedPointRatio: [number, number] | undefined | null,
initialPoint: GlobalPoint, initialPoint: GlobalPoint,
boundElement?: ExcalidrawBindableElement | null, element?: ExcalidrawBindableElement | null,
hoveredElement?: ExcalidrawBindableElement | null,
isDragging?: boolean, isDragging?: boolean,
): GlobalPoint => { ): GlobalPoint => {
if (isDragging) { if (isDragging) {
if (hoveredElement) { if (element) {
const snapPoint = bindPointToSnapToElementOutline( const snapPoint = bindPointToSnapToElementOutline(
arrow, arrow,
hoveredElement, element,
startOrEnd, startOrEnd,
); );
return snapToMid(hoveredElement, snapPoint); return snapToMid(element, snapPoint);
} }
return initialPoint; return initialPoint;
} }
if (boundElement) { if (element) {
const fixedGlobalPoint = getGlobalFixedPointForBindableElement( const fixedGlobalPoint = getGlobalFixedPointForBindableElement(
fixedPointRatio || [0, 0], fixedPointRatio || [0, 0],
boundElement, element,
); );
// NOTE: Resize scales the binding position point too, so we need to update it // NOTE: Resize scales the binding position point too, so we need to update it
return Math.abs( return Math.abs(
distanceToBindableElement(boundElement, fixedGlobalPoint) - distanceToBindableElement(element, fixedGlobalPoint) -
FIXED_BINDING_DISTANCE, FIXED_BINDING_DISTANCE,
) > 0.01 ) > 0.01
? bindPointToSnapToElementOutline(arrow, boundElement, startOrEnd) ? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
: fixedGlobalPoint; : fixedGlobalPoint;
} }

View file

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

View file

@ -1,18 +1,22 @@
import { register } from "../actions/register"; import {
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants"; FONT_FAMILY,
import type { ExcalidrawProps } from "../types"; VERTICAL_ALIGN,
import { escapeDoubleQuotes, getFontString, updateActiveTool } from "../utils"; escapeDoubleQuotes,
import { setCursorForShape } from "../cursor"; getFontString,
} from "@excalidraw/common";
import type { ExcalidrawProps } from "@excalidraw/excalidraw/types";
import type { MarkRequired } from "@excalidraw/common/utility-types";
import { newTextElement } from "./newElement"; import { newTextElement } from "./newElement";
import { wrapText } from "./textWrapping"; import { wrapText } from "./textWrapping";
import { isIframeElement } from "./typeChecks"; import { isIframeElement } from "./typeChecks";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawIframeLikeElement, ExcalidrawIframeLikeElement,
IframeData, IframeData,
} from "./types"; } from "./types";
import type { MarkRequired } from "../utility-types";
import { CaptureUpdateAction } from "../store";
type IframeDataWithSandbox = MarkRequired<IframeData, "sandbox">; type IframeDataWithSandbox = MarkRequired<IframeData, "sandbox">;
@ -317,34 +321,6 @@ export const createPlaceholderEmbeddableLabel = (
}); });
}; };
export const actionSetEmbeddableAsActiveTool = register({
name: "setEmbeddableAsActiveTool",
trackEvent: { category: "toolbar" },
target: "Tool",
label: "toolBar.embeddable",
perform: (elements, appState, _, app) => {
const nextActiveTool = updateActiveTool(appState, {
type: "embeddable",
});
setCursorForShape(app.canvas, {
...appState,
activeTool: nextActiveTool,
});
return {
elements,
appState: {
...appState,
activeTool: updateActiveTool(appState, {
type: "embeddable",
}),
},
captureUpdate: CaptureUpdateAction.EVENTUALLY,
};
},
});
const matchHostname = ( const matchHostname = (
url: string, url: string,
/** using a Set assumes it already contains normalized bare domains */ /** using a Set assumes it already contains normalized bare domains */

View file

@ -1,3 +1,14 @@
import { KEYS, invariant, toBrandedType } from "@excalidraw/common";
import { type GlobalPoint, pointFrom, type LocalPoint } from "@excalidraw/math";
import type {
AppState,
PendingExcalidrawElements,
} from "@excalidraw/excalidraw/types";
import { bindLinearElement } from "./binding";
import { updateElbowArrowPoints } from "./elbowArrow";
import { import {
HEADING_DOWN, HEADING_DOWN,
HEADING_LEFT, HEADING_LEFT,
@ -7,9 +18,17 @@ import {
headingForPointFromElement, headingForPointFromElement,
type Heading, type Heading,
} from "./heading"; } from "./heading";
import { bindLinearElement } from "./binding";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement";
import { newArrowElement, newElement } from "./newElement"; import { newArrowElement, newElement } from "./newElement";
import { aabbForElement } from "./shapes";
import { elementsAreInFrameBounds, elementOverlapsWithFrame } from "./frame";
import {
isBindableElement,
isElbowArrow,
isFrameElement,
isFlowchartNodeElement,
} from "./typeChecks";
import { import {
type ElementsMap, type ElementsMap,
type ExcalidrawBindableElement, type ExcalidrawBindableElement,
@ -19,20 +38,6 @@ import {
type Ordered, type Ordered,
type OrderedExcalidrawElement, type OrderedExcalidrawElement,
} from "./types"; } from "./types";
import { KEYS } from "../keys";
import type { AppState, PendingExcalidrawElements } from "../types";
import { mutateElement } from "./mutateElement";
import { elementOverlapsWithFrame, elementsAreInFrameBounds } from "../frame";
import {
isBindableElement,
isElbowArrow,
isFrameElement,
isFlowchartNodeElement,
} from "./typeChecks";
import { invariant, toBrandedType } from "../utils";
import { pointFrom, type LocalPoint } from "@excalidraw/math";
import { aabbForElement } from "../shapes";
import { updateElbowArrowPoints } from "./elbowArrow";
type LinkDirection = "up" | "right" | "down" | "left"; type LinkDirection = "up" | "right" | "down" | "left";
@ -91,7 +96,7 @@ const getNodeRelatives = (
const heading = headingForPointFromElement(node, aabbForElement(node), [ const heading = headingForPointFromElement(node, aabbForElement(node), [
edgePoint[0] + el.x, edgePoint[0] + el.x,
edgePoint[1] + el.y, edgePoint[1] + el.y,
] as Readonly<LocalPoint>); ] as Readonly<GlobalPoint>);
acc.push({ acc.push({
relative, relative,

View file

@ -1,14 +1,20 @@
import { generateNKeysBetween } from "fractional-indexing"; import { generateNKeysBetween } from "fractional-indexing";
import { mutateElement } from "./element/mutateElement";
import { arrayToMap } from "@excalidraw/common";
import { mutateElement } from "./mutateElement";
import { getBoundTextElement } from "./textElement";
import { hasBoundTextElement } from "./typeChecks";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
FractionalIndex, FractionalIndex,
OrderedExcalidrawElement, OrderedExcalidrawElement,
} from "./element/types"; } from "./types";
import { InvalidFractionalIndexError } from "./errors";
import { hasBoundTextElement } from "./element/typeChecks"; export class InvalidFractionalIndexError extends Error {
import { getBoundTextElement } from "./element/textElement"; public code = "ELEMENT_HAS_INVALID_INDEX" as const;
import { arrayToMap } from "./utils"; }
/** /**
* Envisioned relation between array order and fractional indices: * Envisioned relation between array order and fractional indices:

View file

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

View file

@ -1,3 +1,14 @@
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 { import type {
GroupId, GroupId,
ExcalidrawElement, ExcalidrawElement,
@ -5,16 +16,7 @@ import type {
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
ElementsMapOrArray, ElementsMapOrArray,
ElementsMap, ElementsMap,
} from "./element/types";
import type {
AppClassProperties,
AppState,
InteractiveCanvasAppState,
} from "./types"; } from "./types";
import { getSelectedElements } from "./scene";
import { getBoundTextElement } from "./element/textElement";
import { makeNextSelectedElementIds } from "./scene/selection";
import type { Mutable } from "./utility-types";
export const selectGroup = ( export const selectGroup = (
groupId: GroupId, groupId: GroupId,
@ -213,7 +215,10 @@ export const isSelectedViaGroup = (
) => getSelectedGroupForElement(appState, element) != null; ) => getSelectedGroupForElement(appState, element) != null;
export const getSelectedGroupForElement = ( export const getSelectedGroupForElement = (
appState: InteractiveCanvasAppState, appState: Pick<
InteractiveCanvasAppState,
"editingGroupId" | "selectedGroupIds"
>,
element: ExcalidrawElement, element: ExcalidrawElement,
) => ) =>
element.groupIds element.groupIds
@ -293,24 +298,6 @@ export const getSelectedGroupIdForElement = (
selectedGroupIds: { [groupId: string]: boolean }, selectedGroupIds: { [groupId: string]: boolean },
) => element.groupIds.find((groupId) => selectedGroupIds[groupId]); ) => element.groupIds.find((groupId) => selectedGroupIds[groupId]);
export const getNewGroupIdsForDuplication = (
groupIds: ExcalidrawElement["groupIds"],
editingGroupId: AppState["editingGroupId"],
mapper: (groupId: GroupId) => GroupId,
) => {
const copy = [...groupIds];
const positionOfEditingGroupId = editingGroupId
? groupIds.indexOf(editingGroupId)
: -1;
const endIndex =
positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
for (let index = 0; index < endIndex; index++) {
copy[index] = mapper(copy[index]);
}
return copy;
};
export const addToGroup = ( export const addToGroup = (
prevGroupIds: ExcalidrawElement["groupIds"], prevGroupIds: ExcalidrawElement["groupIds"],
newGroupId: GroupId, newGroupId: GroupId,
@ -397,3 +384,21 @@ export const elementsAreInSameGroup = (
export const isInGroup = (element: NonDeletedExcalidrawElement) => { export const isInGroup = (element: NonDeletedExcalidrawElement) => {
return element.groupIds.length > 0; return element.groupIds.length > 0;
}; };
export const getNewGroupIdsForDuplication = (
groupIds: ExcalidrawElement["groupIds"],
editingGroupId: AppState["editingGroupId"],
mapper: (groupId: GroupId) => GroupId,
) => {
const copy = [...groupIds];
const positionOfEditingGroupId = editingGroupId
? groupIds.indexOf(editingGroupId)
: -1;
const endIndex =
positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
for (let index = 0; index < endIndex; index++) {
copy[index] = mapper(copy[index]);
}
return copy;
};

View file

@ -1,11 +1,5 @@
import type {
LocalPoint,
GlobalPoint,
Triangle,
Vector,
Radians,
} from "@excalidraw/math";
import { import {
normalizeRadians,
pointFrom, pointFrom,
pointRotateRads, pointRotateRads,
pointScaleFromOrigin, pointScaleFromOrigin,
@ -13,7 +7,17 @@ import {
triangleIncludesPoint, triangleIncludesPoint,
vectorFromPoint, vectorFromPoint,
} from "@excalidraw/math"; } from "@excalidraw/math";
import type {
LocalPoint,
GlobalPoint,
Triangle,
Vector,
Radians,
} from "@excalidraw/math";
import { getCenterForBounds, type Bounds } from "./bounds"; import { getCenterForBounds, type Bounds } from "./bounds";
import type { ExcalidrawBindableElement } from "./types"; import type { ExcalidrawBindableElement } from "./types";
export const HEADING_RIGHT = [1, 0] as Heading; export const HEADING_RIGHT = [1, 0] as Heading;
@ -27,8 +31,9 @@ export const headingForDiamond = <Point extends GlobalPoint | LocalPoint>(
b: Point, b: Point,
) => { ) => {
const angle = radiansToDegrees( const angle = radiansToDegrees(
Math.atan2(b[1] - a[1], b[0] - a[0]) as Radians, normalizeRadians(Math.atan2(b[1] - a[1], b[0] - a[0]) as Radians),
); );
if (angle >= 315 || angle < 45) { if (angle >= 315 || angle < 45) {
return HEADING_UP; return HEADING_UP;
} else if (angle >= 45 && angle < 135) { } else if (angle >= 45 && angle < 135) {
@ -74,9 +79,7 @@ export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
// Gets the heading for the point by creating a bounding box around the rotated // 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 // close fitting bounding box, then creating 4 search cones around the center of
// the external bbox. // the external bbox.
export const headingForPointFromElement = < export const headingForPointFromElement = <Point extends GlobalPoint>(
Point extends GlobalPoint | LocalPoint,
>(
element: Readonly<ExcalidrawBindableElement>, element: Readonly<ExcalidrawBindableElement>,
aabb: Readonly<Bounds>, aabb: Readonly<Bounds>,
p: Readonly<Point>, p: Readonly<Point>,

View file

@ -2,9 +2,16 @@
// ExcalidrawImageElement & related helpers // ExcalidrawImageElement & related helpers
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
import { MIME_TYPES, SVG_NS } from "../constants"; import { MIME_TYPES, SVG_NS } from "@excalidraw/common";
import type { AppClassProperties, DataURL, BinaryFiles } from "../types";
import type {
AppClassProperties,
DataURL,
BinaryFiles,
} from "@excalidraw/excalidraw/types";
import { isInitializedImageElement } from "./typeChecks"; import { isInitializedImageElement } from "./typeChecks";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
FileId, FileId,

View file

@ -1,60 +1,11 @@
import { isInvisiblySmallElement } from "./sizeHelpers";
import { isLinearElementType } from "./typeChecks";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
NonDeleted, NonDeleted,
} from "./types"; } from "./types";
import { isInvisiblySmallElement } from "./sizeHelpers";
import { isLinearElementType } from "./typeChecks";
export {
newElement,
newTextElement,
refreshTextDimensions,
newLinearElement,
newArrowElement,
newImageElement,
duplicateElement,
} from "./newElement";
export {
getElementAbsoluteCoords,
getElementBounds,
getCommonBounds,
getDiamondPoints,
getArrowheadPoints,
getClosestElementBounds,
} from "./bounds";
export {
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
getTransformHandlesFromCoords,
getTransformHandles,
} from "./transformHandles";
export {
resizeTest,
getCursorForResizingElement,
getElementWithTransformHandleType,
getTransformHandleTypeFromCoords,
} from "./resizeTest";
export {
transformElements,
getResizeOffsetXY,
getResizeArrowDirection,
} from "./resizeElements";
export {
dragSelectedElements,
getDragOffsetXY,
dragNewElement,
} from "./dragElements";
export { isTextElement, isExcalidrawElement } from "./typeChecks";
export { redrawTextBoundingBox, getTextFromElements } from "./textElement";
export {
getPerfectElementSize,
getLockedLinearCursorAlignSize,
isInvisiblySmallElement,
resizePerfectLineForNWHandler,
getNormalizedDimensions,
} from "./sizeHelpers";
export { showSelectedShapeActions } from "./showSelectedShapeActions";
/** /**
* @deprecated unsafe, use hashElementsVersion instead * @deprecated unsafe, use hashElementsVersion instead

View file

@ -1,3 +1,79 @@
import {
pointCenter,
pointFrom,
pointRotateRads,
pointsEqual,
type GlobalPoint,
type LocalPoint,
pointDistance,
vectorFromPoint,
} from "@excalidraw/math";
import { getCurvePathOps } from "@excalidraw/utils/shape";
import {
DRAGGING_THRESHOLD,
KEYS,
shouldRotateWithDiscreteAngle,
getGridPoint,
invariant,
tupleToCoors,
} from "@excalidraw/common";
// TODO: remove direct dependency on the scene, should be passed in or injected instead
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import Scene from "@excalidraw/excalidraw/scene/Scene";
import type { Store } from "@excalidraw/excalidraw/store";
import type { Radians } from "@excalidraw/math";
import type {
AppState,
PointerCoords,
InteractiveCanvasAppState,
AppClassProperties,
NullableGridSize,
Zoom,
} from "@excalidraw/excalidraw/types";
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 { headingIsHorizontal, vectorToHeading } from "./heading";
import { bumpVersion, mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import {
isBindingElement,
isElbowArrow,
isFixedPointBinding,
} from "./typeChecks";
import { ShapeCache } from "./ShapeCache";
import {
isPathALoop,
getBezierCurveLength,
getControlPointsForBezierCurve,
mapIntervalToBezierT,
getBezierXY,
} from "./shapes";
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
import type { Bounds } from "./bounds";
import type { import type {
NonDeleted, NonDeleted,
ExcalidrawLinearElement, ExcalidrawLinearElement,
@ -12,58 +88,6 @@ import type {
FixedSegment, FixedSegment,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
} from "./types"; } from "./types";
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
import type { Bounds } from "./bounds";
import { getElementPointsCoords, getMinMaxXYFromCurvePathOps } from "./bounds";
import type {
AppState,
PointerCoords,
InteractiveCanvasAppState,
AppClassProperties,
NullableGridSize,
Zoom,
} from "../types";
import { mutateElement } from "./mutateElement";
import {
bindOrUnbindLinearElement,
getHoveredElementForBinding,
isBindingEnabled,
} from "./binding";
import { invariant, tupleToCoors } from "../utils";
import {
isBindingElement,
isElbowArrow,
isFixedPointBinding,
} from "./typeChecks";
import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { DRAGGING_THRESHOLD } from "../constants";
import type { Mutable } from "../utility-types";
import { ShapeCache } from "../scene/ShapeCache";
import type { Store } from "../store";
import type Scene from "../scene/Scene";
import type { Radians } from "@excalidraw/math";
import {
pointCenter,
pointFrom,
pointRotateRads,
pointsEqual,
type GlobalPoint,
type LocalPoint,
pointDistance,
vectorFromPoint,
} from "@excalidraw/math";
import {
getBezierCurveLength,
getBezierXY,
getControlPointsForBezierCurve,
isPathALoop,
mapIntervalToBezierT,
} from "../shapes";
import { getGridPoint } from "../snapping";
import { headingIsHorizontal, vectorToHeading } from "./heading";
import { getCurvePathOps } from "@excalidraw/utils/geometry/shape";
const editorMidPointsCache: { const editorMidPointsCache: {
version: number | null; version: number | null;
@ -228,15 +252,15 @@ export class LinearElementEditor {
) => void, ) => void,
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
scene: Scene, scene: Scene,
): boolean { ): LinearElementEditor | null {
if (!linearElementEditor) { if (!linearElementEditor) {
return false; return null;
} }
const { elementId } = linearElementEditor; const { elementId } = linearElementEditor;
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return false; return null;
} }
if ( if (
@ -244,24 +268,18 @@ export class LinearElementEditor {
!linearElementEditor.pointerDownState.lastClickedIsEndPoint && !linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
linearElementEditor.pointerDownState.lastClickedPoint !== 0 linearElementEditor.pointerDownState.lastClickedPoint !== 0
) { ) {
return false; return null;
} }
const selectedPointsIndices = isElbowArrow(element) const selectedPointsIndices = isElbowArrow(element)
? linearElementEditor.selectedPointsIndices ? [
?.reduce( !!linearElementEditor.selectedPointsIndices?.includes(0)
(startEnd, index) => ? 0
(index === 0 : undefined,
? [0, startEnd[1]] !!linearElementEditor.selectedPointsIndices?.find((idx) => idx > 0)
: [startEnd[0], element.points.length - 1]) as [ ? element.points.length - 1
boolean | number, : undefined,
boolean | number, ].filter((idx): idx is number => idx !== undefined)
],
[false, false] as [number | boolean, number | boolean],
)
.filter(
(idx: number | boolean): idx is number => typeof idx === "number",
)
: linearElementEditor.selectedPointsIndices; : linearElementEditor.selectedPointsIndices;
const lastClickedPoint = isElbowArrow(element) const lastClickedPoint = isElbowArrow(element)
? linearElementEditor.pointerDownState.lastClickedPoint > 0 ? linearElementEditor.pointerDownState.lastClickedPoint > 0
@ -270,9 +288,7 @@ export class LinearElementEditor {
: linearElementEditor.pointerDownState.lastClickedPoint; : linearElementEditor.pointerDownState.lastClickedPoint;
// point that's being dragged (out of all selected points) // point that's being dragged (out of all selected points)
const draggingPoint = element.points[lastClickedPoint] as const draggingPoint = element.points[lastClickedPoint];
| [number, number]
| undefined;
if (selectedPointsIndices && draggingPoint) { if (selectedPointsIndices && draggingPoint) {
if ( if (
@ -380,10 +396,28 @@ export class LinearElementEditor {
} }
} }
return true; return {
...linearElementEditor,
selectedPointsIndices,
segmentMidPointHoveredCoords:
lastClickedPoint !== 0 &&
lastClickedPoint !== element.points.length - 1
? this.getPointGlobalCoordinates(
element,
draggingPoint,
elementsMap,
)
: null,
hoverPointIndex:
lastClickedPoint === 0 ||
lastClickedPoint === element.points.length - 1
? lastClickedPoint
: -1,
isDragging: true,
};
} }
return false; return null;
} }
static handlePointerUp( static handlePointerUp(
@ -1260,6 +1294,7 @@ export class LinearElementEditor {
startBinding?: PointBinding | null; startBinding?: PointBinding | null;
endBinding?: PointBinding | null; endBinding?: PointBinding | null;
}, },
sceneElementsMap?: NonDeletedSceneElementsMap,
) { ) {
const { points } = element; const { points } = element;
@ -1303,6 +1338,7 @@ export class LinearElementEditor {
dragging || targetPoint.isDragging === true, dragging || targetPoint.isDragging === true,
false, false,
), ),
sceneElementsMap,
}, },
); );
} }
@ -1416,6 +1452,7 @@ export class LinearElementEditor {
options?: { options?: {
isDragging?: boolean; isDragging?: boolean;
zoom?: AppState["zoom"]; zoom?: AppState["zoom"];
sceneElementsMap?: NonDeletedSceneElementsMap;
}, },
) { ) {
if (isElbowArrow(element)) { if (isElbowArrow(element)) {
@ -1441,9 +1478,28 @@ export class LinearElementEditor {
updates.points = Array.from(nextPoints); updates.points = Array.from(nextPoints);
if (!options?.sceneElementsMap || Scene.getScene(element)) {
mutateElement(element, updates, true, { mutateElement(element, updates, true, {
isDragging: options?.isDragging, isDragging: options?.isDragging,
}); });
} else {
// The element is not in the scene, so we need to use the provided
// scene map.
Object.assign(element, {
...updates,
angle: 0 as Radians,
...updateElbowArrowPoints(
element,
options.sceneElementsMap,
updates,
{
isDragging: options?.isDragging,
},
),
});
}
bumpVersion(element);
} else { } else {
const nextCoords = getElementPointsCoords(element, nextPoints); const nextCoords = getElementPointsCoords(element, nextPoints);
const prevCoords = getElementPointsCoords(element, element.points); const prevCoords = getElementPointsCoords(element, element.points);

View file

@ -1,14 +1,25 @@
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types"; import {
import Scene from "../scene/Scene"; getSizeFromPoints,
import { getSizeFromPoints } from "../points"; randomInteger,
import { randomInteger } from "../random"; getUpdatedTimestamp,
import { getUpdatedTimestamp, toBrandedType } from "../utils"; toBrandedType,
import type { Mutable } from "../utility-types"; } from "@excalidraw/common";
import { ShapeCache } from "../scene/ShapeCache";
import { isElbowArrow } from "./typeChecks"; // TODO: remove direct dependency on the scene, should be passed in or injected instead
import { updateElbowArrowPoints } from "./elbowArrow"; // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import Scene from "@excalidraw/excalidraw/scene/Scene";
import type { Radians } from "@excalidraw/math"; import type { Radians } from "@excalidraw/math";
import 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";
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit< export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>, Partial<TElement>,
"id" | "version" | "versionNonce" | "updated" "id" | "version" | "versionNonce" | "updated"

View file

@ -1,3 +1,30 @@
import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
VERTICAL_ALIGN,
randomInteger,
randomId,
getFontString,
getUpdatedTimestamp,
getLineHeight,
} from "@excalidraw/common";
import type { Radians } from "@excalidraw/math";
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 type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawImageElement, ExcalidrawImageElement,
@ -6,7 +33,6 @@ import type {
ExcalidrawGenericElement, ExcalidrawGenericElement,
NonDeleted, NonDeleted,
TextAlign, TextAlign,
GroupId,
VerticalAlign, VerticalAlign,
Arrowhead, Arrowhead,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
@ -21,33 +47,6 @@ import type {
FixedSegment, FixedSegment,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
} from "./types"; } from "./types";
import {
arrayToMap,
getFontString,
getUpdatedTimestamp,
isTestEnv,
} from "../utils";
import { randomInteger, randomId } from "../random";
import { bumpVersion, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
import type { AppState } from "../types";
import { getElementAbsoluteCoords } from ".";
import { getResizedElementAbsoluteCoords } from "./bounds";
import { getBoundTextMaxWidth } from "./textElement";
import { wrapText } from "./textWrapping";
import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
ORIG_ID,
VERTICAL_ALIGN,
} from "../constants";
import type { MarkOptional, Merge, Mutable } from "../utility-types";
import { getLineHeight } from "../fonts";
import type { Radians } from "@excalidraw/math";
import { normalizeText, measureText } from "./textMeasurements";
export type ElementConstructorOpts = MarkOptional< export type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">, Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@ -534,260 +533,3 @@ export const newImageElement = (
crop: opts.crop ?? null, crop: opts.crop ?? null,
}; };
}; };
// Simplified deep clone for the purpose of cloning ExcalidrawElement.
//
// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
// Typed arrays and other non-null objects.
//
// Adapted from https://github.com/lukeed/klona
//
// The reason for `deepCopyElement()` wrapper is type safety (only allow
// passing ExcalidrawElement as the top-level argument).
const _deepCopyElement = (val: any, depth: number = 0) => {
// only clone non-primitives
if (val == null || typeof val !== "object") {
return val;
}
const objectType = Object.prototype.toString.call(val);
if (objectType === "[object Object]") {
const tmp =
typeof val.constructor === "function"
? Object.create(Object.getPrototypeOf(val))
: {};
for (const key in val) {
if (val.hasOwnProperty(key)) {
// don't copy non-serializable objects like these caches. They'll be
// populated when the element is rendered.
if (depth === 0 && (key === "shape" || key === "canvas")) {
continue;
}
tmp[key] = _deepCopyElement(val[key], depth + 1);
}
}
return tmp;
}
if (Array.isArray(val)) {
let k = val.length;
const arr = new Array(k);
while (k--) {
arr[k] = _deepCopyElement(val[k], depth + 1);
}
return arr;
}
// we're not cloning non-array & non-plain-object objects because we
// don't support them on excalidraw elements yet. If we do, we need to make
// sure we start cloning them, so let's warn about it.
if (import.meta.env.DEV) {
if (
objectType !== "[object Object]" &&
objectType !== "[object Array]" &&
objectType.startsWith("[object ")
) {
console.warn(
`_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`,
);
}
}
return val;
};
/**
* Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or
* any value. The purpose is to to break object references for immutability
* reasons, whenever we want to keep the original element, but ensure it's not
* mutated.
*
* Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
* Typed arrays and other non-null objects.
*/
export const deepCopyElement = <T extends ExcalidrawElement>(
val: T,
): Mutable<T> => {
return _deepCopyElement(val);
};
const __test__defineOrigId = (clonedObj: object, origId: string) => {
Object.defineProperty(clonedObj, ORIG_ID, {
value: origId,
writable: false,
enumerable: false,
});
};
/**
* utility wrapper to generate new id.
*/
const regenerateId = () => {
return randomId();
};
/**
* Duplicate an element, often used in the alt-drag operation.
* Note that this method has gotten a bit complicated since the
* introduction of gruoping/ungrouping elements.
* @param editingGroupId The current group being edited. The new
* element will inherit this group and its
* parents.
* @param groupIdMapForOperation A Map that maps old group IDs to
* duplicated ones. If you are duplicating
* multiple elements at once, share this map
* amongst all of them
* @param element Element to duplicate
* @param overrides Any element properties to override
*/
export const duplicateElement = <TElement extends ExcalidrawElement>(
editingGroupId: AppState["editingGroupId"],
groupIdMapForOperation: Map<GroupId, GroupId>,
element: TElement,
overrides?: Partial<TElement>,
): Readonly<TElement> => {
let copy = deepCopyElement(element);
if (isTestEnv()) {
__test__defineOrigId(copy, element.id);
}
copy.id = regenerateId();
copy.boundElements = null;
copy.updated = getUpdatedTimestamp();
copy.seed = randomInteger();
copy.groupIds = getNewGroupIdsForDuplication(
copy.groupIds,
editingGroupId,
(groupId) => {
if (!groupIdMapForOperation.has(groupId)) {
groupIdMapForOperation.set(groupId, regenerateId());
}
return groupIdMapForOperation.get(groupId)!;
},
);
if (overrides) {
copy = Object.assign(copy, overrides);
}
return copy;
};
/**
* Clones elements, regenerating their ids (including bindings) and group ids.
*
* If bindings don't exist in the elements array, they are removed. Therefore,
* it's advised to supply the whole elements array, or sets of elements that
* are encapsulated (such as library items), if the purpose is to retain
* bindings to the cloned elements intact.
*
* NOTE by default does not randomize or regenerate anything except the id.
*/
export const duplicateElements = (
elements: readonly ExcalidrawElement[],
opts?: {
/** NOTE also updates version flags and `updated` */
randomizeSeed: boolean;
},
) => {
const clonedElements: ExcalidrawElement[] = [];
const origElementsMap = arrayToMap(elements);
// used for for migrating old ids to new ids
const elementNewIdsMap = new Map<
/* orig */ ExcalidrawElement["id"],
/* new */ ExcalidrawElement["id"]
>();
const maybeGetNewId = (id: ExcalidrawElement["id"]) => {
// if we've already migrated the element id, return the new one directly
if (elementNewIdsMap.has(id)) {
return elementNewIdsMap.get(id)!;
}
// if we haven't migrated the element id, but an old element with the same
// id exists, generate a new id for it and return it
if (origElementsMap.has(id)) {
const newId = regenerateId();
elementNewIdsMap.set(id, newId);
return newId;
}
// if old element doesn't exist, return null to mark it for removal
return null;
};
const groupNewIdsMap = new Map</* orig */ GroupId, /* new */ GroupId>();
for (const element of elements) {
const clonedElement: Mutable<ExcalidrawElement> = _deepCopyElement(element);
clonedElement.id = maybeGetNewId(element.id)!;
if (isTestEnv()) {
__test__defineOrigId(clonedElement, element.id);
}
if (opts?.randomizeSeed) {
clonedElement.seed = randomInteger();
bumpVersion(clonedElement);
}
if (clonedElement.groupIds) {
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
if (!groupNewIdsMap.has(groupId)) {
groupNewIdsMap.set(groupId, regenerateId());
}
return groupNewIdsMap.get(groupId)!;
});
}
if ("containerId" in clonedElement && clonedElement.containerId) {
const newContainerId = maybeGetNewId(clonedElement.containerId);
clonedElement.containerId = newContainerId;
}
if ("boundElements" in clonedElement && clonedElement.boundElements) {
clonedElement.boundElements = clonedElement.boundElements.reduce(
(
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
binding,
) => {
const newBindingId = maybeGetNewId(binding.id);
if (newBindingId) {
acc.push({ ...binding, id: newBindingId });
}
return acc;
},
[],
);
}
if ("endBinding" in clonedElement && clonedElement.endBinding) {
const newEndBindingId = maybeGetNewId(clonedElement.endBinding.elementId);
clonedElement.endBinding = newEndBindingId
? {
...clonedElement.endBinding,
elementId: newEndBindingId,
}
: null;
}
if ("startBinding" in clonedElement && clonedElement.startBinding) {
const newEndBindingId = maybeGetNewId(
clonedElement.startBinding.elementId,
);
clonedElement.startBinding = newEndBindingId
? {
...clonedElement.startBinding,
elementId: newEndBindingId,
}
: null;
}
if (clonedElement.frameId) {
clonedElement.frameId = maybeGetNewId(clonedElement.frameId);
}
clonedElements.push(clonedElement);
}
return clonedElements;
};

View file

@ -1,3 +1,63 @@
import rough from "roughjs/bin/rough";
import { getStroke } from "perfect-freehand";
import { isRightAngleRads } from "@excalidraw/math";
import {
BOUND_TEXT_PADDING,
DEFAULT_REDUCED_GLOBAL_ALPHA,
ELEMENT_READY_TO_ERASE_OPACITY,
FRAME_STYLE,
MIME_TYPES,
THEME,
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 "./textElement";
import { getLineHeightInPx } from "./textMeasurements";
import {
isTextElement,
isLinearElement,
isFreeDrawElement,
isInitializedImageElement,
isArrowElement,
hasBoundTextElement,
isMagicFrameElement,
isImageElement,
} from "./typeChecks";
import { getContainingFrame } from "./frame";
import { getCornerRadius } from "./shapes";
import { ShapeCache } from "./ShapeCache";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawTextElement, ExcalidrawTextElement,
@ -8,62 +68,10 @@ import type {
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
ElementsMap, ElementsMap,
} from "../element/types"; } from "./types";
import {
isTextElement,
isLinearElement,
isFreeDrawElement,
isInitializedImageElement,
isArrowElement,
hasBoundTextElement,
isMagicFrameElement,
isImageElement,
} from "../element/typeChecks";
import { getElementAbsoluteCoords } from "../element/bounds";
import type { RoughCanvas } from "roughjs/bin/canvas";
import type {
StaticCanvasRenderConfig,
RenderableElementsMap,
InteractiveCanvasRenderConfig,
} from "../scene/types";
import { distance, getFontString, isRTL } from "../utils";
import rough from "roughjs/bin/rough";
import type {
AppState,
StaticCanvasAppState,
Zoom,
InteractiveCanvasAppState,
ElementsPendingErasure,
PendingExcalidrawElements,
} from "../types";
import { getDefaultAppState } from "../appState";
import {
BOUND_TEXT_PADDING,
DEFAULT_REDUCED_GLOBAL_ALPHA,
ELEMENT_READY_TO_ERASE_OPACITY,
FRAME_STYLE,
MIME_TYPES,
THEME,
} from "../constants";
import type { StrokeOptions } from "perfect-freehand"; import type { StrokeOptions } from "perfect-freehand";
import { getStroke } from "perfect-freehand"; import type { RoughCanvas } from "roughjs/bin/canvas";
import {
getBoundTextElement,
getContainerCoords,
getContainerElement,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
} from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor";
import { getContainingFrame } from "../frame";
import { ShapeCache } from "../scene/ShapeCache";
import { getVerticalOffset } from "../fonts";
import { isRightAngleRads } from "@excalidraw/math";
import { getCornerRadius } from "../shapes";
import { getUncroppedImageElement } from "../element/cropElement";
import { getLineHeightInPx } from "../element/textMeasurements";
// using a stronger invert (100% vs our regular 93%) and saturate // using a stronger invert (100% vs our regular 93%) and saturate
// as a temp hack to make images in dark theme look closer to original // as a temp hack to make images in dark theme look closer to original
@ -72,8 +80,6 @@ import { getLineHeightInPx } from "../element/textMeasurements";
export const IMAGE_INVERT_FILTER = export const IMAGE_INVERT_FILTER =
"invert(100%) hue-rotate(180deg) saturate(1.25)"; "invert(100%) hue-rotate(180deg) saturate(1.25)";
const defaultAppState = getDefaultAppState();
const isPendingImageElement = ( const isPendingImageElement = (
element: ExcalidrawElement, element: ExcalidrawElement,
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,
@ -533,7 +539,11 @@ const generateElementWithCanvas = (
renderConfig: StaticCanvasRenderConfig, renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
) => { ) => {
const zoom: Zoom = renderConfig ? appState.zoom : defaultAppState.zoom; const zoom: Zoom = renderConfig
? appState.zoom
: {
value: 1 as NormalizedZoomValue,
};
const prevElementWithCanvas = elementWithCanvasCache.get(element); const prevElementWithCanvas = elementWithCanvasCache.get(element);
const shouldRegenerateBecauseZoom = const shouldRegenerateBecauseZoom =
prevElementWithCanvas && prevElementWithCanvas &&

View file

@ -1,5 +1,70 @@
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants"; import {
import { rescalePoints } from "../points"; pointCenter,
normalizeRadians,
pointFrom,
pointFromPair,
pointRotateRads,
type Radians,
type LocalPoint,
} from "@excalidraw/math";
import {
MIN_FONT_SIZE,
SHIFT_LOCKING_ANGLE,
rescalePoints,
getFontString,
} from "@excalidraw/common";
import type { GlobalPoint } from "@excalidraw/math";
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 {
getElementAbsoluteCoords,
getCommonBounds,
getResizedElementAbsoluteCoords,
getCommonBoundingBox,
getElementBounds,
} from "./bounds";
import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement";
import {
getBoundTextElement,
getBoundTextElementId,
getContainerElement,
handleBindTextResize,
getBoundTextMaxWidth,
} from "./textElement";
import {
getMinTextElementWidth,
measureText,
getApproxMinLineWidth,
getApproxMinLineHeight,
} from "./textMeasurements";
import { wrapText } from "./textWrapping";
import {
isArrowElement,
isBoundToContainer,
isElbowArrow,
isFrameLikeElement,
isFreeDrawElement,
isImageElement,
isLinearElement,
isTextElement,
} from "./typeChecks";
import { isInGroup } from "./groups";
import type { BoundingBox } from "./bounds";
import type {
MaybeTransformHandleType,
TransformHandleDirection,
} from "./transformHandles";
import type { import type {
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElement, ExcalidrawTextElement,
@ -12,60 +77,6 @@ import type {
SceneElementsMap, SceneElementsMap,
ExcalidrawElbowArrowElement, ExcalidrawElbowArrowElement,
} from "./types"; } from "./types";
import type { Mutable } from "../utility-types";
import {
getElementAbsoluteCoords,
getCommonBounds,
getResizedElementAbsoluteCoords,
getCommonBoundingBox,
getElementBounds,
} from "./bounds";
import type { BoundingBox } from "./bounds";
import {
isArrowElement,
isBoundToContainer,
isElbowArrow,
isFrameLikeElement,
isFreeDrawElement,
isImageElement,
isLinearElement,
isTextElement,
} from "./typeChecks";
import { mutateElement } from "./mutateElement";
import { getFontString } from "../utils";
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
import type {
MaybeTransformHandleType,
TransformHandleDirection,
} from "./transformHandles";
import type { PointerDownState } from "../types";
import type Scene from "../scene/Scene";
import {
getBoundTextElement,
getBoundTextElementId,
getContainerElement,
handleBindTextResize,
getBoundTextMaxWidth,
} from "./textElement";
import { wrapText } from "./textWrapping";
import { LinearElementEditor } from "./linearElementEditor";
import { isInGroup } from "../groups";
import type { GlobalPoint } from "@excalidraw/math";
import {
pointCenter,
normalizeRadians,
pointFrom,
pointFromPair,
pointRotateRads,
type Radians,
type LocalPoint,
} from "@excalidraw/math";
import {
getMinTextElementWidth,
measureText,
getApproxMinLineWidth,
getApproxMinLineHeight,
} from "./textMeasurements";
// Returns true when transform (resizing/rotation) happened // Returns true when transform (resizing/rotation) happened
export const transformElements = ( export const transformElements = (

View file

@ -1,27 +1,3 @@
import type {
ExcalidrawElement,
PointerType,
NonDeletedExcalidrawElement,
ElementsMap,
} from "./types";
import type {
TransformHandleType,
TransformHandle,
MaybeTransformHandleType,
} from "./transformHandles";
import {
getTransformHandlesFromCoords,
getTransformHandles,
getOmitSidesForDevice,
canResizeFromSides,
} from "./transformHandles";
import type { AppState, Device, Zoom } from "../types";
import type { Bounds } from "./bounds";
import { getElementAbsoluteCoords } from "./bounds";
import { SIDE_RESIZING_THRESHOLD } from "../constants";
import { isImageElement, isLinearElement } from "./typeChecks";
import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
import { import {
pointFrom, pointFrom,
pointOnLineSegment, pointOnLineSegment,
@ -29,6 +5,34 @@ import {
type Radians, type Radians,
} from "@excalidraw/math"; } from "@excalidraw/math";
import { SIDE_RESIZING_THRESHOLD } from "@excalidraw/common";
import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
import type { AppState, Device, Zoom } from "@excalidraw/excalidraw/types";
import { getElementAbsoluteCoords } from "./bounds";
import {
getTransformHandlesFromCoords,
getTransformHandles,
getOmitSidesForDevice,
canResizeFromSides,
} from "./transformHandles";
import { isImageElement, isLinearElement } from "./typeChecks";
import type { Bounds } from "./bounds";
import type {
TransformHandleType,
TransformHandle,
MaybeTransformHandleType,
} from "./transformHandles";
import type {
ExcalidrawElement,
PointerType,
NonDeletedExcalidrawElement,
ElementsMap,
} from "./types";
const isInsideTransformHandle = ( const isInsideTransformHandle = (
transformHandle: TransformHandle, transformHandle: TransformHandle,
x: number, x: number,

View file

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

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