Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 966cd35b08 | |||
| 2e5bf3bb51 | |||
| 0346233358 | |||
| 9cc4f5b1d2 | |||
| 28292f4867 | |||
| 7ea3229e17 | |||
| b0404b10b6 | |||
| eb959128ac | |||
| 4c3d037f9c | |||
| 5852d0d410 | |||
| c1e00c44f5 | |||
| ffcb67b21f | |||
| 46ddd60948 | |||
| 89a9badc27 | |||
| 8b3e149db6 | |||
| a70417f23f | |||
| 1c8e8bb0f3 | |||
| 3a5ef4020d | |||
| 063533aede | |||
| b43260d97b | |||
| 83d3943cd0 | |||
| f39ac4a653 | |||
| 54a9826817 | |||
| d29fd62e41 | |||
| b57f3e0096 | |||
| f12ae80ba1 | |||
| f7b537a8b1 | |||
| 94364af68f | |||
| dfa1ce572b | |||
| b552c60714 | |||
| 216afc3625 | |||
| 802cde3501 | |||
| f5cf81ce42 | |||
| 63cd36e8ad | |||
| e1f6429e49 | |||
| 29ba7fe96d | |||
| be9981bda5 |
@@ -19,11 +19,6 @@
|
||||
"command": "yarn fix",
|
||||
"runAtStart": false
|
||||
},
|
||||
"prettier": {
|
||||
"name": "Prettify",
|
||||
"command": "yarn prettier",
|
||||
"runAtStart": false
|
||||
},
|
||||
"start": {
|
||||
"name": "Start Excalidraw",
|
||||
"command": "yarn start",
|
||||
|
||||
@@ -37,9 +37,6 @@ VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
|
||||
# Set this flag to false if you want to open the overlay by default
|
||||
VITE_APP_COLLAPSE_OVERLAY=true
|
||||
|
||||
# Set this flag to false to disable eslint
|
||||
VITE_APP_ENABLE_ESLINT=true
|
||||
|
||||
# Enable PWA in dev server
|
||||
VITE_APP_ENABLE_PWA=false
|
||||
|
||||
|
||||
@@ -29,6 +29,3 @@ PQIDAQAB'
|
||||
# Set the below flags explicitly to false in production mode since vite loads and merges .env.local vars when running the build command
|
||||
VITE_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=false
|
||||
VITE_APP_COLLAPSE_OVERLAY=false
|
||||
# Enable eslint in dev server
|
||||
VITE_APP_ENABLE_ESLINT=false
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
node_modules/
|
||||
build/
|
||||
package-lock.json
|
||||
.vscode/
|
||||
firebase/
|
||||
dist/
|
||||
public/workbox
|
||||
packages/excalidraw/types
|
||||
examples/**/public
|
||||
dev-dist
|
||||
coverage
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"extends": ["@excalidraw/eslint-config", "react-app"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@excalidraw/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
],
|
||||
"newlines-between": "always-and-inside-groups",
|
||||
"warnOnUnassignedImports": true
|
||||
}
|
||||
],
|
||||
"import/no-anonymous-default-export": "off",
|
||||
"no-restricted-globals": "off",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"prefer": "type-imports",
|
||||
"disallowTypeAnnotations": false,
|
||||
"fixStyle": "separate-type-imports"
|
||||
}
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"name": "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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -8,7 +8,9 @@
|
||||
.history
|
||||
.idea
|
||||
.vercel
|
||||
.vscode
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.recommended.json
|
||||
.yarn
|
||||
*.log
|
||||
*.tgz
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
const { CLIEngine } = require("eslint");
|
||||
|
||||
// see https://github.com/okonet/lint-staged#how-can-i-ignore-files-from-eslintignore-
|
||||
// for explanation
|
||||
const cli = new CLIEngine({});
|
||||
|
||||
module.exports = {
|
||||
"*.{js,ts,tsx}": files => {
|
||||
return (
|
||||
"eslint --max-warnings=0 --fix " + files.filter(file => !cli.isPathIgnored(file)).join(" ")
|
||||
);
|
||||
},
|
||||
"*.{css,scss,json,md,html,yml}": ["prettier --write"],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"proseWrap": "never",
|
||||
"trailingComma": "all"
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
|
||||
"plugins": ["typescript", "react", "jsx-a11y", "import"],
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
],
|
||||
"curly": "warn",
|
||||
"no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": ["info", "warn", "error"]
|
||||
}
|
||||
],
|
||||
"no-else-return": "warn",
|
||||
"no-lonely-if": "warn",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"no-unused-expressions": "warn",
|
||||
"no-useless-return": "warn",
|
||||
"no-var": "warn",
|
||||
"one-var": "warn",
|
||||
"prefer-arrow-callback": "warn",
|
||||
"prefer-const": "warn",
|
||||
"prefer-template": "warn",
|
||||
"typescript/consistent-type-imports": [
|
||||
"error",
|
||||
{
|
||||
"disallowTypeAnnotations": false
|
||||
}
|
||||
],
|
||||
"typescript/no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"../../excalidraw",
|
||||
"../../../packages/excalidraw",
|
||||
"@excalidraw/excalidraw"
|
||||
],
|
||||
"message": "Do not import from '@excalidraw/excalidraw' package anything but types, as this package must be independent.",
|
||||
"allowTypeImports": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"eslint/no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "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
|
||||
}
|
||||
],
|
||||
|
||||
"eslint/no-unreachable": "warn",
|
||||
|
||||
// react
|
||||
"react/jsx-no-comment-textnodes": "error",
|
||||
"react/iframe-missing-sandbox": "warn",
|
||||
"react/rules-of-hooks": "error",
|
||||
"react/no-unescaped-entities": "warn",
|
||||
|
||||
// for later
|
||||
// ----------
|
||||
// "react/no-array-index-key": "warn",
|
||||
// "react/jsx-no-useless-fragment": "warn",
|
||||
|
||||
// will require major refactor
|
||||
// ---------------------------
|
||||
// "react/only-export-components": "warn",
|
||||
|
||||
// type-aware rules (requires --type-aware flag)
|
||||
// -------------------------------------------------------------------------
|
||||
"typescript/switch-exhaustiveness-check": "warn",
|
||||
"typescript/unbound-method": [
|
||||
"warn",
|
||||
{
|
||||
"ignoreStatic": true
|
||||
}
|
||||
],
|
||||
|
||||
// disabled rules
|
||||
// -------------------------------------------------------------------------
|
||||
// may be re-enabled later
|
||||
"typescript/no-redundant-type-constituents": "off",
|
||||
"typescript/no-unsafe-unary-minus": "off",
|
||||
"typescript/no-floating-promises": "off",
|
||||
// not planned
|
||||
"eslint/no-async-promise-executor": "off",
|
||||
"jsx-a11y/no-autofocus": "off",
|
||||
"eslint-plugin-jsx-a11y/click-events-have-key-events": "off",
|
||||
"eslint-plugin-jsx-a11y/label-has-associated-control": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/",
|
||||
"build/",
|
||||
"dist/",
|
||||
".vscode/",
|
||||
"firebase/",
|
||||
"public/workbox",
|
||||
"packages/excalidraw/types",
|
||||
"examples/**/public",
|
||||
"dev-dist",
|
||||
"coverage"
|
||||
// "**/tests/**",
|
||||
// "**/*.test*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"packages/common/src/**/*.ts",
|
||||
"packages/common/src/**/*.tsx",
|
||||
"packages/element/src/**/*.ts",
|
||||
"packages/element/src/**/*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"typescript/no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"group": [
|
||||
"../../excalidraw",
|
||||
"../../../packages/excalidraw",
|
||||
"@excalidraw/excalidraw"
|
||||
],
|
||||
"message": "Do not import from '@excalidraw/excalidraw' package anything but types, as this package must be independent.",
|
||||
"allowTypeImports": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["oxc.oxc-vscode"]
|
||||
}
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"oxc.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,11 @@ import { FONT_FAMILY } from "@excalidraw/excalidraw";
|
||||
|
||||
`FONT_FAMILY` contains all the font families used in `Excalidraw`. The default families are the following:
|
||||
|
||||
| Font Family | Description |
|
||||
| ----------- | ---------------------- |
|
||||
| `Excalifont` | The `Hand-drawn` font |
|
||||
| `Nunito` | The `Normal` Font |
|
||||
| `Comic Shanns` | The `Code` Font |
|
||||
| Font Family | Description |
|
||||
| -------------- | --------------------- |
|
||||
| `Excalifont` | The `Hand-drawn` font |
|
||||
| `Nunito` | The `Normal` Font |
|
||||
| `Comic Shanns` | The `Code` Font |
|
||||
|
||||
Pre-selected family is `FONT_FAMILY.Excalifont`, unless it's overriden with `initialData.appState.currentItemFontFamily`.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Once the callback is triggered, you will need to store the api in state to acces
|
||||
```jsx showLineNumbers
|
||||
export default function App() {
|
||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||
return <Excalidraw excalidrawAPI={(api)=> setExcalidrawAPI(api)} />;
|
||||
return <Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -362,10 +362,9 @@ This API has the below signature. It sets the `tool` passed in param as the acti
|
||||
|
||||
```ts
|
||||
(
|
||||
tool: (
|
||||
| { type: ToolType }
|
||||
| { type: "custom"; customType: string }
|
||||
) & { locked?: boolean },
|
||||
tool: ({ type: ToolType } | { type: "custom"; customType: string }) & {
|
||||
locked?: boolean;
|
||||
},
|
||||
) => {};
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
# initialData
|
||||
|
||||
<pre>
|
||||
{ elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">AppState</a> }
|
||||
{ elements?:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>
|
||||
, appState?:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">
|
||||
AppState
|
||||
</a>{" "}
|
||||
}
|
||||
</pre>
|
||||
|
||||
This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields.
|
||||
@@ -46,7 +54,7 @@ function App() {
|
||||
},
|
||||
],
|
||||
appState: { zenModeEnabled: true, viewBackgroundColor: "#a5d8ff" },
|
||||
scrollToContent: true
|
||||
scrollToContent: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
All `props` are _optional_.
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` | `null` | <code>Promise<object | null></code> | `null` | The initial data with which app loads. |
|
||||
| [`excalidrawAPI`](/docs/@excalidraw/excalidraw/api/props/excalidraw-api) | `function` | \_ | Callback triggered with the excalidraw api once rendered |
|
||||
| [`isCollaborating`](#iscollaborating) | `boolean` | \_ | This indicates if the app is in `collaboration` mode |
|
||||
@@ -31,7 +31,7 @@ All `props` are _optional_.
|
||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
|
||||
| [`renderScrollbars`] | `boolean` | | `false` | Indicates whether scrollbars will be shown |
|
||||
|
||||
### Storing custom data on Excalidraw elements
|
||||
|
||||
@@ -247,7 +247,7 @@ This prop indicates whether to `focus` the Excalidraw component on page load. De
|
||||
Allows you to override `id` generation for files added on canvas (images). By default, an SHA-1 digest of the file is used.
|
||||
|
||||
```tsx
|
||||
(file: File) => string | Promise<string>
|
||||
(file: File) => string | Promise<string>;
|
||||
```
|
||||
|
||||
### validateEmbeddable
|
||||
|
||||
@@ -65,7 +65,7 @@ If user choses to `dock` the sidebar, it will push the right part of the UI towa
|
||||
function App() {
|
||||
return (
|
||||
<div style={{ height: "500px" }}>
|
||||
<Excalidraw UIOptions={{dockedSidebarBreakpoint: 200}}/>
|
||||
<Excalidraw UIOptions={{ dockedSidebarBreakpoint: 200 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -73,9 +73,8 @@ function App() {
|
||||
|
||||
## tools
|
||||
|
||||
This `prop` controls the visibility of the tools in the editor.
|
||||
Currently you can control the visibility of `image` tool via this prop.
|
||||
This `prop` controls the visibility of the tools in the editor. Currently you can control the visibility of `image` tool via this prop.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| image | boolean | true | Decides whether `image` tool should be visible.
|
||||
| Prop | Type | Default | Description |
|
||||
| ----- | ------- | ------- | ----------------------------------------------- |
|
||||
| image | boolean | true | Decides whether `image` tool should be visible. |
|
||||
|
||||
@@ -14,35 +14,44 @@ We're working on much improved export utilities. Stay tuned!
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
exportToCanvas({<br/>
|
||||
elements,<br/>
|
||||
appState<br/>
|
||||
getDimensions,<br/>
|
||||
files,<br/>
|
||||
exportPadding?: number;<br/>
|
||||
}: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/utils/export.ts#L24">ExportOpts</a>
|
||||
exportToCanvas({
|
||||
<br />
|
||||
elements,
|
||||
<br />
|
||||
appState
|
||||
<br />
|
||||
getDimensions,
|
||||
<br />
|
||||
files,
|
||||
<br />
|
||||
exportPadding?: number;
|
||||
<br />
|
||||
}:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/utils/export.ts#L24">
|
||||
ExportOpts
|
||||
</a>
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | | The elements to be exported to canvas. |
|
||||
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L17) | The app state of the scene. |
|
||||
| [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. |
|
||||
| `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59) | _ | The files added to the scene. |
|
||||
| [`getDimensions`](#getdimensions) | `function` | \_ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. |
|
||||
| `maxWidthOrHeight` | `number` | \_ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
|
||||
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59) | \_ | The files added to the scene. |
|
||||
| `exportPadding` | `number` | `10` | The `padding` to be added on canvas. |
|
||||
|
||||
|
||||
#### getDimensions
|
||||
|
||||
```tsx
|
||||
(width: number, height: number) => {
|
||||
(width: number, height: number) => {
|
||||
width: number,
|
||||
height: number,
|
||||
scale?: number
|
||||
height: number,
|
||||
scale?: number
|
||||
}
|
||||
```
|
||||
A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported.
|
||||
|
||||
A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported.
|
||||
|
||||
**How to use**
|
||||
|
||||
@@ -57,17 +66,17 @@ function App() {
|
||||
const [canvasUrl, setCanvasUrl] = useState("");
|
||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="custom-button"
|
||||
onClick={async () => {
|
||||
if (!excalidrawAPI) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const elements = excalidrawAPI.getSceneElements();
|
||||
if (!elements || !elements.length) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const canvas = await exportToCanvas({
|
||||
elements,
|
||||
@@ -76,7 +85,9 @@ function App() {
|
||||
exportWithDarkMode: false,
|
||||
},
|
||||
files: excalidrawAPI.getFiles(),
|
||||
getDimensions: () => { return {width: 350, height: 350}}
|
||||
getDimensions: () => {
|
||||
return { width: 350, height: 350 };
|
||||
},
|
||||
});
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.font = "30px Virgil";
|
||||
@@ -90,15 +101,13 @@ function App() {
|
||||
<img src={canvasUrl} alt="" />
|
||||
</div>
|
||||
<div style={{ height: "400px" }}>
|
||||
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||
/>
|
||||
<Excalidraw excalidrawAPI={(api) => setExcalidrawAPI(api)} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### exportToBlob
|
||||
|
||||
**_Signature_**
|
||||
@@ -114,7 +123,7 @@ exportToBlob(<br/>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `opts` | `object` | _ | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas) |
|
||||
| `opts` | `object` | \_ | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas) |
|
||||
| `mimeType` | `string` | `image/png` | Indicates the image format. |
|
||||
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. |
|
||||
| `exportPadding` | `number` | `10` | The padding to be added on canvas. |
|
||||
@@ -132,26 +141,34 @@ Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
exportToSvg({<br/>
|
||||
elements:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>,<br/>
|
||||
appState:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95"> AppState
|
||||
</a>,<br/>
|
||||
exportPadding: number,<br/>
|
||||
metadata: string,<br/>
|
||||
files:
|
||||
exportToSvg({
|
||||
<br />
|
||||
elements:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">
|
||||
ExcalidrawElement[]
|
||||
</a>
|
||||
,<br />
|
||||
appState:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95">
|
||||
{" "}
|
||||
AppState
|
||||
</a>
|
||||
,<br />
|
||||
exportPadding: number,
|
||||
<br />
|
||||
metadata: string,
|
||||
<br />
|
||||
files:
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L59">
|
||||
BinaryFiles
|
||||
</a>,<br/>
|
||||
});
|
||||
BinaryFiles
|
||||
</a>
|
||||
,<br />
|
||||
});
|
||||
</pre>
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | | The elements to exported as `svg `|
|
||||
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114) | | The elements to exported as `svg ` |
|
||||
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/appState.ts#L11) | The `appState` of the scene |
|
||||
| exportPadding | number | 10 | The `padding` to be added on canvas |
|
||||
| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L64) | undefined | The `files` added to the scene. |
|
||||
@@ -176,7 +193,7 @@ exportToClipboard(<br/>
|
||||
| `opts` | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas). |
|
||||
| `mimeType` | `string` | `image/png` | Indicates the image format, this will be used when exporting as `png`. |
|
||||
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg` / `image/webp` MIME types. This will be used when exporting as `png`. |
|
||||
| `type` | 'png' | 'svg' | 'json' | _ | This determines the format to which the scene data should be `exported`. |
|
||||
| `type` | 'png' | 'svg' | 'json' | \_ | This determines the format to which the scene data should be `exported`. |
|
||||
|
||||
**How to use**
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ import { restoreAppState } from "@excalidraw/excalidraw";
|
||||
This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L95) and if any key is missing, it will be set to its `default` value.
|
||||
|
||||
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of the defaults.
|
||||
Use this as a way to not override user's defaults if you persist them.
|
||||
You can pass `null` / `undefined` if not applicable.
|
||||
Use this as a way to not override user's defaults if you persist them. You can pass `null` / `undefined` if not applicable.
|
||||
|
||||
### restoreElements
|
||||
|
||||
@@ -36,10 +35,10 @@ restoreElements(
|
||||
</pre>
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ---- | ---- | ---- |
|
||||
| --- | --- | --- |
|
||||
| `elements` | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ImportedDataState["elements"]</a> | The `elements` to be restored |
|
||||
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
|
||||
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements
|
||||
| [`localElements`](#localelements) | <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined | When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`. |
|
||||
| [`opts`](#opts) | `Object` | The extra optional parameter to configure restored elements |
|
||||
|
||||
#### localElements
|
||||
|
||||
@@ -47,13 +46,14 @@ When `localElements` are supplied, they are used to ensure that existing restore
|
||||
Use this when you `import` elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the update
|
||||
|
||||
#### opts
|
||||
|
||||
The extra optional parameter to configure restored elements. It has the following attributes
|
||||
|
||||
| Prop | Type | Description|
|
||||
| --- | --- | ------|
|
||||
| Prop | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `refreshDimensions` | `boolean` | Indicates whether we should also _recalculate_ text element dimensions. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration. |
|
||||
| `repairBindings` |`boolean` | Indicates whether the _bindings_ for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
|
||||
| `normalizeIndices` |`boolean` | Indicates whether _fractional indices_ for the elements should be normalized. This is to prevent possible issues caused by using stale (too old, too long) indices. |
|
||||
| `repairBindings` | `boolean` | Indicates whether the _bindings_ for the elements should be repaired. This is to make sure there are no containers with non existent bound text element id and no bound text elements with non existent container id. |
|
||||
| `normalizeIndices` | `boolean` | Indicates whether _fractional indices_ for the elements should be normalized. This is to prevent possible issues caused by using stale (too old, too long) indices. |
|
||||
|
||||
**_How to use_**
|
||||
|
||||
@@ -94,8 +94,12 @@ This function makes sure elements and state is set to appropriate values and set
|
||||
**_Signature_**
|
||||
|
||||
<pre>
|
||||
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>
|
||||
defaultStatus: "published" | "unpublished")
|
||||
restoreLibraryItems(libraryItems:{" "}
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/types.ts#L34">
|
||||
ImportedDataState["libraryItems"]
|
||||
</a>
|
||||
,<br />
|
||||
defaultStatus: "published" | "unpublished")
|
||||
</pre>
|
||||
|
||||
**_How to use_**
|
||||
|
||||
@@ -38,6 +38,7 @@ For a complete list of variables, check [theme.scss](https://github.com/excalidr
|
||||
--color-primary-light: #dcbec9;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx live
|
||||
function App() {
|
||||
return (
|
||||
|
||||
@@ -23,7 +23,7 @@ To start the example app using the `@excalidraw/excalidraw` package, follow the
|
||||
```
|
||||
|
||||
[http://localhost:3001](http://localhost:3001) will open in your default browser.
|
||||
|
||||
|
||||
This is the same example as the [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/master/examples/with-script-in-browser) example.
|
||||
|
||||
## Releasing
|
||||
|
||||
@@ -6,21 +6,17 @@ No, Excalidraw package doesn't come with collaboration built in, since the imple
|
||||
|
||||
### Turning off Aggressive Anti-Fingerprinting in Brave browser
|
||||
|
||||
When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
|
||||
When _Aggressive Anti-Fingerprinting_ is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
|
||||
|
||||
We strongly recommend turning it off. You can follow the steps below on how to do so.
|
||||
|
||||
|
||||
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
|
||||

|
||||
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button 
|
||||
|
||||
<div style={{width:'30rem'}}>
|
||||
|
||||
2. Once opened, look for **Aggressively Block Fingerprinting**
|
||||

|
||||
2. Once opened, look for **Aggressively Block Fingerprinting** 
|
||||
|
||||
3. Switch to **Block Fingerprinting**
|
||||

|
||||
3. Switch to **Block Fingerprinting** 
|
||||
|
||||
4. Thats all. All text elements should be fixed now 🎉
|
||||
|
||||
@@ -28,7 +24,6 @@ We strongly recommend turning it off. You can follow the steps below on how to d
|
||||
|
||||
If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
|
||||
|
||||
|
||||
### ReferenceError: process is not defined
|
||||
|
||||
When using `vite` or any build tools, you will have to make sure the `process` is accessible as we are accessing `process.env.IS_PREACT` to decide whether to use `preact` build.
|
||||
|
||||
@@ -31,9 +31,7 @@ or, if you serve your assets from the root of your CDN, you would do:
|
||||
```js
|
||||
// Vanilla
|
||||
<head>
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = "https://my.cdn.com/assets/";
|
||||
</script>
|
||||
<script>window.EXCALIDRAW_ASSET_PATH = "https://my.cdn.com/assets/";</script>
|
||||
</head>
|
||||
```
|
||||
|
||||
@@ -41,8 +39,8 @@ or, if you prefer the path to be dynamicly set based on the `location.origin`, y
|
||||
|
||||
```jsx
|
||||
// Next.js
|
||||
<Script id="load-env-variables" strategy="beforeInteractive" >
|
||||
{ `window["EXCALIDRAW_ASSET_PATH"] = location.origin;` } // or use just "/"!
|
||||
<Script id="load-env-variables" strategy="beforeInteractive">
|
||||
{`window["EXCALIDRAW_ASSET_PATH"] = location.origin;`} // or use just "/"!
|
||||
</Script>
|
||||
```
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
|
||||
Throughout the documentation we use live, editable Excalidraw examples like the one shown below.
|
||||
|
||||
While we aim for the examples to closely reflect what you'd get if you rendered it yourself, we actually initialize it with some props behind the scenes.
|
||||
For example, we're passing a `theme` prop to it based on the current color theme of the docs you're just reading.
|
||||
While we aim for the examples to closely reflect what you'd get if you rendered it yourself, we actually initialize it with some props behind the scenes. For example, we're passing a `theme` prop to it based on the current color theme of the docs you're just reading.
|
||||
|
||||
:::
|
||||
|
||||
@@ -58,80 +57,76 @@ If you are using `pages router` then importing the wrapper dynamically would wor
|
||||
<Tabs>
|
||||
<TabItem value="Excalidraw Wrapper" label="Excalidraw Wrapper" >
|
||||
|
||||
```jsx showLineNumbers
|
||||
"use client";
|
||||
import { Excalidraw, convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||
```jsx showLineNumbers
|
||||
"use client";
|
||||
import { Excalidraw, convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
import "@excalidraw/excalidraw/index.css";
|
||||
|
||||
const ExcalidrawWrapper: React.FC = () => {
|
||||
console.info(convertToExcalidrawElements([{
|
||||
type: "rectangle",
|
||||
id: "rect-1",
|
||||
width: 186.47265625,
|
||||
height: 141.9765625,
|
||||
},]));
|
||||
return (
|
||||
<div style={{height:"500px", width:"500px"}}>
|
||||
<Excalidraw />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ExcalidrawWrapper;
|
||||
```
|
||||
const ExcalidrawWrapper: React.FC = () => {
|
||||
console.info(convertToExcalidrawElements([{
|
||||
type: "rectangle",
|
||||
id: "rect-1",
|
||||
width: 186.47265625,
|
||||
height: 141.9765625,
|
||||
},]));
|
||||
return (
|
||||
<div style={{height:"500px", width:"500px"}}>
|
||||
<Excalidraw />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ExcalidrawWrapper;
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="pages" label="Pages router">
|
||||
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing
|
||||
// the excalidraw stuff dynamically with ssr false
|
||||
// Since client components get prerenderd on server as well hence importing
|
||||
// the excalidraw stuff dynamically with ssr false
|
||||
|
||||
const ExcalidrawWrapper = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
const ExcalidrawWrapper = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return <ExcalidrawWrapper />;
|
||||
}
|
||||
```
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ExcalidrawWrapper />
|
||||
);
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="app" label="App router">
|
||||
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
```jsx showLineNumbers
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// Since client components get prerenderd on server as well hence importing
|
||||
// the excalidraw stuff dynamically with ssr false
|
||||
// Since client components get prerenderd on server as well hence importing
|
||||
// the excalidraw stuff dynamically with ssr false
|
||||
|
||||
const ExcalidrawWrapper = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
const ExcalidrawWrapper = dynamic(
|
||||
async () => (await import("../excalidrawWrapper")).default,
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ExcalidrawWrapper />
|
||||
);
|
||||
}
|
||||
```
|
||||
export default function Page() {
|
||||
return <ExcalidrawWrapper />;
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
{/* Link should be updated to point to the latest! */}
|
||||
Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs.vercel.app/).
|
||||
{/* Link should be updated to point to the latest! */} Here is a [source code](https://github.com/excalidraw/excalidraw/tree/master/examples/with-nextjs) for the example with app and pages router. You you can try it out [here](https://excalidraw-package-example-with-nextjs.vercel.app/).
|
||||
|
||||
The `types` are available at `@excalidraw/excalidraw/types`, check [CodeSandbox](https://codesandbox.io/p/sandbox/github/excalidraw/excalidraw/tree/master/examples/with-script-in-browser) example for details.
|
||||
|
||||
@@ -154,6 +149,7 @@ Since Vite removes env variables by default, you can update the vite config to e
|
||||
"process.env.IS_PREACT": JSON.stringify("true"),
|
||||
},
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Browser
|
||||
@@ -180,15 +176,16 @@ import TabItem from "@theme/TabItem";
|
||||
/>
|
||||
<link rel="stylesheet" href="./index.css" />
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
|
||||
</script>
|
||||
window.EXCALIDRAW_ASSET_PATH =
|
||||
"https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/prod/";
|
||||
</script>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react": "https://esm.sh/react@19.0.0",
|
||||
"react/jsx-runtime": "https://esm.sh/react@19.0.0/jsx-runtime",
|
||||
"react-dom": "https://esm.sh/react-dom@19.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
@@ -208,9 +205,9 @@ import TabItem from "@theme/TabItem";
|
||||
|
||||
```js showLineNumbers
|
||||
// See https://www.npmjs.com/package/@excalidraw/excalidraw documentation.
|
||||
import * as ExcalidrawLib from 'https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.js?external=react,react-dom';
|
||||
import * as ExcalidrawLib from "https://esm.sh/@excalidraw/excalidraw@0.18.0/dist/dev/index.js?external=react,react-dom";
|
||||
import React from "https://esm.sh/react@19.0.0";
|
||||
import ReactDOM from "https://esm.sh/react-dom@19.0.0"
|
||||
import ReactDOM from "https://esm.sh/react-dom@19.0.0";
|
||||
|
||||
window.ExcalidrawLib = ExcalidrawLib;
|
||||
console.log("Excalidraw library", ExcalidrawLib);
|
||||
|
||||
@@ -41,18 +41,24 @@ flowchart TD
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[Car]
|
||||
```
|
||||
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/c8ea84fc-e9fb-4652-9a12-154136d1a798" width="250" height="200"/>
|
||||
```
|
||||
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/c8ea84fc-e9fb-4652-9a12-154136d1a798"
|
||||
width="250"
|
||||
height="200"
|
||||
/>
|
||||
|
||||
```
|
||||
flowchart LR
|
||||
id1((Hello from Circle))
|
||||
```
|
||||
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/6202a8b9-8aa7-451e-9478-4d8d75c0f2fa" width="250" height="200"/>
|
||||
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/6202a8b9-8aa7-451e-9478-4d8d75c0f2fa"
|
||||
width="250"
|
||||
height="200"
|
||||
/>
|
||||
|
||||
#### Subgraphs
|
||||
|
||||
@@ -72,7 +78,11 @@ flowchart TB
|
||||
end
|
||||
```
|
||||
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/098bce52-8f93-437c-9a06-c6972e27c70a" width="350" height="200"/>
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/098bce52-8f93-437c-9a06-c6972e27c70a"
|
||||
width="350"
|
||||
height="200"
|
||||
/>
|
||||
|
||||
#### Unsupported shapes fallback to Rectangle
|
||||
|
||||
@@ -87,9 +97,14 @@ flowchart LR
|
||||
id5[/Parallelogram fallback to Rectangle /]
|
||||
id6[/Trapezoid fallback to Rectangle\]
|
||||
```
|
||||
|
||||
The above shapes are not supported in Excalidraw hence they fallback to Rectangle
|
||||
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/cb269473-16c5-4c35-bd7a-d631d8cc5b47" width="350" height="200"/>
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/cb269473-16c5-4c35-bd7a-d631d8cc5b47"
|
||||
width="350"
|
||||
height="200"
|
||||
/>
|
||||
|
||||
#### Markdown fallback to Regular text
|
||||
|
||||
@@ -99,7 +114,12 @@ Since we don't support wysiwyg text editor yet, hence [Markdown Strings](https:/
|
||||
flowchart LR
|
||||
A("`Hello **World**`") --> B("`Whats **up** ?`")
|
||||
```
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/107bd428-9ab9-42d4-ba12-b1e29c8db478" width="250" height="200"/>
|
||||
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/107bd428-9ab9-42d4-ba12-b1e29c8db478"
|
||||
width="250"
|
||||
height="200"
|
||||
/>
|
||||
|
||||
#### Basic FontAwesome fallback to text
|
||||
|
||||
@@ -112,8 +132,11 @@ flowchart TD
|
||||
B-->E(A fa:fa-camera-retro perhaps?)
|
||||
```
|
||||
|
||||
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/7a693863-c3f9-42ff-b325-4b3f8303c7af" width="250" height="200"/>
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/7a693863-c3f9-42ff-b325-4b3f8303c7af"
|
||||
width="250"
|
||||
height="200"
|
||||
/>
|
||||
|
||||
#### Cross Arrow head fallback to Bar Arrow head
|
||||
|
||||
@@ -121,8 +144,12 @@ flowchart TD
|
||||
flowchart LR
|
||||
Start x--x Stop
|
||||
```
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/217dd1ad-7f4e-4c80-8c1c-03647b42d821" width="250" height="200"/>
|
||||
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/217dd1ad-7f4e-4c80-8c1c-03647b42d821"
|
||||
width="250"
|
||||
height="200"
|
||||
/>
|
||||
|
||||
## Unsupported Diagram Types
|
||||
|
||||
@@ -135,7 +162,11 @@ erDiagram
|
||||
CUSTOMER }|..|{ DELIVERY-ADDRESS : uses
|
||||
```
|
||||
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/c1d3fdb3-32ef-4bf3-a38a-02ac3d7d2cb9" width="300" height="200"/>
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/c1d3fdb3-32ef-4bf3-a38a-02ac3d7d2cb9"
|
||||
width="300"
|
||||
height="200"
|
||||
/>
|
||||
|
||||
```
|
||||
gitGraph
|
||||
@@ -152,4 +183,8 @@ gitGraph
|
||||
|
||||
```
|
||||
|
||||
<img src="https://github.com/excalidraw/excalidraw/assets/11256141/e5dcec0b-d570-4eb4-b981-412a996aa96c" width="400" height="300"/>
|
||||
<img
|
||||
src="https://github.com/excalidraw/excalidraw/assets/11256141/e5dcec0b-d570-4eb4-b981-412a996aa96c"
|
||||
width="400"
|
||||
height="300"
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
The Codebase is divided into 2 Sections
|
||||
|
||||
* [How Parser Works under the hood](/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser) - If you are interested in understanding and deep diving into inner workings of the Parser, then make sure to checkout this section.
|
||||
- [How Parser Works under the hood](/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser) - If you are interested in understanding and deep diving into inner workings of the Parser, then make sure to checkout this section.
|
||||
|
||||
* [Adding a new diagram type](/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type) - If you want to help us make the mermaid to Excalidraw Parser more powerful, you will find all information in this section to do so.
|
||||
- [Adding a new diagram type](/docs/@excalidraw/mermaid-to-excalidraw/codebase/new-diagram-type) - If you want to help us make the mermaid to Excalidraw Parser more powerful, you will find all information in this section to do so.
|
||||
|
||||
@@ -10,7 +10,7 @@ lets run the playground server in local :point_down:
|
||||
yarn start
|
||||
```
|
||||
|
||||
This will start the playground server in port `1234` and open it in browser so you start playing with the playground.
|
||||
This will start the playground server in port `1234` and open it in browser so you start playing with the playground.
|
||||
|
||||
## Update Supported Diagram Types
|
||||
|
||||
@@ -26,13 +26,13 @@ For this create a file named `{{diagramType}}.ts` in [`src/parser`](https://gith
|
||||
|
||||
The main aim of the parser is :point_down:
|
||||
|
||||
1. Determine how elements are connected in the diagram and thus finding arrow and text bindings.
|
||||
1. Determine how elements are connected in the diagram and thus finding arrow and text bindings.
|
||||
|
||||
For this you might have to dig in to the parser `diagram.parser.yy` and which attributes to parse for the new diagram.
|
||||
|
||||
2. Determine the position and dimensions of each element, for this would be using the `svg`
|
||||
|
||||
Once the parser is ready, lets start using it.
|
||||
Once the parser is ready, lets start using it.
|
||||
|
||||
Add the diagram type in switch case in [`parseMermaid`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L97) and call the parser for the same.
|
||||
|
||||
@@ -51,4 +51,3 @@ Thats it, you have added the new diagram type 🥳, now lets test it out!
|
||||
2. Incase the new diagram type added is present in [`unsupported.ts`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/playground/testcases/unsupported.ts) then remove it from there.
|
||||
|
||||
3. Verify if the test cases are running fine in playground.
|
||||
|
||||
|
||||
@@ -8,12 +8,10 @@ In this section we will be diving into how the [flowchart parser](https://github
|
||||
|
||||
We use `diagram.parser.yy` attribute to parse the data. If you want to know more about how the `diagram.parse.yy` attribute looks like, you can check it [here](https://github.com/mermaid-js/mermaid/blob/00d06c7282a701849793680c1e97da1cfdfcce62/packages/mermaid/src/diagrams/flowchart/flowDb.js#L768), however for scope of flowchart we are using **3** APIs from this parser to compute `vertices`, `edges` and `clusters` as we need these data to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38).
|
||||
|
||||
|
||||
For computing `vertices` and `edge`s lets consider the below svg generated by mermaid
|
||||
|
||||

|
||||
|
||||
|
||||
## Computing the vertices
|
||||
|
||||
We use `getVertices` API from `diagram.parse.yy` to get the vertices for a given flowchart.
|
||||
@@ -42,9 +40,10 @@ Considering the same example this is the response from the API
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The dimensions and position is missing in this response and we need that to transform to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38), for this we have our own parser [`parseVertex`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L178) which takes the above response and uses the `svg` together to compute position, dimensions and cleans up the response.
|
||||
|
||||
The final output from `parseVertex` looks like :point_down:
|
||||
The final output from `parseVertex` looks like :point_down:
|
||||
|
||||
```js
|
||||
{
|
||||
@@ -73,57 +72,55 @@ The dimensions and position is missing in this response and we need that to tran
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Computing the edges
|
||||
|
||||
The lines and arrows are considered as `edges` in mermaid as shown in the above diagram.
|
||||
We use `getEdges` API from `diagram.parse.yy` to get the edges for a given flowchart.
|
||||
Considering the same example this is the response from the API
|
||||
The lines and arrows are considered as `edges` in mermaid as shown in the above diagram. We use `getEdges` API from `diagram.parse.yy` to get the edges for a given flowchart. Considering the same example this is the response from the API
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"start": "start",
|
||||
"end": "stop",
|
||||
"type": "arrow_point",
|
||||
"text": "",
|
||||
"labelType": "text",
|
||||
"stroke": "normal",
|
||||
"length": 1
|
||||
}
|
||||
]
|
||||
{
|
||||
start: "start",
|
||||
end: "stop",
|
||||
type: "arrow_point",
|
||||
text: "",
|
||||
labelType: "text",
|
||||
stroke: "normal",
|
||||
length: 1,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
Similarly here the dimensions and position is missing and we compute that from the svg. The [`parseEdge`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L245) takes the above response along with `svg` and computes the position, dimensions and cleans up the response.
|
||||
|
||||
The final output from `parseEdge` looks like :point_down:
|
||||
The final output from `parseEdge` looks like :point_down:
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"start": "start",
|
||||
"end": "stop",
|
||||
"type": "arrow_point",
|
||||
"text": "",
|
||||
"labelType": "text",
|
||||
"stroke": "normal",
|
||||
"startX": 67.797,
|
||||
"startY": 22,
|
||||
"endX": 117.797,
|
||||
"endY": 22,
|
||||
"reflectionPoints": [
|
||||
{
|
||||
"x": 67.797,
|
||||
"y": 22
|
||||
},
|
||||
{
|
||||
"x": 117.797,
|
||||
"y": 22
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
{
|
||||
start: "start",
|
||||
end: "stop",
|
||||
type: "arrow_point",
|
||||
text: "",
|
||||
labelType: "text",
|
||||
stroke: "normal",
|
||||
startX: 67.797,
|
||||
startY: 22,
|
||||
endX: 117.797,
|
||||
endY: 22,
|
||||
reflectionPoints: [
|
||||
{
|
||||
x: 67.797,
|
||||
y: 22,
|
||||
},
|
||||
{
|
||||
x: 117.797,
|
||||
y: 22,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## Computing the Subgraphs
|
||||
|
||||
`Subgraphs` is collection of elements grouped together. The Subgraphs map to `grouping` elements in Excalidraw.
|
||||
@@ -132,46 +129,35 @@ Lets consider the below example :point_down:
|
||||
|
||||

|
||||
|
||||
We use `getSubgraphs` API to get the subgraph data for a given flowchart.
|
||||
Considering the same example this is the response from the API
|
||||
We use `getSubgraphs` API to get the subgraph data for a given flowchart. Considering the same example this is the response from the API
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"id": "one",
|
||||
"nodes": [
|
||||
"flowchart-a2-1399",
|
||||
"flowchart-a1-1400"
|
||||
],
|
||||
"title": "one",
|
||||
"classes": [],
|
||||
"labelType": "text"
|
||||
}
|
||||
]
|
||||
{
|
||||
id: "one",
|
||||
nodes: ["flowchart-a2-1399", "flowchart-a1-1400"],
|
||||
title: "one",
|
||||
classes: [],
|
||||
labelType: "text",
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
For position and dimensions we use the svg to compute. The [`parseSubgraph`](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/parseMermaid.ts#L139) takes the above response along with `svg` and computes the position, dimensions and cleans up the response.
|
||||
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
"id": "one",
|
||||
"nodes": [
|
||||
"flowchart-a2-1399",
|
||||
"flowchart-a1-1400"
|
||||
],
|
||||
"title": "one",
|
||||
"labelType": "text",
|
||||
"nodeIds": [
|
||||
"a2",
|
||||
"a1"
|
||||
],
|
||||
"x": 75.4921875,
|
||||
"y": 0,
|
||||
"width": 121.25,
|
||||
"height": 188,
|
||||
"text": "one"
|
||||
}
|
||||
]
|
||||
```
|
||||
{
|
||||
id: "one",
|
||||
nodes: ["flowchart-a2-1399", "flowchart-a1-1400"],
|
||||
title: "one",
|
||||
labelType: "text",
|
||||
nodeIds: ["a2", "a1"],
|
||||
x: 75.4921875,
|
||||
y: 0,
|
||||
width: 121.25,
|
||||
height: 188,
|
||||
text: "one",
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
[This](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/index.ts) is the entry point of the library.
|
||||
|
||||
|
||||
`parseMermaidToExcalidraw` function is the only function exposed which receives mermaid syntax as the input, parses the mermaid syntax and resolves to Excalidraw Skeleton.
|
||||
|
||||
Lets look at the high level overview at how the parse works :point_down:
|
||||
@@ -13,10 +12,10 @@ Lets dive deeper into individual section now to understand better.
|
||||
|
||||
## Parsing Mermaid diagram
|
||||
|
||||
One of the dependencies of the library is [`mermaid`](https://www.npmjs.com/package/mermaid) library.
|
||||
We need the mermaid diagram in some extractable format so we can parse it to Excalidraw Elements.
|
||||
One of the dependencies of the library is [`mermaid`](https://www.npmjs.com/package/mermaid) library. We need the mermaid diagram in some extractable format so we can parse it to Excalidraw Elements.
|
||||
|
||||
Parsing is broken into two steps
|
||||
|
||||
1. [`Rendering Mermaid to Svg`](/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser#rendering-mermaid-to-svg) - This helps in determining the position and dimensions of each element in the diagram
|
||||
|
||||
2. [`Parsing the mermaid syntax`](/docs/@excalidraw/mermaid-to-excalidraw/codebase/parser#parsing-the-mermaid-syntax) - We also need to know how elements are connected which isn't possible with svg alone hence we also parse the mermaid syntax which helps in determining the connections and bindings between elements in the diagram.
|
||||
@@ -27,10 +26,8 @@ Parsing is broken into two steps
|
||||
|
||||
The [`mermaid`](https://www.npmjs.com/package/mermaid) library provides the API `mermaid.render` API which gives the output of the diagram in `svg`.
|
||||
|
||||
|
||||
If the diagram isn't supported, this svg is converted to `dataURL` and can be rendered as an image in Excalidraw.
|
||||
|
||||
|
||||
### Parsing the mermaid syntax
|
||||
|
||||
For this we first need to process the options along with mermaid defination for diagram provided by the user.
|
||||
@@ -57,9 +54,8 @@ If you want to understand how flowchart parser works, you can navigate to [Flowc
|
||||
|
||||
Now we have all the data, we just need to transform it to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38) API so it can be rendered in Excalidraw.
|
||||
|
||||
For this we have `converters` which takes the parsed mermaid data and gives back the Excalidraw Skeleton.
|
||||
For Unsupported types, we have already mentioned above that we convert it to `dataURL` and return the ExcalidrawImageSkeleton.
|
||||
For this we have `converters` which takes the parsed mermaid data and gives back the Excalidraw Skeleton. For Unsupported types, we have already mentioned above that we convert it to `dataURL` and return the ExcalidrawImageSkeleton.
|
||||
|
||||
For supported types, currently only flowchart, we have [flowchartConverter](https://github.com/excalidraw/mermaid-to-excalidraw/blob/master/src/converter/types/flowchart.ts#L24) which parses the data and converts to [ExcalidrawElementSkeleton](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/transform.ts#L133C13-L133C38).
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -10,7 +10,6 @@ To set up the library in local, follow the below steps 👇🏼
|
||||
|
||||
Go to [@excalidraw/mermaid-to-excalidraw](https://github.com/excalidraw/mermaid-to-excalidraw) and clone the repository to your local.
|
||||
|
||||
|
||||
```bash
|
||||
git clone git@github.com:excalidraw/mermaid-to-excalidraw.git
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ Once the library is installed, its ready to use.
|
||||
|
||||
```js
|
||||
import { parseMermaidToExcalidraw } from "@excalidraw/mermaid-to-excalidraw";
|
||||
import { convertToExcalidrawElements} from "@excalidraw/excalidraw"
|
||||
import { convertToExcalidrawElements } from "@excalidraw/excalidraw";
|
||||
|
||||
try {
|
||||
const { elements, files } = await parseMermaid(diagramDefinition, {
|
||||
@@ -38,5 +38,4 @@ try {
|
||||
|
||||
## Playground
|
||||
|
||||
Try it out [here](https://mermaid-to-excalidraw.vercel.app)
|
||||
|
||||
Try it out [here](https://mermaid-to-excalidraw.vercel.app)
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
|
||||
|
||||
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you.
|
||||
For new contributors we would recommend to start with *Easy* tasks.
|
||||
We have a [roadmap](https://github.com/orgs/excalidraw/projects/3) which we strongly recommend to go through and check if something interests you. For new contributors we would recommend to start with _Easy_ tasks.
|
||||
|
||||
In case you want to pick up something from the roadmap, comment on that issue and one of the project maintainers will assign it to you, post which you can discuss in the issue and start working on it.
|
||||
|
||||
@@ -60,10 +59,7 @@ It's also a good idea to consider if your change should include additional tests
|
||||
|
||||
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
|
||||
|
||||
:::note
|
||||
Some checks, such as the `lint` and `test`, require approval from the maintainers to run.
|
||||
They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval.
|
||||
:::
|
||||
:::note Some checks, such as the `lint` and `test`, require approval from the maintainers to run. They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval. :::
|
||||
|
||||
## Translating
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Reformat all files with Prettier
|
||||
### Lint & format
|
||||
|
||||
```bash
|
||||
yarn fix
|
||||
@@ -72,12 +72,6 @@ yarn test
|
||||
yarn test:update
|
||||
```
|
||||
|
||||
### Test for formatting with Prettier
|
||||
|
||||
```bash
|
||||
yarn test:code
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
You can use docker-compose to work on Excalidraw locally if you don't want to setup a Node.js env.
|
||||
|
||||
@@ -16,8 +16,8 @@ const FeatureList = [
|
||||
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
|
||||
description: (
|
||||
<>
|
||||
Want to build your own app powered by Excalidraw but don't know where to
|
||||
start?
|
||||
Want to build your own app powered by Excalidraw but don't know
|
||||
where to start?
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
"compilerOptions": {}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const ExcalidrawWrapper: React.FC = () => {
|
||||
<>
|
||||
<App
|
||||
appTitle={"Excalidraw with Nextjs Example"}
|
||||
useCustom={(api: any, args?: any[]) => {}}
|
||||
useCustom={() => {}}
|
||||
excalidrawLib={excalidrawLib}
|
||||
>
|
||||
<Excalidraw />
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "build/types/**/*.ts"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"build/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"outputDirectory": "build"
|
||||
"outputDirectory": "build",
|
||||
"installCommand": "yarn install && yarn --cwd ../../ install"
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ export default function ExampleApp({
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
bottom: "20px",
|
||||
zIndex: 9999999999999999,
|
||||
zIndex: 999999999999999,
|
||||
}}
|
||||
>
|
||||
Toggle Custom Sidebar
|
||||
@@ -250,7 +250,7 @@ export default function ExampleApp({
|
||||
</TTDDialogTrigger>
|
||||
)}
|
||||
<TTDDialog
|
||||
onTextSubmit={async (_) => {
|
||||
onTextSubmit={async () => {
|
||||
console.info("submit");
|
||||
// sleep for 2s
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
@@ -555,7 +555,7 @@ export default function ExampleApp({
|
||||
if (!comment) {
|
||||
return null;
|
||||
}
|
||||
const appState = excalidrawAPI?.getAppState()!;
|
||||
const appState = excalidrawAPI?.getAppState();
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: comment.x, sceneY: comment.y },
|
||||
appState,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -20,7 +20,7 @@ root.render(
|
||||
<StrictMode>
|
||||
<App
|
||||
appTitle={"Excalidraw Example"}
|
||||
useCustom={(api: any, args?: any[]) => {}}
|
||||
useCustom={() => {}}
|
||||
excalidrawLib={window.ExcalidrawLib}
|
||||
>
|
||||
<Excalidraw />
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
"name": "with-script-in-browser",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"@excalidraw/excalidraw": "*",
|
||||
"browser-fs-access": "0.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "5.0.12",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 5002",
|
||||
"build:preview": "yarn build && yarn preview",
|
||||
"build:packages": "yarn --cwd ../../ build:packages"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"browser-fs-access": "0.29.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"vite": "5.0.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"outputDirectory": "dist",
|
||||
"installCommand": "yarn install",
|
||||
"installCommand": "yarn install && yarn --cwd ../../ install",
|
||||
"buildCommand": "yarn build:packages && yarn build"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -192,7 +192,7 @@ if (window.self !== window.top) {
|
||||
if (parentUrl.origin === currentUrl.origin) {
|
||||
isSelfEmbedding = true;
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
@@ -310,7 +310,7 @@ const initializeScene = async (opts: {
|
||||
) {
|
||||
return { scene: data, isExternalScene };
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
return {
|
||||
scene: {
|
||||
appState: {
|
||||
@@ -787,7 +787,7 @@ const ExcalidrawWrapper = () => {
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<h1>I'm not a pretzel!</h1>
|
||||
<h1>I'm not a pretzel!</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,7 +210,9 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
||||
|
||||
const unsubOnUserFollow = this.excalidrawAPI.onUserFollow((payload) => {
|
||||
this.portal.socket && this.portal.broadcastUserFollowed(payload);
|
||||
if (this.portal.socket) {
|
||||
this.portal.broadcastUserFollowed(payload);
|
||||
}
|
||||
});
|
||||
const throttledRelayUserViewportBounds = throttleRAF(
|
||||
this.relayVisibleSceneBounds,
|
||||
@@ -915,9 +917,9 @@ class Collab extends PureComponent<CollabProps, CollabState> {
|
||||
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
|
||||
pointersMap: Gesture["pointers"];
|
||||
}) => {
|
||||
payload.pointersMap.size < 2 &&
|
||||
this.portal.socket &&
|
||||
if (payload.pointersMap.size < 2 && this.portal.socket) {
|
||||
this.portal.broadcastMouseLocation(payload);
|
||||
}
|
||||
},
|
||||
CURSOR_SYNC_TIMEOUT,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../packages/excalidraw/css/variables.module.scss";
|
||||
@use "../../packages/excalidraw/css/variables.module.scss" as *;
|
||||
|
||||
.excalidraw {
|
||||
.collab-errors-button {
|
||||
|
||||
@@ -46,7 +46,7 @@ class Portal {
|
||||
trackEvent("share", "room joined");
|
||||
}
|
||||
});
|
||||
this.socket.on("new-user", async (_socketId: string) => {
|
||||
this.socket.on("new-user", async () => {
|
||||
this.broadcastScene(
|
||||
WS_SUBTYPES.INIT,
|
||||
this.collab.getSceneElementsIncludingDeleted(),
|
||||
|
||||
@@ -95,7 +95,7 @@ export const AIComponents = ({
|
||||
return {
|
||||
html,
|
||||
};
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
throw new Error("Generation failed (invalid response)");
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const AppMainMenu: React.FC<{
|
||||
{isDevEnv() && (
|
||||
<MainMenu.Item
|
||||
icon={eyeIcon}
|
||||
onClick={() => {
|
||||
onSelect={() => {
|
||||
if (window.visualDebug) {
|
||||
delete window.visualDebug;
|
||||
saveDebugState({ enabled: false });
|
||||
@@ -77,6 +77,7 @@ export const AppMainMenu: React.FC<{
|
||||
</MainMenu.Item>
|
||||
)}
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.DefaultItems.Preferences />
|
||||
<MainMenu.DefaultItems.ToggleTheme
|
||||
allowSystemTheme
|
||||
theme={props.theme}
|
||||
|
||||
@@ -16,11 +16,8 @@
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
transparent 60%,
|
||||
var(--sidebar-bg-color) 100%
|
||||
),
|
||||
background-image:
|
||||
radial-gradient(circle, transparent 60%, var(--sidebar-bg-color) 100%),
|
||||
var(--image-source);
|
||||
|
||||
display: flex;
|
||||
|
||||
@@ -33,7 +33,15 @@ export const AppWelcomeScreen: React.FC<{
|
||||
return bit;
|
||||
});
|
||||
} else {
|
||||
headingContent = t("welcomeScreen.app.center_heading");
|
||||
headingContent = (
|
||||
<>
|
||||
{t("welcomeScreen.app.center_heading")}
|
||||
<br />
|
||||
{t("welcomeScreen.app.center_heading_line2")}
|
||||
<br />
|
||||
{t("welcomeScreen.app.center_heading_line3")}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,7 +28,7 @@ export class TopErrorBoundary extends React.Component<
|
||||
for (const [key, value] of Object.entries({ ...localStorage })) {
|
||||
try {
|
||||
_localStorage[key] = JSON.parse(value);
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
_localStorage[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export class TopErrorBoundary extends React.Component<
|
||||
scope.setExtras(errorInfo);
|
||||
const eventId = Sentry.captureException(error);
|
||||
|
||||
this.setState((state) => ({
|
||||
this.setState(() => ({
|
||||
hasError: true,
|
||||
sentryEventId: eventId,
|
||||
localStorage: JSON.stringify(_localStorage),
|
||||
|
||||
@@ -44,7 +44,7 @@ import type { Socket } from "socket.io-client";
|
||||
let FIREBASE_CONFIG: Record<string, any>;
|
||||
try {
|
||||
FIREBASE_CONFIG = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
console.warn(
|
||||
`Error JSON parsing firebase config. Supplied value: ${
|
||||
import.meta.env.VITE_APP_FIREBASE_CONFIG
|
||||
@@ -162,7 +162,7 @@ export const saveFilesToFirebase = async ({
|
||||
cacheControl: `public, max-age=${FILE_CACHE_MAX_AGE_SEC}`,
|
||||
});
|
||||
savedFiles.push(id);
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
erroredFiles.push(id);
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -181,7 +181,7 @@ const legacy_decodeFromBackend = async ({
|
||||
const iv = buffer.slice(0, IV_LENGTH_BYTES);
|
||||
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
|
||||
decrypted = await decryptData(new Uint8Array(iv), encrypted, decryptionKey);
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
// Fixed IV (old format, backward compatibility)
|
||||
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
|
||||
decrypted = await decryptData(fixedIv, buffer, decryptionKey);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../packages/excalidraw/css/variables.module.scss";
|
||||
@use "../packages/excalidraw/css/variables.module.scss" as *;
|
||||
|
||||
.excalidraw {
|
||||
--color-primary-contrast-offset: #625ee0; // to offset Chubb illusion
|
||||
|
||||
+28
-29
@@ -3,6 +3,34 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true vite build",
|
||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA cross-env VITE_APP_ENABLE_TRACKING=true vite build",
|
||||
"build:version": "node ../scripts/build-version.js",
|
||||
"build": "yarn build:app && yarn build:version",
|
||||
"start": "yarn && vite",
|
||||
"start:production": "yarn build && yarn serve",
|
||||
"serve": "npx http-server build -a localhost -p 5001 -o",
|
||||
"build:preview": "yarn build && vite preview --port 5000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
"@sentry/browser": "9.0.1",
|
||||
"callsites": "4.2.0",
|
||||
"firebase": "11.3.1",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
"idb-keyval": "6.0.3",
|
||||
"jotai": "2.11.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"socket.io-client": "4.7.2",
|
||||
"uqr": "0.1.2",
|
||||
"vite-plugin-html": "3.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite-plugin-sitemap": "0.7.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
@@ -24,34 +52,5 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
"@sentry/browser": "9.0.1",
|
||||
"callsites": "4.2.0",
|
||||
"firebase": "11.3.1",
|
||||
"i18next-browser-languagedetector": "6.1.4",
|
||||
"idb-keyval": "6.0.3",
|
||||
"jotai": "2.11.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"socket.io-client": "4.7.2",
|
||||
"uqr": "0.1.2",
|
||||
"vite-plugin-html": "3.2.2"
|
||||
},
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "cross-env VITE_APP_DISABLE_SENTRY=true vite build",
|
||||
"build:app": "cross-env VITE_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA cross-env VITE_APP_ENABLE_TRACKING=true vite build",
|
||||
"build:version": "node ../scripts/build-version.js",
|
||||
"build": "yarn build:app && yarn build:version",
|
||||
"start": "yarn && vite",
|
||||
"start:production": "yarn build && yarn serve",
|
||||
"serve": "npx http-server build -a localhost -p 5001 -o",
|
||||
"build:preview": "yarn build && vite preview --port 5000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite-plugin-sitemap": "0.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "../../packages/excalidraw/css/variables.module.scss";
|
||||
@use "../../packages/excalidraw/css/variables.module.scss" as *;
|
||||
|
||||
.excalidraw {
|
||||
.ShareDialog {
|
||||
|
||||
@@ -73,7 +73,7 @@ const ActiveRoomDialog = ({
|
||||
const copyRoomLink = async () => {
|
||||
try {
|
||||
await copyTextToSystemClipboard(activeRoomLink);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
collabAPI.setCollabError(t("errors.copyToSystemClipboardFailed"));
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ const ActiveRoomDialog = ({
|
||||
text: t("roomDialog.shareTitle"),
|
||||
url: activeRoomLink,
|
||||
});
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
// Just ignore.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -50,7 +50,11 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
||||
<div
|
||||
class="welcome-screen-center__heading welcome-screen-decor excalifont"
|
||||
>
|
||||
All your data is saved locally in your browser.
|
||||
Your drawings are saved in your browser's storage.
|
||||
<br />
|
||||
Browser storage can be cleared unexpectedly.
|
||||
<br />
|
||||
Save your work to a file regularly to avoid losing it.
|
||||
</div>
|
||||
<div
|
||||
class="welcome-screen-menu"
|
||||
|
||||
Vendored
-3
@@ -26,9 +26,6 @@ interface ImportMetaEnv {
|
||||
// Set this flag to false if you want to open the overlay by default
|
||||
VITE_APP_COLLAPSE_OVERLAY: string;
|
||||
|
||||
// Enable eslint in dev server
|
||||
VITE_APP_ENABLE_ESLINT: string;
|
||||
|
||||
// Enable PWA in dev server
|
||||
VITE_APP_ENABLE_PWA: string;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import svgrPlugin from "vite-plugin-svgr";
|
||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import checker from "vite-plugin-checker";
|
||||
import oxlint from "vite-plugin-oxlint";
|
||||
import { createHtmlPlugin } from "vite-plugin-html";
|
||||
import Sitemap from "vite-plugin-sitemap";
|
||||
import { woff2BrowserPlugin } from "../scripts/woff2/woff2-vite-plugins";
|
||||
@@ -125,15 +126,16 @@ export default defineConfig(({ mode }) => {
|
||||
react(),
|
||||
checker({
|
||||
typescript: true,
|
||||
eslint:
|
||||
envVars.VITE_APP_ENABLE_ESLINT === "false"
|
||||
? undefined
|
||||
: { lintCommand: 'eslint "./**/*.{js,ts,tsx}"' },
|
||||
overlay: {
|
||||
initialIsOpen: envVars.VITE_APP_COLLAPSE_OVERLAY === "false",
|
||||
badgeStyle: "margin-bottom: 4rem; margin-left: 1rem",
|
||||
},
|
||||
}),
|
||||
oxlint({
|
||||
configFile: path.resolve(__dirname, "../.oxlintrc.json"),
|
||||
path: path.resolve(__dirname, ".."),
|
||||
oxlintPath: path.resolve(__dirname, "../node_modules/.bin/oxlint"),
|
||||
}),
|
||||
svgrPlugin(),
|
||||
ViteEjsPlugin(),
|
||||
VitePWA({
|
||||
|
||||
+47
-53
@@ -1,53 +1,11 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "excalidraw-monorepo",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"workspaces": [
|
||||
"excalidraw-app",
|
||||
"packages/*",
|
||||
"examples/*"
|
||||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@excalidraw/eslint-config": "1.0.3",
|
||||
"@excalidraw/prettier-config": "1.0.2",
|
||||
"@types/chai": "4.3.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/lodash.throttle": "4.1.7",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/socket.io-client": "3.0.0",
|
||||
"@vitejs/plugin-react": "3.1.0",
|
||||
"@vitest/coverage-v8": "3.0.7",
|
||||
"@vitest/ui": "2.0.5",
|
||||
"chai": "4.3.6",
|
||||
"dotenv": "16.0.1",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-config-react-app": "7.0.1",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-prettier": "3.3.1",
|
||||
"http-server": "14.1.1",
|
||||
"husky": "7.0.4",
|
||||
"jsdom": "22.1.0",
|
||||
"lint-staged": "12.3.7",
|
||||
"pepjs": "0.5.3",
|
||||
"prettier": "2.6.2",
|
||||
"rewire": "6.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-checker": "0.7.2",
|
||||
"vite-plugin-ejs": "1.7.0",
|
||||
"vite-plugin-pwa": "0.21.1",
|
||||
"vite-plugin-svgr": "4.2.0",
|
||||
"vitest": "3.0.6",
|
||||
"vitest-canvas-mock": "0.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"homepage": ".",
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
"scripts": {
|
||||
"build-node": "node ./scripts/build-node.js",
|
||||
"build:app:docker": "yarn --cwd ./excalidraw-app build:app:docker",
|
||||
@@ -65,21 +23,21 @@
|
||||
"start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
|
||||
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
||||
"test:app": "vitest",
|
||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||
"test:other": "yarn prettier --list-different",
|
||||
"test:code": "oxlint .",
|
||||
"test:other": "oxfmt --check .",
|
||||
"test:typecheck": "tsc",
|
||||
"test:update": "yarn test:app --update --watch=false",
|
||||
"test": "yarn test:app",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:coverage:watch": "vitest --coverage --watch",
|
||||
"test:ui": "yarn test --ui --coverage.enabled=true",
|
||||
"fix:code": "yarn test:code --fix",
|
||||
"fix:other": "yarn prettier --write",
|
||||
"fix": "yarn fix:other && yarn fix:code",
|
||||
"lint": "oxlint",
|
||||
"lint:type-aware": "oxlint --type-aware",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"format:fix": "oxfmt",
|
||||
"fix": "yarn lint:fix && yarn format:fix",
|
||||
"locales-coverage": "node scripts/build-locales-coverage.js",
|
||||
"locales-coverage:description": "node scripts/locales-coverage-description.js",
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
|
||||
"release": "node scripts/release.js",
|
||||
"release:test": "node scripts/release.js --tag=test",
|
||||
"release:next": "node scripts/release.js --tag=next",
|
||||
@@ -88,7 +46,43 @@
|
||||
"rm:node_modules": "rimraf --glob node_modules excalidraw-app/node_modules packages/*/node_modules",
|
||||
"clean-install": "yarn rm:node_modules && yarn install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "7.26.9",
|
||||
"@types/chai": "4.3.0",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/lodash.throttle": "4.1.7",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/socket.io-client": "3.0.0",
|
||||
"@vitejs/plugin-react": "3.1.0",
|
||||
"@vitest/coverage-v8": "3.0.7",
|
||||
"@vitest/ui": "2.0.5",
|
||||
"chai": "4.3.6",
|
||||
"dotenv": "16.0.1",
|
||||
"http-server": "14.1.1",
|
||||
"jsdom": "22.1.0",
|
||||
"lint-staged": "12.3.7",
|
||||
"oxfmt": "0.26.0",
|
||||
"oxlint": "1.41.0",
|
||||
"oxlint-tsgolint": "0.11.1",
|
||||
"pepjs": "0.5.3",
|
||||
"rewire": "6.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-checker": "0.7.2",
|
||||
"vite-plugin-ejs": "1.7.0",
|
||||
"vite-plugin-oxlint": "github:dwelle/vite-plugin-oxlint",
|
||||
"vite-plugin-pwa": "0.21.1",
|
||||
"vite-plugin-svgr": "4.2.0",
|
||||
"vitest": "3.0.6",
|
||||
"vitest-canvas-mock": "0.3.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"strip-ansi": "6.0.1"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["../eslintrc.base.json"]
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
debug: typeof Debug;
|
||||
}
|
||||
}
|
||||
|
||||
const lessPrecise = (num: number, precision = 5) =>
|
||||
parseFloat(num.toPrecision(precision));
|
||||
|
||||
@@ -157,6 +151,70 @@ export class Debug {
|
||||
return ret;
|
||||
};
|
||||
};
|
||||
|
||||
private static CHANGED_CACHE: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
public static logChanged(name: string, obj: Record<string, unknown>) {
|
||||
const prev = Debug.CHANGED_CACHE[name];
|
||||
|
||||
Debug.CHANGED_CACHE[name] = obj;
|
||||
|
||||
if (!prev) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allKeys = new Set([...Object.keys(prev), ...Object.keys(obj)]);
|
||||
const changed: Record<string, { prev: unknown; next: unknown }> = {};
|
||||
|
||||
for (const key of allKeys) {
|
||||
const prevVal = prev[key];
|
||||
const nextVal = obj[key];
|
||||
if (!deepEqual(prevVal, nextVal)) {
|
||||
changed[key] = { prev: prevVal, next: nextVal };
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(changed).length > 0) {
|
||||
console.info(`[${name}] changed:`, changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deepEqual(a: unknown, b: unknown): boolean {
|
||||
if (Object.is(a, b)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
a === null ||
|
||||
b === null ||
|
||||
typeof a !== "object" ||
|
||||
typeof b !== "object"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) !== Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keysA = Object.keys(a as Record<string, unknown>);
|
||||
const keysB = Object.keys(b as Record<string, unknown>);
|
||||
|
||||
if (keysA.length !== keysB.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key of keysA) {
|
||||
if (
|
||||
!deepEqual(
|
||||
(a as Record<string, unknown>)[key],
|
||||
(b as Record<string, unknown>)[key],
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
//@ts-ignore
|
||||
window.debug = Debug;
|
||||
@@ -1,10 +1,21 @@
|
||||
{
|
||||
"name": "@excalidraw/common",
|
||||
"version": "0.18.0",
|
||||
"description": "Excalidraw common functions, constants, etc.",
|
||||
"keywords": [
|
||||
"excalidraw",
|
||||
"excalidraw-utils"
|
||||
],
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./dist/types/common/src/index.d.ts",
|
||||
"main": "./dist/prod/index.js",
|
||||
"module": "./dist/prod/index.js",
|
||||
"types": "./dist/types/common/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/common/src/index.d.ts",
|
||||
@@ -19,18 +30,19 @@
|
||||
"default": "./dist/prod/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"description": "Excalidraw common functions, constants, etc.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"excalidraw",
|
||||
"excalidraw-utils"
|
||||
],
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"tinycolor2": "1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/tinycolor2": "1.4.6"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
@@ -49,17 +61,5 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"tinycolor2": "1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/tinycolor2": "1.4.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,12 +118,15 @@ const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||
source: R,
|
||||
keys: K,
|
||||
) => {
|
||||
return keys.reduce((acc, key: K[number]) => {
|
||||
if (key in source) {
|
||||
acc[key] = source[key];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Pick<R, K[number]>) as Pick<R, K[number]>;
|
||||
return keys.reduce(
|
||||
(acc, key: K[number]) => {
|
||||
if (key in source) {
|
||||
acc[key] = source[key];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Pick<R, K[number]>,
|
||||
) as Pick<R, K[number]>;
|
||||
};
|
||||
|
||||
export type ColorTuple = readonly [string, string, string, string, string];
|
||||
@@ -346,7 +349,7 @@ export const normalizeInputColor = (color: string): string | null => {
|
||||
if (tc.isValid()) {
|
||||
// testing for `#` first fixes a bug on Electron (more specfically, an
|
||||
// Obsidian popout window), where a hex color without `#` is considered valid
|
||||
if (tc.getFormat() === "hex" && !color.startsWith("#")) {
|
||||
if (["hex", "hex8"].includes(tc.getFormat()) && !color.startsWith("#")) {
|
||||
return `#${color}`;
|
||||
}
|
||||
return color;
|
||||
|
||||
@@ -106,6 +106,7 @@ export const CLASSES = {
|
||||
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||
SHAPE_ACTIONS_THEME_SCOPE: "shape-actions-theme-scope",
|
||||
FRAME_NAME: "frame-name",
|
||||
DROPDOWN_MENU_EVENT_WRAPPER: "dropdown-menu-event-wrapper",
|
||||
};
|
||||
|
||||
export const FONT_SIZES = {
|
||||
@@ -251,6 +252,7 @@ export const STRING_MIME_TYPES = {
|
||||
json: "application/json",
|
||||
// excalidraw data
|
||||
excalidraw: "application/vnd.excalidraw+json",
|
||||
excalidrawClipboard: "application/vnd.excalidraw.clipboard+json",
|
||||
// LEGACY: fully-qualified library JSON data
|
||||
excalidrawlib: "application/vnd.excalidrawlib+json",
|
||||
// list of excalidraw library item ids
|
||||
|
||||
@@ -193,7 +193,7 @@ export const loadDesktopUIModePreference = () => {
|
||||
if (stored === "compact" || stored === "full") {
|
||||
return stored as EditorInterface["desktopUIMode"];
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// ignore storage access issues (e.g., Safari private mode)
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ const persistDesktopUIMode = (mode: EditorInterface["desktopUIMode"]) => {
|
||||
}
|
||||
try {
|
||||
window.localStorage.setItem(DESKTOP_UI_MODE_STORAGE_KEY, mode);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// ignore storage access issues (e.g., Safari private mode)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from "./url";
|
||||
export * from "./utils";
|
||||
export * from "./emitter";
|
||||
export * from "./editorInterface";
|
||||
export { Debug } from "../debug";
|
||||
|
||||
@@ -44,9 +44,8 @@ export type ForwardRef<T, P = any> = Parameters<
|
||||
CallableType<React.ForwardRefRenderFunction<T, P>>
|
||||
>[1];
|
||||
|
||||
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
|
||||
? U
|
||||
: never;
|
||||
export type ExtractSetType<T extends Set<any>> =
|
||||
T extends Set<infer U> ? U : never;
|
||||
|
||||
export type SameType<T, U> = T extends U ? (U extends T ? true : false) : false;
|
||||
export type Assert<T extends true> = T;
|
||||
@@ -74,6 +73,5 @@ 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;
|
||||
export type MapEntry<M extends Map<any, any>> =
|
||||
M extends Map<infer K, infer V> ? [K, V] : never;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { average } from "@excalidraw/math";
|
||||
|
||||
import type { GlobalCoord } from "@excalidraw/math";
|
||||
|
||||
import type { FontFamilyValues, FontString } from "@excalidraw/element/types";
|
||||
|
||||
import type {
|
||||
@@ -441,7 +443,7 @@ export const viewportCoordsToSceneCoords = (
|
||||
const x = (clientX - offsetLeft) / zoom.value - scrollX;
|
||||
const y = (clientY - offsetTop) / zoom.value - scrollY;
|
||||
|
||||
return { x, y };
|
||||
return { x, y } as GlobalCoord;
|
||||
};
|
||||
|
||||
export const sceneCoordsToViewportCoords = (
|
||||
@@ -667,10 +669,13 @@ export const arrayToMap = <T extends { id: string } | string>(
|
||||
if (items instanceof Map) {
|
||||
return items;
|
||||
}
|
||||
return items.reduce((acc: Map<string, T>, element) => {
|
||||
acc.set(typeof element === "string" ? element : element.id, element);
|
||||
return acc;
|
||||
}, new Map() as Map<string, T>);
|
||||
return items.reduce(
|
||||
(acc: Map<string, T>, element) => {
|
||||
acc.set(typeof element === "string" ? element : element.id, element);
|
||||
return acc;
|
||||
},
|
||||
new Map() as Map<string, T>,
|
||||
);
|
||||
};
|
||||
|
||||
export const arrayToMapWithIndex = <T extends { id: string }>(
|
||||
@@ -688,10 +693,13 @@ export const arrayToObject = <T>(
|
||||
array: readonly T[],
|
||||
groupBy?: (value: T) => string | number,
|
||||
) =>
|
||||
array.reduce((acc, value, idx) => {
|
||||
acc[groupBy ? groupBy(value) : idx] = value;
|
||||
return acc;
|
||||
}, {} as { [key: string]: T });
|
||||
array.reduce(
|
||||
(acc, value, idx) => {
|
||||
acc[groupBy ? groupBy(value) : idx] = value;
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: T },
|
||||
);
|
||||
|
||||
/** Doubly linked node */
|
||||
export type Node<T> = T & {
|
||||
@@ -799,7 +807,7 @@ export const isPrimitive = (val: any) => {
|
||||
export const getFrame = () => {
|
||||
try {
|
||||
return window.self === window.top ? "top" : "iframe";
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return "iframe";
|
||||
}
|
||||
};
|
||||
@@ -1019,8 +1027,8 @@ export const isMemberOf = <T extends string>(
|
||||
return collection instanceof Set || collection instanceof Map
|
||||
? collection.has(value as T)
|
||||
: "includes" in collection
|
||||
? collection.includes(value as T)
|
||||
: collection.hasOwnProperty(value);
|
||||
? collection.includes(value as T)
|
||||
: collection.hasOwnProperty(value);
|
||||
};
|
||||
|
||||
export const cloneJSON = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
|
||||
@@ -1151,35 +1159,38 @@ export type HasBrand<T> = {
|
||||
[K in keyof T]: K extends `~brand${infer _}` | "_brand" ? true : never;
|
||||
}[keyof T];
|
||||
|
||||
type RemoveAllBrands<T> = HasBrand<T> extends true
|
||||
? {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
[K in keyof T as K extends `~brand~${infer _}` | "_brand"
|
||||
? never
|
||||
: K]: T[K];
|
||||
}
|
||||
: T;
|
||||
type RemoveAllBrands<T> =
|
||||
HasBrand<T> extends true
|
||||
? {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
[K in keyof T as K extends `~brand~${infer _}` | "_brand"
|
||||
? never
|
||||
: K]: T[K];
|
||||
}
|
||||
: T;
|
||||
|
||||
// For accepting values - uses loose matching for branded types
|
||||
// Preserves readonly modifier: mutable array requires mutable input
|
||||
type UnbrandForValue<T> = T extends Map<infer E, infer F>
|
||||
? Map<UnbrandForValue<E>, UnbrandForValue<F>>
|
||||
: T extends Set<infer E>
|
||||
? Set<UnbrandForValue<E>>
|
||||
: T extends readonly any[]
|
||||
? T extends any[]
|
||||
? unknown[] // mutable array - require mutable input
|
||||
: readonly unknown[] // readonly array - accept readonly input
|
||||
: RemoveAllBrands<T>;
|
||||
type UnbrandForValue<T> =
|
||||
T extends Map<infer E, infer F>
|
||||
? Map<UnbrandForValue<E>, UnbrandForValue<F>>
|
||||
: T extends Set<infer E>
|
||||
? Set<UnbrandForValue<E>>
|
||||
: T extends readonly any[]
|
||||
? T extends any[]
|
||||
? unknown[] // mutable array - require mutable input
|
||||
: readonly unknown[] // readonly array - accept readonly input
|
||||
: RemoveAllBrands<T>;
|
||||
|
||||
// For return types - preserves array element unbranding
|
||||
export type Unbrand<T> = T extends Map<infer E, infer F>
|
||||
? Map<Unbrand<E>, Unbrand<F>>
|
||||
: T extends Set<infer E>
|
||||
? Set<Unbrand<E>>
|
||||
: T extends readonly (infer E)[]
|
||||
? Array<Unbrand<E>>
|
||||
: RemoveAllBrands<T>;
|
||||
export type Unbrand<T> =
|
||||
T extends Map<infer E, infer F>
|
||||
? Map<Unbrand<E>, Unbrand<F>>
|
||||
: T extends Set<infer E>
|
||||
? Set<Unbrand<E>>
|
||||
: T extends readonly (infer E)[]
|
||||
? Array<Unbrand<E>>
|
||||
: RemoveAllBrands<T>;
|
||||
|
||||
export type CombineBrands<BrandedType, CurrentType> =
|
||||
BrandedType extends readonly (infer BE)[]
|
||||
@@ -1191,8 +1202,8 @@ export type CombineBrands<BrandedType, CurrentType> =
|
||||
export type CombineBrandsIfNeeded<T, Required> = [T] extends [Required]
|
||||
? T[]
|
||||
: HasBrand<T> extends true
|
||||
? CombineBrands<T, Required>[]
|
||||
: Required[];
|
||||
? CombineBrands<T, Required>[]
|
||||
: Required[];
|
||||
|
||||
/**
|
||||
* Makes type into a branded type, ensuring that value is assignable to
|
||||
@@ -1259,8 +1270,8 @@ export const sizeOf = (
|
||||
return isReadonlyArray(value)
|
||||
? value.length
|
||||
: value instanceof Map || value instanceof Set
|
||||
? value.size
|
||||
: Object.keys(value).length;
|
||||
? value.size
|
||||
: Object.keys(value).length;
|
||||
};
|
||||
|
||||
export const reduceToCommonValue = <T, R = T>(
|
||||
@@ -1330,3 +1341,10 @@ export const setFeatureFlag = <F extends keyof FEATURE_FLAGS>(
|
||||
console.error("unable to set feature flag", e);
|
||||
}
|
||||
};
|
||||
|
||||
export const oneOf = <N extends string | number | symbol | null, H extends N>(
|
||||
needle: N,
|
||||
haystack: readonly H[],
|
||||
): needle is H => {
|
||||
return haystack.includes(needle as any);
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["../eslintrc.base.json"]
|
||||
}
|
||||
@@ -1,10 +1,21 @@
|
||||
{
|
||||
"name": "@excalidraw/element",
|
||||
"version": "0.18.0",
|
||||
"description": "Excalidraw elements-related logic",
|
||||
"keywords": [
|
||||
"excalidraw",
|
||||
"excalidraw-utils"
|
||||
],
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./dist/types/element/src/index.d.ts",
|
||||
"main": "./dist/prod/index.js",
|
||||
"module": "./dist/prod/index.js",
|
||||
"types": "./dist/types/element/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/element/src/index.d.ts",
|
||||
@@ -25,18 +36,17 @@
|
||||
"default": "./dist/prod/visualdebug.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
],
|
||||
"description": "Excalidraw elements-related logic",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"excalidraw",
|
||||
"excalidraw-utils"
|
||||
],
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/common": "0.18.0",
|
||||
"@excalidraw/math": "0.18.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
@@ -55,15 +65,5 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||
"repository": "https://github.com/excalidraw/excalidraw",
|
||||
"scripts": {
|
||||
"gen:types": "rimraf types && tsc",
|
||||
"build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/common": "0.18.0",
|
||||
"@excalidraw/math": "0.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const hashSelectionOpts = (
|
||||
type _ = Assert<
|
||||
SameType<
|
||||
Required<HashableKeys>,
|
||||
Pick<Required<HashableKeys>, typeof keys[number]>
|
||||
Pick<Required<HashableKeys>, (typeof keys)[number]>
|
||||
>
|
||||
>;
|
||||
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
import { pointDistance, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||
import { invariant } from "@excalidraw/common";
|
||||
|
||||
import type { AppState, NullableGridSize } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import {
|
||||
bindBindingElement,
|
||||
calculateFixedPointForNonElbowArrowBinding,
|
||||
FOCUS_POINT_SIZE,
|
||||
getBindingGap,
|
||||
getGlobalFixedPointForBindableElement,
|
||||
isBindingEnabled,
|
||||
maxBindingDistance_simple,
|
||||
unbindBindingElement,
|
||||
updateBoundPoint,
|
||||
} from "../binding";
|
||||
import {
|
||||
isBindableElement,
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
} from "../typeChecks";
|
||||
import { LinearElementEditor } from "../linearElementEditor";
|
||||
import { getHoveredElementForFocusPoint, hitElementItself } from "../collision";
|
||||
import { moveArrowAboveBindable } from "../zindex";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
PointsPositionUpdates,
|
||||
} from "../types";
|
||||
|
||||
import type { Scene } from "../Scene";
|
||||
|
||||
export const isFocusPointVisible = (
|
||||
focusPoint: GlobalPoint,
|
||||
arrow: ExcalidrawArrowElement,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: {
|
||||
isBindingEnabled: AppState["isBindingEnabled"];
|
||||
zoom: AppState["zoom"];
|
||||
},
|
||||
startOrEnd: "start" | "end",
|
||||
ignoreOverlap = false,
|
||||
): boolean => {
|
||||
// No focus point management for elbow arrows, because elbow arrows
|
||||
// always have their focus point at the arrow point itself
|
||||
if (
|
||||
isElbowArrow(arrow) ||
|
||||
!isBindingEnabled(appState) ||
|
||||
arrow.points.length !== 2
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Avoid showing the focus point indicator if the focus point is essentially
|
||||
// on top of the arrow point it belongs to itself, if not ignoring specifically
|
||||
if (!ignoreOverlap) {
|
||||
const associatedPointIdx =
|
||||
arrow.startBinding?.elementId === bindableElement.id
|
||||
? 0
|
||||
: arrow.points.length - 1;
|
||||
const associatedArrowPoint =
|
||||
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
associatedPointIdx,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
if (
|
||||
pointDistance(focusPoint, associatedArrowPoint) <
|
||||
(FOCUS_POINT_SIZE * 1.5) / appState.zoom.value
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const arrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
startOrEnd === "end" ? arrow.points.length - 1 : 0,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Check if the focus point is within the element's shape bounds
|
||||
// Endpoint dragging takes precedence
|
||||
return (
|
||||
pointDistance(focusPoint, arrowPoint) >=
|
||||
(FOCUS_POINT_SIZE * 1.5) / appState.zoom.value &&
|
||||
hitElementItself({
|
||||
element: bindableElement,
|
||||
elementsMap,
|
||||
point: focusPoint,
|
||||
threshold: getBindingGap(bindableElement, arrow),
|
||||
overrideShouldTestInside: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Updates the arrow endpoints in "orbit" configuration
|
||||
const focusPointUpdate = (
|
||||
arrow: ExcalidrawArrowElement,
|
||||
bindableElement: ExcalidrawBindableElement | null,
|
||||
isStartBinding: boolean,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
switchToInsideBinding: boolean,
|
||||
) => {
|
||||
const pointUpdates = new Map();
|
||||
|
||||
const bindingField = isStartBinding ? "startBinding" : "endBinding";
|
||||
const adjacentBindingField = isStartBinding ? "endBinding" : "startBinding";
|
||||
let currentBinding = arrow[bindingField];
|
||||
let adjacentBinding = arrow[adjacentBindingField];
|
||||
|
||||
// Update the dragged focus point related end
|
||||
if (currentBinding && bindableElement) {
|
||||
// Update the targeted bindings
|
||||
const boundToSameElement =
|
||||
bindableElement &&
|
||||
adjacentBinding &&
|
||||
currentBinding.elementId === adjacentBinding.elementId;
|
||||
if (switchToInsideBinding || boundToSameElement) {
|
||||
currentBinding = {
|
||||
...currentBinding,
|
||||
mode: "inside",
|
||||
};
|
||||
} else {
|
||||
currentBinding = {
|
||||
...currentBinding,
|
||||
mode: "orbit",
|
||||
};
|
||||
}
|
||||
|
||||
const pointIndex = isStartBinding ? 0 : arrow.points.length - 1;
|
||||
const newPoint = updateBoundPoint(
|
||||
arrow,
|
||||
bindingField as "startBinding" | "endBinding",
|
||||
currentBinding,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
true,
|
||||
);
|
||||
|
||||
if (newPoint) {
|
||||
pointUpdates.set(pointIndex, { point: newPoint });
|
||||
}
|
||||
}
|
||||
|
||||
// Also update the adjacent end if it has a binding
|
||||
if (adjacentBinding && adjacentBinding.mode === "orbit") {
|
||||
const adjacentBindableElement = elementsMap.get(
|
||||
adjacentBinding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
|
||||
if (
|
||||
adjacentBindableElement &&
|
||||
isBindableElement(adjacentBindableElement) &&
|
||||
isBindingEnabled(appState)
|
||||
) {
|
||||
// Same shape bound on both ends
|
||||
const boundToSameElementAfterUpdate =
|
||||
bindableElement && adjacentBinding.elementId === bindableElement.id;
|
||||
if (switchToInsideBinding || boundToSameElementAfterUpdate) {
|
||||
adjacentBinding = {
|
||||
...adjacentBinding,
|
||||
mode: "inside",
|
||||
};
|
||||
} else {
|
||||
adjacentBinding = {
|
||||
...adjacentBinding,
|
||||
mode: "orbit",
|
||||
};
|
||||
}
|
||||
|
||||
const adjacentPointIndex = isStartBinding ? arrow.points.length - 1 : 0;
|
||||
const adjacentNewPoint = updateBoundPoint(
|
||||
arrow,
|
||||
adjacentBindingField,
|
||||
adjacentBinding,
|
||||
adjacentBindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
if (adjacentNewPoint) {
|
||||
pointUpdates.set(adjacentPointIndex, {
|
||||
point: adjacentNewPoint,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pointUpdates.size > 0) {
|
||||
LinearElementEditor.movePoints(arrow, scene, pointUpdates, {
|
||||
[bindingField]: currentBinding,
|
||||
[adjacentBindingField]: adjacentBinding,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const handleFocusPointDrag = (
|
||||
linearElementEditor: LinearElementEditor,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
pointerCoords: { x: number; y: number },
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
gridSize: NullableGridSize,
|
||||
switchToInsideBinding: boolean,
|
||||
) => {
|
||||
const arrow = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
elementsMap,
|
||||
) as any;
|
||||
|
||||
// Sanity checks
|
||||
if (
|
||||
!arrow ||
|
||||
!isBindingElement(arrow) ||
|
||||
isElbowArrow(arrow) ||
|
||||
!linearElementEditor.hoveredFocusPointBinding ||
|
||||
!linearElementEditor.draggedFocusPointBinding
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isStartBinding =
|
||||
linearElementEditor.draggedFocusPointBinding === "start";
|
||||
const binding = isStartBinding ? arrow.startBinding : arrow.endBinding;
|
||||
const { x: offsetX, y: offsetY } = linearElementEditor.pointerOffset;
|
||||
const point = pointFrom<GlobalPoint>(
|
||||
pointerCoords.x - offsetX,
|
||||
pointerCoords.y - offsetY,
|
||||
);
|
||||
const bindingField = isStartBinding ? "startBinding" : "endBinding";
|
||||
const hit = getHoveredElementForFocusPoint(
|
||||
point,
|
||||
arrow,
|
||||
scene.getNonDeletedElements(),
|
||||
elementsMap,
|
||||
maxBindingDistance_simple(appState.zoom),
|
||||
);
|
||||
|
||||
// Hovering a bindable element
|
||||
if (hit && isBindingEnabled(appState)) {
|
||||
// Break existing binding if bound to another shape or if binding is disabled
|
||||
if (arrow[bindingField] && hit.id !== binding?.elementId) {
|
||||
unbindBindingElement(
|
||||
arrow,
|
||||
linearElementEditor.draggedFocusPointBinding,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle binding mode switch
|
||||
const newMode =
|
||||
switchToInsideBinding && arrow[bindingField]?.mode === "orbit"
|
||||
? "inside"
|
||||
: !switchToInsideBinding && arrow[bindingField]?.mode === "inside"
|
||||
? "orbit"
|
||||
: null;
|
||||
|
||||
// If no existing binding, create it
|
||||
if (!arrow[bindingField] || newMode) {
|
||||
// Create a new binding if none exists
|
||||
bindBindingElement(
|
||||
arrow,
|
||||
hit,
|
||||
newMode || "orbit",
|
||||
linearElementEditor.draggedFocusPointBinding,
|
||||
scene,
|
||||
point,
|
||||
);
|
||||
}
|
||||
|
||||
// Update the binding's fixed point
|
||||
scene.mutateElement(arrow, {
|
||||
[bindingField]: {
|
||||
...arrow[bindingField],
|
||||
elementId: hit.id,
|
||||
mode: newMode || arrow[bindingField]?.mode || "orbit",
|
||||
...calculateFixedPointForNonElbowArrowBinding(
|
||||
arrow,
|
||||
hit,
|
||||
linearElementEditor.draggedFocusPointBinding,
|
||||
elementsMap,
|
||||
point,
|
||||
),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Not hovering any bindable element, move the arrow endpoint
|
||||
const pointUpdates: PointsPositionUpdates = new Map();
|
||||
const pointIndex = isStartBinding ? 0 : arrow.points.length - 1;
|
||||
pointUpdates.set(pointIndex, {
|
||||
point: LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
point[0],
|
||||
point[1],
|
||||
gridSize,
|
||||
),
|
||||
});
|
||||
LinearElementEditor.movePoints(arrow, scene, pointUpdates);
|
||||
if (arrow[bindingField]) {
|
||||
unbindBindingElement(arrow, isStartBinding ? "start" : "end", scene);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the arrow endpoints
|
||||
focusPointUpdate(
|
||||
arrow,
|
||||
hit,
|
||||
isStartBinding,
|
||||
elementsMap,
|
||||
scene,
|
||||
appState,
|
||||
switchToInsideBinding,
|
||||
);
|
||||
|
||||
if (hit && isBindingEnabled(appState)) {
|
||||
moveArrowAboveBindable(
|
||||
point,
|
||||
arrow,
|
||||
scene.getElementsIncludingDeleted(),
|
||||
elementsMap,
|
||||
scene,
|
||||
hit,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleFocusPointPointerDown = (
|
||||
arrow: ExcalidrawArrowElement,
|
||||
pointerDownState: { origin: { x: number; y: number } },
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
appState: AppState,
|
||||
): {
|
||||
hitFocusPoint: "start" | "end" | null;
|
||||
pointerOffset: { x: number; y: number };
|
||||
} => {
|
||||
const pointerPos = pointFrom(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
);
|
||||
const hitThreshold = (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value;
|
||||
|
||||
// Check start binding focus point
|
||||
if (arrow.startBinding?.elementId) {
|
||||
const bindableElement = elementsMap.get(arrow.startBinding.elementId);
|
||||
if (
|
||||
bindableElement &&
|
||||
isBindableElement(bindableElement) &&
|
||||
!bindableElement.isDeleted
|
||||
) {
|
||||
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||
arrow.startBinding.fixedPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
if (
|
||||
isFocusPointVisible(
|
||||
focusPoint,
|
||||
arrow,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
appState,
|
||||
"start",
|
||||
) &&
|
||||
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||
) {
|
||||
return {
|
||||
hitFocusPoint: "start",
|
||||
pointerOffset: {
|
||||
x: pointerPos[0] - focusPoint[0],
|
||||
y: pointerPos[1] - focusPoint[1],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check end binding focus point (only if start not already hit)
|
||||
if (arrow.endBinding?.elementId) {
|
||||
const bindableElement = elementsMap.get(arrow.endBinding.elementId);
|
||||
if (
|
||||
bindableElement &&
|
||||
isBindableElement(bindableElement) &&
|
||||
!bindableElement.isDeleted
|
||||
) {
|
||||
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||
arrow.endBinding.fixedPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
if (
|
||||
isFocusPointVisible(
|
||||
focusPoint,
|
||||
arrow,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
appState,
|
||||
"end",
|
||||
) &&
|
||||
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||
) {
|
||||
return {
|
||||
hitFocusPoint: "end",
|
||||
pointerOffset: {
|
||||
x: pointerPos[0] - focusPoint[0],
|
||||
y: pointerPos[1] - focusPoint[1],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hitFocusPoint: null,
|
||||
pointerOffset: { x: 0, y: 0 },
|
||||
};
|
||||
};
|
||||
|
||||
export const handleFocusPointPointerUp = (
|
||||
linearElementEditor: LinearElementEditor,
|
||||
scene: Scene,
|
||||
) => {
|
||||
invariant(
|
||||
linearElementEditor.draggedFocusPointBinding,
|
||||
"Must have a dragged focus point at pointer release",
|
||||
);
|
||||
|
||||
const arrow = LinearElementEditor.getElement<ExcalidrawArrowElement>(
|
||||
linearElementEditor.elementId,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
invariant(arrow, "Arrow must be in the scene");
|
||||
|
||||
// Clean up
|
||||
const bindingKey =
|
||||
linearElementEditor.draggedFocusPointBinding === "start"
|
||||
? "startBinding"
|
||||
: "endBinding";
|
||||
const otherBindingKey =
|
||||
linearElementEditor.draggedFocusPointBinding === "start"
|
||||
? "endBinding"
|
||||
: "startBinding";
|
||||
const boundElementId = arrow[bindingKey]?.elementId;
|
||||
const otherBoundElementId = arrow[otherBindingKey]?.elementId;
|
||||
const oldBoundElement =
|
||||
boundElementId &&
|
||||
scene
|
||||
.getNonDeletedElements()
|
||||
.find(
|
||||
(element) =>
|
||||
element.id !== boundElementId &&
|
||||
element.id !== otherBoundElementId &&
|
||||
isBindableElement(element) &&
|
||||
element.boundElements?.find(({ id }) => id === arrow.id),
|
||||
);
|
||||
if (oldBoundElement) {
|
||||
scene.mutateElement(oldBoundElement, {
|
||||
boundElements: oldBoundElement.boundElements?.filter(
|
||||
({ id }) => id !== arrow.id,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Record the new bound element
|
||||
const boundElement =
|
||||
boundElementId && scene.getNonDeletedElementsMap().get(boundElementId);
|
||||
if (boundElement) {
|
||||
scene.mutateElement(boundElement, {
|
||||
boundElements: [
|
||||
...(boundElement.boundElements || []).filter(
|
||||
({ id }) => id !== arrow.id,
|
||||
),
|
||||
{
|
||||
id: arrow.id,
|
||||
type: "arrow",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const handleFocusPointHover = (
|
||||
arrow: ExcalidrawArrowElement,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
): "start" | "end" | null => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const pointerPos = pointFrom(scenePointerX, scenePointerY);
|
||||
const hitThreshold = (FOCUS_POINT_SIZE * 1.5) / appState.zoom.value;
|
||||
|
||||
// Check start binding focus point
|
||||
if (arrow.startBinding?.elementId) {
|
||||
const bindableElement = elementsMap.get(arrow.startBinding.elementId);
|
||||
if (
|
||||
bindableElement &&
|
||||
isBindableElement(bindableElement) &&
|
||||
!bindableElement.isDeleted
|
||||
) {
|
||||
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||
arrow.startBinding.fixedPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
if (
|
||||
isFocusPointVisible(
|
||||
focusPoint,
|
||||
arrow,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
appState,
|
||||
"start",
|
||||
) &&
|
||||
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||
) {
|
||||
return "start";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check end binding focus point (only if start not already hovered)
|
||||
if (arrow.endBinding?.elementId) {
|
||||
const bindableElement = elementsMap.get(arrow.endBinding.elementId);
|
||||
if (
|
||||
bindableElement &&
|
||||
isBindableElement(bindableElement) &&
|
||||
!bindableElement.isDeleted
|
||||
) {
|
||||
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||
arrow.endBinding.fixedPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
if (
|
||||
isFocusPointVisible(
|
||||
focusPoint,
|
||||
arrow,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
appState,
|
||||
"end",
|
||||
) &&
|
||||
pointDistance(pointerPos, focusPoint) <= hitThreshold
|
||||
) {
|
||||
return "end";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { App } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { LinearElementEditor } from "../linearElementEditor";
|
||||
|
||||
import { handleFocusPointDrag } from "./focus";
|
||||
|
||||
export const maybeHandleArrowPointlikeDrag = ({
|
||||
app,
|
||||
event,
|
||||
}: {
|
||||
app: App;
|
||||
event: KeyboardEvent | React.KeyboardEvent<Element> | PointerEvent;
|
||||
}): boolean => {
|
||||
const appState = app.state;
|
||||
if (appState.selectedLinearElement && app.lastPointerMoveCoords) {
|
||||
// Update focus point status if the binding mode is changing
|
||||
if (appState.selectedLinearElement.draggedFocusPointBinding) {
|
||||
handleFocusPointDrag(
|
||||
appState.selectedLinearElement,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.lastPointerMoveCoords,
|
||||
app.scene,
|
||||
appState,
|
||||
app.getEffectiveGridSize(),
|
||||
event.altKey,
|
||||
);
|
||||
return true;
|
||||
} else if (
|
||||
appState.selectedLinearElement.hoverPointIndex !== null &&
|
||||
app.lastPointerMoveEvent &&
|
||||
appState.selectedLinearElement.initialState.lastClickedPoint >= 0 &&
|
||||
appState.selectedLinearElement.isDragging
|
||||
) {
|
||||
LinearElementEditor.handlePointDragging(
|
||||
app.lastPointerMoveEvent,
|
||||
app,
|
||||
app.lastPointerMoveCoords.x,
|
||||
app.lastPointerMoveCoords.y,
|
||||
appState.selectedLinearElement,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
+374
-247
@@ -27,14 +27,11 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
|
||||
import type { Bounds } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
doBoundsIntersect,
|
||||
getCenterForBounds,
|
||||
getElementBounds,
|
||||
} from "./bounds";
|
||||
import { getCenterForBounds } from "./bounds";
|
||||
import {
|
||||
getAllHoveredElementAtPoint,
|
||||
getHoveredElementForBinding,
|
||||
hitElementItself,
|
||||
intersectElementWithLineSegment,
|
||||
isBindableElementInsideOtherBindable,
|
||||
isPointInElement,
|
||||
@@ -113,8 +110,10 @@ export type BindingStrategy =
|
||||
*
|
||||
* IMPORTANT: currently must be > 0 (this also applies to the computed gap)
|
||||
*/
|
||||
export const BASE_BINDING_GAP = 10;
|
||||
export const BASE_BINDING_GAP = 5;
|
||||
export const BASE_BINDING_GAP_ELBOW = 5;
|
||||
export const BASE_ARROW_MIN_LENGTH = 10;
|
||||
export const FOCUS_POINT_SIZE = 10 / 1.5;
|
||||
|
||||
export const getBindingGap = (
|
||||
bindTarget: ExcalidrawBindableElement,
|
||||
@@ -144,7 +143,9 @@ export const shouldEnableBindingForPointerEvent = (
|
||||
return !event[KEYS.CTRL_OR_CMD];
|
||||
};
|
||||
|
||||
export const isBindingEnabled = (appState: AppState): boolean => {
|
||||
export const isBindingEnabled = (appState: {
|
||||
isBindingEnabled: AppState["isBindingEnabled"];
|
||||
}): boolean => {
|
||||
return appState.isBindingEnabled;
|
||||
};
|
||||
|
||||
@@ -258,7 +259,7 @@ const bindingStrategyForElbowArrowEndpointDragging = (
|
||||
globalPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
(element) => maxBindingDistance_simple(zoom),
|
||||
maxBindingDistance_simple(zoom),
|
||||
);
|
||||
|
||||
const current = hit
|
||||
@@ -391,7 +392,7 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = (
|
||||
element: otherElement,
|
||||
focusPoint: shiftKey
|
||||
? elementCenterPoint(otherElement, elementsMap)
|
||||
: origin ?? pointFrom<GlobalPoint>(arrow.x, arrow.y),
|
||||
: (origin ?? pointFrom<GlobalPoint>(arrow.x, arrow.y)),
|
||||
};
|
||||
|
||||
// We are hovering another element with the end point
|
||||
@@ -522,41 +523,35 @@ const bindingStrategyForSimpleArrowEndpointDragging_complex = (
|
||||
}
|
||||
// The opposite binding is inside the same element
|
||||
// eslint-disable-next-line no-else-return
|
||||
else {
|
||||
current = { element: hit, mode: "inside", focusPoint: point };
|
||||
|
||||
return { current, other: isMultiPoint ? { mode: undefined } : other };
|
||||
}
|
||||
}
|
||||
// The opposite binding is on a different element (or nested)
|
||||
// eslint-disable-next-line no-else-return
|
||||
else {
|
||||
// Handle the nested element case
|
||||
if (isOverlapping && oppositeElement && !otherIsTransparent) {
|
||||
current = {
|
||||
element: oppositeElement,
|
||||
mode: "inside",
|
||||
focusPoint: point,
|
||||
};
|
||||
} else {
|
||||
current = {
|
||||
element: hit,
|
||||
mode: "orbit",
|
||||
focusPoint: isNested ? point : point,
|
||||
};
|
||||
}
|
||||
current = { element: hit, mode: "inside", focusPoint: point };
|
||||
|
||||
return { current, other: isMultiPoint ? { mode: undefined } : other };
|
||||
}
|
||||
// The opposite binding is on a different element (or nested)
|
||||
// eslint-disable-next-line no-else-return
|
||||
// Handle the nested element case
|
||||
if (isOverlapping && oppositeElement && !otherIsTransparent) {
|
||||
current = {
|
||||
element: oppositeElement,
|
||||
mode: "inside",
|
||||
focusPoint: point,
|
||||
};
|
||||
} else {
|
||||
current = {
|
||||
element: hit,
|
||||
mode: "orbit",
|
||||
focusPoint: isNested ? point : point,
|
||||
};
|
||||
}
|
||||
|
||||
return { current, other: isMultiPoint ? { mode: undefined } : other };
|
||||
}
|
||||
// The opposite binding is on a different element or no binding
|
||||
else {
|
||||
current = {
|
||||
element: hit,
|
||||
mode: "orbit",
|
||||
focusPoint: point,
|
||||
};
|
||||
}
|
||||
current = {
|
||||
element: hit,
|
||||
mode: "orbit",
|
||||
focusPoint: point,
|
||||
};
|
||||
|
||||
// Must return as only one endpoint is dragged, therefore
|
||||
// the end binding strategy might accidentally gets overriden
|
||||
@@ -683,7 +678,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
globalPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
(e) => maxBindingDistance_simple(appState.zoom),
|
||||
maxBindingDistance_simple(appState.zoom),
|
||||
);
|
||||
const pointInElement =
|
||||
hit &&
|
||||
@@ -710,7 +705,13 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
const otherFocusPointIsInElement =
|
||||
otherBindableElement &&
|
||||
otherFocusPoint &&
|
||||
isPointInElement(otherFocusPoint, otherBindableElement, elementsMap);
|
||||
hitElementItself({
|
||||
point: otherFocusPoint,
|
||||
element: otherBindableElement,
|
||||
elementsMap,
|
||||
threshold: 0,
|
||||
overrideShouldTestInside: true,
|
||||
});
|
||||
|
||||
// Handle outside-outside binding to the same element
|
||||
if (otherBinding && otherBinding.elementId === hit?.id) {
|
||||
@@ -726,13 +727,13 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
focusPoint: startDragged
|
||||
? globalPoint
|
||||
: // NOTE: Can only affect the start point because new arrows always drag the end point
|
||||
opts?.newArrow
|
||||
? appState.selectedLinearElement!.initialState.origin!
|
||||
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
0,
|
||||
elementsMap,
|
||||
), // startFixedPoint,
|
||||
opts?.newArrow
|
||||
? appState.selectedLinearElement!.initialState.origin!
|
||||
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
0,
|
||||
elementsMap,
|
||||
), // startFixedPoint,
|
||||
},
|
||||
end: {
|
||||
mode: "inside",
|
||||
@@ -790,6 +791,7 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
hit,
|
||||
startDragged ? "start" : "end",
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
) || globalPoint,
|
||||
}
|
||||
: { mode: null };
|
||||
@@ -799,30 +801,45 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
|
||||
startDragged ? -1 : 0,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const other: BindingStrategy =
|
||||
const pointIsCloseToOtherElement =
|
||||
otherFocusPoint &&
|
||||
otherBindableElement &&
|
||||
!otherFocusPointIsInElement &&
|
||||
appState.selectedLinearElement?.initialState.altFocusPoint
|
||||
hitElementItself({
|
||||
point: globalPoint,
|
||||
element: otherBindableElement,
|
||||
elementsMap,
|
||||
threshold: maxBindingDistance_simple(appState.zoom),
|
||||
overrideShouldTestInside: true,
|
||||
});
|
||||
const otherNeverOverride = opts?.newArrow
|
||||
? appState.selectedLinearElement?.initialState.arrowStartIsInside
|
||||
: otherBinding?.mode === "inside";
|
||||
const other: BindingStrategy = !otherNeverOverride
|
||||
? otherBindableElement &&
|
||||
!otherFocusPointIsInElement &&
|
||||
!pointIsCloseToOtherElement &&
|
||||
appState.selectedLinearElement?.initialState.altFocusPoint
|
||||
? {
|
||||
mode: "orbit",
|
||||
element: otherBindableElement,
|
||||
focusPoint: appState.selectedLinearElement.initialState.altFocusPoint,
|
||||
}
|
||||
: opts?.angleLocked && otherBindableElement
|
||||
? {
|
||||
mode: "orbit",
|
||||
element: otherBindableElement,
|
||||
focusPoint:
|
||||
projectFixedPointOntoDiagonal(
|
||||
arrow,
|
||||
otherEndpoint,
|
||||
otherBindableElement,
|
||||
startDragged ? "end" : "start",
|
||||
elementsMap,
|
||||
) || otherEndpoint,
|
||||
}
|
||||
: { mode: undefined };
|
||||
? {
|
||||
mode: "orbit",
|
||||
element: otherBindableElement,
|
||||
focusPoint:
|
||||
projectFixedPointOntoDiagonal(
|
||||
arrow,
|
||||
otherEndpoint,
|
||||
otherBindableElement,
|
||||
startDragged ? "end" : "start",
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
) || otherEndpoint,
|
||||
}
|
||||
: { mode: undefined }
|
||||
: { mode: undefined };
|
||||
|
||||
return {
|
||||
start: startDragged ? current : other,
|
||||
@@ -1086,7 +1103,7 @@ export const updateBoundElements = (
|
||||
});
|
||||
}
|
||||
|
||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||
const visitor = (element: ExcalidrawElement | undefined) => {
|
||||
if (!isArrowElement(element) || element.isDeleted) {
|
||||
return;
|
||||
}
|
||||
@@ -1158,7 +1175,71 @@ export const updateBoundElements = (
|
||||
if (boundText && !boundText.isDeleted) {
|
||||
handleBindTextResize(element, scene, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
boundElementsVisitor(elementsMap, changedElement, visitor);
|
||||
};
|
||||
|
||||
const updateArrowBindings = (
|
||||
latestElement: ExcalidrawArrowElement,
|
||||
startOrEnd: "startBinding" | "endBinding",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
appState: AppState,
|
||||
) => {
|
||||
invariant(
|
||||
!isElbowArrow(latestElement),
|
||||
"Elbow arrows not supported for indirect updates",
|
||||
);
|
||||
|
||||
const binding = latestElement[startOrEnd];
|
||||
const bindableElement =
|
||||
binding &&
|
||||
(elementsMap.get(binding.elementId) as ExcalidrawBindableElement);
|
||||
const point = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
latestElement,
|
||||
startOrEnd === "startBinding" ? 0 : -1,
|
||||
elementsMap,
|
||||
);
|
||||
const hit =
|
||||
bindableElement &&
|
||||
hitElementItself({
|
||||
element: bindableElement,
|
||||
point,
|
||||
elementsMap,
|
||||
threshold: maxBindingDistance_simple(appState.zoom),
|
||||
});
|
||||
const strategyName = startOrEnd === "startBinding" ? "start" : "end";
|
||||
unbindBindingElement(latestElement, strategyName, scene);
|
||||
if (hit) {
|
||||
const pointIdx =
|
||||
startOrEnd === "startBinding" ? 0 : latestElement.points.length - 1;
|
||||
const localPoint = latestElement.points[pointIdx];
|
||||
const strategy =
|
||||
getBindingStrategyForDraggingBindingElementEndpoints_simple(
|
||||
latestElement,
|
||||
new Map([[pointIdx, { point: localPoint }]]),
|
||||
point[0],
|
||||
point[1],
|
||||
elementsMap,
|
||||
scene.getNonDeletedElements(),
|
||||
appState,
|
||||
);
|
||||
if (
|
||||
strategy[strategyName] &&
|
||||
strategy[strategyName].element?.id === bindableElement.id &&
|
||||
strategy[strategyName].mode
|
||||
) {
|
||||
bindBindingElement(
|
||||
latestElement,
|
||||
bindableElement,
|
||||
strategy[strategyName].mode,
|
||||
strategyName,
|
||||
scene,
|
||||
strategy[strategyName].focusPoint,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const updateBindings = (
|
||||
@@ -1171,14 +1252,27 @@ export const updateBindings = (
|
||||
},
|
||||
) => {
|
||||
if (isArrowElement(latestElement)) {
|
||||
bindOrUnbindBindingElement(
|
||||
latestElement,
|
||||
new Map(),
|
||||
Infinity,
|
||||
Infinity,
|
||||
scene,
|
||||
appState,
|
||||
);
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
if (latestElement.startBinding) {
|
||||
updateArrowBindings(
|
||||
latestElement,
|
||||
"startBinding",
|
||||
elementsMap,
|
||||
scene,
|
||||
appState,
|
||||
);
|
||||
}
|
||||
|
||||
if (latestElement.endBinding) {
|
||||
updateArrowBindings(
|
||||
latestElement,
|
||||
"endBinding",
|
||||
elementsMap,
|
||||
scene,
|
||||
appState,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
updateBoundElements(latestElement, scene, {
|
||||
...options,
|
||||
@@ -1292,14 +1386,16 @@ export const bindPointToSnapToElementOutline = (
|
||||
headingForPointFromElement(bindableElement, aabb, point),
|
||||
);
|
||||
const snapPoint = snapToMid(
|
||||
arrowElement,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
edgePoint,
|
||||
0.05,
|
||||
arrowElement,
|
||||
);
|
||||
const resolved = snapPoint || point;
|
||||
const otherPoint = pointFrom<GlobalPoint>(
|
||||
isHorizontal ? bindableCenter[0] : snapPoint[0],
|
||||
!isHorizontal ? bindableCenter[1] : snapPoint[1],
|
||||
isHorizontal ? bindableCenter[0] : resolved[0],
|
||||
!isHorizontal ? bindableCenter[1] : resolved[1],
|
||||
);
|
||||
const intersector =
|
||||
customIntersector ??
|
||||
@@ -1307,7 +1403,7 @@ export const bindPointToSnapToElementOutline = (
|
||||
otherPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(snapPoint, otherPoint)),
|
||||
vectorNormalize(vectorFromPoint(resolved, otherPoint)),
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||
),
|
||||
otherPoint,
|
||||
@@ -1322,14 +1418,14 @@ export const bindPointToSnapToElementOutline = (
|
||||
|
||||
if (!intersection) {
|
||||
const anotherPoint = pointFrom<GlobalPoint>(
|
||||
!isHorizontal ? bindableCenter[0] : snapPoint[0],
|
||||
isHorizontal ? bindableCenter[1] : snapPoint[1],
|
||||
!isHorizontal ? bindableCenter[0] : resolved[0],
|
||||
isHorizontal ? bindableCenter[1] : resolved[1],
|
||||
);
|
||||
const anotherIntersector = lineSegment(
|
||||
anotherPoint,
|
||||
pointFromVector(
|
||||
vectorScale(
|
||||
vectorNormalize(vectorFromPoint(snapPoint, anotherPoint)),
|
||||
vectorNormalize(vectorFromPoint(resolved, anotherPoint)),
|
||||
Math.max(bindableElement.width, bindableElement.height) * 2,
|
||||
),
|
||||
anotherPoint,
|
||||
@@ -1476,18 +1572,18 @@ export const avoidRectangularCorner = (
|
||||
return p;
|
||||
};
|
||||
|
||||
const snapToMid = (
|
||||
arrowElement: ExcalidrawArrowElement,
|
||||
export const snapToMid = (
|
||||
bindTarget: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
p: GlobalPoint,
|
||||
tolerance: number = 0.05,
|
||||
): GlobalPoint => {
|
||||
arrowElement?: ExcalidrawArrowElement,
|
||||
): GlobalPoint | undefined => {
|
||||
const { x, y, width, height, angle } = bindTarget;
|
||||
const center = elementCenterPoint(bindTarget, elementsMap, -0.1, -0.1);
|
||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||
|
||||
const bindingGap = getBindingGap(bindTarget, arrowElement);
|
||||
const bindingGap = arrowElement ? getBindingGap(bindTarget, arrowElement) : 0;
|
||||
|
||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||
// above and below certain px distance
|
||||
@@ -1496,7 +1592,7 @@ const snapToMid = (
|
||||
|
||||
// Too close to the center makes it hard to resolve direction precisely
|
||||
if (pointDistance(center, nonRotated) < bindingGap) {
|
||||
return p;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -1505,8 +1601,8 @@ const snapToMid = (
|
||||
nonRotated[1] < center[1] + verticalThreshold
|
||||
) {
|
||||
// LEFT
|
||||
return pointRotateRads<GlobalPoint>(
|
||||
pointFrom(x - bindingGap, center[1]),
|
||||
return pointRotateRads(
|
||||
pointFrom<GlobalPoint>(x - bindingGap, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1516,7 +1612,11 @@ const snapToMid = (
|
||||
nonRotated[0] < center[0] + horizontalThreshold
|
||||
) {
|
||||
// TOP
|
||||
return pointRotateRads(pointFrom(center[0], y - bindingGap), center, angle);
|
||||
return pointRotateRads(
|
||||
pointFrom<GlobalPoint>(center[0], y - bindingGap),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
} else if (
|
||||
nonRotated[0] >= x + width / 2 &&
|
||||
nonRotated[1] > center[1] - verticalThreshold &&
|
||||
@@ -1524,7 +1624,7 @@ const snapToMid = (
|
||||
) {
|
||||
// RIGHT
|
||||
return pointRotateRads(
|
||||
pointFrom(x + width + bindingGap, center[1]),
|
||||
pointFrom<GlobalPoint>(x + width + bindingGap, center[1]),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1535,7 +1635,7 @@ const snapToMid = (
|
||||
) {
|
||||
// DOWN
|
||||
return pointRotateRads(
|
||||
pointFrom(center[0], y + height + bindingGap),
|
||||
pointFrom<GlobalPoint>(center[0], y + height + bindingGap),
|
||||
center,
|
||||
angle,
|
||||
);
|
||||
@@ -1584,13 +1684,44 @@ const snapToMid = (
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const compareElementArea = (
|
||||
a: ExcalidrawBindableElement,
|
||||
b: ExcalidrawBindableElement,
|
||||
) => b.width ** 2 + b.height ** 2 - (a.width ** 2 + a.height ** 2);
|
||||
const extractBinding = (
|
||||
arrow: ExcalidrawArrowElement,
|
||||
startOrEnd: "startBinding" | "endBinding",
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const binding = arrow[startOrEnd];
|
||||
if (!binding) {
|
||||
return {
|
||||
element: null,
|
||||
fixedPoint: null,
|
||||
focusPoint: null,
|
||||
binding,
|
||||
mode: null,
|
||||
};
|
||||
}
|
||||
|
||||
const element = elementsMap.get(
|
||||
binding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
|
||||
return {
|
||||
element,
|
||||
fixedPoint: binding.fixedPoint,
|
||||
focusPoint: getGlobalFixedPointForBindableElement(
|
||||
normalizeFixedPoint(binding.fixedPoint),
|
||||
element,
|
||||
elementsMap,
|
||||
),
|
||||
binding,
|
||||
mode: binding.mode,
|
||||
};
|
||||
};
|
||||
|
||||
const elementArea = (element: ExcalidrawBindableElement) =>
|
||||
element.width * element.height;
|
||||
|
||||
export const updateBoundPoint = (
|
||||
arrow: NonDeleted<ExcalidrawArrowElement>,
|
||||
@@ -1598,7 +1729,7 @@ export const updateBoundPoint = (
|
||||
binding: FixedPointBinding | null | undefined,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
customIntersector?: LineSegment<GlobalPoint>,
|
||||
dragging?: boolean,
|
||||
): LocalPoint | null => {
|
||||
if (
|
||||
binding == null ||
|
||||
@@ -1613,152 +1744,136 @@ export const updateBoundPoint = (
|
||||
return null;
|
||||
}
|
||||
|
||||
const global = getGlobalFixedPointForBindableElement(
|
||||
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||
normalizeFixedPoint(binding.fixedPoint),
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
const pointIndex =
|
||||
startOrEnd === "startBinding" ? 0 : arrow.points.length - 1;
|
||||
const elbowed = isElbowArrow(arrow);
|
||||
const otherBinding =
|
||||
startOrEnd === "startBinding" ? arrow.endBinding : arrow.startBinding;
|
||||
const otherBindableElement =
|
||||
otherBinding &&
|
||||
(elementsMap.get(otherBinding.elementId)! as ExcalidrawBindableElement);
|
||||
const bounds = getElementBounds(bindableElement, elementsMap);
|
||||
const otherBounds =
|
||||
otherBindableElement && getElementBounds(otherBindableElement, elementsMap);
|
||||
const isLargerThanOther =
|
||||
otherBindableElement &&
|
||||
compareElementArea(bindableElement, otherBindableElement) <
|
||||
// if both shapes the same size, pretend the other is larger
|
||||
(startOrEnd === "endBinding" ? 1 : 0);
|
||||
const isOverlapping = otherBounds && doBoundsIntersect(bounds, otherBounds);
|
||||
|
||||
// GOAL: If the arrow becomes too short, we want to jump the arrow endpoints
|
||||
// to the exact focus points on the elements.
|
||||
// INTUITION: We're not interested in the exacts length of the arrow (which
|
||||
// will change if we change where we route it), we want to know the length of
|
||||
// the part which lies outside of both shapes and consider that as a trigger
|
||||
// to change where we point the arrow. Avoids jumping the arrow in and out
|
||||
// at every frame.
|
||||
let arrowTooShort = false;
|
||||
if (
|
||||
!isOverlapping &&
|
||||
!elbowed &&
|
||||
arrow.startBinding &&
|
||||
arrow.endBinding &&
|
||||
otherBindableElement &&
|
||||
arrow.points.length === 2
|
||||
) {
|
||||
const startFocusPoint = getGlobalFixedPointForBindableElement(
|
||||
arrow.startBinding.fixedPoint,
|
||||
startOrEnd === "startBinding" ? bindableElement : otherBindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
const endFocusPoint = getGlobalFixedPointForBindableElement(
|
||||
arrow.endBinding.fixedPoint,
|
||||
startOrEnd === "endBinding" ? bindableElement : otherBindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
const segment = lineSegment(startFocusPoint, endFocusPoint);
|
||||
const startIntersection = intersectElementWithLineSegment(
|
||||
startOrEnd === "endBinding" ? bindableElement : otherBindableElement,
|
||||
elementsMap,
|
||||
segment,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
const endIntersection = intersectElementWithLineSegment(
|
||||
startOrEnd === "startBinding" ? bindableElement : otherBindableElement,
|
||||
elementsMap,
|
||||
segment,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
if (startIntersection.length > 0 && endIntersection.length > 0) {
|
||||
const len = pointDistance(startIntersection[0], endIntersection[0]);
|
||||
arrowTooShort = len < 40;
|
||||
}
|
||||
}
|
||||
|
||||
const isNested = (arrowTooShort || isOverlapping) && isLargerThanOther;
|
||||
|
||||
let _customIntersector = customIntersector;
|
||||
if (!elbowed && !_customIntersector) {
|
||||
const [x1, y1, x2, y2] = LinearElementEditor.getElementAbsoluteCoords(
|
||||
// 0. Short-circuit for inside binding as it doesn't require any
|
||||
// calculations and is not affected by other bindings
|
||||
if (binding.mode === "inside") {
|
||||
return LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
|
||||
const edgePoint = isRectanguloidElement(bindableElement)
|
||||
? avoidRectangularCorner(arrow, bindableElement, elementsMap, global)
|
||||
: global;
|
||||
const adjacentPoint = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(
|
||||
arrow.x +
|
||||
arrow.points[pointIndex === 0 ? 1 : arrow.points.length - 2][0],
|
||||
arrow.y +
|
||||
arrow.points[pointIndex === 0 ? 1 : arrow.points.length - 2][1],
|
||||
),
|
||||
center,
|
||||
arrow.angle as Radians,
|
||||
);
|
||||
const bindingGap = getBindingGap(bindableElement, arrow);
|
||||
const halfVector = vectorScale(
|
||||
vectorNormalize(vectorFromPoint(edgePoint, adjacentPoint)),
|
||||
pointDistance(edgePoint, adjacentPoint) +
|
||||
Math.max(bindableElement.width, bindableElement.height) +
|
||||
bindingGap * 2,
|
||||
);
|
||||
_customIntersector = lineSegment(
|
||||
pointFromVector(halfVector, adjacentPoint),
|
||||
pointFromVector(vectorScale(halfVector, -1), adjacentPoint),
|
||||
focusPoint[0],
|
||||
focusPoint[1],
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
const maybeOutlineGlobal =
|
||||
binding.mode === "orbit" && bindableElement
|
||||
? isNested
|
||||
? global
|
||||
: bindPointToSnapToElementOutline(
|
||||
{
|
||||
...arrow,
|
||||
points: [
|
||||
pointIndex === 0
|
||||
? LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
global[0],
|
||||
global[1],
|
||||
null,
|
||||
)
|
||||
: arrow.points[0],
|
||||
...arrow.points.slice(1, -1),
|
||||
pointIndex === arrow.points.length - 1
|
||||
? LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
global[0],
|
||||
global[1],
|
||||
null,
|
||||
)
|
||||
: arrow.points[arrow.points.length - 1],
|
||||
],
|
||||
},
|
||||
bindableElement,
|
||||
pointIndex === 0 ? "start" : "end",
|
||||
elementsMap,
|
||||
_customIntersector,
|
||||
)
|
||||
: global;
|
||||
const { element: otherBindable, focusPoint: otherFocusPoint } =
|
||||
extractBinding(
|
||||
arrow,
|
||||
startOrEnd === "startBinding" ? "endBinding" : "startBinding",
|
||||
elementsMap,
|
||||
);
|
||||
const otherArrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
startOrEnd === "startBinding" ? -1 : 0,
|
||||
elementsMap,
|
||||
);
|
||||
const otherFocusPointOrArrowPoint = otherFocusPoint || otherArrowPoint;
|
||||
const intersector =
|
||||
otherFocusPointOrArrowPoint &&
|
||||
lineSegment(focusPoint, otherFocusPointOrArrowPoint);
|
||||
const otherOutlinePoint =
|
||||
otherBindable &&
|
||||
intersector &&
|
||||
intersectElementWithLineSegment(
|
||||
otherBindable,
|
||||
elementsMap,
|
||||
intersector,
|
||||
getBindingGap(otherBindable, arrow),
|
||||
).sort(
|
||||
(a, b) => pointDistanceSq(a, focusPoint) - pointDistanceSq(b, focusPoint),
|
||||
)[0];
|
||||
const outlinePoint =
|
||||
intersector &&
|
||||
intersectElementWithLineSegment(
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
intersector,
|
||||
getBindingGap(bindableElement, arrow),
|
||||
).sort(
|
||||
(a, b) =>
|
||||
pointDistanceSq(a, otherFocusPointOrArrowPoint) -
|
||||
pointDistanceSq(b, otherFocusPointOrArrowPoint),
|
||||
)[0];
|
||||
const startHasArrowhead = arrow.startArrowhead !== null;
|
||||
const endHasArrowhead = arrow.endArrowhead !== null;
|
||||
const resolvedTarget =
|
||||
(!startHasArrowhead && !endHasArrowhead) ||
|
||||
(startOrEnd === "startBinding" && startHasArrowhead) ||
|
||||
(startOrEnd === "endBinding" && endHasArrowhead)
|
||||
? focusPoint
|
||||
: outlinePoint || focusPoint;
|
||||
|
||||
// 1. Handle case when the outline point (or focus point) is inside
|
||||
// the other shape by short-circuiting to the focus point, otherwise
|
||||
// the arrow would invert
|
||||
if (
|
||||
otherBindable &&
|
||||
outlinePoint &&
|
||||
!dragging &&
|
||||
// Arbitrary threshold to handle wireframing use cases
|
||||
elementArea(otherBindable) < elementArea(bindableElement) * 2 &&
|
||||
hitElementItself({
|
||||
element: otherBindable,
|
||||
point: outlinePoint,
|
||||
elementsMap,
|
||||
threshold: getBindingGap(otherBindable, arrow),
|
||||
overrideShouldTestInside: true,
|
||||
})
|
||||
) {
|
||||
return LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
resolvedTarget[0],
|
||||
resolvedTarget[1],
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
const otherTargetPoint = otherBindable
|
||||
? otherOutlinePoint || otherFocusPoint || otherArrowPoint
|
||||
: otherArrowPoint;
|
||||
const arrowTooShort =
|
||||
pointDistance(otherTargetPoint, outlinePoint || focusPoint) <=
|
||||
BASE_ARROW_MIN_LENGTH;
|
||||
|
||||
// 2. If the arrow is unconnected at the other end, just check arrow size
|
||||
// and short-circuit to the focus point if the arrow is too short to
|
||||
// avoid inversion
|
||||
if (!otherBindable) {
|
||||
return LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
arrowTooShort ? focusPoint[0] : (outlinePoint?.[0] ?? focusPoint[0]),
|
||||
arrowTooShort ? focusPoint[1] : (outlinePoint?.[1] ?? focusPoint[1]),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
// 3. If the arrow is too short while connected on both ends and
|
||||
// the other arrow endpoint will not be inside the bindable, just
|
||||
// check the arrow size and make a decision based on that
|
||||
if (arrowTooShort) {
|
||||
return LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
resolvedTarget?.[0] || focusPoint[0],
|
||||
resolvedTarget?.[1] || focusPoint[1],
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. In the general case, snap to the outline if possible
|
||||
return LinearElementEditor.createPointAt(
|
||||
arrow,
|
||||
elementsMap,
|
||||
maybeOutlineGlobal[0],
|
||||
maybeOutlineGlobal[1],
|
||||
outlinePoint?.[0] || focusPoint[0],
|
||||
outlinePoint?.[1] || focusPoint[1],
|
||||
null,
|
||||
);
|
||||
};
|
||||
@@ -1808,7 +1923,7 @@ export const calculateFixedPointForNonElbowArrowBinding = (
|
||||
elementsMap: ElementsMap,
|
||||
focusPoint?: GlobalPoint,
|
||||
): { fixedPoint: FixedPoint } => {
|
||||
const edgePoint = focusPoint
|
||||
const edgePoint: GlobalPoint = focusPoint
|
||||
? focusPoint
|
||||
: LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
linearElement,
|
||||
@@ -1816,11 +1931,7 @@ export const calculateFixedPointForNonElbowArrowBinding = (
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Convert the global point to element-local coordinates
|
||||
const elementCenter = pointFrom(
|
||||
hoveredElement.x + hoveredElement.width / 2,
|
||||
hoveredElement.y + hoveredElement.height / 2,
|
||||
);
|
||||
const elementCenter = elementCenterPoint(hoveredElement, elementsMap);
|
||||
|
||||
// Rotate the point to account for element rotation
|
||||
const nonRotatedPoint = pointRotateRads(
|
||||
@@ -1944,9 +2055,9 @@ const newBoundElements = (
|
||||
nextBoundElements.push(
|
||||
...elementsToAdd.map(
|
||||
(x) =>
|
||||
({ id: x.id, type: x.type } as
|
||||
({ id: x.id, type: x.type }) as
|
||||
| ExcalidrawArrowElement
|
||||
| ExcalidrawTextElement),
|
||||
| ExcalidrawTextElement,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2330,21 +2441,37 @@ export const getArrowLocalFixedPoints = (
|
||||
];
|
||||
};
|
||||
|
||||
export const normalizeFixedPoint = <T extends FixedPoint | null>(
|
||||
export const isFixedPoint = (
|
||||
fixedPoint: any,
|
||||
): fixedPoint is FixedPointBinding["fixedPoint"] => {
|
||||
return (
|
||||
Array.isArray(fixedPoint) &&
|
||||
fixedPoint.length === 2 &&
|
||||
fixedPoint.every((coord) => Number.isFinite(coord))
|
||||
);
|
||||
};
|
||||
|
||||
export const normalizeFixedPoint = <T extends FixedPoint>(
|
||||
fixedPoint: T,
|
||||
): T extends null ? null : FixedPoint => {
|
||||
): FixedPoint => {
|
||||
if (!isFixedPoint(fixedPoint)) {
|
||||
return [0.5001, 0.5001];
|
||||
}
|
||||
|
||||
const EPSILON = 0.0001;
|
||||
|
||||
// Do not allow a precise 0.5 for fixed point ratio
|
||||
// to avoid jumping arrow heading due to floating point imprecision
|
||||
if (
|
||||
fixedPoint &&
|
||||
(Math.abs(fixedPoint[0] - 0.5) < 0.0001 ||
|
||||
Math.abs(fixedPoint[1] - 0.5) < 0.0001)
|
||||
Math.abs(fixedPoint[0] - 0.5) < EPSILON ||
|
||||
Math.abs(fixedPoint[1] - 0.5) < EPSILON
|
||||
) {
|
||||
return fixedPoint.map((ratio) =>
|
||||
Math.abs(ratio - 0.5) < 0.0001 ? 0.5001 : ratio,
|
||||
) as T extends null ? null : FixedPoint;
|
||||
Math.abs(ratio - 0.5) < EPSILON ? 0.5001 : ratio,
|
||||
) as FixedPoint;
|
||||
}
|
||||
return fixedPoint as any as T extends null ? null : FixedPoint;
|
||||
|
||||
return fixedPoint;
|
||||
};
|
||||
|
||||
type Side =
|
||||
@@ -2414,7 +2541,7 @@ const SHAPE_CONFIGS: Record<ShapeType, SectorConfig[]> = {
|
||||
const getSectorBoundaries = (
|
||||
config: SectorConfig[],
|
||||
): Array<{ start: number; end: number; side: Side }> => {
|
||||
return config.map((sector, index) => {
|
||||
return config.map((sector) => {
|
||||
const halfWidth = sector.sectorWidth / 2;
|
||||
let start = sector.centerAngle - halfWidth;
|
||||
let end = sector.centerAngle + halfWidth;
|
||||
|
||||
@@ -59,8 +59,11 @@ import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
||||
import { distanceToElement } from "./distance";
|
||||
|
||||
import { getBindingGap } from "./binding";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
@@ -290,7 +293,7 @@ export const getAllHoveredElementAtPoint = (
|
||||
point: Readonly<GlobalPoint>,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
toleranceFn?: (element: ExcalidrawBindableElement) => number,
|
||||
tolerance?: number,
|
||||
): NonDeleted<ExcalidrawBindableElement>[] => {
|
||||
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||
@@ -306,7 +309,7 @@ export const getAllHoveredElementAtPoint = (
|
||||
|
||||
if (
|
||||
isBindableElement(element, false) &&
|
||||
bindingBorderTest(element, point, elementsMap, toleranceFn?.(element))
|
||||
bindingBorderTest(element, point, elementsMap, tolerance)
|
||||
) {
|
||||
candidateElements.push(element);
|
||||
|
||||
@@ -323,13 +326,13 @@ export const getHoveredElementForBinding = (
|
||||
point: Readonly<GlobalPoint>,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
toleranceFn?: (element: ExcalidrawBindableElement) => number,
|
||||
tolerance?: number,
|
||||
): NonDeleted<ExcalidrawBindableElement> | null => {
|
||||
const candidateElements = getAllHoveredElementAtPoint(
|
||||
point,
|
||||
elements,
|
||||
elementsMap,
|
||||
toleranceFn,
|
||||
tolerance,
|
||||
);
|
||||
|
||||
if (!candidateElements || candidateElements.length === 0) {
|
||||
@@ -348,6 +351,56 @@ export const getHoveredElementForBinding = (
|
||||
.pop() as NonDeleted<ExcalidrawBindableElement>;
|
||||
};
|
||||
|
||||
export const getHoveredElementForFocusPoint = (
|
||||
point: GlobalPoint,
|
||||
arrow: ExcalidrawArrowElement,
|
||||
elements: readonly Ordered<NonDeletedExcalidrawElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
tolerance?: number,
|
||||
): ExcalidrawBindableElement | null => {
|
||||
const candidateElements: NonDeleted<ExcalidrawBindableElement>[] = [];
|
||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||
// because array is ordered from lower z-index to highest and we want element z-index
|
||||
// with higher z-index
|
||||
for (let index = elements.length - 1; index >= 0; --index) {
|
||||
const element = elements[index];
|
||||
|
||||
invariant(
|
||||
!element.isDeleted,
|
||||
"Elements in the function parameter for getAllElementsAtPositionForBinding() should not contain deleted elements",
|
||||
);
|
||||
|
||||
if (
|
||||
isBindableElement(element, false) &&
|
||||
bindingBorderTest(element, point, elementsMap, tolerance)
|
||||
) {
|
||||
candidateElements.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
if (!candidateElements || candidateElements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (candidateElements.length === 1) {
|
||||
return candidateElements[0];
|
||||
}
|
||||
|
||||
const distanceFilteredCandidateElements = candidateElements
|
||||
// Resolve by distance
|
||||
.filter(
|
||||
(el) =>
|
||||
distanceToElement(el, elementsMap, point) <= getBindingGap(el, arrow) ||
|
||||
isPointInElement(point, el, elementsMap),
|
||||
);
|
||||
|
||||
if (distanceFilteredCandidateElements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return distanceFilteredCandidateElements[0] as NonDeleted<ExcalidrawBindableElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Intersect a line with an element for binding test
|
||||
*
|
||||
|
||||
@@ -467,6 +467,7 @@ export class Delta<T> {
|
||||
} else {
|
||||
assertNever(
|
||||
join,
|
||||
// oxlint-disable-next-line typescript/restrict-template-expressions
|
||||
`Unknown distinctKeysIterator's join param "${join}"`,
|
||||
true,
|
||||
);
|
||||
@@ -860,6 +861,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
default:
|
||||
assertNever(
|
||||
key,
|
||||
// oxlint-disable-next-line typescript/restrict-template-expressions
|
||||
`Unknown ObservedElementsAppState's key "${key}"`,
|
||||
true,
|
||||
);
|
||||
@@ -974,7 +976,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
inserted,
|
||||
"selectedElementIds",
|
||||
// ts language server has a bit trouble resolving this, so we are giving it a little push
|
||||
(_) => true as ValueOf<T["selectedElementIds"]>,
|
||||
() => true as ValueOf<T["selectedElementIds"]>,
|
||||
);
|
||||
Delta.diffObjects(
|
||||
deleted,
|
||||
@@ -995,9 +997,8 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
return [deleted, inserted];
|
||||
}
|
||||
return [deleted, inserted];
|
||||
}
|
||||
|
||||
private static orderAppStateKeys(partial: Partial<ObservedAppState>) {
|
||||
@@ -1333,6 +1334,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
for (const key of Object.keys(partial) as Array<keyof typeof partial>) {
|
||||
// do not update following props:
|
||||
// - `boundElements`, as it is a reference value which is postprocessed to contain only deleted/inserted keys
|
||||
// oxlint-disable-next-line typescript/switch-exhaustiveness-check
|
||||
switch (key) {
|
||||
case "boundElements":
|
||||
latestPartial[key] = partial[key];
|
||||
@@ -1459,9 +1461,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
return [nextElements, flags.containsVisibleDifference];
|
||||
}
|
||||
return [nextElements, flags.containsVisibleDifference];
|
||||
}
|
||||
|
||||
public squash(delta: ElementsDelta): this {
|
||||
@@ -1802,7 +1803,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
|
||||
// updated delta is affecting the binding only in case it contains changed binding or bindable property
|
||||
for (const [id] of Array.from(Object.entries(this.updated)).filter(
|
||||
([_, delta]) =>
|
||||
([, delta]) =>
|
||||
Object.keys({ ...delta.deleted, ...delta.inserted }).find((prop) =>
|
||||
bindingProperties.has(prop as BindingProp | BindableProp),
|
||||
),
|
||||
@@ -1908,9 +1909,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
return nextElements;
|
||||
}
|
||||
return nextElements;
|
||||
}
|
||||
|
||||
private static redrawTextBoundingBoxes(
|
||||
@@ -2051,9 +2051,9 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
if (isTestEnv() || isDevEnv()) {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
return [deleted, inserted];
|
||||
}
|
||||
|
||||
return [deleted, inserted];
|
||||
}
|
||||
|
||||
private static stripIrrelevantProps(
|
||||
|
||||
@@ -255,11 +255,12 @@ const handleSegmentRenormalization = (
|
||||
);
|
||||
}
|
||||
|
||||
isDevEnv() &&
|
||||
if (isDevEnv()) {
|
||||
invariant(
|
||||
validateElbowPoints(nextPoints),
|
||||
"Invalid elbow points with fixed segments",
|
||||
);
|
||||
}
|
||||
|
||||
return normalizeArrowElementUpdate(
|
||||
nextPoints,
|
||||
@@ -521,8 +522,8 @@ const handleSegmentMove = (
|
||||
? segmentLength / 2
|
||||
: BASE_PADDING
|
||||
: segmentIsTooShort
|
||||
? -segmentLength / 2
|
||||
: -BASE_PADDING;
|
||||
? -segmentLength / 2
|
||||
: -BASE_PADDING;
|
||||
fixedSegments[activelyModifiedSegmentIdx].start = pointFrom<LocalPoint>(
|
||||
fixedSegments[activelyModifiedSegmentIdx].start[0] +
|
||||
(startIsHorizontal ? padding : 0),
|
||||
@@ -547,8 +548,8 @@ const handleSegmentMove = (
|
||||
? segmentLength / 2
|
||||
: BASE_PADDING
|
||||
: segmentIsTooShort
|
||||
? -segmentLength / 2
|
||||
: -BASE_PADDING;
|
||||
? -segmentLength / 2
|
||||
: -BASE_PADDING;
|
||||
fixedSegments[activelyModifiedSegmentIdx].end = pointFrom<LocalPoint>(
|
||||
fixedSegments[activelyModifiedSegmentIdx].end[0] +
|
||||
(endIsHorizontal ? padding : 0),
|
||||
@@ -571,7 +572,7 @@ const handleSegmentMove = (
|
||||
}));
|
||||
|
||||
// For start, clone old arrow points
|
||||
const newPoints: GlobalPoint[] = arrow.points.map((p, i) =>
|
||||
const newPoints: GlobalPoint[] = arrow.points.map((p) =>
|
||||
pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1]),
|
||||
);
|
||||
|
||||
@@ -720,11 +721,11 @@ const handleEndpointDrag = (
|
||||
i === 0
|
||||
? pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1])
|
||||
: i === updatedPoints.length - 1
|
||||
? pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1])
|
||||
: pointFrom<GlobalPoint>(
|
||||
arrow.x + arrow.points[i][0],
|
||||
arrow.y + arrow.points[i][1],
|
||||
),
|
||||
? pointFrom<GlobalPoint>(arrow.x + p[0], arrow.y + p[1])
|
||||
: pointFrom<GlobalPoint>(
|
||||
arrow.x + arrow.points[i][0],
|
||||
arrow.y + arrow.points[i][1],
|
||||
),
|
||||
);
|
||||
const nextFixedSegments = fixedSegments.map((segment) => ({
|
||||
...segment,
|
||||
@@ -982,8 +983,8 @@ export const updateElbowArrowPoints = (
|
||||
idx === 0
|
||||
? updates.points![0]
|
||||
: idx === arrow.points.length - 1
|
||||
? updates.points![1]
|
||||
: p,
|
||||
? updates.points![1]
|
||||
: p,
|
||||
)
|
||||
: updates.points.slice()
|
||||
: arrow.points.slice();
|
||||
@@ -1489,8 +1490,12 @@ const routeElbowArrow = (
|
||||
node.pos[0],
|
||||
node.pos[1],
|
||||
]) as GlobalPoint[];
|
||||
startDongle && points.unshift(startGlobalPoint);
|
||||
endDongle && points.push(endGlobalPoint);
|
||||
if (startDongle) {
|
||||
points.unshift(startGlobalPoint);
|
||||
}
|
||||
if (endDongle) {
|
||||
points.push(endGlobalPoint);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
@@ -1680,29 +1685,29 @@ const generateDynamicAABBs = (
|
||||
? Math.min((startEl[0] + endEl[2]) / 2, a[0] - startLeft)
|
||||
: (startEl[0] + endEl[2]) / 2
|
||||
: a[0] > b[0]
|
||||
? a[0] - startLeft
|
||||
: common[0] - startLeft,
|
||||
? a[0] - startLeft
|
||||
: common[0] - startLeft,
|
||||
a[1] > b[3]
|
||||
? a[0] > b[2] || a[2] < b[0]
|
||||
? Math.min((startEl[1] + endEl[3]) / 2, a[1] - startUp)
|
||||
: (startEl[1] + endEl[3]) / 2
|
||||
: a[1] > b[1]
|
||||
? a[1] - startUp
|
||||
: common[1] - startUp,
|
||||
? a[1] - startUp
|
||||
: common[1] - startUp,
|
||||
a[2] < b[0]
|
||||
? a[1] > b[3] || a[3] < b[1]
|
||||
? Math.max((startEl[2] + endEl[0]) / 2, a[2] + startRight)
|
||||
: (startEl[2] + endEl[0]) / 2
|
||||
: a[2] < b[2]
|
||||
? a[2] + startRight
|
||||
: common[2] + startRight,
|
||||
? a[2] + startRight
|
||||
: common[2] + startRight,
|
||||
a[3] < b[1]
|
||||
? a[0] > b[2] || a[2] < b[0]
|
||||
? Math.max((startEl[3] + endEl[1]) / 2, a[3] + startDown)
|
||||
: (startEl[3] + endEl[1]) / 2
|
||||
: a[3] < b[3]
|
||||
? a[3] + startDown
|
||||
: common[3] + startDown,
|
||||
? a[3] + startDown
|
||||
: common[3] + startDown,
|
||||
] as Bounds;
|
||||
const second = [
|
||||
b[0] > a[2]
|
||||
@@ -1710,29 +1715,29 @@ const generateDynamicAABBs = (
|
||||
? Math.min((endEl[0] + startEl[2]) / 2, b[0] - endLeft)
|
||||
: (endEl[0] + startEl[2]) / 2
|
||||
: b[0] > a[0]
|
||||
? b[0] - endLeft
|
||||
: common[0] - endLeft,
|
||||
? b[0] - endLeft
|
||||
: common[0] - endLeft,
|
||||
b[1] > a[3]
|
||||
? b[0] > a[2] || b[2] < a[0]
|
||||
? Math.min((endEl[1] + startEl[3]) / 2, b[1] - endUp)
|
||||
: (endEl[1] + startEl[3]) / 2
|
||||
: b[1] > a[1]
|
||||
? b[1] - endUp
|
||||
: common[1] - endUp,
|
||||
? b[1] - endUp
|
||||
: common[1] - endUp,
|
||||
b[2] < a[0]
|
||||
? b[1] > a[3] || b[3] < a[1]
|
||||
? Math.max((endEl[2] + startEl[0]) / 2, b[2] + endRight)
|
||||
: (endEl[2] + startEl[0]) / 2
|
||||
: b[2] < a[2]
|
||||
? b[2] + endRight
|
||||
: common[2] + endRight,
|
||||
? b[2] + endRight
|
||||
: common[2] + endRight,
|
||||
b[3] < a[1]
|
||||
? b[0] > a[2] || b[2] < a[0]
|
||||
? Math.max((endEl[3] + startEl[1]) / 2, b[3] + endDown)
|
||||
: (endEl[3] + startEl[1]) / 2
|
||||
: b[3] < a[3]
|
||||
? b[3] + endDown
|
||||
: common[3] + endDown,
|
||||
? b[3] + endDown
|
||||
: common[3] + endDown,
|
||||
] as Bounds;
|
||||
|
||||
const c = commonAABB([first, second]);
|
||||
@@ -2276,7 +2281,7 @@ const getHoveredElement = (
|
||||
origPoint,
|
||||
elements,
|
||||
elementsMap,
|
||||
(element) => maxBindingDistance_simple(zoom),
|
||||
maxBindingDistance_simple(zoom),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { ExcalidrawElement } from "./types";
|
||||
export const defaultGetElementLinkFromSelection: Exclude<
|
||||
AppProps["generateLinkForSelection"],
|
||||
undefined
|
||||
> = (id, type) => {
|
||||
> = (id) => {
|
||||
const url = window.location.href;
|
||||
|
||||
try {
|
||||
@@ -86,7 +86,7 @@ export const isElementLink = (url: string) => {
|
||||
_url.searchParams.has(ELEMENT_LINK_KEY) &&
|
||||
_url.host === window.location.host
|
||||
);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,14 +56,14 @@ const RE_REDDIT =
|
||||
const RE_REDDIT_EMBED =
|
||||
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
|
||||
|
||||
const parseYouTubeTimestamp = (url: string): number => {
|
||||
const parseYouTubeLikeTimestamp = (url: string): number => {
|
||||
let timeParam: string | null | undefined;
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`);
|
||||
timeParam =
|
||||
urlObj.searchParams.get("t") || urlObj.searchParams.get("start");
|
||||
} catch (error) {
|
||||
} catch {
|
||||
const timeMatch = url.match(/[?&#](?:t|start)=([^&#\s]+)/);
|
||||
timeParam = timeMatch?.[1];
|
||||
}
|
||||
@@ -85,11 +85,57 @@ const parseYouTubeTimestamp = (url: string): number => {
|
||||
return parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(seconds);
|
||||
};
|
||||
|
||||
const parseGoogleDriveVideoLink = (
|
||||
url: string,
|
||||
): { fileId: string; resourceKey?: string; timestamp?: number } | null => {
|
||||
try {
|
||||
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`);
|
||||
const hostname = urlObj.hostname.replace(/^www\./, "");
|
||||
if (hostname !== "drive.google.com") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let fileId: string | null = null;
|
||||
const pathMatch = urlObj.pathname.match(/^\/file\/d\/([^/]+)(?:\/|$)/);
|
||||
if (pathMatch?.[1]) {
|
||||
fileId = pathMatch[1];
|
||||
} else if (urlObj.pathname === "/open" || urlObj.pathname === "/uc") {
|
||||
// Shared Drive links can be emitted as:
|
||||
// - /open?id=<fileId> (common "open in Drive" format)
|
||||
// - /uc?...&id=<fileId> (download/export endpoint often seen in copied links)
|
||||
fileId = urlObj.searchParams.get("id");
|
||||
}
|
||||
|
||||
if (!fileId || !/^[a-zA-Z0-9_-]+$/.test(fileId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Some Drive share links include `resourcekey` for access to link-shared
|
||||
// files; preserve it in the preview URL so embeds keep working.
|
||||
const resourceKey = urlObj.searchParams.get("resourcekey");
|
||||
const timestamp = parseYouTubeLikeTimestamp(urlObj.toString());
|
||||
|
||||
return {
|
||||
fileId,
|
||||
resourceKey:
|
||||
resourceKey && /^[a-zA-Z0-9_-]+$/.test(resourceKey)
|
||||
? resourceKey
|
||||
: undefined,
|
||||
// Drive accepts YouTube-like `t` formats (e.g. `t=90`, `t=1m30s`);
|
||||
// normalize to seconds for a stable preview URL.
|
||||
timestamp: timestamp > 0 ? timestamp : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const ALLOWED_DOMAINS = new Set([
|
||||
"youtube.com",
|
||||
"youtu.be",
|
||||
"vimeo.com",
|
||||
"player.vimeo.com",
|
||||
"drive.google.com",
|
||||
"figma.com",
|
||||
"link.excalidraw.com",
|
||||
"gist.github.com",
|
||||
@@ -108,6 +154,7 @@ const ALLOW_SAME_ORIGIN = new Set([
|
||||
"youtu.be",
|
||||
"vimeo.com",
|
||||
"player.vimeo.com",
|
||||
"drive.google.com",
|
||||
"figma.com",
|
||||
"twitter.com",
|
||||
"x.com",
|
||||
@@ -142,7 +189,7 @@ export const getEmbedLink = (
|
||||
let aspectRatio = { w: 560, h: 840 };
|
||||
const ytLink = link.match(RE_YOUTUBE);
|
||||
if (ytLink?.[2]) {
|
||||
const startTime = parseYouTubeTimestamp(originalLink);
|
||||
const startTime = parseYouTubeLikeTimestamp(originalLink);
|
||||
const time = startTime > 0 ? `&start=${startTime}` : ``;
|
||||
const isPortrait = link.includes("shorts");
|
||||
type = "video";
|
||||
@@ -201,6 +248,36 @@ export const getEmbedLink = (
|
||||
};
|
||||
}
|
||||
|
||||
const googleDriveVideo = parseGoogleDriveVideoLink(link);
|
||||
if (googleDriveVideo) {
|
||||
type = "video";
|
||||
const searchParams = new URLSearchParams();
|
||||
if (googleDriveVideo.resourceKey) {
|
||||
searchParams.set("resourcekey", googleDriveVideo.resourceKey);
|
||||
}
|
||||
if (googleDriveVideo.timestamp) {
|
||||
searchParams.set("t", `${googleDriveVideo.timestamp}`);
|
||||
}
|
||||
|
||||
const search = searchParams.toString();
|
||||
link = `https://drive.google.com/file/d/${googleDriveVideo.fileId}/preview${
|
||||
search ? `?${search}` : ""
|
||||
}`;
|
||||
aspectRatio = { w: 560, h: 315 };
|
||||
embeddedLinkCache.set(originalLink, {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
sandbox: { allowSameOrigin },
|
||||
});
|
||||
return {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
sandbox: { allowSameOrigin },
|
||||
};
|
||||
}
|
||||
|
||||
const figmaLink = link.match(RE_FIGMA);
|
||||
if (figmaLink) {
|
||||
type = "generic";
|
||||
@@ -388,7 +465,7 @@ const matchHostname = (
|
||||
if (bareDomain === bareAllowedHostname) {
|
||||
return bareAllowedHostname;
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -198,8 +198,8 @@ const getOffsets = (
|
||||
linkedNodes.length === 0
|
||||
? 0
|
||||
: (linkedNodes.length + 1) % 2 === 0
|
||||
? ((linkedNodes.length + 1) / 2) * _HORIZONTAL_OFFSET
|
||||
: (linkedNodes.length / 2) * _HORIZONTAL_OFFSET * -1;
|
||||
? ((linkedNodes.length + 1) / 2) * _HORIZONTAL_OFFSET
|
||||
: (linkedNodes.length / 2) * _HORIZONTAL_OFFSET * -1;
|
||||
|
||||
if (direction === "up") {
|
||||
return {
|
||||
@@ -222,8 +222,8 @@ const getOffsets = (
|
||||
linkedNodes.length === 0
|
||||
? 0
|
||||
: (linkedNodes.length + 1) % 2 === 0
|
||||
? ((linkedNodes.length + 1) / 2) * _VERTICAL_OFFSET
|
||||
: (linkedNodes.length / 2) * _VERTICAL_OFFSET * -1;
|
||||
? ((linkedNodes.length + 1) / 2) * _VERTICAL_OFFSET
|
||||
: (linkedNodes.length / 2) * _VERTICAL_OFFSET * -1;
|
||||
|
||||
if (direction === "left") {
|
||||
return {
|
||||
|
||||
@@ -187,7 +187,7 @@ export const syncMovedIndices = (
|
||||
for (const [element, { index }] of elementsUpdates) {
|
||||
mutateElement(element, elementsMap, { index });
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// fallback to default sync
|
||||
syncInvalidIndices(elements);
|
||||
}
|
||||
|
||||
@@ -797,7 +797,7 @@ export const isElementInFrame = (
|
||||
|
||||
for (const gid of _element.groupIds) {
|
||||
if (opts?.checkedGroups?.has(gid)) {
|
||||
return opts.checkedGroups.get(gid)!!;
|
||||
return opts.checkedGroups.get(gid)!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -231,8 +231,8 @@ export const getSelectedGroupIds = (
|
||||
appState: InteractiveCanvasAppState,
|
||||
): GroupId[] =>
|
||||
Object.entries(appState.selectedGroupIds)
|
||||
.filter(([groupId, isSelected]) => isSelected)
|
||||
.map(([groupId, isSelected]) => groupId);
|
||||
.filter(([, isSelected]) => isSelected)
|
||||
.map(([groupId]) => groupId);
|
||||
|
||||
// given a list of elements, return the the actual group ids that should be selected
|
||||
// or used to update the elements
|
||||
@@ -325,8 +325,8 @@ export const getMaximumGroups = (
|
||||
elements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
): ExcalidrawElement[][] => {
|
||||
const groups: Map<String, ExcalidrawElement[]> = new Map<
|
||||
String,
|
||||
const groups: Map<string, ExcalidrawElement[]> = new Map<
|
||||
string,
|
||||
ExcalidrawElement[]
|
||||
>();
|
||||
elements.forEach((element: ExcalidrawElement) => {
|
||||
|
||||
@@ -268,16 +268,16 @@ export const headingForPointFromElement = <Point extends GlobalPoint>(
|
||||
)
|
||||
? HEADING_UP
|
||||
: triangleIncludesPoint<Point>(
|
||||
[topRight, bottomRight, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
? HEADING_RIGHT
|
||||
: triangleIncludesPoint<Point>(
|
||||
[bottomRight, bottomLeft, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
? HEADING_DOWN
|
||||
: HEADING_LEFT;
|
||||
[topRight, bottomRight, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
? HEADING_RIGHT
|
||||
: triangleIncludesPoint<Point>(
|
||||
[bottomRight, bottomLeft, midPoint] as Triangle<Point>,
|
||||
p,
|
||||
)
|
||||
? HEADING_DOWN
|
||||
: HEADING_LEFT;
|
||||
};
|
||||
|
||||
export const flipHeading = (h: Heading): Heading =>
|
||||
|
||||
@@ -69,7 +69,7 @@ export const updateImageCache = async ({
|
||||
const image = await imagePromise;
|
||||
|
||||
imageCache.set(fileId, { ...data, image });
|
||||
} catch (error: any) {
|
||||
} catch {
|
||||
erroredFiles.set(fileId, true);
|
||||
}
|
||||
})(),
|
||||
|
||||
@@ -70,6 +70,7 @@ export * from "./elbowArrow";
|
||||
export * from "./elementLink";
|
||||
export * from "./embeddable";
|
||||
export * from "./flowchart";
|
||||
export * from "./arrows/focus";
|
||||
export * from "./fractionalIndex";
|
||||
export * from "./frame";
|
||||
export * from "./groups";
|
||||
@@ -97,3 +98,4 @@ export * from "./transformHandles";
|
||||
export * from "./typeChecks";
|
||||
export * from "./utils";
|
||||
export * from "./zindex";
|
||||
export * from "./arrows/helpers";
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
vectorFromPoint,
|
||||
curveLength,
|
||||
curvePointAtLength,
|
||||
lineSegment,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { getCurvePathOps } from "@excalidraw/utils/shape";
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
|
||||
import {
|
||||
deconstructLinearOrFreeDrawElement,
|
||||
getSnapOutlineMidPoint,
|
||||
isPathALoop,
|
||||
moveArrowAboveBindable,
|
||||
projectFixedPointOntoDiagonal,
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
calculateFixedPointForNonElbowArrowBinding,
|
||||
getBindingStrategyForDraggingBindingElementEndpoints,
|
||||
isBindingEnabled,
|
||||
snapToMid,
|
||||
updateBoundPoint,
|
||||
} from "./binding";
|
||||
import {
|
||||
@@ -149,6 +150,8 @@ export class LinearElementEditor {
|
||||
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
|
||||
public readonly hoverPointIndex: number;
|
||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||
public readonly hoveredFocusPointBinding: "start" | "end" | null;
|
||||
public readonly draggedFocusPointBinding: "start" | "end" | null;
|
||||
public readonly elbowed: boolean;
|
||||
public readonly customLineAngle: number | null;
|
||||
public readonly isEditing: boolean;
|
||||
@@ -194,6 +197,8 @@ export class LinearElementEditor {
|
||||
};
|
||||
this.hoverPointIndex = -1;
|
||||
this.segmentMidPointHoveredCoords = null;
|
||||
this.hoveredFocusPointBinding = null;
|
||||
this.draggedFocusPointBinding = null;
|
||||
this.elbowed = isElbowArrow(element) && element.elbowed;
|
||||
this.customLineAngle = null;
|
||||
this.isEditing = isEditing;
|
||||
@@ -351,6 +356,7 @@ export class LinearElementEditor {
|
||||
app,
|
||||
shouldRotateWithDiscreteAngle(event),
|
||||
event.altKey,
|
||||
linearElementEditor,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, app.scene, positions, {
|
||||
@@ -404,13 +410,14 @@ export class LinearElementEditor {
|
||||
altFocusPoint:
|
||||
!linearElementEditor.initialState.altFocusPoint &&
|
||||
startBindingElement &&
|
||||
updates?.suggestedBinding?.id !== startBindingElement.id
|
||||
updates?.suggestedBinding?.element.id !== startBindingElement.id
|
||||
? projectFixedPointOntoDiagonal(
|
||||
element,
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
startBindingElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
)
|
||||
: linearElementEditor.initialState.altFocusPoint,
|
||||
},
|
||||
@@ -528,6 +535,7 @@ export class LinearElementEditor {
|
||||
app,
|
||||
shouldRotateWithDiscreteAngle(event) && singlePointDragged,
|
||||
event.altKey,
|
||||
linearElementEditor,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, app.scene, positions, {
|
||||
@@ -603,13 +611,13 @@ export class LinearElementEditor {
|
||||
const altFocusPointBindableElement =
|
||||
endIsSelected && // The "other" end (i.e. "end") is dragged
|
||||
startBindingElement &&
|
||||
updates?.suggestedBinding?.id !== startBindingElement.id // The end point is not hovering the start bindable + it's binding gap
|
||||
updates?.suggestedBinding?.element.id !== startBindingElement.id // The end point is not hovering the start bindable + it's binding gap
|
||||
? startBindingElement
|
||||
: startIsSelected && // The "other" end (i.e. "start") is dragged
|
||||
endBindingElement &&
|
||||
updates?.suggestedBinding?.id !== endBindingElement.id // The start point is not hovering the end bindable + it's binding gap
|
||||
? endBindingElement
|
||||
: null;
|
||||
endBindingElement &&
|
||||
updates?.suggestedBinding?.element.id !== endBindingElement.id // The start point is not hovering the end bindable + it's binding gap
|
||||
? endBindingElement
|
||||
: null;
|
||||
|
||||
const newLinearElementEditor: LinearElementEditor = {
|
||||
...linearElementEditor,
|
||||
@@ -627,6 +635,7 @@ export class LinearElementEditor {
|
||||
altFocusPointBindableElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
)
|
||||
: linearElementEditor.initialState.altFocusPoint,
|
||||
},
|
||||
@@ -721,8 +730,8 @@ export class LinearElementEditor {
|
||||
)
|
||||
: selectedPointsIndices
|
||||
: selectedPointsIndices?.includes(pointerDownState.lastClickedPoint)
|
||||
? [pointerDownState.lastClickedPoint]
|
||||
: selectedPointsIndices,
|
||||
? [pointerDownState.lastClickedPoint]
|
||||
: selectedPointsIndices,
|
||||
isDragging: false,
|
||||
customLineAngle: null,
|
||||
initialState: {
|
||||
@@ -975,7 +984,7 @@ export class LinearElementEditor {
|
||||
const appState = app.state;
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
||||
const ret: ReturnType<(typeof LinearElementEditor)["handlePointerDown"]> = {
|
||||
didAddPoint: false,
|
||||
hitElement: null,
|
||||
linearElementEditor: null,
|
||||
@@ -2076,6 +2085,7 @@ const pointDraggingUpdates = (
|
||||
app: AppClassProperties,
|
||||
angleLocked: boolean,
|
||||
altKey: boolean,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
): {
|
||||
positions: PointsPositionUpdates;
|
||||
updates?: PointMoveOtherUpdates;
|
||||
@@ -2123,18 +2133,89 @@ const pointDraggingUpdates = (
|
||||
);
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
const suggestedBindingElement = startIsDragged
|
||||
? start.element
|
||||
: endIsDragged
|
||||
? end.element
|
||||
: null;
|
||||
|
||||
return {
|
||||
positions: naiveDraggingPoints,
|
||||
updates: {
|
||||
suggestedBinding: startIsDragged
|
||||
? start.element
|
||||
: endIsDragged
|
||||
? end.element
|
||||
suggestedBinding: suggestedBindingElement
|
||||
? {
|
||||
element: suggestedBindingElement,
|
||||
midPoint: snapToMid(
|
||||
suggestedBindingElement,
|
||||
elementsMap,
|
||||
pointFrom<GlobalPoint>(
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
),
|
||||
),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Handle the case where neither endpoint is being dragged
|
||||
// but we need to update bound endpoints
|
||||
if (!startIsDragged && !endIsDragged) {
|
||||
const nextArrow = {
|
||||
...element,
|
||||
points: element.points.map((p, idx) => {
|
||||
return naiveDraggingPoints.get(idx)?.point ?? p;
|
||||
}),
|
||||
};
|
||||
const positions = new Map(naiveDraggingPoints);
|
||||
|
||||
if (element.startBinding) {
|
||||
const startBindable = elementsMap.get(element.startBinding.elementId) as
|
||||
| ExcalidrawBindableElement
|
||||
| undefined;
|
||||
if (startBindable) {
|
||||
const startPoint =
|
||||
updateBoundPoint(
|
||||
nextArrow,
|
||||
"startBinding",
|
||||
element.startBinding,
|
||||
startBindable,
|
||||
elementsMap,
|
||||
) ?? null;
|
||||
if (startPoint) {
|
||||
positions.set(0, { point: startPoint, isDragging: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (element.endBinding) {
|
||||
const endBindable = elementsMap.get(element.endBinding.elementId) as
|
||||
| ExcalidrawBindableElement
|
||||
| undefined;
|
||||
if (endBindable) {
|
||||
const endPoint =
|
||||
updateBoundPoint(
|
||||
nextArrow,
|
||||
"endBinding",
|
||||
element.endBinding,
|
||||
endBindable,
|
||||
elementsMap,
|
||||
) ?? null;
|
||||
if (endPoint) {
|
||||
positions.set(element.points.length - 1, {
|
||||
point: endPoint,
|
||||
isDragging: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
positions,
|
||||
};
|
||||
}
|
||||
|
||||
if (startIsDragged === endIsDragged) {
|
||||
return {
|
||||
positions: naiveDraggingPoints,
|
||||
@@ -2165,7 +2246,20 @@ const pointDraggingUpdates = (
|
||||
(updates.startBinding.mode === "orbit" ||
|
||||
!getFeatureFlag("COMPLEX_BINDINGS"))
|
||||
) {
|
||||
updates.suggestedBinding = start.element;
|
||||
updates.suggestedBinding = start.element
|
||||
? {
|
||||
element: start.element,
|
||||
midPoint: getSnapOutlineMidPoint(
|
||||
pointFrom<GlobalPoint>(
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
),
|
||||
start.element,
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
} else if (startIsDragged) {
|
||||
updates.suggestedBinding = app.state.suggestedBinding;
|
||||
@@ -2191,7 +2285,20 @@ const pointDraggingUpdates = (
|
||||
(updates.endBinding.mode === "orbit" ||
|
||||
!getFeatureFlag("COMPLEX_BINDINGS"))
|
||||
) {
|
||||
updates.suggestedBinding = end.element;
|
||||
updates.suggestedBinding = end.element
|
||||
? {
|
||||
element: end.element,
|
||||
midPoint: getSnapOutlineMidPoint(
|
||||
pointFrom<GlobalPoint>(
|
||||
scenePointerX - linearElementEditor.pointerOffset.x,
|
||||
scenePointerY - linearElementEditor.pointerOffset.y,
|
||||
),
|
||||
end.element,
|
||||
elementsMap,
|
||||
app.state.zoom,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
} else if (endIsDragged) {
|
||||
updates.suggestedBinding = app.state.suggestedBinding;
|
||||
@@ -2221,29 +2328,16 @@ const pointDraggingUpdates = (
|
||||
updates.startBinding === undefined
|
||||
? element.startBinding
|
||||
: updates.startBinding === null
|
||||
? null
|
||||
: updates.startBinding,
|
||||
? null
|
||||
: updates.startBinding,
|
||||
endBinding:
|
||||
updates.endBinding === undefined
|
||||
? element.endBinding
|
||||
: updates.endBinding === null
|
||||
? null
|
||||
: updates.endBinding,
|
||||
? null
|
||||
: updates.endBinding,
|
||||
};
|
||||
|
||||
// We need to use a custom intersector to ensure that if there is a big "jump"
|
||||
// in the arrow's position, we can position it with outline avoidance
|
||||
// pixel-perfectly and avoid "dancing" arrows.
|
||||
// NOTE: Direction matters here, so we create two intersectors
|
||||
const startCustomIntersector =
|
||||
start.focusPoint && end.focusPoint
|
||||
? lineSegment(start.focusPoint, end.focusPoint)
|
||||
: undefined;
|
||||
const endCustomIntersector =
|
||||
start.focusPoint && end.focusPoint
|
||||
? lineSegment(end.focusPoint, start.focusPoint)
|
||||
: undefined;
|
||||
|
||||
// Needed to handle a special case where an existing arrow is dragged over
|
||||
// the same element it is bound to on the other side
|
||||
const startIsDraggingOverEndElement =
|
||||
@@ -2260,28 +2354,28 @@ const pointDraggingUpdates = (
|
||||
// We need to update the non-dragged point too if bound,
|
||||
// so we look up the old binding to trigger updateBoundPoint
|
||||
const endBindable = nextArrow.endBinding
|
||||
? end.element ??
|
||||
? (end.element ??
|
||||
(elementsMap.get(
|
||||
nextArrow.endBinding.elementId,
|
||||
)! as ExcalidrawBindableElement)
|
||||
)! as ExcalidrawBindableElement))
|
||||
: null;
|
||||
|
||||
const endLocalPoint = startIsDraggingOverEndElement
|
||||
? nextArrow.points[nextArrow.points.length - 1]
|
||||
: endIsDraggingOverStartElement &&
|
||||
app.state.bindMode !== "inside" &&
|
||||
getFeatureFlag("COMPLEX_BINDINGS")
|
||||
? nextArrow.points[0]
|
||||
: endBindable
|
||||
? updateBoundPoint(
|
||||
element,
|
||||
"endBinding",
|
||||
nextArrow.endBinding,
|
||||
endBindable,
|
||||
elementsMap,
|
||||
endCustomIntersector,
|
||||
) || nextArrow.points[nextArrow.points.length - 1]
|
||||
: nextArrow.points[nextArrow.points.length - 1];
|
||||
app.state.bindMode !== "inside" &&
|
||||
getFeatureFlag("COMPLEX_BINDINGS")
|
||||
? nextArrow.points[0]
|
||||
: endBindable
|
||||
? updateBoundPoint(
|
||||
element,
|
||||
"endBinding",
|
||||
nextArrow.endBinding,
|
||||
endBindable,
|
||||
elementsMap,
|
||||
endIsDragged,
|
||||
) || nextArrow.points[nextArrow.points.length - 1]
|
||||
: nextArrow.points[nextArrow.points.length - 1];
|
||||
|
||||
// We need to keep the simulated next arrow up-to-date, because
|
||||
// updateBoundPoint looks at the opposite point
|
||||
@@ -2290,35 +2384,38 @@ const pointDraggingUpdates = (
|
||||
// We need to update the non-dragged point too if bound,
|
||||
// so we look up the old binding to trigger updateBoundPoint
|
||||
const startBindable = nextArrow.startBinding
|
||||
? start.element ??
|
||||
? (start.element ??
|
||||
(elementsMap.get(
|
||||
nextArrow.startBinding.elementId,
|
||||
)! as ExcalidrawBindableElement)
|
||||
)! as ExcalidrawBindableElement))
|
||||
: null;
|
||||
|
||||
const startLocalPoint =
|
||||
endIsDraggingOverStartElement && getFeatureFlag("COMPLEX_BINDINGS")
|
||||
? nextArrow.points[0]
|
||||
: startIsDraggingOverEndElement &&
|
||||
app.state.bindMode !== "inside" &&
|
||||
getFeatureFlag("COMPLEX_BINDINGS")
|
||||
? nextArrow.points[nextArrow.points.length - 1]
|
||||
: startBindable
|
||||
? updateBoundPoint(
|
||||
element,
|
||||
"startBinding",
|
||||
nextArrow.startBinding,
|
||||
startBindable,
|
||||
elementsMap,
|
||||
startCustomIntersector,
|
||||
) || nextArrow.points[0]
|
||||
: nextArrow.points[0];
|
||||
app.state.bindMode !== "inside" &&
|
||||
getFeatureFlag("COMPLEX_BINDINGS")
|
||||
? endLocalPoint
|
||||
: startBindable
|
||||
? updateBoundPoint(
|
||||
element,
|
||||
"startBinding",
|
||||
nextArrow.startBinding,
|
||||
startBindable,
|
||||
elementsMap,
|
||||
startIsDragged,
|
||||
) || nextArrow.points[0]
|
||||
: nextArrow.points[0];
|
||||
|
||||
const endChanged =
|
||||
pointDistance(
|
||||
endLocalPoint,
|
||||
nextArrow.points[nextArrow.points.length - 1],
|
||||
) !== 0;
|
||||
!startIsDraggingOverEndElement &&
|
||||
!(
|
||||
endIsDraggingOverStartElement &&
|
||||
app.state.bindMode !== "inside" &&
|
||||
getFeatureFlag("COMPLEX_BINDINGS")
|
||||
) &&
|
||||
!!endBindable;
|
||||
const startChanged =
|
||||
pointDistance(startLocalPoint, nextArrow.points[0]) !== 0;
|
||||
|
||||
@@ -2332,13 +2429,7 @@ const pointDraggingUpdates = (
|
||||
const indices = Array.from(indicesSet);
|
||||
|
||||
return {
|
||||
updates:
|
||||
updates.startBinding || updates.suggestedBinding
|
||||
? {
|
||||
startBinding: updates.startBinding,
|
||||
suggestedBinding: updates.suggestedBinding,
|
||||
}
|
||||
: undefined,
|
||||
updates,
|
||||
positions: new Map(
|
||||
indices.map((idx) => {
|
||||
return [
|
||||
@@ -2349,11 +2440,11 @@ const pointDraggingUpdates = (
|
||||
isDragging: true,
|
||||
}
|
||||
: idx === element.points.length - 1
|
||||
? {
|
||||
point: endLocalPoint,
|
||||
isDragging: true,
|
||||
}
|
||||
: naiveDraggingPoints.get(idx)!,
|
||||
? {
|
||||
point: endLocalPoint,
|
||||
isDragging: true,
|
||||
}
|
||||
: naiveDraggingPoints.get(idx)!,
|
||||
];
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -230,8 +230,8 @@ const getTextElementPositionOffsets = (
|
||||
opts.textAlign === "center"
|
||||
? metrics.width / 2
|
||||
: opts.textAlign === "right"
|
||||
? metrics.width
|
||||
: 0,
|
||||
? metrics.width
|
||||
: 0,
|
||||
y: opts.verticalAlign === "middle" ? metrics.height / 2 : 0,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
getVerticalOffset,
|
||||
invariant,
|
||||
applyDarkModeFilter,
|
||||
isSafari,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
@@ -360,8 +361,9 @@ IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||
const drawImagePlaceholder = (
|
||||
element: ExcalidrawImageElement,
|
||||
context: CanvasRenderingContext2D,
|
||||
theme: StaticCanvasRenderConfig["theme"],
|
||||
) => {
|
||||
context.fillStyle = "#E7E7E7";
|
||||
context.fillStyle = theme === THEME.DARK ? "#2E2E2E" : "#E7E7E7";
|
||||
context.fillRect(0, 0, element.width, element.height);
|
||||
|
||||
const imageMinWidthOrHeight = Math.min(element.width, element.height);
|
||||
@@ -443,13 +445,6 @@ const drawElementOnCanvas = (
|
||||
? cacheEntry?.image
|
||||
: undefined;
|
||||
|
||||
const shouldInvertImage =
|
||||
renderConfig.theme === THEME.DARK &&
|
||||
cacheEntry?.mimeType === MIME_TYPES.svg;
|
||||
|
||||
if (shouldInvertImage) {
|
||||
context.filter = DARK_THEME_FILTER;
|
||||
}
|
||||
if (img != null && !(img instanceof Promise)) {
|
||||
if (element.roundness && context.roundRect) {
|
||||
context.beginPath();
|
||||
@@ -472,19 +467,78 @@ const drawElementOnCanvas = (
|
||||
height: img.naturalHeight,
|
||||
};
|
||||
|
||||
context.drawImage(
|
||||
img,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
0 /* hardcoded for the selection box*/,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
);
|
||||
const shouldInvertImage =
|
||||
renderConfig.theme === THEME.DARK &&
|
||||
cacheEntry?.mimeType === MIME_TYPES.svg;
|
||||
|
||||
if (shouldInvertImage && isSafari) {
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
tempCanvas.width = element.width * devicePixelRatio;
|
||||
tempCanvas.height = element.height * devicePixelRatio;
|
||||
const tempContext = tempCanvas.getContext("2d");
|
||||
|
||||
if (tempContext) {
|
||||
tempContext.scale(devicePixelRatio, devicePixelRatio);
|
||||
tempContext.drawImage(
|
||||
img,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
0,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
);
|
||||
|
||||
const imageData = tempContext.getImageData(
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height,
|
||||
);
|
||||
|
||||
const data = imageData.data;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
data[i] = 255 - data[i];
|
||||
data[i + 1] = 255 - data[i + 1];
|
||||
data[i + 2] = 255 - data[i + 2];
|
||||
}
|
||||
|
||||
tempContext.putImageData(imageData, 0, 0);
|
||||
context.drawImage(
|
||||
tempCanvas,
|
||||
0,
|
||||
0,
|
||||
tempCanvas.width,
|
||||
tempCanvas.height,
|
||||
0,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (shouldInvertImage) {
|
||||
context.filter = DARK_THEME_FILTER;
|
||||
}
|
||||
|
||||
context.drawImage(
|
||||
img,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
0 /* hardcoded for the selection box*/,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
drawImagePlaceholder(element, context);
|
||||
drawImagePlaceholder(element, context, renderConfig.theme);
|
||||
}
|
||||
context.restore();
|
||||
break;
|
||||
@@ -514,8 +568,8 @@ const drawElementOnCanvas = (
|
||||
element.textAlign === "center"
|
||||
? element.width / 2
|
||||
: element.textAlign === "right"
|
||||
? element.width
|
||||
: 0;
|
||||
? element.width
|
||||
: 0;
|
||||
|
||||
const lineHeightPx = getLineHeightInPx(
|
||||
element.fontSize,
|
||||
|
||||
@@ -819,8 +819,8 @@ export const resizeSingleElement = (
|
||||
nextHeight,
|
||||
origElement.angle,
|
||||
handleDirection,
|
||||
shouldMaintainAspectRatio!!,
|
||||
shouldResizeFromCenter!!,
|
||||
shouldMaintainAspectRatio,
|
||||
shouldResizeFromCenter,
|
||||
);
|
||||
|
||||
if (isLinearElement(origElement) && rescaledPoints.points) {
|
||||
@@ -1370,7 +1370,7 @@ export const resizeMultipleElements = (
|
||||
false,
|
||||
);
|
||||
|
||||
const update: typeof elementsAndUpdates[0]["update"] = {
|
||||
const update: (typeof elementsAndUpdates)[0]["update"] = {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
|
||||
@@ -137,22 +137,28 @@ export const getElementWithTransformHandleType = (
|
||||
elementsMap: ElementsMap,
|
||||
editorInterface: EditorInterface,
|
||||
) => {
|
||||
return elements.reduce((result, element) => {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const transformHandleType = resizeTest(
|
||||
element,
|
||||
elementsMap,
|
||||
appState,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
zoom,
|
||||
pointerType,
|
||||
editorInterface,
|
||||
);
|
||||
return transformHandleType ? { element, transformHandleType } : null;
|
||||
}, null as { element: NonDeletedExcalidrawElement; transformHandleType: MaybeTransformHandleType } | null);
|
||||
return elements.reduce(
|
||||
(result, element) => {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const transformHandleType = resizeTest(
|
||||
element,
|
||||
elementsMap,
|
||||
appState,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
zoom,
|
||||
pointerType,
|
||||
editorInterface,
|
||||
);
|
||||
return transformHandleType ? { element, transformHandleType } : null;
|
||||
},
|
||||
null as {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
transformHandleType: MaybeTransformHandleType;
|
||||
} | null,
|
||||
);
|
||||
};
|
||||
|
||||
export const getTransformHandleTypeFromCoords = <
|
||||
|
||||
@@ -223,10 +223,10 @@ export const getTargetElements = (
|
||||
appState.editingTextElement
|
||||
? [appState.editingTextElement]
|
||||
: appState.newElement
|
||||
? [appState.newElement]
|
||||
: getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
});
|
||||
? [appState.newElement]
|
||||
: getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* returns prevState's selectedElementids if no change from previous, so as to
|
||||
|
||||
@@ -201,8 +201,8 @@ export const generateRoughOptions = (
|
||||
element.strokeStyle === "dashed"
|
||||
? getDashArrayDashed(element.strokeWidth)
|
||||
: element.strokeStyle === "dotted"
|
||||
? getDashArrayDotted(element.strokeWidth)
|
||||
: undefined,
|
||||
? getDashArrayDotted(element.strokeWidth)
|
||||
: undefined,
|
||||
// for non-solid strokes, disable multiStroke because it tends to make
|
||||
// dashes/dots overlay each other
|
||||
disableMultiStroke: element.strokeStyle !== "solid",
|
||||
@@ -235,8 +235,8 @@ export const generateRoughOptions = (
|
||||
options.fill = isTransparent(element.backgroundColor)
|
||||
? undefined
|
||||
: isDarkMode
|
||||
? applyDarkModeFilter(element.backgroundColor)
|
||||
: element.backgroundColor;
|
||||
? applyDarkModeFilter(element.backgroundColor)
|
||||
: element.backgroundColor;
|
||||
if (element.type === "ellipse") {
|
||||
options.curveFitting = 1;
|
||||
}
|
||||
@@ -250,8 +250,8 @@ export const generateRoughOptions = (
|
||||
element.backgroundColor === "transparent"
|
||||
? undefined
|
||||
: isDarkMode
|
||||
? applyDarkModeFilter(element.backgroundColor)
|
||||
: element.backgroundColor;
|
||||
? applyDarkModeFilter(element.backgroundColor)
|
||||
: element.backgroundColor;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -708,20 +708,20 @@ const _generateElementShape = (
|
||||
rightX - verticalRadius
|
||||
} ${rightY - horizontalRadius}
|
||||
C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
|
||||
rightX - verticalRadius
|
||||
} ${rightY + horizontalRadius}
|
||||
rightX - verticalRadius
|
||||
} ${rightY + horizontalRadius}
|
||||
L ${bottomX + verticalRadius} ${bottomY - horizontalRadius}
|
||||
C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
|
||||
bottomX - verticalRadius
|
||||
} ${bottomY - horizontalRadius}
|
||||
bottomX - verticalRadius
|
||||
} ${bottomY - horizontalRadius}
|
||||
L ${leftX + verticalRadius} ${leftY + horizontalRadius}
|
||||
C ${leftX} ${leftY}, ${leftX} ${leftY}, ${leftX + verticalRadius} ${
|
||||
leftY - horizontalRadius
|
||||
}
|
||||
leftY - horizontalRadius
|
||||
}
|
||||
L ${topX - verticalRadius} ${topY + horizontalRadius}
|
||||
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
|
||||
topY + horizontalRadius
|
||||
}`,
|
||||
topY + horizontalRadius
|
||||
}`,
|
||||
generateRoughOptions(element, true, isDarkMode),
|
||||
);
|
||||
} else {
|
||||
@@ -1057,8 +1057,8 @@ export const getFreedrawOutlinePoints = (
|
||||
const inputPoints = element.simulatePressure
|
||||
? element.points
|
||||
: element.points.length
|
||||
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
|
||||
: [[0, 0, 0.5]];
|
||||
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
|
||||
: [[0, 0, 0.5]];
|
||||
|
||||
return getStroke(inputPoints as number[][], {
|
||||
simulatePressure: element.simulatePressure,
|
||||
|
||||
@@ -10,13 +10,13 @@ export const showSelectedShapeActions = (
|
||||
) =>
|
||||
Boolean(
|
||||
!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||
((appState.activeTool.type !== "custom" &&
|
||||
(appState.editingTextElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
appState.activeTool.type !== "lasso" &&
|
||||
appState.activeTool.type !== "eraser" &&
|
||||
appState.activeTool.type !== "hand" &&
|
||||
appState.activeTool.type !== "laser"))) ||
|
||||
getSelectedElements(elements, appState).length),
|
||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||
((appState.activeTool.type !== "custom" &&
|
||||
(appState.editingTextElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
appState.activeTool.type !== "lasso" &&
|
||||
appState.activeTool.type !== "eraser" &&
|
||||
appState.activeTool.type !== "hand" &&
|
||||
appState.activeTool.type !== "laser"))) ||
|
||||
getSelectedElements(elements, appState).length),
|
||||
);
|
||||
|
||||
@@ -89,7 +89,7 @@ const CJK = {
|
||||
* ↑ BREAK BEFORE "「"
|
||||
* ↑ BREAK AFTER "」"
|
||||
*/
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
// prettier-ignore
|
||||
OPENING:/([{〈《⦅「「『【〖〔〘〚<〝/u,
|
||||
CLOSING: /)]}〉》⦆」」』】〗〕〙〛>。.,、〟‥?!:;・〜〞/u,
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user