Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77edb39a7e | ||
|
|
56c9ea9259 | ||
|
|
0d34177761 | ||
|
|
a7b64f02b3 | ||
|
|
0d4abd1ddc | ||
|
|
9e77373c81 | ||
|
|
d108053351 | ||
|
|
d4e85a9480 | ||
|
|
08cd4c4f9a | ||
|
|
469caadb87 | ||
|
|
ca1a4f25e7 | ||
|
|
56c05b3099 | ||
|
|
6c0ff7fc5c | ||
|
|
7cad3645a0 | ||
|
|
5921ebc416 | ||
|
|
864353be5f | ||
|
|
db2911c6c4 | ||
|
|
fc3e062074 | ||
|
|
87c87a9fb1 | ||
|
|
4dc205537c | ||
|
|
cc571c4681 | ||
|
|
14d512f321 | ||
|
|
41c036e1a5 | ||
|
|
91d36e9b81 | ||
|
|
27522110df | ||
|
|
712f267519 | ||
|
|
41a7613dff | ||
|
|
95d89a751a | ||
|
|
6b5fb30d69 | ||
|
|
d92a849038 | ||
|
|
0a534f1bc6 | ||
|
|
4ca5f53b1f | ||
|
|
f7dcc893ea | ||
|
|
4dfb8a3f8e | ||
|
|
298812e1d0 | ||
|
|
35bb449a4b | ||
|
|
c4c064982f | ||
|
|
51dbd4831b | ||
|
|
7e41026812 | ||
|
|
a8ebe514da | ||
|
|
a30e1b25c6 | ||
|
|
ff2ed5d26a | ||
|
|
e058a08b33 | ||
|
|
a306a909a0 | ||
|
|
3dc54a724a | ||
|
|
a7c61319dd | ||
|
|
cec5232a7a | ||
|
|
d4f70e9f31 | ||
|
|
e19fd1332a | ||
|
|
6e655cdb24 | ||
|
|
192c4e7658 | ||
|
|
195a743874 | ||
|
|
4a60fe3d22 | ||
|
|
2a0d15799c | ||
|
|
a18b139a60 | ||
|
|
1913599594 |
@@ -1,3 +1,5 @@
|
|||||||
|
MODE="development"
|
||||||
|
|
||||||
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
||||||
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
MODE="production"
|
||||||
|
|
||||||
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
||||||
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,12 @@
|
|||||||
"name": "jotai",
|
"name": "jotai",
|
||||||
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-no-target-blank": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowReferrer": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Project coding standards
|
||||||
|
|
||||||
|
## Generic Communication Guidelines
|
||||||
|
|
||||||
|
- Be succint and be aware that expansive generative AI answers are costly and slow
|
||||||
|
- Avoid providing explanations, trying to teach unless asked for, your chat partner is an expert
|
||||||
|
- Stop apologising if corrected, just provide the correct information or code
|
||||||
|
- Prefer code unless asked for explanation
|
||||||
|
- Stop summarizing what you've changed after modifications unless asked for
|
||||||
|
|
||||||
|
## TypeScript Guidelines
|
||||||
|
|
||||||
|
- Use TypeScript for all new code
|
||||||
|
- Where possible, prefer implementations without allocation
|
||||||
|
- When there is an option, opt for more performant solutions and trade RAM usage for less CPU cycles
|
||||||
|
- Prefer immutable data (const, readonly)
|
||||||
|
- Use optional chaining (?.) and nullish coalescing (??) operators
|
||||||
|
|
||||||
|
## React Guidelines
|
||||||
|
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Follow the React hooks rules (no conditional hooks)
|
||||||
|
- Keep components small and focused
|
||||||
|
- Use CSS modules for component styling
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
- Use PascalCase for component names, interfaces, and type aliases
|
||||||
|
- Use camelCase for variables, functions, and methods
|
||||||
|
- Use ALL_CAPS for constants
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Use try/catch blocks for async operations
|
||||||
|
- Implement proper error boundaries in React components
|
||||||
|
- Always log errors with contextual information
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Always attempt to fix #problems
|
||||||
|
- Always offer to run `yarn test:app` in the project root after modifications are complete and attempt fixing the issues reported
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
- Always include `packages/math/src/types.ts` in the context when your write math related code and always use the Point type instead of { x, y}
|
||||||
+1
-6
@@ -26,9 +26,4 @@ coverage
|
|||||||
dev-dist
|
dev-dist
|
||||||
html
|
html
|
||||||
meta*.json
|
meta*.json
|
||||||
|
.claude
|
||||||
# Playwright
|
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/blob-report/
|
|
||||||
/playwright/.cache/
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
Excalidraw is a **monorepo** with a clear separation between the core library and the application:
|
||||||
|
|
||||||
|
- **`packages/excalidraw/`** - Main React component library published to npm as `@excalidraw/excalidraw`
|
||||||
|
- **`excalidraw-app/`** - Full-featured web application (excalidraw.com) that uses the library
|
||||||
|
- **`packages/`** - Core packages: `@excalidraw/common`, `@excalidraw/element`, `@excalidraw/math`, `@excalidraw/utils`
|
||||||
|
- **`examples/`** - Integration examples (NextJS, browser script)
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Package Development**: Work in `packages/*` for editor features
|
||||||
|
2. **App Development**: Work in `excalidraw-app/` for app-specific features
|
||||||
|
3. **Testing**: Always run `yarn test:update` before committing
|
||||||
|
4. **Type Safety**: Use `yarn test:typecheck` to verify TypeScript
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn test:typecheck # TypeScript type checking
|
||||||
|
yarn test:update # Run all tests (with snapshot updates)
|
||||||
|
yarn fix # Auto-fix formatting and linting issues
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
### Package System
|
||||||
|
|
||||||
|
- Uses Yarn workspaces for monorepo management
|
||||||
|
- Internal packages use path aliases (see `vitest.config.mts`)
|
||||||
|
- Build system uses esbuild for packages, Vite for the app
|
||||||
|
- TypeScript throughout with strict configuration
|
||||||
@@ -34,6 +34,9 @@
|
|||||||
<a href="https://discord.gg/UexuTaE">
|
<a href="https://discord.gg/UexuTaE">
|
||||||
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://deepwiki.com/excalidraw/excalidraw">
|
||||||
|
<img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" />
|
||||||
|
</a>
|
||||||
<a href="https://twitter.com/excalidraw">
|
<a href="https://twitter.com/excalidraw">
|
||||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
||||||
</a>
|
</a>
|
||||||
@@ -63,7 +66,7 @@ The Excalidraw editor (npm package) supports:
|
|||||||
- 🏗️ Customizable.
|
- 🏗️ Customizable.
|
||||||
- 📷 Image support.
|
- 📷 Image support.
|
||||||
- 😀 Shape libraries support.
|
- 😀 Shape libraries support.
|
||||||
- 👅 Localization (i18n) support.
|
- 🌐 Localization (i18n) support.
|
||||||
- 🖼️ Export to PNG, SVG & clipboard.
|
- 🖼️ Export to PNG, SVG & clipboard.
|
||||||
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
||||||
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 59 KiB |
@@ -363,13 +363,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti
|
|||||||
```ts
|
```ts
|
||||||
(
|
(
|
||||||
tool: (
|
tool: (
|
||||||
| (
|
| { type: ToolType }
|
||||||
| { type: Exclude<ToolType, "image"> }
|
|
||||||
| {
|
|
||||||
type: Extract<ToolType, "image">;
|
|
||||||
insertOnCanvasDirectly?: boolean;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
| { type: "custom"; customType: string }
|
| { type: "custom"; customType: string }
|
||||||
) & { locked?: boolean },
|
) & { locked?: boolean },
|
||||||
) => {};
|
) => {};
|
||||||
@@ -377,7 +371,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti
|
|||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` |
|
| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool |
|
||||||
| `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
|
| `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface |
|
||||||
|
|
||||||
## setCursor
|
## setCursor
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ If you want to only import `Excalidraw` component you can do :point_down:
|
|||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
const Excalidraw = dynamic(
|
const Excalidraw = dynamic(
|
||||||
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
|
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw .panelColumn {
|
.excalidraw .selected-shape-actions {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+274
-3
@@ -4,6 +4,7 @@ import {
|
|||||||
TTDDialogTrigger,
|
TTDDialogTrigger,
|
||||||
CaptureUpdateAction,
|
CaptureUpdateAction,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
|
convertToExcalidrawElements,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
import { getDefaultAppState } from "@excalidraw/excalidraw/appState";
|
||||||
@@ -47,10 +48,10 @@ import {
|
|||||||
share,
|
share,
|
||||||
youtubeIcon,
|
youtubeIcon,
|
||||||
} from "@excalidraw/excalidraw/components/icons";
|
} from "@excalidraw/excalidraw/components/icons";
|
||||||
import { isElementLink } from "@excalidraw/element/elementLink";
|
import { isElementLink } from "@excalidraw/element";
|
||||||
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
import { restore, restoreAppState } from "@excalidraw/excalidraw/data/restore";
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
parseLibraryTokensFromUrl,
|
parseLibraryTokensFromUrl,
|
||||||
@@ -471,6 +472,276 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
|
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
|
||||||
loadImages(data, /* isInitialLoad */ true);
|
loadImages(data, /* isInitialLoad */ true);
|
||||||
|
if (data.scene) {
|
||||||
|
data.scene.elements = data.scene.elements?.length
|
||||||
|
? data.scene.elements
|
||||||
|
: convertToExcalidrawElements([
|
||||||
|
{
|
||||||
|
id: "MyWwgGwbjyZoxKSa2pfBI",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 43.5,
|
||||||
|
y: 52.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#ff8787",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 246249428,
|
||||||
|
version: 427,
|
||||||
|
versionNonce: 2079096276,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ABor4pClpJJs6adQb4iqs",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 357.5,
|
||||||
|
y: 481.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#b2f2bb",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 1800887636,
|
||||||
|
version: 445,
|
||||||
|
versionNonce: 650486612,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "-FGknlZxdFGYZNoUr2lG5",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 1280.5,
|
||||||
|
y: 47.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#69db7c",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 883462868,
|
||||||
|
version: 532,
|
||||||
|
versionNonce: 912136404,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "XQ3bRtQnS_ydtq13c_p99",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 863.5,
|
||||||
|
y: 334.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#a5d8ff",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 2033653844,
|
||||||
|
version: 459,
|
||||||
|
versionNonce: 1984683604,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "GmC3zgvVq3xUd2Qn0cNX3",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 897.5,
|
||||||
|
y: 697.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#ffec99",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 81325524,
|
||||||
|
version: 488,
|
||||||
|
versionNonce: 1332016084,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "8nvmvwbmFmNAcLAmwZryR",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 456.5,
|
||||||
|
y: 229.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#eebefa",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 950276948,
|
||||||
|
version: 649,
|
||||||
|
versionNonce: 532633940,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7dyXEvhPDLA0-e3XCaM2H",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 1446.5,
|
||||||
|
y: 609.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#d0bfff",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 1659799764,
|
||||||
|
version: 570,
|
||||||
|
versionNonce: 313593556,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gJubXeAjkEAr6yywtiZ0T",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 1182.5,
|
||||||
|
y: 800.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#96f2d7",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 391007828,
|
||||||
|
version: 563,
|
||||||
|
versionNonce: 2026902612,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pSB2Izj9osR_XxVCzyVnD",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 1477.5,
|
||||||
|
y: 295.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#9775fa",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 110715860,
|
||||||
|
version: 590,
|
||||||
|
versionNonce: 535980500,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "yc7IKdtbnjLRaxJI5_q7m",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 696.5,
|
||||||
|
y: 21.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#4dabf7",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 1457899860,
|
||||||
|
version: 587,
|
||||||
|
versionNonce: 724438868,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "7KjY1vcMQm_i_i05wowTC",
|
||||||
|
type: "rectangle",
|
||||||
|
x: 447.5,
|
||||||
|
y: 771.5,
|
||||||
|
width: 357,
|
||||||
|
height: 280,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
backgroundColor: "#ffc9c9",
|
||||||
|
fillStyle: "solid",
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeStyle: "solid",
|
||||||
|
roughness: 1,
|
||||||
|
opacity: 100,
|
||||||
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
|
roundness: { type: 3 },
|
||||||
|
seed: 2072608468,
|
||||||
|
version: 610,
|
||||||
|
versionNonce: 1792607444,
|
||||||
|
boundElements: [],
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
initialStatePromiseRef.current.promise.resolve(data.scene);
|
initialStatePromiseRef.current.promise.resolve(data.scene);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,9 @@ import {
|
|||||||
throttleRAF,
|
throttleRAF,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
import { decryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { getVisibleSceneBounds } from "@excalidraw/element/bounds";
|
import { getVisibleSceneBounds } from "@excalidraw/element";
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
import {
|
import { isImageElement, isInitializedImageElement } from "@excalidraw/element";
|
||||||
isImageElement,
|
|
||||||
isInitializedImageElement,
|
|
||||||
} from "@excalidraw/element/typeChecks";
|
|
||||||
import { AbortError } from "@excalidraw/excalidraw/errors";
|
import { AbortError } from "@excalidraw/excalidraw/errors";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
import { withBatchedUpdates } from "@excalidraw/excalidraw/reactUtils";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||||
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
import { trackEvent } from "@excalidraw/excalidraw/analytics";
|
||||||
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
import { encryptData } from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
|
|
||||||
import type { UserIdleState } from "@excalidraw/common";
|
import type { UserIdleState } from "@excalidraw/common";
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const AIComponents = ({
|
|||||||
</br>
|
</br>
|
||||||
<div>You can also try <a href="${
|
<div>You can also try <a href="${
|
||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noopener">Excalidraw+</a> to get more requests.</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`,
|
</html>`,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
|
|||||||
className="encrypted-icon tooltip"
|
className="encrypted-icon tooltip"
|
||||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
aria-label={t("encrypted.link")}
|
aria-label={t("encrypted.link")}
|
||||||
>
|
>
|
||||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const ExcalidrawPlusAppLink = () => {
|
|||||||
import.meta.env.VITE_APP_PLUS_APP
|
import.meta.env.VITE_APP_PLUS_APP
|
||||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
className="plus-button"
|
className="plus-button"
|
||||||
>
|
>
|
||||||
Go to Excalidraw+
|
Go to Excalidraw+
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
} from "@excalidraw/excalidraw/data/encryption";
|
} from "@excalidraw/excalidraw/data/encryption";
|
||||||
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
import { serializeAsJSON } from "@excalidraw/excalidraw/data/json";
|
||||||
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element";
|
||||||
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
import { useI18n } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||||
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
import { compressData } from "@excalidraw/excalidraw/data/encode";
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ 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 { isInvisiblySmallElement } from "@excalidraw/element";
|
||||||
import { isInitializedImageElement } from "@excalidraw/element/typeChecks";
|
import { isInitializedImageElement } from "@excalidraw/element";
|
||||||
import { t } from "@excalidraw/excalidraw/i18n";
|
import { t } from "@excalidraw/excalidraw/i18n";
|
||||||
import { bytesToHexString } from "@excalidraw/common";
|
import { bytesToHexString } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { UserIdleState } 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/element/bounds";
|
import type { SceneBounds } from "@excalidraw/element";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||||||
<a
|
<a
|
||||||
class="welcome-screen-menu-item "
|
class="welcome-screen-menu-item "
|
||||||
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ import {
|
|||||||
createRedoAction,
|
createRedoAction,
|
||||||
createUndoAction,
|
createUndoAction,
|
||||||
} from "@excalidraw/excalidraw/actions/actionHistory";
|
} from "@excalidraw/excalidraw/actions/actionHistory";
|
||||||
import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncInvalidIndices } from "@excalidraw/element";
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
import { act, render, waitFor } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
|
import { StoreIncrement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { DurableIncrement, EphemeralIncrement } from "@excalidraw/element";
|
||||||
|
|
||||||
import ExcalidrawApp from "../App";
|
import ExcalidrawApp from "../App";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@@ -65,6 +69,79 @@ vi.mock("socket.io-client", () => {
|
|||||||
* i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
|
* i.e. multiplayer history tests could be a good first candidate, as we could test both history stacks simultaneously.
|
||||||
*/
|
*/
|
||||||
describe("collaboration", () => {
|
describe("collaboration", () => {
|
||||||
|
it("should emit two ephemeral increments even though updates get batched", async () => {
|
||||||
|
const durableIncrements: DurableIncrement[] = [];
|
||||||
|
const ephemeralIncrements: EphemeralIncrement[] = [];
|
||||||
|
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
h.store.onStoreIncrementEmitter.on((increment) => {
|
||||||
|
if (StoreIncrement.isDurable(increment)) {
|
||||||
|
durableIncrements.push(increment);
|
||||||
|
} else {
|
||||||
|
ephemeralIncrements.push(increment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect(h.store["scheduledMicroActions"].length).toBe(0);
|
||||||
|
expect(durableIncrements.length).toBe(0);
|
||||||
|
expect(ephemeralIncrements.length).toBe(0);
|
||||||
|
|
||||||
|
const rectProps = {
|
||||||
|
type: "rectangle",
|
||||||
|
id: "A",
|
||||||
|
height: 200,
|
||||||
|
width: 100,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const rect = API.createElement({ ...rectProps });
|
||||||
|
|
||||||
|
API.updateScene({
|
||||||
|
elements: [rect],
|
||||||
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// expect(commitSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(durableIncrements.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// simulate two batched remote updates
|
||||||
|
act(() => {
|
||||||
|
h.app.updateScene({
|
||||||
|
elements: [newElementWith(h.elements[0], { x: 100 })],
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
});
|
||||||
|
h.app.updateScene({
|
||||||
|
elements: [newElementWith(h.elements[0], { x: 200 })],
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
});
|
||||||
|
|
||||||
|
// we scheduled two micro actions,
|
||||||
|
// which confirms they are going to be executed as part of one batched component update
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect(h.store["scheduledMicroActions"].length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// altough the updates get batched,
|
||||||
|
// we expect two ephemeral increments for each update,
|
||||||
|
// and each such update should have the expected change
|
||||||
|
expect(ephemeralIncrements.length).toBe(2);
|
||||||
|
expect(ephemeralIncrements[0].change.elements.A).toEqual(
|
||||||
|
expect.objectContaining({ x: 100 }),
|
||||||
|
);
|
||||||
|
expect(ephemeralIncrements[1].change.elements.A).toEqual(
|
||||||
|
expect.objectContaining({ x: 200 }),
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line dot-notation
|
||||||
|
expect(h.store["scheduledMicroActions"].length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should allow to undo / redo even on force-deleted elements", async () => {
|
it("should allow to undo / redo even on force-deleted elements", async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
const rect1Props = {
|
const rect1Props = {
|
||||||
@@ -122,12 +199,13 @@ describe("collaboration", () => {
|
|||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const undoAction = createUndoAction(h.history, h.store);
|
const undoAction = createUndoAction(h.history);
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
// with explicit undo (as addition) we expect our item to be restored from the snapshot!
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
expect(API.getUndoStack().length).toBe(1);
|
||||||
|
expect(API.getRedoStack().length).toBe(1);
|
||||||
expect(API.getSnapshot()).toEqual([
|
expect(API.getSnapshot()).toEqual([
|
||||||
expect.objectContaining(rect1Props),
|
expect.objectContaining(rect1Props),
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
expect.objectContaining({ ...rect2Props, isDeleted: false }),
|
||||||
@@ -154,7 +232,7 @@ describe("collaboration", () => {
|
|||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const redoAction = createRedoAction(h.history, h.store);
|
const redoAction = createRedoAction(h.history);
|
||||||
act(() => h.app.actionManager.executeAction(redoAction));
|
act(() => h.app.actionManager.executeAction(redoAction));
|
||||||
|
|
||||||
// with explicit redo (as removal) we again restore the element from the snapshot!
|
// with explicit redo (as removal) we again restore the element from the snapshot!
|
||||||
@@ -170,79 +248,5 @@ describe("collaboration", () => {
|
|||||||
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
expect.objectContaining({ ...rect2Props, isDeleted: true }),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
|
||||||
|
|
||||||
// simulate local update
|
|
||||||
API.updateScene({
|
|
||||||
elements: syncInvalidIndices([
|
|
||||||
h.elements[0],
|
|
||||||
newElementWith(h.elements[1], { x: 100 }),
|
|
||||||
]),
|
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
|
||||||
|
|
||||||
// we expect to iterate the stack to the first visible change
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// simulate force deleting the element remotely
|
|
||||||
API.updateScene({
|
|
||||||
elements: syncInvalidIndices([rect1]),
|
|
||||||
captureUpdate: CaptureUpdateAction.NEVER,
|
|
||||||
});
|
|
||||||
|
|
||||||
// snapshot was correctly updated and marked the element as deleted
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(1);
|
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining(rect1Props),
|
|
||||||
expect.objectContaining({ ...rect2Props, isDeleted: true, x: 0 }),
|
|
||||||
]);
|
|
||||||
expect(h.elements).toEqual([expect.objectContaining(rect1Props)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
act(() => h.app.actionManager.executeAction(redoAction));
|
|
||||||
|
|
||||||
// with explicit redo (as update) we again restored the element from the snapshot!
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(API.getUndoStack().length).toBe(2);
|
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
|
||||||
expect(API.getSnapshot()).toEqual([
|
|
||||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
|
||||||
expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
|
|
||||||
]);
|
|
||||||
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
|
||||||
expect(h.elements).toEqual([
|
|
||||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
|
||||||
expect.objectContaining({ id: "B", isDeleted: true, x: 100 }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-6
@@ -11,11 +11,9 @@
|
|||||||
"@babel/preset-env": "7.26.9",
|
"@babel/preset-env": "7.26.9",
|
||||||
"@excalidraw/eslint-config": "1.0.3",
|
"@excalidraw/eslint-config": "1.0.3",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@playwright/experimental-ct-react": "1.52.0",
|
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/lodash.throttle": "4.1.7",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
"@types/node": "22.14.1",
|
|
||||||
"@types/react": "19.0.10",
|
"@types/react": "19.0.10",
|
||||||
"@types/react-dom": "19.0.4",
|
"@types/react-dom": "19.0.4",
|
||||||
"@types/socket.io-client": "3.0.0",
|
"@types/socket.io-client": "3.0.0",
|
||||||
@@ -35,6 +33,7 @@
|
|||||||
"pepjs": "0.5.3",
|
"pepjs": "0.5.3",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
"rewire": "6.0.0",
|
"rewire": "6.0.0",
|
||||||
|
"rimraf": "^5.0.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"vite": "5.0.12",
|
"vite": "5.0.12",
|
||||||
"vite-plugin-checker": "0.7.2",
|
"vite-plugin-checker": "0.7.2",
|
||||||
@@ -80,10 +79,9 @@
|
|||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease:excalidraw": "node scripts/prerelease.js",
|
"prerelease:excalidraw": "node scripts/prerelease.js",
|
||||||
"release:excalidraw": "node scripts/release.js",
|
"release:excalidraw": "node scripts/release.js",
|
||||||
"rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/{build,dist}",
|
"rm:build": "rimraf --glob excalidraw-app/build excalidraw-app/dist excalidraw-app/dev-dist packages/*/dist packages/*/build examples/*/build examples/*/dist",
|
||||||
"rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules",
|
"rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules",
|
||||||
"clean-install": "yarn rm:node_modules && yarn install",
|
"clean-install": "yarn rm:node_modules && yarn install"
|
||||||
"test-ct": "playwright test -c playwright-ct.config.ts"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"strip-ansi": "6.0.1"
|
"strip-ansi": "6.0.1"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"default": "./dist/prod/index.js"
|
"default": "./dist/prod/index.js"
|
||||||
},
|
},
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./../common/dist/types/common/src/*.d.ts"
|
"types": "./dist/types/common/src/*.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
"repository": "https://github.com/excalidraw/excalidraw",
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gen:types": "rm -rf types && tsc",
|
"gen:types": "rimraf types && tsc",
|
||||||
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|||||||
export const isWindows = /^Win/.test(navigator.platform);
|
export const isWindows = /^Win/.test(navigator.platform);
|
||||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||||
export const isFirefox =
|
export const isFirefox =
|
||||||
|
typeof window !== "undefined" &&
|
||||||
"netscape" in window &&
|
"netscape" in window &&
|
||||||
navigator.userAgent.indexOf("rv:") > 1 &&
|
navigator.userAgent.indexOf("rv:") > 1 &&
|
||||||
navigator.userAgent.indexOf("Gecko") > 1;
|
navigator.userAgent.indexOf("Gecko") > 1;
|
||||||
@@ -119,6 +120,7 @@ export const CLASSES = {
|
|||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
ZOOM_ACTIONS: "zoom-actions",
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||||
|
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
||||||
@@ -142,21 +144,52 @@ export const FONT_FAMILY = {
|
|||||||
"Lilita One": 7,
|
"Lilita One": 7,
|
||||||
"Comic Shanns": 8,
|
"Comic Shanns": 8,
|
||||||
"Liberation Sans": 9,
|
"Liberation Sans": 9,
|
||||||
|
Assistant: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Segoe UI Emoji fails to properly fallback for some glyphs: ∞, ∫, ≠
|
||||||
|
// so we need to have generic font fallback before it
|
||||||
|
export const SANS_SERIF_GENERIC_FONT = "sans-serif";
|
||||||
|
export const MONOSPACE_GENERIC_FONT = "monospace";
|
||||||
|
|
||||||
|
export const FONT_FAMILY_GENERIC_FALLBACKS = {
|
||||||
|
[SANS_SERIF_GENERIC_FONT]: 998,
|
||||||
|
[MONOSPACE_GENERIC_FONT]: 999,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FONT_FAMILY_FALLBACKS = {
|
export const FONT_FAMILY_FALLBACKS = {
|
||||||
[CJK_HAND_DRAWN_FALLBACK_FONT]: 100,
|
[CJK_HAND_DRAWN_FALLBACK_FONT]: 100,
|
||||||
|
...FONT_FAMILY_GENERIC_FALLBACKS,
|
||||||
[WINDOWS_EMOJI_FALLBACK_FONT]: 1000,
|
[WINDOWS_EMOJI_FALLBACK_FONT]: 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getGenericFontFamilyFallback(
|
||||||
|
fontFamily: number,
|
||||||
|
): keyof typeof FONT_FAMILY_GENERIC_FALLBACKS {
|
||||||
|
switch (fontFamily) {
|
||||||
|
case FONT_FAMILY.Cascadia:
|
||||||
|
case FONT_FAMILY["Comic Shanns"]:
|
||||||
|
return MONOSPACE_GENERIC_FONT;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return SANS_SERIF_GENERIC_FONT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getFontFamilyFallbacks = (
|
export const getFontFamilyFallbacks = (
|
||||||
fontFamily: number,
|
fontFamily: number,
|
||||||
): Array<keyof typeof FONT_FAMILY_FALLBACKS> => {
|
): Array<keyof typeof FONT_FAMILY_FALLBACKS> => {
|
||||||
|
const genericFallbackFont = getGenericFontFamilyFallback(fontFamily);
|
||||||
|
|
||||||
switch (fontFamily) {
|
switch (fontFamily) {
|
||||||
case FONT_FAMILY.Excalifont:
|
case FONT_FAMILY.Excalifont:
|
||||||
return [CJK_HAND_DRAWN_FALLBACK_FONT, WINDOWS_EMOJI_FALLBACK_FONT];
|
return [
|
||||||
|
CJK_HAND_DRAWN_FALLBACK_FONT,
|
||||||
|
genericFallbackFont,
|
||||||
|
WINDOWS_EMOJI_FALLBACK_FONT,
|
||||||
|
];
|
||||||
default:
|
default:
|
||||||
return [WINDOWS_EMOJI_FALLBACK_FONT];
|
return [genericFallbackFont, WINDOWS_EMOJI_FALLBACK_FONT];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -253,7 +286,7 @@ export const EXPORT_DATA_TYPES = {
|
|||||||
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
|
excalidrawClipboardWithAPI: "excalidraw-api/clipboard",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_SOURCE =
|
export const getExportSource = () =>
|
||||||
window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
|
window.EXCALIDRAW_EXPORT_SOURCE || window.location.origin;
|
||||||
|
|
||||||
// time in milliseconds
|
// time in milliseconds
|
||||||
@@ -474,3 +507,10 @@ export enum UserIdleState {
|
|||||||
AWAY = "away",
|
AWAY = "away",
|
||||||
IDLE = "idle",
|
IDLE = "idle",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* distance at which we merge points instead of adding a new merge-point
|
||||||
|
* when converting a line to a polygon (merge currently means overlaping
|
||||||
|
* the start and end points)
|
||||||
|
*/
|
||||||
|
export const LINE_POLYGON_POINT_MERGE_DISTANCE = 20;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { UnsubscribeCallback } from "./types";
|
import type { UnsubscribeCallback } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
type Subscriber<T extends any[]> = (...payload: T) => void;
|
type Subscriber<T extends any[]> = (...payload: T) => void;
|
||||||
|
|
||||||
@@ -22,8 +22,10 @@ export interface FontMetadata {
|
|||||||
};
|
};
|
||||||
/** flag to indicate a deprecated font */
|
/** flag to indicate a deprecated font */
|
||||||
deprecated?: true;
|
deprecated?: true;
|
||||||
/** flag to indicate a server-side only font */
|
/**
|
||||||
serverSide?: true;
|
* whether this is a font that users can use (= shown in font picker)
|
||||||
|
*/
|
||||||
|
private?: true;
|
||||||
/** flag to indiccate a local-only font */
|
/** flag to indiccate a local-only font */
|
||||||
local?: true;
|
local?: true;
|
||||||
/** flag to indicate a fallback font */
|
/** flag to indicate a fallback font */
|
||||||
@@ -44,7 +46,7 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
unitsPerEm: 1000,
|
unitsPerEm: 1000,
|
||||||
ascender: 1011,
|
ascender: 1011,
|
||||||
descender: -353,
|
descender: -353,
|
||||||
lineHeight: 1.35,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[FONT_FAMILY["Lilita One"]]: {
|
[FONT_FAMILY["Lilita One"]]: {
|
||||||
@@ -98,14 +100,23 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
descender: -434,
|
descender: -434,
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.15,
|
||||||
},
|
},
|
||||||
serverSide: true,
|
private: true,
|
||||||
|
},
|
||||||
|
[FONT_FAMILY.Assistant]: {
|
||||||
|
metrics: {
|
||||||
|
unitsPerEm: 2048,
|
||||||
|
ascender: 1021,
|
||||||
|
descender: -287,
|
||||||
|
lineHeight: 1.25,
|
||||||
|
},
|
||||||
|
private: true,
|
||||||
},
|
},
|
||||||
[FONT_FAMILY_FALLBACKS.Xiaolai]: {
|
[FONT_FAMILY_FALLBACKS.Xiaolai]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
unitsPerEm: 1000,
|
unitsPerEm: 1000,
|
||||||
ascender: 880,
|
ascender: 880,
|
||||||
descender: -144,
|
descender: -144,
|
||||||
lineHeight: 1.15,
|
lineHeight: 1.25,
|
||||||
},
|
},
|
||||||
fallback: true,
|
fallback: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ export * from "./promise-pool";
|
|||||||
export * from "./random";
|
export * from "./random";
|
||||||
export * from "./url";
|
export * from "./url";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
|
export * from "./emitter";
|
||||||
|
|||||||
@@ -68,3 +68,12 @@ export type MaybePromise<T> = T | Promise<T>;
|
|||||||
|
|
||||||
// get union of all keys from the union of types
|
// get union of all keys from the union of types
|
||||||
export type AllPossibleKeys<T> = T extends any ? keyof T : never;
|
export type AllPossibleKeys<T> = T extends any ? keyof T : never;
|
||||||
|
|
||||||
|
/** Strip all the methods or functions from a type */
|
||||||
|
export type DTO<T> = {
|
||||||
|
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MapEntry<M extends Map<any, any>> = M extends Map<infer K, infer V>
|
||||||
|
? [K, V]
|
||||||
|
: never;
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
isTransparent,
|
||||||
|
mapFind,
|
||||||
|
reduceToCommonValue,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
describe("@excalidraw/common/utils", () => {
|
||||||
|
describe("isTransparent()", () => {
|
||||||
|
it("should return true when color is rgb transparent", () => {
|
||||||
|
expect(isTransparent("#ff00")).toEqual(true);
|
||||||
|
expect(isTransparent("#fff00000")).toEqual(true);
|
||||||
|
expect(isTransparent("transparent")).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when color is not transparent", () => {
|
||||||
|
expect(isTransparent("#ced4da")).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reduceToCommonValue()", () => {
|
||||||
|
it("should return the common value when all values are the same", () => {
|
||||||
|
expect(reduceToCommonValue([1, 1])).toEqual(1);
|
||||||
|
expect(reduceToCommonValue([0, 0])).toEqual(0);
|
||||||
|
expect(reduceToCommonValue(["a", "a"])).toEqual("a");
|
||||||
|
expect(reduceToCommonValue(new Set([1]))).toEqual(1);
|
||||||
|
expect(reduceToCommonValue([""])).toEqual("");
|
||||||
|
expect(reduceToCommonValue([0])).toEqual(0);
|
||||||
|
|
||||||
|
const o = {};
|
||||||
|
expect(reduceToCommonValue([o, o])).toEqual(o);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
reduceToCommonValue([{ a: 1 }, { a: 1, b: 2 }], (o) => o.a),
|
||||||
|
).toEqual(1);
|
||||||
|
expect(
|
||||||
|
reduceToCommonValue(new Set([{ a: 1 }, { a: 1, b: 2 }]), (o) => o.a),
|
||||||
|
).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return `null` when values are different", () => {
|
||||||
|
expect(reduceToCommonValue([1, 2, 3])).toEqual(null);
|
||||||
|
expect(reduceToCommonValue(new Set([1, 2]))).toEqual(null);
|
||||||
|
expect(reduceToCommonValue([{ a: 1 }, { a: 2 }], (o) => o.a)).toEqual(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return `null` when some values are nullable", () => {
|
||||||
|
expect(reduceToCommonValue([1, null, 1])).toEqual(null);
|
||||||
|
expect(reduceToCommonValue([null, 1])).toEqual(null);
|
||||||
|
expect(reduceToCommonValue([1, undefined])).toEqual(null);
|
||||||
|
expect(reduceToCommonValue([undefined, 1])).toEqual(null);
|
||||||
|
expect(reduceToCommonValue([null])).toEqual(null);
|
||||||
|
expect(reduceToCommonValue([undefined])).toEqual(null);
|
||||||
|
expect(reduceToCommonValue([])).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("mapFind()", () => {
|
||||||
|
it("should return the first mapped non-null element", () => {
|
||||||
|
{
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
const result = mapFind(["a", "b", "c"], (value) => {
|
||||||
|
counter++;
|
||||||
|
return value === "b" ? 42 : null;
|
||||||
|
});
|
||||||
|
expect(result).toEqual(42);
|
||||||
|
expect(counter).toBe(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mapFind([1, 2], (value) => value * 0)).toBe(0);
|
||||||
|
expect(mapFind([1, 2], () => false)).toBe(false);
|
||||||
|
expect(mapFind([1, 2], () => "")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined if no mapped element is found", () => {
|
||||||
|
expect(mapFind([1, 2], () => undefined)).toBe(undefined);
|
||||||
|
expect(mapFind([1, 2], () => null)).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||||
|
import { getCenterForBounds, getElementBounds } from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
FontString,
|
FontString,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -101,7 +103,6 @@ export const getFontFamilyString = ({
|
|||||||
}) => {
|
}) => {
|
||||||
for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
|
for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
|
||||||
if (id === fontFamily) {
|
if (id === fontFamily) {
|
||||||
// TODO: we should fallback first to generic family names first
|
|
||||||
return `${fontFamilyString}${getFontFamilyFallbacks(id)
|
return `${fontFamilyString}${getFontFamilyFallbacks(id)
|
||||||
.map((x) => `, ${x}`)
|
.map((x) => `, ${x}`)
|
||||||
.join("")}`;
|
.join("")}`;
|
||||||
@@ -544,6 +545,20 @@ export const findLastIndex = <T>(
|
|||||||
return -1;
|
return -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** returns the first non-null mapped value */
|
||||||
|
export const mapFind = <T, K>(
|
||||||
|
collection: readonly T[],
|
||||||
|
iteratee: (value: T, index: number) => K | undefined | null,
|
||||||
|
): K | undefined => {
|
||||||
|
for (let idx = 0; idx < collection.length; idx++) {
|
||||||
|
const result = iteratee(collection[idx], idx);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const isTransparent = (color: string) => {
|
export const isTransparent = (color: string) => {
|
||||||
const isRGBTransparent = color.length === 5 && color.substr(4, 1) === "0";
|
const isRGBTransparent = color.length === 5 && color.substr(4, 1) === "0";
|
||||||
const isRRGGBBTransparent = color.length === 9 && color.substr(7, 2) === "00";
|
const isRRGGBBTransparent = color.length === 9 && color.substr(7, 2) === "00";
|
||||||
@@ -680,7 +695,7 @@ export const arrayToMap = <T extends { id: string } | string>(
|
|||||||
return items.reduce((acc: Map<string, T>, element) => {
|
return items.reduce((acc: Map<string, T>, element) => {
|
||||||
acc.set(typeof element === "string" ? element : element.id, element);
|
acc.set(typeof element === "string" ? element : element.id, element);
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map());
|
}, new Map() as Map<string, T>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const arrayToMapWithIndex = <T extends { id: string }>(
|
export const arrayToMapWithIndex = <T extends { id: string }>(
|
||||||
@@ -698,8 +713,8 @@ export const arrayToObject = <T>(
|
|||||||
array: readonly T[],
|
array: readonly T[],
|
||||||
groupBy?: (value: T) => string | number,
|
groupBy?: (value: T) => string | number,
|
||||||
) =>
|
) =>
|
||||||
array.reduce((acc, value) => {
|
array.reduce((acc, value, idx) => {
|
||||||
acc[groupBy ? groupBy(value) : String(value)] = value;
|
acc[groupBy ? groupBy(value) : idx] = value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [key: string]: T });
|
}, {} as { [key: string]: T });
|
||||||
|
|
||||||
@@ -735,6 +750,25 @@ export const arrayToList = <T>(array: readonly T[]): Node<T>[] =>
|
|||||||
return acc;
|
return acc;
|
||||||
}, [] as Node<T>[]);
|
}, [] as Node<T>[]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a readonly array or map into an iterable.
|
||||||
|
* Useful for avoiding entry allocations when iterating object / map on each iteration.
|
||||||
|
*/
|
||||||
|
export const toIterable = <T>(
|
||||||
|
values: readonly T[] | ReadonlyMap<string, T>,
|
||||||
|
): Iterable<T> => {
|
||||||
|
return Array.isArray(values) ? values : values.values();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a readonly array or map into an array.
|
||||||
|
*/
|
||||||
|
export const toArray = <T>(
|
||||||
|
values: readonly T[] | ReadonlyMap<string, T>,
|
||||||
|
): T[] => {
|
||||||
|
return Array.isArray(values) ? values : Array.from(toIterable(values));
|
||||||
|
};
|
||||||
|
|
||||||
export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
||||||
|
|
||||||
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
||||||
@@ -1207,16 +1241,13 @@ export const castArray = <T>(value: T | T[]): T[] =>
|
|||||||
|
|
||||||
export const elementCenterPoint = (
|
export const elementCenterPoint = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
xOffset: number = 0,
|
xOffset: number = 0,
|
||||||
yOffset: number = 0,
|
yOffset: number = 0,
|
||||||
) => {
|
) => {
|
||||||
const { x, y, width, height } = element;
|
const [x, y] = getCenterForBounds(getElementBounds(element, elementsMap));
|
||||||
|
|
||||||
const centerXPoint = x + width / 2 + xOffset;
|
return pointFrom<GlobalPoint>(x + xOffset, y + yOffset);
|
||||||
|
|
||||||
const centerYPoint = y + height / 2 + yOffset;
|
|
||||||
|
|
||||||
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** hack for Array.isArray type guard not working with readonly value[] */
|
/** hack for Array.isArray type guard not working with readonly value[] */
|
||||||
@@ -1225,11 +1256,39 @@ export const isReadonlyArray = (value?: any): value is readonly any[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const sizeOf = (
|
export const sizeOf = (
|
||||||
value: readonly number[] | Readonly<Map<any, any>> | Record<any, any>,
|
value:
|
||||||
|
| readonly unknown[]
|
||||||
|
| Readonly<Map<string, unknown>>
|
||||||
|
| Readonly<Record<string, unknown>>
|
||||||
|
| ReadonlySet<unknown>,
|
||||||
): number => {
|
): number => {
|
||||||
return isReadonlyArray(value)
|
return isReadonlyArray(value)
|
||||||
? value.length
|
? value.length
|
||||||
: value instanceof Map
|
: value instanceof Map || value instanceof Set
|
||||||
? value.size
|
? value.size
|
||||||
: Object.keys(value).length;
|
: Object.keys(value).length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const reduceToCommonValue = <T, R = T>(
|
||||||
|
collection: readonly T[] | ReadonlySet<T>,
|
||||||
|
getValue?: (item: T) => R,
|
||||||
|
): R | null => {
|
||||||
|
if (sizeOf(collection) === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueExtractor = getValue || ((item: T) => item as unknown as R);
|
||||||
|
|
||||||
|
let commonValue: R | null = null;
|
||||||
|
|
||||||
|
for (const item of collection) {
|
||||||
|
const value = valueExtractor(item);
|
||||||
|
if ((commonValue === null || commonValue === value) && value != null) {
|
||||||
|
commonValue = value;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commonValue;
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"default": "./dist/prod/index.js"
|
"default": "./dist/prod/index.js"
|
||||||
},
|
},
|
||||||
"./*": {
|
"./*": {
|
||||||
"types": "./../element/dist/types/element/src/*.d.ts"
|
"types": "./dist/types/element/src/*.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
"repository": "https://github.com/excalidraw/excalidraw",
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gen:types": "rm -rf types && tsc",
|
"gen:types": "rimraf types && tsc",
|
||||||
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,22 @@ import {
|
|||||||
toBrandedType,
|
toBrandedType,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
isReadonlyArray,
|
toArray,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { isNonDeletedElement } from "@excalidraw/element";
|
import { isNonDeletedElement } from "@excalidraw/element";
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { isFrameLikeElement } from "@excalidraw/element";
|
||||||
import { getElementsInGroup } from "@excalidraw/element/groups";
|
import { getElementsInGroup } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
syncInvalidIndices,
|
syncInvalidIndices,
|
||||||
syncMovedIndices,
|
syncMovedIndices,
|
||||||
validateFractionalIndices,
|
validateFractionalIndices,
|
||||||
} from "@excalidraw/element/fractionalIndex";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { getSelectedElements } from "@excalidraw/element/selection";
|
import { getSelectedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { mutateElement, type ElementUpdate } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
@@ -33,12 +34,13 @@ import type {
|
|||||||
Ordered,
|
Ordered,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Assert, SameType } from "@excalidraw/common/utility-types";
|
import type {
|
||||||
|
Assert,
|
||||||
|
Mutable,
|
||||||
|
SameType,
|
||||||
|
} from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../../excalidraw/types";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
|
||||||
|
|
||||||
type SceneStateCallback = () => void;
|
type SceneStateCallback = () => void;
|
||||||
type SceneStateCallbackRemover = () => void;
|
type SceneStateCallbackRemover = () => void;
|
||||||
@@ -103,44 +105,7 @@ const hashSelectionOpts = (
|
|||||||
// in our codebase
|
// in our codebase
|
||||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||||
|
|
||||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
export class Scene {
|
||||||
if (typeof elementKey === "string") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Scene {
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// static methods/props
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
|
||||||
private static sceneMapById = new Map<string, Scene>();
|
|
||||||
|
|
||||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
|
||||||
if (isIdKey(elementKey)) {
|
|
||||||
// for cases where we don't have access to the element object
|
|
||||||
// (e.g. restore serialized appState with id references)
|
|
||||||
this.sceneMapById.set(elementKey, scene);
|
|
||||||
} else {
|
|
||||||
this.sceneMapByElement.set(elementKey, scene);
|
|
||||||
// if mapping element objects, also cache the id string when later
|
|
||||||
// looking up by id alone
|
|
||||||
this.sceneMapById.set(elementKey.id, scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated pass down `app.scene` and use it directly
|
|
||||||
*/
|
|
||||||
static getScene(elementKey: ElementKey): Scene | null {
|
|
||||||
if (isIdKey(elementKey)) {
|
|
||||||
return this.sceneMapById.get(elementKey) || null;
|
|
||||||
}
|
|
||||||
return this.sceneMapByElement.get(elementKey) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// instance methods/props
|
// instance methods/props
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -199,6 +164,12 @@ class Scene {
|
|||||||
return this.frames;
|
return this.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(elements: ElementsMapOrArray | null = null) {
|
||||||
|
if (elements) {
|
||||||
|
this.replaceAllElements(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getSelectedElements(opts: {
|
getSelectedElements(opts: {
|
||||||
// NOTE can be ommitted by making Scene constructor require App instance
|
// NOTE can be ommitted by making Scene constructor require App instance
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
@@ -293,9 +264,8 @@ class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||||
const _nextElements = isReadonlyArray(nextElements)
|
// we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
|
||||||
? nextElements
|
const _nextElements = toArray(nextElements);
|
||||||
: Array.from(nextElements.values());
|
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
||||||
validateIndicesThrottled(_nextElements);
|
validateIndicesThrottled(_nextElements);
|
||||||
@@ -307,7 +277,6 @@ class Scene {
|
|||||||
nextFrameLikes.push(element);
|
nextFrameLikes.push(element);
|
||||||
}
|
}
|
||||||
this.elementsMap.set(element.id, element);
|
this.elementsMap.set(element.id, element);
|
||||||
Scene.mapElementToScene(element, this);
|
|
||||||
});
|
});
|
||||||
const nonDeletedElements = getNonDeletedElements(this.elements);
|
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||||
this.nonDeletedElements = nonDeletedElements.elements;
|
this.nonDeletedElements = nonDeletedElements.elements;
|
||||||
@@ -352,12 +321,6 @@ class Scene {
|
|||||||
this.selectedElementsCache.elements = null;
|
this.selectedElementsCache.elements = null;
|
||||||
this.selectedElementsCache.cache.clear();
|
this.selectedElementsCache.cache.clear();
|
||||||
|
|
||||||
Scene.sceneMapById.forEach((scene, elementKey) => {
|
|
||||||
if (scene === this) {
|
|
||||||
Scene.sceneMapById.delete(elementKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// done not for memory leaks, but to guard against possible late fires
|
// done not for memory leaks, but to guard against possible late fires
|
||||||
// (I guess?)
|
// (I guess?)
|
||||||
this.callbacks.clear();
|
this.callbacks.clear();
|
||||||
@@ -454,6 +417,40 @@ class Scene {
|
|||||||
// then, check if the id is a group
|
// then, check if the id is a group
|
||||||
return getElementsInGroup(elementsMap, id);
|
return getElementsInGroup(elementsMap, id);
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default Scene;
|
// Mutate an element with passed updates and trigger the component to update. Make sure you
|
||||||
|
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||||
|
mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||||
|
element: TElement,
|
||||||
|
updates: ElementUpdate<TElement>,
|
||||||
|
options: {
|
||||||
|
informMutation: boolean;
|
||||||
|
isDragging: boolean;
|
||||||
|
} = {
|
||||||
|
informMutation: true,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const elementsMap = this.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
const { version: prevVersion } = element;
|
||||||
|
const { version: nextVersion } = mutateElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
updates,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
// skip if the element is not in the scene (i.e. selection)
|
||||||
|
this.elementsMap.has(element.id) &&
|
||||||
|
// skip if the element's version hasn't changed, as mutateElement returned the same element
|
||||||
|
prevVersion !== nextVersion &&
|
||||||
|
options.informMutation
|
||||||
|
) {
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
|
|
||||||
import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math";
|
import {
|
||||||
|
pointFrom,
|
||||||
|
pointDistance,
|
||||||
|
type LocalPoint,
|
||||||
|
pointRotateRads,
|
||||||
|
} from "@excalidraw/math";
|
||||||
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
|
import type { GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types";
|
||||||
@@ -20,7 +29,12 @@ import { headingForPointIsHorizontal } from "./heading";
|
|||||||
|
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import { generateFreeDrawShape } from "./renderElement";
|
import { generateFreeDrawShape } from "./renderElement";
|
||||||
import { getArrowheadPoints, getDiamondPoints } from "./bounds";
|
import {
|
||||||
|
getArrowheadPoints,
|
||||||
|
getCenterForBounds,
|
||||||
|
getDiamondPoints,
|
||||||
|
getElementBounds,
|
||||||
|
} from "./bounds";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -28,10 +42,11 @@ import type {
|
|||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
@@ -303,6 +318,172 @@ const getArrowheadShapes = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateLinearCollisionShape = (
|
||||||
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
|
const generator = new RoughGenerator();
|
||||||
|
const options: Options = {
|
||||||
|
seed: element.seed,
|
||||||
|
disableMultiStroke: true,
|
||||||
|
disableMultiStrokeFill: true,
|
||||||
|
roughness: 0,
|
||||||
|
preserveVertices: true,
|
||||||
|
};
|
||||||
|
const center = getCenterForBounds(
|
||||||
|
getElementBounds(element, elementsMap, true),
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (element.type) {
|
||||||
|
case "line":
|
||||||
|
case "arrow": {
|
||||||
|
// points array can be empty in the beginning, so it is important to add
|
||||||
|
// initial position to it
|
||||||
|
const points = element.points.length
|
||||||
|
? element.points
|
||||||
|
: [pointFrom<LocalPoint>(0, 0)];
|
||||||
|
|
||||||
|
if (isElbowArrow(element)) {
|
||||||
|
return generator.path(generateElbowArrowShape(points, 16), options)
|
||||||
|
.sets[0].ops;
|
||||||
|
} else if (!element.roundness) {
|
||||||
|
return points.map((point, idx) => {
|
||||||
|
const p = pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(element.x + point[0], element.y + point[1]),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: idx === 0 ? "move" : "lineTo",
|
||||||
|
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return generator
|
||||||
|
.curve(points as unknown as RoughPoint[], options)
|
||||||
|
.sets[0].ops.slice(0, element.points.length)
|
||||||
|
.map((op, i) => {
|
||||||
|
if (i === 0) {
|
||||||
|
const p = pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[0],
|
||||||
|
element.y + op.data[1],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: "move",
|
||||||
|
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: "bcurveTo",
|
||||||
|
data: [
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[0],
|
||||||
|
element.y + op.data[1],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[2],
|
||||||
|
element.y + op.data[3],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[4],
|
||||||
|
element.y + op.data[5],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.map((p) =>
|
||||||
|
pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||||
|
)
|
||||||
|
.flat(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "freedraw": {
|
||||||
|
if (element.points.length < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const simplifiedPoints = simplify(
|
||||||
|
element.points as Mutable<LocalPoint[]>,
|
||||||
|
0.75,
|
||||||
|
);
|
||||||
|
|
||||||
|
return generator
|
||||||
|
.curve(simplifiedPoints as [number, number][], options)
|
||||||
|
.sets[0].ops.slice(0, element.points.length)
|
||||||
|
.map((op, i) => {
|
||||||
|
if (i === 0) {
|
||||||
|
const p = pointRotateRads<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[0],
|
||||||
|
element.y + op.data[1],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: "move",
|
||||||
|
data: pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
op: "bcurveTo",
|
||||||
|
data: [
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[0],
|
||||||
|
element.y + op.data[1],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[2],
|
||||||
|
element.y + op.data[3],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
pointRotateRads(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[4],
|
||||||
|
element.y + op.data[5],
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.map((p) =>
|
||||||
|
pointFrom<LocalPoint>(p[0] - element.x, p[1] - element.y),
|
||||||
|
)
|
||||||
|
.flat(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the roughjs shape for given element.
|
* Generates the roughjs shape for given element.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBoundingBox } from "./bounds";
|
import { getCommonBoundingBox } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { BoundingBox } from "./bounds";
|
import type { BoundingBox } from "./bounds";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
position: "start" | "center" | "end";
|
position: "start" | "center" | "end";
|
||||||
@@ -15,10 +14,10 @@ export interface Alignment {
|
|||||||
|
|
||||||
export const alignElements = (
|
export const alignElements = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -33,12 +32,13 @@ export const alignElements = (
|
|||||||
);
|
);
|
||||||
return group.map((element) => {
|
return group.map((element) => {
|
||||||
// update element
|
// update element
|
||||||
const updatedEle = mutateElement(element, {
|
const updatedEle = scene.mutateElement(element, {
|
||||||
x: element.x + translation.x,
|
x: element.x + translation.x,
|
||||||
y: element.y + translation.y,
|
y: element.y + translation.y,
|
||||||
});
|
});
|
||||||
|
|
||||||
// update bound elements
|
// update bound elements
|
||||||
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: group,
|
simultaneouslyUpdated: group,
|
||||||
});
|
});
|
||||||
return updatedEle;
|
return updatedEle;
|
||||||
|
|||||||
+250
-210
@@ -27,15 +27,11 @@ import {
|
|||||||
PRECISION,
|
PRECISION,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { isPointOnShape } from "@excalidraw/utils/collision";
|
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getCenterForBounds,
|
getCenterForBounds,
|
||||||
@@ -43,7 +39,7 @@ import {
|
|||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { intersectElementWithLineSegment } from "./collision";
|
import { intersectElementWithLineSegment } from "./collision";
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
headingForPointFromElement,
|
headingForPointFromElement,
|
||||||
headingIsHorizontal,
|
headingIsHorizontal,
|
||||||
@@ -65,9 +61,11 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
import { aabbForElement } from "./shapes";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
import type {
|
import type {
|
||||||
@@ -81,11 +79,10 @@ import type {
|
|||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
OrderedExcalidrawElement,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
SceneElementsMap,
|
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
|
PointsPositionUpdates,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export type SuggestedBinding =
|
export type SuggestedBinding =
|
||||||
@@ -110,7 +107,6 @@ export const isBindingEnabled = (appState: AppState): boolean => {
|
|||||||
|
|
||||||
export const FIXED_BINDING_DISTANCE = 5;
|
export const FIXED_BINDING_DISTANCE = 5;
|
||||||
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
export const BINDING_HIGHLIGHT_THICKNESS = 10;
|
||||||
export const BINDING_HIGHLIGHT_OFFSET = 4;
|
|
||||||
|
|
||||||
const getNonDeletedElements = (
|
const getNonDeletedElements = (
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
@@ -130,9 +126,9 @@ export const bindOrUnbindLinearElement = (
|
|||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startBindingElement: ExcalidrawBindableElement | null | "keep",
|
startBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||||
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||||
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||||
bindOrUnbindLinearElementEdge(
|
bindOrUnbindLinearElementEdge(
|
||||||
@@ -142,6 +138,7 @@ export const bindOrUnbindLinearElement = (
|
|||||||
"start",
|
"start",
|
||||||
boundToElementIds,
|
boundToElementIds,
|
||||||
unboundFromElementIds,
|
unboundFromElementIds,
|
||||||
|
scene,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
bindOrUnbindLinearElementEdge(
|
bindOrUnbindLinearElementEdge(
|
||||||
@@ -151,6 +148,7 @@ export const bindOrUnbindLinearElement = (
|
|||||||
"end",
|
"end",
|
||||||
boundToElementIds,
|
boundToElementIds,
|
||||||
unboundFromElementIds,
|
unboundFromElementIds,
|
||||||
|
scene,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -159,7 +157,7 @@ export const bindOrUnbindLinearElement = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
|
getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
boundElements: element.boundElements?.filter(
|
boundElements: element.boundElements?.filter(
|
||||||
(element) =>
|
(element) =>
|
||||||
element.type !== "arrow" || element.id !== linearElement.id,
|
element.type !== "arrow" || element.id !== linearElement.id,
|
||||||
@@ -177,7 +175,8 @@ const bindOrUnbindLinearElementEdge = (
|
|||||||
boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
|
boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||||
// Is mutated
|
// Is mutated
|
||||||
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): void => {
|
): void => {
|
||||||
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
||||||
if (bindableElement === "keep") {
|
if (bindableElement === "keep") {
|
||||||
@@ -186,7 +185,7 @@ const bindOrUnbindLinearElementEdge = (
|
|||||||
|
|
||||||
// null means break the bind, so nothing to consider here
|
// null means break the bind, so nothing to consider here
|
||||||
if (bindableElement === null) {
|
if (bindableElement === null) {
|
||||||
const unbound = unbindLinearElement(linearElement, startOrEnd);
|
const unbound = unbindLinearElement(linearElement, startOrEnd, scene);
|
||||||
if (unbound != null) {
|
if (unbound != null) {
|
||||||
unboundFromElementIds.add(unbound);
|
unboundFromElementIds.add(unbound);
|
||||||
}
|
}
|
||||||
@@ -209,57 +208,38 @@ const bindOrUnbindLinearElementEdge = (
|
|||||||
: startOrEnd === "start" ||
|
: startOrEnd === "start" ||
|
||||||
otherEdgeBindableElement.id !== bindableElement.id)
|
otherEdgeBindableElement.id !== bindableElement.id)
|
||||||
) {
|
) {
|
||||||
bindLinearElement(
|
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||||
linearElement,
|
|
||||||
bindableElement,
|
|
||||||
startOrEnd,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
boundToElementIds.add(bindableElement.id);
|
boundToElementIds.add(bindableElement.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap);
|
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||||
boundToElementIds.add(bindableElement.id);
|
boundToElementIds.add(bindableElement.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOriginalBindingIfStillCloseOfLinearElementEdge = (
|
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
edge: "start" | "end",
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
zoom?: AppState["zoom"],
|
|
||||||
): NonDeleted<ExcalidrawElement> | null => {
|
|
||||||
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
|
||||||
const elementId =
|
|
||||||
edge === "start"
|
|
||||||
? linearElement.startBinding?.elementId
|
|
||||||
: linearElement.endBinding?.elementId;
|
|
||||||
if (elementId) {
|
|
||||||
const element = elementsMap.get(elementId);
|
|
||||||
if (
|
|
||||||
isBindableElement(element) &&
|
|
||||||
bindingBorderTest(element, coors, elementsMap, zoom)
|
|
||||||
) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
const getOriginalBindingsIfStillCloseToArrowEnds = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
): (NonDeleted<ExcalidrawElement> | null)[] =>
|
||||||
["start", "end"].map((edge) =>
|
(["start", "end"] as const).map((edge) => {
|
||||||
getOriginalBindingIfStillCloseOfLinearElementEdge(
|
const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap);
|
||||||
linearElement,
|
const elementId =
|
||||||
edge as "start" | "end",
|
edge === "start"
|
||||||
elementsMap,
|
? linearElement.startBinding?.elementId
|
||||||
zoom,
|
: linearElement.endBinding?.elementId;
|
||||||
),
|
if (elementId) {
|
||||||
);
|
const element = elementsMap.get(elementId);
|
||||||
|
if (
|
||||||
|
isBindableElement(element) &&
|
||||||
|
bindingBorderTest(element, coors, elementsMap, zoom)
|
||||||
|
) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const getBindingStrategyForDraggingArrowEndpoints = (
|
const getBindingStrategyForDraggingArrowEndpoints = (
|
||||||
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
selectedElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
@@ -275,7 +255,7 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1;
|
||||||
const start = startDragged
|
const start = startDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -283,19 +263,10 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: null // If binding is disabled and start is dragged, break all binds
|
: null // If binding is disabled and start is dragged, break all binds
|
||||||
: !isElbowArrow(selectedElement)
|
|
||||||
? // We have to update the focus and gap of the binding, so let's rebind
|
|
||||||
getElligibleElementForBindingElement(
|
|
||||||
selectedElement,
|
|
||||||
"start",
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
zoom,
|
|
||||||
)
|
|
||||||
: "keep";
|
: "keep";
|
||||||
const end = endDragged
|
const end = endDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -303,15 +274,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: null // If binding is disabled and end is dragged, break all binds
|
: null // If binding is disabled and end is dragged, break all binds
|
||||||
: !isElbowArrow(selectedElement)
|
|
||||||
? // We have to update the focus and gap of the binding, so let's rebind
|
|
||||||
getElligibleElementForBindingElement(
|
|
||||||
selectedElement,
|
|
||||||
"end",
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
zoom,
|
|
||||||
)
|
|
||||||
: "keep";
|
: "keep";
|
||||||
|
|
||||||
return [start, end];
|
return [start, end];
|
||||||
@@ -336,7 +298,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
);
|
);
|
||||||
const start = startIsClose
|
const start = startIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -347,7 +309,7 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
: null;
|
: null;
|
||||||
const end = endIsClose
|
const end = endIsClose
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
? getElligibleElementForBindingElement(
|
? getEligibleElementForBindingElement(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -362,11 +324,9 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||||||
|
|
||||||
export const bindOrUnbindLinearElements = (
|
export const bindOrUnbindLinearElements = (
|
||||||
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
|
||||||
isBindingEnabled: boolean,
|
isBindingEnabled: boolean,
|
||||||
draggingPoints: readonly number[] | null,
|
draggingPoints: readonly number[] | null,
|
||||||
|
scene: Scene,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): void => {
|
): void => {
|
||||||
selectedElements.forEach((selectedElement) => {
|
selectedElements.forEach((selectedElement) => {
|
||||||
@@ -376,20 +336,20 @@ export const bindOrUnbindLinearElements = (
|
|||||||
selectedElement,
|
selectedElement,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
draggingPoints ?? [],
|
draggingPoints ?? [],
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
elements,
|
scene.getNonDeletedElements(),
|
||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: // The arrow itself (the shaft) or the inner joins are dragged
|
: // The arrow itself (the shaft) or the inner joins are dragged
|
||||||
getBindingStrategyForDraggingArrowOrJoints(
|
getBindingStrategyForDraggingArrowOrJoints(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
elements,
|
scene.getNonDeletedElements(),
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
zoom,
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
|
bindOrUnbindLinearElement(selectedElement, start, end, scene);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -429,15 +389,17 @@ export const maybeBindLinearElement = (
|
|||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
pointerCoords: { x: number; y: number },
|
pointerCoords: { x: number; y: number },
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
): void => {
|
): void => {
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
if (appState.startBoundElement != null) {
|
if (appState.startBoundElement != null) {
|
||||||
bindLinearElement(
|
bindLinearElement(
|
||||||
linearElement,
|
linearElement,
|
||||||
appState.startBoundElement,
|
appState.startBoundElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +420,7 @@ export const maybeBindLinearElement = (
|
|||||||
"end",
|
"end",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
|
bindLinearElement(linearElement, hoveredElement, "end", scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -466,28 +428,19 @@ export const maybeBindLinearElement = (
|
|||||||
const normalizePointBinding = (
|
const normalizePointBinding = (
|
||||||
binding: { focus: number; gap: number },
|
binding: { focus: number; gap: number },
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
) => {
|
) => ({
|
||||||
let gap = binding.gap;
|
...binding,
|
||||||
const maxGap = maxBindingGap(
|
gap: Math.min(
|
||||||
hoveredElement,
|
binding.gap,
|
||||||
hoveredElement.width,
|
maxBindingGap(hoveredElement, hoveredElement.width, hoveredElement.height),
|
||||||
hoveredElement.height,
|
),
|
||||||
);
|
});
|
||||||
|
|
||||||
if (gap > maxGap) {
|
|
||||||
gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...binding,
|
|
||||||
gap,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const bindLinearElement = (
|
export const bindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
if (!isArrowElement(linearElement)) {
|
if (!isArrowElement(linearElement)) {
|
||||||
return;
|
return;
|
||||||
@@ -500,7 +453,7 @@ export const bindLinearElement = (
|
|||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
),
|
),
|
||||||
@@ -513,18 +466,18 @@ export const bindLinearElement = (
|
|||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(linearElement, {
|
scene.mutateElement(linearElement, {
|
||||||
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
|
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
|
||||||
if (!boundElementsMap.has(linearElement.id)) {
|
if (!boundElementsMap.has(linearElement.id)) {
|
||||||
mutateElement(hoveredElement, {
|
scene.mutateElement(hoveredElement, {
|
||||||
boundElements: (hoveredElement.boundElements || []).concat({
|
boundElements: (hoveredElement.boundElements || []).concat({
|
||||||
id: linearElement.id,
|
id: linearElement.id,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
@@ -566,13 +519,14 @@ const isLinearElementSimple = (
|
|||||||
const unbindLinearElement = (
|
const unbindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
scene: Scene,
|
||||||
): ExcalidrawBindableElement["id"] | null => {
|
): ExcalidrawBindableElement["id"] | null => {
|
||||||
const field = startOrEnd === "start" ? "startBinding" : "endBinding";
|
const field = startOrEnd === "start" ? "startBinding" : "endBinding";
|
||||||
const binding = linearElement[field];
|
const binding = linearElement[field];
|
||||||
if (binding == null) {
|
if (binding == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
mutateElement(linearElement, { [field]: null });
|
scene.mutateElement(linearElement, { [field]: null });
|
||||||
return binding.elementId;
|
return binding.elementId;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -728,32 +682,42 @@ const calculateFocusAndGap = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
focus: determineFocusDistance(
|
||||||
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
hoveredElement,
|
||||||
|
elementsMap,
|
||||||
|
adjacentPoint,
|
||||||
|
edgePoint,
|
||||||
|
),
|
||||||
|
gap: Math.max(1, distanceToElement(hoveredElement, elementsMap, edgePoint)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Supports translating, rotating and scaling `changedElement` with bound
|
// Supports translating, rotating and scaling `changedElement` with bound
|
||||||
// linear elements.
|
// linear elements.
|
||||||
// Because scaling involves moving the focus points as well, it is
|
|
||||||
// done before the `changedElement` is updated, and the `newSize` is passed
|
|
||||||
// in explicitly.
|
|
||||||
export const updateBoundElements = (
|
export const updateBoundElements = (
|
||||||
changedElement: NonDeletedExcalidrawElement,
|
changedElement: NonDeletedExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
scene: Scene,
|
||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
changedElements?: Map<string, ExcalidrawElement>;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
|
if (!isBindableElement(changedElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||||
simultaneouslyUpdated,
|
simultaneouslyUpdated,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isBindableElement(changedElement)) {
|
let elementsMap: ElementsMap = scene.getNonDeletedElementsMap();
|
||||||
return;
|
if (options?.changedElements) {
|
||||||
|
elementsMap = new Map(elementsMap) as typeof elementsMap;
|
||||||
|
options.changedElements.forEach((element) => {
|
||||||
|
elementsMap.set(element.id, element);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||||
@@ -796,7 +760,7 @@ export const updateBoundElements = (
|
|||||||
|
|
||||||
// `linearElement` is being moved/scaled already, just update the binding
|
// `linearElement` is being moved/scaled already, just update the binding
|
||||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||||
mutateElement(element, bindings, true);
|
scene.mutateElement(element, bindings);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,48 +786,56 @@ export const updateBoundElements = (
|
|||||||
bindableElement,
|
bindableElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (point) {
|
if (point) {
|
||||||
return {
|
return [
|
||||||
index:
|
bindingProp === "startBinding" ? 0 : element.points.length - 1,
|
||||||
bindingProp === "startBinding" ? 0 : element.points.length - 1,
|
{ point },
|
||||||
point,
|
] as MapEntry<PointsPositionUpdates>;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
).filter(
|
).filter(
|
||||||
(
|
(update): update is MapEntry<PointsPositionUpdates> => update !== null,
|
||||||
update,
|
|
||||||
): update is NonNullable<{
|
|
||||||
index: number;
|
|
||||||
point: LocalPoint;
|
|
||||||
isDragging?: boolean;
|
|
||||||
}> => update !== null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(element, scene, new Map(updates), {
|
||||||
element,
|
...(changedElement.id === element.startBinding?.elementId
|
||||||
updates,
|
? { startBinding: bindings.startBinding }
|
||||||
{
|
: {}),
|
||||||
...(changedElement.id === element.startBinding?.elementId
|
...(changedElement.id === element.endBinding?.elementId
|
||||||
? { startBinding: bindings.startBinding }
|
? { endBinding: bindings.endBinding }
|
||||||
: {}),
|
: {}),
|
||||||
...(changedElement.id === element.endBinding?.elementId
|
});
|
||||||
? { endBinding: bindings.endBinding }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !boundText.isDeleted) {
|
if (boundText && !boundText.isDeleted) {
|
||||||
handleBindTextResize(element, elementsMap, false);
|
handleBindTextResize(element, scene, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateBindings = (
|
||||||
|
latestElement: ExcalidrawElement,
|
||||||
|
scene: Scene,
|
||||||
|
options?: {
|
||||||
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
|
newSize?: { width: number; height: number };
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if (isLinearElement(latestElement)) {
|
||||||
|
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
||||||
|
} else {
|
||||||
|
updateBoundElements(latestElement, scene, {
|
||||||
|
...options,
|
||||||
|
changedElements: new Map([[latestElement.id, latestElement]]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const doesNeedUpdate = (
|
const doesNeedUpdate = (
|
||||||
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
changedElement: ExcalidrawBindableElement,
|
changedElement: ExcalidrawBindableElement,
|
||||||
@@ -885,8 +857,8 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
otherPoint: Readonly<GlobalPoint>,
|
otherPoint: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||||
aabb: Bounds | undefined | null,
|
aabb: Bounds | undefined | null,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): Heading => {
|
): Heading => {
|
||||||
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p));
|
||||||
@@ -904,7 +876,7 @@ export const getHeadingForElbowArrowSnap = (
|
|||||||
|
|
||||||
if (!distance) {
|
if (!distance) {
|
||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
vectorFromPoint(p, elementCenterPoint(bindableElement, elementsMap)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,7 +889,7 @@ const getDistanceForBinding = (
|
|||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const distance = distanceToBindableElement(bindableElement, point);
|
const distance = distanceToElement(bindableElement, elementsMap, point);
|
||||||
const bindDistance = maxBindingGap(
|
const bindDistance = maxBindingGap(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
bindableElement.width,
|
bindableElement.width,
|
||||||
@@ -932,12 +904,13 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
arrow: ExcalidrawElbowArrowElement,
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
if (isDevEnv() || isTestEnv()) {
|
if (isDevEnv() || isTestEnv()) {
|
||||||
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
invariant(arrow.points.length > 1, "Arrow should have at least 2 points");
|
||||||
}
|
}
|
||||||
|
|
||||||
const aabb = aabbForElement(bindableElement);
|
const aabb = aabbForElement(bindableElement, elementsMap);
|
||||||
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>(
|
||||||
@@ -945,7 +918,7 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
arrow.y + localP[1],
|
arrow.y + localP[1],
|
||||||
);
|
);
|
||||||
const edgePoint = isRectanguloidElement(bindableElement)
|
const edgePoint = isRectanguloidElement(bindableElement)
|
||||||
? avoidRectangularCorner(bindableElement, globalP)
|
? avoidRectangularCorner(bindableElement, elementsMap, globalP)
|
||||||
: globalP;
|
: globalP;
|
||||||
const elbowed = isElbowArrow(arrow);
|
const elbowed = isElbowArrow(arrow);
|
||||||
const center = getCenterForBounds(aabb);
|
const center = getCenterForBounds(aabb);
|
||||||
@@ -964,26 +937,31 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
const isHorizontal = headingIsHorizontal(
|
const isHorizontal = headingIsHorizontal(
|
||||||
headingForPointFromElement(bindableElement, aabb, globalP),
|
headingForPointFromElement(bindableElement, aabb, globalP),
|
||||||
);
|
);
|
||||||
|
const snapPoint = snapToMid(bindableElement, elementsMap, edgePoint);
|
||||||
const otherPoint = pointFrom<GlobalPoint>(
|
const otherPoint = pointFrom<GlobalPoint>(
|
||||||
isHorizontal ? center[0] : edgePoint[0],
|
isHorizontal ? center[0] : snapPoint[0],
|
||||||
!isHorizontal ? center[1] : edgePoint[1],
|
!isHorizontal ? center[1] : snapPoint[1],
|
||||||
|
);
|
||||||
|
const intersector = lineSegment(
|
||||||
|
otherPoint,
|
||||||
|
pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
|
||||||
|
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||||
|
),
|
||||||
|
otherPoint,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
intersection = intersectElementWithLineSegment(
|
intersection = intersectElementWithLineSegment(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
lineSegment(
|
elementsMap,
|
||||||
otherPoint,
|
intersector,
|
||||||
pointFromVector(
|
FIXED_BINDING_DISTANCE,
|
||||||
vectorScale(
|
).sort(pointDistanceSq)[0];
|
||||||
vectorNormalize(vectorFromPoint(edgePoint, otherPoint)),
|
|
||||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
|
||||||
),
|
|
||||||
otherPoint,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)[0];
|
|
||||||
} else {
|
} else {
|
||||||
intersection = intersectElementWithLineSegment(
|
intersection = intersectElementWithLineSegment(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
lineSegment(
|
lineSegment(
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
pointFromVector(
|
pointFromVector(
|
||||||
@@ -1010,31 +988,15 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
return edgePoint;
|
return edgePoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elbowed) {
|
return elbowed ? intersection : edgePoint;
|
||||||
const scalar =
|
|
||||||
pointDistanceSq(edgePoint, center) -
|
|
||||||
pointDistanceSq(intersection, center) >
|
|
||||||
0
|
|
||||||
? FIXED_BINDING_DISTANCE
|
|
||||||
: -FIXED_BINDING_DISTANCE;
|
|
||||||
|
|
||||||
return pointFromVector(
|
|
||||||
vectorScale(
|
|
||||||
vectorNormalize(vectorFromPoint(edgePoint, intersection)),
|
|
||||||
scalar,
|
|
||||||
),
|
|
||||||
intersection,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return edgePoint;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const avoidRectangularCorner = (
|
export const avoidRectangularCorner = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||||
@@ -1127,35 +1089,34 @@ export const avoidRectangularCorner = (
|
|||||||
|
|
||||||
export const snapToMid = (
|
export const snapToMid = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
tolerance: number = 0.05,
|
tolerance: number = 0.05,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
|
const center = elementCenterPoint(element, elementsMap, -0.1, -0.1);
|
||||||
const center = elementCenterPoint(element, -0.1, -0.1);
|
|
||||||
|
|
||||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||||
|
|
||||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||||
// above and below certain px distance
|
// above and below certain px distance
|
||||||
const verticalThrehsold = clamp(tolerance * height, 5, 80);
|
const verticalThreshold = clamp(tolerance * height, 5, 80);
|
||||||
const horizontalThrehsold = clamp(tolerance * width, 5, 80);
|
const horizontalThreshold = clamp(tolerance * width, 5, 80);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nonRotated[0] <= x + width / 2 &&
|
nonRotated[0] <= x + width / 2 &&
|
||||||
nonRotated[1] > center[1] - verticalThrehsold &&
|
nonRotated[1] > center[1] - verticalThreshold &&
|
||||||
nonRotated[1] < center[1] + verticalThrehsold
|
nonRotated[1] < center[1] + verticalThreshold
|
||||||
) {
|
) {
|
||||||
// LEFT
|
// LEFT
|
||||||
return pointRotateRads(
|
return pointRotateRads<GlobalPoint>(
|
||||||
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
pointFrom(x - FIXED_BINDING_DISTANCE, center[1]),
|
||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
nonRotated[1] <= y + height / 2 &&
|
nonRotated[1] <= y + height / 2 &&
|
||||||
nonRotated[0] > center[0] - horizontalThrehsold &&
|
nonRotated[0] > center[0] - horizontalThreshold &&
|
||||||
nonRotated[0] < center[0] + horizontalThrehsold
|
nonRotated[0] < center[0] + horizontalThreshold
|
||||||
) {
|
) {
|
||||||
// TOP
|
// TOP
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
@@ -1165,8 +1126,8 @@ export const snapToMid = (
|
|||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
nonRotated[0] >= x + width / 2 &&
|
nonRotated[0] >= x + width / 2 &&
|
||||||
nonRotated[1] > center[1] - verticalThrehsold &&
|
nonRotated[1] > center[1] - verticalThreshold &&
|
||||||
nonRotated[1] < center[1] + verticalThrehsold
|
nonRotated[1] < center[1] + verticalThreshold
|
||||||
) {
|
) {
|
||||||
// RIGHT
|
// RIGHT
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
@@ -1176,8 +1137,8 @@ export const snapToMid = (
|
|||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
nonRotated[1] >= y + height / 2 &&
|
nonRotated[1] >= y + height / 2 &&
|
||||||
nonRotated[0] > center[0] - horizontalThrehsold &&
|
nonRotated[0] > center[0] - horizontalThreshold &&
|
||||||
nonRotated[0] < center[0] + horizontalThrehsold
|
nonRotated[0] < center[0] + horizontalThreshold
|
||||||
) {
|
) {
|
||||||
// DOWN
|
// DOWN
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
@@ -1185,6 +1146,49 @@ export const snapToMid = (
|
|||||||
center,
|
center,
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
|
} else if (element.type === "diamond") {
|
||||||
|
const distance = FIXED_BINDING_DISTANCE;
|
||||||
|
const topLeft = pointFrom<GlobalPoint>(
|
||||||
|
x + width / 4 - distance,
|
||||||
|
y + height / 4 - distance,
|
||||||
|
);
|
||||||
|
const topRight = pointFrom<GlobalPoint>(
|
||||||
|
x + (3 * width) / 4 + distance,
|
||||||
|
y + height / 4 - distance,
|
||||||
|
);
|
||||||
|
const bottomLeft = pointFrom<GlobalPoint>(
|
||||||
|
x + width / 4 - distance,
|
||||||
|
y + (3 * height) / 4 + distance,
|
||||||
|
);
|
||||||
|
const bottomRight = pointFrom<GlobalPoint>(
|
||||||
|
x + (3 * width) / 4 + distance,
|
||||||
|
y + (3 * height) / 4 + distance,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
pointDistance(topLeft, nonRotated) <
|
||||||
|
Math.max(horizontalThreshold, verticalThreshold)
|
||||||
|
) {
|
||||||
|
return pointRotateRads(topLeft, center, angle);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
pointDistance(topRight, nonRotated) <
|
||||||
|
Math.max(horizontalThreshold, verticalThreshold)
|
||||||
|
) {
|
||||||
|
return pointRotateRads(topRight, center, angle);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
pointDistance(bottomLeft, nonRotated) <
|
||||||
|
Math.max(horizontalThreshold, verticalThreshold)
|
||||||
|
) {
|
||||||
|
return pointRotateRads(bottomLeft, center, angle);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
pointDistance(bottomRight, nonRotated) <
|
||||||
|
Math.max(horizontalThreshold, verticalThreshold)
|
||||||
|
) {
|
||||||
|
return pointRotateRads(bottomRight, center, angle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
@@ -1218,7 +1222,7 @@ const updateBoundPoint = (
|
|||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
elementsMap,
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = elementCenterPoint(bindableElement);
|
const globalMidPoint = elementCenterPoint(bindableElement, elementsMap);
|
||||||
const global = pointFrom<GlobalPoint>(
|
const global = pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||||
@@ -1244,6 +1248,7 @@ const updateBoundPoint = (
|
|||||||
);
|
);
|
||||||
const focusPointAbsolute = determineFocusPoint(
|
const focusPointAbsolute = determineFocusPoint(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
binding.focus,
|
binding.focus,
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
);
|
);
|
||||||
@@ -1262,7 +1267,7 @@ const updateBoundPoint = (
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const center = elementCenterPoint(bindableElement);
|
const center = elementCenterPoint(bindableElement, elementsMap);
|
||||||
const interceptorLength =
|
const interceptorLength =
|
||||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||||
pointDistance(adjacentPoint, center) +
|
pointDistance(adjacentPoint, center) +
|
||||||
@@ -1270,6 +1275,7 @@ const updateBoundPoint = (
|
|||||||
const intersections = [
|
const intersections = [
|
||||||
...intersectElementWithLineSegment(
|
...intersectElementWithLineSegment(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
|
elementsMap,
|
||||||
lineSegment<GlobalPoint>(
|
lineSegment<GlobalPoint>(
|
||||||
adjacentPoint,
|
adjacentPoint,
|
||||||
pointFromVector(
|
pointFromVector(
|
||||||
@@ -1332,6 +1338,7 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
const globalMidPoint = pointFrom(
|
const globalMidPoint = pointFrom(
|
||||||
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
@@ -1375,7 +1382,7 @@ const maybeCalculateNewGapWhenScaling = (
|
|||||||
return { ...currentBinding, gap: newGap };
|
return { ...currentBinding, gap: newGap };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getElligibleElementForBindingElement = (
|
const getEligibleElementForBindingElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
@@ -1486,8 +1493,12 @@ export const fixBindingsAfterDeletion = (
|
|||||||
const elements = arrayToMap(sceneElements);
|
const elements = arrayToMap(sceneElements);
|
||||||
|
|
||||||
for (const element of deletedElements) {
|
for (const element of deletedElements) {
|
||||||
BoundElement.unbindAffected(elements, element, mutateElement);
|
BoundElement.unbindAffected(elements, element, (element, updates) =>
|
||||||
BindableElement.unbindAffected(elements, element, mutateElement);
|
mutateElement(element, elements, updates),
|
||||||
|
);
|
||||||
|
BindableElement.unbindAffected(elements, element, (element, updates) =>
|
||||||
|
mutateElement(element, elements, updates),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1523,14 +1534,38 @@ export const bindingBorderTest = (
|
|||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
fullShape?: boolean,
|
fullShape?: boolean,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
const p = pointFrom<GlobalPoint>(x, y);
|
||||||
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
|
const shouldTestInside =
|
||||||
|
// disable fullshape snapping for frame elements so we
|
||||||
|
// can bind to frame children
|
||||||
|
(fullShape || !isBindingFallthroughEnabled(element)) &&
|
||||||
|
!isFrameLikeElement(element);
|
||||||
|
|
||||||
const shape = getElementShape(element, elementsMap);
|
// PERF: Run a cheap test to see if the binding element
|
||||||
return (
|
// is even close to the element
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
const bounds = [
|
||||||
(fullShape === true &&
|
x - threshold,
|
||||||
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
y - threshold,
|
||||||
|
x + threshold,
|
||||||
|
y + threshold,
|
||||||
|
] as Bounds;
|
||||||
|
const elementBounds = getElementBounds(element, elementsMap);
|
||||||
|
if (!doBoundsIntersect(bounds, elementBounds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the intersection test against the element since it's close enough
|
||||||
|
const intersections = intersectElementWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
lineSegment(elementCenterPoint(element, elementsMap), p),
|
||||||
);
|
);
|
||||||
|
const distance = distanceToElement(element, elementsMap, p);
|
||||||
|
|
||||||
|
return shouldTestInside
|
||||||
|
? intersections.length === 0 || distance <= threshold
|
||||||
|
: intersections.length > 0 && distance <= threshold;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maxBindingGap = (
|
export const maxBindingGap = (
|
||||||
@@ -1550,7 +1585,7 @@ export const maxBindingGap = (
|
|||||||
// bigger bindable boundary for bigger elements
|
// bigger bindable boundary for bigger elements
|
||||||
Math.min(0.25 * smallerDimension, 32),
|
Math.min(0.25 * smallerDimension, 32),
|
||||||
// keep in sync with the zoomed highlight
|
// keep in sync with the zoomed highlight
|
||||||
BINDING_HIGHLIGHT_THICKNESS / zoomValue + BINDING_HIGHLIGHT_OFFSET,
|
BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1561,12 +1596,13 @@ export const maxBindingGap = (
|
|||||||
// of the element.
|
// of the element.
|
||||||
const determineFocusDistance = (
|
const determineFocusDistance = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
// Point on the line, in absolute coordinates
|
// Point on the line, in absolute coordinates
|
||||||
a: GlobalPoint,
|
a: GlobalPoint,
|
||||||
// Another point on the line, in absolute coordinates (closer to element)
|
// Another point on the line, in absolute coordinates (closer to element)
|
||||||
b: GlobalPoint,
|
b: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
if (pointsEqual(a, b)) {
|
if (pointsEqual(a, b)) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1691,12 +1727,13 @@ const determineFocusDistance = (
|
|||||||
|
|
||||||
const determineFocusPoint = (
|
const determineFocusPoint = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
// The oriented, relative distance from the center of `element` of the
|
// The oriented, relative distance from the center of `element` of the
|
||||||
// returned focusPoint
|
// returned focusPoint
|
||||||
focus: number,
|
focus: number,
|
||||||
adjacentPoint: GlobalPoint,
|
adjacentPoint: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
if (focus === 0) {
|
if (focus === 0) {
|
||||||
return center;
|
return center;
|
||||||
@@ -2119,6 +2156,7 @@ export class BindableElement {
|
|||||||
export const getGlobalFixedPointForBindableElement = (
|
export const getGlobalFixedPointForBindableElement = (
|
||||||
fixedPointRatio: [number, number],
|
fixedPointRatio: [number, number],
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
||||||
|
|
||||||
@@ -2127,7 +2165,7 @@ export const getGlobalFixedPointForBindableElement = (
|
|||||||
element.x + element.width * fixedX,
|
element.x + element.width * fixedX,
|
||||||
element.y + element.height * fixedY,
|
element.y + element.height * fixedY,
|
||||||
),
|
),
|
||||||
elementCenterPoint(element),
|
elementCenterPoint(element, elementsMap),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -2151,6 +2189,7 @@ export const getGlobalFixedPoints = (
|
|||||||
? getGlobalFixedPointForBindableElement(
|
? getGlobalFixedPointForBindableElement(
|
||||||
arrow.startBinding.fixedPoint,
|
arrow.startBinding.fixedPoint,
|
||||||
startElement as ExcalidrawBindableElement,
|
startElement as ExcalidrawBindableElement,
|
||||||
|
elementsMap,
|
||||||
)
|
)
|
||||||
: pointFrom<GlobalPoint>(
|
: pointFrom<GlobalPoint>(
|
||||||
arrow.x + arrow.points[0][0],
|
arrow.x + arrow.points[0][0],
|
||||||
@@ -2161,6 +2200,7 @@ export const getGlobalFixedPoints = (
|
|||||||
? getGlobalFixedPointForBindableElement(
|
? getGlobalFixedPointForBindableElement(
|
||||||
arrow.endBinding.fixedPoint,
|
arrow.endBinding.fixedPoint,
|
||||||
endElement as ExcalidrawBindableElement,
|
endElement as ExcalidrawBindableElement,
|
||||||
|
elementsMap,
|
||||||
)
|
)
|
||||||
: pointFrom<GlobalPoint>(
|
: pointFrom<GlobalPoint>(
|
||||||
arrow.x + arrow.points[arrow.points.length - 1][0],
|
arrow.x + arrow.points[arrow.points.length - 1][0],
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
rescalePoints,
|
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
invariant,
|
invariant,
|
||||||
|
rescalePoints,
|
||||||
sizeOf,
|
sizeOf,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
pointFrom,
|
|
||||||
pointDistance,
|
pointDistance,
|
||||||
|
pointFrom,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
@@ -33,8 +33,8 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
|||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { ShapeCache } from "./ShapeCache";
|
|
||||||
import { generateRoughOptions } from "./Shape";
|
import { generateRoughOptions } from "./Shape";
|
||||||
|
import { ShapeCache } from "./ShapeCache";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import {
|
import {
|
||||||
@@ -52,20 +52,20 @@ import {
|
|||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
Arrowhead,
|
|
||||||
ExcalidrawFreeDrawElement,
|
|
||||||
NonDeleted,
|
|
||||||
ExcalidrawTextElementWithContainer,
|
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawRectanguloidElement,
|
|
||||||
ExcalidrawEllipseElement,
|
|
||||||
ElementsMapOrArray,
|
|
||||||
} from "./types";
|
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
import type {
|
||||||
|
Arrowhead,
|
||||||
|
ElementsMap,
|
||||||
|
ElementsMapOrArray,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawRectanguloidElement,
|
||||||
|
ExcalidrawTextElementWithContainer,
|
||||||
|
NonDeleted,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export type RectangleBox = {
|
export type RectangleBox = {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -102,9 +102,23 @@ export class ElementBounds {
|
|||||||
version: ExcalidrawElement["version"];
|
version: ExcalidrawElement["version"];
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
private static nonRotatedBoundsCache = new WeakMap<
|
||||||
|
ExcalidrawElement,
|
||||||
|
{
|
||||||
|
bounds: Bounds;
|
||||||
|
version: ExcalidrawElement["version"];
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
static getBounds(element: ExcalidrawElement, elementsMap: ElementsMap) {
|
static getBounds(
|
||||||
const cachedBounds = ElementBounds.boundsCache.get(element);
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
nonRotated: boolean = false,
|
||||||
|
) {
|
||||||
|
const cachedBounds =
|
||||||
|
nonRotated && element.angle !== 0
|
||||||
|
? ElementBounds.nonRotatedBoundsCache.get(element)
|
||||||
|
: ElementBounds.boundsCache.get(element);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cachedBounds?.version &&
|
cachedBounds?.version &&
|
||||||
@@ -115,6 +129,23 @@ export class ElementBounds {
|
|||||||
) {
|
) {
|
||||||
return cachedBounds.bounds;
|
return cachedBounds.bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nonRotated && element.angle !== 0) {
|
||||||
|
const nonRotatedBounds = ElementBounds.calculateBounds(
|
||||||
|
{
|
||||||
|
...element,
|
||||||
|
angle: 0 as Radians,
|
||||||
|
},
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
ElementBounds.nonRotatedBoundsCache.set(element, {
|
||||||
|
version: element.version,
|
||||||
|
bounds: nonRotatedBounds,
|
||||||
|
});
|
||||||
|
|
||||||
|
return nonRotatedBounds;
|
||||||
|
}
|
||||||
|
|
||||||
const bounds = ElementBounds.calculateBounds(element, elementsMap);
|
const bounds = ElementBounds.calculateBounds(element, elementsMap);
|
||||||
|
|
||||||
ElementBounds.boundsCache.set(element, {
|
ElementBounds.boundsCache.set(element, {
|
||||||
@@ -939,8 +970,9 @@ const getLinearElementRotatedBounds = (
|
|||||||
export const getElementBounds = (
|
export const getElementBounds = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
|
nonRotated: boolean = false,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
return ElementBounds.getBounds(element, elementsMap);
|
return ElementBounds.getBounds(element, elementsMap, nonRotated);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
|
|||||||
+318
-132
@@ -2,51 +2,60 @@ import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
|||||||
import {
|
import {
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
line,
|
|
||||||
lineSegment,
|
lineSegment,
|
||||||
lineSegmentIntersectionPoints,
|
lineSegmentIntersectionPoints,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
|
pointFromVector,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
pointsEqual,
|
pointsEqual,
|
||||||
|
vectorFromPoint,
|
||||||
|
vectorNormalize,
|
||||||
|
vectorScale,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ellipse,
|
ellipse,
|
||||||
ellipseLineIntersectionPoints,
|
ellipseSegmentInterceptPoints,
|
||||||
} from "@excalidraw/math/ellipse";
|
} from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math";
|
||||||
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
|
||||||
Polygon,
|
|
||||||
Radians,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
|
|
||||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
import { isPathALoop } from "./shapes";
|
||||||
import { getElementBounds } from "./bounds";
|
import {
|
||||||
|
type Bounds,
|
||||||
|
doBoundsIntersect,
|
||||||
|
getCenterForBounds,
|
||||||
|
getElementBounds,
|
||||||
|
} from "./bounds";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
|
isFreeDrawElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
deconstructDiamondElement,
|
deconstructDiamondElement,
|
||||||
|
deconstructLinearOrFreeDrawElement,
|
||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
|
import { getBoundTextElement } from "./textElement";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
|
||||||
|
import { distanceToElement } from "./distance";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -72,45 +81,64 @@ export const shouldTestInside = (element: ExcalidrawElement) => {
|
|||||||
return isDraggableFromInside || isImageElement(element);
|
return isDraggableFromInside || isImageElement(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
|
export type HitTestArgs = {
|
||||||
x: number;
|
point: GlobalPoint;
|
||||||
y: number;
|
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
shape: GeometricShape<Point>;
|
threshold: number;
|
||||||
threshold?: number;
|
elementsMap: ElementsMap;
|
||||||
frameNameBound?: FrameNameBounds | null;
|
frameNameBound?: FrameNameBounds | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
export const hitElementItself = ({
|
||||||
x,
|
point,
|
||||||
y,
|
|
||||||
element,
|
element,
|
||||||
shape,
|
threshold,
|
||||||
threshold = 10,
|
elementsMap,
|
||||||
frameNameBound = null,
|
frameNameBound = null,
|
||||||
}: HitTestArgs<Point>) => {
|
}: HitTestArgs) => {
|
||||||
let hit = shouldTestInside(element)
|
// Hit test against a frame's name
|
||||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
const hitFrameName = frameNameBound
|
||||||
// we would need `onShape` as well to include the "borders"
|
? isPointWithinBounds(
|
||||||
isPointInShape(pointFrom(x, y), shape) ||
|
pointFrom(frameNameBound.x - threshold, frameNameBound.y - threshold),
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
point,
|
||||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
pointFrom(
|
||||||
|
frameNameBound.x + frameNameBound.width + threshold,
|
||||||
|
frameNameBound.y + frameNameBound.height + threshold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
|
||||||
// hit test against a frame's name
|
// Hit test against the extended, rotated bounding box of the element first
|
||||||
if (!hit && frameNameBound) {
|
const bounds = getElementBounds(element, elementsMap, true);
|
||||||
hit = isPointInShape(pointFrom(x, y), {
|
const hitBounds = isPointWithinBounds(
|
||||||
type: "polygon",
|
pointFrom(bounds[0] - threshold, bounds[1] - threshold),
|
||||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
pointRotateRads(
|
||||||
.data as Polygon<Point>,
|
point,
|
||||||
});
|
getCenterForBounds(bounds),
|
||||||
|
-element.angle as Radians,
|
||||||
|
),
|
||||||
|
pointFrom(bounds[2] + threshold, bounds[3] + threshold),
|
||||||
|
);
|
||||||
|
|
||||||
|
// PERF: Bail out early if the point is not even in the
|
||||||
|
// rotated bounding box or not hitting the frame name (saves 99%)
|
||||||
|
if (!hitBounds && !hitFrameName) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hit;
|
// Do the precise (and relatively costly) hit test
|
||||||
|
const hitElement = shouldTestInside(element)
|
||||||
|
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||||
|
// we would need `onShape` as well to include the "borders"
|
||||||
|
isPointInElement(point, element, elementsMap) ||
|
||||||
|
isPointOnElementOutline(point, element, elementsMap, threshold)
|
||||||
|
: isPointOnElementOutline(point, element, elementsMap, threshold);
|
||||||
|
|
||||||
|
return hitElement || hitFrameName;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBox = (
|
export const hitElementBoundingBox = (
|
||||||
x: number,
|
point: GlobalPoint,
|
||||||
y: number,
|
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
tolerance = 0,
|
tolerance = 0,
|
||||||
@@ -120,37 +148,42 @@ export const hitElementBoundingBox = (
|
|||||||
y1 -= tolerance;
|
y1 -= tolerance;
|
||||||
x2 += tolerance;
|
x2 += tolerance;
|
||||||
y2 += tolerance;
|
y2 += tolerance;
|
||||||
return isPointWithinBounds(
|
return isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2));
|
||||||
pointFrom(x1, y1),
|
|
||||||
pointFrom(x, y),
|
|
||||||
pointFrom(x2, y2),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBoxOnly = <
|
export const hitElementBoundingBoxOnly = (
|
||||||
Point extends GlobalPoint | LocalPoint,
|
hitArgs: HitTestArgs,
|
||||||
>(
|
|
||||||
hitArgs: HitTestArgs<Point>,
|
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) =>
|
||||||
return (
|
!hitElementItself(hitArgs) &&
|
||||||
!hitElementItself(hitArgs) &&
|
// bound text is considered part of the element (even if it's outside the bounding box)
|
||||||
// bound text is considered part of the element (even if it's outside the bounding box)
|
!hitElementBoundText(hitArgs.point, hitArgs.element, elementsMap) &&
|
||||||
!hitElementBoundText(
|
hitElementBoundingBox(hitArgs.point, hitArgs.element, elementsMap);
|
||||||
hitArgs.x,
|
|
||||||
hitArgs.y,
|
|
||||||
getBoundTextShape(hitArgs.element, elementsMap),
|
|
||||||
) &&
|
|
||||||
hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
export const hitElementBoundText = (
|
||||||
x: number,
|
point: GlobalPoint,
|
||||||
y: number,
|
element: ExcalidrawElement,
|
||||||
textShape: GeometricShape<Point> | null,
|
elementsMap: ElementsMap,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return !!textShape && isPointInShape(pointFrom(x, y), textShape);
|
const boundTextElementCandidate = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
|
if (!boundTextElementCandidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const boundTextElement = isLinearElement(element)
|
||||||
|
? {
|
||||||
|
...boundTextElementCandidate,
|
||||||
|
// arrow's bound text accurate position is not stored in the element's property
|
||||||
|
// but rather calculated and returned from the following static method
|
||||||
|
...LinearElementEditor.getBoundTextElementPosition(
|
||||||
|
element,
|
||||||
|
boundTextElementCandidate,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: boundTextElementCandidate;
|
||||||
|
|
||||||
|
return isPointInElement(point, boundTextElement, elementsMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,9 +196,26 @@ export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
*/
|
*/
|
||||||
export const intersectElementWithLineSegment = (
|
export const intersectElementWithLineSegment = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
line: LineSegment<GlobalPoint>,
|
line: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
|
onlyFirst = false,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
|
// First check if the line intersects the element's axis-aligned bounding box
|
||||||
|
// as it is much faster than checking intersection against the element's shape
|
||||||
|
const intersectorBounds = [
|
||||||
|
Math.min(line[0][0] - offset, line[1][0] - offset),
|
||||||
|
Math.min(line[0][1] - offset, line[1][1] - offset),
|
||||||
|
Math.max(line[0][0] + offset, line[1][0] + offset),
|
||||||
|
Math.max(line[0][1] + offset, line[1][1] + offset),
|
||||||
|
] as Bounds;
|
||||||
|
const elementBounds = getElementBounds(element, elementsMap);
|
||||||
|
|
||||||
|
if (!doBoundsIntersect(intersectorBounds, elementBounds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the actual intersection test against the element's shape
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
@@ -173,23 +223,88 @@ export const intersectElementWithLineSegment = (
|
|||||||
case "iframe":
|
case "iframe":
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
|
case "selection":
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
return intersectRectanguloidWithLineSegment(element, line, offset);
|
return intersectRectanguloidWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
line,
|
||||||
|
offset,
|
||||||
|
onlyFirst,
|
||||||
|
);
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return intersectDiamondWithLineSegment(element, line, offset);
|
return intersectDiamondWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
line,
|
||||||
|
offset,
|
||||||
|
onlyFirst,
|
||||||
|
);
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return intersectEllipseWithLineSegment(element, line, offset);
|
return intersectEllipseWithLineSegment(
|
||||||
default:
|
element,
|
||||||
throw new Error(`Unimplemented element type '${element.type}'`);
|
elementsMap,
|
||||||
|
line,
|
||||||
|
offset,
|
||||||
|
);
|
||||||
|
case "line":
|
||||||
|
case "freedraw":
|
||||||
|
case "arrow":
|
||||||
|
return intersectLinearOrFreeDrawWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
line,
|
||||||
|
onlyFirst,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const intersectLinearOrFreeDrawWithLineSegment = (
|
||||||
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
segment: LineSegment<GlobalPoint>,
|
||||||
|
onlyFirst = false,
|
||||||
|
): GlobalPoint[] => {
|
||||||
|
const [lines, curves] = deconstructLinearOrFreeDrawElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const intersections = [];
|
||||||
|
|
||||||
|
for (const l of lines) {
|
||||||
|
const intersection = lineSegmentIntersectionPoints(l, segment);
|
||||||
|
if (intersection) {
|
||||||
|
intersections.push(intersection);
|
||||||
|
|
||||||
|
if (onlyFirst) {
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of curves) {
|
||||||
|
const hits = curveIntersectLineSegment(c, segment);
|
||||||
|
|
||||||
|
if (hits.length > 0) {
|
||||||
|
intersections.push(...hits);
|
||||||
|
|
||||||
|
if (onlyFirst) {
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
|
};
|
||||||
|
|
||||||
const intersectRectanguloidWithLineSegment = (
|
const intersectRectanguloidWithLineSegment = (
|
||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
|
onlyFirst = false,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||||
@@ -206,34 +321,37 @@ const intersectRectanguloidWithLineSegment = (
|
|||||||
// Get the element's building components we can test against
|
// Get the element's building components we can test against
|
||||||
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
const [sides, corners] = deconstructRectanguloidElement(element, offset);
|
||||||
|
|
||||||
return (
|
const intersections: GlobalPoint[] = [];
|
||||||
// Test intersection against the sides, keep only the valid
|
|
||||||
// intersection points and rotate them back to scene space
|
for (const s of sides) {
|
||||||
sides
|
const intersection = lineSegmentIntersectionPoints(
|
||||||
.map((s) =>
|
lineSegment(rotatedA, rotatedB),
|
||||||
lineSegmentIntersectionPoints(
|
s,
|
||||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
);
|
||||||
s,
|
if (intersection) {
|
||||||
),
|
intersections.push(pointRotateRads(intersection, center, element.angle));
|
||||||
)
|
|
||||||
.filter((x) => x != null)
|
if (onlyFirst) {
|
||||||
.map((j) => pointRotateRads<GlobalPoint>(j!, center, element.angle))
|
return intersections;
|
||||||
// Test intersection against the corners which are cubic bezier curves,
|
}
|
||||||
// keep only the valid intersection points and rotate them back to scene
|
}
|
||||||
// space
|
}
|
||||||
.concat(
|
|
||||||
corners
|
for (const t of corners) {
|
||||||
.flatMap((t) =>
|
const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB));
|
||||||
curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)),
|
|
||||||
)
|
if (hits.length > 0) {
|
||||||
.filter((i) => i != null)
|
for (const j of hits) {
|
||||||
.map((j) => pointRotateRads(j, center, element.angle)),
|
intersections.push(pointRotateRads(j, center, element.angle));
|
||||||
)
|
}
|
||||||
// Remove duplicates
|
|
||||||
.filter(
|
if (onlyFirst) {
|
||||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
return intersections;
|
||||||
)
|
}
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -245,43 +363,51 @@ const intersectRectanguloidWithLineSegment = (
|
|||||||
*/
|
*/
|
||||||
const intersectDiamondWithLineSegment = (
|
const intersectDiamondWithLineSegment = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
|
onlyFirst = false,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
||||||
const [sides, curves] = deconstructDiamondElement(element, offset);
|
const [sides, corners] = deconstructDiamondElement(element, offset);
|
||||||
|
|
||||||
return (
|
const intersections: GlobalPoint[] = [];
|
||||||
sides
|
|
||||||
.map((s) =>
|
for (const s of sides) {
|
||||||
lineSegmentIntersectionPoints(
|
const intersection = lineSegmentIntersectionPoints(
|
||||||
lineSegment<GlobalPoint>(rotatedA, rotatedB),
|
lineSegment(rotatedA, rotatedB),
|
||||||
s,
|
s,
|
||||||
),
|
);
|
||||||
)
|
if (intersection) {
|
||||||
.filter((p): p is GlobalPoint => p != null)
|
intersections.push(pointRotateRads(intersection, center, element.angle));
|
||||||
// Rotate back intersection points
|
|
||||||
.map((p) => pointRotateRads<GlobalPoint>(p!, center, element.angle))
|
if (onlyFirst) {
|
||||||
.concat(
|
return intersections;
|
||||||
curves
|
}
|
||||||
.flatMap((p) =>
|
}
|
||||||
curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)),
|
}
|
||||||
)
|
|
||||||
.filter((p) => p != null)
|
for (const t of corners) {
|
||||||
// Rotate back intersection points
|
const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB));
|
||||||
.map((p) => pointRotateRads(p, center, element.angle)),
|
|
||||||
)
|
if (hits.length > 0) {
|
||||||
// Remove duplicates
|
for (const j of hits) {
|
||||||
.filter(
|
intersections.push(pointRotateRads(j, center, element.angle));
|
||||||
(p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx,
|
}
|
||||||
)
|
|
||||||
);
|
if (onlyFirst) {
|
||||||
|
return intersections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,16 +419,76 @@ const intersectDiamondWithLineSegment = (
|
|||||||
*/
|
*/
|
||||||
const intersectEllipseWithLineSegment = (
|
const intersectEllipseWithLineSegment = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
||||||
return ellipseLineIntersectionPoints(
|
return ellipseSegmentInterceptPoints(
|
||||||
ellipse(center, element.width / 2 + offset, element.height / 2 + offset),
|
ellipse(center, element.width / 2 + offset, element.height / 2 + offset),
|
||||||
line(rotatedA, rotatedB),
|
lineSegment(rotatedA, rotatedB),
|
||||||
).map((p) => pointRotateRads(p, center, element.angle));
|
).map((p) => pointRotateRads(p, center, element.angle));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given point is considered on the given shape's border
|
||||||
|
*
|
||||||
|
* @param point
|
||||||
|
* @param element
|
||||||
|
* @param tolerance
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const isPointOnElementOutline = (
|
||||||
|
point: GlobalPoint,
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
tolerance = 1,
|
||||||
|
) => distanceToElement(element, elementsMap, point) <= tolerance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given point is considered inside the element's border
|
||||||
|
*
|
||||||
|
* @param point
|
||||||
|
* @param element
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const isPointInElement = (
|
||||||
|
point: GlobalPoint,
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
(isLinearElement(element) || isFreeDrawElement(element)) &&
|
||||||
|
!isPathALoop(element.points)
|
||||||
|
) {
|
||||||
|
// There isn't any "inside" for a non-looping path
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
|
||||||
|
|
||||||
|
if (!isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
||||||
|
const otherPoint = pointFromVector(
|
||||||
|
vectorScale(
|
||||||
|
vectorNormalize(vectorFromPoint(point, center, 0.1)),
|
||||||
|
Math.max(element.width, element.height) * 2,
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
);
|
||||||
|
const intersector = lineSegment(point, otherPoint);
|
||||||
|
const intersections = intersectElementWithLineSegment(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
intersector,
|
||||||
|
).filter((p, pos, arr) => arr.findIndex((q) => pointsEqual(q, p)) === pos);
|
||||||
|
|
||||||
|
return intersections.length % 2 === 1;
|
||||||
|
};
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const MINIMAL_CROP_SIZE = 10;
|
|||||||
|
|
||||||
export const cropElement = (
|
export const cropElement = (
|
||||||
element: ExcalidrawImageElement,
|
element: ExcalidrawImageElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
transformHandle: TransformHandleType,
|
transformHandle: TransformHandleType,
|
||||||
naturalWidth: number,
|
naturalWidth: number,
|
||||||
naturalHeight: number,
|
naturalHeight: number,
|
||||||
@@ -63,7 +64,7 @@ export const cropElement = (
|
|||||||
|
|
||||||
const rotatedPointer = pointRotateRads(
|
const rotatedPointer = pointRotateRads(
|
||||||
pointFrom(pointerX, pointerY),
|
pointFrom(pointerX, pointerY),
|
||||||
elementCenterPoint(element),
|
elementCenterPoint(element, elementsMap),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,21 +12,27 @@ import type { GlobalPoint, Radians } from "@excalidraw/math";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
deconstructDiamondElement,
|
deconstructDiamondElement,
|
||||||
|
deconstructLinearOrFreeDrawElement,
|
||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ElementsMap,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const distanceToBindableElement = (
|
export const distanceToElement = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
|
case "selection":
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
@@ -34,11 +40,15 @@ export const distanceToBindableElement = (
|
|||||||
case "embeddable":
|
case "embeddable":
|
||||||
case "frame":
|
case "frame":
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
return distanceToRectanguloidElement(element, p);
|
return distanceToRectanguloidElement(element, elementsMap, p);
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return distanceToDiamondElement(element, p);
|
return distanceToDiamondElement(element, elementsMap, p);
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return distanceToEllipseElement(element, p);
|
return distanceToEllipseElement(element, elementsMap, p);
|
||||||
|
case "line":
|
||||||
|
case "arrow":
|
||||||
|
case "freedraw":
|
||||||
|
return distanceToLinearOrFreeDraElement(element, elementsMap, p);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,9 +62,10 @@ export const distanceToBindableElement = (
|
|||||||
*/
|
*/
|
||||||
const distanceToRectanguloidElement = (
|
const distanceToRectanguloidElement = (
|
||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
) => {
|
) => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
@@ -80,9 +91,10 @@ const distanceToRectanguloidElement = (
|
|||||||
*/
|
*/
|
||||||
const distanceToDiamondElement = (
|
const distanceToDiamondElement = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
@@ -108,12 +120,28 @@ const distanceToDiamondElement = (
|
|||||||
*/
|
*/
|
||||||
const distanceToEllipseElement = (
|
const distanceToEllipseElement = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
return ellipseDistanceFromPoint(
|
return ellipseDistanceFromPoint(
|
||||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||||
pointRotateRads(p, center, -element.angle as Radians),
|
pointRotateRads(p, center, -element.angle as Radians),
|
||||||
ellipse(center, element.width / 2, element.height / 2),
|
ellipse(center, element.width / 2, element.height / 2),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const distanceToLinearOrFreeDraElement = (
|
||||||
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
p: GlobalPoint,
|
||||||
|
) => {
|
||||||
|
const [lines, curves] = deconstructLinearOrFreeDrawElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
return Math.min(
|
||||||
|
...lines.map((s) => distanceToLineSegment(p, s)),
|
||||||
|
...curves.map((a) => curvePointDistance(a, p)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ import type {
|
|||||||
PointerDownState,
|
PointerDownState,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { getMinTextElementWidth } from "./textMeasurements";
|
import { getMinTextElementWidth } from "./textMeasurements";
|
||||||
@@ -29,6 +26,8 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
@@ -104,7 +103,7 @@ export const dragSelectedElements = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||||
if (!isArrowElement(element)) {
|
if (!isArrowElement(element)) {
|
||||||
// skip arrow labels since we calculate its position during render
|
// skip arrow labels since we calculate its position during render
|
||||||
const textElement = getBoundTextElement(
|
const textElement = getBoundTextElement(
|
||||||
@@ -112,9 +111,14 @@ export const dragSelectedElements = (
|
|||||||
scene.getNonDeletedElementsMap(),
|
scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
if (textElement) {
|
if (textElement) {
|
||||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
updateElementCoords(
|
||||||
|
pointerDownState,
|
||||||
|
textElement,
|
||||||
|
scene,
|
||||||
|
adjustedOffset,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -155,6 +159,7 @@ const calculateOffset = (
|
|||||||
const updateElementCoords = (
|
const updateElementCoords = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
scene: Scene,
|
||||||
dragOffset: { x: number; y: number },
|
dragOffset: { x: number; y: number },
|
||||||
) => {
|
) => {
|
||||||
const originalElement =
|
const originalElement =
|
||||||
@@ -163,7 +168,7 @@ const updateElementCoords = (
|
|||||||
const nextX = originalElement.x + dragOffset.x;
|
const nextX = originalElement.x + dragOffset.x;
|
||||||
const nextY = originalElement.y + dragOffset.y;
|
const nextY = originalElement.y + dragOffset.y;
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
x: nextX,
|
x: nextX,
|
||||||
y: nextY,
|
y: nextY,
|
||||||
});
|
});
|
||||||
@@ -190,6 +195,7 @@ export const dragNewElement = ({
|
|||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
zoom,
|
zoom,
|
||||||
|
scene,
|
||||||
widthAspectRatio = null,
|
widthAspectRatio = null,
|
||||||
originOffset = null,
|
originOffset = null,
|
||||||
informMutation = true,
|
informMutation = true,
|
||||||
@@ -205,6 +211,7 @@ export const dragNewElement = ({
|
|||||||
shouldMaintainAspectRatio: boolean;
|
shouldMaintainAspectRatio: boolean;
|
||||||
shouldResizeFromCenter: boolean;
|
shouldResizeFromCenter: boolean;
|
||||||
zoom: NormalizedZoomValue;
|
zoom: NormalizedZoomValue;
|
||||||
|
scene: Scene;
|
||||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||||
true */
|
true */
|
||||||
widthAspectRatio?: number | null;
|
widthAspectRatio?: number | null;
|
||||||
@@ -285,7 +292,7 @@ export const dragNewElement = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
x: newX + (originOffset?.x ?? 0),
|
x: newX + (originOffset?.x ?? 0),
|
||||||
@@ -295,7 +302,7 @@ export const dragNewElement = ({
|
|||||||
...textAutoResize,
|
...textAutoResize,
|
||||||
...imageInitialDimension,
|
...imageInitialDimension,
|
||||||
},
|
},
|
||||||
informMutation,
|
{ informMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,10 +29,9 @@ import {
|
|||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
getHeadingForElbowArrowSnap,
|
getHeadingForElbowArrowSnap,
|
||||||
getGlobalFixedPointForBindableElement,
|
getGlobalFixedPointForBindableElement,
|
||||||
snapToMid,
|
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
compareHeading,
|
compareHeading,
|
||||||
flipHeading,
|
flipHeading,
|
||||||
@@ -50,7 +49,6 @@ import { isBindableElement } from "./typeChecks";
|
|||||||
import {
|
import {
|
||||||
type ExcalidrawElbowArrowElement,
|
type ExcalidrawElbowArrowElement,
|
||||||
type NonDeletedSceneElementsMap,
|
type NonDeletedSceneElementsMap,
|
||||||
type SceneElementsMap,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||||
@@ -887,7 +885,7 @@ export const updateElbowArrowPoints = (
|
|||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
updates: {
|
updates: {
|
||||||
points?: readonly LocalPoint[];
|
points?: readonly LocalPoint[];
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: readonly FixedSegment[] | null;
|
||||||
startBinding?: FixedPointBinding | null;
|
startBinding?: FixedPointBinding | null;
|
||||||
endBinding?: FixedPointBinding | null;
|
endBinding?: FixedPointBinding | null;
|
||||||
},
|
},
|
||||||
@@ -899,50 +897,6 @@ export const updateElbowArrowPoints = (
|
|||||||
return { points: updates.points ?? arrow.points };
|
return { points: updates.points ?? arrow.points };
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE (mtolmacs): This is a temporary check to ensure that the incoming elbow
|
|
||||||
// arrow size is valid. This check will be removed once the issue is identified
|
|
||||||
if (
|
|
||||||
arrow.x < -MAX_POS ||
|
|
||||||
arrow.x > MAX_POS ||
|
|
||||||
arrow.y < -MAX_POS ||
|
|
||||||
arrow.y > MAX_POS ||
|
|
||||||
arrow.x + (updates?.points?.[updates?.points?.length - 1]?.[0] ?? 0) <
|
|
||||||
-MAX_POS ||
|
|
||||||
arrow.x + (updates?.points?.[updates?.points?.length - 1]?.[0] ?? 0) >
|
|
||||||
MAX_POS ||
|
|
||||||
arrow.y + (updates?.points?.[updates?.points?.length - 1]?.[1] ?? 0) <
|
|
||||||
-MAX_POS ||
|
|
||||||
arrow.y + (updates?.points?.[updates?.points?.length - 1]?.[1] ?? 0) >
|
|
||||||
MAX_POS ||
|
|
||||||
arrow.x + (arrow?.points?.[arrow?.points?.length - 1]?.[0] ?? 0) <
|
|
||||||
-MAX_POS ||
|
|
||||||
arrow.x + (arrow?.points?.[arrow?.points?.length - 1]?.[0] ?? 0) >
|
|
||||||
MAX_POS ||
|
|
||||||
arrow.y + (arrow?.points?.[arrow?.points?.length - 1]?.[1] ?? 0) <
|
|
||||||
-MAX_POS ||
|
|
||||||
arrow.y + (arrow?.points?.[arrow?.points?.length - 1]?.[1] ?? 0) > MAX_POS
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
"Elbow arrow (or update) is outside reasonable bounds (> 1e6)",
|
|
||||||
{
|
|
||||||
arrow,
|
|
||||||
updates,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// @ts-ignore See above note
|
|
||||||
arrow.x = clamp(arrow.x, -MAX_POS, MAX_POS);
|
|
||||||
// @ts-ignore See above note
|
|
||||||
arrow.y = clamp(arrow.y, -MAX_POS, MAX_POS);
|
|
||||||
if (updates.points) {
|
|
||||||
updates.points = updates.points.map(([x, y]) =>
|
|
||||||
pointFrom<LocalPoint>(
|
|
||||||
clamp(x, -MAX_POS, MAX_POS),
|
|
||||||
clamp(y, -MAX_POS, MAX_POS),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!import.meta.env.PROD) {
|
if (!import.meta.env.PROD) {
|
||||||
invariant(
|
invariant(
|
||||||
!updates.points || updates.points.length >= 2,
|
!updates.points || updates.points.length >= 2,
|
||||||
@@ -975,6 +929,25 @@ export const updateElbowArrowPoints = (
|
|||||||
),
|
),
|
||||||
"Elbow arrow segments must be either horizontal or vertical",
|
"Elbow arrow segments must be either horizontal or vertical",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
invariant(
|
||||||
|
updates.fixedSegments?.find(
|
||||||
|
(segment) =>
|
||||||
|
segment.index === 1 &&
|
||||||
|
pointsEqual(segment.start, (updates.points ?? arrow.points)[0]),
|
||||||
|
) == null &&
|
||||||
|
updates.fixedSegments?.find(
|
||||||
|
(segment) =>
|
||||||
|
segment.index === (updates.points ?? arrow.points).length - 1 &&
|
||||||
|
pointsEqual(
|
||||||
|
segment.end,
|
||||||
|
(updates.points ?? arrow.points)[
|
||||||
|
(updates.points ?? arrow.points).length - 1
|
||||||
|
],
|
||||||
|
),
|
||||||
|
) == null,
|
||||||
|
"The first and last segments cannot be fixed",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||||
@@ -1255,6 +1228,7 @@ const getElbowArrowData = (
|
|||||||
arrow.startBinding?.fixedPoint,
|
arrow.startBinding?.fixedPoint,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
|
elementsMap,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
const endGlobalPoint = getGlobalPoint(
|
const endGlobalPoint = getGlobalPoint(
|
||||||
@@ -1268,21 +1242,22 @@ const getElbowArrowData = (
|
|||||||
arrow.endBinding?.fixedPoint,
|
arrow.endBinding?.fixedPoint,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
|
elementsMap,
|
||||||
options?.isDragging,
|
options?.isDragging,
|
||||||
);
|
);
|
||||||
const startHeading = getBindPointHeading(
|
const startHeading = getBindPointHeading(
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
elementsMap,
|
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
const endHeading = getBindPointHeading(
|
const endHeading = getBindPointHeading(
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
elementsMap,
|
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
const startPointBounds = [
|
const startPointBounds = [
|
||||||
startGlobalPoint[0] - 2,
|
startGlobalPoint[0] - 2,
|
||||||
@@ -1299,6 +1274,7 @@ const getElbowArrowData = (
|
|||||||
const startElementBounds = hoveredStartElement
|
const startElementBounds = hoveredStartElement
|
||||||
? aabbForElement(
|
? aabbForElement(
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
|
elementsMap,
|
||||||
offsetFromHeading(
|
offsetFromHeading(
|
||||||
startHeading,
|
startHeading,
|
||||||
arrow.startArrowhead
|
arrow.startArrowhead
|
||||||
@@ -1311,6 +1287,7 @@ const getElbowArrowData = (
|
|||||||
const endElementBounds = hoveredEndElement
|
const endElementBounds = hoveredEndElement
|
||||||
? aabbForElement(
|
? aabbForElement(
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
|
elementsMap,
|
||||||
offsetFromHeading(
|
offsetFromHeading(
|
||||||
endHeading,
|
endHeading,
|
||||||
arrow.endArrowhead
|
arrow.endArrowhead
|
||||||
@@ -1326,6 +1303,7 @@ const getElbowArrowData = (
|
|||||||
hoveredEndElement
|
hoveredEndElement
|
||||||
? aabbForElement(
|
? aabbForElement(
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
|
elementsMap,
|
||||||
offsetFromHeading(endHeading, BASE_PADDING, BASE_PADDING),
|
offsetFromHeading(endHeading, BASE_PADDING, BASE_PADDING),
|
||||||
)
|
)
|
||||||
: endPointBounds,
|
: endPointBounds,
|
||||||
@@ -1335,6 +1313,7 @@ const getElbowArrowData = (
|
|||||||
hoveredStartElement
|
hoveredStartElement
|
||||||
? aabbForElement(
|
? aabbForElement(
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
|
elementsMap,
|
||||||
offsetFromHeading(startHeading, BASE_PADDING, BASE_PADDING),
|
offsetFromHeading(startHeading, BASE_PADDING, BASE_PADDING),
|
||||||
)
|
)
|
||||||
: startPointBounds,
|
: startPointBounds,
|
||||||
@@ -1381,8 +1360,8 @@ const getElbowArrowData = (
|
|||||||
BASE_PADDING,
|
BASE_PADDING,
|
||||||
),
|
),
|
||||||
boundsOverlap,
|
boundsOverlap,
|
||||||
hoveredStartElement && aabbForElement(hoveredStartElement),
|
hoveredStartElement && aabbForElement(hoveredStartElement, elementsMap),
|
||||||
hoveredEndElement && aabbForElement(hoveredEndElement),
|
hoveredEndElement && aabbForElement(hoveredEndElement, elementsMap),
|
||||||
);
|
);
|
||||||
const startDonglePosition = getDonglePosition(
|
const startDonglePosition = getDonglePosition(
|
||||||
dynamicAABBs[0],
|
dynamicAABBs[0],
|
||||||
@@ -2213,34 +2192,35 @@ const getGlobalPoint = (
|
|||||||
fixedPointRatio: [number, number] | undefined | null,
|
fixedPointRatio: [number, number] | undefined | null,
|
||||||
initialPoint: GlobalPoint,
|
initialPoint: GlobalPoint,
|
||||||
element?: ExcalidrawBindableElement | null,
|
element?: ExcalidrawBindableElement | null,
|
||||||
|
elementsMap?: ElementsMap,
|
||||||
isDragging?: boolean,
|
isDragging?: boolean,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
if (element) {
|
if (element && elementsMap) {
|
||||||
const snapPoint = bindPointToSnapToElementOutline(
|
return bindPointToSnapToElementOutline(
|
||||||
arrow,
|
arrow,
|
||||||
element,
|
element,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
return snapToMid(element, snapPoint);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return initialPoint;
|
return initialPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element) {
|
if (element && elementsMap) {
|
||||||
const fixedGlobalPoint = getGlobalFixedPointForBindableElement(
|
const fixedGlobalPoint = getGlobalFixedPointForBindableElement(
|
||||||
fixedPointRatio || [0, 0],
|
fixedPointRatio || [0, 0],
|
||||||
element,
|
element,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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(element, fixedGlobalPoint) -
|
distanceToElement(element, elementsMap, fixedGlobalPoint) -
|
||||||
FIXED_BINDING_DISTANCE,
|
FIXED_BINDING_DISTANCE,
|
||||||
) > 0.01
|
) > 0.01
|
||||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
? bindPointToSnapToElementOutline(arrow, element, startOrEnd, elementsMap)
|
||||||
: fixedGlobalPoint;
|
: fixedGlobalPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2250,9 +2230,9 @@ const getGlobalPoint = (
|
|||||||
const getBindPointHeading = (
|
const getBindPointHeading = (
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
otherPoint: GlobalPoint,
|
otherPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
): Heading =>
|
): Heading =>
|
||||||
getHeadingForElbowArrowSnap(
|
getHeadingForElbowArrowSnap(
|
||||||
p,
|
p,
|
||||||
@@ -2261,15 +2241,16 @@ const getBindPointHeading = (
|
|||||||
hoveredElement &&
|
hoveredElement &&
|
||||||
aabbForElement(
|
aabbForElement(
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
Array(4).fill(distanceToBindableElement(hoveredElement, p)) as [
|
elementsMap,
|
||||||
|
Array(4).fill(distanceToElement(hoveredElement, elementsMap, p)) as [
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
elementsMap,
|
|
||||||
origPoint,
|
origPoint,
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getHoveredElement = (
|
const getHoveredElement = (
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const RE_GH_GIST = /^https:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)/;
|
|||||||
const RE_GH_GIST_EMBED =
|
const RE_GH_GIST_EMBED =
|
||||||
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i;
|
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist\.github\.com\/.*?)\.js["']/i;
|
||||||
|
|
||||||
|
const RE_MSFORMS = /^(?:https?:\/\/)?forms\.microsoft\.com\//;
|
||||||
|
|
||||||
// not anchored to start to allow <blockquote> twitter embeds
|
// not anchored to start to allow <blockquote> twitter embeds
|
||||||
const RE_TWITTER =
|
const RE_TWITTER =
|
||||||
/(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/;
|
/(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com\/[^/]+\/status\/(\d+)/;
|
||||||
@@ -69,6 +71,7 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"val.town",
|
"val.town",
|
||||||
"giphy.com",
|
"giphy.com",
|
||||||
"reddit.com",
|
"reddit.com",
|
||||||
|
"forms.microsoft.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ALLOW_SAME_ORIGIN = new Set([
|
const ALLOW_SAME_ORIGIN = new Set([
|
||||||
@@ -82,6 +85,7 @@ const ALLOW_SAME_ORIGIN = new Set([
|
|||||||
"*.simplepdf.eu",
|
"*.simplepdf.eu",
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"reddit.com",
|
"reddit.com",
|
||||||
|
"forms.microsoft.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const createSrcDoc = (body: string) => {
|
export const createSrcDoc = (body: string) => {
|
||||||
@@ -206,6 +210,10 @@ export const getEmbedLink = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RE_MSFORMS.test(link) && !link.includes("embed=true")) {
|
||||||
|
link += link.includes("?") ? "&embed=true" : "?embed=true";
|
||||||
|
}
|
||||||
|
|
||||||
if (RE_TWITTER.test(link)) {
|
if (RE_TWITTER.test(link)) {
|
||||||
const postId = link.match(RE_TWITTER)![1];
|
const postId = link.match(RE_TWITTER)![1];
|
||||||
// the embed srcdoc still supports twitter.com domain only.
|
// the embed srcdoc still supports twitter.com domain only.
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ import {
|
|||||||
type OrderedExcalidrawElement,
|
type OrderedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
type LinkDirection = "up" | "right" | "down" | "left";
|
type LinkDirection = "up" | "right" | "down" | "left";
|
||||||
|
|
||||||
const VERTICAL_OFFSET = 100;
|
const VERTICAL_OFFSET = 100;
|
||||||
@@ -93,10 +95,11 @@ const getNodeRelatives = (
|
|||||||
type === "predecessors" ? el.points[el.points.length - 1] : [0, 0]
|
type === "predecessors" ? el.points[el.points.length - 1] : [0, 0]
|
||||||
) as Readonly<LocalPoint>;
|
) as Readonly<LocalPoint>;
|
||||||
|
|
||||||
const heading = headingForPointFromElement(node, aabbForElement(node), [
|
const heading = headingForPointFromElement(
|
||||||
edgePoint[0] + el.x,
|
node,
|
||||||
edgePoint[1] + el.y,
|
aabbForElement(node, elementsMap),
|
||||||
] as Readonly<GlobalPoint>);
|
[edgePoint[0] + el.x, edgePoint[1] + el.y] as Readonly<GlobalPoint>,
|
||||||
|
);
|
||||||
|
|
||||||
acc.push({
|
acc.push({
|
||||||
relative,
|
relative,
|
||||||
@@ -236,10 +239,11 @@ const getOffsets = (
|
|||||||
|
|
||||||
const addNewNode = (
|
const addNewNode = (
|
||||||
element: ExcalidrawFlowchartNodeElement,
|
element: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const successors = getSuccessors(element, elementsMap, direction);
|
const successors = getSuccessors(element, elementsMap, direction);
|
||||||
const predeccessors = getPredecessors(element, elementsMap, direction);
|
const predeccessors = getPredecessors(element, elementsMap, direction);
|
||||||
|
|
||||||
@@ -274,9 +278,9 @@ const addNewNode = (
|
|||||||
const bindingArrow = createBindingArrow(
|
const bindingArrow = createBindingArrow(
|
||||||
element,
|
element,
|
||||||
nextNode,
|
nextNode,
|
||||||
elementsMap,
|
|
||||||
direction,
|
direction,
|
||||||
appState,
|
appState,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -287,9 +291,9 @@ const addNewNode = (
|
|||||||
|
|
||||||
export const addNewNodes = (
|
export const addNewNodes = (
|
||||||
startNode: ExcalidrawFlowchartNodeElement,
|
startNode: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
numberOfNodes: number,
|
numberOfNodes: number,
|
||||||
) => {
|
) => {
|
||||||
// always start from 0 and distribute evenly
|
// always start from 0 and distribute evenly
|
||||||
@@ -352,9 +356,9 @@ export const addNewNodes = (
|
|||||||
const bindingArrow = createBindingArrow(
|
const bindingArrow = createBindingArrow(
|
||||||
startNode,
|
startNode,
|
||||||
nextNode,
|
nextNode,
|
||||||
elementsMap,
|
|
||||||
direction,
|
direction,
|
||||||
appState,
|
appState,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
newNodes.push(nextNode);
|
newNodes.push(nextNode);
|
||||||
@@ -367,9 +371,9 @@ export const addNewNodes = (
|
|||||||
const createBindingArrow = (
|
const createBindingArrow = (
|
||||||
startBindingElement: ExcalidrawFlowchartNodeElement,
|
startBindingElement: ExcalidrawFlowchartNodeElement,
|
||||||
endBindingElement: ExcalidrawFlowchartNodeElement,
|
endBindingElement: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
let startX: number;
|
let startX: number;
|
||||||
let startY: number;
|
let startY: number;
|
||||||
@@ -440,18 +444,10 @@ const createBindingArrow = (
|
|||||||
elbowed: true,
|
elbowed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
bindLinearElement(
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
bindingArrow,
|
|
||||||
startBindingElement,
|
bindLinearElement(bindingArrow, startBindingElement, "start", scene);
|
||||||
"start",
|
bindLinearElement(bindingArrow, endBindingElement, "end", scene);
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
bindLinearElement(
|
|
||||||
bindingArrow,
|
|
||||||
endBindingElement,
|
|
||||||
"end",
|
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
||||||
changedElements.set(
|
changedElements.set(
|
||||||
@@ -467,12 +463,18 @@ const createBindingArrow = (
|
|||||||
bindingArrow as OrderedExcalidrawElement,
|
bindingArrow as OrderedExcalidrawElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(bindingArrow, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
bindingArrow,
|
||||||
index: 1,
|
scene,
|
||||||
point: bindingArrow.points[1],
|
new Map([
|
||||||
},
|
[
|
||||||
]);
|
1,
|
||||||
|
{
|
||||||
|
point: bindingArrow.points[1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const update = updateElbowArrowPoints(
|
const update = updateElbowArrowPoints(
|
||||||
bindingArrow,
|
bindingArrow,
|
||||||
@@ -632,16 +634,17 @@ export class FlowChartCreator {
|
|||||||
|
|
||||||
createNodes(
|
createNodes(
|
||||||
startNode: ExcalidrawFlowchartNodeElement,
|
startNode: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
) {
|
) {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
if (direction !== this.direction) {
|
if (direction !== this.direction) {
|
||||||
const { nextNode, bindingArrow } = addNewNode(
|
const { nextNode, bindingArrow } = addNewNode(
|
||||||
startNode,
|
startNode,
|
||||||
elementsMap,
|
|
||||||
appState,
|
appState,
|
||||||
direction,
|
direction,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.numberOfNodes = 1;
|
this.numberOfNodes = 1;
|
||||||
@@ -652,9 +655,9 @@ export class FlowChartCreator {
|
|||||||
this.numberOfNodes += 1;
|
this.numberOfNodes += 1;
|
||||||
const newNodes = addNewNodes(
|
const newNodes = addNewNodes(
|
||||||
startNode,
|
startNode,
|
||||||
elementsMap,
|
|
||||||
appState,
|
appState,
|
||||||
direction,
|
direction,
|
||||||
|
scene,
|
||||||
this.numberOfNodes,
|
this.numberOfNodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -682,13 +685,9 @@ export class FlowChartCreator {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.pendingNodes = this.pendingNodes.map((node) =>
|
this.pendingNodes = this.pendingNodes.map((node) =>
|
||||||
mutateElement(
|
mutateElement(node, elementsMap, {
|
||||||
node,
|
frameId: startNode.frameId,
|
||||||
{
|
}),
|
||||||
frameId: startNode.frameId,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { generateNKeysBetween } from "fractional-indexing";
|
|||||||
|
|
||||||
import { arrayToMap } from "@excalidraw/common";
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement, newElementWith } from "./mutateElement";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { hasBoundTextElement } from "./typeChecks";
|
import { hasBoundTextElement } from "./typeChecks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export class InvalidFractionalIndexError extends Error {
|
export class InvalidFractionalIndexError extends Error {
|
||||||
@@ -152,16 +154,23 @@ export const orderByFractionalIndex = (
|
|||||||
*/
|
*/
|
||||||
export const syncMovedIndices = (
|
export const syncMovedIndices = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement>,
|
movedElements: ElementsMap,
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
try {
|
try {
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
const indicesGroups = getMovedIndicesGroups(elements, movedElements);
|
const indicesGroups = getMovedIndicesGroups(elements, movedElements);
|
||||||
|
|
||||||
// try generatating indices, throws on invalid movedElements
|
// try generatating indices, throws on invalid movedElements
|
||||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
const elementsCandidates = elements.map((x) =>
|
const elementsCandidates = elements.map((x) => {
|
||||||
elementsUpdates.has(x) ? { ...x, ...elementsUpdates.get(x) } : x,
|
const elementUpdates = elementsUpdates.get(x);
|
||||||
);
|
|
||||||
|
if (elementUpdates) {
|
||||||
|
return { ...x, index: elementUpdates.index };
|
||||||
|
}
|
||||||
|
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
|
||||||
// ensure next indices are valid before mutation, throws on invalid ones
|
// ensure next indices are valid before mutation, throws on invalid ones
|
||||||
validateFractionalIndices(
|
validateFractionalIndices(
|
||||||
@@ -175,8 +184,8 @@ export const syncMovedIndices = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// split mutation so we don't end up in an incosistent state
|
// split mutation so we don't end up in an incosistent state
|
||||||
for (const [element, update] of elementsUpdates) {
|
for (const [element, { index }] of elementsUpdates) {
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, elementsMap, { index });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// fallback to default sync
|
// fallback to default sync
|
||||||
@@ -187,22 +196,43 @@ export const syncMovedIndices = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronizes all invalid fractional indices with the array order by mutating passed elements.
|
* Synchronizes all invalid fractional indices within the array order by mutating elements in the passed array.
|
||||||
*
|
*
|
||||||
* WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself.
|
* WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself.
|
||||||
*/
|
*/
|
||||||
export const syncInvalidIndices = (
|
export const syncInvalidIndices = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
const indicesGroups = getInvalidIndicesGroups(elements);
|
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
for (const [element, update] of elementsUpdates) {
|
|
||||||
mutateElement(element, update, false);
|
for (const [element, { index }] of elementsUpdates) {
|
||||||
|
mutateElement(element, elementsMap, { index });
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements as OrderedExcalidrawElement[];
|
return elements as OrderedExcalidrawElement[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes all invalid fractional indices within the array order by creating new instances of elements with corrected indices.
|
||||||
|
*
|
||||||
|
* WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself.
|
||||||
|
*/
|
||||||
|
export const syncInvalidIndicesImmutable = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
): SceneElementsMap | undefined => {
|
||||||
|
const syncedElements = arrayToMap(elements);
|
||||||
|
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||||
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
|
|
||||||
|
for (const [element, { index }] of elementsUpdates) {
|
||||||
|
syncedElements.set(element.id, newElementWith(element, { index }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncedElements as SceneElementsMap;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get contiguous groups of indices of passed moved elements.
|
* Get contiguous groups of indices of passed moved elements.
|
||||||
*
|
*
|
||||||
@@ -210,7 +240,7 @@ export const syncInvalidIndices = (
|
|||||||
*/
|
*/
|
||||||
const getMovedIndicesGroups = (
|
const getMovedIndicesGroups = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement>,
|
movedElements: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
const indicesGroups: number[][] = [];
|
const indicesGroups: number[][] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
|
|||||||
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
||||||
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
||||||
|
|
||||||
import type { ExcalidrawElementsIncludingDeleted } from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppState,
|
AppState,
|
||||||
@@ -29,6 +27,8 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type { ExcalidrawElementsIncludingDeleted } from "./Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
@@ -56,13 +56,9 @@ export const bindElementsToFramesAfterDuplication = (
|
|||||||
const nextFrameId = origIdToDuplicateId.get(element.frameId);
|
const nextFrameId = origIdToDuplicateId.get(element.frameId);
|
||||||
const nextElement = nextElementId && nextElementMap.get(nextElementId);
|
const nextElement = nextElementId && nextElementMap.get(nextElementId);
|
||||||
if (nextElement) {
|
if (nextElement) {
|
||||||
mutateElement(
|
mutateElement(nextElement, nextElementMap, {
|
||||||
nextElement,
|
frameId: nextFrameId ?? null,
|
||||||
{
|
});
|
||||||
frameId: nextFrameId ?? null,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -565,13 +561,9 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const element of finalElementsToAdd) {
|
for (const element of finalElementsToAdd) {
|
||||||
mutateElement(
|
mutateElement(element, elementsMap, {
|
||||||
element,
|
frameId: frame.id,
|
||||||
{
|
});
|
||||||
frameId: frame.id,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allElements;
|
return allElements;
|
||||||
@@ -609,13 +601,9 @@ export const removeElementsFromFrame = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [, element] of _elementsToRemove) {
|
for (const [, element] of _elementsToRemove) {
|
||||||
mutateElement(
|
mutateElement(element, elementsMap, {
|
||||||
element,
|
frameId: null,
|
||||||
{
|
});
|
||||||
frameId: null,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -917,13 +905,16 @@ export const shouldApplyFrameClip = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFrameLikeTitle = (element: ExcalidrawFrameLikeElement) => {
|
const DEFAULT_FRAME_NAME = "Frame";
|
||||||
|
const DEFAULT_AI_FRAME_NAME = "AI Frame";
|
||||||
|
|
||||||
|
export const getDefaultFrameName = (element: ExcalidrawFrameLikeElement) => {
|
||||||
// TODO name frames "AI" only if specific to AI frames
|
// TODO name frames "AI" only if specific to AI frames
|
||||||
return element.name === null
|
return isFrameElement(element) ? DEFAULT_FRAME_NAME : DEFAULT_AI_FRAME_NAME;
|
||||||
? isFrameElement(element)
|
};
|
||||||
? "Frame"
|
|
||||||
: "AI Frame"
|
export const getFrameLikeTitle = (element: ExcalidrawFrameLikeElement) => {
|
||||||
: element.name;
|
return element.name === null ? getDefaultFrameName(element) : element.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getElementsOverlappingFrame = (
|
export const getElementsOverlappingFrame = (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { toIterable } from "@excalidraw/common";
|
||||||
|
|
||||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||||
import { isLinearElementType } from "./typeChecks";
|
import { isLinearElementType } from "./typeChecks";
|
||||||
|
|
||||||
@@ -5,6 +7,7 @@ import type {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
ElementsMapOrArray,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,12 +19,10 @@ export const getSceneVersion = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
/**
|
/**
|
||||||
* Hashes elements' versionNonce (using djb2 algo). Order of elements matters.
|
* Hashes elements' versionNonce (using djb2 algo). Order of elements matters.
|
||||||
*/
|
*/
|
||||||
export const hashElementsVersion = (
|
export const hashElementsVersion = (elements: ElementsMapOrArray): number => {
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
): number => {
|
|
||||||
let hash = 5381;
|
let hash = 5381;
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (const element of toIterable(elements)) {
|
||||||
hash = (hash << 5) + hash + elements[i].versionNonce;
|
hash = (hash << 5) + hash + element.versionNonce;
|
||||||
}
|
}
|
||||||
return hash >>> 0; // Ensure unsigned 32-bit integer
|
return hash >>> 0; // Ensure unsigned 32-bit integer
|
||||||
};
|
};
|
||||||
@@ -71,3 +72,47 @@ export const clearElementsForExport = (
|
|||||||
export const clearElementsForLocalStorage = (
|
export const clearElementsForLocalStorage = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) => _clearElements(elements);
|
) => _clearElements(elements);
|
||||||
|
|
||||||
|
export * from "./align";
|
||||||
|
export * from "./binding";
|
||||||
|
export * from "./bounds";
|
||||||
|
export * from "./collision";
|
||||||
|
export * from "./comparisons";
|
||||||
|
export * from "./containerCache";
|
||||||
|
export * from "./cropElement";
|
||||||
|
export * from "./delta";
|
||||||
|
export * from "./distance";
|
||||||
|
export * from "./distribute";
|
||||||
|
export * from "./dragElements";
|
||||||
|
export * from "./duplicate";
|
||||||
|
export * from "./elbowArrow";
|
||||||
|
export * from "./elementLink";
|
||||||
|
export * from "./embeddable";
|
||||||
|
export * from "./flowchart";
|
||||||
|
export * from "./fractionalIndex";
|
||||||
|
export * from "./frame";
|
||||||
|
export * from "./groups";
|
||||||
|
export * from "./heading";
|
||||||
|
export * from "./image";
|
||||||
|
export * from "./linearElementEditor";
|
||||||
|
export * from "./mutateElement";
|
||||||
|
export * from "./newElement";
|
||||||
|
export * from "./renderElement";
|
||||||
|
export * from "./resizeElements";
|
||||||
|
export * from "./resizeTest";
|
||||||
|
export * from "./Scene";
|
||||||
|
export * from "./selection";
|
||||||
|
export * from "./Shape";
|
||||||
|
export * from "./ShapeCache";
|
||||||
|
export * from "./shapes";
|
||||||
|
export * from "./showSelectedShapeActions";
|
||||||
|
export * from "./sizeHelpers";
|
||||||
|
export * from "./sortElements";
|
||||||
|
export * from "./store";
|
||||||
|
export * from "./textElement";
|
||||||
|
export * from "./textMeasurements";
|
||||||
|
export * from "./textWrapping";
|
||||||
|
export * from "./transformHandles";
|
||||||
|
export * from "./typeChecks";
|
||||||
|
export * from "./utils";
|
||||||
|
export * from "./zindex";
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ import {
|
|||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
import type { Store } from "@excalidraw/element";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { Store } from "@excalidraw/excalidraw/store";
|
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
@@ -50,10 +46,8 @@ import {
|
|||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
|
||||||
|
|
||||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
@@ -69,10 +63,15 @@ import {
|
|||||||
getControlPointsForBezierCurve,
|
getControlPointsForBezierCurve,
|
||||||
mapIntervalToBezierT,
|
mapIntervalToBezierT,
|
||||||
getBezierXY,
|
getBezierXY,
|
||||||
|
toggleLinePolygonState,
|
||||||
} from "./shapes";
|
} from "./shapes";
|
||||||
|
|
||||||
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
||||||
|
|
||||||
|
import { isLineElement } from "./typeChecks";
|
||||||
|
|
||||||
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
@@ -84,16 +83,40 @@ import type {
|
|||||||
ElementsMap,
|
ElementsMap,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
SceneElementsMap,
|
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
|
PointsPositionUpdates,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
/**
|
||||||
version: number | null;
|
* Normalizes line points so that the start point is at [0,0]. This is
|
||||||
points: (GlobalPoint | null)[];
|
* expected in various parts of the codebase.
|
||||||
zoom: number | null;
|
*
|
||||||
} = { version: null, points: [], zoom: null };
|
* Also returns the offsets - [0,0] if no normalization needed.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const getNormalizedPoints = ({
|
||||||
|
points,
|
||||||
|
}: {
|
||||||
|
points: ExcalidrawLinearElement["points"];
|
||||||
|
}): {
|
||||||
|
points: LocalPoint[];
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
} => {
|
||||||
|
const offsetX = points[0][0];
|
||||||
|
const offsetY = points[0][1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
points: points.map((p) => {
|
||||||
|
return pointFrom(p[0] - offsetX, p[1] - offsetY);
|
||||||
|
}),
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export class LinearElementEditor {
|
export class LinearElementEditor {
|
||||||
public readonly elementId: ExcalidrawElement["id"] & {
|
public readonly elementId: ExcalidrawElement["id"] & {
|
||||||
_brand: "excalidrawLinearElementId";
|
_brand: "excalidrawLinearElementId";
|
||||||
@@ -126,16 +149,23 @@ export class LinearElementEditor {
|
|||||||
public readonly hoverPointIndex: number;
|
public readonly hoverPointIndex: number;
|
||||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||||
public readonly elbowed: boolean;
|
public readonly elbowed: boolean;
|
||||||
|
public readonly customLineAngle: number | null;
|
||||||
|
|
||||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
constructor(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) {
|
||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
_brand: "excalidrawLinearElementId";
|
_brand: "excalidrawLinearElementId";
|
||||||
};
|
};
|
||||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||||
console.error("Linear element is not normalized", Error().stack);
|
console.error("Linear element is not normalized", Error().stack);
|
||||||
LinearElementEditor.normalizePoints(element);
|
mutateElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
LinearElementEditor.getNormalizeElementPointsAndCoords(element),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedPointsIndices = null;
|
this.selectedPointsIndices = null;
|
||||||
this.lastUncommittedPoint = null;
|
this.lastUncommittedPoint = null;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
@@ -157,6 +187,7 @@ export class LinearElementEditor {
|
|||||||
this.hoverPointIndex = -1;
|
this.hoverPointIndex = -1;
|
||||||
this.segmentMidPointHoveredCoords = null;
|
this.segmentMidPointHoveredCoords = null;
|
||||||
this.elbowed = isElbowArrow(element) && element.elbowed;
|
this.elbowed = isElbowArrow(element) && element.elbowed;
|
||||||
|
this.customLineAngle = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -260,6 +291,7 @@ export class LinearElementEditor {
|
|||||||
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);
|
||||||
|
let customLineAngle = linearElementEditor.customLineAngle;
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -300,6 +332,12 @@ export class LinearElementEditor {
|
|||||||
const selectedIndex = selectedPointsIndices[0];
|
const selectedIndex = selectedPointsIndices[0];
|
||||||
const referencePoint =
|
const referencePoint =
|
||||||
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
|
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
|
||||||
|
customLineAngle =
|
||||||
|
linearElementEditor.customLineAngle ??
|
||||||
|
Math.atan2(
|
||||||
|
element.points[selectedIndex][1] - referencePoint[1],
|
||||||
|
element.points[selectedIndex][0] - referencePoint[0],
|
||||||
|
);
|
||||||
|
|
||||||
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
const [width, height] = LinearElementEditor._getShiftLockedDelta(
|
||||||
element,
|
element,
|
||||||
@@ -307,18 +345,25 @@ export class LinearElementEditor {
|
|||||||
referencePoint,
|
referencePoint,
|
||||||
pointFrom(scenePointerX, scenePointerY),
|
pointFrom(scenePointerX, scenePointerY),
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
|
customLineAngle,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
element,
|
||||||
index: selectedIndex,
|
scene,
|
||||||
point: pointFrom(
|
new Map([
|
||||||
width + referencePoint[0],
|
[
|
||||||
height + referencePoint[1],
|
selectedIndex,
|
||||||
),
|
{
|
||||||
isDragging: selectedIndex === lastClickedPoint,
|
point: pointFrom(
|
||||||
},
|
width + referencePoint[0],
|
||||||
]);
|
height + referencePoint[1],
|
||||||
|
),
|
||||||
|
isDragging: selectedIndex === lastClickedPoint,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
@@ -333,32 +378,39 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
element,
|
element,
|
||||||
selectedPointsIndices.map((pointIndex) => {
|
scene,
|
||||||
const newPointPosition: LocalPoint =
|
new Map(
|
||||||
pointIndex === lastClickedPoint
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
? LinearElementEditor.createPointAt(
|
const newPointPosition: LocalPoint =
|
||||||
element,
|
pointIndex === lastClickedPoint
|
||||||
elementsMap,
|
? LinearElementEditor.createPointAt(
|
||||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
element,
|
||||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
elementsMap,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||||
)
|
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||||
: pointFrom(
|
event[KEYS.CTRL_OR_CMD]
|
||||||
element.points[pointIndex][0] + deltaX,
|
? null
|
||||||
element.points[pointIndex][1] + deltaY,
|
: app.getEffectiveGridSize(),
|
||||||
);
|
)
|
||||||
return {
|
: pointFrom(
|
||||||
index: pointIndex,
|
element.points[pointIndex][0] + deltaX,
|
||||||
point: newPointPosition,
|
element.points[pointIndex][1] + deltaY,
|
||||||
isDragging: pointIndex === lastClickedPoint,
|
);
|
||||||
};
|
return [
|
||||||
}),
|
pointIndex,
|
||||||
|
{
|
||||||
|
point: newPointPosition,
|
||||||
|
isDragging: pointIndex === lastClickedPoint,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
handleBindTextResize(element, elementsMap, false);
|
handleBindTextResize(element, scene, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// suggest bindings for first and last point if selected
|
// suggest bindings for first and last point if selected
|
||||||
@@ -415,6 +467,7 @@ export class LinearElementEditor {
|
|||||||
? lastClickedPoint
|
? lastClickedPoint
|
||||||
: -1,
|
: -1,
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
|
customLineAngle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,15 +506,33 @@ export class LinearElementEditor {
|
|||||||
selectedPoint === element.points.length - 1
|
selectedPoint === element.points.length - 1
|
||||||
) {
|
) {
|
||||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||||
LinearElementEditor.movePoints(element, [
|
if (isLineElement(element)) {
|
||||||
{
|
scene.mutateElement(
|
||||||
index: selectedPoint,
|
element,
|
||||||
point:
|
{
|
||||||
selectedPoint === 0
|
...toggleLinePolygonState(element, true),
|
||||||
? element.points[element.points.length - 1]
|
},
|
||||||
: element.points[0],
|
{
|
||||||
},
|
informMutation: false,
|
||||||
]);
|
isDragging: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LinearElementEditor.movePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
selectedPoint,
|
||||||
|
{
|
||||||
|
point:
|
||||||
|
selectedPoint === 0
|
||||||
|
? element.points[element.points.length - 1]
|
||||||
|
: element.points[0],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindingElement = isBindingEnabled(appState)
|
const bindingElement = isBindingEnabled(appState)
|
||||||
@@ -491,6 +562,8 @@ export class LinearElementEditor {
|
|||||||
return {
|
return {
|
||||||
...editingLinearElement,
|
...editingLinearElement,
|
||||||
...bindings,
|
...bindings,
|
||||||
|
segmentMidPointHoveredCoords: null,
|
||||||
|
hoverPointIndex: -1,
|
||||||
// if clicking without previously dragging a point(s), and not holding
|
// if clicking without previously dragging a point(s), and not holding
|
||||||
// shift, deselect all points except the one clicked. If holding shift,
|
// shift, deselect all points except the one clicked. If holding shift,
|
||||||
// toggle the point.
|
// toggle the point.
|
||||||
@@ -512,6 +585,7 @@ export class LinearElementEditor {
|
|||||||
: selectedPointsIndices,
|
: selectedPointsIndices,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
pointerOffset: { x: 0, y: 0 },
|
pointerOffset: { x: 0, y: 0 },
|
||||||
|
customLineAngle: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +593,7 @@ export class LinearElementEditor {
|
|||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
): typeof editorMidPointsCache["points"] => {
|
): (GlobalPoint | null)[] => {
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
// Since its not needed outside editor unless 2 pointer lines or bound text
|
// Since its not needed outside editor unless 2 pointer lines or bound text
|
||||||
@@ -531,25 +605,7 @@ export class LinearElementEditor {
|
|||||||
) {
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
editorMidPointsCache.version === element.version &&
|
|
||||||
editorMidPointsCache.zoom === appState.zoom.value
|
|
||||||
) {
|
|
||||||
return editorMidPointsCache.points;
|
|
||||||
}
|
|
||||||
LinearElementEditor.updateEditorMidPointsCache(
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return editorMidPointsCache.points!;
|
|
||||||
};
|
|
||||||
|
|
||||||
static updateEditorMidPointsCache = (
|
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: InteractiveCanvasAppState,
|
|
||||||
) => {
|
|
||||||
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -581,9 +637,8 @@ export class LinearElementEditor {
|
|||||||
midpoints.push(segmentMidPoint);
|
midpoints.push(segmentMidPoint);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
editorMidPointsCache.points = midpoints;
|
|
||||||
editorMidPointsCache.version = element.version;
|
return midpoints;
|
||||||
editorMidPointsCache.zoom = appState.zoom.value;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static getSegmentMidpointHitCoords = (
|
static getSegmentMidpointHitCoords = (
|
||||||
@@ -637,8 +692,11 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const midPoints: typeof editorMidPointsCache["points"] =
|
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
element,
|
||||||
|
elementsMap,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
while (index < midPoints.length) {
|
while (index < midPoints.length) {
|
||||||
if (midPoints[index] !== null) {
|
if (midPoints[index] !== null) {
|
||||||
@@ -795,7 +853,7 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
} else if (event.altKey && appState.editingLinearElement) {
|
} else if (event.altKey && appState.editingLinearElement) {
|
||||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
points: [
|
points: [
|
||||||
...element.points,
|
...element.points,
|
||||||
LinearElementEditor.createPointAt(
|
LinearElementEditor.createPointAt(
|
||||||
@@ -809,7 +867,7 @@ export class LinearElementEditor {
|
|||||||
});
|
});
|
||||||
ret.didAddPoint = true;
|
ret.didAddPoint = true;
|
||||||
}
|
}
|
||||||
store.shouldCaptureIncrement();
|
store.scheduleCapture();
|
||||||
ret.linearElementEditor = {
|
ret.linearElementEditor = {
|
||||||
...linearElementEditor,
|
...linearElementEditor,
|
||||||
pointerDownState: {
|
pointerDownState: {
|
||||||
@@ -861,7 +919,6 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -934,13 +991,13 @@ export class LinearElementEditor {
|
|||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
): LinearElementEditor | null {
|
): LinearElementEditor | null {
|
||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
if (!appState.editingLinearElement) {
|
if (!appState.editingLinearElement) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return appState.editingLinearElement;
|
return appState.editingLinearElement;
|
||||||
@@ -951,7 +1008,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
if (!event.altKey) {
|
if (!event.altKey) {
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
LinearElementEditor.deletePoints(element, app, [points.length - 1]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
@@ -989,14 +1046,20 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
element,
|
||||||
index: element.points.length - 1,
|
app.scene,
|
||||||
point: newPoint,
|
new Map([
|
||||||
},
|
[
|
||||||
]);
|
element.points.length - 1,
|
||||||
|
{
|
||||||
|
point: newPoint,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
|
LinearElementEditor.addPoints(element, app.scene, [newPoint]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
@@ -1139,23 +1202,16 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes line points so that the start point is at [0,0]. This is
|
* Normalizes line points so that the start point is at [0,0]. This is
|
||||||
* expected in various parts of the codebase. Also returns new x/y to account
|
* expected in various parts of the codebase.
|
||||||
* for the potential normalization.
|
*
|
||||||
|
* Also returns normalized x and y coords to account for the normalization
|
||||||
|
* of the points.
|
||||||
*/
|
*/
|
||||||
static getNormalizedPoints(element: ExcalidrawLinearElement): {
|
static getNormalizeElementPointsAndCoords(element: ExcalidrawLinearElement) {
|
||||||
points: LocalPoint[];
|
const { points, offsetX, offsetY } = getNormalizedPoints(element);
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
} {
|
|
||||||
const { points } = element;
|
|
||||||
|
|
||||||
const offsetX = points[0][0];
|
|
||||||
const offsetY = points[0][1];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
points: points.map((p) => {
|
points,
|
||||||
return pointFrom(p[0] - offsetX, p[1] - offsetY);
|
|
||||||
}),
|
|
||||||
x: element.x + offsetX,
|
x: element.x + offsetX,
|
||||||
y: element.y + offsetY,
|
y: element.y + offsetY,
|
||||||
};
|
};
|
||||||
@@ -1163,20 +1219,13 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
// element-mutating methods
|
// element-mutating methods
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
|
||||||
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) {
|
|
||||||
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
|
||||||
}
|
|
||||||
|
|
||||||
static duplicateSelectedPoints(
|
|
||||||
appState: AppState,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
): AppState {
|
|
||||||
invariant(
|
invariant(
|
||||||
appState.editingLinearElement,
|
appState.editingLinearElement,
|
||||||
"Not currently editing a linear element",
|
"Not currently editing a linear element",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
|
|
||||||
@@ -1219,18 +1268,22 @@ export class LinearElementEditor {
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
mutateElement(element, { points: nextPoints });
|
scene.mutateElement(element, { points: nextPoints });
|
||||||
|
|
||||||
// temp hack to ensure the line doesn't move when adding point to the end,
|
// temp hack to ensure the line doesn't move when adding point to the end,
|
||||||
// potentially expanding the bounding box
|
// potentially expanding the bounding box
|
||||||
if (pointAddedToEnd) {
|
if (pointAddedToEnd) {
|
||||||
const lastPoint = element.points[element.points.length - 1];
|
const lastPoint = element.points[element.points.length - 1];
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
element,
|
||||||
index: element.points.length - 1,
|
scene,
|
||||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
new Map([
|
||||||
},
|
[
|
||||||
]);
|
element.points.length - 1,
|
||||||
|
{ point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30) },
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1244,82 +1297,127 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static deletePoints(
|
static deletePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
app: AppClassProperties,
|
||||||
pointIndices: readonly number[],
|
pointIndices: readonly number[],
|
||||||
) {
|
) {
|
||||||
let offsetX = 0;
|
const isUncommittedPoint =
|
||||||
let offsetY = 0;
|
app.state.editingLinearElement?.lastUncommittedPoint ===
|
||||||
|
element.points[element.points.length - 1];
|
||||||
|
|
||||||
const isDeletingOriginPoint = pointIndices.includes(0);
|
const nextPoints = element.points.filter((_, idx) => {
|
||||||
|
return !pointIndices.includes(idx);
|
||||||
|
});
|
||||||
|
|
||||||
// if deleting first point, make the next to be [0,0] and recalculate
|
const isPolygon = isLineElement(element) && element.polygon;
|
||||||
// positions of the rest with respect to it
|
|
||||||
if (isDeletingOriginPoint) {
|
// keep polygon intact if deleting start/end point or uncommitted point
|
||||||
const firstNonDeletedPoint = element.points.find((point, idx) => {
|
if (
|
||||||
return !pointIndices.includes(idx);
|
isPolygon &&
|
||||||
});
|
(isUncommittedPoint ||
|
||||||
if (firstNonDeletedPoint) {
|
pointIndices.includes(0) ||
|
||||||
offsetX = firstNonDeletedPoint[0];
|
pointIndices.includes(element.points.length - 1))
|
||||||
offsetY = firstNonDeletedPoint[1];
|
) {
|
||||||
}
|
nextPoints[0] = pointFrom(
|
||||||
|
nextPoints[nextPoints.length - 1][0],
|
||||||
|
nextPoints[nextPoints.length - 1][1],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPoints = element.points.reduce((acc: LocalPoint[], p, idx) => {
|
const {
|
||||||
if (!pointIndices.includes(idx)) {
|
points: normalizedPoints,
|
||||||
acc.push(
|
offsetX,
|
||||||
!acc.length
|
offsetY,
|
||||||
? pointFrom(0, 0)
|
} = getNormalizedPoints({ points: nextPoints });
|
||||||
: pointFrom(p[0] - offsetX, p[1] - offsetY),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
LinearElementEditor._updatePoints(
|
||||||
|
element,
|
||||||
|
app.scene,
|
||||||
|
normalizedPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addPoints(
|
static addPoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
targetPoints: { point: LocalPoint }[],
|
scene: Scene,
|
||||||
|
addedPoints: LocalPoint[],
|
||||||
) {
|
) {
|
||||||
const offsetX = 0;
|
const nextPoints = [...element.points, ...addedPoints];
|
||||||
const offsetY = 0;
|
|
||||||
|
|
||||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
if (isLineElement(element) && element.polygon) {
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
nextPoints[0] = pointFrom(
|
||||||
|
nextPoints[nextPoints.length - 1][0],
|
||||||
|
nextPoints[nextPoints.length - 1][1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
points: normalizedPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
} = getNormalizedPoints({ points: nextPoints });
|
||||||
|
|
||||||
|
LinearElementEditor._updatePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
normalizedPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static movePoints(
|
static movePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
scene: Scene,
|
||||||
|
pointUpdates: PointsPositionUpdates,
|
||||||
otherUpdates?: {
|
otherUpdates?: {
|
||||||
startBinding?: PointBinding | null;
|
startBinding?: PointBinding | null;
|
||||||
endBinding?: PointBinding | null;
|
endBinding?: PointBinding | null;
|
||||||
},
|
},
|
||||||
sceneElementsMap?: NonDeletedSceneElementsMap,
|
|
||||||
) {
|
) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
|
// if polygon, move start and end points together
|
||||||
|
if (isLineElement(element) && element.polygon) {
|
||||||
|
const firstPointUpdate = pointUpdates.get(0);
|
||||||
|
const lastPointUpdate = pointUpdates.get(points.length - 1);
|
||||||
|
|
||||||
|
if (firstPointUpdate) {
|
||||||
|
pointUpdates.set(points.length - 1, {
|
||||||
|
point: pointFrom(
|
||||||
|
firstPointUpdate.point[0],
|
||||||
|
firstPointUpdate.point[1],
|
||||||
|
),
|
||||||
|
isDragging: firstPointUpdate.isDragging,
|
||||||
|
});
|
||||||
|
} else if (lastPointUpdate) {
|
||||||
|
pointUpdates.set(0, {
|
||||||
|
point: pointFrom(lastPointUpdate.point[0], lastPointUpdate.point[1]),
|
||||||
|
isDragging: lastPointUpdate.isDragging,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// in case we're moving start point, instead of modifying its position
|
// in case we're moving start point, instead of modifying its position
|
||||||
// which would break the invariant of it being at [0,0], we move
|
// which would break the invariant of it being at [0,0], we move
|
||||||
// all the other points in the opposite direction by delta to
|
// all the other points in the opposite direction by delta to
|
||||||
// offset it. We do the same with actual element.x/y position, so
|
// offset it. We do the same with actual element.x/y position, so
|
||||||
// this hacks are completely transparent to the user.
|
// this hacks are completely transparent to the user.
|
||||||
const [deltaX, deltaY] =
|
|
||||||
targetPoints.find(({ index }) => index === 0)?.point ??
|
const updatedOriginPoint =
|
||||||
pointFrom<LocalPoint>(0, 0);
|
pointUpdates.get(0)?.point ?? pointFrom<LocalPoint>(0, 0);
|
||||||
const [offsetX, offsetY] = pointFrom<LocalPoint>(
|
|
||||||
deltaX - points[0][0],
|
const [offsetX, offsetY] = updatedOriginPoint;
|
||||||
deltaY - points[0][1],
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextPoints = isElbowArrow(element)
|
const nextPoints = isElbowArrow(element)
|
||||||
? [
|
? [
|
||||||
targetPoints.find((t) => t.index === 0)?.point ?? points[0],
|
pointUpdates.get(0)?.point ?? points[0],
|
||||||
targetPoints.find((t) => t.index === points.length - 1)?.point ??
|
pointUpdates.get(points.length - 1)?.point ??
|
||||||
points[points.length - 1],
|
points[points.length - 1],
|
||||||
]
|
]
|
||||||
: points.map((p, idx) => {
|
: points.map((p, idx) => {
|
||||||
const current = targetPoints.find((t) => t.index === idx)?.point ?? p;
|
const current = pointUpdates.get(idx)?.point ?? p;
|
||||||
|
|
||||||
return pointFrom<LocalPoint>(
|
return pointFrom<LocalPoint>(
|
||||||
current[0] - offsetX,
|
current[0] - offsetX,
|
||||||
@@ -1329,17 +1427,13 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
LinearElementEditor._updatePoints(
|
LinearElementEditor._updatePoints(
|
||||||
element,
|
element,
|
||||||
|
scene,
|
||||||
nextPoints,
|
nextPoints,
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
otherUpdates,
|
otherUpdates,
|
||||||
{
|
{
|
||||||
isDragging: targetPoints.reduce(
|
isDragging: Array.from(pointUpdates.values()).some((t) => t.isDragging),
|
||||||
(dragging, targetPoint): boolean =>
|
|
||||||
dragging || targetPoint.isDragging === true,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
sceneElementsMap,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1394,8 +1488,9 @@ export class LinearElementEditor {
|
|||||||
pointerCoords: PointerCoords,
|
pointerCoords: PointerCoords,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
snapToGrid: boolean,
|
snapToGrid: boolean,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
) {
|
) {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(
|
const element = LinearElementEditor.getElement(
|
||||||
linearElementEditor.elementId,
|
linearElementEditor.elementId,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -1425,9 +1520,7 @@ export class LinearElementEditor {
|
|||||||
...element.points.slice(segmentMidpoint.index!),
|
...element.points.slice(segmentMidpoint.index!),
|
||||||
];
|
];
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, { points });
|
||||||
points,
|
|
||||||
});
|
|
||||||
|
|
||||||
ret.pointerDownState = {
|
ret.pointerDownState = {
|
||||||
...linearElementEditor.pointerDownState,
|
...linearElementEditor.pointerDownState,
|
||||||
@@ -1443,6 +1536,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
private static _updatePoints(
|
private static _updatePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
nextPoints: readonly LocalPoint[],
|
nextPoints: readonly LocalPoint[],
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
@@ -1479,29 +1573,12 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
updates.points = Array.from(nextPoints);
|
updates.points = Array.from(nextPoints);
|
||||||
|
|
||||||
if (!options?.sceneElementsMap || Scene.getScene(element)) {
|
scene.mutateElement(element, updates, {
|
||||||
mutateElement(element, updates, true, {
|
informMutation: true,
|
||||||
isDragging: options?.isDragging,
|
isDragging: options?.isDragging ?? false,
|
||||||
});
|
});
|
||||||
} 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 {
|
||||||
|
// TODO do we need to get precise coords here just to calc centers?
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
const prevCoords = getElementPointsCoords(element, element.points);
|
const prevCoords = getElementPointsCoords(element, element.points);
|
||||||
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
|
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
|
||||||
@@ -1510,16 +1587,16 @@ export class LinearElementEditor {
|
|||||||
const prevCenterY = (prevCoords[1] + prevCoords[3]) / 2;
|
const prevCenterY = (prevCoords[1] + prevCoords[3]) / 2;
|
||||||
const dX = prevCenterX - nextCenterX;
|
const dX = prevCenterX - nextCenterX;
|
||||||
const dY = prevCenterY - nextCenterY;
|
const dY = prevCenterY - nextCenterY;
|
||||||
const rotated = pointRotateRads(
|
const rotatedOffset = pointRotateRads(
|
||||||
pointFrom(offsetX, offsetY),
|
pointFrom(offsetX, offsetY),
|
||||||
pointFrom(dX, dY),
|
pointFrom(dX, dY),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
...otherUpdates,
|
...otherUpdates,
|
||||||
points: nextPoints,
|
points: nextPoints,
|
||||||
x: element.x + rotated[0],
|
x: element.x + rotatedOffset[0],
|
||||||
y: element.y + rotated[1],
|
y: element.y + rotatedOffset[1],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1530,6 +1607,7 @@ export class LinearElementEditor {
|
|||||||
referencePoint: LocalPoint,
|
referencePoint: LocalPoint,
|
||||||
scenePointer: GlobalPoint,
|
scenePointer: GlobalPoint,
|
||||||
gridSize: NullableGridSize,
|
gridSize: NullableGridSize,
|
||||||
|
customLineAngle?: number,
|
||||||
) {
|
) {
|
||||||
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
|
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
|
||||||
element,
|
element,
|
||||||
@@ -1555,6 +1633,7 @@ export class LinearElementEditor {
|
|||||||
referencePointCoords[1],
|
referencePointCoords[1],
|
||||||
gridX,
|
gridX,
|
||||||
gridY,
|
gridY,
|
||||||
|
customLineAngle,
|
||||||
);
|
);
|
||||||
|
|
||||||
return pointRotateRads(
|
return pointRotateRads(
|
||||||
@@ -1574,7 +1653,7 @@ export class LinearElementEditor {
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
mutateElement(boundTextElement, { isDeleted: true });
|
mutateElement(boundTextElement, elementsMap, { isDeleted: true });
|
||||||
}
|
}
|
||||||
let x = 0;
|
let x = 0;
|
||||||
let y = 0;
|
let y = 0;
|
||||||
@@ -1589,23 +1668,14 @@ export class LinearElementEditor {
|
|||||||
y = midPoint[1] - boundTextElement.height / 2;
|
y = midPoint[1] - boundTextElement.height / 2;
|
||||||
} else {
|
} else {
|
||||||
const index = element.points.length / 2 - 1;
|
const index = element.points.length / 2 - 1;
|
||||||
|
const midSegmentMidpoint = LinearElementEditor.getSegmentMidPoint(
|
||||||
|
element,
|
||||||
|
points[index],
|
||||||
|
points[index + 1],
|
||||||
|
index + 1,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
let midSegmentMidpoint = editorMidPointsCache.points[index];
|
|
||||||
if (element.points.length === 2) {
|
|
||||||
midSegmentMidpoint = pointCenter(points[0], points[1]);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!midSegmentMidpoint ||
|
|
||||||
editorMidPointsCache.version !== element.version
|
|
||||||
) {
|
|
||||||
midSegmentMidpoint = LinearElementEditor.getSegmentMidPoint(
|
|
||||||
element,
|
|
||||||
points[index],
|
|
||||||
points[index + 1],
|
|
||||||
index + 1,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
x = midSegmentMidpoint[0] - boundTextElement.width / 2;
|
x = midSegmentMidpoint[0] - boundTextElement.width / 2;
|
||||||
y = midSegmentMidpoint[1] - boundTextElement.height / 2;
|
y = midSegmentMidpoint[1] - boundTextElement.height / 2;
|
||||||
}
|
}
|
||||||
@@ -1781,8 +1851,9 @@ export class LinearElementEditor {
|
|||||||
index: number,
|
index: number,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
): LinearElementEditor {
|
): LinearElementEditor {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(
|
const element = LinearElementEditor.getElement(
|
||||||
linearElement.elementId,
|
linearElement.elementId,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -1825,7 +1896,7 @@ export class LinearElementEditor {
|
|||||||
.map((segment) => segment.index)
|
.map((segment) => segment.index)
|
||||||
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fixedSegments: nextFixedSegments,
|
fixedSegments: nextFixedSegments,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1859,14 +1930,14 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static deleteFixedSegment(
|
static deleteFixedSegment(
|
||||||
element: ExcalidrawElbowArrowElement,
|
element: ExcalidrawElbowArrowElement,
|
||||||
|
scene: Scene,
|
||||||
index: number,
|
index: number,
|
||||||
): void {
|
): void {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fixedSegments: element.fixedSegments?.filter(
|
fixedSegments: element.fixedSegments?.filter(
|
||||||
(segment) => segment.index !== index,
|
(segment) => segment.index !== index,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
mutateElement(element, {}, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,8 @@ import {
|
|||||||
getSizeFromPoints,
|
getSizeFromPoints,
|
||||||
randomInteger,
|
randomInteger,
|
||||||
getUpdatedTimestamp,
|
getUpdatedTimestamp,
|
||||||
toBrandedType,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
@@ -16,35 +11,42 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
|||||||
import { ShapeCache } from "./ShapeCache";
|
import { ShapeCache } from "./ShapeCache";
|
||||||
|
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
import { isElbowArrow } from "./typeChecks";
|
import { isElbowArrow } from "./typeChecks";
|
||||||
|
|
||||||
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types";
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
|
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" | "updated"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// This function tracks updates of text elements for the purposes for collaboration.
|
/**
|
||||||
// The version is used to compare updates when more than one user is working in
|
* This function tracks updates of text elements for the purposes for collaboration.
|
||||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
* The version is used to compare updates when more than one user is working in
|
||||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
* the same drawing.
|
||||||
|
*
|
||||||
|
* WARNING: this won't trigger the component to update, so if you need to trigger component update,
|
||||||
|
* use `scene.mutateElement` or `ExcalidrawImperativeAPI.mutateElement` instead.
|
||||||
|
*/
|
||||||
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
informMutation = true,
|
|
||||||
options?: {
|
options?: {
|
||||||
// Currently only for elbow arrows.
|
|
||||||
// If true, the elbow arrow tries to bind to the nearest element. If false
|
|
||||||
// it tries to keep the same bound element, if any.
|
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
},
|
},
|
||||||
): TElement => {
|
) => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
const { points, fixedSegments, fileId, startBinding, endBinding } =
|
const { points, fixedSegments, startBinding, endBinding, fileId } =
|
||||||
updates as any;
|
updates as any;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -55,10 +57,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
typeof startBinding !== "undefined" ||
|
typeof startBinding !== "undefined" ||
|
||||||
typeof endBinding !== "undefined") // manual binding to element
|
typeof endBinding !== "undefined") // manual binding to element
|
||||||
) {
|
) {
|
||||||
const elementsMap = toBrandedType<NonDeletedSceneElementsMap>(
|
|
||||||
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
|
||||||
);
|
|
||||||
|
|
||||||
updates = {
|
updates = {
|
||||||
...updates,
|
...updates,
|
||||||
angle: 0 as Radians,
|
angle: 0 as Radians,
|
||||||
@@ -68,16 +66,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
x: updates.x || element.x,
|
x: updates.x || element.x,
|
||||||
y: updates.y || element.y,
|
y: updates.y || element.y,
|
||||||
},
|
},
|
||||||
elementsMap,
|
elementsMap as NonDeletedSceneElementsMap,
|
||||||
{
|
updates as ElementUpdate<ExcalidrawElbowArrowElement>,
|
||||||
fixedSegments,
|
options,
|
||||||
points,
|
|
||||||
startBinding,
|
|
||||||
endBinding,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isDragging: options?.isDragging,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (typeof points !== "undefined") {
|
} else if (typeof points !== "undefined") {
|
||||||
@@ -146,14 +137,10 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
element.version++;
|
element.version = updates.version ?? element.version + 1;
|
||||||
element.versionNonce = randomInteger();
|
element.versionNonce = updates.versionNonce ?? randomInteger();
|
||||||
element.updated = getUpdatedTimestamp();
|
element.updated = getUpdatedTimestamp();
|
||||||
|
|
||||||
if (informMutation) {
|
|
||||||
Scene.getScene(element)?.triggerUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,9 +172,9 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||||||
return {
|
return {
|
||||||
...element,
|
...element,
|
||||||
...updates,
|
...updates,
|
||||||
|
version: updates.version ?? element.version + 1,
|
||||||
|
versionNonce: updates.versionNonce ?? randomInteger(),
|
||||||
updated: getUpdatedTimestamp(),
|
updated: getUpdatedTimestamp(),
|
||||||
version: element.version + 1,
|
|
||||||
versionNonce: randomInteger(),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { getBoundTextMaxWidth } from "./textElement";
|
|||||||
import { normalizeText, measureText } from "./textMeasurements";
|
import { normalizeText, measureText } from "./textMeasurements";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
|
|
||||||
|
import { isLineElement } from "./typeChecks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
@@ -44,8 +46,8 @@ import type {
|
|||||||
ExcalidrawIframeElement,
|
ExcalidrawIframeElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
FixedSegment,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
|
ExcalidrawLineElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
@@ -458,9 +460,10 @@ export const newLinearElement = (
|
|||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawLinearElement["type"];
|
type: ExcalidrawLinearElement["type"];
|
||||||
points?: ExcalidrawLinearElement["points"];
|
points?: ExcalidrawLinearElement["points"];
|
||||||
|
polygon?: ExcalidrawLineElement["polygon"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawLinearElement> => {
|
): NonDeleted<ExcalidrawLinearElement> => {
|
||||||
return {
|
const element = {
|
||||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||||
points: opts.points || [],
|
points: opts.points || [],
|
||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
@@ -469,6 +472,17 @@ export const newLinearElement = (
|
|||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLineElement(element)) {
|
||||||
|
const lineElement: NonDeleted<ExcalidrawLineElement> = {
|
||||||
|
...element,
|
||||||
|
polygon: opts.polygon ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return lineElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const newArrowElement = <T extends boolean>(
|
export const newArrowElement = <T extends boolean>(
|
||||||
@@ -478,7 +492,7 @@ export const newArrowElement = <T extends boolean>(
|
|||||||
endArrowhead?: Arrowhead | null;
|
endArrowhead?: Arrowhead | null;
|
||||||
points?: ExcalidrawArrowElement["points"];
|
points?: ExcalidrawArrowElement["points"];
|
||||||
elbowed?: T;
|
elbowed?: T;
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"] | null;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): T extends true
|
): T extends true
|
||||||
? NonDeleted<ExcalidrawElbowArrowElement>
|
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||||
|
|||||||
@@ -351,12 +351,20 @@ const generateElementCanvas = (
|
|||||||
|
|
||||||
export const DEFAULT_LINK_SIZE = 14;
|
export const DEFAULT_LINK_SIZE = 14;
|
||||||
|
|
||||||
const IMAGE_PLACEHOLDER_IMG = document.createElement("img");
|
const IMAGE_PLACEHOLDER_IMG =
|
||||||
|
typeof document !== "undefined"
|
||||||
|
? document.createElement("img")
|
||||||
|
: ({ src: "" } as HTMLImageElement); // mock image element outside of browser
|
||||||
|
|
||||||
IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||||
`<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="image" class="svg-inline--fa fa-image fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#888" d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"></path></svg>`,
|
`<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="image" class="svg-inline--fa fa-image fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#888" d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"></path></svg>`,
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
const IMAGE_ERROR_PLACEHOLDER_IMG = document.createElement("img");
|
const IMAGE_ERROR_PLACEHOLDER_IMG =
|
||||||
|
typeof document !== "undefined"
|
||||||
|
? document.createElement("img")
|
||||||
|
: ({ src: "" } as HTMLImageElement); // mock image element outside of browser
|
||||||
|
|
||||||
IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||||
`<svg viewBox="0 0 668 668" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48ZM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56ZM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.81709 0 0 .81709 124.825 145.825)"/><path d="M256 8C119.034 8 8 119.033 8 256c0 136.967 111.034 248 248 248s248-111.034 248-248S392.967 8 256 8Zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676ZM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.30366 0 0 .30366 506.822 60.065)"/></svg>`,
|
`<svg viewBox="0 0 668 668" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><path d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48ZM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56ZM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.81709 0 0 .81709 124.825 145.825)"/><path d="M256 8C119.034 8 8 119.033 8 256c0 136.967 111.034 248 248 248s248-111.034 248-248S392.967 8 256 8Zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676ZM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676Z" style="fill:#888;fill-rule:nonzero" transform="matrix(.30366 0 0 .30366 506.822 60.065)"/></svg>`,
|
||||||
)}`;
|
)}`;
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import {
|
|||||||
|
|
||||||
import type { GlobalPoint } from "@excalidraw/math";
|
import type { GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
@@ -32,7 +30,6 @@ import {
|
|||||||
getElementBounds,
|
getElementBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
@@ -60,6 +57,8 @@ import {
|
|||||||
|
|
||||||
import { isInGroup } from "./groups";
|
import { isInGroup } from "./groups";
|
||||||
|
|
||||||
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { BoundingBox } from "./bounds";
|
import type { BoundingBox } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
@@ -74,7 +73,6 @@ import type {
|
|||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
SceneElementsMap,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -83,7 +81,6 @@ export const transformElements = (
|
|||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: SceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
@@ -93,31 +90,31 @@ export const transformElements = (
|
|||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
if (transformHandleType === "rotation") {
|
if (transformHandleType === "rotation") {
|
||||||
if (!isElbowArrow(element)) {
|
if (!isElbowArrow(element)) {
|
||||||
rotateSingleElement(
|
rotateSingleElement(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
);
|
);
|
||||||
updateBoundElements(element, elementsMap);
|
updateBoundElements(element, scene);
|
||||||
}
|
}
|
||||||
} else if (isTextElement(element) && transformHandleType) {
|
} else if (isTextElement(element) && transformHandleType) {
|
||||||
resizeSingleTextElement(
|
resizeSingleTextElement(
|
||||||
originalElements,
|
originalElements,
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
scene,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
);
|
);
|
||||||
updateBoundElements(element, elementsMap);
|
updateBoundElements(element, scene);
|
||||||
return true;
|
return true;
|
||||||
} else if (transformHandleType) {
|
} else if (transformHandleType) {
|
||||||
const elementId = selectedElements[0].id;
|
const elementId = selectedElements[0].id;
|
||||||
@@ -129,8 +126,6 @@ export const transformElements = (
|
|||||||
getNextSingleWidthAndHeightFromPointer(
|
getNextSingleWidthAndHeightFromPointer(
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElements,
|
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
@@ -145,8 +140,8 @@ export const transformElements = (
|
|||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElements,
|
originalElements,
|
||||||
|
scene,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
{
|
{
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
@@ -161,7 +156,6 @@ export const transformElements = (
|
|||||||
rotateMultipleElements(
|
rotateMultipleElements(
|
||||||
originalElements,
|
originalElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
@@ -210,13 +204,15 @@ export const transformElements = (
|
|||||||
|
|
||||||
const rotateSingleElement = (
|
const rotateSingleElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
let angle: Radians;
|
let angle: Radians;
|
||||||
@@ -233,13 +229,13 @@ const rotateSingleElement = (
|
|||||||
}
|
}
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
mutateElement(element, { angle });
|
scene.mutateElement(element, { angle });
|
||||||
if (boundTextElementId) {
|
if (boundTextElementId) {
|
||||||
const textElement =
|
const textElement =
|
||||||
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
||||||
|
|
||||||
if (textElement && !isArrowElement(element)) {
|
if (textElement && !isArrowElement(element)) {
|
||||||
mutateElement(textElement, { angle });
|
scene.mutateElement(textElement, { angle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -289,12 +285,13 @@ export const measureFontSizeFromWidth = (
|
|||||||
const resizeSingleTextElement = (
|
const resizeSingleTextElement = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
transformHandleType: TransformHandleDirection,
|
transformHandleType: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -393,7 +390,7 @@ const resizeSingleTextElement = (
|
|||||||
);
|
);
|
||||||
const [nextX, nextY] = newTopLeft;
|
const [nextX, nextY] = newTopLeft;
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fontSize: metrics.size,
|
fontSize: metrics.size,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
@@ -508,14 +505,13 @@ const resizeSingleTextElement = (
|
|||||||
autoResize: false,
|
autoResize: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(element, resizedElement);
|
scene.mutateElement(element, resizedElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateMultipleElements = (
|
const rotateMultipleElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: SceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
@@ -523,6 +519,7 @@ const rotateMultipleElements = (
|
|||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
let centerAngle =
|
let centerAngle =
|
||||||
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
||||||
if (shouldRotateWithDiscreteAngle) {
|
if (shouldRotateWithDiscreteAngle) {
|
||||||
@@ -543,38 +540,30 @@ const rotateMultipleElements = (
|
|||||||
(centerAngle + origAngle - element.angle) as Radians,
|
(centerAngle + origAngle - element.angle) as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
const updates = isElbowArrow(element)
|
||||||
// Needed to re-route the arrow
|
? {
|
||||||
mutateElement(element, {
|
// Needed to re-route the arrow
|
||||||
points: getArrowLocalFixedPoints(element, elementsMap),
|
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||||
});
|
}
|
||||||
} else {
|
: {
|
||||||
mutateElement(
|
|
||||||
element,
|
|
||||||
{
|
|
||||||
x: element.x + (rotatedCX - cx),
|
x: element.x + (rotatedCX - cx),
|
||||||
y: element.y + (rotatedCY - cy),
|
y: element.y + (rotatedCY - cy),
|
||||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||||
},
|
};
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
scene.mutateElement(element, updates);
|
||||||
|
|
||||||
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: elements,
|
simultaneouslyUpdated: elements,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !isArrowElement(element)) {
|
if (boundText && !isArrowElement(element)) {
|
||||||
mutateElement(
|
scene.mutateElement(boundText, {
|
||||||
boundText,
|
x: boundText.x + (rotatedCX - cx),
|
||||||
{
|
y: boundText.y + (rotatedCY - cy),
|
||||||
x: boundText.x + (rotatedCX - cx),
|
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||||
y: boundText.y + (rotatedCY - cy),
|
});
|
||||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -819,8 +808,8 @@ export const resizeSingleElement = (
|
|||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
handleDirection: TransformHandleDirection,
|
handleDirection: TransformHandleDirection,
|
||||||
{
|
{
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
@@ -833,6 +822,7 @@ export const resizeSingleElement = (
|
|||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
let boundTextFont: { fontSize?: number } = {};
|
let boundTextFont: { fontSize?: number } = {};
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
@@ -932,7 +922,7 @@ export const resizeSingleElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ("scale" in latestElement && "scale" in origElement) {
|
if ("scale" in latestElement && "scale" in origElement) {
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
scale: [
|
scale: [
|
||||||
// defaulting because scaleX/Y can be 0/-0
|
// defaulting because scaleX/Y can be 0/-0
|
||||||
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
|
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
|
||||||
@@ -967,32 +957,33 @@ export const resizeSingleElement = (
|
|||||||
...rescaledPoints,
|
...rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(latestElement, updates, shouldInformMutation);
|
scene.mutateElement(latestElement, updates, {
|
||||||
|
informMutation: shouldInformMutation,
|
||||||
updateBoundElements(latestElement, elementsMap as SceneElementsMap, {
|
isDragging: false,
|
||||||
// TODO: confirm with MARK if this actually makes sense
|
|
||||||
newSize: { width: nextWidth, height: nextHeight },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (boundTextElement && boundTextFont != null) {
|
if (boundTextElement && boundTextFont != null) {
|
||||||
mutateElement(boundTextElement, {
|
scene.mutateElement(boundTextElement, {
|
||||||
fontSize: boundTextFont.fontSize,
|
fontSize: boundTextFont.fontSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleBindTextResize(
|
handleBindTextResize(
|
||||||
latestElement,
|
latestElement,
|
||||||
elementsMap,
|
scene,
|
||||||
handleDirection,
|
handleDirection,
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
updateBoundElements(latestElement, scene, {
|
||||||
|
// TODO: confirm with MARK if this actually makes sense
|
||||||
|
newSize: { width: nextWidth, height: nextHeight },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNextSingleWidthAndHeightFromPointer = (
|
const getNextSingleWidthAndHeightFromPointer = (
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
|
||||||
handleDirection: TransformHandleDirection,
|
handleDirection: TransformHandleDirection,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
@@ -1527,27 +1518,24 @@ export const resizeMultipleElements = (
|
|||||||
} of elementsAndUpdates) {
|
} of elementsAndUpdates) {
|
||||||
const { width, height, angle } = update;
|
const { width, height, angle } = update;
|
||||||
|
|
||||||
mutateElement(element, update, false, {
|
scene.mutateElement(element, update, {
|
||||||
|
informMutation: true,
|
||||||
// needed for the fixed binding point udpate to take effect
|
// needed for the fixed binding point udpate to take effect
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: elementsToUpdate,
|
simultaneouslyUpdated: elementsToUpdate,
|
||||||
newSize: { width, height },
|
newSize: { width, height },
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement && boundTextFontSize) {
|
if (boundTextElement && boundTextFontSize) {
|
||||||
mutateElement(
|
scene.mutateElement(boundTextElement, {
|
||||||
boundTextElement,
|
fontSize: boundTextFontSize,
|
||||||
{
|
angle: isLinearElement(element) ? undefined : angle,
|
||||||
fontSize: boundTextFontSize,
|
});
|
||||||
angle: isLinearElement(element) ? undefined : angle,
|
handleBindTextResize(element, scene, handleDirection, true);
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
handleBindTextResize(element, elementsMap, handleDirection, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isShallowEqual } from "@excalidraw/common";
|
import { arrayToMap, isShallowEqual } from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
@@ -169,25 +169,6 @@ export const isSomeElementSelected = (function () {
|
|||||||
return ret;
|
return ret;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns common attribute (picked by `getAttribute` callback) of selected
|
|
||||||
* elements. If elements don't share the same value, returns `null`.
|
|
||||||
*/
|
|
||||||
export const getCommonAttributeOfSelectedElements = <T>(
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
appState: Pick<AppState, "selectedElementIds">,
|
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
|
||||||
): T | null => {
|
|
||||||
const attributes = Array.from(
|
|
||||||
new Set(
|
|
||||||
getSelectedElements(elements, appState).map((element) =>
|
|
||||||
getAttribute(element),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return attributes.length === 1 ? attributes[0] : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSelectedElements = (
|
export const getSelectedElements = (
|
||||||
elements: ElementsMapOrArray,
|
elements: ElementsMapOrArray,
|
||||||
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
||||||
@@ -264,6 +245,7 @@ export const makeNextSelectedElementIds = (
|
|||||||
|
|
||||||
const _getLinearElementEditor = (
|
const _getLinearElementEditor = (
|
||||||
targetElements: readonly ExcalidrawElement[],
|
targetElements: readonly ExcalidrawElement[],
|
||||||
|
allElements: readonly NonDeletedExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
const linears = targetElements.filter(isLinearElement);
|
const linears = targetElements.filter(isLinearElement);
|
||||||
if (linears.length === 1) {
|
if (linears.length === 1) {
|
||||||
@@ -274,7 +256,7 @@ const _getLinearElementEditor = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (onlySingleLinearSelected) {
|
if (onlySingleLinearSelected) {
|
||||||
return new LinearElementEditor(linear);
|
return new LinearElementEditor(linear, arrayToMap(allElements));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +269,7 @@ export const getSelectionStateForElements = (
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
selectedLinearElement: _getLinearElementEditor(targetElements),
|
selectedLinearElement: _getLinearElementEditor(targetElements, allElements),
|
||||||
...selectGroupsForSelectedElements(
|
...selectGroupsForSelectedElements(
|
||||||
{
|
{
|
||||||
editingGroupId: appState.editingGroupId,
|
editingGroupId: appState.editingGroupId,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
invariant,
|
invariant,
|
||||||
elementCenterPoint,
|
elementCenterPoint,
|
||||||
|
LINE_POLYGON_POINT_MERGE_DISTANCE,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
@@ -35,10 +36,13 @@ import { ShapeCache } from "./ShapeCache";
|
|||||||
|
|
||||||
import { getElementAbsoluteCoords, type Bounds } from "./bounds";
|
import { getElementAbsoluteCoords, type Bounds } from "./bounds";
|
||||||
|
|
||||||
|
import { canBecomePolygon } from "./typeChecks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawLineElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -287,6 +291,7 @@ export const mapIntervalToBezierT = <P extends GlobalPoint | LocalPoint>(
|
|||||||
*/
|
*/
|
||||||
export const aabbForElement = (
|
export const aabbForElement = (
|
||||||
element: Readonly<ExcalidrawElement>,
|
element: Readonly<ExcalidrawElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
offset?: [number, number, number, number],
|
offset?: [number, number, number, number],
|
||||||
) => {
|
) => {
|
||||||
const bbox = {
|
const bbox = {
|
||||||
@@ -298,7 +303,7 @@ export const aabbForElement = (
|
|||||||
midY: element.y + element.height / 2,
|
midY: element.y + element.height / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const center = elementCenterPoint(element);
|
const center = elementCenterPoint(element, elementsMap);
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(bbox.minX, bbox.minY),
|
pointFrom(bbox.minX, bbox.minY),
|
||||||
center,
|
center,
|
||||||
@@ -396,3 +401,47 @@ export const isPathALoop = (
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toggleLinePolygonState = (
|
||||||
|
element: ExcalidrawLineElement,
|
||||||
|
nextPolygonState: boolean,
|
||||||
|
): {
|
||||||
|
polygon: ExcalidrawLineElement["polygon"];
|
||||||
|
points: ExcalidrawLineElement["points"];
|
||||||
|
} | null => {
|
||||||
|
const updatedPoints = [...element.points];
|
||||||
|
|
||||||
|
if (nextPolygonState) {
|
||||||
|
if (!canBecomePolygon(element.points)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstPoint = updatedPoints[0];
|
||||||
|
const lastPoint = updatedPoints[updatedPoints.length - 1];
|
||||||
|
|
||||||
|
const distance = Math.hypot(
|
||||||
|
firstPoint[0] - lastPoint[0],
|
||||||
|
firstPoint[1] - lastPoint[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
distance > LINE_POLYGON_POINT_MERGE_DISTANCE ||
|
||||||
|
updatedPoints.length < 4
|
||||||
|
) {
|
||||||
|
updatedPoints.push(pointFrom(firstPoint[0], firstPoint[1]));
|
||||||
|
} else {
|
||||||
|
updatedPoints[updatedPoints.length - 1] = pointFrom(
|
||||||
|
firstPoint[0],
|
||||||
|
firstPoint[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: satisfies ElementUpdate<ExcalidrawLineElement>
|
||||||
|
const ret = {
|
||||||
|
polygon: nextPolygonState,
|
||||||
|
points: updatedPoints,
|
||||||
|
};
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,15 +2,28 @@ import {
|
|||||||
SHIFT_LOCKING_ANGLE,
|
SHIFT_LOCKING_ANGLE,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
import {
|
||||||
|
normalizeRadians,
|
||||||
|
radiansBetweenAngles,
|
||||||
|
radiansDifference,
|
||||||
|
type Radians,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { pointsEqual } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import {
|
||||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
isArrowElement,
|
||||||
|
isFreeDrawElement,
|
||||||
|
isLinearElement,
|
||||||
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
|
export const INVISIBLY_SMALL_ELEMENT_SIZE = 0.1;
|
||||||
|
|
||||||
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
|
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
|
||||||
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
|
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
|
||||||
// - could also be part of `_clearElements`
|
// - could also be part of `_clearElements`
|
||||||
@@ -18,8 +31,18 @@ export const isInvisiblySmallElement = (
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
return element.points.length < 2;
|
return (
|
||||||
|
element.points.length < 2 ||
|
||||||
|
(element.points.length === 2 &&
|
||||||
|
isArrowElement(element) &&
|
||||||
|
pointsEqual(
|
||||||
|
element.points[0],
|
||||||
|
element.points[element.points.length - 1],
|
||||||
|
INVISIBLY_SMALL_ELEMENT_SIZE,
|
||||||
|
))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return element.width === 0 && element.height === 0;
|
return element.width === 0 && element.height === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,13 +158,42 @@ export const getLockedLinearCursorAlignSize = (
|
|||||||
originY: number,
|
originY: number,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
customAngle?: number,
|
||||||
) => {
|
) => {
|
||||||
let width = x - originX;
|
let width = x - originX;
|
||||||
let height = y - originY;
|
let height = y - originY;
|
||||||
|
|
||||||
const lockedAngle =
|
const angle = Math.atan2(height, width) as Radians;
|
||||||
Math.round(Math.atan(height / width) / SHIFT_LOCKING_ANGLE) *
|
let lockedAngle = (Math.round(angle / SHIFT_LOCKING_ANGLE) *
|
||||||
SHIFT_LOCKING_ANGLE;
|
SHIFT_LOCKING_ANGLE) as Radians;
|
||||||
|
|
||||||
|
if (customAngle) {
|
||||||
|
// If custom angle is provided, we check if the angle is close to the
|
||||||
|
// custom angle, snap to that if close engough, otherwise snap to the
|
||||||
|
// higher or lower angle depending on the current angle vs custom angle.
|
||||||
|
const lower = (Math.floor(customAngle / SHIFT_LOCKING_ANGLE) *
|
||||||
|
SHIFT_LOCKING_ANGLE) as Radians;
|
||||||
|
if (
|
||||||
|
radiansBetweenAngles(
|
||||||
|
angle,
|
||||||
|
lower,
|
||||||
|
(lower + SHIFT_LOCKING_ANGLE) as Radians,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
radiansDifference(angle, customAngle as Radians) <
|
||||||
|
SHIFT_LOCKING_ANGLE / 6
|
||||||
|
) {
|
||||||
|
lockedAngle = customAngle as Radians;
|
||||||
|
} else if (
|
||||||
|
normalizeRadians(angle) > normalizeRadians(customAngle as Radians)
|
||||||
|
) {
|
||||||
|
lockedAngle = (lower + SHIFT_LOCKING_ANGLE) as Radians;
|
||||||
|
} else {
|
||||||
|
lockedAngle = lower;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (lockedAngle === 0) {
|
if (lockedAngle === 0) {
|
||||||
height = 0;
|
height = 0;
|
||||||
@@ -170,41 +222,6 @@ export const getLockedLinearCursorAlignSize = (
|
|||||||
return { width, height };
|
return { width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resizePerfectLineForNWHandler = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
) => {
|
|
||||||
const anchorX = element.x + element.width;
|
|
||||||
const anchorY = element.y + element.height;
|
|
||||||
const distanceToAnchorX = x - anchorX;
|
|
||||||
const distanceToAnchorY = y - anchorY;
|
|
||||||
if (Math.abs(distanceToAnchorX) < Math.abs(distanceToAnchorY) / 2) {
|
|
||||||
mutateElement(element, {
|
|
||||||
x: anchorX,
|
|
||||||
width: 0,
|
|
||||||
y,
|
|
||||||
height: -distanceToAnchorY,
|
|
||||||
});
|
|
||||||
} else if (Math.abs(distanceToAnchorY) < Math.abs(element.width) / 2) {
|
|
||||||
mutateElement(element, {
|
|
||||||
y: anchorY,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const nextHeight =
|
|
||||||
Math.sign(distanceToAnchorY) *
|
|
||||||
Math.sign(distanceToAnchorX) *
|
|
||||||
element.width;
|
|
||||||
mutateElement(element, {
|
|
||||||
x,
|
|
||||||
y: anchorY - nextHeight,
|
|
||||||
width: -distanceToAnchorX,
|
|
||||||
height: nextHeight,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNormalizedDimensions = (
|
export const getNormalizedDimensions = (
|
||||||
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
|
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
|
||||||
): {
|
): {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -14,12 +14,14 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
|||||||
|
|
||||||
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { measureText } from "./textMeasurements";
|
import { measureText } from "./textMeasurements";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import {
|
import {
|
||||||
@@ -28,7 +30,7 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { Radians } from "../../math/src";
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import type {
|
import type {
|
||||||
@@ -44,9 +46,10 @@ import type {
|
|||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
informMutation = true,
|
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
|
|
||||||
if (!isProdEnv()) {
|
if (!isProdEnv()) {
|
||||||
@@ -106,38 +109,43 @@ export const redrawTextBoundingBox = (
|
|||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: nextHeight }, informMutation);
|
scene.mutateElement(container, { height: nextHeight });
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metrics.width > maxContainerWidth) {
|
if (metrics.width > maxContainerWidth) {
|
||||||
const nextWidth = computeContainerDimensionForBoundText(
|
const nextWidth = computeContainerDimensionForBoundText(
|
||||||
metrics.width,
|
metrics.width,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { width: nextWidth }, informMutation);
|
scene.mutateElement(container, { width: nextWidth });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTextElement = {
|
const updatedTextElement = {
|
||||||
...textElement,
|
...textElement,
|
||||||
...boundTextUpdates,
|
...boundTextUpdates,
|
||||||
} as ExcalidrawTextElementWithContainer;
|
} as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
const { x, y } = computeBoundTextPosition(
|
const { x, y } = computeBoundTextPosition(
|
||||||
container,
|
container,
|
||||||
updatedTextElement,
|
updatedTextElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
boundTextUpdates.x = x;
|
boundTextUpdates.x = x;
|
||||||
boundTextUpdates.y = y;
|
boundTextUpdates.y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(textElement, boundTextUpdates, informMutation);
|
scene.mutateElement(textElement, boundTextUpdates);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
container: NonDeletedExcalidrawElement,
|
container: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
shouldMaintainAspectRatio = false,
|
shouldMaintainAspectRatio = false,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId) {
|
if (!boundTextElementId) {
|
||||||
return;
|
return;
|
||||||
@@ -190,20 +198,20 @@ export const handleBindTextResize = (
|
|||||||
transformHandleType === "n")
|
transformHandleType === "n")
|
||||||
? container.y - diff
|
? container.y - diff
|
||||||
: container.y;
|
: container.y;
|
||||||
mutateElement(container, {
|
scene.mutateElement(container, {
|
||||||
height: containerHeight,
|
height: containerHeight,
|
||||||
y: updatedY,
|
y: updatedY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(textElement, {
|
scene.mutateElement(textElement, {
|
||||||
text,
|
text,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
textElement,
|
textElement,
|
||||||
computeBoundTextPosition(container, textElement, elementsMap),
|
computeBoundTextPosition(container, textElement, elementsMap),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ROUNDNESS, assertNever } from "@excalidraw/common";
|
import { ROUNDNESS, assertNever } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { pointsEqual } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
|
import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { MarkNonNullable } from "@excalidraw/common/utility-types";
|
import type { MarkNonNullable } from "@excalidraw/common/utility-types";
|
||||||
@@ -25,9 +27,11 @@ import type {
|
|||||||
ExcalidrawMagicFrameElement,
|
ExcalidrawMagicFrameElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
|
ExcalidrawLineElement,
|
||||||
PointBinding,
|
PointBinding,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
ExcalidrawFlowchartNodeElement,
|
ExcalidrawFlowchartNodeElement,
|
||||||
|
ExcalidrawLinearElementSubType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isInitializedImageElement = (
|
export const isInitializedImageElement = (
|
||||||
@@ -107,6 +111,12 @@ export const isLinearElement = (
|
|||||||
return element != null && isLinearElementType(element.type);
|
return element != null && isLinearElementType(element.type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isLineElement = (
|
||||||
|
element?: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawLineElement => {
|
||||||
|
return element != null && element.type === "line";
|
||||||
|
};
|
||||||
|
|
||||||
export const isArrowElement = (
|
export const isArrowElement = (
|
||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawArrowElement => {
|
): element is ExcalidrawArrowElement => {
|
||||||
@@ -119,6 +129,29 @@ export const isElbowArrow = (
|
|||||||
return isArrowElement(element) && element.elbowed;
|
return isArrowElement(element) && element.elbowed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sharp or curved arrow, but not elbow
|
||||||
|
*/
|
||||||
|
export const isSimpleArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return isArrowElement(element) && !element.elbowed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSharpArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return isArrowElement(element) && !element.elbowed && !element.roundness;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCurvedArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return (
|
||||||
|
isArrowElement(element) && !element.elbowed && element.roundness !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isLinearElementType = (
|
export const isLinearElementType = (
|
||||||
elementType: ElementOrToolType,
|
elementType: ElementOrToolType,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
@@ -271,6 +304,10 @@ export const isBoundToContainer = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isArrowBoundToElement = (element: ExcalidrawArrowElement) => {
|
||||||
|
return !!element.startBinding || !!element.endBinding;
|
||||||
|
};
|
||||||
|
|
||||||
export const isUsingAdaptiveRadius = (type: string) =>
|
export const isUsingAdaptiveRadius = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "embeddable" ||
|
type === "embeddable" ||
|
||||||
@@ -338,3 +375,41 @@ export const isBounds = (box: unknown): box is Bounds =>
|
|||||||
typeof box[1] === "number" &&
|
typeof box[1] === "number" &&
|
||||||
typeof box[2] === "number" &&
|
typeof box[2] === "number" &&
|
||||||
typeof box[3] === "number";
|
typeof box[3] === "number";
|
||||||
|
|
||||||
|
export const getLinearElementSubType = (
|
||||||
|
element: ExcalidrawLinearElement,
|
||||||
|
): ExcalidrawLinearElementSubType => {
|
||||||
|
if (isSharpArrow(element)) {
|
||||||
|
return "sharpArrow";
|
||||||
|
}
|
||||||
|
if (isCurvedArrow(element)) {
|
||||||
|
return "curvedArrow";
|
||||||
|
}
|
||||||
|
if (isElbowArrow(element)) {
|
||||||
|
return "elbowArrow";
|
||||||
|
}
|
||||||
|
return "line";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if current element points meet all the conditions for polygon=true
|
||||||
|
* (this isn't a element type check, for that use isLineElement).
|
||||||
|
*
|
||||||
|
* If you want to check if points *can* be turned into a polygon, use
|
||||||
|
* canBecomePolygon(points).
|
||||||
|
*/
|
||||||
|
export const isValidPolygon = (
|
||||||
|
points: ExcalidrawLineElement["points"],
|
||||||
|
): boolean => {
|
||||||
|
return points.length > 3 && pointsEqual(points[0], points[points.length - 1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const canBecomePolygon = (
|
||||||
|
points: ExcalidrawLineElement["points"],
|
||||||
|
): boolean => {
|
||||||
|
return (
|
||||||
|
points.length > 3 ||
|
||||||
|
// 3-point polygons can't have all points in a single line
|
||||||
|
(points.length === 3 && !pointsEqual(points[0], points[points.length - 1]))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -195,7 +195,8 @@ export type ExcalidrawRectanguloidElement =
|
|||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawIframeLikeElement
|
| ExcalidrawIframeLikeElement
|
||||||
| ExcalidrawFrameLikeElement
|
| ExcalidrawFrameLikeElement
|
||||||
| ExcalidrawEmbeddableElement;
|
| ExcalidrawEmbeddableElement
|
||||||
|
| ExcalidrawSelectionElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
||||||
@@ -296,6 +297,13 @@ export type FixedPointBinding = Merge<
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type Index = number;
|
||||||
|
|
||||||
|
export type PointsPositionUpdates = Map<
|
||||||
|
Index,
|
||||||
|
{ point: LocalPoint; isDragging?: boolean }
|
||||||
|
>;
|
||||||
|
|
||||||
export type Arrowhead =
|
export type Arrowhead =
|
||||||
| "arrow"
|
| "arrow"
|
||||||
| "bar"
|
| "bar"
|
||||||
@@ -321,10 +329,16 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
endArrowhead: Arrowhead | null;
|
endArrowhead: Arrowhead | null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawLineElement = ExcalidrawLinearElement &
|
||||||
|
Readonly<{
|
||||||
|
type: "line";
|
||||||
|
polygon: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type FixedSegment = {
|
export type FixedSegment = {
|
||||||
start: LocalPoint;
|
start: LocalPoint;
|
||||||
end: LocalPoint;
|
end: LocalPoint;
|
||||||
index: number;
|
index: Index;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExcalidrawArrowElement = ExcalidrawLinearElement &
|
export type ExcalidrawArrowElement = ExcalidrawLinearElement &
|
||||||
@@ -412,3 +426,13 @@ export type NonDeletedSceneElementsMap = Map<
|
|||||||
export type ElementsMapOrArray =
|
export type ElementsMapOrArray =
|
||||||
| readonly ExcalidrawElement[]
|
| readonly ExcalidrawElement[]
|
||||||
| Readonly<ElementsMap>;
|
| Readonly<ElementsMap>;
|
||||||
|
|
||||||
|
export type ExcalidrawLinearElementSubType =
|
||||||
|
| "line"
|
||||||
|
| "sharpArrow"
|
||||||
|
| "curvedArrow"
|
||||||
|
| "elbowArrow";
|
||||||
|
|
||||||
|
export type ConvertibleGenericTypes = "rectangle" | "diamond" | "ellipse";
|
||||||
|
export type ConvertibleLinearTypes = ExcalidrawLinearElementSubType;
|
||||||
|
export type ConvertibleTypes = ConvertibleGenericTypes | ConvertibleLinearTypes;
|
||||||
|
|||||||
+314
-239
@@ -1,28 +1,168 @@
|
|||||||
import {
|
import {
|
||||||
curve,
|
curve,
|
||||||
|
curveCatmullRomCubicApproxPoints,
|
||||||
|
curveOffsetPoints,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
pointFromVector,
|
pointFromArray,
|
||||||
rectangle,
|
rectangle,
|
||||||
vectorFromPoint,
|
|
||||||
vectorNormalize,
|
|
||||||
vectorScale,
|
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { elementCenterPoint } from "@excalidraw/common";
|
import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import { getCornerRadius } from "./shapes";
|
import { getCornerRadius } from "./shapes";
|
||||||
|
|
||||||
import { getDiamondPoints } from "./bounds";
|
import { getDiamondPoints } from "./bounds";
|
||||||
|
|
||||||
|
import { generateLinearCollisionShape } from "./Shape";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
type ElementShape = [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]];
|
||||||
|
|
||||||
|
const ElementShapesCache = new WeakMap<
|
||||||
|
ExcalidrawElement,
|
||||||
|
{ version: ExcalidrawElement["version"]; shapes: Map<number, ElementShape> }
|
||||||
|
>();
|
||||||
|
|
||||||
|
const getElementShapesCacheEntry = <T extends ExcalidrawElement>(
|
||||||
|
element: T,
|
||||||
|
offset: number,
|
||||||
|
): ElementShape | undefined => {
|
||||||
|
const record = ElementShapesCache.get(element);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { version, shapes } = record;
|
||||||
|
|
||||||
|
if (version !== element.version) {
|
||||||
|
ElementShapesCache.delete(element);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shapes.get(offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setElementShapesCacheEntry = <T extends ExcalidrawElement>(
|
||||||
|
element: T,
|
||||||
|
shape: ElementShape,
|
||||||
|
offset: number,
|
||||||
|
) => {
|
||||||
|
const record = ElementShapesCache.get(element);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
ElementShapesCache.set(element, {
|
||||||
|
version: element.version,
|
||||||
|
shapes: new Map([[offset, shape]]),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { version, shapes } = record;
|
||||||
|
|
||||||
|
if (version !== element.version) {
|
||||||
|
ElementShapesCache.set(element, {
|
||||||
|
version: element.version,
|
||||||
|
shapes: new Map([[offset, shape]]),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shapes.set(offset, shape);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function deconstructLinearOrFreeDrawElement(
|
||||||
|
element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||||
|
const cachedShape = getElementShapesCacheEntry(element, 0);
|
||||||
|
|
||||||
|
if (cachedShape) {
|
||||||
|
return cachedShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ops = generateLinearCollisionShape(element, elementsMap) as {
|
||||||
|
op: string;
|
||||||
|
data: number[];
|
||||||
|
}[];
|
||||||
|
const lines = [];
|
||||||
|
const curves = [];
|
||||||
|
|
||||||
|
for (let idx = 0; idx < ops.length; idx += 1) {
|
||||||
|
const op = ops[idx];
|
||||||
|
const prevPoint =
|
||||||
|
ops[idx - 1] && pointFromArray<LocalPoint>(ops[idx - 1].data.slice(-2));
|
||||||
|
switch (op.op) {
|
||||||
|
case "move":
|
||||||
|
continue;
|
||||||
|
case "lineTo":
|
||||||
|
if (!prevPoint) {
|
||||||
|
throw new Error("prevPoint is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
lineSegment<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + prevPoint[0],
|
||||||
|
element.y + prevPoint[1],
|
||||||
|
),
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[0],
|
||||||
|
element.y + op.data[1],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
case "bcurveTo":
|
||||||
|
if (!prevPoint) {
|
||||||
|
throw new Error("prevPoint is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
curves.push(
|
||||||
|
curve<GlobalPoint>(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + prevPoint[0],
|
||||||
|
element.y + prevPoint[1],
|
||||||
|
),
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[0],
|
||||||
|
element.y + op.data[1],
|
||||||
|
),
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[2],
|
||||||
|
element.y + op.data[3],
|
||||||
|
),
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + op.data[4],
|
||||||
|
element.y + op.data[5],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
default: {
|
||||||
|
console.error("Unknown op type", op.op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shape = [lines, curves] as ElementShape;
|
||||||
|
setElementShapesCacheEntry(element, shape, 0);
|
||||||
|
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the building components of a rectanguloid element in the form of
|
* Get the building components of a rectanguloid element in the form of
|
||||||
* line segments and curves.
|
* line segments and curves.
|
||||||
@@ -35,175 +175,132 @@ export function deconstructRectanguloidElement(
|
|||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||||
const roundness = getCornerRadius(
|
const cachedShape = getElementShapesCacheEntry(element, offset);
|
||||||
|
|
||||||
|
if (cachedShape) {
|
||||||
|
return cachedShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
let radius = getCornerRadius(
|
||||||
Math.min(element.width, element.height),
|
Math.min(element.width, element.height),
|
||||||
element,
|
element,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (roundness <= 0) {
|
if (radius === 0) {
|
||||||
const r = rectangle(
|
radius = 0.01;
|
||||||
pointFrom(element.x - offset, element.y - offset),
|
|
||||||
pointFrom(
|
|
||||||
element.x + element.width + offset,
|
|
||||||
element.y + element.height + offset,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const top = lineSegment<GlobalPoint>(
|
|
||||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[0][1]),
|
|
||||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[0][1]),
|
|
||||||
);
|
|
||||||
const right = lineSegment<GlobalPoint>(
|
|
||||||
pointFrom<GlobalPoint>(r[1][0], r[0][1] + roundness),
|
|
||||||
pointFrom<GlobalPoint>(r[1][0], r[1][1] - roundness),
|
|
||||||
);
|
|
||||||
const bottom = lineSegment<GlobalPoint>(
|
|
||||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[1][1]),
|
|
||||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[1][1]),
|
|
||||||
);
|
|
||||||
const left = lineSegment<GlobalPoint>(
|
|
||||||
pointFrom<GlobalPoint>(r[0][0], r[1][1] - roundness),
|
|
||||||
pointFrom<GlobalPoint>(r[0][0], r[0][1] + roundness),
|
|
||||||
);
|
|
||||||
const sides = [top, right, bottom, left];
|
|
||||||
|
|
||||||
return [sides, []];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = elementCenterPoint(element);
|
|
||||||
|
|
||||||
const r = rectangle(
|
const r = rectangle(
|
||||||
pointFrom(element.x, element.y),
|
pointFrom(element.x, element.y),
|
||||||
pointFrom(element.x + element.width, element.y + element.height),
|
pointFrom(element.x + element.width, element.y + element.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
const top = lineSegment<GlobalPoint>(
|
const top = lineSegment<GlobalPoint>(
|
||||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[0][1]),
|
pointFrom<GlobalPoint>(r[0][0] + radius, r[0][1]),
|
||||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[0][1]),
|
pointFrom<GlobalPoint>(r[1][0] - radius, r[0][1]),
|
||||||
);
|
);
|
||||||
const right = lineSegment<GlobalPoint>(
|
const right = lineSegment<GlobalPoint>(
|
||||||
pointFrom<GlobalPoint>(r[1][0], r[0][1] + roundness),
|
pointFrom<GlobalPoint>(r[1][0], r[0][1] + radius),
|
||||||
pointFrom<GlobalPoint>(r[1][0], r[1][1] - roundness),
|
pointFrom<GlobalPoint>(r[1][0], r[1][1] - radius),
|
||||||
);
|
);
|
||||||
const bottom = lineSegment<GlobalPoint>(
|
const bottom = lineSegment<GlobalPoint>(
|
||||||
pointFrom<GlobalPoint>(r[0][0] + roundness, r[1][1]),
|
pointFrom<GlobalPoint>(r[0][0] + radius, r[1][1]),
|
||||||
pointFrom<GlobalPoint>(r[1][0] - roundness, r[1][1]),
|
pointFrom<GlobalPoint>(r[1][0] - radius, r[1][1]),
|
||||||
);
|
);
|
||||||
const left = lineSegment<GlobalPoint>(
|
const left = lineSegment<GlobalPoint>(
|
||||||
pointFrom<GlobalPoint>(r[0][0], r[1][1] - roundness),
|
pointFrom<GlobalPoint>(r[0][0], r[1][1] - radius),
|
||||||
pointFrom<GlobalPoint>(r[0][0], r[0][1] + roundness),
|
pointFrom<GlobalPoint>(r[0][0], r[0][1] + radius),
|
||||||
);
|
);
|
||||||
|
|
||||||
const offsets = [
|
const baseCorners = [
|
||||||
vectorScale(
|
|
||||||
vectorNormalize(
|
|
||||||
vectorFromPoint(pointFrom(r[0][0] - offset, r[0][1] - offset), center),
|
|
||||||
),
|
|
||||||
offset,
|
|
||||||
), // TOP LEFT
|
|
||||||
vectorScale(
|
|
||||||
vectorNormalize(
|
|
||||||
vectorFromPoint(pointFrom(r[1][0] + offset, r[0][1] - offset), center),
|
|
||||||
),
|
|
||||||
offset,
|
|
||||||
), //TOP RIGHT
|
|
||||||
vectorScale(
|
|
||||||
vectorNormalize(
|
|
||||||
vectorFromPoint(pointFrom(r[1][0] + offset, r[1][1] + offset), center),
|
|
||||||
),
|
|
||||||
offset,
|
|
||||||
), // BOTTOM RIGHT
|
|
||||||
vectorScale(
|
|
||||||
vectorNormalize(
|
|
||||||
vectorFromPoint(pointFrom(r[0][0] - offset, r[1][1] + offset), center),
|
|
||||||
),
|
|
||||||
offset,
|
|
||||||
), // BOTTOM LEFT
|
|
||||||
];
|
|
||||||
|
|
||||||
const corners = [
|
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(offsets[0], left[1]),
|
left[1],
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[0],
|
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
||||||
pointFrom<GlobalPoint>(
|
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
||||||
left[1][0] + (2 / 3) * (r[0][0] - left[1][0]),
|
|
||||||
left[1][1] + (2 / 3) * (r[0][1] - left[1][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[0],
|
top[0][0] + (2 / 3) * (r[0][0] - top[0][0]),
|
||||||
pointFrom<GlobalPoint>(
|
top[0][1] + (2 / 3) * (r[0][1] - top[0][1]),
|
||||||
top[0][0] + (2 / 3) * (r[0][0] - top[0][0]),
|
|
||||||
top[0][1] + (2 / 3) * (r[0][1] - top[0][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[0], top[0]),
|
top[0],
|
||||||
), // TOP LEFT
|
), // TOP LEFT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(offsets[1], top[1]),
|
top[1],
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[1],
|
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
||||||
pointFrom<GlobalPoint>(
|
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
||||||
top[1][0] + (2 / 3) * (r[1][0] - top[1][0]),
|
|
||||||
top[1][1] + (2 / 3) * (r[0][1] - top[1][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[1],
|
right[0][0] + (2 / 3) * (r[1][0] - right[0][0]),
|
||||||
pointFrom<GlobalPoint>(
|
right[0][1] + (2 / 3) * (r[0][1] - right[0][1]),
|
||||||
right[0][0] + (2 / 3) * (r[1][0] - right[0][0]),
|
|
||||||
right[0][1] + (2 / 3) * (r[0][1] - right[0][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[1], right[0]),
|
right[0],
|
||||||
), // TOP RIGHT
|
), // TOP RIGHT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(offsets[2], right[1]),
|
right[1],
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[2],
|
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
||||||
pointFrom<GlobalPoint>(
|
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
||||||
right[1][0] + (2 / 3) * (r[1][0] - right[1][0]),
|
|
||||||
right[1][1] + (2 / 3) * (r[1][1] - right[1][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[2],
|
bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]),
|
||||||
pointFrom<GlobalPoint>(
|
bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]),
|
||||||
bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]),
|
|
||||||
bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[2], bottom[1]),
|
bottom[1],
|
||||||
), // BOTTOM RIGHT
|
), // BOTTOM RIGHT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(offsets[3], bottom[0]),
|
bottom[0],
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[3],
|
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
||||||
pointFrom<GlobalPoint>(
|
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
||||||
bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]),
|
|
||||||
bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[3],
|
left[0][0] + (2 / 3) * (r[0][0] - left[0][0]),
|
||||||
pointFrom<GlobalPoint>(
|
left[0][1] + (2 / 3) * (r[1][1] - left[0][1]),
|
||||||
left[0][0] + (2 / 3) * (r[0][0] - left[0][0]),
|
|
||||||
left[0][1] + (2 / 3) * (r[1][1] - left[0][1]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[3], left[0]),
|
left[0],
|
||||||
), // BOTTOM LEFT
|
), // BOTTOM LEFT
|
||||||
];
|
];
|
||||||
|
|
||||||
const sides = [
|
const corners =
|
||||||
lineSegment<GlobalPoint>(corners[0][3], corners[1][0]),
|
offset > 0
|
||||||
lineSegment<GlobalPoint>(corners[1][3], corners[2][0]),
|
? baseCorners.map(
|
||||||
lineSegment<GlobalPoint>(corners[2][3], corners[3][0]),
|
(corner) =>
|
||||||
lineSegment<GlobalPoint>(corners[3][3], corners[0][0]),
|
curveCatmullRomCubicApproxPoints(
|
||||||
];
|
curveOffsetPoints(corner, offset),
|
||||||
|
)!,
|
||||||
|
)
|
||||||
|
: [
|
||||||
|
[baseCorners[0]],
|
||||||
|
[baseCorners[1]],
|
||||||
|
[baseCorners[2]],
|
||||||
|
[baseCorners[3]],
|
||||||
|
];
|
||||||
|
|
||||||
return [sides, corners];
|
const sides = [
|
||||||
|
lineSegment<GlobalPoint>(
|
||||||
|
corners[0][corners[0].length - 1][3],
|
||||||
|
corners[1][0][0],
|
||||||
|
),
|
||||||
|
lineSegment<GlobalPoint>(
|
||||||
|
corners[1][corners[1].length - 1][3],
|
||||||
|
corners[2][0][0],
|
||||||
|
),
|
||||||
|
lineSegment<GlobalPoint>(
|
||||||
|
corners[2][corners[2].length - 1][3],
|
||||||
|
corners[3][0][0],
|
||||||
|
),
|
||||||
|
lineSegment<GlobalPoint>(
|
||||||
|
corners[3][corners[3].length - 1][3],
|
||||||
|
corners[0][0][0],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const shape = [sides, corners.flat()] as ElementShape;
|
||||||
|
|
||||||
|
setElementShapesCacheEntry(element, shape, offset);
|
||||||
|
|
||||||
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,42 +315,20 @@ export function deconstructDiamondElement(
|
|||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
): [LineSegment<GlobalPoint>[], Curve<GlobalPoint>[]] {
|
||||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
const cachedShape = getElementShapesCacheEntry(element, offset);
|
||||||
getDiamondPoints(element);
|
|
||||||
const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element);
|
|
||||||
const horizontalRadius = getCornerRadius(Math.abs(rightY - topY), element);
|
|
||||||
|
|
||||||
if (element.roundness?.type == null) {
|
if (cachedShape) {
|
||||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
return cachedShape;
|
||||||
pointFrom(element.x + topX, element.y + topY - offset),
|
|
||||||
pointFrom(element.x + rightX + offset, element.y + rightY),
|
|
||||||
pointFrom(element.x + bottomX, element.y + bottomY + offset),
|
|
||||||
pointFrom(element.x + leftX - offset, element.y + leftY),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Create the line segment parts of the diamond
|
|
||||||
// NOTE: Horizontal and vertical seems to be flipped here
|
|
||||||
const topRight = lineSegment<GlobalPoint>(
|
|
||||||
pointFrom(top[0] + verticalRadius, top[1] + horizontalRadius),
|
|
||||||
pointFrom(right[0] - verticalRadius, right[1] - horizontalRadius),
|
|
||||||
);
|
|
||||||
const bottomRight = lineSegment<GlobalPoint>(
|
|
||||||
pointFrom(right[0] - verticalRadius, right[1] + horizontalRadius),
|
|
||||||
pointFrom(bottom[0] + verticalRadius, bottom[1] - horizontalRadius),
|
|
||||||
);
|
|
||||||
const bottomLeft = lineSegment<GlobalPoint>(
|
|
||||||
pointFrom(bottom[0] - verticalRadius, bottom[1] - horizontalRadius),
|
|
||||||
pointFrom(left[0] + verticalRadius, left[1] + horizontalRadius),
|
|
||||||
);
|
|
||||||
const topLeft = lineSegment<GlobalPoint>(
|
|
||||||
pointFrom(left[0] + verticalRadius, left[1] - horizontalRadius),
|
|
||||||
pointFrom(top[0] - verticalRadius, top[1] + horizontalRadius),
|
|
||||||
);
|
|
||||||
|
|
||||||
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = elementCenterPoint(element);
|
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||||
|
getDiamondPoints(element);
|
||||||
|
const verticalRadius = element.roundness
|
||||||
|
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||||
|
: (topX - leftX) * 0.01;
|
||||||
|
const horizontalRadius = element.roundness
|
||||||
|
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||||
|
: (rightY - topY) * 0.01;
|
||||||
|
|
||||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||||
pointFrom(element.x + topX, element.y + topY),
|
pointFrom(element.x + topX, element.y + topY),
|
||||||
@@ -262,94 +337,94 @@ export function deconstructDiamondElement(
|
|||||||
pointFrom(element.x + leftX, element.y + leftY),
|
pointFrom(element.x + leftX, element.y + leftY),
|
||||||
];
|
];
|
||||||
|
|
||||||
const offsets = [
|
const baseCorners = [
|
||||||
vectorScale(vectorNormalize(vectorFromPoint(right, center)), offset), // RIGHT
|
|
||||||
vectorScale(vectorNormalize(vectorFromPoint(bottom, center)), offset), // BOTTOM
|
|
||||||
vectorScale(vectorNormalize(vectorFromPoint(left, center)), offset), // LEFT
|
|
||||||
vectorScale(vectorNormalize(vectorFromPoint(top, center)), offset), // TOP
|
|
||||||
];
|
|
||||||
|
|
||||||
const corners = [
|
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[0],
|
right[0] - verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
right[1] - horizontalRadius,
|
||||||
right[0] - verticalRadius,
|
|
||||||
right[1] - horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[0], right),
|
right,
|
||||||
pointFromVector(offsets[0], right),
|
right,
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[0],
|
right[0] - verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
right[1] + horizontalRadius,
|
||||||
right[0] - verticalRadius,
|
|
||||||
right[1] + horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
), // RIGHT
|
), // RIGHT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[1],
|
bottom[0] + verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
bottom[1] - horizontalRadius,
|
||||||
bottom[0] + verticalRadius,
|
|
||||||
bottom[1] - horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[1], bottom),
|
bottom,
|
||||||
pointFromVector(offsets[1], bottom),
|
bottom,
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[1],
|
bottom[0] - verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
bottom[1] - horizontalRadius,
|
||||||
bottom[0] - verticalRadius,
|
|
||||||
bottom[1] - horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
), // BOTTOM
|
), // BOTTOM
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[2],
|
left[0] + verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
left[1] + horizontalRadius,
|
||||||
left[0] + verticalRadius,
|
|
||||||
left[1] + horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[2], left),
|
left,
|
||||||
pointFromVector(offsets[2], left),
|
left,
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[2],
|
left[0] + verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
left[1] - horizontalRadius,
|
||||||
left[0] + verticalRadius,
|
|
||||||
left[1] - horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
), // LEFT
|
), // LEFT
|
||||||
curve(
|
curve(
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[3],
|
top[0] - verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
top[1] + horizontalRadius,
|
||||||
top[0] - verticalRadius,
|
|
||||||
top[1] + horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
pointFromVector(offsets[3], top),
|
top,
|
||||||
pointFromVector(offsets[3], top),
|
top,
|
||||||
pointFromVector(
|
pointFrom<GlobalPoint>(
|
||||||
offsets[3],
|
top[0] + verticalRadius,
|
||||||
pointFrom<GlobalPoint>(
|
top[1] + horizontalRadius,
|
||||||
top[0] + verticalRadius,
|
|
||||||
top[1] + horizontalRadius,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
), // TOP
|
), // TOP
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const corners =
|
||||||
|
offset > 0
|
||||||
|
? baseCorners.map(
|
||||||
|
(corner) =>
|
||||||
|
curveCatmullRomCubicApproxPoints(
|
||||||
|
curveOffsetPoints(corner, offset),
|
||||||
|
)!,
|
||||||
|
)
|
||||||
|
: [
|
||||||
|
[baseCorners[0]],
|
||||||
|
[baseCorners[1]],
|
||||||
|
[baseCorners[2]],
|
||||||
|
[baseCorners[3]],
|
||||||
|
];
|
||||||
|
|
||||||
const sides = [
|
const sides = [
|
||||||
lineSegment<GlobalPoint>(corners[0][3], corners[1][0]),
|
lineSegment<GlobalPoint>(
|
||||||
lineSegment<GlobalPoint>(corners[1][3], corners[2][0]),
|
corners[0][corners[0].length - 1][3],
|
||||||
lineSegment<GlobalPoint>(corners[2][3], corners[3][0]),
|
corners[1][0][0],
|
||||||
lineSegment<GlobalPoint>(corners[3][3], corners[0][0]),
|
),
|
||||||
|
lineSegment<GlobalPoint>(
|
||||||
|
corners[1][corners[1].length - 1][3],
|
||||||
|
corners[2][0][0],
|
||||||
|
),
|
||||||
|
lineSegment<GlobalPoint>(
|
||||||
|
corners[2][corners[2].length - 1][3],
|
||||||
|
corners[3][0][0],
|
||||||
|
),
|
||||||
|
lineSegment<GlobalPoint>(
|
||||||
|
corners[3][corners[3].length - 1][3],
|
||||||
|
corners[0][0][0],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return [sides, corners];
|
const shape = [sides, corners.flat()] as ElementShape;
|
||||||
|
|
||||||
|
setElementShapesCacheEntry(element, shape, offset);
|
||||||
|
|
||||||
|
return shape;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
|||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import { isFrameLikeElement } from "./typeChecks";
|
import { isFrameLikeElement } from "./typeChecks";
|
||||||
|
|
||||||
import { getElementsInGroup } from "./groups";
|
import { getElementsInGroup } from "./groups";
|
||||||
@@ -12,6 +10,8 @@ import { syncMovedIndices } from "./fractionalIndex";
|
|||||||
|
|
||||||
import { getSelectedElements } from "./selection";
|
import { getSelectedElements } from "./selection";
|
||||||
|
|
||||||
|
import type { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
||||||
|
|
||||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo
|
|||||||
class="excalidraw-wysiwyg"
|
class="excalidraw-wysiwyg"
|
||||||
data-type="wysiwyg"
|
data-type="wysiwyg"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 26.25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Xiaolai, Segoe UI Emoji;"
|
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 26.25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Xiaolai, sans-serif, Segoe UI Emoji;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
wrap="off"
|
wrap="off"
|
||||||
/>
|
/>
|
||||||
@@ -35,6 +35,7 @@ const createAndSelectTwoRectangles = () => {
|
|||||||
// The second rectangle is already reselected because it was the last element created
|
// The second rectangle is already reselected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -52,6 +53,7 @@ const createAndSelectTwoRectanglesWithDifferentSizes = () => {
|
|||||||
// The second rectangle is already reselected because it was the last element created
|
// The second rectangle is already reselected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -202,6 +204,7 @@ describe("aligning", () => {
|
|||||||
// The second rectangle is already reselected because it was the last element created
|
// The second rectangle is already reselected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -215,6 +218,7 @@ describe("aligning", () => {
|
|||||||
// Add the created group to the current selection
|
// Add the created group to the current selection
|
||||||
mouse.restorePosition(0, 0);
|
mouse.restorePosition(0, 0);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -316,6 +320,7 @@ describe("aligning", () => {
|
|||||||
// The second rectangle is already selected because it was the last element created
|
// The second rectangle is already selected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -330,7 +335,7 @@ describe("aligning", () => {
|
|||||||
mouse.down();
|
mouse.down();
|
||||||
mouse.up(100, 100);
|
mouse.up(100, 100);
|
||||||
|
|
||||||
mouse.restorePosition(200, 200);
|
mouse.restorePosition(210, 200);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
@@ -341,6 +346,7 @@ describe("aligning", () => {
|
|||||||
// The second group is already selected because it was the last group created
|
// The second group is already selected because it was the last group created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -454,6 +460,7 @@ describe("aligning", () => {
|
|||||||
// The second rectangle is already reselected because it was the last element created
|
// The second rectangle is already reselected because it was the last element created
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -466,7 +473,7 @@ describe("aligning", () => {
|
|||||||
mouse.up(100, 100);
|
mouse.up(100, 100);
|
||||||
|
|
||||||
// Add group to current selection
|
// Add group to current selection
|
||||||
mouse.restorePosition(0, 0);
|
mouse.restorePosition(10, 0);
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
@@ -482,6 +489,7 @@ describe("aligning", () => {
|
|||||||
// Select the nested group, the rectangle is already selected
|
// Select the nested group, the rectangle is already selected
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.moveTo(10, 0);
|
||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
|
|||||||
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import { getTransformHandles } from "../src/transformHandles";
|
import { getTransformHandles } from "../src/transformHandles";
|
||||||
|
import {
|
||||||
|
getTextEditor,
|
||||||
|
TEXT_EDITOR_SELECTOR,
|
||||||
|
} from "../../excalidraw/tests/queries/dom";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@@ -172,12 +176,12 @@ describe("element binding", () => {
|
|||||||
const arrow = UI.createElement("arrow", {
|
const arrow = UI.createElement("arrow", {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
size: 50,
|
size: 49,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(arrow.endBinding).toBe(null);
|
expect(arrow.endBinding).toBe(null);
|
||||||
|
|
||||||
mouse.downAt(50, 50);
|
mouse.downAt(49, 49);
|
||||||
mouse.moveTo(51, 0);
|
mouse.moveTo(51, 0);
|
||||||
mouse.up(0, 0);
|
mouse.up(0, 0);
|
||||||
|
|
||||||
@@ -244,18 +248,12 @@ describe("element binding", () => {
|
|||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = await getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "" } });
|
fireEvent.change(editor, { target: { value: "" } });
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
|
||||||
expect(
|
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
||||||
document.querySelector(".excalidraw-textEditorContainer > textarea"),
|
|
||||||
).toBe(null);
|
|
||||||
expect(arrow.endBinding).toBe(null);
|
expect(arrow.endBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -285,18 +283,14 @@ describe("element binding", () => {
|
|||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
const editor = document.querySelector(
|
const editor = await getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "asdasdasdasdas" } });
|
fireEvent.change(editor, { target: { value: "asdasdasdasdas" } });
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
|
||||||
expect(
|
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
||||||
document.querySelector(".excalidraw-textEditorContainer > textarea"),
|
|
||||||
).toBe(null);
|
|
||||||
expect(arrow.endBinding?.elementId).toBe(text.id);
|
expect(arrow.endBinding?.elementId).toBe(text.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { type GlobalPoint, type LocalPoint, pointFrom } from "@excalidraw/math";
|
||||||
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
import { UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
import "@excalidraw/utils/test-utils";
|
||||||
|
import { render } from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import { hitElementItself } from "../src/collision";
|
||||||
|
|
||||||
|
describe("check rotated elements can be hit:", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("arrow", () => {
|
||||||
|
UI.createElement("arrow", {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 124,
|
||||||
|
height: 302,
|
||||||
|
angle: 1.8700426423973724,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[120, -198],
|
||||||
|
[-4, -302],
|
||||||
|
] as LocalPoint[],
|
||||||
|
});
|
||||||
|
//const p = [120, -211];
|
||||||
|
//const p = [0, 13];
|
||||||
|
const hit = hitElementItself({
|
||||||
|
point: pointFrom<GlobalPoint>(87, -68),
|
||||||
|
element: window.h.elements[0],
|
||||||
|
threshold: 10,
|
||||||
|
elementsMap: window.h.scene.getNonDeletedElementsMap(),
|
||||||
|
});
|
||||||
|
expect(hit).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
+20
-11
@@ -3,21 +3,30 @@ import { vi } from "vitest";
|
|||||||
|
|
||||||
import { KEYS, cloneJSON } from "@excalidraw/common";
|
import { KEYS, cloneJSON } from "@excalidraw/common";
|
||||||
|
|
||||||
import { duplicateElement } from "@excalidraw/element/duplicate";
|
import {
|
||||||
|
Excalidraw,
|
||||||
|
exportToCanvas,
|
||||||
|
exportToSvg,
|
||||||
|
} from "@excalidraw/excalidraw";
|
||||||
|
import {
|
||||||
|
actionFlipHorizontal,
|
||||||
|
actionFlipVertical,
|
||||||
|
} from "@excalidraw/excalidraw/actions";
|
||||||
|
|
||||||
import type {
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
ExcalidrawImageElement,
|
import { Keyboard, Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
ImageCrop,
|
import {
|
||||||
} from "@excalidraw/element/types";
|
act,
|
||||||
|
GlobalTestState,
|
||||||
|
render,
|
||||||
|
unmountComponent,
|
||||||
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import { Excalidraw, exportToCanvas, exportToSvg } from "..";
|
import type { NormalizedZoomValue } from "@excalidraw/excalidraw/types";
|
||||||
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
|
|
||||||
|
|
||||||
import { API } from "./helpers/api";
|
import { duplicateElement } from "../src/duplicate";
|
||||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
|
||||||
import { act, GlobalTestState, render, unmountComponent } from "./test-utils";
|
|
||||||
|
|
||||||
import type { NormalizedZoomValue } from "../types";
|
import type { ExcalidrawImageElement, ImageCrop } from "../src/types";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import type { ObservedAppState } from "@excalidraw/excalidraw/types";
|
||||||
|
import type { LinearElementEditor } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { AppStateDelta } from "../src/delta";
|
||||||
|
|
||||||
|
describe("AppStateDelta", () => {
|
||||||
|
describe("ensure stable delta properties order", () => {
|
||||||
|
it("should maintain stable order for root properties", () => {
|
||||||
|
const name = "untitled scene";
|
||||||
|
const selectedLinearElementId = "id1" as LinearElementEditor["elementId"];
|
||||||
|
|
||||||
|
const commonAppState = {
|
||||||
|
viewBackgroundColor: "#ffffff",
|
||||||
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
croppingElementId: null,
|
||||||
|
editingLinearElementId: null,
|
||||||
|
lockedMultiSelections: {},
|
||||||
|
activeLockedId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevAppState1: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
name: "",
|
||||||
|
selectedLinearElementId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextAppState1: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
name,
|
||||||
|
selectedLinearElementId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevAppState2: ObservedAppState = {
|
||||||
|
selectedLinearElementId: null,
|
||||||
|
name: "",
|
||||||
|
...commonAppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextAppState2: ObservedAppState = {
|
||||||
|
selectedLinearElementId,
|
||||||
|
name,
|
||||||
|
...commonAppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
|
||||||
|
const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
|
||||||
|
|
||||||
|
expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should maintain stable order for selectedElementIds", () => {
|
||||||
|
const commonAppState = {
|
||||||
|
name: "",
|
||||||
|
viewBackgroundColor: "#ffffff",
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
croppingElementId: null,
|
||||||
|
selectedLinearElementId: null,
|
||||||
|
editingLinearElementId: null,
|
||||||
|
activeLockedId: null,
|
||||||
|
lockedMultiSelections: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevAppState1: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
selectedElementIds: { id5: true, id2: true, id4: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextAppState1: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
selectedElementIds: {
|
||||||
|
id1: true,
|
||||||
|
id2: true,
|
||||||
|
id3: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevAppState2: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
selectedElementIds: { id4: true, id2: true, id5: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextAppState2: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
selectedElementIds: {
|
||||||
|
id3: true,
|
||||||
|
id2: true,
|
||||||
|
id1: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
|
||||||
|
const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
|
||||||
|
|
||||||
|
expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should maintain stable order for selectedGroupIds", () => {
|
||||||
|
const commonAppState = {
|
||||||
|
name: "",
|
||||||
|
viewBackgroundColor: "#ffffff",
|
||||||
|
selectedElementIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
croppingElementId: null,
|
||||||
|
selectedLinearElementId: null,
|
||||||
|
editingLinearElementId: null,
|
||||||
|
activeLockedId: null,
|
||||||
|
lockedMultiSelections: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevAppState1: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
selectedGroupIds: { id5: false, id2: true, id4: true, id0: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextAppState1: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
selectedGroupIds: {
|
||||||
|
id0: true,
|
||||||
|
id1: true,
|
||||||
|
id2: false,
|
||||||
|
id3: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevAppState2: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
selectedGroupIds: { id0: true, id4: true, id2: true, id5: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextAppState2: ObservedAppState = {
|
||||||
|
...commonAppState,
|
||||||
|
selectedGroupIds: {
|
||||||
|
id3: true,
|
||||||
|
id2: false,
|
||||||
|
id1: true,
|
||||||
|
id0: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const delta1 = AppStateDelta.calculate(prevAppState1, nextAppState1);
|
||||||
|
const delta2 = AppStateDelta.calculate(prevAppState2, nextAppState2);
|
||||||
|
|
||||||
|
expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
isPrimitive,
|
isPrimitive,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||||
|
|
||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { mutateElement } from "../src/mutateElement";
|
|
||||||
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
||||||
|
|
||||||
import type { ExcalidrawLinearElement } from "../src/types";
|
import type { ExcalidrawLinearElement } from "../src/types";
|
||||||
@@ -62,7 +61,7 @@ describe("duplicating single elements", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
element.__proto__ = { hello: "world" };
|
element.__proto__ = { hello: "world" };
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, new Map(), {
|
||||||
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -506,8 +505,6 @@ describe("group-related duplication", () => {
|
|||||||
mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50);
|
mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log(h.elements);
|
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ id: frame.id },
|
{ id: frame.id },
|
||||||
{ id: rectangle1.id, frameId: frame.id },
|
{ id: rectangle1.id, frameId: frame.id },
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ARROW_TYPE } from "@excalidraw/common";
|
import { ARROW_TYPE } from "@excalidraw/common";
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
||||||
|
|
||||||
@@ -23,6 +22,8 @@ import type { LocalPoint } from "@excalidraw/math";
|
|||||||
|
|
||||||
import { bindLinearElement } from "../src/binding";
|
import { bindLinearElement } from "../src/binding";
|
||||||
|
|
||||||
|
import { Scene } from "../src/Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
@@ -142,7 +143,7 @@ describe("elbow arrow routing", () => {
|
|||||||
elbowed: true,
|
elbowed: true,
|
||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
mutateElement(arrow, {
|
h.app.scene.mutateElement(arrow, {
|
||||||
points: [
|
points: [
|
||||||
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
||||||
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
||||||
@@ -187,14 +188,14 @@ describe("elbow arrow routing", () => {
|
|||||||
scene.insertElement(rectangle1);
|
scene.insertElement(rectangle1);
|
||||||
scene.insertElement(rectangle2);
|
scene.insertElement(rectangle2);
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
bindLinearElement(arrow, rectangle1, "start", elementsMap);
|
bindLinearElement(arrow, rectangle1, "start", scene);
|
||||||
bindLinearElement(arrow, rectangle2, "end", elementsMap);
|
bindLinearElement(arrow, rectangle2, "end", scene);
|
||||||
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
mutateElement(arrow, {
|
h.app.scene.mutateElement(arrow, {
|
||||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import {
|
|||||||
syncInvalidIndices,
|
syncInvalidIndices,
|
||||||
syncMovedIndices,
|
syncMovedIndices,
|
||||||
validateFractionalIndices,
|
validateFractionalIndices,
|
||||||
} from "@excalidraw/element/fractionalIndex";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
import { deepCopyElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
@@ -749,7 +750,7 @@ function testInvalidIndicesSync(args: {
|
|||||||
function prepareArguments(
|
function prepareArguments(
|
||||||
elementsLike: { id: string; index?: string }[],
|
elementsLike: { id: string; index?: string }[],
|
||||||
movedElementsIds?: string[],
|
movedElementsIds?: string[],
|
||||||
): [ExcalidrawElement[], Map<string, ExcalidrawElement> | undefined] {
|
): [ExcalidrawElement[], ElementsMap | undefined] {
|
||||||
const elements = elementsLike.map((x) =>
|
const elements = elementsLike.map((x) =>
|
||||||
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
|
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
|
||||||
);
|
);
|
||||||
@@ -764,7 +765,7 @@ function prepareArguments(
|
|||||||
function test(
|
function test(
|
||||||
name: string,
|
name: string,
|
||||||
elements: ExcalidrawElement[],
|
elements: ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement> | undefined,
|
movedElements: ElementsMap | undefined,
|
||||||
expectUnchangedElements: Map<string, { id: string }>,
|
expectUnchangedElements: Map<string, { id: string }>,
|
||||||
expectValidInput?: boolean,
|
expectValidInput?: boolean,
|
||||||
) {
|
) {
|
||||||
|
|||||||
+184
-64
@@ -1,8 +1,5 @@
|
|||||||
import { newArrowElement } from "@excalidraw/element/newElement";
|
|
||||||
|
|
||||||
import { pointCenter, pointFrom } from "@excalidraw/math";
|
import { pointCenter, pointFrom } from "@excalidraw/math";
|
||||||
import { act, queryByTestId, queryByText } from "@testing-library/react";
|
import { act, queryByTestId, queryByText } from "@testing-library/react";
|
||||||
import React from "react";
|
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -13,36 +10,39 @@ import {
|
|||||||
arrayToMap,
|
arrayToMap,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import {
|
import * as InteractiveCanvas from "@excalidraw/excalidraw/renderer/interactiveScene";
|
||||||
getBoundTextElementPosition,
|
import * as StaticScene from "@excalidraw/excalidraw/renderer/staticScene";
|
||||||
getBoundTextMaxWidth,
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
} from "@excalidraw/element/textElement";
|
|
||||||
import * as textElementUtils from "@excalidraw/element/textElement";
|
|
||||||
import { wrapText } from "@excalidraw/element/textWrapping";
|
|
||||||
|
|
||||||
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
import { Keyboard, Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
|
|
||||||
import type {
|
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
ExcalidrawTextElementWithContainer,
|
|
||||||
FontString,
|
|
||||||
} from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import { Excalidraw, mutateElement } from "../index";
|
|
||||||
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
|
||||||
import * as StaticScene from "../renderer/staticScene";
|
|
||||||
import { API } from "../tests/helpers/api";
|
|
||||||
|
|
||||||
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
|
||||||
import {
|
import {
|
||||||
screen,
|
screen,
|
||||||
render,
|
render,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
GlobalTestState,
|
GlobalTestState,
|
||||||
unmountComponent,
|
unmountComponent,
|
||||||
} from "./test-utils";
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
|
import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { wrapText } from "../src";
|
||||||
|
import * as textElementUtils from "../src/textElement";
|
||||||
|
import { getBoundTextElementPosition, getBoundTextMaxWidth } from "../src";
|
||||||
|
import { LinearElementEditor } from "../src";
|
||||||
|
import { newArrowElement } from "../src";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTextEditor,
|
||||||
|
TEXT_EDITOR_SELECTOR,
|
||||||
|
} from "../../excalidraw/tests/queries/dom";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawTextElementWithContainer,
|
||||||
|
FontString,
|
||||||
|
} from "../src/types";
|
||||||
|
|
||||||
const renderInteractiveScene = vi.spyOn(
|
const renderInteractiveScene = vi.spyOn(
|
||||||
InteractiveCanvas,
|
InteractiveCanvas,
|
||||||
@@ -118,7 +118,7 @@ describe("Test Linear Elements", () => {
|
|||||||
],
|
],
|
||||||
roundness,
|
roundness,
|
||||||
});
|
});
|
||||||
mutateElement(line, { points: line.points });
|
h.app.scene.mutateElement(line, { points: line.points });
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
mouse.clickAt(p1[0], p1[1]);
|
mouse.clickAt(p1[0], p1[1]);
|
||||||
return line;
|
return line;
|
||||||
@@ -177,7 +177,7 @@ describe("Test Linear Elements", () => {
|
|||||||
pointFrom<LocalPoint>(0.5, 0),
|
pointFrom<LocalPoint>(0.5, 0),
|
||||||
pointFrom<LocalPoint>(100, 100),
|
pointFrom<LocalPoint>(100, 100),
|
||||||
]);
|
]);
|
||||||
new LinearElementEditor(element);
|
new LinearElementEditor(element, arrayToMap(h.elements));
|
||||||
expect(element.points).toEqual([
|
expect(element.points).toEqual([
|
||||||
pointFrom<LocalPoint>(0, 0),
|
pointFrom<LocalPoint>(0, 0),
|
||||||
pointFrom<LocalPoint>(99.5, 100),
|
pointFrom<LocalPoint>(99.5, 100),
|
||||||
@@ -256,7 +256,49 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should enter line editor when using double clicked with ctrl key", () => {
|
it("should enter line editor via enter (line)", () => {
|
||||||
|
createTwoPointerLinearElement("line");
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
||||||
|
|
||||||
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ctrl+enter alias (to align with arrows)
|
||||||
|
it("should enter line editor via ctrl+enter (line)", () => {
|
||||||
|
createTwoPointerLinearElement("line");
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
||||||
|
|
||||||
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
});
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enter line editor via ctrl+enter (arrow)", () => {
|
||||||
|
createTwoPointerLinearElement("arrow");
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
||||||
|
|
||||||
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
});
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enter line editor on ctrl+dblclick (simple arrow)", () => {
|
||||||
|
createTwoPointerLinearElement("arrow");
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
|
mouse.doubleClick();
|
||||||
|
});
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should enter line editor on ctrl+dblclick (line)", () => {
|
||||||
createTwoPointerLinearElement("line");
|
createTwoPointerLinearElement("line");
|
||||||
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
||||||
|
|
||||||
@@ -266,6 +308,37 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should enter line editor on dblclick (line)", () => {
|
||||||
|
createTwoPointerLinearElement("line");
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
||||||
|
|
||||||
|
mouse.doubleClick();
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not enter line editor on dblclick (arrow)", async () => {
|
||||||
|
createTwoPointerLinearElement("arrow");
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
||||||
|
|
||||||
|
mouse.doubleClick();
|
||||||
|
expect(h.state.editingLinearElement).toEqual(null);
|
||||||
|
await getTextEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't create text element on double click in line editor (arrow)", async () => {
|
||||||
|
createTwoPointerLinearElement("arrow");
|
||||||
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
enterLineEditingMode(arrow);
|
||||||
|
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id);
|
||||||
|
|
||||||
|
mouse.doubleClick();
|
||||||
|
expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id);
|
||||||
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
|
expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
describe("Inside editor", () => {
|
describe("Inside editor", () => {
|
||||||
it("should not drag line and add midpoint when dragged irrespective of threshold", () => {
|
it("should not drag line and add midpoint when dragged irrespective of threshold", () => {
|
||||||
createTwoPointerLinearElement("line");
|
createTwoPointerLinearElement("line");
|
||||||
@@ -994,12 +1067,10 @@ describe("Test Linear Elements", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should match styles for text editor", () => {
|
it("should match styles for text editor", async () => {
|
||||||
createTwoPointerLinearElement("arrow");
|
createTwoPointerLinearElement("arrow");
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = document.querySelector(
|
const editor = await getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
expect(editor).toMatchSnapshot();
|
expect(editor).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1016,9 +1087,7 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(arrow.id);
|
expect(text.containerId).toBe(arrow.id);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = await getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
fireEvent.change(editor, {
|
||||||
target: { value: DEFAULT_TEXT },
|
target: { value: DEFAULT_TEXT },
|
||||||
@@ -1046,9 +1115,7 @@ describe("Test Linear Elements", () => {
|
|||||||
const textElement = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const textElement = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(textElement.type).toBe("text");
|
expect(textElement.type).toBe("text");
|
||||||
expect(textElement.containerId).toBe(arrow.id);
|
expect(textElement.containerId).toBe(arrow.id);
|
||||||
const editor = document.querySelector(
|
const editor = await getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
fireEvent.change(editor, {
|
||||||
target: { value: DEFAULT_TEXT },
|
target: { value: DEFAULT_TEXT },
|
||||||
@@ -1067,13 +1134,7 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
mouse.doubleClickAt(line.x, line.y);
|
mouse.doubleClickAt(line.x, line.y);
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
expect(h.elements.length).toBe(2);
|
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
||||||
expect(text.type).toBe("text");
|
|
||||||
expect(text.containerId).toBeNull();
|
|
||||||
expect(line.boundElements).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO fix #7029 and rewrite this test
|
// TODO fix #7029 and rewrite this test
|
||||||
@@ -1238,9 +1299,7 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
mouse.select(arrow);
|
mouse.select(arrow);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = document.querySelector(
|
const editor = await getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
fireEvent.change(editor, { target: { value: DEFAULT_TEXT } });
|
fireEvent.change(editor, { target: { value: DEFAULT_TEXT } });
|
||||||
Keyboard.exitTextEditor(editor);
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
@@ -1266,12 +1325,12 @@ describe("Test Linear Elements", () => {
|
|||||||
mouse.downAt(rect.x, rect.y);
|
mouse.downAt(rect.x, rect.y);
|
||||||
mouse.moveTo(200, 0);
|
mouse.moveTo(200, 0);
|
||||||
mouse.upAt(200, 0);
|
mouse.upAt(200, 0);
|
||||||
expect(arrow.width).toBeCloseTo(204, 0);
|
expect(arrow.width).toBeCloseTo(200, 0);
|
||||||
expect(rect.x).toBe(200);
|
expect(rect.x).toBe(200);
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||||
h.elements[0],
|
h.elements[0],
|
||||||
arrayToMap(h.elements),
|
h.app.scene,
|
||||||
"nw",
|
"nw",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -1384,19 +1443,30 @@ describe("Test Linear Elements", () => {
|
|||||||
const [origStartX, origStartY] = [line.x, line.y];
|
const [origStartX, origStartY] = [line.x, line.y];
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
LinearElementEditor.movePoints(line, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
line,
|
||||||
index: 0,
|
h.app.scene,
|
||||||
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
|
new Map([
|
||||||
},
|
[
|
||||||
{
|
0,
|
||||||
index: line.points.length - 1,
|
{
|
||||||
point: pointFrom(
|
point: pointFrom(
|
||||||
line.points[line.points.length - 1][0] - 10,
|
line.points[0][0] + 10,
|
||||||
line.points[line.points.length - 1][1] - 10,
|
line.points[0][1] + 10,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]);
|
],
|
||||||
|
[
|
||||||
|
line.points.length - 1,
|
||||||
|
{
|
||||||
|
point: pointFrom(
|
||||||
|
line.points[line.points.length - 1][0] - 10,
|
||||||
|
line.points[line.points.length - 1][1] - 10,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(line.x).toBe(origStartX + 10);
|
expect(line.x).toBe(origStartX + 10);
|
||||||
expect(line.y).toBe(origStartY + 10);
|
expect(line.y).toBe(origStartY + 10);
|
||||||
@@ -1404,5 +1474,55 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(line.points[line.points.length - 1][0]).toBe(20);
|
expect(line.points[line.points.length - 1][0]).toBe(20);
|
||||||
expect(line.points[line.points.length - 1][1]).toBe(-20);
|
expect(line.points[line.points.length - 1][1]).toBe(-20);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should preserve original angle when dragging endpoint with SHIFT key", () => {
|
||||||
|
createTwoPointerLinearElement("line");
|
||||||
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
enterLineEditingMode(line);
|
||||||
|
|
||||||
|
const elementsMap = arrayToMap(h.elements);
|
||||||
|
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
|
line,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate original angle between first and last point
|
||||||
|
const originalAngle = Math.atan2(
|
||||||
|
points[1][1] - points[0][1],
|
||||||
|
points[1][0] - points[0][0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drag the second point (endpoint) with SHIFT key pressed
|
||||||
|
const startPoint = pointFrom<GlobalPoint>(points[1][0], points[1][1]);
|
||||||
|
const endPoint = pointFrom<GlobalPoint>(
|
||||||
|
startPoint[0] + 4,
|
||||||
|
startPoint[1] + 4,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perform drag with SHIFT key modifier
|
||||||
|
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||||
|
mouse.downAt(startPoint[0], startPoint[1]);
|
||||||
|
mouse.moveTo(endPoint[0], endPoint[1]);
|
||||||
|
mouse.upAt(endPoint[0], endPoint[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get updated points after drag
|
||||||
|
const updatedPoints = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
|
line,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate new angle
|
||||||
|
const newAngle = Math.atan2(
|
||||||
|
updatedPoints[1][1] - updatedPoints[0][1],
|
||||||
|
updatedPoints[1][0] - updatedPoints[0][0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// The angle should be preserved (within a small tolerance for floating point precision)
|
||||||
|
const angleDifference = Math.abs(newAngle - originalAngle);
|
||||||
|
const tolerance = 0.01; // Small tolerance for floating point precision
|
||||||
|
|
||||||
|
expect(angleDifference).toBeLessThan(tolerance);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -333,7 +333,7 @@ describe("line element", () => {
|
|||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"ne",
|
"ne",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ describe("line element", () => {
|
|||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"se",
|
"se",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -424,7 +424,7 @@ describe("line element", () => {
|
|||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"e",
|
"e",
|
||||||
{
|
{
|
||||||
shouldResizeFromCenter: true,
|
shouldResizeFromCenter: true,
|
||||||
@@ -510,12 +510,12 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize(rectangle, "se", [-200, -150]);
|
UI.resize(rectangle, "se", [-200, -150]);
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -538,11 +538,11 @@ describe("arrow element", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawElbowArrowElement;
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.05);
|
||||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -819,7 +819,7 @@ describe("image element", () => {
|
|||||||
|
|
||||||
UI.resize(image, "ne", [40, 0]);
|
UI.resize(image, "ne", [40, 0]);
|
||||||
|
|
||||||
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0);
|
expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0);
|
||||||
|
|
||||||
const imageWidth = image.width;
|
const imageWidth = image.width;
|
||||||
const scale = 20 / image.height;
|
const scale = 20 / image.height;
|
||||||
@@ -1033,7 +1033,7 @@ describe("multiple selection", () => {
|
|||||||
|
|
||||||
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
expect(leftBoundArrow.x).toBeCloseTo(-110);
|
||||||
expect(leftBoundArrow.y).toBeCloseTo(50);
|
expect(leftBoundArrow.y).toBeCloseTo(50);
|
||||||
expect(leftBoundArrow.width).toBeCloseTo(143, 0);
|
expect(leftBoundArrow.width).toBeCloseTo(140, 0);
|
||||||
expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
expect(leftBoundArrow.height).toBeCloseTo(7, 0);
|
||||||
expect(leftBoundArrow.angle).toEqual(0);
|
expect(leftBoundArrow.angle).toEqual(0);
|
||||||
expect(leftBoundArrow.startBinding).toBeNull();
|
expect(leftBoundArrow.startBinding).toBeNull();
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
import * as constants from "@excalidraw/common";
|
|
||||||
|
|
||||||
import { getPerfectElementSize } from "../src/sizeHelpers";
|
import { getPerfectElementSize } from "../src/sizeHelpers";
|
||||||
|
|
||||||
const EPSILON_DIGITS = 3;
|
const EPSILON_DIGITS = 3;
|
||||||
@@ -57,13 +55,4 @@ describe("getPerfectElementSize", () => {
|
|||||||
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should respond to SHIFT_LOCKING_ANGLE constant", () => {
|
|
||||||
it("should have only 2 locking angles per section if SHIFT_LOCKING_ANGLE = 45 deg (Math.PI/4)", () => {
|
|
||||||
(constants as any).SHIFT_LOCKING_ANGLE = Math.PI / 4;
|
|
||||||
const { height, width } = getPerfectElementSize("arrow", 120, 185);
|
|
||||||
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
|
|
||||||
expect(height).toBeCloseTo(120, EPSILON_DIGITS);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import { mutateElement } from "../src/mutateElement";
|
import { mutateElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import { normalizeElementOrder } from "../src/sortElements";
|
import { normalizeElementOrder } from "../src/sortElements";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "../src/types";
|
import type { ExcalidrawElement } from "../src/types";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
const assertOrder = (
|
const assertOrder = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
expectedOrder: string[],
|
expectedOrder: string[],
|
||||||
@@ -35,7 +37,7 @@ describe("normalizeElementsOrder", () => {
|
|||||||
boundElements: [],
|
boundElements: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(container, {
|
mutateElement(container, new Map(), {
|
||||||
boundElements: [{ type: "text", id: boundText.id }],
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -352,7 +354,7 @@ describe("normalizeElementsOrder", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [
|
boundElements: [
|
||||||
{ type: "text", id: boundText.id },
|
{ type: "text", id: boundText.id },
|
||||||
{ type: "text", id: "xxx" },
|
{ type: "text", id: "xxx" },
|
||||||
@@ -387,7 +389,7 @@ describe("normalizeElementsOrder", () => {
|
|||||||
boundElements: [],
|
boundElements: [],
|
||||||
groupIds: ["C", "A"],
|
groupIds: ["C", "A"],
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [{ type: "text", id: boundText.id }],
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { LIBRARY_DISABLED_TYPES, randomId } from "@excalidraw/common";
|
import { LIBRARY_DISABLED_TYPES, randomId } from "@excalidraw/common";
|
||||||
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
import { deepCopyElement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { isFrameLikeElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
|
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import { KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
import { KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
||||||
|
|
||||||
import { alignElements } from "@excalidraw/element/align";
|
import { alignElements } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Alignment } from "@excalidraw/element/align";
|
import type { Alignment } from "@excalidraw/element";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +27,6 @@ import {
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -50,14 +51,8 @@ const alignSelectedElements = (
|
|||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
const elementsMap = arrayToMap(elements);
|
|
||||||
|
|
||||||
const updatedElements = alignElements(
|
const updatedElements = alignElements(selectedElements, alignment, app.scene);
|
||||||
selectedElements,
|
|
||||||
elementsMap,
|
|
||||||
alignment,
|
|
||||||
app.scene,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import {
|
|||||||
getOriginalContainerHeightFromCache,
|
getOriginalContainerHeightFromCache,
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "@excalidraw/element/containerCache";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
} from "@excalidraw/element/textElement";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
@@ -25,14 +25,15 @@ import {
|
|||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
import { measureText } from "@excalidraw/element";
|
||||||
import { measureText } from "@excalidraw/element/textMeasurements";
|
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element";
|
||||||
|
|
||||||
import { newElement } from "@excalidraw/element/newElement";
|
import { newElement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -43,12 +44,10 @@ import type {
|
|||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "../store";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
import type { Radians } from "../../math/src";
|
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
|
|
||||||
export const actionUnbindText = register({
|
export const actionUnbindText = register({
|
||||||
@@ -80,7 +79,7 @@ export const actionUnbindText = register({
|
|||||||
boundTextElement,
|
boundTextElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
mutateElement(boundTextElement as ExcalidrawTextElement, {
|
app.scene.mutateElement(boundTextElement as ExcalidrawTextElement, {
|
||||||
containerId: null,
|
containerId: null,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -88,7 +87,7 @@ export const actionUnbindText = register({
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
boundElements: element.boundElements?.filter(
|
boundElements: element.boundElements?.filter(
|
||||||
(ele) => ele.id !== boundTextElement.id,
|
(ele) => ele.id !== boundTextElement.id,
|
||||||
),
|
),
|
||||||
@@ -153,25 +152,21 @@ export const actionBindText = register({
|
|||||||
textElement = selectedElements[1] as ExcalidrawTextElement;
|
textElement = selectedElements[1] as ExcalidrawTextElement;
|
||||||
container = selectedElements[0] as ExcalidrawTextContainer;
|
container = selectedElements[0] as ExcalidrawTextContainer;
|
||||||
}
|
}
|
||||||
mutateElement(textElement, {
|
app.scene.mutateElement(textElement, {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
app.scene.mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
type: "text",
|
type: "text",
|
||||||
id: textElement.id,
|
id: textElement.id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const originalContainerHeight = container.height;
|
const originalContainerHeight = container.height;
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(textElement, container, app.scene);
|
||||||
textElement,
|
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
// overwritting the cache with original container height so
|
// overwritting the cache with original container height so
|
||||||
// it can be restored when unbind
|
// it can be restored when unbind
|
||||||
updateOriginalContainerCache(container.id, originalContainerHeight);
|
updateOriginalContainerCache(container.id, originalContainerHeight);
|
||||||
@@ -301,27 +296,23 @@ export const actionWrapTextInContainer = register({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (startBinding || endBinding) {
|
if (startBinding || endBinding) {
|
||||||
mutateElement(ele, { startBinding, endBinding }, false);
|
app.scene.mutateElement(ele, {
|
||||||
|
startBinding,
|
||||||
|
endBinding,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(
|
app.scene.mutateElement(textElement, {
|
||||||
textElement,
|
containerId: container.id,
|
||||||
{
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
containerId: container.id,
|
boundElements: null,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
boundElements: null,
|
autoResize: true,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
});
|
||||||
autoResize: true,
|
|
||||||
},
|
redrawTextBoundingBox(textElement, container, app.scene);
|
||||||
false,
|
|
||||||
);
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
textElement,
|
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
updatedElements = pushContainerBelowText(
|
updatedElements = pushContainerBelowText(
|
||||||
[...updatedElements, container],
|
[...updatedElements, container],
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import {
|
|||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
import { getCommonBounds, type SceneBounds } from "@excalidraw/element/bounds";
|
import { getCommonBounds, type SceneBounds } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
@@ -44,7 +46,6 @@ import { t } from "../i18n";
|
|||||||
import { getNormalizedZoom } from "../scene";
|
import { getNormalizedZoom } from "../scene";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { isTextElement } from "@excalidraw/element/typeChecks";
|
import { isTextElement } from "@excalidraw/element";
|
||||||
import { getTextFromElements } from "@excalidraw/element/textElement";
|
import { getTextFromElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import { CODES, KEYS, isFirefox } from "@excalidraw/common";
|
import { CODES, KEYS, isFirefox } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
@@ -15,8 +17,6 @@ import { DuplicateIcon, cutIcon, pngIcon, svgIcon } from "../components/icons";
|
|||||||
import { exportCanvas, prepareElementsForExport } from "../data/index";
|
import { exportCanvas, prepareElementsForExport } from "../data/index";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
import { isImageElement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawImageElement } from "@excalidraw/element/types";
|
import type { ExcalidrawImageElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { cropIcon } from "../components/icons";
|
import { cropIcon } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Excalidraw, mutateElement } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { act, assertElements, render } from "../tests/test-utils";
|
import { act, assertElements, render } from "../tests/test-utils";
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ describe("deleting selected elements when frame selected should keep children +
|
|||||||
frameId: f1.id,
|
frameId: f1.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ describe("deleting selected elements when frame selected should keep children +
|
|||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ describe("deleting selected elements when frame selected should keep children +
|
|||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ describe("deleting selected elements when frame selected should keep children +
|
|||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(a1, {
|
h.app.scene.mutateElement(a1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
import { KEYS, updateActiveTool } from "@excalidraw/common";
|
import { KEYS, updateActiveTool } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import { fixBindingsAfterDeletion } from "@excalidraw/element/binding";
|
import { fixBindingsAfterDeletion } from "@excalidraw/element";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element";
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element";
|
||||||
mutateElement,
|
import { getContainerElement } from "@excalidraw/element";
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
|
||||||
import {
|
import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element";
|
||||||
import { getFrameChildren } from "@excalidraw/element/frame";
|
import { getFrameChildren } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getElementsInGroup,
|
getElementsInGroup,
|
||||||
selectGroupsForSelectedElements,
|
selectGroupsForSelectedElements,
|
||||||
} from "@excalidraw/element/groups";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
import { TrashIcon } from "../components/icons";
|
import { TrashIcon } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
|
||||||
@@ -94,7 +92,7 @@ const deleteSelectedElements = (
|
|||||||
el.boundElements.forEach((candidate) => {
|
el.boundElements.forEach((candidate) => {
|
||||||
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
||||||
if (bound && isElbowArrow(bound)) {
|
if (bound && isElbowArrow(bound)) {
|
||||||
mutateElement(bound, {
|
app.scene.mutateElement(bound, {
|
||||||
startBinding:
|
startBinding:
|
||||||
el.id === bound.startBinding?.elementId
|
el.id === bound.startBinding?.elementId
|
||||||
? null
|
? null
|
||||||
@@ -102,7 +100,6 @@ const deleteSelectedElements = (
|
|||||||
endBinding:
|
endBinding:
|
||||||
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
||||||
});
|
});
|
||||||
mutateElement(bound, { points: bound.points });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -261,7 +258,7 @@ export const actionDeleteSelected = register({
|
|||||||
: endBindingElement,
|
: endBindingElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
LinearElementEditor.deletePoints(element, selectedPointsIndices);
|
LinearElementEditor.deletePoints(element, app, selectedPointsIndices);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements,
|
elements,
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { isFrameLikeElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import { CODES, KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
import { CODES, KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
||||||
|
|
||||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
|
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import { distributeElements } from "@excalidraw/element/distribute";
|
import { distributeElements } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Distribution } from "@excalidraw/element/distribute";
|
import type { Distribution } from "@excalidraw/element";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import {
|
import {
|
||||||
@@ -21,7 +23,6 @@ import {
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -7,23 +7,24 @@ import {
|
|||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
getSelectionStateForElements,
|
getSelectionStateForElements,
|
||||||
} from "@excalidraw/element/selection";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element";
|
||||||
|
|
||||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
import { duplicateElements } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export const actionDuplicateSelection = register({
|
|||||||
try {
|
try {
|
||||||
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
||||||
appState,
|
appState,
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import {
|
|||||||
canCreateLinkFromElements,
|
canCreateLinkFromElements,
|
||||||
defaultGetElementLinkFromSelection,
|
defaultGetElementLinkFromSelection,
|
||||||
getLinkIdAndTypeFromSelection,
|
getLinkIdAndTypeFromSelection,
|
||||||
} from "@excalidraw/element/elementLink";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import { copyTextToSystemClipboard } from "../clipboard";
|
import { copyTextToSystemClipboard } from "../clipboard";
|
||||||
import { copyIcon, elementLinkIcon } from "../components/icons";
|
import { copyIcon, elementLinkIcon } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { KEYS, arrayToMap } from "@excalidraw/common";
|
import { KEYS, arrayToMap, randomId } from "@excalidraw/common";
|
||||||
|
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import {
|
||||||
|
elementsAreInSameGroup,
|
||||||
|
newElementWith,
|
||||||
|
selectGroupsFromGivenElements,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { LockedIcon, UnlockedIcon } from "../components/icons";
|
import { LockedIcon, UnlockedIcon } from "../components/icons";
|
||||||
|
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
import type { AppState } from "../types";
|
||||||
|
|
||||||
const shouldLock = (elements: readonly ExcalidrawElement[]) =>
|
const shouldLock = (elements: readonly ExcalidrawElement[]) =>
|
||||||
elements.every((el) => !el.locked);
|
elements.every((el) => !el.locked);
|
||||||
|
|
||||||
@@ -23,15 +28,10 @@ export const actionToggleElementLock = register({
|
|||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: false,
|
includeBoundTextElement: false,
|
||||||
});
|
});
|
||||||
if (selected.length === 1 && !isFrameLikeElement(selected[0])) {
|
|
||||||
return selected[0].locked
|
|
||||||
? "labels.elementLock.unlock"
|
|
||||||
: "labels.elementLock.lock";
|
|
||||||
}
|
|
||||||
|
|
||||||
return shouldLock(selected)
|
return shouldLock(selected)
|
||||||
? "labels.elementLock.lockAll"
|
? "labels.elementLock.lock"
|
||||||
: "labels.elementLock.unlockAll";
|
: "labels.elementLock.unlock";
|
||||||
},
|
},
|
||||||
icon: (appState, elements) => {
|
icon: (appState, elements) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
@@ -58,19 +58,84 @@ export const actionToggleElementLock = register({
|
|||||||
|
|
||||||
const nextLockState = shouldLock(selectedElements);
|
const nextLockState = shouldLock(selectedElements);
|
||||||
const selectedElementsMap = arrayToMap(selectedElements);
|
const selectedElementsMap = arrayToMap(selectedElements);
|
||||||
return {
|
|
||||||
elements: elements.map((element) => {
|
|
||||||
if (!selectedElementsMap.has(element.id)) {
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newElementWith(element, { locked: nextLockState });
|
const isAGroup =
|
||||||
}),
|
selectedElements.length > 1 && elementsAreInSameGroup(selectedElements);
|
||||||
|
const isASingleUnit = selectedElements.length === 1 || isAGroup;
|
||||||
|
const newGroupId = isASingleUnit ? null : randomId();
|
||||||
|
|
||||||
|
let nextLockedMultiSelections = { ...appState.lockedMultiSelections };
|
||||||
|
|
||||||
|
if (nextLockState) {
|
||||||
|
nextLockedMultiSelections = {
|
||||||
|
...appState.lockedMultiSelections,
|
||||||
|
...(newGroupId ? { [newGroupId]: true } : {}),
|
||||||
|
};
|
||||||
|
} else if (isAGroup) {
|
||||||
|
const groupId = selectedElements[0].groupIds.at(-1)!;
|
||||||
|
delete nextLockedMultiSelections[groupId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextElements = elements.map((element) => {
|
||||||
|
if (!selectedElementsMap.has(element.id)) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextGroupIds = element.groupIds;
|
||||||
|
|
||||||
|
// if locking together, add to group
|
||||||
|
// if unlocking, remove the temporary group
|
||||||
|
if (nextLockState) {
|
||||||
|
if (newGroupId) {
|
||||||
|
nextGroupIds = [...nextGroupIds, newGroupId];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextGroupIds = nextGroupIds.filter(
|
||||||
|
(groupId) => !appState.lockedMultiSelections[groupId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElementWith(element, {
|
||||||
|
locked: nextLockState,
|
||||||
|
// do not recreate the array unncessarily
|
||||||
|
groupIds:
|
||||||
|
nextGroupIds.length !== element.groupIds.length
|
||||||
|
? nextGroupIds
|
||||||
|
: element.groupIds,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextElementsMap = arrayToMap(nextElements);
|
||||||
|
const nextSelectedElementIds: AppState["selectedElementIds"] = nextLockState
|
||||||
|
? {}
|
||||||
|
: Object.fromEntries(selectedElements.map((el) => [el.id, true]));
|
||||||
|
const unlockedSelectedElements = selectedElements.map(
|
||||||
|
(el) => nextElementsMap.get(el.id) || el,
|
||||||
|
);
|
||||||
|
const nextSelectedGroupIds = nextLockState
|
||||||
|
? {}
|
||||||
|
: selectGroupsFromGivenElements(unlockedSelectedElements, appState);
|
||||||
|
|
||||||
|
const activeLockedId = nextLockState
|
||||||
|
? newGroupId
|
||||||
|
? newGroupId
|
||||||
|
: isAGroup
|
||||||
|
? selectedElements[0].groupIds.at(-1)!
|
||||||
|
: selectedElements[0].id
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: nextElements,
|
||||||
|
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
selectedElementIds: nextSelectedElementIds,
|
||||||
|
selectedGroupIds: nextSelectedGroupIds,
|
||||||
selectedLinearElement: nextLockState
|
selectedLinearElement: nextLockState
|
||||||
? null
|
? null
|
||||||
: appState.selectedLinearElement,
|
: appState.selectedLinearElement,
|
||||||
|
lockedMultiSelections: nextLockedMultiSelections,
|
||||||
|
activeLockedId,
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
@@ -103,18 +168,44 @@ export const actionUnlockAllElements = register({
|
|||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
const lockedElements = elements.filter((el) => el.locked);
|
const lockedElements = elements.filter((el) => el.locked);
|
||||||
|
|
||||||
|
const nextElements = elements.map((element) => {
|
||||||
|
if (element.locked) {
|
||||||
|
// remove the temporary groupId if it exists
|
||||||
|
const nextGroupIds = element.groupIds.filter(
|
||||||
|
(gid) => !appState.lockedMultiSelections[gid],
|
||||||
|
);
|
||||||
|
|
||||||
|
return newElementWith(element, {
|
||||||
|
locked: false,
|
||||||
|
groupIds:
|
||||||
|
// do not recreate the array unncessarily
|
||||||
|
element.groupIds.length !== nextGroupIds.length
|
||||||
|
? nextGroupIds
|
||||||
|
: element.groupIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextElementsMap = arrayToMap(nextElements);
|
||||||
|
|
||||||
|
const unlockedElements = lockedElements.map(
|
||||||
|
(el) => nextElementsMap.get(el.id) || el,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) => {
|
elements: nextElements,
|
||||||
if (element.locked) {
|
|
||||||
return newElementWith(element, { locked: false });
|
|
||||||
}
|
|
||||||
return element;
|
|
||||||
}),
|
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: Object.fromEntries(
|
selectedElementIds: Object.fromEntries(
|
||||||
lockedElements.map((el) => [el.id, true]),
|
lockedElements.map((el) => [el.id, true]),
|
||||||
),
|
),
|
||||||
|
selectedGroupIds: selectGroupsFromGivenElements(
|
||||||
|
unlockedElements,
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
lockedMultiSelections: {},
|
||||||
|
activeLockedId: null,
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { updateActiveTool } from "@excalidraw/common";
|
import { updateActiveTool } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import { setCursorForShape } from "../cursor";
|
import { setCursorForShape } from "../cursor";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { Theme } from "@excalidraw/element/types";
|
import type { Theme } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { useDevice } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
@@ -24,7 +26,6 @@ import { resaveAsImageWithScene } from "../data/resave";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { getExportSize } from "../scene/export";
|
import { getExportSize } from "../scene/export";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import "../components/ToolIcon.scss";
|
import "../components/ToolIcon.scss";
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,35 @@ import { pointFrom } from "@excalidraw/math";
|
|||||||
import {
|
import {
|
||||||
maybeBindLinearElement,
|
maybeBindLinearElement,
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
|
isBindingEnabled,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { isValidPolygon, LinearElementEditor } from "@excalidraw/element";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
|
isFreeDrawElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
isLineElement,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { KEYS, arrayToMap, updateActiveTool } from "@excalidraw/common";
|
import { KEYS, arrayToMap, updateActiveTool } from "@excalidraw/common";
|
||||||
import { isPathALoop } from "@excalidraw/element/shapes";
|
import { isPathALoop } from "@excalidraw/element";
|
||||||
|
|
||||||
import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
|
import { isInvisiblySmallElement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
NonDeleted,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { resetCursor } from "../cursor";
|
import { resetCursor } from "../cursor";
|
||||||
import { done } from "../components/icons";
|
import { done } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -30,11 +41,50 @@ export const actionFinalize = register({
|
|||||||
name: "finalize",
|
name: "finalize",
|
||||||
label: "",
|
label: "",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, data, app) => {
|
||||||
const { interactiveCanvas, focusContainer, scene } = app;
|
const { interactiveCanvas, focusContainer, scene } = app;
|
||||||
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
if (data?.event && appState.selectedLinearElement) {
|
||||||
|
const linearElementEditor = LinearElementEditor.handlePointerUp(
|
||||||
|
data.event,
|
||||||
|
appState.selectedLinearElement,
|
||||||
|
appState,
|
||||||
|
app.scene,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { startBindingElement, endBindingElement } = linearElementEditor;
|
||||||
|
const element = app.scene.getElement(linearElementEditor.elementId);
|
||||||
|
if (isBindingElement(element)) {
|
||||||
|
bindOrUnbindLinearElement(
|
||||||
|
element,
|
||||||
|
startBindingElement,
|
||||||
|
endBindingElement,
|
||||||
|
app.scene,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linearElementEditor !== appState.selectedLinearElement) {
|
||||||
|
let newElements = elements;
|
||||||
|
if (element && isInvisiblySmallElement(element)) {
|
||||||
|
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
||||||
|
newElements = newElements.filter((el) => el.id !== element!.id);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
elements: newElements,
|
||||||
|
appState: {
|
||||||
|
selectedLinearElement: {
|
||||||
|
...linearElementEditor,
|
||||||
|
selectedPointsIndices: null,
|
||||||
|
},
|
||||||
|
suggestedBindings: [],
|
||||||
|
},
|
||||||
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const { elementId, startBindingElement, endBindingElement } =
|
const { elementId, startBindingElement, endBindingElement } =
|
||||||
appState.editingLinearElement;
|
appState.editingLinearElement;
|
||||||
@@ -46,10 +96,15 @@ export const actionFinalize = register({
|
|||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isLineElement(element) && !isValidPolygon(element.points)) {
|
||||||
|
scene.mutateElement(element, {
|
||||||
|
polygon: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements:
|
elements:
|
||||||
element.points.length < 2 || isInvisiblySmallElement(element)
|
element.points.length < 2 || isInvisiblySmallElement(element)
|
||||||
@@ -67,93 +122,102 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
let newElements = elements;
|
let newElements = elements;
|
||||||
|
|
||||||
const pendingImageElement =
|
|
||||||
appState.pendingImageElementId &&
|
|
||||||
scene.getElement(appState.pendingImageElementId);
|
|
||||||
|
|
||||||
if (pendingImageElement) {
|
|
||||||
mutateElement(pendingImageElement, { isDeleted: true }, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
focusContainer();
|
focusContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
const multiPointElement = appState.multiElement
|
let element: NonDeleted<ExcalidrawElement> | null = null;
|
||||||
? appState.multiElement
|
if (appState.multiElement) {
|
||||||
: appState.newElement?.type === "freedraw"
|
element = appState.multiElement;
|
||||||
? appState.newElement
|
} else if (
|
||||||
: null;
|
appState.newElement?.type === "freedraw" ||
|
||||||
|
isBindingElement(appState.newElement)
|
||||||
|
) {
|
||||||
|
element = appState.newElement;
|
||||||
|
} else if (Object.keys(appState.selectedElementIds).length === 1) {
|
||||||
|
const candidate = elementsMap.get(
|
||||||
|
Object.keys(appState.selectedElementIds)[0],
|
||||||
|
) as NonDeleted<ExcalidrawLinearElement> | undefined;
|
||||||
|
if (candidate) {
|
||||||
|
element = candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (multiPointElement) {
|
if (element) {
|
||||||
// pen and mouse have hover
|
// pen and mouse have hover
|
||||||
if (
|
if (
|
||||||
multiPointElement.type !== "freedraw" &&
|
appState.multiElement &&
|
||||||
|
element.type !== "freedraw" &&
|
||||||
appState.lastPointerDownWith !== "touch"
|
appState.lastPointerDownWith !== "touch"
|
||||||
) {
|
) {
|
||||||
const { points, lastCommittedPoint } = multiPointElement;
|
const { points, lastCommittedPoint } = element;
|
||||||
if (
|
if (
|
||||||
!lastCommittedPoint ||
|
!lastCommittedPoint ||
|
||||||
points[points.length - 1] !== lastCommittedPoint
|
points[points.length - 1] !== lastCommittedPoint
|
||||||
) {
|
) {
|
||||||
mutateElement(multiPointElement, {
|
scene.mutateElement(element, {
|
||||||
points: multiPointElement.points.slice(0, -1),
|
points: element.points.slice(0, -1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInvisiblySmallElement(multiPointElement)) {
|
if (element && isInvisiblySmallElement(element)) {
|
||||||
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
||||||
newElements = newElements.filter(
|
newElements = newElements.filter((el) => el.id !== element!.id);
|
||||||
(el) => el.id !== multiPointElement.id,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the multi point line closes the loop,
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
// set the last point to first point.
|
// If the multi point line closes the loop,
|
||||||
// This ensures that loop remains closed at different scales.
|
// set the last point to first point.
|
||||||
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
|
// This ensures that loop remains closed at different scales.
|
||||||
if (
|
const isLoop = isPathALoop(element.points, appState.zoom.value);
|
||||||
multiPointElement.type === "line" ||
|
|
||||||
multiPointElement.type === "freedraw"
|
if (isLoop && (isLineElement(element) || isFreeDrawElement(element))) {
|
||||||
) {
|
const linePoints = element.points;
|
||||||
if (isLoop) {
|
|
||||||
const linePoints = multiPointElement.points;
|
|
||||||
const firstPoint = linePoints[0];
|
const firstPoint = linePoints[0];
|
||||||
mutateElement(multiPointElement, {
|
const points: LocalPoint[] = linePoints.map((p, index) =>
|
||||||
points: linePoints.map((p, index) =>
|
index === linePoints.length - 1
|
||||||
index === linePoints.length - 1
|
? pointFrom(firstPoint[0], firstPoint[1])
|
||||||
? pointFrom(firstPoint[0], firstPoint[1])
|
: p,
|
||||||
: p,
|
);
|
||||||
),
|
if (isLineElement(element)) {
|
||||||
|
scene.mutateElement(element, {
|
||||||
|
points,
|
||||||
|
polygon: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
scene.mutateElement(element, {
|
||||||
|
points,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLineElement(element) && !isValidPolygon(element.points)) {
|
||||||
|
scene.mutateElement(element, {
|
||||||
|
polygon: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isBindingElement(multiPointElement) &&
|
isBindingElement(element) &&
|
||||||
!isLoop &&
|
!isLoop &&
|
||||||
multiPointElement.points.length > 1
|
element.points.length > 1 &&
|
||||||
) {
|
isBindingEnabled(appState)
|
||||||
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
) {
|
||||||
multiPointElement,
|
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
-1,
|
element,
|
||||||
arrayToMap(elements),
|
-1,
|
||||||
);
|
arrayToMap(elements),
|
||||||
maybeBindLinearElement(
|
);
|
||||||
multiPointElement,
|
maybeBindLinearElement(element, appState, { x, y }, scene);
|
||||||
appState,
|
}
|
||||||
{ x, y },
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!appState.activeTool.locked &&
|
(!appState.activeTool.locked &&
|
||||||
appState.activeTool.type !== "freedraw") ||
|
appState.activeTool.type !== "freedraw") ||
|
||||||
!multiPointElement
|
!element
|
||||||
) {
|
) {
|
||||||
resetCursor(interactiveCanvas);
|
resetCursor(interactiveCanvas);
|
||||||
}
|
}
|
||||||
@@ -180,7 +244,7 @@ export const actionFinalize = register({
|
|||||||
activeTool:
|
activeTool:
|
||||||
(appState.activeTool.locked ||
|
(appState.activeTool.locked ||
|
||||||
appState.activeTool.type === "freedraw") &&
|
appState.activeTool.type === "freedraw") &&
|
||||||
multiPointElement
|
element
|
||||||
? appState.activeTool
|
? appState.activeTool
|
||||||
: activeTool,
|
: activeTool,
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
@@ -191,20 +255,19 @@ export const actionFinalize = register({
|
|||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
selectedElementIds:
|
selectedElementIds:
|
||||||
multiPointElement &&
|
element &&
|
||||||
!appState.activeTool.locked &&
|
!appState.activeTool.locked &&
|
||||||
appState.activeTool.type !== "freedraw"
|
appState.activeTool.type !== "freedraw"
|
||||||
? {
|
? {
|
||||||
...appState.selectedElementIds,
|
...appState.selectedElementIds,
|
||||||
[multiPointElement.id]: true,
|
[element.id]: true,
|
||||||
}
|
}
|
||||||
: appState.selectedElementIds,
|
: appState.selectedElementIds,
|
||||||
// To select the linear element when user has finished mutipoint editing
|
// To select the linear element when user has finished mutipoint editing
|
||||||
selectedLinearElement:
|
selectedLinearElement:
|
||||||
multiPointElement && isLinearElement(multiPointElement)
|
element && isLinearElement(element)
|
||||||
? new LinearElementEditor(multiPointElement)
|
? new LinearElementEditor(element, arrayToMap(newElements))
|
||||||
: appState.selectedLinearElement,
|
: appState.selectedLinearElement,
|
||||||
pendingImageElementId: null,
|
|
||||||
},
|
},
|
||||||
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
|
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
|||||||
@@ -2,22 +2,21 @@ import { getNonDeletedElements } from "@excalidraw/element";
|
|||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElements,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element";
|
||||||
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
import { getCommonBoundingBox } from "@excalidraw/element";
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element";
|
||||||
mutateElement,
|
import { deepCopyElement } from "@excalidraw/element";
|
||||||
newElementWith,
|
import { resizeMultipleElements } from "@excalidraw/element";
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
|
||||||
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element";
|
||||||
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
|
import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element";
|
||||||
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
@@ -27,7 +26,6 @@ import type {
|
|||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { flipHorizontal, flipVertical } from "../components/icons";
|
import { flipHorizontal, flipVertical } from "../components/icons";
|
||||||
|
|
||||||
@@ -162,11 +160,9 @@ const flipElements = (
|
|||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isLinearElement),
|
||||||
elementsMap,
|
|
||||||
app.scene.getNonDeletedElements(),
|
|
||||||
app.scene,
|
|
||||||
isBindingEnabled(appState),
|
isBindingEnabled(appState),
|
||||||
[],
|
[],
|
||||||
|
app.scene,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -194,13 +190,13 @@ const flipElements = (
|
|||||||
getCommonBoundingBox(selectedElements);
|
getCommonBoundingBox(selectedElements);
|
||||||
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||||
otherElements.forEach((element) =>
|
otherElements.forEach((element) =>
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
x: element.x + diffX,
|
x: element.x + diffX,
|
||||||
y: element.y + diffY,
|
y: element.y + diffY,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
elbowArrows.forEach((element) =>
|
elbowArrows.forEach((element) =>
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
x: element.x + diffX,
|
x: element.x + diffX,
|
||||||
y: element.y + diffY,
|
y: element.y + diffY,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
import { mutateElement } from "@excalidraw/element";
|
||||||
import { newFrameElement } from "@excalidraw/element/newElement";
|
import { newFrameElement } from "@excalidraw/element";
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { isFrameLikeElement } from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
addElementsToFrame,
|
addElementsToFrame,
|
||||||
removeAllElementsFromFrame,
|
removeAllElementsFromFrame,
|
||||||
} from "@excalidraw/element/frame";
|
} from "@excalidraw/element";
|
||||||
import { getFrameChildren } from "@excalidraw/element/frame";
|
import { getFrameChildren } from "@excalidraw/element";
|
||||||
|
|
||||||
import { KEYS, updateActiveTool } from "@excalidraw/common";
|
import { KEYS, updateActiveTool } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getElementsInGroup } from "@excalidraw/element/groups";
|
import { getElementsInGroup } from "@excalidraw/element";
|
||||||
|
|
||||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
import { getCommonBounds } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { setCursorForShape } from "../cursor";
|
import { setCursorForShape } from "../cursor";
|
||||||
import { frameToolIcon } from "../components/icons";
|
import { frameToolIcon } from "../components/icons";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -173,11 +174,9 @@ export const actionWrapSelectionInFrame = register({
|
|||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements, elementsMap);
|
||||||
selectedElements,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
const PADDING = 16;
|
const PADDING = 16;
|
||||||
const frame = newFrameElement({
|
const frame = newFrameElement({
|
||||||
x: x1 - PADDING,
|
x: x1 - PADDING,
|
||||||
@@ -196,13 +195,9 @@ export const actionWrapSelectionInFrame = register({
|
|||||||
for (const elementInGroup of elementsInGroup) {
|
for (const elementInGroup of elementsInGroup) {
|
||||||
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
||||||
|
|
||||||
mutateElement(
|
mutateElement(elementInGroup, elementsMap, {
|
||||||
elementInGroup,
|
groupIds: elementInGroup.groupIds.slice(0, index),
|
||||||
{
|
});
|
||||||
groupIds: elementInGroup.groupIds.slice(0, index),
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
import { newElementWith } from "@excalidraw/element";
|
||||||
|
|
||||||
import { isBoundToContainer } from "@excalidraw/element/typeChecks";
|
import { isBoundToContainer } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
frameAndChildrenSelectedTogether,
|
frameAndChildrenSelectedTogether,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
groupByFrameLikes,
|
groupByFrameLikes,
|
||||||
removeElementsFromFrame,
|
removeElementsFromFrame,
|
||||||
replaceAllElementsInFrame,
|
replaceAllElementsInFrame,
|
||||||
} from "@excalidraw/element/frame";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { KEYS, randomId, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
import { KEYS, randomId, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
||||||
|
|
||||||
@@ -24,9 +24,11 @@ import {
|
|||||||
addToGroup,
|
addToGroup,
|
||||||
removeFromSelectedGroups,
|
removeFromSelectedGroups,
|
||||||
isElementInGroup,
|
isElementInGroup,
|
||||||
} from "@excalidraw/element/groups";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
@@ -40,7 +42,6 @@ import { UngroupIcon, GroupIcon } from "../components/icons";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common";
|
import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { orderByFractionalIndex } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { SceneElementsMap } from "@excalidraw/element/types";
|
import type { SceneElementsMap } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
@@ -7,10 +11,8 @@ import { UndoIcon, RedoIcon } from "../components/icons";
|
|||||||
import { HistoryChangedEvent } from "../history";
|
import { HistoryChangedEvent } from "../history";
|
||||||
import { useEmitter } from "../hooks/useEmitter";
|
import { useEmitter } from "../hooks/useEmitter";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import type { History } from "../history";
|
import type { History } from "../history";
|
||||||
import type { Store } from "../store";
|
|
||||||
import type { AppClassProperties, AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
import type { Action, ActionResult } from "./types";
|
import type { Action, ActionResult } from "./types";
|
||||||
|
|
||||||
@@ -35,7 +37,11 @@ const executeHistoryAction = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [nextElementsMap, nextAppState] = result;
|
const [nextElementsMap, nextAppState] = result;
|
||||||
const nextElements = Array.from(nextElementsMap.values());
|
|
||||||
|
// order by fractional indices in case the map was accidently modified in the meantime
|
||||||
|
const nextElements = orderByFractionalIndex(
|
||||||
|
Array.from(nextElementsMap.values()),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: nextAppState,
|
appState: nextAppState,
|
||||||
@@ -47,9 +53,9 @@ const executeHistoryAction = (
|
|||||||
return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
|
return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
|
||||||
};
|
};
|
||||||
|
|
||||||
type ActionCreator = (history: History, store: Store) => Action;
|
type ActionCreator = (history: History) => Action;
|
||||||
|
|
||||||
export const createUndoAction: ActionCreator = (history, store) => ({
|
export const createUndoAction: ActionCreator = (history) => ({
|
||||||
name: "undo",
|
name: "undo",
|
||||||
label: "buttons.undo",
|
label: "buttons.undo",
|
||||||
icon: UndoIcon,
|
icon: UndoIcon,
|
||||||
@@ -57,11 +63,7 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
|||||||
viewMode: false,
|
viewMode: false,
|
||||||
perform: (elements, appState, value, app) =>
|
perform: (elements, appState, value, app) =>
|
||||||
executeHistoryAction(app, appState, () =>
|
executeHistoryAction(app, appState, () =>
|
||||||
history.undo(
|
history.undo(arrayToMap(elements) as SceneElementsMap, appState),
|
||||||
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
|
|
||||||
appState,
|
|
||||||
store.snapshot,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && matchKey(event, KEYS.Z) && !event.shiftKey,
|
event[KEYS.CTRL_OR_CMD] && matchKey(event, KEYS.Z) && !event.shiftKey,
|
||||||
@@ -88,19 +90,15 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createRedoAction: ActionCreator = (history, store) => ({
|
export const createRedoAction: ActionCreator = (history) => ({
|
||||||
name: "redo",
|
name: "redo",
|
||||||
label: "buttons.redo",
|
label: "buttons.redo",
|
||||||
icon: RedoIcon,
|
icon: RedoIcon,
|
||||||
trackEvent: { category: "history" },
|
trackEvent: { category: "history" },
|
||||||
viewMode: false,
|
viewMode: false,
|
||||||
perform: (elements, appState, _, app) =>
|
perform: (elements, appState, __, app) =>
|
||||||
executeHistoryAction(app, appState, () =>
|
executeHistoryAction(app, appState, () =>
|
||||||
history.redo(
|
history.redo(arrayToMap(elements) as SceneElementsMap, appState),
|
||||||
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
|
|
||||||
appState,
|
|
||||||
store.snapshot,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
(event[KEYS.CTRL_OR_CMD] && event.shiftKey && matchKey(event, KEYS.Z)) ||
|
(event[KEYS.CTRL_OR_CMD] && event.shiftKey && matchKey(event, KEYS.Z)) ||
|
||||||
|
|||||||
@@ -1,15 +1,28 @@
|
|||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element";
|
||||||
|
import {
|
||||||
|
isElbowArrow,
|
||||||
|
isLinearElement,
|
||||||
|
isLineElement,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
|
import type {
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawLineElement,
|
||||||
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { lineEditorIcon } from "../components/icons";
|
import { lineEditorIcon, polygonIcon } from "../components/icons";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
import { ButtonIcon } from "../components/ButtonIcon";
|
||||||
|
|
||||||
|
import { newElementWith } from "../../element/src/mutateElement";
|
||||||
|
|
||||||
|
import { toggleLinePolygonState } from "../../element/src/shapes";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
@@ -50,7 +63,7 @@ export const actionToggleLinearEditor = register({
|
|||||||
const editingLinearElement =
|
const editingLinearElement =
|
||||||
appState.editingLinearElement?.elementId === selectedElement.id
|
appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
? null
|
? null
|
||||||
: new LinearElementEditor(selectedElement);
|
: new LinearElementEditor(selectedElement, arrayToMap(elements));
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
@@ -80,3 +93,110 @@ export const actionToggleLinearEditor = register({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionTogglePolygon = register({
|
||||||
|
name: "togglePolygon",
|
||||||
|
category: DEFAULT_CATEGORIES.elements,
|
||||||
|
icon: polygonIcon,
|
||||||
|
keywords: ["loop"],
|
||||||
|
label: (elements, appState, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allPolygons = !selectedElements.some(
|
||||||
|
(element) => !isLineElement(element) || !element.polygon,
|
||||||
|
);
|
||||||
|
|
||||||
|
return allPolygons
|
||||||
|
? "labels.polygon.breakPolygon"
|
||||||
|
: "labels.polygon.convertToPolygon";
|
||||||
|
},
|
||||||
|
trackEvent: {
|
||||||
|
category: "element",
|
||||||
|
},
|
||||||
|
predicate: (elements, appState, _, app) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedElements.length > 0 &&
|
||||||
|
selectedElements.every(
|
||||||
|
(element) => isLineElement(element) && element.points.length >= 4,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
perform(elements, appState, _, app) {
|
||||||
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
|
if (selectedElements.some((element) => !isLineElement(element))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetElements = selectedElements as ExcalidrawLineElement[];
|
||||||
|
|
||||||
|
// if one element not a polygon, convert all to polygon
|
||||||
|
const nextPolygonState = targetElements.some((element) => !element.polygon);
|
||||||
|
|
||||||
|
const targetElementsMap = arrayToMap(targetElements);
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: elements.map((element) => {
|
||||||
|
if (!targetElementsMap.has(element.id) || !isLineElement(element)) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElementWith(element, {
|
||||||
|
backgroundColor: nextPolygonState
|
||||||
|
? element.backgroundColor
|
||||||
|
: "transparent",
|
||||||
|
...toggleLinePolygonState(element, nextPolygonState),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
appState,
|
||||||
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ appState, updateData, app }) => {
|
||||||
|
const selectedElements = app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedElements.length === 0 ||
|
||||||
|
selectedElements.some(
|
||||||
|
(element) =>
|
||||||
|
!isLineElement(element) ||
|
||||||
|
// only show polygon button if every selected element is already
|
||||||
|
// a polygon, effectively showing this button only to allow for
|
||||||
|
// disabling the polygon state
|
||||||
|
!element.polygon ||
|
||||||
|
element.points.length < 3,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPolygon = selectedElements.every(
|
||||||
|
(element) => isLineElement(element) && element.polygon,
|
||||||
|
);
|
||||||
|
|
||||||
|
const label = t(
|
||||||
|
allPolygon
|
||||||
|
? "labels.polygon.breakPolygon"
|
||||||
|
: "labels.polygon.convertToPolygon",
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonIcon
|
||||||
|
icon={polygonIcon}
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
active={allPolygon}
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
style={{ marginLeft: "auto" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
import { isEmbeddableElement } from "@excalidraw/element";
|
||||||
|
|
||||||
import { KEYS, getShortcutKey } from "@excalidraw/common";
|
import { KEYS, getShortcutKey } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
|
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
|
||||||
import { LinkIcon } from "../components/icons";
|
import { LinkIcon } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { KEYS } from "@excalidraw/common";
|
|||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions";
|
import { showSelectedShapeActions } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { HamburgerMenuIcon, HelpIconThin, palette } from "../components/icons";
|
import { HamburgerMenuIcon, HelpIconThin, palette } from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
export const actionToggleCanvasMenu = register({
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "@excalidraw/element";
|
||||||
|
|
||||||
import { getClientColor } from "../clients";
|
import { getClientColor } from "../clients";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
import {
|
import {
|
||||||
@@ -8,7 +10,6 @@ import {
|
|||||||
microphoneMutedIcon,
|
microphoneMutedIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CaptureUpdateAction } from "../store";
|
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user