mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Compare commits
15 commits
6fc85022ae
...
195a743874
Author | SHA1 | Date | |
---|---|---|---|
|
195a743874 | ||
|
4a60fe3d22 | ||
|
2a0d15799c | ||
|
a18b139a60 | ||
|
1913599594 | ||
|
debf2ad608 | ||
|
8fb2f70414 | ||
|
5fc13e4309 | ||
|
b5d60973b7 | ||
|
a5d6939826 | ||
|
0cf36d6b30 | ||
|
58f7d33d80 | ||
|
6fe7de8020 | ||
|
01304aac49 | ||
|
dff69e9191 |
117 changed files with 3386 additions and 1959 deletions
|
@ -32,6 +32,12 @@
|
||||||
"name": "jotai",
|
"name": "jotai",
|
||||||
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-no-target-blank": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowReferrer": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
||||||
|
|
||||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
|
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should be a valid React Node.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ function App() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This will only for `Desktop` devices.
|
This will only work for `Desktop` devices.
|
||||||
|
|
||||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||||
|
|
||||||
|
@ -65,4 +65,4 @@ const App = () => (
|
||||||
// Need to render when code is span across multiple components
|
// Need to render when code is span across multiple components
|
||||||
// in Live Code blocks editor
|
// in Live Code blocks editor
|
||||||
render(<App />);
|
render(<App />);
|
||||||
```
|
```
|
||||||
|
|
|
@ -31,6 +31,7 @@ All `props` are _optional_.
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
|
||||||
|
|
||||||
### Storing custom data on Excalidraw elements
|
### Storing custom data on Excalidraw elements
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ export default function ExampleApp({
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||||
|
const [renderScrollbars, setRenderScrollbars] = useState(false);
|
||||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||||
|
@ -192,6 +193,7 @@ export default function ExampleApp({
|
||||||
}) => setPointerData(payload),
|
}) => setPointerData(payload),
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
|
renderScrollbars,
|
||||||
gridModeEnabled,
|
gridModeEnabled,
|
||||||
theme,
|
theme,
|
||||||
name: "Custom name of drawing",
|
name: "Custom name of drawing",
|
||||||
|
@ -710,6 +712,14 @@ export default function ExampleApp({
|
||||||
/>
|
/>
|
||||||
Grid mode
|
Grid mode
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={renderScrollbars}
|
||||||
|
onChange={() => setRenderScrollbars(!renderScrollbars)}
|
||||||
|
/>
|
||||||
|
Render scrollbars
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
@ -73,7 +73,7 @@ export const AIComponents = ({
|
||||||
</br>
|
</br>
|
||||||
<div>You can also try <a href="${
|
<div>You can also try <a href="${
|
||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noopener">Excalidraw+</a> to get more requests.</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`,
|
</html>`,
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
|
||||||
className="encrypted-icon tooltip"
|
className="encrypted-icon tooltip"
|
||||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
aria-label={t("encrypted.link")}
|
aria-label={t("encrypted.link")}
|
||||||
>
|
>
|
||||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const ExcalidrawPlusAppLink = () => {
|
||||||
import.meta.env.VITE_APP_PLUS_APP
|
import.meta.env.VITE_APP_PLUS_APP
|
||||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
className="plus-button"
|
className="plus-button"
|
||||||
>
|
>
|
||||||
Go to Excalidraw+
|
Go to Excalidraw+
|
||||||
|
|
|
@ -198,7 +198,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
||||||
<a
|
<a
|
||||||
class="welcome-screen-menu-item "
|
class="welcome-screen-menu-item "
|
||||||
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -112,12 +112,14 @@ export const YOUTUBE_STATES = {
|
||||||
export const ENV = {
|
export const ENV = {
|
||||||
TEST: "test",
|
TEST: "test",
|
||||||
DEVELOPMENT: "development",
|
DEVELOPMENT: "development",
|
||||||
|
PRODUCTION: "production",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CLASSES = {
|
export const CLASSES = {
|
||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
ZOOM_ACTIONS: "zoom-actions",
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||||
|
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
||||||
|
@ -318,6 +320,9 @@ export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
|
||||||
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
|
export const MAX_ALLOWED_FILE_BYTES = 4 * 1024 * 1024;
|
||||||
|
|
||||||
export const SVG_NS = "http://www.w3.org/2000/svg";
|
export const SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
export const SVG_DOCUMENT_PREAMBLE = `<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
`;
|
||||||
|
|
||||||
export const ENCRYPTION_KEY_BITS = 128;
|
export const ENCRYPTION_KEY_BITS = 128;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { average } from "@excalidraw/math";
|
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
FontString,
|
FontString,
|
||||||
|
ExcalidrawElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -679,7 +680,7 @@ export const arrayToMap = <T extends { id: string } | string>(
|
||||||
return items.reduce((acc: Map<string, T>, element) => {
|
return items.reduce((acc: Map<string, T>, element) => {
|
||||||
acc.set(typeof element === "string" ? element : element.id, element);
|
acc.set(typeof element === "string" ? element : element.id, element);
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map());
|
}, new Map() as Map<string, T>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const arrayToMapWithIndex = <T extends { id: string }>(
|
export const arrayToMapWithIndex = <T extends { id: string }>(
|
||||||
|
@ -738,6 +739,8 @@ export const isTestEnv = () => import.meta.env.MODE === ENV.TEST;
|
||||||
|
|
||||||
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
export const isDevEnv = () => import.meta.env.MODE === ENV.DEVELOPMENT;
|
||||||
|
|
||||||
|
export const isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
|
||||||
|
|
||||||
export const isServerEnv = () =>
|
export const isServerEnv = () =>
|
||||||
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
||||||
|
|
||||||
|
@ -1201,3 +1204,32 @@ export const escapeDoubleQuotes = (str: string) => {
|
||||||
|
|
||||||
export const castArray = <T>(value: T | T[]): T[] =>
|
export const castArray = <T>(value: T | T[]): T[] =>
|
||||||
Array.isArray(value) ? value : [value];
|
Array.isArray(value) ? value : [value];
|
||||||
|
|
||||||
|
export const elementCenterPoint = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
xOffset: number = 0,
|
||||||
|
yOffset: number = 0,
|
||||||
|
) => {
|
||||||
|
const { x, y, width, height } = element;
|
||||||
|
|
||||||
|
const centerXPoint = x + width / 2 + xOffset;
|
||||||
|
|
||||||
|
const centerYPoint = y + height / 2 + yOffset;
|
||||||
|
|
||||||
|
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** hack for Array.isArray type guard not working with readonly value[] */
|
||||||
|
export const isReadonlyArray = (value?: any): value is readonly any[] => {
|
||||||
|
return Array.isArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sizeOf = (
|
||||||
|
value: readonly number[] | Readonly<Map<any, any>> | Record<any, any>,
|
||||||
|
): number => {
|
||||||
|
return isReadonlyArray(value)
|
||||||
|
? value.length
|
||||||
|
: value instanceof Map
|
||||||
|
? value.size
|
||||||
|
: Object.keys(value).length;
|
||||||
|
};
|
||||||
|
|
|
@ -6,12 +6,14 @@ import {
|
||||||
toBrandedType,
|
toBrandedType,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
|
isReadonlyArray,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { isNonDeletedElement } from "@excalidraw/element";
|
import { isNonDeletedElement } from "@excalidraw/element";
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
||||||
import { getElementsInGroup } from "@excalidraw/element/groups";
|
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
orderByFractionalIndex,
|
||||||
syncInvalidIndices,
|
syncInvalidIndices,
|
||||||
syncMovedIndices,
|
syncMovedIndices,
|
||||||
validateFractionalIndices,
|
validateFractionalIndices,
|
||||||
|
@ -19,7 +21,11 @@ import {
|
||||||
|
|
||||||
import { getSelectedElements } from "@excalidraw/element/selection";
|
import { getSelectedElements } from "@excalidraw/element/selection";
|
||||||
|
|
||||||
import type { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import {
|
||||||
|
mutateElement,
|
||||||
|
type ElementUpdate,
|
||||||
|
} from "@excalidraw/element/mutateElement";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
@ -32,12 +38,13 @@ import type {
|
||||||
Ordered,
|
Ordered,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Assert, SameType } from "@excalidraw/common/utility-types";
|
import type {
|
||||||
|
Assert,
|
||||||
|
Mutable,
|
||||||
|
SameType,
|
||||||
|
} from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../../excalidraw/types";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
|
||||||
|
|
||||||
type SceneStateCallback = () => void;
|
type SceneStateCallback = () => void;
|
||||||
type SceneStateCallbackRemover = () => void;
|
type SceneStateCallbackRemover = () => void;
|
||||||
|
@ -102,44 +109,7 @@ const hashSelectionOpts = (
|
||||||
// in our codebase
|
// in our codebase
|
||||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||||
|
|
||||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
|
||||||
if (typeof elementKey === "string") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Scene {
|
class Scene {
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// static methods/props
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
|
||||||
private static sceneMapById = new Map<string, Scene>();
|
|
||||||
|
|
||||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
|
||||||
if (isIdKey(elementKey)) {
|
|
||||||
// for cases where we don't have access to the element object
|
|
||||||
// (e.g. restore serialized appState with id references)
|
|
||||||
this.sceneMapById.set(elementKey, scene);
|
|
||||||
} else {
|
|
||||||
this.sceneMapByElement.set(elementKey, scene);
|
|
||||||
// if mapping element objects, also cache the id string when later
|
|
||||||
// looking up by id alone
|
|
||||||
this.sceneMapById.set(elementKey.id, scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated pass down `app.scene` and use it directly
|
|
||||||
*/
|
|
||||||
static getScene(elementKey: ElementKey): Scene | null {
|
|
||||||
if (isIdKey(elementKey)) {
|
|
||||||
return this.sceneMapById.get(elementKey) || null;
|
|
||||||
}
|
|
||||||
return this.sceneMapByElement.get(elementKey) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// instance methods/props
|
// instance methods/props
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -198,6 +168,12 @@ class Scene {
|
||||||
return this.frames;
|
return this.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(elements: ElementsMapOrArray | null = null) {
|
||||||
|
if (elements) {
|
||||||
|
this.replaceAllElements(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getSelectedElements(opts: {
|
getSelectedElements(opts: {
|
||||||
// NOTE can be ommitted by making Scene constructor require App instance
|
// NOTE can be ommitted by making Scene constructor require App instance
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
|
@ -292,23 +268,25 @@ class Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||||
const _nextElements =
|
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
if (!isReadonlyArray(nextElements)) {
|
||||||
nextElements instanceof Array
|
// need to order by fractional indices to get the correct order
|
||||||
? nextElements
|
nextElements = orderByFractionalIndex(
|
||||||
: Array.from(nextElements.values());
|
Array.from(nextElements.values()) as OrderedExcalidrawElement[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
||||||
validateIndicesThrottled(_nextElements);
|
validateIndicesThrottled(nextElements);
|
||||||
|
|
||||||
this.elements = syncInvalidIndices(_nextElements);
|
this.elements = syncInvalidIndices(nextElements);
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
this.elements.forEach((element) => {
|
this.elements.forEach((element) => {
|
||||||
if (isFrameLikeElement(element)) {
|
if (isFrameLikeElement(element)) {
|
||||||
nextFrameLikes.push(element);
|
nextFrameLikes.push(element);
|
||||||
}
|
}
|
||||||
this.elementsMap.set(element.id, element);
|
this.elementsMap.set(element.id, element);
|
||||||
Scene.mapElementToScene(element, this);
|
|
||||||
});
|
});
|
||||||
const nonDeletedElements = getNonDeletedElements(this.elements);
|
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||||
this.nonDeletedElements = nonDeletedElements.elements;
|
this.nonDeletedElements = nonDeletedElements.elements;
|
||||||
|
@ -353,12 +331,6 @@ class Scene {
|
||||||
this.selectedElementsCache.elements = null;
|
this.selectedElementsCache.elements = null;
|
||||||
this.selectedElementsCache.cache.clear();
|
this.selectedElementsCache.cache.clear();
|
||||||
|
|
||||||
Scene.sceneMapById.forEach((scene, elementKey) => {
|
|
||||||
if (scene === this) {
|
|
||||||
Scene.sceneMapById.delete(elementKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// done not for memory leaks, but to guard against possible late fires
|
// done not for memory leaks, but to guard against possible late fires
|
||||||
// (I guess?)
|
// (I guess?)
|
||||||
this.callbacks.clear();
|
this.callbacks.clear();
|
||||||
|
@ -455,6 +427,42 @@ class Scene {
|
||||||
// then, check if the id is a group
|
// then, check if the id is a group
|
||||||
return getElementsInGroup(elementsMap, id);
|
return getElementsInGroup(elementsMap, id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mutate an element with passed updates and trigger the component to update. Make sure you
|
||||||
|
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||||
|
mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||||
|
element: TElement,
|
||||||
|
updates: ElementUpdate<TElement>,
|
||||||
|
options: {
|
||||||
|
informMutation: boolean;
|
||||||
|
isDragging: boolean;
|
||||||
|
} = {
|
||||||
|
informMutation: true,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const elementsMap = this.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
const { version: prevVersion } = element;
|
||||||
|
const { version: nextVersion } = mutateElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
updates,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
// skip if the element is not in the scene (i.e. selection)
|
||||||
|
this.elementsMap.has(element.id) &&
|
||||||
|
// skip if the element's version hasn't changed, as mutateElement returned the same element
|
||||||
|
prevVersion !== nextVersion &&
|
||||||
|
options.informMutation
|
||||||
|
) {
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Scene;
|
export default Scene;
|
|
@ -1,12 +1,11 @@
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBoundingBox } from "./bounds";
|
import { getCommonBoundingBox } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { BoundingBox } from "./bounds";
|
import type { BoundingBox } from "./bounds";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
position: "start" | "center" | "end";
|
position: "start" | "center" | "end";
|
||||||
|
@ -15,10 +14,10 @@ export interface Alignment {
|
||||||
|
|
||||||
export const alignElements = (
|
export const alignElements = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -33,12 +32,13 @@ export const alignElements = (
|
||||||
);
|
);
|
||||||
return group.map((element) => {
|
return group.map((element) => {
|
||||||
// update element
|
// update element
|
||||||
const updatedEle = mutateElement(element, {
|
const updatedEle = scene.mutateElement(element, {
|
||||||
x: element.x + translation.x,
|
x: element.x + translation.x,
|
||||||
y: element.y + translation.y,
|
y: element.y + translation.y,
|
||||||
});
|
});
|
||||||
|
|
||||||
// update bound elements
|
// update bound elements
|
||||||
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: group,
|
simultaneouslyUpdated: group,
|
||||||
});
|
});
|
||||||
return updatedEle;
|
return updatedEle;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
invariant,
|
invariant,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
|
elementCenterPoint,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -30,8 +31,6 @@ import { isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
@ -55,7 +54,6 @@ import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isBindableElement,
|
isBindableElement,
|
||||||
isBindingElement,
|
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
|
@ -68,6 +66,8 @@ import {
|
||||||
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
import type {
|
import type {
|
||||||
|
@ -81,10 +81,8 @@ import type {
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
OrderedExcalidrawElement,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
SceneElementsMap,
|
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
@ -130,7 +128,6 @@ export const bindOrUnbindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startBindingElement: ExcalidrawBindableElement | null | "keep",
|
startBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||||
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||||
|
@ -142,7 +139,7 @@ export const bindOrUnbindLinearElement = (
|
||||||
"start",
|
"start",
|
||||||
boundToElementIds,
|
boundToElementIds,
|
||||||
unboundFromElementIds,
|
unboundFromElementIds,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
bindOrUnbindLinearElementEdge(
|
bindOrUnbindLinearElementEdge(
|
||||||
linearElement,
|
linearElement,
|
||||||
|
@ -151,7 +148,7 @@ export const bindOrUnbindLinearElement = (
|
||||||
"end",
|
"end",
|
||||||
boundToElementIds,
|
boundToElementIds,
|
||||||
unboundFromElementIds,
|
unboundFromElementIds,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
||||||
|
@ -159,7 +156,7 @@ export const bindOrUnbindLinearElement = (
|
||||||
);
|
);
|
||||||
|
|
||||||
getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
|
getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
boundElements: element.boundElements?.filter(
|
boundElements: element.boundElements?.filter(
|
||||||
(element) =>
|
(element) =>
|
||||||
element.type !== "arrow" || element.id !== linearElement.id,
|
element.type !== "arrow" || element.id !== linearElement.id,
|
||||||
|
@ -177,7 +174,7 @@ const bindOrUnbindLinearElementEdge = (
|
||||||
boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
|
boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||||
// Is mutated
|
// Is mutated
|
||||||
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
||||||
if (bindableElement === "keep") {
|
if (bindableElement === "keep") {
|
||||||
|
@ -186,7 +183,7 @@ const bindOrUnbindLinearElementEdge = (
|
||||||
|
|
||||||
// null means break the bind, so nothing to consider here
|
// null means break the bind, so nothing to consider here
|
||||||
if (bindableElement === null) {
|
if (bindableElement === null) {
|
||||||
const unbound = unbindLinearElement(linearElement, startOrEnd);
|
const unbound = unbindLinearElement(linearElement, startOrEnd, scene);
|
||||||
if (unbound != null) {
|
if (unbound != null) {
|
||||||
unboundFromElementIds.add(unbound);
|
unboundFromElementIds.add(unbound);
|
||||||
}
|
}
|
||||||
|
@ -209,16 +206,11 @@ const bindOrUnbindLinearElementEdge = (
|
||||||
: startOrEnd === "start" ||
|
: startOrEnd === "start" ||
|
||||||
otherEdgeBindableElement.id !== bindableElement.id)
|
otherEdgeBindableElement.id !== bindableElement.id)
|
||||||
) {
|
) {
|
||||||
bindLinearElement(
|
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||||
linearElement,
|
|
||||||
bindableElement,
|
|
||||||
startOrEnd,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
boundToElementIds.add(bindableElement.id);
|
boundToElementIds.add(bindableElement.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap);
|
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||||
boundToElementIds.add(bindableElement.id);
|
boundToElementIds.add(bindableElement.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -283,15 +275,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: null // If binding is disabled and start is dragged, break all binds
|
: null // If binding is disabled and start is dragged, break all binds
|
||||||
: !isElbowArrow(selectedElement)
|
|
||||||
? // We have to update the focus and gap of the binding, so let's rebind
|
|
||||||
getElligibleElementForBindingElement(
|
|
||||||
selectedElement,
|
|
||||||
"start",
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
zoom,
|
|
||||||
)
|
|
||||||
: "keep";
|
: "keep";
|
||||||
const end = endDragged
|
const end = endDragged
|
||||||
? isBindingEnabled
|
? isBindingEnabled
|
||||||
|
@ -303,15 +286,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: null // If binding is disabled and end is dragged, break all binds
|
: null // If binding is disabled and end is dragged, break all binds
|
||||||
: !isElbowArrow(selectedElement)
|
|
||||||
? // We have to update the focus and gap of the binding, so let's rebind
|
|
||||||
getElligibleElementForBindingElement(
|
|
||||||
selectedElement,
|
|
||||||
"end",
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
zoom,
|
|
||||||
)
|
|
||||||
: "keep";
|
: "keep";
|
||||||
|
|
||||||
return [start, end];
|
return [start, end];
|
||||||
|
@ -362,11 +336,9 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
||||||
|
|
||||||
export const bindOrUnbindLinearElements = (
|
export const bindOrUnbindLinearElements = (
|
||||||
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
|
||||||
isBindingEnabled: boolean,
|
isBindingEnabled: boolean,
|
||||||
draggingPoints: readonly number[] | null,
|
draggingPoints: readonly number[] | null,
|
||||||
|
scene: Scene,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): void => {
|
): void => {
|
||||||
selectedElements.forEach((selectedElement) => {
|
selectedElements.forEach((selectedElement) => {
|
||||||
|
@ -376,20 +348,20 @@ export const bindOrUnbindLinearElements = (
|
||||||
selectedElement,
|
selectedElement,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
draggingPoints ?? [],
|
draggingPoints ?? [],
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
elements,
|
scene.getNonDeletedElements(),
|
||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: // The arrow itself (the shaft) or the inner joins are dragged
|
: // The arrow itself (the shaft) or the inner joins are dragged
|
||||||
getBindingStrategyForDraggingArrowOrJoints(
|
getBindingStrategyForDraggingArrowOrJoints(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
elements,
|
scene.getNonDeletedElements(),
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
zoom,
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
|
bindOrUnbindLinearElement(selectedElement, start, end, scene);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -429,15 +401,17 @@ export const maybeBindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
pointerCoords: { x: number; y: number },
|
pointerCoords: { x: number; y: number },
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
): void => {
|
): void => {
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
if (appState.startBoundElement != null) {
|
if (appState.startBoundElement != null) {
|
||||||
bindLinearElement(
|
bindLinearElement(
|
||||||
linearElement,
|
linearElement,
|
||||||
appState.startBoundElement,
|
appState.startBoundElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,7 +432,7 @@ export const maybeBindLinearElement = (
|
||||||
"end",
|
"end",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
|
bindLinearElement(linearElement, hoveredElement, "end", scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -487,7 +461,7 @@ export const bindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
if (!isArrowElement(linearElement)) {
|
if (!isArrowElement(linearElement)) {
|
||||||
return;
|
return;
|
||||||
|
@ -500,7 +474,7 @@ export const bindLinearElement = (
|
||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
),
|
),
|
||||||
|
@ -513,18 +487,17 @@ export const bindLinearElement = (
|
||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
elementsMap,
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(linearElement, {
|
scene.mutateElement(linearElement, {
|
||||||
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
|
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
|
||||||
if (!boundElementsMap.has(linearElement.id)) {
|
if (!boundElementsMap.has(linearElement.id)) {
|
||||||
mutateElement(hoveredElement, {
|
scene.mutateElement(hoveredElement, {
|
||||||
boundElements: (hoveredElement.boundElements || []).concat({
|
boundElements: (hoveredElement.boundElements || []).concat({
|
||||||
id: linearElement.id,
|
id: linearElement.id,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
|
@ -566,13 +539,14 @@ const isLinearElementSimple = (
|
||||||
const unbindLinearElement = (
|
const unbindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
scene: Scene,
|
||||||
): ExcalidrawBindableElement["id"] | null => {
|
): ExcalidrawBindableElement["id"] | null => {
|
||||||
const field = startOrEnd === "start" ? "startBinding" : "endBinding";
|
const field = startOrEnd === "start" ? "startBinding" : "endBinding";
|
||||||
const binding = linearElement[field];
|
const binding = linearElement[field];
|
||||||
if (binding == null) {
|
if (binding == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
mutateElement(linearElement, { [field]: null });
|
scene.mutateElement(linearElement, { [field]: null });
|
||||||
return binding.elementId;
|
return binding.elementId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -735,25 +709,30 @@ const calculateFocusAndGap = (
|
||||||
|
|
||||||
// Supports translating, rotating and scaling `changedElement` with bound
|
// Supports translating, rotating and scaling `changedElement` with bound
|
||||||
// linear elements.
|
// linear elements.
|
||||||
// Because scaling involves moving the focus points as well, it is
|
|
||||||
// done before the `changedElement` is updated, and the `newSize` is passed
|
|
||||||
// in explicitly.
|
|
||||||
export const updateBoundElements = (
|
export const updateBoundElements = (
|
||||||
changedElement: NonDeletedExcalidrawElement,
|
changedElement: NonDeletedExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
scene: Scene,
|
||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
changedElements?: Map<string, ExcalidrawElement>;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
|
if (!isBindableElement(changedElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||||
simultaneouslyUpdated,
|
simultaneouslyUpdated,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isBindableElement(changedElement)) {
|
let elementsMap: ElementsMap = scene.getNonDeletedElementsMap();
|
||||||
return;
|
if (options?.changedElements) {
|
||||||
|
elementsMap = new Map(elementsMap) as typeof elementsMap;
|
||||||
|
options.changedElements.forEach((element) => {
|
||||||
|
elementsMap.set(element.id, element);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||||
|
@ -796,7 +775,7 @@ export const updateBoundElements = (
|
||||||
|
|
||||||
// `linearElement` is being moved/scaled already, just update the binding
|
// `linearElement` is being moved/scaled already, just update the binding
|
||||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||||
mutateElement(element, bindings, true);
|
scene.mutateElement(element, bindings);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -843,27 +822,41 @@ export const updateBoundElements = (
|
||||||
}> => update !== null,
|
}> => update !== null,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(element, scene, updates, {
|
||||||
element,
|
...(changedElement.id === element.startBinding?.elementId
|
||||||
updates,
|
? { startBinding: bindings.startBinding }
|
||||||
{
|
: {}),
|
||||||
...(changedElement.id === element.startBinding?.elementId
|
...(changedElement.id === element.endBinding?.elementId
|
||||||
? { startBinding: bindings.startBinding }
|
? { endBinding: bindings.endBinding }
|
||||||
: {}),
|
: {}),
|
||||||
...(changedElement.id === element.endBinding?.elementId
|
});
|
||||||
? { endBinding: bindings.endBinding }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !boundText.isDeleted) {
|
if (boundText && !boundText.isDeleted) {
|
||||||
handleBindTextResize(element, elementsMap, false);
|
handleBindTextResize(element, scene, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateBindings = (
|
||||||
|
latestElement: ExcalidrawElement,
|
||||||
|
scene: Scene,
|
||||||
|
options?: {
|
||||||
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
|
newSize?: { width: number; height: number };
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if (isLinearElement(latestElement)) {
|
||||||
|
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
||||||
|
} else {
|
||||||
|
updateBoundElements(latestElement, scene, {
|
||||||
|
...options,
|
||||||
|
changedElements: new Map([[latestElement.id, latestElement]]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const doesNeedUpdate = (
|
const doesNeedUpdate = (
|
||||||
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
changedElement: ExcalidrawBindableElement,
|
changedElement: ExcalidrawBindableElement,
|
||||||
|
@ -885,7 +878,6 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
otherPoint: Readonly<GlobalPoint>,
|
otherPoint: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||||
aabb: Bounds | undefined | null,
|
aabb: Bounds | undefined | null,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): Heading => {
|
): Heading => {
|
||||||
|
@ -895,22 +887,11 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
return otherPointHeading;
|
return otherPointHeading;
|
||||||
}
|
}
|
||||||
|
|
||||||
const distance = getDistanceForBinding(
|
const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
|
||||||
origPoint,
|
|
||||||
bindableElement,
|
|
||||||
elementsMap,
|
|
||||||
zoom,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!distance) {
|
if (!distance) {
|
||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
vectorFromPoint(
|
vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
||||||
p,
|
|
||||||
pointFrom<GlobalPoint>(
|
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -920,7 +901,6 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
const getDistanceForBinding = (
|
const getDistanceForBinding = (
|
||||||
point: Readonly<GlobalPoint>,
|
point: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const distance = distanceToBindableElement(bindableElement, point);
|
const distance = distanceToBindableElement(bindableElement, point);
|
||||||
|
@ -1040,10 +1020,7 @@ export const avoidRectangularCorner = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawBindableElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
|
||||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||||
|
@ -1140,10 +1117,9 @@ export const snapToMid = (
|
||||||
tolerance: number = 0.05,
|
tolerance: number = 0.05,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const { x, y, width, height, angle } = element;
|
const { x, y, width, height, angle } = element;
|
||||||
const center = pointFrom<GlobalPoint>(
|
|
||||||
x + width / 2 - 0.1,
|
const center = elementCenterPoint(element, -0.1, -0.1);
|
||||||
y + height / 2 - 0.1,
|
|
||||||
);
|
|
||||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||||
|
|
||||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||||
|
@ -1226,12 +1202,8 @@ const updateBoundPoint = (
|
||||||
linearElement,
|
linearElement,
|
||||||
bindableElement,
|
bindableElement,
|
||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
const globalMidPoint = elementCenterPoint(bindableElement);
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
);
|
|
||||||
const global = pointFrom<GlobalPoint>(
|
const global = pointFrom<GlobalPoint>(
|
||||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||||
|
@ -1275,10 +1247,7 @@ const updateBoundPoint = (
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(bindableElement);
|
||||||
bindableElement.x + bindableElement.width / 2,
|
|
||||||
bindableElement.y + bindableElement.height / 2,
|
|
||||||
);
|
|
||||||
const interceptorLength =
|
const interceptorLength =
|
||||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||||
pointDistance(adjacentPoint, center) +
|
pointDistance(adjacentPoint, center) +
|
||||||
|
@ -1336,7 +1305,6 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||||
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): { fixedPoint: FixedPoint } => {
|
): { fixedPoint: FixedPoint } => {
|
||||||
const bounds = [
|
const bounds = [
|
||||||
hoveredElement.x,
|
hoveredElement.x,
|
||||||
|
@ -1424,19 +1392,19 @@ const getLinearElementEdgeCoors = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fixDuplicatedBindingsAfterDuplication = (
|
export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
newElements: ExcalidrawElement[],
|
duplicatedElements: ExcalidrawElement[],
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
duplicatedElementsMap: NonDeletedSceneElementsMap,
|
duplicateElementsMap: NonDeletedSceneElementsMap,
|
||||||
) => {
|
) => {
|
||||||
for (const element of newElements) {
|
for (const duplicateElement of duplicatedElements) {
|
||||||
if ("boundElements" in element && element.boundElements) {
|
if ("boundElements" in duplicateElement && duplicateElement.boundElements) {
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
boundElements: element.boundElements.reduce(
|
boundElements: duplicateElement.boundElements.reduce(
|
||||||
(
|
(
|
||||||
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
||||||
binding,
|
binding,
|
||||||
) => {
|
) => {
|
||||||
const newBindingId = oldIdToDuplicatedId.get(binding.id);
|
const newBindingId = origIdToDuplicateId.get(binding.id);
|
||||||
if (newBindingId) {
|
if (newBindingId) {
|
||||||
acc.push({ ...binding, id: newBindingId });
|
acc.push({ ...binding, id: newBindingId });
|
||||||
}
|
}
|
||||||
|
@ -1447,46 +1415,47 @@ export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("containerId" in element && element.containerId) {
|
if ("containerId" in duplicateElement && duplicateElement.containerId) {
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
containerId: oldIdToDuplicatedId.get(element.containerId) ?? null,
|
containerId:
|
||||||
|
origIdToDuplicateId.get(duplicateElement.containerId) ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("endBinding" in element && element.endBinding) {
|
if ("endBinding" in duplicateElement && duplicateElement.endBinding) {
|
||||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
const newEndBindingId = origIdToDuplicateId.get(
|
||||||
element.endBinding.elementId,
|
duplicateElement.endBinding.elementId,
|
||||||
);
|
);
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
endBinding: newEndBindingId
|
endBinding: newEndBindingId
|
||||||
? {
|
? {
|
||||||
...element.endBinding,
|
...duplicateElement.endBinding,
|
||||||
elementId: newEndBindingId,
|
elementId: newEndBindingId,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ("startBinding" in element && element.startBinding) {
|
if ("startBinding" in duplicateElement && duplicateElement.startBinding) {
|
||||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
const newEndBindingId = origIdToDuplicateId.get(
|
||||||
element.startBinding.elementId,
|
duplicateElement.startBinding.elementId,
|
||||||
);
|
);
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
startBinding: newEndBindingId
|
startBinding: newEndBindingId
|
||||||
? {
|
? {
|
||||||
...element.startBinding,
|
...duplicateElement.startBinding,
|
||||||
elementId: newEndBindingId,
|
elementId: newEndBindingId,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(duplicateElement)) {
|
||||||
Object.assign(
|
Object.assign(
|
||||||
element,
|
duplicateElement,
|
||||||
updateElbowArrowPoints(element, duplicatedElementsMap, {
|
updateElbowArrowPoints(duplicateElement, duplicateElementsMap, {
|
||||||
points: [
|
points: [
|
||||||
element.points[0],
|
duplicateElement.points[0],
|
||||||
element.points[element.points.length - 1],
|
duplicateElement.points[duplicateElement.points.length - 1],
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -1494,196 +1463,6 @@ export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fixReversedBindingsForBindables = (
|
|
||||||
original: ExcalidrawBindableElement,
|
|
||||||
duplicate: ExcalidrawBindableElement,
|
|
||||||
originalElements: Map<string, ExcalidrawElement>,
|
|
||||||
elementsWithClones: ExcalidrawElement[],
|
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
|
||||||
) => {
|
|
||||||
original.boundElements?.forEach((binding, idx) => {
|
|
||||||
if (binding.type !== "arrow") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldArrow = elementsWithClones.find((el) => el.id === binding.id);
|
|
||||||
|
|
||||||
if (!isBindingElement(oldArrow)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalElements.has(binding.id)) {
|
|
||||||
// Linked arrow is in the selection, so find the duplicate pair
|
|
||||||
const newArrowId = oldIdToDuplicatedId.get(binding.id) ?? binding.id;
|
|
||||||
const newArrow = elementsWithClones.find(
|
|
||||||
(el) => el.id === newArrowId,
|
|
||||||
)! as ExcalidrawArrowElement;
|
|
||||||
|
|
||||||
mutateElement(newArrow, {
|
|
||||||
startBinding:
|
|
||||||
oldArrow.startBinding?.elementId === binding.id
|
|
||||||
? {
|
|
||||||
...oldArrow.startBinding,
|
|
||||||
elementId: duplicate.id,
|
|
||||||
}
|
|
||||||
: newArrow.startBinding,
|
|
||||||
endBinding:
|
|
||||||
oldArrow.endBinding?.elementId === binding.id
|
|
||||||
? {
|
|
||||||
...oldArrow.endBinding,
|
|
||||||
elementId: duplicate.id,
|
|
||||||
}
|
|
||||||
: newArrow.endBinding,
|
|
||||||
});
|
|
||||||
mutateElement(duplicate, {
|
|
||||||
boundElements: [
|
|
||||||
...(duplicate.boundElements ?? []).filter(
|
|
||||||
(el) => el.id !== binding.id && el.id !== newArrowId,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
id: newArrowId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Linked arrow is outside the selection,
|
|
||||||
// so we move the binding to the duplicate
|
|
||||||
mutateElement(oldArrow, {
|
|
||||||
startBinding:
|
|
||||||
oldArrow.startBinding?.elementId === original.id
|
|
||||||
? {
|
|
||||||
...oldArrow.startBinding,
|
|
||||||
elementId: duplicate.id,
|
|
||||||
}
|
|
||||||
: oldArrow.startBinding,
|
|
||||||
endBinding:
|
|
||||||
oldArrow.endBinding?.elementId === original.id
|
|
||||||
? {
|
|
||||||
...oldArrow.endBinding,
|
|
||||||
elementId: duplicate.id,
|
|
||||||
}
|
|
||||||
: oldArrow.endBinding,
|
|
||||||
});
|
|
||||||
mutateElement(duplicate, {
|
|
||||||
boundElements: [
|
|
||||||
...(duplicate.boundElements ?? []),
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
id: oldArrow.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
mutateElement(original, {
|
|
||||||
boundElements:
|
|
||||||
original.boundElements?.filter((_, i) => i !== idx) ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fixReversedBindingsForArrows = (
|
|
||||||
original: ExcalidrawArrowElement,
|
|
||||||
duplicate: ExcalidrawArrowElement,
|
|
||||||
originalElements: Map<string, ExcalidrawElement>,
|
|
||||||
bindingProp: "startBinding" | "endBinding",
|
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
|
||||||
elementsWithClones: ExcalidrawElement[],
|
|
||||||
) => {
|
|
||||||
const oldBindableId = original[bindingProp]?.elementId;
|
|
||||||
|
|
||||||
if (oldBindableId) {
|
|
||||||
if (originalElements.has(oldBindableId)) {
|
|
||||||
// Linked element is in the selection
|
|
||||||
const newBindableId =
|
|
||||||
oldIdToDuplicatedId.get(oldBindableId) ?? oldBindableId;
|
|
||||||
const newBindable = elementsWithClones.find(
|
|
||||||
(el) => el.id === newBindableId,
|
|
||||||
) as ExcalidrawBindableElement;
|
|
||||||
mutateElement(duplicate, {
|
|
||||||
[bindingProp]: {
|
|
||||||
...original[bindingProp],
|
|
||||||
elementId: newBindableId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
mutateElement(newBindable, {
|
|
||||||
boundElements: [
|
|
||||||
...(newBindable.boundElements ?? []).filter(
|
|
||||||
(el) => el.id !== original.id && el.id !== duplicate.id,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
id: duplicate.id,
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Linked element is outside the selection
|
|
||||||
const originalBindable = elementsWithClones.find(
|
|
||||||
(el) => el.id === oldBindableId,
|
|
||||||
);
|
|
||||||
if (originalBindable) {
|
|
||||||
mutateElement(duplicate, {
|
|
||||||
[bindingProp]: original[bindingProp],
|
|
||||||
});
|
|
||||||
mutateElement(original, {
|
|
||||||
[bindingProp]: null,
|
|
||||||
});
|
|
||||||
mutateElement(originalBindable, {
|
|
||||||
boundElements: [
|
|
||||||
...(originalBindable.boundElements?.filter(
|
|
||||||
(el) => el.id !== original.id,
|
|
||||||
) ?? []),
|
|
||||||
{
|
|
||||||
id: duplicate.id,
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fixReversedBindings = (
|
|
||||||
originalElements: Map<string, ExcalidrawElement>,
|
|
||||||
elementsWithClones: ExcalidrawElement[],
|
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
|
||||||
) => {
|
|
||||||
for (const original of originalElements.values()) {
|
|
||||||
const duplicate = elementsWithClones.find(
|
|
||||||
(el) => el.id === oldIdToDuplicatedId.get(original.id),
|
|
||||||
)!;
|
|
||||||
|
|
||||||
if (isBindableElement(original) && isBindableElement(duplicate)) {
|
|
||||||
fixReversedBindingsForBindables(
|
|
||||||
original,
|
|
||||||
duplicate,
|
|
||||||
originalElements,
|
|
||||||
elementsWithClones,
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
);
|
|
||||||
} else if (isArrowElement(original) && isArrowElement(duplicate)) {
|
|
||||||
fixReversedBindingsForArrows(
|
|
||||||
original,
|
|
||||||
duplicate,
|
|
||||||
originalElements,
|
|
||||||
"startBinding",
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
elementsWithClones,
|
|
||||||
);
|
|
||||||
fixReversedBindingsForArrows(
|
|
||||||
original,
|
|
||||||
duplicate,
|
|
||||||
originalElements,
|
|
||||||
"endBinding",
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
elementsWithClones,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fixBindingsAfterDeletion = (
|
export const fixBindingsAfterDeletion = (
|
||||||
sceneElements: readonly ExcalidrawElement[],
|
sceneElements: readonly ExcalidrawElement[],
|
||||||
deletedElements: readonly ExcalidrawElement[],
|
deletedElements: readonly ExcalidrawElement[],
|
||||||
|
@ -1691,8 +1470,12 @@ export const fixBindingsAfterDeletion = (
|
||||||
const elements = arrayToMap(sceneElements);
|
const elements = arrayToMap(sceneElements);
|
||||||
|
|
||||||
for (const element of deletedElements) {
|
for (const element of deletedElements) {
|
||||||
BoundElement.unbindAffected(elements, element, mutateElement);
|
BoundElement.unbindAffected(elements, element, (element, updates) =>
|
||||||
BindableElement.unbindAffected(elements, element, mutateElement);
|
mutateElement(element, elements, updates),
|
||||||
|
);
|
||||||
|
BindableElement.unbindAffected(elements, element, (element, updates) =>
|
||||||
|
mutateElement(element, elements, updates),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1771,10 +1554,7 @@ const determineFocusDistance = (
|
||||||
// Another point on the line, in absolute coordinates (closer to element)
|
// Another point on the line, in absolute coordinates (closer to element)
|
||||||
b: GlobalPoint,
|
b: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pointsEqual(a, b)) {
|
if (pointsEqual(a, b)) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -1904,10 +1684,7 @@ const determineFocusPoint = (
|
||||||
focus: number,
|
focus: number,
|
||||||
adjacentPoint: GlobalPoint,
|
adjacentPoint: GlobalPoint,
|
||||||
): GlobalPoint => {
|
): GlobalPoint => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (focus === 0) {
|
if (focus === 0) {
|
||||||
return center;
|
return center;
|
||||||
|
@ -2338,10 +2115,7 @@ export const getGlobalFixedPointForBindableElement = (
|
||||||
element.x + element.width * fixedX,
|
element.x + element.width * fixedX,
|
||||||
element.y + element.height * fixedY,
|
element.y + element.height * fixedY,
|
||||||
),
|
),
|
||||||
pointFrom<GlobalPoint>(
|
elementCenterPoint(element),
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
),
|
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
|
||||||
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
|
import {
|
||||||
|
rescalePoints,
|
||||||
|
arrayToMap,
|
||||||
|
invariant,
|
||||||
|
sizeOf,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
|
@ -57,6 +62,7 @@ import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
|
ElementsMapOrArray,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
@ -938,10 +944,10 @@ export const getElementBounds = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: ElementsMapOrArray,
|
||||||
elementsMap?: ElementsMap,
|
elementsMap?: ElementsMap,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
if (!elements.length) {
|
if (!sizeOf(elements)) {
|
||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { isTransparent } from "@excalidraw/common";
|
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
curveIntersectLineSegment,
|
curveIntersectLineSegment,
|
||||||
isPointWithinBounds,
|
isPointWithinBounds,
|
||||||
|
@ -16,7 +16,7 @@ import {
|
||||||
} from "@excalidraw/math/ellipse";
|
} from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
import { getPolygonShape } from "@excalidraw/utils/shape";
|
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GlobalPoint,
|
GlobalPoint,
|
||||||
|
@ -26,8 +26,6 @@ import type {
|
||||||
Radians,
|
Radians,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||||
|
@ -191,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||||
|
@ -253,10 +248,7 @@ const intersectDiamondWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
|
@ -304,10 +296,7 @@ const intersectEllipseWithLineSegment = (
|
||||||
l: LineSegment<GlobalPoint>,
|
l: LineSegment<GlobalPoint>,
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
): GlobalPoint[] => {
|
): GlobalPoint[] => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||||
|
|
|
@ -14,6 +14,8 @@ import {
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { type Point } from "points-on-curve";
|
import { type Point } from "points-on-curve";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
getResizedElementAbsoluteCoords,
|
getResizedElementAbsoluteCoords,
|
||||||
|
@ -61,7 +63,7 @@ export const cropElement = (
|
||||||
|
|
||||||
const rotatedPointer = pointRotateRads(
|
const rotatedPointer = pointRotateRads(
|
||||||
pointFrom(pointerX, pointerY),
|
pointFrom(pointerX, pointerY),
|
||||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
elementCenterPoint(element),
|
||||||
-element.angle as Radians,
|
-element.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import {
|
import {
|
||||||
curvePointDistance,
|
curvePointDistance,
|
||||||
distanceToLineSegment,
|
distanceToLineSegment,
|
||||||
pointFrom,
|
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -53,10 +54,7 @@ const distanceToRectanguloidElement = (
|
||||||
element: ExcalidrawRectanguloidElement,
|
element: ExcalidrawRectanguloidElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
) => {
|
) => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||||
// instead. It's all the same distance-wise.
|
// instead. It's all the same distance-wise.
|
||||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||||
|
@ -84,10 +82,7 @@ const distanceToDiamondElement = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||||
// points. It's all the same distance-wise.
|
// points. It's all the same distance-wise.
|
||||||
|
@ -115,10 +110,7 @@ const distanceToEllipseElement = (
|
||||||
element: ExcalidrawEllipseElement,
|
element: ExcalidrawEllipseElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
const center = pointFrom(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
return ellipseDistanceFromPoint(
|
return ellipseDistanceFromPoint(
|
||||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||||
pointRotateRads(p, center, -element.angle as Radians),
|
pointRotateRads(p, center, -element.angle as Radians),
|
||||||
|
|
|
@ -11,13 +11,10 @@ import type {
|
||||||
PointerDownState,
|
PointerDownState,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { getMinTextElementWidth } from "./textMeasurements";
|
import { getMinTextElementWidth } from "./textMeasurements";
|
||||||
|
@ -29,6 +26,8 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
|
@ -104,7 +103,7 @@ export const dragSelectedElements = (
|
||||||
);
|
);
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||||
if (!isArrowElement(element)) {
|
if (!isArrowElement(element)) {
|
||||||
// skip arrow labels since we calculate its position during render
|
// skip arrow labels since we calculate its position during render
|
||||||
const textElement = getBoundTextElement(
|
const textElement = getBoundTextElement(
|
||||||
|
@ -112,9 +111,14 @@ export const dragSelectedElements = (
|
||||||
scene.getNonDeletedElementsMap(),
|
scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
if (textElement) {
|
if (textElement) {
|
||||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
updateElementCoords(
|
||||||
|
pointerDownState,
|
||||||
|
textElement,
|
||||||
|
scene,
|
||||||
|
adjustedOffset,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -155,6 +159,7 @@ const calculateOffset = (
|
||||||
const updateElementCoords = (
|
const updateElementCoords = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
scene: Scene,
|
||||||
dragOffset: { x: number; y: number },
|
dragOffset: { x: number; y: number },
|
||||||
) => {
|
) => {
|
||||||
const originalElement =
|
const originalElement =
|
||||||
|
@ -163,7 +168,7 @@ const updateElementCoords = (
|
||||||
const nextX = originalElement.x + dragOffset.x;
|
const nextX = originalElement.x + dragOffset.x;
|
||||||
const nextY = originalElement.y + dragOffset.y;
|
const nextY = originalElement.y + dragOffset.y;
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
x: nextX,
|
x: nextX,
|
||||||
y: nextY,
|
y: nextY,
|
||||||
});
|
});
|
||||||
|
@ -190,6 +195,7 @@ export const dragNewElement = ({
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
zoom,
|
zoom,
|
||||||
|
scene,
|
||||||
widthAspectRatio = null,
|
widthAspectRatio = null,
|
||||||
originOffset = null,
|
originOffset = null,
|
||||||
informMutation = true,
|
informMutation = true,
|
||||||
|
@ -205,6 +211,7 @@ export const dragNewElement = ({
|
||||||
shouldMaintainAspectRatio: boolean;
|
shouldMaintainAspectRatio: boolean;
|
||||||
shouldResizeFromCenter: boolean;
|
shouldResizeFromCenter: boolean;
|
||||||
zoom: NormalizedZoomValue;
|
zoom: NormalizedZoomValue;
|
||||||
|
scene: Scene;
|
||||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||||
true */
|
true */
|
||||||
widthAspectRatio?: number | null;
|
widthAspectRatio?: number | null;
|
||||||
|
@ -285,7 +292,7 @@ export const dragNewElement = ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
x: newX + (originOffset?.x ?? 0),
|
x: newX + (originOffset?.x ?? 0),
|
||||||
|
@ -295,7 +302,7 @@ export const dragNewElement = ({
|
||||||
...textAutoResize,
|
...textAutoResize,
|
||||||
...imageInitialDimension,
|
...imageInitialDimension,
|
||||||
},
|
},
|
||||||
informMutation,
|
{ informMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,10 +36,7 @@ import {
|
||||||
|
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
|
|
||||||
import {
|
import { fixDuplicatedBindingsAfterDuplication } from "./binding";
|
||||||
fixDuplicatedBindingsAfterDuplication,
|
|
||||||
fixReversedBindings,
|
|
||||||
} from "./binding";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
@ -60,16 +57,14 @@ import type {
|
||||||
* multiple elements at once, share this map
|
* multiple elements at once, share this map
|
||||||
* amongst all of them
|
* amongst all of them
|
||||||
* @param element Element to duplicate
|
* @param element Element to duplicate
|
||||||
* @param overrides Any element properties to override
|
|
||||||
*/
|
*/
|
||||||
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||||
editingGroupId: AppState["editingGroupId"],
|
editingGroupId: AppState["editingGroupId"],
|
||||||
groupIdMapForOperation: Map<GroupId, GroupId>,
|
groupIdMapForOperation: Map<GroupId, GroupId>,
|
||||||
element: TElement,
|
element: TElement,
|
||||||
overrides?: Partial<TElement>,
|
|
||||||
randomizeSeed?: boolean,
|
randomizeSeed?: boolean,
|
||||||
): Readonly<TElement> => {
|
): Readonly<TElement> => {
|
||||||
let copy = deepCopyElement(element);
|
const copy = deepCopyElement(element);
|
||||||
|
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
__test__defineOrigId(copy, element.id);
|
__test__defineOrigId(copy, element.id);
|
||||||
|
@ -92,9 +87,6 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||||
return groupIdMapForOperation.get(groupId)!;
|
return groupIdMapForOperation.get(groupId)!;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (overrides) {
|
|
||||||
copy = Object.assign(copy, overrides);
|
|
||||||
}
|
|
||||||
return copy;
|
return copy;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,9 +94,14 @@ export const duplicateElements = (
|
||||||
opts: {
|
opts: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
randomizeSeed?: boolean;
|
randomizeSeed?: boolean;
|
||||||
overrides?: (
|
overrides?: (data: {
|
||||||
originalElement: ExcalidrawElement,
|
duplicateElement: ExcalidrawElement;
|
||||||
) => Partial<ExcalidrawElement>;
|
origElement: ExcalidrawElement;
|
||||||
|
origIdToDuplicateId: Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement["id"]
|
||||||
|
>;
|
||||||
|
}) => Partial<ExcalidrawElement>;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
/**
|
/**
|
||||||
|
@ -132,14 +129,6 @@ export const duplicateElements = (
|
||||||
editingGroupId: AppState["editingGroupId"];
|
editingGroupId: AppState["editingGroupId"];
|
||||||
selectedGroupIds: AppState["selectedGroupIds"];
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* If true, duplicated elements are inserted _before_ specified
|
|
||||||
* elements. Case: alt-dragging elements to duplicate them.
|
|
||||||
*
|
|
||||||
* TODO: remove this once (if) we stop replacing the original element
|
|
||||||
* with the duplicated one in the scene array.
|
|
||||||
*/
|
|
||||||
reverseOrder: boolean;
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
) => {
|
) => {
|
||||||
|
@ -153,8 +142,6 @@ export const duplicateElements = (
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
} as const);
|
} as const);
|
||||||
|
|
||||||
const reverseOrder = opts.type === "in-place" ? opts.reverseOrder : false;
|
|
||||||
|
|
||||||
// Ids of elements that have already been processed so we don't push them
|
// Ids of elements that have already been processed so we don't push them
|
||||||
// into the array twice if we end up backtracking when retrieving
|
// into the array twice if we end up backtracking when retrieving
|
||||||
// discontiguous group of elements (can happen due to a bug, or in edge
|
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||||
|
@ -167,10 +154,17 @@ export const duplicateElements = (
|
||||||
// loop over them.
|
// loop over them.
|
||||||
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||||
const groupIdMap = new Map();
|
const groupIdMap = new Map();
|
||||||
const newElements: ExcalidrawElement[] = [];
|
const duplicatedElements: ExcalidrawElement[] = [];
|
||||||
const oldElements: ExcalidrawElement[] = [];
|
const origElements: ExcalidrawElement[] = [];
|
||||||
const oldIdToDuplicatedId = new Map();
|
const origIdToDuplicateId = new Map<
|
||||||
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement["id"]
|
||||||
|
>();
|
||||||
|
const duplicateIdToOrigElement = new Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement
|
||||||
|
>();
|
||||||
|
const duplicateElementsMap = new Map<string, ExcalidrawElement>();
|
||||||
const elementsMap = arrayToMap(elements) as ElementsMap;
|
const elementsMap = arrayToMap(elements) as ElementsMap;
|
||||||
const _idsOfElementsToDuplicate =
|
const _idsOfElementsToDuplicate =
|
||||||
opts.type === "in-place"
|
opts.type === "in-place"
|
||||||
|
@ -188,7 +182,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
elements = normalizeElementOrder(elements);
|
elements = normalizeElementOrder(elements);
|
||||||
|
|
||||||
const elementsWithClones: ExcalidrawElement[] = elements.slice();
|
const elementsWithDuplicates: ExcalidrawElement[] = elements.slice();
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
@ -214,17 +208,17 @@ export const duplicateElements = (
|
||||||
appState.editingGroupId,
|
appState.editingGroupId,
|
||||||
groupIdMap,
|
groupIdMap,
|
||||||
element,
|
element,
|
||||||
opts.overrides?.(element),
|
|
||||||
opts.randomizeSeed,
|
opts.randomizeSeed,
|
||||||
);
|
);
|
||||||
|
|
||||||
processedIds.set(newElement.id, true);
|
processedIds.set(newElement.id, true);
|
||||||
|
|
||||||
duplicatedElementsMap.set(newElement.id, newElement);
|
duplicateElementsMap.set(newElement.id, newElement);
|
||||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
origIdToDuplicateId.set(element.id, newElement.id);
|
||||||
|
duplicateIdToOrigElement.set(newElement.id, element);
|
||||||
|
|
||||||
oldElements.push(element);
|
origElements.push(element);
|
||||||
newElements.push(newElement);
|
duplicatedElements.push(newElement);
|
||||||
|
|
||||||
acc.push(newElement);
|
acc.push(newElement);
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -248,21 +242,12 @@ export const duplicateElements = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reverseOrder && index < 1) {
|
if (index > elementsWithDuplicates.length - 1) {
|
||||||
elementsWithClones.unshift(...castArray(elements));
|
elementsWithDuplicates.push(...castArray(elements));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reverseOrder && index > elementsWithClones.length - 1) {
|
elementsWithDuplicates.splice(index + 1, 0, ...castArray(elements));
|
||||||
elementsWithClones.push(...castArray(elements));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
elementsWithClones.splice(
|
|
||||||
index + (reverseOrder ? 0 : 1),
|
|
||||||
0,
|
|
||||||
...castArray(elements),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const frameIdsToDuplicate = new Set(
|
const frameIdsToDuplicate = new Set(
|
||||||
|
@ -294,13 +279,9 @@ export const duplicateElements = (
|
||||||
: [element],
|
: [element],
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetIndex = reverseOrder
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
? elementsWithClones.findIndex((el) => {
|
return el.groupIds?.includes(groupId);
|
||||||
return el.groupIds?.includes(groupId);
|
});
|
||||||
})
|
|
||||||
: findLastIndex(elementsWithClones, (el) => {
|
|
||||||
return el.groupIds?.includes(groupId);
|
|
||||||
});
|
|
||||||
|
|
||||||
insertBeforeOrAfterIndex(targetIndex, copyElements(groupElements));
|
insertBeforeOrAfterIndex(targetIndex, copyElements(groupElements));
|
||||||
continue;
|
continue;
|
||||||
|
@ -318,7 +299,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
const frameChildren = getFrameChildren(elements, frameId);
|
const frameChildren = getFrameChildren(elements, frameId);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return el.frameId === frameId || el.id === frameId;
|
return el.frameId === frameId || el.id === frameId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -335,7 +316,7 @@ export const duplicateElements = (
|
||||||
if (hasBoundTextElement(element)) {
|
if (hasBoundTextElement(element)) {
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return (
|
return (
|
||||||
el.id === element.id ||
|
el.id === element.id ||
|
||||||
("containerId" in el && el.containerId === element.id)
|
("containerId" in el && el.containerId === element.id)
|
||||||
|
@ -344,7 +325,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
insertBeforeOrAfterIndex(
|
insertBeforeOrAfterIndex(
|
||||||
targetIndex + (reverseOrder ? -1 : 0),
|
targetIndex,
|
||||||
copyElements([element, boundTextElement]),
|
copyElements([element, boundTextElement]),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -357,7 +338,7 @@ export const duplicateElements = (
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
const container = getContainerElement(element, elementsMap);
|
const container = getContainerElement(element, elementsMap);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return el.id === element.id || el.id === container?.id;
|
return el.id === element.id || el.id === container?.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -377,7 +358,7 @@ export const duplicateElements = (
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
insertBeforeOrAfterIndex(
|
insertBeforeOrAfterIndex(
|
||||||
findLastIndex(elementsWithClones, (el) => el.id === element.id),
|
findLastIndex(elementsWithDuplicates, (el) => el.id === element.id),
|
||||||
copyElements(element),
|
copyElements(element),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -385,28 +366,38 @@ export const duplicateElements = (
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fixDuplicatedBindingsAfterDuplication(
|
fixDuplicatedBindingsAfterDuplication(
|
||||||
newElements,
|
duplicatedElements,
|
||||||
oldIdToDuplicatedId,
|
origIdToDuplicateId,
|
||||||
duplicatedElementsMap as NonDeletedSceneElementsMap,
|
duplicateElementsMap as NonDeletedSceneElementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (reverseOrder) {
|
|
||||||
fixReversedBindings(
|
|
||||||
_idsOfElementsToDuplicate,
|
|
||||||
elementsWithClones,
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindElementsToFramesAfterDuplication(
|
bindElementsToFramesAfterDuplication(
|
||||||
elementsWithClones,
|
elementsWithDuplicates,
|
||||||
oldElements,
|
origElements,
|
||||||
oldIdToDuplicatedId,
|
origIdToDuplicateId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (opts.overrides) {
|
||||||
|
for (const duplicateElement of duplicatedElements) {
|
||||||
|
const origElement = duplicateIdToOrigElement.get(duplicateElement.id);
|
||||||
|
if (origElement) {
|
||||||
|
Object.assign(
|
||||||
|
duplicateElement,
|
||||||
|
opts.overrides({
|
||||||
|
duplicateElement,
|
||||||
|
origElement,
|
||||||
|
origIdToDuplicateId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newElements,
|
duplicatedElements,
|
||||||
elementsWithClones,
|
duplicateElementsMap,
|
||||||
|
elementsWithDuplicates,
|
||||||
|
origIdToDuplicateId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,6 @@ import { isBindableElement } from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
type ExcalidrawElbowArrowElement,
|
type ExcalidrawElbowArrowElement,
|
||||||
type NonDeletedSceneElementsMap,
|
type NonDeletedSceneElementsMap,
|
||||||
type SceneElementsMap,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||||
|
@ -887,7 +886,7 @@ export const updateElbowArrowPoints = (
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
updates: {
|
updates: {
|
||||||
points?: readonly LocalPoint[];
|
points?: readonly LocalPoint[];
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: readonly FixedSegment[] | null;
|
||||||
startBinding?: FixedPointBinding | null;
|
startBinding?: FixedPointBinding | null;
|
||||||
endBinding?: FixedPointBinding | null;
|
endBinding?: FixedPointBinding | null;
|
||||||
},
|
},
|
||||||
|
@ -1273,14 +1272,12 @@ const getElbowArrowData = (
|
||||||
const startHeading = getBindPointHeading(
|
const startHeading = getBindPointHeading(
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
elementsMap,
|
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
);
|
);
|
||||||
const endHeading = getBindPointHeading(
|
const endHeading = getBindPointHeading(
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
elementsMap,
|
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
);
|
);
|
||||||
|
@ -2250,7 +2247,6 @@ const getGlobalPoint = (
|
||||||
const getBindPointHeading = (
|
const getBindPointHeading = (
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
otherPoint: GlobalPoint,
|
otherPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
): Heading =>
|
): Heading =>
|
||||||
|
@ -2268,7 +2264,6 @@ const getBindPointHeading = (
|
||||||
number,
|
number,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
elementsMap,
|
|
||||||
origPoint,
|
origPoint,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ import {
|
||||||
type OrderedExcalidrawElement,
|
type OrderedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
type LinkDirection = "up" | "right" | "down" | "left";
|
type LinkDirection = "up" | "right" | "down" | "left";
|
||||||
|
|
||||||
const VERTICAL_OFFSET = 100;
|
const VERTICAL_OFFSET = 100;
|
||||||
|
@ -236,10 +238,11 @@ const getOffsets = (
|
||||||
|
|
||||||
const addNewNode = (
|
const addNewNode = (
|
||||||
element: ExcalidrawFlowchartNodeElement,
|
element: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const successors = getSuccessors(element, elementsMap, direction);
|
const successors = getSuccessors(element, elementsMap, direction);
|
||||||
const predeccessors = getPredecessors(element, elementsMap, direction);
|
const predeccessors = getPredecessors(element, elementsMap, direction);
|
||||||
|
|
||||||
|
@ -274,9 +277,9 @@ const addNewNode = (
|
||||||
const bindingArrow = createBindingArrow(
|
const bindingArrow = createBindingArrow(
|
||||||
element,
|
element,
|
||||||
nextNode,
|
nextNode,
|
||||||
elementsMap,
|
|
||||||
direction,
|
direction,
|
||||||
appState,
|
appState,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -287,9 +290,9 @@ const addNewNode = (
|
||||||
|
|
||||||
export const addNewNodes = (
|
export const addNewNodes = (
|
||||||
startNode: ExcalidrawFlowchartNodeElement,
|
startNode: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
numberOfNodes: number,
|
numberOfNodes: number,
|
||||||
) => {
|
) => {
|
||||||
// always start from 0 and distribute evenly
|
// always start from 0 and distribute evenly
|
||||||
|
@ -352,9 +355,9 @@ export const addNewNodes = (
|
||||||
const bindingArrow = createBindingArrow(
|
const bindingArrow = createBindingArrow(
|
||||||
startNode,
|
startNode,
|
||||||
nextNode,
|
nextNode,
|
||||||
elementsMap,
|
|
||||||
direction,
|
direction,
|
||||||
appState,
|
appState,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
newNodes.push(nextNode);
|
newNodes.push(nextNode);
|
||||||
|
@ -367,9 +370,9 @@ export const addNewNodes = (
|
||||||
const createBindingArrow = (
|
const createBindingArrow = (
|
||||||
startBindingElement: ExcalidrawFlowchartNodeElement,
|
startBindingElement: ExcalidrawFlowchartNodeElement,
|
||||||
endBindingElement: ExcalidrawFlowchartNodeElement,
|
endBindingElement: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
let startX: number;
|
let startX: number;
|
||||||
let startY: number;
|
let startY: number;
|
||||||
|
@ -440,18 +443,10 @@ const createBindingArrow = (
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
bindLinearElement(
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
bindingArrow,
|
|
||||||
startBindingElement,
|
bindLinearElement(bindingArrow, startBindingElement, "start", scene);
|
||||||
"start",
|
bindLinearElement(bindingArrow, endBindingElement, "end", scene);
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
bindLinearElement(
|
|
||||||
bindingArrow,
|
|
||||||
endBindingElement,
|
|
||||||
"end",
|
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
||||||
changedElements.set(
|
changedElements.set(
|
||||||
|
@ -467,7 +462,7 @@ const createBindingArrow = (
|
||||||
bindingArrow as OrderedExcalidrawElement,
|
bindingArrow as OrderedExcalidrawElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(bindingArrow, [
|
LinearElementEditor.movePoints(bindingArrow, scene, [
|
||||||
{
|
{
|
||||||
index: 1,
|
index: 1,
|
||||||
point: bindingArrow.points[1],
|
point: bindingArrow.points[1],
|
||||||
|
@ -632,16 +627,17 @@ export class FlowChartCreator {
|
||||||
|
|
||||||
createNodes(
|
createNodes(
|
||||||
startNode: ExcalidrawFlowchartNodeElement,
|
startNode: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
) {
|
) {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
if (direction !== this.direction) {
|
if (direction !== this.direction) {
|
||||||
const { nextNode, bindingArrow } = addNewNode(
|
const { nextNode, bindingArrow } = addNewNode(
|
||||||
startNode,
|
startNode,
|
||||||
elementsMap,
|
|
||||||
appState,
|
appState,
|
||||||
direction,
|
direction,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.numberOfNodes = 1;
|
this.numberOfNodes = 1;
|
||||||
|
@ -652,9 +648,9 @@ export class FlowChartCreator {
|
||||||
this.numberOfNodes += 1;
|
this.numberOfNodes += 1;
|
||||||
const newNodes = addNewNodes(
|
const newNodes = addNewNodes(
|
||||||
startNode,
|
startNode,
|
||||||
elementsMap,
|
|
||||||
appState,
|
appState,
|
||||||
direction,
|
direction,
|
||||||
|
scene,
|
||||||
this.numberOfNodes,
|
this.numberOfNodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -682,13 +678,9 @@ export class FlowChartCreator {
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.pendingNodes = this.pendingNodes.map((node) =>
|
this.pendingNodes = this.pendingNodes.map((node) =>
|
||||||
mutateElement(
|
mutateElement(node, elementsMap, {
|
||||||
node,
|
frameId: startNode.frameId,
|
||||||
{
|
}),
|
||||||
frameId: startNode.frameId,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { getBoundTextElement } from "./textElement";
|
||||||
import { hasBoundTextElement } from "./typeChecks";
|
import { hasBoundTextElement } from "./typeChecks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
|
@ -152,9 +153,10 @@ export const orderByFractionalIndex = (
|
||||||
*/
|
*/
|
||||||
export const syncMovedIndices = (
|
export const syncMovedIndices = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement>,
|
movedElements: ElementsMap,
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
try {
|
try {
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
const indicesGroups = getMovedIndicesGroups(elements, movedElements);
|
const indicesGroups = getMovedIndicesGroups(elements, movedElements);
|
||||||
|
|
||||||
// try generatating indices, throws on invalid movedElements
|
// try generatating indices, throws on invalid movedElements
|
||||||
|
@ -176,7 +178,7 @@ export const syncMovedIndices = (
|
||||||
|
|
||||||
// split mutation so we don't end up in an incosistent state
|
// split mutation so we don't end up in an incosistent state
|
||||||
for (const [element, update] of elementsUpdates) {
|
for (const [element, update] of elementsUpdates) {
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, elementsMap, update);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// fallback to default sync
|
// fallback to default sync
|
||||||
|
@ -194,10 +196,12 @@ export const syncMovedIndices = (
|
||||||
export const syncInvalidIndices = (
|
export const syncInvalidIndices = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
const indicesGroups = getInvalidIndicesGroups(elements);
|
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
|
|
||||||
for (const [element, update] of elementsUpdates) {
|
for (const [element, update] of elementsUpdates) {
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, elementsMap, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements as OrderedExcalidrawElement[];
|
return elements as OrderedExcalidrawElement[];
|
||||||
|
@ -210,7 +214,7 @@ export const syncInvalidIndices = (
|
||||||
*/
|
*/
|
||||||
const getMovedIndicesGroups = (
|
const getMovedIndicesGroups = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement>,
|
movedElements: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
const indicesGroups: number[][] = [];
|
const indicesGroups: number[][] = [];
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
|
||||||
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
||||||
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
||||||
|
|
||||||
import type { ExcalidrawElementsIncludingDeleted } from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -29,6 +27,8 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type { ExcalidrawElementsIncludingDeleted } from "./Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
|
@ -41,30 +41,24 @@ import type {
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
nextElements: readonly ExcalidrawElement[],
|
nextElements: readonly ExcalidrawElement[],
|
||||||
oldElements: readonly ExcalidrawElement[],
|
origElements: readonly ExcalidrawElement[],
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
) => {
|
) => {
|
||||||
const nextElementMap = arrayToMap(nextElements) as Map<
|
const nextElementMap = arrayToMap(nextElements) as Map<
|
||||||
ExcalidrawElement["id"],
|
ExcalidrawElement["id"],
|
||||||
ExcalidrawElement
|
ExcalidrawElement
|
||||||
>;
|
>;
|
||||||
|
|
||||||
for (const element of oldElements) {
|
for (const element of origElements) {
|
||||||
if (element.frameId) {
|
if (element.frameId) {
|
||||||
// use its frameId to get the new frameId
|
// use its frameId to get the new frameId
|
||||||
const nextElementId = oldIdToDuplicatedId.get(element.id);
|
const nextElementId = origIdToDuplicateId.get(element.id);
|
||||||
const nextFrameId = oldIdToDuplicatedId.get(element.frameId);
|
const nextFrameId = origIdToDuplicateId.get(element.frameId);
|
||||||
if (nextElementId) {
|
const nextElement = nextElementId && nextElementMap.get(nextElementId);
|
||||||
const nextElement = nextElementMap.get(nextElementId);
|
if (nextElement) {
|
||||||
if (nextElement) {
|
mutateElement(nextElement, nextElementMap, {
|
||||||
mutateElement(
|
frameId: nextFrameId ?? null,
|
||||||
nextElement,
|
});
|
||||||
{
|
|
||||||
frameId: nextFrameId ?? element.frameId,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -567,13 +561,9 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const element of finalElementsToAdd) {
|
for (const element of finalElementsToAdd) {
|
||||||
mutateElement(
|
mutateElement(element, elementsMap, {
|
||||||
element,
|
frameId: frame.id,
|
||||||
{
|
});
|
||||||
frameId: frame.id,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allElements;
|
return allElements;
|
||||||
|
@ -611,13 +601,9 @@ export const removeElementsFromFrame = (
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [, element] of _elementsToRemove) {
|
for (const [, element] of _elementsToRemove) {
|
||||||
mutateElement(
|
mutateElement(element, elementsMap, {
|
||||||
element,
|
frameId: null,
|
||||||
{
|
});
|
||||||
frameId: null,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,6 @@ import {
|
||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { Store } from "@excalidraw/excalidraw/store";
|
import type { Store } from "@excalidraw/excalidraw/store";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
@ -50,10 +46,8 @@ import {
|
||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
|
||||||
|
|
||||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
|
@ -73,6 +67,8 @@ import {
|
||||||
|
|
||||||
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
@ -84,7 +80,6 @@ import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
SceneElementsMap,
|
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
@ -127,15 +122,17 @@ export class LinearElementEditor {
|
||||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||||
public readonly elbowed: boolean;
|
public readonly elbowed: boolean;
|
||||||
|
|
||||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
constructor(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) {
|
||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
_brand: "excalidrawLinearElementId";
|
_brand: "excalidrawLinearElementId";
|
||||||
};
|
};
|
||||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||||
console.error("Linear element is not normalized", Error().stack);
|
console.error("Linear element is not normalized", Error().stack);
|
||||||
LinearElementEditor.normalizePoints(element);
|
LinearElementEditor.normalizePoints(element, elementsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedPointsIndices = null;
|
this.selectedPointsIndices = null;
|
||||||
this.lastUncommittedPoint = null;
|
this.lastUncommittedPoint = null;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
|
@ -309,7 +306,7 @@ export class LinearElementEditor {
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, scene, [
|
||||||
{
|
{
|
||||||
index: selectedIndex,
|
index: selectedIndex,
|
||||||
point: pointFrom(
|
point: pointFrom(
|
||||||
|
@ -333,6 +330,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
element,
|
element,
|
||||||
|
scene,
|
||||||
selectedPointsIndices.map((pointIndex) => {
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
const newPointPosition: LocalPoint =
|
const newPointPosition: LocalPoint =
|
||||||
pointIndex === lastClickedPoint
|
pointIndex === lastClickedPoint
|
||||||
|
@ -358,7 +356,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
handleBindTextResize(element, elementsMap, false);
|
handleBindTextResize(element, scene, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// suggest bindings for first and last point if selected
|
// suggest bindings for first and last point if selected
|
||||||
|
@ -453,7 +451,7 @@ export class LinearElementEditor {
|
||||||
selectedPoint === element.points.length - 1
|
selectedPoint === element.points.length - 1
|
||||||
) {
|
) {
|
||||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, scene, [
|
||||||
{
|
{
|
||||||
index: selectedPoint,
|
index: selectedPoint,
|
||||||
point:
|
point:
|
||||||
|
@ -795,7 +793,7 @@ export class LinearElementEditor {
|
||||||
);
|
);
|
||||||
} else if (event.altKey && appState.editingLinearElement) {
|
} else if (event.altKey && appState.editingLinearElement) {
|
||||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
points: [
|
points: [
|
||||||
...element.points,
|
...element.points,
|
||||||
LinearElementEditor.createPointAt(
|
LinearElementEditor.createPointAt(
|
||||||
|
@ -861,7 +859,6 @@ export class LinearElementEditor {
|
||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -934,13 +931,13 @@ export class LinearElementEditor {
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
): LinearElementEditor | null {
|
): LinearElementEditor | null {
|
||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
if (!appState.editingLinearElement) {
|
if (!appState.editingLinearElement) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return appState.editingLinearElement;
|
return appState.editingLinearElement;
|
||||||
|
@ -951,7 +948,9 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
if (!event.altKey) {
|
if (!event.altKey) {
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
LinearElementEditor.deletePoints(element, app.scene, [
|
||||||
|
points.length - 1,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
|
@ -989,14 +988,14 @@ export class LinearElementEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, app.scene, [
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: newPoint,
|
point: newPoint,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
|
LinearElementEditor.addPoints(element, app.scene, [{ point: newPoint }]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
|
@ -1160,23 +1159,26 @@ export class LinearElementEditor {
|
||||||
y: element.y + offsetY,
|
y: element.y + offsetY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// element-mutating methods
|
// element-mutating methods
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
static normalizePoints(
|
||||||
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) {
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
elementsMap: ElementsMap,
|
||||||
|
) {
|
||||||
|
mutateElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
LinearElementEditor.getNormalizedPoints(element),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static duplicateSelectedPoints(
|
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
|
||||||
appState: AppState,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
): AppState {
|
|
||||||
invariant(
|
invariant(
|
||||||
appState.editingLinearElement,
|
appState.editingLinearElement,
|
||||||
"Not currently editing a linear element",
|
"Not currently editing a linear element",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
|
|
||||||
|
@ -1219,13 +1221,13 @@ export class LinearElementEditor {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
mutateElement(element, { points: nextPoints });
|
scene.mutateElement(element, { points: nextPoints });
|
||||||
|
|
||||||
// temp hack to ensure the line doesn't move when adding point to the end,
|
// temp hack to ensure the line doesn't move when adding point to the end,
|
||||||
// potentially expanding the bounding box
|
// potentially expanding the bounding box
|
||||||
if (pointAddedToEnd) {
|
if (pointAddedToEnd) {
|
||||||
const lastPoint = element.points[element.points.length - 1];
|
const lastPoint = element.points[element.points.length - 1];
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, scene, [
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||||
|
@ -1244,6 +1246,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
static deletePoints(
|
static deletePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
pointIndices: readonly number[],
|
pointIndices: readonly number[],
|
||||||
) {
|
) {
|
||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
|
@ -1274,28 +1277,41 @@ export class LinearElementEditor {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
LinearElementEditor._updatePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
nextPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addPoints(
|
static addPoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
targetPoints: { point: LocalPoint }[],
|
targetPoints: { point: LocalPoint }[],
|
||||||
) {
|
) {
|
||||||
const offsetX = 0;
|
const offsetX = 0;
|
||||||
const offsetY = 0;
|
const offsetY = 0;
|
||||||
|
|
||||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
LinearElementEditor._updatePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
nextPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static movePoints(
|
static movePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
||||||
otherUpdates?: {
|
otherUpdates?: {
|
||||||
startBinding?: PointBinding | null;
|
startBinding?: PointBinding | null;
|
||||||
endBinding?: PointBinding | null;
|
endBinding?: PointBinding | null;
|
||||||
},
|
},
|
||||||
sceneElementsMap?: NonDeletedSceneElementsMap,
|
|
||||||
) {
|
) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
|
@ -1329,6 +1345,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(
|
LinearElementEditor._updatePoints(
|
||||||
element,
|
element,
|
||||||
|
scene,
|
||||||
nextPoints,
|
nextPoints,
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
|
@ -1339,7 +1356,6 @@ export class LinearElementEditor {
|
||||||
dragging || targetPoint.isDragging === true,
|
dragging || targetPoint.isDragging === true,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
sceneElementsMap,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1394,8 +1410,9 @@ export class LinearElementEditor {
|
||||||
pointerCoords: PointerCoords,
|
pointerCoords: PointerCoords,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
snapToGrid: boolean,
|
snapToGrid: boolean,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
) {
|
) {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(
|
const element = LinearElementEditor.getElement(
|
||||||
linearElementEditor.elementId,
|
linearElementEditor.elementId,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -1425,9 +1442,7 @@ export class LinearElementEditor {
|
||||||
...element.points.slice(segmentMidpoint.index!),
|
...element.points.slice(segmentMidpoint.index!),
|
||||||
];
|
];
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, { points });
|
||||||
points,
|
|
||||||
});
|
|
||||||
|
|
||||||
ret.pointerDownState = {
|
ret.pointerDownState = {
|
||||||
...linearElementEditor.pointerDownState,
|
...linearElementEditor.pointerDownState,
|
||||||
|
@ -1443,6 +1458,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
private static _updatePoints(
|
private static _updatePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
nextPoints: readonly LocalPoint[],
|
nextPoints: readonly LocalPoint[],
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
|
@ -1479,28 +1495,10 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
updates.points = Array.from(nextPoints);
|
updates.points = Array.from(nextPoints);
|
||||||
|
|
||||||
if (!options?.sceneElementsMap || Scene.getScene(element)) {
|
scene.mutateElement(element, updates, {
|
||||||
mutateElement(element, updates, true, {
|
informMutation: true,
|
||||||
isDragging: options?.isDragging,
|
isDragging: options?.isDragging ?? false,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// The element is not in the scene, so we need to use the provided
|
|
||||||
// scene map.
|
|
||||||
Object.assign(element, {
|
|
||||||
...updates,
|
|
||||||
angle: 0 as Radians,
|
|
||||||
|
|
||||||
...updateElbowArrowPoints(
|
|
||||||
element,
|
|
||||||
options.sceneElementsMap,
|
|
||||||
updates,
|
|
||||||
{
|
|
||||||
isDragging: options?.isDragging,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
bumpVersion(element);
|
|
||||||
} else {
|
} else {
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
const prevCoords = getElementPointsCoords(element, element.points);
|
const prevCoords = getElementPointsCoords(element, element.points);
|
||||||
|
@ -1515,7 +1513,7 @@ export class LinearElementEditor {
|
||||||
pointFrom(dX, dY),
|
pointFrom(dX, dY),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
...otherUpdates,
|
...otherUpdates,
|
||||||
points: nextPoints,
|
points: nextPoints,
|
||||||
x: element.x + rotated[0],
|
x: element.x + rotated[0],
|
||||||
|
@ -1574,7 +1572,7 @@ export class LinearElementEditor {
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
mutateElement(boundTextElement, { isDeleted: true });
|
mutateElement(boundTextElement, elementsMap, { isDeleted: true });
|
||||||
}
|
}
|
||||||
let x = 0;
|
let x = 0;
|
||||||
let y = 0;
|
let y = 0;
|
||||||
|
@ -1781,8 +1779,9 @@ export class LinearElementEditor {
|
||||||
index: number,
|
index: number,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
): LinearElementEditor {
|
): LinearElementEditor {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(
|
const element = LinearElementEditor.getElement(
|
||||||
linearElement.elementId,
|
linearElement.elementId,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -1825,7 +1824,7 @@ export class LinearElementEditor {
|
||||||
.map((segment) => segment.index)
|
.map((segment) => segment.index)
|
||||||
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fixedSegments: nextFixedSegments,
|
fixedSegments: nextFixedSegments,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1859,14 +1858,14 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
static deleteFixedSegment(
|
static deleteFixedSegment(
|
||||||
element: ExcalidrawElbowArrowElement,
|
element: ExcalidrawElbowArrowElement,
|
||||||
|
scene: Scene,
|
||||||
index: number,
|
index: number,
|
||||||
): void {
|
): void {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fixedSegments: element.fixedSegments?.filter(
|
fixedSegments: element.fixedSegments?.filter(
|
||||||
(segment) => segment.index !== index,
|
(segment) => segment.index !== index,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
mutateElement(element, {}, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,8 @@ import {
|
||||||
getSizeFromPoints,
|
getSizeFromPoints,
|
||||||
randomInteger,
|
randomInteger,
|
||||||
getUpdatedTimestamp,
|
getUpdatedTimestamp,
|
||||||
toBrandedType,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
@ -16,35 +11,42 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
import { ShapeCache } from "./ShapeCache";
|
import { ShapeCache } from "./ShapeCache";
|
||||||
|
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
import { isElbowArrow } from "./typeChecks";
|
import { isElbowArrow } from "./typeChecks";
|
||||||
|
|
||||||
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types";
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
"id" | "version" | "versionNonce" | "updated"
|
"id" | "version" | "versionNonce" | "updated"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// This function tracks updates of text elements for the purposes for collaboration.
|
/**
|
||||||
// The version is used to compare updates when more than one user is working in
|
* This function tracks updates of text elements for the purposes for collaboration.
|
||||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
* The version is used to compare updates when more than one user is working in
|
||||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
* the same drawing.
|
||||||
|
*
|
||||||
|
* WARNING: this won't trigger the component to update, so if you need to trigger component update,
|
||||||
|
* use `scene.mutateElement` or `ExcalidrawImperativeAPI.mutateElement` instead.
|
||||||
|
*/
|
||||||
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
informMutation = true,
|
|
||||||
options?: {
|
options?: {
|
||||||
// Currently only for elbow arrows.
|
|
||||||
// If true, the elbow arrow tries to bind to the nearest element. If false
|
|
||||||
// it tries to keep the same bound element, if any.
|
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
},
|
},
|
||||||
): TElement => {
|
) => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
const { points, fixedSegments, fileId, startBinding, endBinding } =
|
const { points, fixedSegments, startBinding, endBinding, fileId } =
|
||||||
updates as any;
|
updates as any;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -55,10 +57,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
typeof startBinding !== "undefined" ||
|
typeof startBinding !== "undefined" ||
|
||||||
typeof endBinding !== "undefined") // manual binding to element
|
typeof endBinding !== "undefined") // manual binding to element
|
||||||
) {
|
) {
|
||||||
const elementsMap = toBrandedType<NonDeletedSceneElementsMap>(
|
|
||||||
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
|
||||||
);
|
|
||||||
|
|
||||||
updates = {
|
updates = {
|
||||||
...updates,
|
...updates,
|
||||||
angle: 0 as Radians,
|
angle: 0 as Radians,
|
||||||
|
@ -68,16 +66,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
x: updates.x || element.x,
|
x: updates.x || element.x,
|
||||||
y: updates.y || element.y,
|
y: updates.y || element.y,
|
||||||
},
|
},
|
||||||
elementsMap,
|
elementsMap as NonDeletedSceneElementsMap,
|
||||||
{
|
updates as ElementUpdate<ExcalidrawElbowArrowElement>,
|
||||||
fixedSegments,
|
options,
|
||||||
points,
|
|
||||||
startBinding,
|
|
||||||
endBinding,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isDragging: options?.isDragging,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (typeof points !== "undefined") {
|
} else if (typeof points !== "undefined") {
|
||||||
|
@ -150,10 +141,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element.versionNonce = randomInteger();
|
element.versionNonce = randomInteger();
|
||||||
element.updated = getUpdatedTimestamp();
|
element.updated = getUpdatedTimestamp();
|
||||||
|
|
||||||
if (informMutation) {
|
|
||||||
Scene.getScene(element)?.triggerUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,6 @@ import type {
|
||||||
ExcalidrawIframeElement,
|
ExcalidrawIframeElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
FixedSegment,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
@ -478,7 +477,7 @@ export const newArrowElement = <T extends boolean>(
|
||||||
endArrowhead?: Arrowhead | null;
|
endArrowhead?: Arrowhead | null;
|
||||||
points?: ExcalidrawArrowElement["points"];
|
points?: ExcalidrawArrowElement["points"];
|
||||||
elbowed?: T;
|
elbowed?: T;
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"] | null;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): T extends true
|
): T extends true
|
||||||
? NonDeleted<ExcalidrawElbowArrowElement>
|
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||||
|
|
|
@ -17,8 +17,6 @@ import {
|
||||||
|
|
||||||
import type { GlobalPoint } from "@excalidraw/math";
|
import type { GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
@ -32,7 +30,6 @@ import {
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
|
@ -60,6 +57,8 @@ import {
|
||||||
|
|
||||||
import { isInGroup } from "./groups";
|
import { isInGroup } from "./groups";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { BoundingBox } from "./bounds";
|
import type { BoundingBox } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
|
@ -74,7 +73,6 @@ import type {
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
SceneElementsMap,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
@ -83,7 +81,6 @@ export const transformElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: SceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
|
@ -93,31 +90,31 @@ export const transformElements = (
|
||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
if (transformHandleType === "rotation") {
|
if (transformHandleType === "rotation") {
|
||||||
if (!isElbowArrow(element)) {
|
if (!isElbowArrow(element)) {
|
||||||
rotateSingleElement(
|
rotateSingleElement(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
);
|
);
|
||||||
updateBoundElements(element, elementsMap);
|
updateBoundElements(element, scene);
|
||||||
}
|
}
|
||||||
} else if (isTextElement(element) && transformHandleType) {
|
} else if (isTextElement(element) && transformHandleType) {
|
||||||
resizeSingleTextElement(
|
resizeSingleTextElement(
|
||||||
originalElements,
|
originalElements,
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
scene,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
);
|
);
|
||||||
updateBoundElements(element, elementsMap);
|
updateBoundElements(element, scene);
|
||||||
return true;
|
return true;
|
||||||
} else if (transformHandleType) {
|
} else if (transformHandleType) {
|
||||||
const elementId = selectedElements[0].id;
|
const elementId = selectedElements[0].id;
|
||||||
|
@ -129,8 +126,6 @@ export const transformElements = (
|
||||||
getNextSingleWidthAndHeightFromPointer(
|
getNextSingleWidthAndHeightFromPointer(
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElements,
|
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
|
@ -145,8 +140,8 @@ export const transformElements = (
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElements,
|
originalElements,
|
||||||
|
scene,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
{
|
{
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
|
@ -161,7 +156,6 @@ export const transformElements = (
|
||||||
rotateMultipleElements(
|
rotateMultipleElements(
|
||||||
originalElements,
|
originalElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
|
@ -210,13 +204,15 @@ export const transformElements = (
|
||||||
|
|
||||||
const rotateSingleElement = (
|
const rotateSingleElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
let angle: Radians;
|
let angle: Radians;
|
||||||
|
@ -233,13 +229,13 @@ const rotateSingleElement = (
|
||||||
}
|
}
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
mutateElement(element, { angle });
|
scene.mutateElement(element, { angle });
|
||||||
if (boundTextElementId) {
|
if (boundTextElementId) {
|
||||||
const textElement =
|
const textElement =
|
||||||
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
||||||
|
|
||||||
if (textElement && !isArrowElement(element)) {
|
if (textElement && !isArrowElement(element)) {
|
||||||
mutateElement(textElement, { angle });
|
scene.mutateElement(textElement, { angle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -289,12 +285,13 @@ export const measureFontSizeFromWidth = (
|
||||||
const resizeSingleTextElement = (
|
const resizeSingleTextElement = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
transformHandleType: TransformHandleDirection,
|
transformHandleType: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -393,7 +390,7 @@ const resizeSingleTextElement = (
|
||||||
);
|
);
|
||||||
const [nextX, nextY] = newTopLeft;
|
const [nextX, nextY] = newTopLeft;
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fontSize: metrics.size,
|
fontSize: metrics.size,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
|
@ -508,14 +505,13 @@ const resizeSingleTextElement = (
|
||||||
autoResize: false,
|
autoResize: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(element, resizedElement);
|
scene.mutateElement(element, resizedElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateMultipleElements = (
|
const rotateMultipleElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: SceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
|
@ -523,6 +519,7 @@ const rotateMultipleElements = (
|
||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
let centerAngle =
|
let centerAngle =
|
||||||
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
||||||
if (shouldRotateWithDiscreteAngle) {
|
if (shouldRotateWithDiscreteAngle) {
|
||||||
|
@ -543,38 +540,30 @@ const rotateMultipleElements = (
|
||||||
(centerAngle + origAngle - element.angle) as Radians,
|
(centerAngle + origAngle - element.angle) as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
const updates = isElbowArrow(element)
|
||||||
// Needed to re-route the arrow
|
? {
|
||||||
mutateElement(element, {
|
// Needed to re-route the arrow
|
||||||
points: getArrowLocalFixedPoints(element, elementsMap),
|
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||||
});
|
}
|
||||||
} else {
|
: {
|
||||||
mutateElement(
|
|
||||||
element,
|
|
||||||
{
|
|
||||||
x: element.x + (rotatedCX - cx),
|
x: element.x + (rotatedCX - cx),
|
||||||
y: element.y + (rotatedCY - cy),
|
y: element.y + (rotatedCY - cy),
|
||||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||||
},
|
};
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
scene.mutateElement(element, updates);
|
||||||
|
|
||||||
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: elements,
|
simultaneouslyUpdated: elements,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !isArrowElement(element)) {
|
if (boundText && !isArrowElement(element)) {
|
||||||
mutateElement(
|
scene.mutateElement(boundText, {
|
||||||
boundText,
|
x: boundText.x + (rotatedCX - cx),
|
||||||
{
|
y: boundText.y + (rotatedCY - cy),
|
||||||
x: boundText.x + (rotatedCX - cx),
|
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||||
y: boundText.y + (rotatedCY - cy),
|
});
|
||||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -819,8 +808,8 @@ export const resizeSingleElement = (
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
handleDirection: TransformHandleDirection,
|
handleDirection: TransformHandleDirection,
|
||||||
{
|
{
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
|
@ -833,6 +822,7 @@ export const resizeSingleElement = (
|
||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
let boundTextFont: { fontSize?: number } = {};
|
let boundTextFont: { fontSize?: number } = {};
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
|
@ -932,7 +922,7 @@ export const resizeSingleElement = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("scale" in latestElement && "scale" in origElement) {
|
if ("scale" in latestElement && "scale" in origElement) {
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
scale: [
|
scale: [
|
||||||
// defaulting because scaleX/Y can be 0/-0
|
// defaulting because scaleX/Y can be 0/-0
|
||||||
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
|
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
|
||||||
|
@ -967,21 +957,24 @@ export const resizeSingleElement = (
|
||||||
...rescaledPoints,
|
...rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(latestElement, updates, shouldInformMutation);
|
scene.mutateElement(latestElement, updates, {
|
||||||
|
informMutation: shouldInformMutation,
|
||||||
|
isDragging: false,
|
||||||
|
});
|
||||||
|
|
||||||
updateBoundElements(latestElement, elementsMap as SceneElementsMap, {
|
updateBoundElements(latestElement, scene, {
|
||||||
// TODO: confirm with MARK if this actually makes sense
|
// TODO: confirm with MARK if this actually makes sense
|
||||||
newSize: { width: nextWidth, height: nextHeight },
|
newSize: { width: nextWidth, height: nextHeight },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (boundTextElement && boundTextFont != null) {
|
if (boundTextElement && boundTextFont != null) {
|
||||||
mutateElement(boundTextElement, {
|
scene.mutateElement(boundTextElement, {
|
||||||
fontSize: boundTextFont.fontSize,
|
fontSize: boundTextFont.fontSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleBindTextResize(
|
handleBindTextResize(
|
||||||
latestElement,
|
latestElement,
|
||||||
elementsMap,
|
scene,
|
||||||
handleDirection,
|
handleDirection,
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
);
|
);
|
||||||
|
@ -991,8 +984,6 @@ export const resizeSingleElement = (
|
||||||
const getNextSingleWidthAndHeightFromPointer = (
|
const getNextSingleWidthAndHeightFromPointer = (
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
|
||||||
handleDirection: TransformHandleDirection,
|
handleDirection: TransformHandleDirection,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
|
@ -1527,27 +1518,24 @@ export const resizeMultipleElements = (
|
||||||
} of elementsAndUpdates) {
|
} of elementsAndUpdates) {
|
||||||
const { width, height, angle } = update;
|
const { width, height, angle } = update;
|
||||||
|
|
||||||
mutateElement(element, update, false, {
|
scene.mutateElement(element, update, {
|
||||||
|
informMutation: true,
|
||||||
// needed for the fixed binding point udpate to take effect
|
// needed for the fixed binding point udpate to take effect
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: elementsToUpdate,
|
simultaneouslyUpdated: elementsToUpdate,
|
||||||
newSize: { width, height },
|
newSize: { width, height },
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement && boundTextFontSize) {
|
if (boundTextElement && boundTextFontSize) {
|
||||||
mutateElement(
|
scene.mutateElement(boundTextElement, {
|
||||||
boundTextElement,
|
fontSize: boundTextFontSize,
|
||||||
{
|
angle: isLinearElement(element) ? undefined : angle,
|
||||||
fontSize: boundTextFontSize,
|
});
|
||||||
angle: isLinearElement(element) ? undefined : angle,
|
handleBindTextResize(element, scene, handleDirection, true);
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
handleBindTextResize(element, elementsMap, handleDirection, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { isShallowEqual } from "@excalidraw/common";
|
import { arrayToMap, isShallowEqual } from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -7,13 +7,20 @@ import type {
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||||
import { isElementInViewport } from "./sizeHelpers";
|
import { isElementInViewport } from "./sizeHelpers";
|
||||||
import { isBoundToContainer, isFrameLikeElement } from "./typeChecks";
|
import {
|
||||||
|
isBoundToContainer,
|
||||||
|
isFrameLikeElement,
|
||||||
|
isLinearElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
getContainingFrame,
|
getContainingFrame,
|
||||||
getFrameChildren,
|
getFrameChildren,
|
||||||
} from "./frame";
|
} from "./frame";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { selectGroupsForSelectedElements } from "./groups";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
|
@ -254,3 +261,49 @@ export const makeNextSelectedElementIds = (
|
||||||
|
|
||||||
return nextSelectedElementIds;
|
return nextSelectedElementIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _getLinearElementEditor = (
|
||||||
|
targetElements: readonly ExcalidrawElement[],
|
||||||
|
allElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const linears = targetElements.filter(isLinearElement);
|
||||||
|
if (linears.length === 1) {
|
||||||
|
const linear = linears[0];
|
||||||
|
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
||||||
|
const onlySingleLinearSelected = targetElements.every(
|
||||||
|
(el) => el.id === linear.id || boundElements.includes(el.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onlySingleLinearSelected) {
|
||||||
|
return new LinearElementEditor(linear, arrayToMap(allElements));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSelectionStateForElements = (
|
||||||
|
targetElements: readonly ExcalidrawElement[],
|
||||||
|
allElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
selectedLinearElement: _getLinearElementEditor(targetElements, allElements),
|
||||||
|
...selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
editingGroupId: appState.editingGroupId,
|
||||||
|
selectedElementIds: excludeElementsInFramesFromSelection(
|
||||||
|
targetElements,
|
||||||
|
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
|
if (!isBoundToContainer(element)) {
|
||||||
|
acc[element.id] = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
},
|
||||||
|
allElements,
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
LINE_CONFIRM_THRESHOLD,
|
LINE_CONFIRM_THRESHOLD,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
invariant,
|
invariant,
|
||||||
|
elementCenterPoint,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
isPoint,
|
isPoint,
|
||||||
|
@ -297,7 +298,7 @@ export const aabbForElement = (
|
||||||
midY: element.y + element.height / 2,
|
midY: element.y + element.height / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const center = pointFrom(bbox.midX, bbox.midY);
|
const center = elementCenterPoint(element);
|
||||||
const [topLeftX, topLeftY] = pointRotateRads(
|
const [topLeftX, topLeftY] = pointRotateRads(
|
||||||
pointFrom(bbox.minX, bbox.minY),
|
pointFrom(bbox.minX, bbox.minY),
|
||||||
center,
|
center,
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||||
|
@ -170,41 +169,6 @@ export const getLockedLinearCursorAlignSize = (
|
||||||
return { width, height };
|
return { width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resizePerfectLineForNWHandler = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
) => {
|
|
||||||
const anchorX = element.x + element.width;
|
|
||||||
const anchorY = element.y + element.height;
|
|
||||||
const distanceToAnchorX = x - anchorX;
|
|
||||||
const distanceToAnchorY = y - anchorY;
|
|
||||||
if (Math.abs(distanceToAnchorX) < Math.abs(distanceToAnchorY) / 2) {
|
|
||||||
mutateElement(element, {
|
|
||||||
x: anchorX,
|
|
||||||
width: 0,
|
|
||||||
y,
|
|
||||||
height: -distanceToAnchorY,
|
|
||||||
});
|
|
||||||
} else if (Math.abs(distanceToAnchorY) < Math.abs(element.width) / 2) {
|
|
||||||
mutateElement(element, {
|
|
||||||
y: anchorY,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const nextHeight =
|
|
||||||
Math.sign(distanceToAnchorY) *
|
|
||||||
Math.sign(distanceToAnchorX) *
|
|
||||||
element.width;
|
|
||||||
mutateElement(element, {
|
|
||||||
x,
|
|
||||||
y: anchorY - nextHeight,
|
|
||||||
width: -distanceToAnchorX,
|
|
||||||
height: nextHeight,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNormalizedDimensions = (
|
export const getNormalizedDimensions = (
|
||||||
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
|
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
|
||||||
): {
|
): {
|
||||||
|
|
|
@ -6,18 +6,22 @@ import {
|
||||||
TEXT_ALIGN,
|
TEXT_ALIGN,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
getFontString,
|
getFontString,
|
||||||
|
isProdEnv,
|
||||||
|
invariant,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { measureText } from "./textMeasurements";
|
import { measureText } from "./textMeasurements";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import {
|
import {
|
||||||
|
@ -26,6 +30,8 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
@ -40,17 +46,30 @@ import type {
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
informMutation = true,
|
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
|
|
||||||
|
if (!isProdEnv()) {
|
||||||
|
invariant(
|
||||||
|
!container || !isArrowElement(container) || textElement.angle === 0,
|
||||||
|
"text element angle must be 0 if bound to arrow container",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const boundTextUpdates = {
|
const boundTextUpdates = {
|
||||||
x: textElement.x,
|
x: textElement.x,
|
||||||
y: textElement.y,
|
y: textElement.y,
|
||||||
text: textElement.text,
|
text: textElement.text,
|
||||||
width: textElement.width,
|
width: textElement.width,
|
||||||
height: textElement.height,
|
height: textElement.height,
|
||||||
angle: container?.angle ?? textElement.angle,
|
angle: (container
|
||||||
|
? isArrowElement(container)
|
||||||
|
? 0
|
||||||
|
: container.angle
|
||||||
|
: textElement.angle) as Radians,
|
||||||
};
|
};
|
||||||
|
|
||||||
boundTextUpdates.text = textElement.text;
|
boundTextUpdates.text = textElement.text;
|
||||||
|
@ -90,38 +109,43 @@ export const redrawTextBoundingBox = (
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: nextHeight }, informMutation);
|
scene.mutateElement(container, { height: nextHeight });
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metrics.width > maxContainerWidth) {
|
if (metrics.width > maxContainerWidth) {
|
||||||
const nextWidth = computeContainerDimensionForBoundText(
|
const nextWidth = computeContainerDimensionForBoundText(
|
||||||
metrics.width,
|
metrics.width,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { width: nextWidth }, informMutation);
|
scene.mutateElement(container, { width: nextWidth });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTextElement = {
|
const updatedTextElement = {
|
||||||
...textElement,
|
...textElement,
|
||||||
...boundTextUpdates,
|
...boundTextUpdates,
|
||||||
} as ExcalidrawTextElementWithContainer;
|
} as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
const { x, y } = computeBoundTextPosition(
|
const { x, y } = computeBoundTextPosition(
|
||||||
container,
|
container,
|
||||||
updatedTextElement,
|
updatedTextElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
boundTextUpdates.x = x;
|
boundTextUpdates.x = x;
|
||||||
boundTextUpdates.y = y;
|
boundTextUpdates.y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(textElement, boundTextUpdates, informMutation);
|
scene.mutateElement(textElement, boundTextUpdates);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
container: NonDeletedExcalidrawElement,
|
container: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
shouldMaintainAspectRatio = false,
|
shouldMaintainAspectRatio = false,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId) {
|
if (!boundTextElementId) {
|
||||||
return;
|
return;
|
||||||
|
@ -174,20 +198,20 @@ export const handleBindTextResize = (
|
||||||
transformHandleType === "n")
|
transformHandleType === "n")
|
||||||
? container.y - diff
|
? container.y - diff
|
||||||
: container.y;
|
: container.y;
|
||||||
mutateElement(container, {
|
scene.mutateElement(container, {
|
||||||
height: containerHeight,
|
height: containerHeight,
|
||||||
y: updatedY,
|
y: updatedY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(textElement, {
|
scene.mutateElement(textElement, {
|
||||||
text,
|
text,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
textElement,
|
textElement,
|
||||||
computeBoundTextPosition(container, textElement, elementsMap),
|
computeBoundTextPosition(container, textElement, elementsMap),
|
||||||
);
|
);
|
||||||
|
@ -335,7 +359,10 @@ export const getTextElementAngle = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawTextContainer | null,
|
container: ExcalidrawTextContainer | null,
|
||||||
) => {
|
) => {
|
||||||
if (!container || isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!container) {
|
||||||
return textElement.angle;
|
return textElement.angle;
|
||||||
}
|
}
|
||||||
return container.angle;
|
return container.angle;
|
||||||
|
|
|
@ -119,6 +119,20 @@ export const isElbowArrow = (
|
||||||
return isArrowElement(element) && element.elbowed;
|
return isArrowElement(element) && element.elbowed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isSharpArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return isArrowElement(element) && !element.elbowed && !element.roundness;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCurvedArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return (
|
||||||
|
isArrowElement(element) && !element.elbowed && element.roundness !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isLinearElementType = (
|
export const isLinearElementType = (
|
||||||
elementType: ElementOrToolType,
|
elementType: ElementOrToolType,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
@ -271,6 +285,10 @@ export const isBoundToContainer = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isArrowBoundToElement = (element: ExcalidrawArrowElement) => {
|
||||||
|
return !!element.startBinding || !!element.endBinding;
|
||||||
|
};
|
||||||
|
|
||||||
export const isUsingAdaptiveRadius = (type: string) =>
|
export const isUsingAdaptiveRadius = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "embeddable" ||
|
type === "embeddable" ||
|
||||||
|
|
|
@ -412,3 +412,11 @@ export type NonDeletedSceneElementsMap = Map<
|
||||||
export type ElementsMapOrArray =
|
export type ElementsMapOrArray =
|
||||||
| readonly ExcalidrawElement[]
|
| readonly ExcalidrawElement[]
|
||||||
| Readonly<ElementsMap>;
|
| Readonly<ElementsMap>;
|
||||||
|
|
||||||
|
export type ConvertibleGenericTypes = "rectangle" | "diamond" | "ellipse";
|
||||||
|
export type ConvertibleLinearTypes =
|
||||||
|
| "line"
|
||||||
|
| "sharpArrow"
|
||||||
|
| "curvedArrow"
|
||||||
|
| "elbowArrow";
|
||||||
|
export type ConvertibleTypes = ConvertibleGenericTypes | ConvertibleLinearTypes;
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
import type { Curve, LineSegment } from "@excalidraw/math";
|
||||||
|
|
||||||
import { getCornerRadius } from "./shapes";
|
import { getCornerRadius } from "./shapes";
|
||||||
|
@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
|
||||||
return [sides, []];
|
return [sides, []];
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const r = rectangle(
|
const r = rectangle(
|
||||||
pointFrom(element.x, element.y),
|
pointFrom(element.x, element.y),
|
||||||
|
@ -254,10 +253,7 @@ export function deconstructDiamondElement(
|
||||||
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
||||||
}
|
}
|
||||||
|
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = elementCenterPoint(element);
|
||||||
element.x + element.width / 2,
|
|
||||||
element.y + element.height / 2,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||||
pointFrom(element.x + topX, element.y + topY),
|
pointFrom(element.x + topX, element.y + topY),
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import { isFrameLikeElement } from "./typeChecks";
|
import { isFrameLikeElement } from "./typeChecks";
|
||||||
|
|
||||||
import { getElementsInGroup } from "./groups";
|
import { getElementsInGroup } from "./groups";
|
||||||
|
@ -12,6 +10,8 @@ import { syncMovedIndices } from "./fractionalIndex";
|
||||||
|
|
||||||
import { getSelectedElements } from "./selection";
|
import { getSelectedElements } from "./selection";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
||||||
|
|
||||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import React from "react";
|
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -8,7 +7,7 @@ import {
|
||||||
isPrimitive,
|
isPrimitive,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||||
|
|
||||||
|
@ -25,7 +24,6 @@ import {
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { mutateElement } from "../src/mutateElement";
|
|
||||||
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
||||||
|
|
||||||
import type { ExcalidrawLinearElement } from "../src/types";
|
import type { ExcalidrawLinearElement } from "../src/types";
|
||||||
|
@ -63,11 +61,11 @@ describe("duplicating single elements", () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
element.__proto__ = { hello: "world" };
|
element.__proto__ = { hello: "world" };
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, new Map(), {
|
||||||
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(null, new Map(), element, undefined, true);
|
const copy = duplicateElement(null, new Map(), element, true);
|
||||||
|
|
||||||
assertCloneObjects(element, copy);
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
|
@ -173,7 +171,7 @@ describe("duplicating multiple elements", () => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const { duplicatedElements } = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
});
|
});
|
||||||
|
@ -181,10 +179,10 @@ describe("duplicating multiple elements", () => {
|
||||||
// generic id in-equality checks
|
// generic id in-equality checks
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
expect(origElements.map((e) => e.type)).toEqual(
|
expect(origElements.map((e) => e.type)).toEqual(
|
||||||
clonedElements.map((e) => e.type),
|
duplicatedElements.map((e) => e.type),
|
||||||
);
|
);
|
||||||
origElements.forEach((origElement, idx) => {
|
origElements.forEach((origElement, idx) => {
|
||||||
const clonedElement = clonedElements[idx];
|
const clonedElement = duplicatedElements[idx];
|
||||||
expect(origElement).toEqual(
|
expect(origElement).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.not.stringMatching(clonedElement.id),
|
id: expect.not.stringMatching(clonedElement.id),
|
||||||
|
@ -217,12 +215,12 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
const clonedArrows = clonedElements.filter(
|
const clonedArrows = duplicatedElements.filter(
|
||||||
(e) => e.type === "arrow",
|
(e) => e.type === "arrow",
|
||||||
) as ExcalidrawLinearElement[];
|
) as ExcalidrawLinearElement[];
|
||||||
|
|
||||||
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
|
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
|
||||||
clonedElements as any as typeof origElements;
|
duplicatedElements as any as typeof origElements;
|
||||||
|
|
||||||
expect(clonedText1.containerId).toBe(clonedRectangle.id);
|
expect(clonedText1.containerId).toBe(clonedRectangle.id);
|
||||||
expect(
|
expect(
|
||||||
|
@ -327,10 +325,10 @@ describe("duplicating multiple elements", () => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const duplicatedElements = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
}) as any as { newElements: typeof origElements };
|
}).duplicatedElements as any as typeof origElements;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
clonedRectangle,
|
clonedRectangle,
|
||||||
|
@ -338,7 +336,7 @@ describe("duplicating multiple elements", () => {
|
||||||
clonedArrow1,
|
clonedArrow1,
|
||||||
clonedArrow2,
|
clonedArrow2,
|
||||||
clonedArrow3,
|
clonedArrow3,
|
||||||
] = clonedElements;
|
] = duplicatedElements;
|
||||||
|
|
||||||
expect(clonedRectangle.boundElements).toEqual([
|
expect(clonedRectangle.boundElements).toEqual([
|
||||||
{ id: clonedArrow1.id, type: "arrow" },
|
{ id: clonedArrow1.id, type: "arrow" },
|
||||||
|
@ -374,12 +372,12 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const { duplicatedElements } = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
}) as any as { newElements: typeof origElements };
|
});
|
||||||
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
||||||
clonedElements;
|
duplicatedElements;
|
||||||
|
|
||||||
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
||||||
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
|
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
|
||||||
|
@ -399,7 +397,7 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
newElements: [clonedRectangle1],
|
duplicatedElements: [clonedRectangle1],
|
||||||
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
||||||
|
|
||||||
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
||||||
|
@ -408,6 +406,117 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("group-related duplication", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("action-duplicating within group", async () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: ["group1"] },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe("group1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("alt-duplicating within group", async () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle2.x + 5, rectangle2.y + 5);
|
||||||
|
mouse.up(rectangle2.x + 50, rectangle2.y + 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: ["group1"] },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe("group1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("alt-duplicating within group away outside frame", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([frame, rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle2.x + 5, rectangle2.y + 5);
|
||||||
|
mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(h.elements);
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: frame.id },
|
||||||
|
{ id: rectangle1.id, frameId: frame.id },
|
||||||
|
{ id: rectangle2.id, frameId: frame.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: [], frameId: null },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("duplication z-order", () => {
|
describe("duplication z-order", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<Excalidraw />);
|
await render(<Excalidraw />);
|
||||||
|
@ -503,8 +612,8 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ id: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
]);
|
]);
|
||||||
|
@ -538,8 +647,8 @@ describe("duplication z-order", () => {
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ id: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ [ORIG_ID]: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
{ id: rectangle3.id, selected: true },
|
{ [ORIG_ID]: rectangle3.id, selected: true },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -569,8 +678,8 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ id: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
]);
|
]);
|
||||||
|
@ -605,19 +714,19 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ [ORIG_ID]: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ [ORIG_ID]: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id, selected: true },
|
{ [ORIG_ID]: rectangle2.id, selected: true },
|
||||||
{ id: rectangle3.id, selected: true },
|
{ [ORIG_ID]: rectangle3.id, selected: true },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating text container (in-order)", async () => {
|
it("alt-duplicating text container (in-order)", async () => {
|
||||||
const [rectangle, text] = API.createTextContainer();
|
const [rectangle, text] = API.createTextContainer();
|
||||||
API.setElements([rectangle, text]);
|
API.setElements([rectangle, text]);
|
||||||
API.setSelectedElements([rectangle, text]);
|
API.setSelectedElements([rectangle]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||||
|
@ -625,20 +734,20 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle.id },
|
{ id: rectangle.id },
|
||||||
|
{ id: text.id, containerId: rectangle.id },
|
||||||
|
{ [ORIG_ID]: rectangle.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: rectangle.id, selected: true },
|
|
||||||
{ id: text.id, containerId: rectangle.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating text container (out-of-order)", async () => {
|
it("alt-duplicating text container (out-of-order)", async () => {
|
||||||
const [rectangle, text] = API.createTextContainer();
|
const [rectangle, text] = API.createTextContainer();
|
||||||
API.setElements([text, rectangle]);
|
API.setElements([text, rectangle]);
|
||||||
API.setSelectedElements([rectangle, text]);
|
API.setSelectedElements([rectangle]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||||
|
@ -646,21 +755,21 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle.id },
|
{ id: rectangle.id },
|
||||||
|
{ id: text.id, containerId: rectangle.id },
|
||||||
|
{ [ORIG_ID]: rectangle.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: rectangle.id, selected: true },
|
|
||||||
{ id: text.id, containerId: rectangle.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating labeled arrows (in-order)", async () => {
|
it("alt-duplicating labeled arrows (in-order)", async () => {
|
||||||
const [arrow, text] = API.createLabeledArrow();
|
const [arrow, text] = API.createLabeledArrow();
|
||||||
|
|
||||||
API.setElements([arrow, text]);
|
API.setElements([arrow, text]);
|
||||||
API.setSelectedElements([arrow, text]);
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(arrow.x + 5, arrow.y + 5);
|
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||||
|
@ -668,21 +777,24 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: arrow.id },
|
{ id: arrow.id },
|
||||||
|
{ id: text.id, containerId: arrow.id },
|
||||||
|
{ [ORIG_ID]: arrow.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(arrow.id)?.id,
|
containerId: getCloneByOrigId(arrow.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: arrow.id, selected: true },
|
|
||||||
{ id: text.id, containerId: arrow.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
|
expect(h.state.selectedLinearElement).toEqual(
|
||||||
|
expect.objectContaining({ elementId: getCloneByOrigId(arrow.id)?.id }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating labeled arrows (out-of-order)", async () => {
|
it("alt-duplicating labeled arrows (out-of-order)", async () => {
|
||||||
const [arrow, text] = API.createLabeledArrow();
|
const [arrow, text] = API.createLabeledArrow();
|
||||||
|
|
||||||
API.setElements([text, arrow]);
|
API.setElements([text, arrow]);
|
||||||
API.setSelectedElements([arrow, text]);
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(arrow.x + 5, arrow.y + 5);
|
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||||
|
@ -690,17 +802,17 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: arrow.id },
|
{ id: arrow.id },
|
||||||
|
{ id: text.id, containerId: arrow.id },
|
||||||
|
{ [ORIG_ID]: arrow.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(arrow.id)?.id,
|
containerId: getCloneByOrigId(arrow.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: arrow.id, selected: true },
|
|
||||||
{ id: text.id, containerId: arrow.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating bindable element with bound arrow should keep the arrow on the duplicate", () => {
|
it("alt-duplicating bindable element with bound arrow should keep the arrow on the duplicate", async () => {
|
||||||
const rect = UI.createElement("rectangle", {
|
const rect = UI.createElement("rectangle", {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -722,11 +834,18 @@ describe("duplication z-order", () => {
|
||||||
mouse.up(15, 15);
|
mouse.up(15, 15);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(window.h.elements).toHaveLength(3);
|
assertElements(h.elements, [
|
||||||
|
{
|
||||||
const newRect = window.h.elements[0];
|
id: rect.id,
|
||||||
|
boundElements: expect.arrayContaining([
|
||||||
expect(arrow.endBinding?.elementId).toBe(newRect.id);
|
expect.objectContaining({ id: arrow.id }),
|
||||||
expect(newRect.boundElements?.[0]?.id).toBe(arrow.id);
|
]),
|
||||||
|
},
|
||||||
|
{ [ORIG_ID]: rect.id, boundElements: [], selected: true },
|
||||||
|
{
|
||||||
|
id: arrow.id,
|
||||||
|
endBinding: expect.objectContaining({ elementId: rect.id }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { ARROW_TYPE } from "@excalidraw/common";
|
import { ARROW_TYPE } from "@excalidraw/common";
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
||||||
|
|
||||||
|
@ -23,6 +22,8 @@ import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { bindLinearElement } from "../src/binding";
|
import { bindLinearElement } from "../src/binding";
|
||||||
|
|
||||||
|
import Scene from "../src/Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
|
@ -142,7 +143,7 @@ describe("elbow arrow routing", () => {
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
mutateElement(arrow, {
|
h.app.scene.mutateElement(arrow, {
|
||||||
points: [
|
points: [
|
||||||
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
||||||
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
||||||
|
@ -187,14 +188,14 @@ describe("elbow arrow routing", () => {
|
||||||
scene.insertElement(rectangle1);
|
scene.insertElement(rectangle1);
|
||||||
scene.insertElement(rectangle2);
|
scene.insertElement(rectangle2);
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
bindLinearElement(arrow, rectangle1, "start", elementsMap);
|
bindLinearElement(arrow, rectangle1, "start", scene);
|
||||||
bindLinearElement(arrow, rectangle2, "end", elementsMap);
|
bindLinearElement(arrow, rectangle2, "end", scene);
|
||||||
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
mutateElement(arrow, {
|
h.app.scene.mutateElement(arrow, {
|
||||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
@ -749,7 +750,7 @@ function testInvalidIndicesSync(args: {
|
||||||
function prepareArguments(
|
function prepareArguments(
|
||||||
elementsLike: { id: string; index?: string }[],
|
elementsLike: { id: string; index?: string }[],
|
||||||
movedElementsIds?: string[],
|
movedElementsIds?: string[],
|
||||||
): [ExcalidrawElement[], Map<string, ExcalidrawElement> | undefined] {
|
): [ExcalidrawElement[], ElementsMap | undefined] {
|
||||||
const elements = elementsLike.map((x) =>
|
const elements = elementsLike.map((x) =>
|
||||||
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
|
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
|
||||||
);
|
);
|
||||||
|
@ -764,7 +765,7 @@ function prepareArguments(
|
||||||
function test(
|
function test(
|
||||||
name: string,
|
name: string,
|
||||||
elements: ExcalidrawElement[],
|
elements: ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement> | undefined,
|
movedElements: ElementsMap | undefined,
|
||||||
expectUnchangedElements: Map<string, { id: string }>,
|
expectUnchangedElements: Map<string, { id: string }>,
|
||||||
expectValidInput?: boolean,
|
expectValidInput?: boolean,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -333,7 +333,7 @@ describe("line element", () => {
|
||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"ne",
|
"ne",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -369,7 +369,7 @@ describe("line element", () => {
|
||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"se",
|
"se",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -424,7 +424,7 @@ describe("line element", () => {
|
||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"e",
|
"e",
|
||||||
{
|
{
|
||||||
shouldResizeFromCenter: true,
|
shouldResizeFromCenter: true,
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import { mutateElement } from "../src/mutateElement";
|
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||||
|
|
||||||
import { normalizeElementOrder } from "../src/sortElements";
|
import { normalizeElementOrder } from "../src/sortElements";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "../src/types";
|
import type { ExcalidrawElement } from "../src/types";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
const assertOrder = (
|
const assertOrder = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
expectedOrder: string[],
|
expectedOrder: string[],
|
||||||
|
@ -35,7 +37,7 @@ describe("normalizeElementsOrder", () => {
|
||||||
boundElements: [],
|
boundElements: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(container, {
|
mutateElement(container, new Map(), {
|
||||||
boundElements: [{ type: "text", id: boundText.id }],
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -352,7 +354,7 @@ describe("normalizeElementsOrder", () => {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [
|
boundElements: [
|
||||||
{ type: "text", id: boundText.id },
|
{ type: "text", id: boundText.id },
|
||||||
{ type: "text", id: "xxx" },
|
{ type: "text", id: "xxx" },
|
||||||
|
@ -387,7 +389,7 @@ describe("normalizeElementsOrder", () => {
|
||||||
boundElements: [],
|
boundElements: [],
|
||||||
groupIds: ["C", "A"],
|
groupIds: ["C", "A"],
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [{ type: "text", id: boundText.id }],
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -50,14 +50,8 @@ const alignSelectedElements = (
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
const elementsMap = arrayToMap(elements);
|
|
||||||
|
|
||||||
const updatedElements = alignElements(
|
const updatedElements = alignElements(selectedElements, alignment, app.scene);
|
||||||
selectedElements,
|
|
||||||
elementsMap,
|
|
||||||
alignment,
|
|
||||||
app.scene,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,12 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
|
isArrowElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import { measureText } from "@excalidraw/element/textMeasurements";
|
import { measureText } from "@excalidraw/element/textMeasurements";
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
@ -42,6 +42,8 @@ import type {
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
@ -77,7 +79,7 @@ export const actionUnbindText = register({
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
mutateElement(boundTextElement as ExcalidrawTextElement, {
|
app.scene.mutateElement(boundTextElement as ExcalidrawTextElement, {
|
||||||
containerId: null,
|
containerId: null,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
@ -85,7 +87,7 @@ export const actionUnbindText = register({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
boundElements: element.boundElements?.filter(
|
boundElements: element.boundElements?.filter(
|
||||||
(ele) => ele.id !== boundTextElement.id,
|
(ele) => ele.id !== boundTextElement.id,
|
||||||
),
|
),
|
||||||
|
@ -150,24 +152,21 @@ export const actionBindText = register({
|
||||||
textElement = selectedElements[1] as ExcalidrawTextElement;
|
textElement = selectedElements[1] as ExcalidrawTextElement;
|
||||||
container = selectedElements[0] as ExcalidrawTextContainer;
|
container = selectedElements[0] as ExcalidrawTextContainer;
|
||||||
}
|
}
|
||||||
mutateElement(textElement, {
|
app.scene.mutateElement(textElement, {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
|
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
app.scene.mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
type: "text",
|
type: "text",
|
||||||
id: textElement.id,
|
id: textElement.id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const originalContainerHeight = container.height;
|
const originalContainerHeight = container.height;
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(textElement, container, app.scene);
|
||||||
textElement,
|
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
// overwritting the cache with original container height so
|
// overwritting the cache with original container height so
|
||||||
// it can be restored when unbind
|
// it can be restored when unbind
|
||||||
updateOriginalContainerCache(container.id, originalContainerHeight);
|
updateOriginalContainerCache(container.id, originalContainerHeight);
|
||||||
|
@ -297,27 +296,23 @@ export const actionWrapTextInContainer = register({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startBinding || endBinding) {
|
if (startBinding || endBinding) {
|
||||||
mutateElement(ele, { startBinding, endBinding }, false);
|
app.scene.mutateElement(ele, {
|
||||||
|
startBinding,
|
||||||
|
endBinding,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(
|
app.scene.mutateElement(textElement, {
|
||||||
textElement,
|
containerId: container.id,
|
||||||
{
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
containerId: container.id,
|
boundElements: null,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
boundElements: null,
|
autoResize: true,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
});
|
||||||
autoResize: true,
|
|
||||||
},
|
redrawTextBoundingBox(textElement, container, app.scene);
|
||||||
false,
|
|
||||||
);
|
|
||||||
redrawTextBoundingBox(
|
|
||||||
textElement,
|
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
updatedElements = pushContainerBelowText(
|
updatedElements = pushContainerBelowText(
|
||||||
[...updatedElements, container],
|
[...updatedElements, container],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Excalidraw, mutateElement } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { act, assertElements, render } from "../tests/test-utils";
|
import { act, assertElements, render } from "../tests/test-utils";
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||||
frameId: f1.id,
|
frameId: f1.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(a1, {
|
h.app.scene.mutateElement(a1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,7 @@ import { KEYS, updateActiveTool } from "@excalidraw/common";
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import { fixBindingsAfterDeletion } from "@excalidraw/element/binding";
|
import { fixBindingsAfterDeletion } from "@excalidraw/element/binding";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||||
import {
|
import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
|
@ -94,7 +91,7 @@ const deleteSelectedElements = (
|
||||||
el.boundElements.forEach((candidate) => {
|
el.boundElements.forEach((candidate) => {
|
||||||
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
||||||
if (bound && isElbowArrow(bound)) {
|
if (bound && isElbowArrow(bound)) {
|
||||||
mutateElement(bound, {
|
app.scene.mutateElement(bound, {
|
||||||
startBinding:
|
startBinding:
|
||||||
el.id === bound.startBinding?.elementId
|
el.id === bound.startBinding?.elementId
|
||||||
? null
|
? null
|
||||||
|
@ -102,7 +99,6 @@ const deleteSelectedElements = (
|
||||||
endBinding:
|
endBinding:
|
||||||
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
||||||
});
|
});
|
||||||
mutateElement(bound, { points: bound.points });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -261,7 +257,11 @@ export const actionDeleteSelected = register({
|
||||||
: endBindingElement,
|
: endBindingElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
LinearElementEditor.deletePoints(element, selectedPointsIndices);
|
LinearElementEditor.deletePoints(
|
||||||
|
element,
|
||||||
|
app.scene,
|
||||||
|
selectedPointsIndices,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements,
|
elements,
|
||||||
|
|
|
@ -7,26 +7,17 @@ import {
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
|
||||||
isBoundToContainer,
|
|
||||||
isLinearElement,
|
|
||||||
} from "@excalidraw/element/typeChecks";
|
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
excludeElementsInFramesFromSelection,
|
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
|
getSelectionStateForElements,
|
||||||
} from "@excalidraw/element/selection";
|
} from "@excalidraw/element/selection";
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
import { duplicateElements } from "@excalidraw/element/duplicate";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
|
||||||
|
@ -52,7 +43,7 @@ export const actionDuplicateSelection = register({
|
||||||
try {
|
try {
|
||||||
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
||||||
appState,
|
appState,
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -65,52 +56,49 @@ export const actionDuplicateSelection = register({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let { newElements: duplicatedElements, elementsWithClones: nextElements } =
|
let { duplicatedElements, elementsWithDuplicates } = duplicateElements({
|
||||||
duplicateElements({
|
type: "in-place",
|
||||||
type: "in-place",
|
elements,
|
||||||
elements,
|
idsOfElementsToDuplicate: arrayToMap(
|
||||||
idsOfElementsToDuplicate: arrayToMap(
|
getSelectedElements(elements, appState, {
|
||||||
getSelectedElements(elements, appState, {
|
includeBoundTextElement: true,
|
||||||
includeBoundTextElement: true,
|
includeElementsInFrames: true,
|
||||||
includeElementsInFrames: true,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
appState,
|
|
||||||
randomizeSeed: true,
|
|
||||||
overrides: (element) => ({
|
|
||||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
|
||||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
|
||||||
}),
|
}),
|
||||||
reverseOrder: false,
|
),
|
||||||
});
|
appState,
|
||||||
|
randomizeSeed: true,
|
||||||
|
overrides: ({ origElement, origIdToDuplicateId }) => {
|
||||||
|
const duplicateFrameId =
|
||||||
|
origElement.frameId && origIdToDuplicateId.get(origElement.frameId);
|
||||||
|
return {
|
||||||
|
x: origElement.x + DEFAULT_GRID_SIZE / 2,
|
||||||
|
y: origElement.y + DEFAULT_GRID_SIZE / 2,
|
||||||
|
frameId: duplicateFrameId ?? origElement.frameId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (app.props.onDuplicate && nextElements) {
|
if (app.props.onDuplicate && elementsWithDuplicates) {
|
||||||
const mappedElements = app.props.onDuplicate(nextElements, elements);
|
const mappedElements = app.props.onDuplicate(
|
||||||
|
elementsWithDuplicates,
|
||||||
|
elements,
|
||||||
|
);
|
||||||
if (mappedElements) {
|
if (mappedElements) {
|
||||||
nextElements = mappedElements;
|
elementsWithDuplicates = mappedElements;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
|
elements: syncMovedIndices(
|
||||||
|
elementsWithDuplicates,
|
||||||
|
arrayToMap(duplicatedElements),
|
||||||
|
),
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...updateLinearElementEditors(duplicatedElements),
|
...getSelectionStateForElements(
|
||||||
...selectGroupsForSelectedElements(
|
duplicatedElements,
|
||||||
{
|
getNonDeletedElements(elementsWithDuplicates),
|
||||||
editingGroupId: appState.editingGroupId,
|
|
||||||
selectedElementIds: excludeElementsInFramesFromSelection(
|
|
||||||
duplicatedElements,
|
|
||||||
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
|
||||||
if (!isBoundToContainer(element)) {
|
|
||||||
acc[element.id] = true;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
getNonDeletedElements(nextElements),
|
|
||||||
appState,
|
appState,
|
||||||
null,
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
@ -130,24 +118,3 @@ export const actionDuplicateSelection = register({
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
|
|
||||||
const linears = clonedElements.filter(isLinearElement);
|
|
||||||
if (linears.length === 1) {
|
|
||||||
const linear = linears[0];
|
|
||||||
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
|
||||||
const onlySingleLinearSelected = clonedElements.every(
|
|
||||||
(el) => el.id === linear.id || boundElements.includes(el.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (onlySingleLinearSelected) {
|
|
||||||
return {
|
|
||||||
selectedLinearElement: new LinearElementEditor(linear),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedLinearElement: null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
|
@ -46,7 +46,6 @@ export const actionFinalize = register({
|
||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -72,7 +71,11 @@ export const actionFinalize = register({
|
||||||
scene.getElement(appState.pendingImageElementId);
|
scene.getElement(appState.pendingImageElementId);
|
||||||
|
|
||||||
if (pendingImageElement) {
|
if (pendingImageElement) {
|
||||||
mutateElement(pendingImageElement, { isDeleted: true }, false);
|
scene.mutateElement(
|
||||||
|
pendingImageElement,
|
||||||
|
{ isDeleted: true },
|
||||||
|
{ informMutation: false, isDragging: false },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
|
@ -96,7 +99,7 @@ export const actionFinalize = register({
|
||||||
!lastCommittedPoint ||
|
!lastCommittedPoint ||
|
||||||
points[points.length - 1] !== lastCommittedPoint
|
points[points.length - 1] !== lastCommittedPoint
|
||||||
) {
|
) {
|
||||||
mutateElement(multiPointElement, {
|
scene.mutateElement(multiPointElement, {
|
||||||
points: multiPointElement.points.slice(0, -1),
|
points: multiPointElement.points.slice(0, -1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -120,7 +123,7 @@ export const actionFinalize = register({
|
||||||
if (isLoop) {
|
if (isLoop) {
|
||||||
const linePoints = multiPointElement.points;
|
const linePoints = multiPointElement.points;
|
||||||
const firstPoint = linePoints[0];
|
const firstPoint = linePoints[0];
|
||||||
mutateElement(multiPointElement, {
|
scene.mutateElement(multiPointElement, {
|
||||||
points: linePoints.map((p, index) =>
|
points: linePoints.map((p, index) =>
|
||||||
index === linePoints.length - 1
|
index === linePoints.length - 1
|
||||||
? pointFrom(firstPoint[0], firstPoint[1])
|
? pointFrom(firstPoint[0], firstPoint[1])
|
||||||
|
@ -140,13 +143,7 @@ export const actionFinalize = register({
|
||||||
-1,
|
-1,
|
||||||
arrayToMap(elements),
|
arrayToMap(elements),
|
||||||
);
|
);
|
||||||
maybeBindLinearElement(
|
maybeBindLinearElement(multiPointElement, appState, { x, y }, scene);
|
||||||
multiPointElement,
|
|
||||||
appState,
|
|
||||||
{ x, y },
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +199,10 @@ export const actionFinalize = register({
|
||||||
// To select the linear element when user has finished mutipoint editing
|
// To select the linear element when user has finished mutipoint editing
|
||||||
selectedLinearElement:
|
selectedLinearElement:
|
||||||
multiPointElement && isLinearElement(multiPointElement)
|
multiPointElement && isLinearElement(multiPointElement)
|
||||||
? new LinearElementEditor(multiPointElement)
|
? new LinearElementEditor(
|
||||||
|
multiPointElement,
|
||||||
|
arrayToMap(newElements),
|
||||||
|
)
|
||||||
: appState.selectedLinearElement,
|
: appState.selectedLinearElement,
|
||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,10 +4,7 @@ import {
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||||
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
||||||
import {
|
import {
|
||||||
|
@ -162,11 +159,9 @@ const flipElements = (
|
||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isLinearElement),
|
||||||
elementsMap,
|
|
||||||
app.scene.getNonDeletedElements(),
|
|
||||||
app.scene,
|
|
||||||
isBindingEnabled(appState),
|
isBindingEnabled(appState),
|
||||||
[],
|
[],
|
||||||
|
app.scene,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -194,13 +189,13 @@ const flipElements = (
|
||||||
getCommonBoundingBox(selectedElements);
|
getCommonBoundingBox(selectedElements);
|
||||||
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||||
otherElements.forEach((element) =>
|
otherElements.forEach((element) =>
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
x: element.x + diffX,
|
x: element.x + diffX,
|
||||||
y: element.y + diffY,
|
y: element.y + diffY,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
elbowArrows.forEach((element) =>
|
elbowArrows.forEach((element) =>
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
x: element.x + diffX,
|
x: element.x + diffX,
|
||||||
y: element.y + diffY,
|
y: element.y + diffY,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -173,11 +173,9 @@ export const actionWrapSelectionInFrame = register({
|
||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements, elementsMap);
|
||||||
selectedElements,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
const PADDING = 16;
|
const PADDING = 16;
|
||||||
const frame = newFrameElement({
|
const frame = newFrameElement({
|
||||||
x: x1 - PADDING,
|
x: x1 - PADDING,
|
||||||
|
@ -196,13 +194,9 @@ export const actionWrapSelectionInFrame = register({
|
||||||
for (const elementInGroup of elementsInGroup) {
|
for (const elementInGroup of elementsInGroup) {
|
||||||
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
||||||
|
|
||||||
mutateElement(
|
mutateElement(elementInGroup, elementsMap, {
|
||||||
elementInGroup,
|
groupIds: elementInGroup.groupIds.slice(0, index),
|
||||||
{
|
});
|
||||||
groupIds: elementInGroup.groupIds.slice(0, index),
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
|
import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
|
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
||||||
|
@ -50,7 +52,7 @@ export const actionToggleLinearEditor = register({
|
||||||
const editingLinearElement =
|
const editingLinearElement =
|
||||||
appState.editingLinearElement?.elementId === selectedElement.id
|
appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
? null
|
? null
|
||||||
: new LinearElementEditor(selectedElement);
|
: new LinearElementEditor(selectedElement, arrayToMap(elements));
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
|
|
@ -34,10 +34,7 @@ import {
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
|
@ -61,6 +58,7 @@ import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
@ -68,9 +66,10 @@ import type {
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
||||||
|
@ -207,25 +206,22 @@ export const getFormValue = function <T extends Primitive>(
|
||||||
const offsetElementAfterFontResize = (
|
const offsetElementAfterFontResize = (
|
||||||
prevElement: ExcalidrawTextElement,
|
prevElement: ExcalidrawTextElement,
|
||||||
nextElement: ExcalidrawTextElement,
|
nextElement: ExcalidrawTextElement,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
|
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
|
||||||
return nextElement;
|
return nextElement;
|
||||||
}
|
}
|
||||||
return mutateElement(
|
return scene.mutateElement(nextElement, {
|
||||||
nextElement,
|
x:
|
||||||
{
|
prevElement.textAlign === "left"
|
||||||
x:
|
? prevElement.x
|
||||||
prevElement.textAlign === "left"
|
: prevElement.x +
|
||||||
? prevElement.x
|
(prevElement.width - nextElement.width) /
|
||||||
: prevElement.x +
|
(prevElement.textAlign === "center" ? 2 : 1),
|
||||||
(prevElement.width - nextElement.width) /
|
// centering vertically is non-standard, but for Excalidraw I think
|
||||||
(prevElement.textAlign === "center" ? 2 : 1),
|
// it makes sense
|
||||||
// centering vertically is non-standard, but for Excalidraw I think
|
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
||||||
// it makes sense
|
});
|
||||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeFontSize = (
|
const changeFontSize = (
|
||||||
|
@ -251,10 +247,14 @@ const changeFontSize = (
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
newElement,
|
newElement,
|
||||||
app.scene.getContainerElement(oldElement),
|
app.scene.getContainerElement(oldElement),
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
newElement = offsetElementAfterFontResize(
|
||||||
|
oldElement,
|
||||||
|
newElement,
|
||||||
|
app.scene,
|
||||||
|
);
|
||||||
|
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
@ -264,15 +264,11 @@ const changeFontSize = (
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update arrow elements after text elements have been updated
|
// Update arrow elements after text elements have been updated
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
|
||||||
getSelectedElements(elements, appState, {
|
getSelectedElements(elements, appState, {
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
}).forEach((element) => {
|
}).forEach((element) => {
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
updateBoundElements(
|
updateBoundElements(element, app.scene);
|
||||||
element,
|
|
||||||
updatedElementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -778,7 +774,7 @@ type ChangeFontFamilyData = Partial<
|
||||||
>
|
>
|
||||||
> & {
|
> & {
|
||||||
/** cache of selected & editing elements populated on opened popup */
|
/** cache of selected & editing elements populated on opened popup */
|
||||||
cachedElements?: Map<string, ExcalidrawElement>;
|
cachedElements?: ElementsMap;
|
||||||
/** flag to reset all elements to their cached versions */
|
/** flag to reset all elements to their cached versions */
|
||||||
resetAll?: true;
|
resetAll?: true;
|
||||||
/** flag to reset all containers to their cached versions */
|
/** flag to reset all containers to their cached versions */
|
||||||
|
@ -919,7 +915,7 @@ export const actionChangeFontFamily = register({
|
||||||
|
|
||||||
if (resetContainers && container && cachedContainer) {
|
if (resetContainers && container && cachedContainer) {
|
||||||
// reset the container back to it's cached version
|
// reset the container back to it's cached version
|
||||||
mutateElement(container, { ...cachedContainer }, false);
|
app.scene.mutateElement(container, { ...cachedContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipFontFaceCheck) {
|
if (!skipFontFaceCheck) {
|
||||||
|
@ -950,12 +946,7 @@ export const actionChangeFontFamily = register({
|
||||||
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
||||||
for (const [element, container] of elementContainerMapping) {
|
for (const [element, container] of elementContainerMapping) {
|
||||||
// trigger synchronous redraw
|
// trigger synchronous redraw
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(element, container, app.scene);
|
||||||
element,
|
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
|
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
|
||||||
|
@ -972,8 +963,7 @@ export const actionChangeFontFamily = register({
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
latestElement as ExcalidrawTextElement,
|
latestElement as ExcalidrawTextElement,
|
||||||
latestContainer,
|
latestContainer,
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -987,7 +977,7 @@ export const actionChangeFontFamily = register({
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, app, updateData }) => {
|
PanelComponent: ({ elements, appState, app, updateData }) => {
|
||||||
const cachedElementsRef = useRef<Map<string, ExcalidrawElement>>(new Map());
|
const cachedElementsRef = useRef<ElementsMap>(new Map());
|
||||||
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
||||||
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
|
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
|
||||||
const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
|
const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
|
||||||
|
@ -996,7 +986,7 @@ export const actionChangeFontFamily = register({
|
||||||
const selectedFontFamily = useMemo(() => {
|
const selectedFontFamily = useMemo(() => {
|
||||||
const getFontFamily = (
|
const getFontFamily = (
|
||||||
elementsArray: readonly ExcalidrawElement[],
|
elementsArray: readonly ExcalidrawElement[],
|
||||||
elementsMap: Map<string, ExcalidrawElement>,
|
elementsMap: ElementsMap,
|
||||||
) =>
|
) =>
|
||||||
getFormValue(
|
getFormValue(
|
||||||
elementsArray,
|
elementsArray,
|
||||||
|
@ -1179,7 +1169,7 @@ export const actionChangeTextAlign = register({
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
newElement,
|
newElement,
|
||||||
app.scene.getContainerElement(oldElement),
|
app.scene.getContainerElement(oldElement),
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
@ -1270,7 +1260,7 @@ export const actionChangeVerticalAlign = register({
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
newElement,
|
newElement,
|
||||||
app.scene.getContainerElement(oldElement),
|
app.scene.getContainerElement(oldElement),
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
@ -1670,10 +1660,10 @@ export const actionChangeArrowType = register({
|
||||||
newElement,
|
newElement,
|
||||||
startHoveredElement,
|
startHoveredElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
app.scene,
|
||||||
);
|
);
|
||||||
endHoveredElement &&
|
endHoveredElement &&
|
||||||
bindLinearElement(newElement, endHoveredElement, "end", elementsMap);
|
bindLinearElement(newElement, endHoveredElement, "end", app.scene);
|
||||||
|
|
||||||
const startBinding =
|
const startBinding =
|
||||||
startElement && newElement.startBinding
|
startElement && newElement.startBinding
|
||||||
|
@ -1684,7 +1674,6 @@ export const actionChangeArrowType = register({
|
||||||
newElement,
|
newElement,
|
||||||
startElement,
|
startElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
@ -1697,7 +1686,6 @@ export const actionChangeArrowType = register({
|
||||||
newElement,
|
newElement,
|
||||||
endElement,
|
endElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
@ -1729,7 +1717,7 @@ export const actionChangeArrowType = register({
|
||||||
newElement.startBinding.elementId,
|
newElement.startBinding.elementId,
|
||||||
) as ExcalidrawBindableElement;
|
) as ExcalidrawBindableElement;
|
||||||
if (startElement) {
|
if (startElement) {
|
||||||
bindLinearElement(newElement, startElement, "start", elementsMap);
|
bindLinearElement(newElement, startElement, "start", app.scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newElement.endBinding) {
|
if (newElement.endBinding) {
|
||||||
|
@ -1737,7 +1725,7 @@ export const actionChangeArrowType = register({
|
||||||
newElement.endBinding.elementId,
|
newElement.endBinding.elementId,
|
||||||
) as ExcalidrawBindableElement;
|
) as ExcalidrawBindableElement;
|
||||||
if (endElement) {
|
if (endElement) {
|
||||||
bindLinearElement(newElement, endElement, "end", elementsMap);
|
bindLinearElement(newElement, endElement, "end", app.scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1758,6 +1746,7 @@ export const actionChangeArrowType = register({
|
||||||
if (selected) {
|
if (selected) {
|
||||||
newState.selectedLinearElement = new LinearElementEditor(
|
newState.selectedLinearElement = new LinearElementEditor(
|
||||||
selected as ExcalidrawLinearElement,
|
selected as ExcalidrawLinearElement,
|
||||||
|
arrayToMap(elements),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks";
|
import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { KEYS } from "@excalidraw/common";
|
import { arrayToMap, KEYS } from "@excalidraw/common";
|
||||||
|
|
||||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ export const actionSelectAll = register({
|
||||||
// single linear element selected
|
// single linear element selected
|
||||||
Object.keys(selectedElementIds).length === 1 &&
|
Object.keys(selectedElementIds).length === 1 &&
|
||||||
isLinearElement(elements[0])
|
isLinearElement(elements[0])
|
||||||
? new LinearElementEditor(elements[0])
|
? new LinearElementEditor(elements[0], arrayToMap(elements))
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
|
|
@ -139,11 +139,8 @@ export const actionPasteStyles = register({
|
||||||
element.id === newElement.containerId,
|
element.id === newElement.containerId,
|
||||||
) || null;
|
) || null;
|
||||||
}
|
}
|
||||||
redrawTextBoundingBox(
|
|
||||||
newElement,
|
redrawTextBoundingBox(newElement, container, app.scene);
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
34
packages/excalidraw/actions/actionToggleShapeSwitch.tsx
Normal file
34
packages/excalidraw/actions/actionToggleShapeSwitch.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getConversionTypeFromElements,
|
||||||
|
convertElementTypePopupAtom,
|
||||||
|
} from "../components/ConvertElementTypePopup";
|
||||||
|
import { editorJotaiStore } from "../editor-jotai";
|
||||||
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionToggleShapeSwitch = register({
|
||||||
|
name: "toggleShapeSwitch",
|
||||||
|
label: "labels.shapeSwitch",
|
||||||
|
icon: () => null,
|
||||||
|
viewMode: true,
|
||||||
|
trackEvent: {
|
||||||
|
category: "shape_switch",
|
||||||
|
action: "toggle",
|
||||||
|
},
|
||||||
|
keywords: ["change", "switch", "swap"],
|
||||||
|
perform(elements, appState, _, app) {
|
||||||
|
editorJotaiStore.set(convertElementTypePopupAtom, {
|
||||||
|
type: "panel",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
checked: (appState) => appState.gridModeEnabled,
|
||||||
|
predicate: (elements, appState, props) =>
|
||||||
|
getConversionTypeFromElements(elements as ExcalidrawElement[]) !== null,
|
||||||
|
});
|
|
@ -140,7 +140,8 @@ export type ActionName =
|
||||||
| "linkToElement"
|
| "linkToElement"
|
||||||
| "cropEditor"
|
| "cropEditor"
|
||||||
| "wrapSelectionInFrame"
|
| "wrapSelectionInFrame"
|
||||||
| "toggleLassoTool";
|
| "toggleLassoTool"
|
||||||
|
| "toggleShapeSwitch";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
@ -195,7 +196,8 @@ export interface Action {
|
||||||
| "menu"
|
| "menu"
|
||||||
| "collab"
|
| "collab"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "search_menu";
|
| "search_menu"
|
||||||
|
| "shape_switch";
|
||||||
action?: string;
|
action?: string;
|
||||||
predicate?: (
|
predicate?: (
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
|
|
@ -129,7 +129,6 @@ export class AnimatedTrail implements Trail {
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.pastTrails = [];
|
|
||||||
this.start();
|
this.start();
|
||||||
if (this.trailAnimation) {
|
if (this.trailAnimation) {
|
||||||
this.trailAnimation.setAttribute("begin", "indefinite");
|
this.trailAnimation.setAttribute("begin", "indefinite");
|
||||||
|
|
|
@ -37,6 +37,8 @@ import {
|
||||||
syncMovedIndices,
|
syncMovedIndices,
|
||||||
} from "@excalidraw/element/fractionalIndex";
|
} from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
|
import Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
|
import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
|
||||||
|
|
||||||
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
||||||
|
@ -490,6 +492,7 @@ export class AppStateChange implements Change<AppState> {
|
||||||
nextElements.get(
|
nextElements.get(
|
||||||
selectedLinearElementId,
|
selectedLinearElementId,
|
||||||
) as NonDeleted<ExcalidrawLinearElement>,
|
) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
nextElements,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -499,6 +502,7 @@ export class AppStateChange implements Change<AppState> {
|
||||||
nextElements.get(
|
nextElements.get(
|
||||||
editingLinearElementId,
|
editingLinearElementId,
|
||||||
) as NonDeleted<ExcalidrawLinearElement>,
|
) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
nextElements,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -1132,9 +1136,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
|
|
||||||
ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements);
|
|
||||||
|
|
||||||
// the following reorder performs also mutations, but only on new instances of changed elements
|
// the following reorder performs also mutations, but only on new instances of changed elements
|
||||||
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
||||||
nextElements = ElementsChange.reorderElements(
|
nextElements = ElementsChange.reorderElements(
|
||||||
|
@ -1143,8 +1144,14 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
flags,
|
flags,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
||||||
|
// we also don't have a scene on the server
|
||||||
|
// so we are creating a temp scene just to query and mutate elements
|
||||||
|
const tempScene = new Scene(nextElements);
|
||||||
|
|
||||||
|
ElementsChange.redrawTextBoundingBoxes(tempScene, changedElements);
|
||||||
// Need ordered nextElements to avoid z-index binding issues
|
// Need ordered nextElements to avoid z-index binding issues
|
||||||
ElementsChange.redrawBoundArrows(nextElements, changedElements);
|
ElementsChange.redrawBoundArrows(tempScene, changedElements);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Couldn't mutate elements after applying elements change`,
|
`Couldn't mutate elements after applying elements change`,
|
||||||
|
@ -1337,8 +1344,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
} else {
|
} else {
|
||||||
affectedElement = mutateElement(
|
affectedElement = mutateElement(
|
||||||
nextElement,
|
nextElement,
|
||||||
|
nextElements,
|
||||||
updates as ElementUpdate<OrderedExcalidrawElement>,
|
updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||||
);
|
) as OrderedExcalidrawElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAffectedElements.set(affectedElement.id, affectedElement);
|
nextAffectedElements.set(affectedElement.id, affectedElement);
|
||||||
|
@ -1456,9 +1464,10 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static redrawTextBoundingBoxes(
|
private static redrawTextBoundingBoxes(
|
||||||
elements: SceneElementsMap,
|
scene: Scene,
|
||||||
changed: Map<string, OrderedExcalidrawElement>,
|
changed: Map<string, OrderedExcalidrawElement>,
|
||||||
) {
|
) {
|
||||||
|
const elements = scene.getNonDeletedElementsMap();
|
||||||
const boxesToRedraw = new Map<
|
const boxesToRedraw = new Map<
|
||||||
string,
|
string,
|
||||||
{ container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
|
{ container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
|
||||||
|
@ -1498,17 +1507,17 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
redrawTextBoundingBox(boundText, container, elements, false);
|
redrawTextBoundingBox(boundText, container, scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static redrawBoundArrows(
|
private static redrawBoundArrows(
|
||||||
elements: SceneElementsMap,
|
scene: Scene,
|
||||||
changed: Map<string, OrderedExcalidrawElement>,
|
changed: Map<string, OrderedExcalidrawElement>,
|
||||||
) {
|
) {
|
||||||
for (const element of changed.values()) {
|
for (const element of changed.values()) {
|
||||||
if (!element.isDeleted && isBindableElement(element)) {
|
if (!element.isDeleted && isBindableElement(element)) {
|
||||||
updateBoundElements(element, elements, {
|
updateBoundElements(element, scene, {
|
||||||
changedElements: changed,
|
changedElements: changed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,7 +172,7 @@ export const serializeAsClipboardJSON = ({
|
||||||
!framesToCopy.has(getContainingFrame(element, elementsMap)!)
|
!framesToCopy.has(getContainingFrame(element, elementsMap)!)
|
||||||
) {
|
) {
|
||||||
const copiedElement = deepCopyElement(element);
|
const copiedElement = deepCopyElement(element);
|
||||||
mutateElement(copiedElement, {
|
mutateElement(copiedElement, elementsMap, {
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
return copiedElement;
|
return copiedElement;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,8 @@ import {
|
||||||
isWritableElement,
|
isWritableElement,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
|
||||||
|
|
||||||
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -410,6 +412,14 @@ function CommandPaletteInner({
|
||||||
actionManager.executeAction(actionToggleSearchMenu);
|
actionManager.executeAction(actionToggleSearchMenu);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("labels.shapeSwitch"),
|
||||||
|
category: DEFAULT_CATEGORIES.elements,
|
||||||
|
icon: boltIcon,
|
||||||
|
perform: () => {
|
||||||
|
actionManager.executeAction(actionToggleShapeSwitch);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t("labels.changeStroke"),
|
label: t("labels.changeStroke"),
|
||||||
keywords: ["color", "outline"],
|
keywords: ["color", "outline"],
|
||||||
|
|
18
packages/excalidraw/components/ConvertElementTypePopup.scss
Normal file
18
packages/excalidraw/components/ConvertElementTypePopup.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
@import "../css//variables.module.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.ConvertElementTypePopup {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--island-bg-color);
|
||||||
|
box-shadow: var(--shadow-island);
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1047
packages/excalidraw/components/ConvertElementTypePopup.tsx
Normal file
1047
packages/excalidraw/components/ConvertElementTypePopup.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
@ -6,9 +6,10 @@ import {
|
||||||
defaultGetElementLinkFromSelection,
|
defaultGetElementLinkFromSelection,
|
||||||
getLinkIdAndTypeFromSelection,
|
getLinkIdAndTypeFromSelection,
|
||||||
} from "@excalidraw/element/elementLink";
|
} from "@excalidraw/element/elementLink";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
@ -21,20 +22,20 @@ import { TrashIcon } from "./icons";
|
||||||
import "./ElementLinkDialog.scss";
|
import "./ElementLinkDialog.scss";
|
||||||
|
|
||||||
import type { AppProps, AppState, UIAppState } from "../types";
|
import type { AppProps, AppState, UIAppState } from "../types";
|
||||||
|
|
||||||
const ElementLinkDialog = ({
|
const ElementLinkDialog = ({
|
||||||
sourceElementId,
|
sourceElementId,
|
||||||
onClose,
|
onClose,
|
||||||
elementsMap,
|
|
||||||
appState,
|
appState,
|
||||||
|
scene,
|
||||||
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
||||||
}: {
|
}: {
|
||||||
sourceElementId: ExcalidrawElement["id"];
|
sourceElementId: ExcalidrawElement["id"];
|
||||||
elementsMap: ElementsMap;
|
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
|
scene: Scene;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
||||||
}) => {
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
||||||
|
|
||||||
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
||||||
|
@ -70,7 +71,7 @@ const ElementLinkDialog = ({
|
||||||
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
|
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
|
||||||
const elementToLink = elementsMap.get(sourceElementId);
|
const elementToLink = elementsMap.get(sourceElementId);
|
||||||
elementToLink &&
|
elementToLink &&
|
||||||
mutateElement(elementToLink, {
|
scene.mutateElement(elementToLink, {
|
||||||
link: nextLink,
|
link: nextLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -78,13 +79,13 @@ const ElementLinkDialog = ({
|
||||||
if (!nextLink && linkEdited && sourceElementId) {
|
if (!nextLink && linkEdited && sourceElementId) {
|
||||||
const elementToLink = elementsMap.get(sourceElementId);
|
const elementToLink = elementsMap.get(sourceElementId);
|
||||||
elementToLink &&
|
elementToLink &&
|
||||||
mutateElement(elementToLink, {
|
scene.mutateElement(elementToLink, {
|
||||||
link: null,
|
link: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
|
}, [sourceElementId, nextLink, elementsMap, linkEdited, scene, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|
|
@ -21,7 +21,7 @@ const Header = () => (
|
||||||
className="HelpDialog__btn"
|
className="HelpDialog__btn"
|
||||||
href="https://docs.excalidraw.com"
|
href="https://docs.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
{t("helpDialog.documentation")}
|
{t("helpDialog.documentation")}
|
||||||
|
@ -30,7 +30,7 @@ const Header = () => (
|
||||||
className="HelpDialog__btn"
|
className="HelpDialog__btn"
|
||||||
href="https://plus.excalidraw.com/blog"
|
href="https://plus.excalidraw.com/blog"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
{t("helpDialog.blog")}
|
{t("helpDialog.blog")}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
CLASSES,
|
CLASSES,
|
||||||
DEFAULT_SIDEBAR,
|
DEFAULT_SIDEBAR,
|
||||||
TOOL_TYPE,
|
TOOL_TYPE,
|
||||||
|
arrayToMap,
|
||||||
capitalizeString,
|
capitalizeString,
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
@ -17,7 +18,6 @@ import { ShapeCache } from "@excalidraw/element/ShapeCache";
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { actionToggleStats } from "../actions";
|
import { actionToggleStats } from "../actions";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { isHandToolActive } from "../appState";
|
import { isHandToolActive } from "../appState";
|
||||||
|
@ -446,22 +446,18 @@ const LayerUI = ({
|
||||||
|
|
||||||
if (selectedElements.length) {
|
if (selectedElements.length) {
|
||||||
for (const element of selectedElements) {
|
for (const element of selectedElements) {
|
||||||
mutateElement(
|
mutateElement(element, arrayToMap(elements), {
|
||||||
element,
|
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||||
{
|
? colorPickerType === "elementBackground"
|
||||||
[altKey && eyeDropperState.swapPreviewOnAlt
|
? "strokeColor"
|
||||||
? colorPickerType === "elementBackground"
|
: "backgroundColor"
|
||||||
? "strokeColor"
|
: colorPickerType === "elementBackground"
|
||||||
: "backgroundColor"
|
? "backgroundColor"
|
||||||
: colorPickerType === "elementBackground"
|
: "strokeColor"]: color,
|
||||||
? "backgroundColor"
|
});
|
||||||
: "strokeColor"]: color,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
Scene.getScene(selectedElements[0])?.triggerUpdate();
|
app.scene.triggerUpdate();
|
||||||
} else if (colorPickerType === "elementBackground") {
|
} else if (colorPickerType === "elementBackground") {
|
||||||
setAppState({
|
setAppState({
|
||||||
currentItemBackgroundColor: color,
|
currentItemBackgroundColor: color,
|
||||||
|
@ -494,7 +490,7 @@ const LayerUI = ({
|
||||||
openDialog: null,
|
openDialog: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
scene={app.scene}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
generateLinkForSelection={generateLinkForSelection}
|
generateLinkForSelection={generateLinkForSelection}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -166,7 +166,7 @@ export default function LibraryMenuItems({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: item.elements,
|
elements: item.elements,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
}).newElements,
|
}).duplicatedElements,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -389,7 +389,7 @@ const PublishLibrary = ({
|
||||||
<a
|
<a
|
||||||
href="https://libraries.excalidraw.com"
|
href="https://libraries.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
{el}
|
{el}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
@ -9,13 +7,16 @@ import type { Degrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
|
|
||||||
|
import { updateBindings } from "../../../element/src/binding";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface AngleProps {
|
interface AngleProps {
|
||||||
|
@ -35,7 +36,6 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
scene,
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement && !isElbowArrow(origElement)) {
|
if (origElement && !isElbowArrow(origElement)) {
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
|
@ -45,14 +45,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextAngle = degreesToRadians(nextValue as Degrees);
|
const nextAngle = degreesToRadians(nextValue as Degrees);
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
updateBindings(latestElement, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -71,14 +71,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
|
|
||||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||||
|
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
updateBindings(latestElement, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { getNormalizedGridStep } from "../../scene";
|
import { getNormalizedGridStep } from "../../scene";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface PositionProps {
|
interface PositionProps {
|
||||||
|
|
|
@ -5,17 +5,17 @@ import {
|
||||||
MINIMAL_CROP_SIZE,
|
MINIMAL_CROP_SIZE,
|
||||||
getUncroppedWidthAndHeight,
|
getUncroppedWidthAndHeight,
|
||||||
} from "@excalidraw/element/cropElement";
|
} from "@excalidraw/element/cropElement";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import { resizeSingleElement } from "@excalidraw/element/resizeElements";
|
import { resizeSingleElement } from "@excalidraw/element/resizeElements";
|
||||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface DimensionDragInputProps {
|
interface DimensionDragInputProps {
|
||||||
|
@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||||
|
@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
height: nextCropHeight,
|
height: nextCropHeight,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||||
|
@ -176,8 +176,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldMaintainAspectRatio: keepAspectRatio,
|
shouldMaintainAspectRatio: keepAspectRatio,
|
||||||
|
@ -223,8 +223,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldMaintainAspectRatio: keepAspectRatio,
|
shouldMaintainAspectRatio: keepAspectRatio,
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "../../store";
|
import { CaptureUpdateAction } from "../../store";
|
||||||
import { useApp } from "../App";
|
import { useApp } from "../App";
|
||||||
import { InlineIcon } from "../InlineIcon";
|
import { InlineIcon } from "../InlineIcon";
|
||||||
|
@ -16,7 +18,6 @@ import { SMALLEST_DELTA } from "./utils";
|
||||||
import "./DragInput.scss";
|
import "./DragInput.scss";
|
||||||
|
|
||||||
import type { StatsInputProperty } from "./utils";
|
import type { StatsInputProperty } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
export type DragInputCallbackType<
|
export type DragInputCallbackType<
|
||||||
|
@ -216,13 +217,12 @@ const StatsDragInput = <
|
||||||
y: number;
|
y: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
let originalElementsMap: ElementsMap | null = app.scene
|
||||||
app.scene
|
.getNonDeletedElements()
|
||||||
.getNonDeletedElements()
|
.reduce((acc: ElementsMap, element) => {
|
||||||
.reduce((acc: ElementsMap, element) => {
|
acc.set(element.id, deepCopyElement(element));
|
||||||
acc.set(element.id, deepCopyElement(element));
|
return acc;
|
||||||
return acc;
|
}, new Map());
|
||||||
}, new Map());
|
|
||||||
|
|
||||||
let originalElements: readonly E[] | null = elements.map(
|
let originalElements: readonly E[] | null = elements.map(
|
||||||
(element) => originalElementsMap!.get(element.id) as E,
|
(element) => originalElementsMap!.get(element.id) as E,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
|
@ -13,13 +12,14 @@ import type {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { fontSizeIcon } from "../icons";
|
import { fontSizeIcon } from "../icons";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface FontSizeProps {
|
interface FontSizeProps {
|
||||||
|
@ -68,13 +68,13 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextFontSize) {
|
if (nextFontSize) {
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
});
|
});
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
latestElement,
|
latestElement,
|
||||||
scene.getContainerElement(latestElement),
|
scene.getContainerElement(latestElement),
|
||||||
scene.getNonDeletedElementsMap(),
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import { isArrowElement } from "@excalidraw/element/typeChecks";
|
import { isArrowElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
@ -11,13 +9,14 @@ import type { Degrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiAngleProps {
|
interface MultiAngleProps {
|
||||||
|
@ -54,17 +53,13 @@ const handleDegreeChange: DragInputCallbackType<
|
||||||
if (!element) {
|
if (!element) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
mutateElement(
|
scene.mutateElement(element, {
|
||||||
element,
|
angle: nextAngle,
|
||||||
{
|
});
|
||||||
angle: nextAngle,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(element)) {
|
if (boundTextElement && !isArrowElement(element)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,17 +87,13 @@ const handleDegreeChange: DragInputCallbackType<
|
||||||
|
|
||||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||||
|
|
||||||
mutateElement(
|
scene.mutateElement(latestElement, {
|
||||||
latestElement,
|
angle: nextAngle,
|
||||||
{
|
});
|
||||||
angle: nextAngle,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useMemo } from "react";
|
||||||
|
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
|
import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
|
||||||
import { updateBoundElements } from "@excalidraw/element/binding";
|
import { updateBoundElements } from "@excalidraw/element/binding";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
rescalePointsInElement,
|
rescalePointsInElement,
|
||||||
resizeSingleElement,
|
resizeSingleElement,
|
||||||
|
@ -23,13 +22,14 @@ import type {
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
import { getElementsInAtomicUnit } from "./utils";
|
import { getElementsInAtomicUnit } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiDimensionProps {
|
interface MultiDimensionProps {
|
||||||
|
@ -75,33 +75,31 @@ const resizeElementInGroup = (
|
||||||
scale: number,
|
scale: number,
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||||
|
|
||||||
mutateElement(latestElement, updates, false);
|
scene.mutateElement(latestElement, updates);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
origElement,
|
origElement,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const newFontSize = boundTextElement.fontSize * scale;
|
const newFontSize = boundTextElement.fontSize * scale;
|
||||||
updateBoundElements(latestElement, elementsMap, {
|
updateBoundElements(latestElement, scene, {
|
||||||
newSize: { width: updates.width, height: updates.height },
|
newSize: { width: updates.width, height: updates.height },
|
||||||
});
|
});
|
||||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||||
mutateElement(
|
scene.mutateElement(latestBoundTextElement, {
|
||||||
latestBoundTextElement,
|
fontSize: newFontSize,
|
||||||
{
|
});
|
||||||
fontSize: newFontSize,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
handleBindTextResize(
|
handleBindTextResize(
|
||||||
latestElement,
|
latestElement,
|
||||||
elementsMap,
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
@ -118,8 +116,8 @@ const resizeGroup = (
|
||||||
property: MultiDimensionProps["property"],
|
property: MultiDimensionProps["property"],
|
||||||
latestElements: ExcalidrawElement[],
|
latestElements: ExcalidrawElement[],
|
||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
// keep aspect ratio for groups
|
// keep aspect ratio for groups
|
||||||
if (property === "width") {
|
if (property === "width") {
|
||||||
|
@ -141,8 +139,8 @@ const resizeGroup = (
|
||||||
scale,
|
scale,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -194,8 +192,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [el] = elementsInUnit;
|
const [el] = elementsInUnit;
|
||||||
|
@ -237,8 +235,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldInformMutation: false,
|
shouldInformMutation: false,
|
||||||
|
@ -301,8 +299,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [el] = elementsInUnit;
|
const [el] = elementsInUnit;
|
||||||
|
@ -340,8 +338,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldInformMutation: false,
|
shouldInformMutation: false,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
|
@ -16,13 +15,14 @@ import type {
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { fontSizeIcon } from "../icons";
|
import { fontSizeIcon } from "../icons";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiFontSizeProps {
|
interface MultiFontSizeProps {
|
||||||
|
@ -84,19 +84,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||||
|
|
||||||
for (const textElement of latestTextElements) {
|
for (const textElement of latestTextElements) {
|
||||||
mutateElement(
|
scene.mutateElement(textElement, {
|
||||||
textElement,
|
fontSize: nextFontSize,
|
||||||
{
|
});
|
||||||
fontSize: nextFontSize,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
textElement,
|
textElement,
|
||||||
scene.getContainerElement(textElement),
|
scene.getContainerElement(textElement),
|
||||||
elementsMap,
|
scene,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,19 +112,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
||||||
}
|
}
|
||||||
mutateElement(
|
scene.mutateElement(latestElement, {
|
||||||
latestElement,
|
fontSize: nextFontSize,
|
||||||
{
|
});
|
||||||
fontSize: nextFontSize,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
latestElement,
|
latestElement,
|
||||||
scene.getContainerElement(latestElement),
|
scene.getContainerElement(latestElement),
|
||||||
elementsMap,
|
scene,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,9 @@ import { isTextElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||||
|
|
||||||
import type {
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawElement,
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
@ -18,7 +15,6 @@ import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiPositionProps {
|
interface MultiPositionProps {
|
||||||
|
@ -36,13 +32,11 @@ const moveElements = (
|
||||||
property: MultiPositionProps["property"],
|
property: MultiPositionProps["property"],
|
||||||
changeInTopX: number,
|
changeInTopX: number,
|
||||||
changeInTopY: number,
|
changeInTopY: number,
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
originalElements: readonly ExcalidrawElement[],
|
originalElements: readonly ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < originalElements.length; i++) {
|
||||||
const origElement = originalElements[i];
|
const origElement = originalElements[i];
|
||||||
|
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
|
@ -65,8 +59,6 @@ const moveElements = (
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
|
@ -78,11 +70,10 @@ const moveGroupTo = (
|
||||||
nextX: number,
|
nextX: number,
|
||||||
nextY: number,
|
nextY: number,
|
||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
const [x1, y1, ,] = getCommonBounds(originalElements);
|
||||||
const offsetX = nextX - x1;
|
const offsetX = nextX - x1;
|
||||||
const offsetY = nextY - y1;
|
const offsetY = nextY - y1;
|
||||||
|
@ -112,8 +103,6 @@ const moveGroupTo = (
|
||||||
topLeftX + offsetX,
|
topLeftX + offsetX,
|
||||||
topLeftY + offsetY,
|
topLeftY + offsetY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
|
@ -135,7 +124,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
originalAppState,
|
originalAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
for (const atomicUnit of getAtomicUnits(
|
for (const atomicUnit of getAtomicUnits(
|
||||||
|
@ -159,8 +147,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
elementsInUnit.map((el) => el.original),
|
elementsInUnit.map((el) => el.original),
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
|
@ -188,8 +174,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
|
@ -214,8 +198,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
changeInTopX,
|
changeInTopX,
|
||||||
changeInTopY,
|
changeInTopY,
|
||||||
originalElements,
|
originalElements,
|
||||||
originalElements,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,16 +4,16 @@ import {
|
||||||
getFlipAdjustedCropPosition,
|
getFlipAdjustedCropPosition,
|
||||||
getUncroppedWidthAndHeight,
|
getUncroppedWidthAndHeight,
|
||||||
} from "@excalidraw/element/cropElement";
|
} from "@excalidraw/element/cropElement";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue, moveElement } from "./utils";
|
import { getStepSizedValue, moveElement } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface PositionProps {
|
interface PositionProps {
|
||||||
|
@ -38,7 +38,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
originalAppState,
|
originalAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
|
@ -101,7 +100,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -119,7 +118,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
|
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -133,8 +132,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
|
@ -166,8 +163,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,7 @@ import type {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { Excalidraw, getCommonBounds, mutateElement } from "../..";
|
import { Excalidraw, getCommonBounds } from "../..";
|
||||||
import { actionGroup } from "../../actions";
|
import { actionGroup } from "../../actions";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import * as StaticScene from "../../renderer/staticScene";
|
import * as StaticScene from "../../renderer/staticScene";
|
||||||
|
@ -478,7 +478,7 @@ describe("stats for a non-generic element", () => {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [{ type: "text", id: text.id }],
|
boundElements: [{ type: "text", id: text.id }],
|
||||||
});
|
});
|
||||||
API.setElements([container, text]);
|
API.setElements([container, text]);
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
|
||||||
bindOrUnbindLinearElements,
|
|
||||||
updateBoundElements,
|
|
||||||
} from "@excalidraw/element/binding";
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import {
|
import {
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
@ -24,10 +18,12 @@ import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
|
import { updateBindings } from "../../../element/src/binding";
|
||||||
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
export type StatsInputProperty =
|
export type StatsInputProperty =
|
||||||
|
@ -119,12 +115,11 @@ export const moveElement = (
|
||||||
newTopLeftX: number,
|
newTopLeftX: number,
|
||||||
newTopLeftY: number,
|
newTopLeftY: number,
|
||||||
originalElement: ExcalidrawElement,
|
originalElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const latestElement = elementsMap.get(originalElement.id);
|
const latestElement = elementsMap.get(originalElement.id);
|
||||||
if (!latestElement) {
|
if (!latestElement) {
|
||||||
return;
|
return;
|
||||||
|
@ -148,15 +143,15 @@ export const moveElement = (
|
||||||
-originalElement.angle as Radians,
|
-originalElement.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
latestElement,
|
latestElement,
|
||||||
{
|
{
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
},
|
},
|
||||||
shouldInformMutation,
|
{ informMutation: shouldInformMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
updateBindings(latestElement, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
originalElement,
|
originalElement,
|
||||||
|
@ -165,13 +160,13 @@ export const moveElement = (
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||||
latestBoundTextElement &&
|
latestBoundTextElement &&
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
latestBoundTextElement,
|
latestBoundTextElement,
|
||||||
{
|
{
|
||||||
x: boundTextElement.x + changeInX,
|
x: boundTextElement.x + changeInX,
|
||||||
y: boundTextElement.y + changeInY,
|
y: boundTextElement.y + changeInY,
|
||||||
},
|
},
|
||||||
shouldInformMutation,
|
{ informMutation: shouldInformMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -196,29 +191,3 @@ export const getAtomicUnits = (
|
||||||
});
|
});
|
||||||
return _atomicUnits;
|
return _atomicUnits;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateBindings = (
|
|
||||||
latestElement: ExcalidrawElement,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
|
||||||
options?: {
|
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
|
||||||
newSize?: { width: number; height: number };
|
|
||||||
zoom?: AppState["zoom"];
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
if (isLinearElement(latestElement)) {
|
|
||||||
bindOrUnbindLinearElements(
|
|
||||||
[latestElement],
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
true,
|
|
||||||
[],
|
|
||||||
options?.zoom,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updateBoundElements(latestElement, elementsMap, options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ type InteractiveCanvasProps = {
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: InteractiveCanvasAppState;
|
appState: InteractiveCanvasAppState;
|
||||||
|
renderScrollbars: boolean;
|
||||||
device: Device;
|
device: Device;
|
||||||
renderInteractiveSceneCallback: (
|
renderInteractiveSceneCallback: (
|
||||||
data: RenderInteractiveSceneCallback,
|
data: RenderInteractiveSceneCallback,
|
||||||
|
@ -143,7 +144,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||||
remotePointerUsernames,
|
remotePointerUsernames,
|
||||||
remotePointerUserStates,
|
remotePointerUserStates,
|
||||||
selectionColor,
|
selectionColor,
|
||||||
renderScrollbars: false,
|
renderScrollbars: props.renderScrollbars,
|
||||||
},
|
},
|
||||||
device: props.device,
|
device: props.device,
|
||||||
callback: props.renderInteractiveSceneCallback,
|
callback: props.renderInteractiveSceneCallback,
|
||||||
|
@ -230,7 +231,8 @@ const areEqual = (
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||||
prevProps.selectedElements !== nextProps.selectedElements
|
prevProps.selectedElements !== nextProps.selectedElements ||
|
||||||
|
prevProps.renderScrollbars !== nextProps.renderScrollbars
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ const DropdownMenuItemLink = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
className = "",
|
className = "",
|
||||||
selected,
|
selected,
|
||||||
rel = "noreferrer",
|
rel = "noopener",
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
href: string;
|
href: string;
|
||||||
|
@ -31,11 +31,12 @@ const DropdownMenuItemLink = ({
|
||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line react/jsx-no-target-blank
|
||||||
<a
|
<a
|
||||||
{...rest}
|
{...rest}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel={rel || "noopener"}
|
||||||
className={getDropdownMenuItemClassName(className, selected)}
|
className={getDropdownMenuItemClassName(className, selected)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|
|
@ -21,8 +21,6 @@ import {
|
||||||
embeddableURLValidator,
|
embeddableURLValidator,
|
||||||
} from "@excalidraw/element/embeddable";
|
} from "@excalidraw/element/embeddable";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
|
@ -33,6 +31,8 @@ import {
|
||||||
|
|
||||||
import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawEmbeddableElement,
|
ExcalidrawEmbeddableElement,
|
||||||
|
@ -70,14 +70,14 @@ const embeddableLinkCache = new Map<
|
||||||
|
|
||||||
export const Hyperlink = ({
|
export const Hyperlink = ({
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
scene,
|
||||||
setAppState,
|
setAppState,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
setToast,
|
setToast,
|
||||||
updateEmbedValidationStatus,
|
updateEmbedValidationStatus,
|
||||||
}: {
|
}: {
|
||||||
element: NonDeletedExcalidrawElement;
|
element: NonDeletedExcalidrawElement;
|
||||||
elementsMap: ElementsMap;
|
scene: Scene;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||||
setToast: (
|
setToast: (
|
||||||
|
@ -88,6 +88,7 @@ export const Hyperlink = ({
|
||||||
status: boolean,
|
status: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const appProps = useAppProps();
|
const appProps = useAppProps();
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
|
@ -114,7 +115,7 @@ export const Hyperlink = ({
|
||||||
setAppState({ activeEmbeddable: null });
|
setAppState({ activeEmbeddable: null });
|
||||||
}
|
}
|
||||||
if (!link) {
|
if (!link) {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
link: null,
|
link: null,
|
||||||
});
|
});
|
||||||
updateEmbedValidationStatus(element, false);
|
updateEmbedValidationStatus(element, false);
|
||||||
|
@ -126,7 +127,7 @@ export const Hyperlink = ({
|
||||||
setToast({ message: t("toast.unableToEmbed"), closable: true });
|
setToast({ message: t("toast.unableToEmbed"), closable: true });
|
||||||
}
|
}
|
||||||
element.link && embeddableLinkCache.set(element.id, element.link);
|
element.link && embeddableLinkCache.set(element.id, element.link);
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
link,
|
link,
|
||||||
});
|
});
|
||||||
updateEmbedValidationStatus(element, false);
|
updateEmbedValidationStatus(element, false);
|
||||||
|
@ -144,7 +145,7 @@ export const Hyperlink = ({
|
||||||
: 1;
|
: 1;
|
||||||
const hasLinkChanged =
|
const hasLinkChanged =
|
||||||
embeddableLinkCache.get(element.id) !== element.link;
|
embeddableLinkCache.get(element.id) !== element.link;
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
...(hasLinkChanged
|
...(hasLinkChanged
|
||||||
? {
|
? {
|
||||||
width:
|
width:
|
||||||
|
@ -169,10 +170,11 @@ export const Hyperlink = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mutateElement(element, { link });
|
scene.mutateElement(element, { link });
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
element,
|
element,
|
||||||
|
scene,
|
||||||
setToast,
|
setToast,
|
||||||
appProps.validateEmbeddable,
|
appProps.validateEmbeddable,
|
||||||
appState.activeEmbeddable,
|
appState.activeEmbeddable,
|
||||||
|
@ -229,9 +231,9 @@ export const Hyperlink = ({
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
const handleRemove = useCallback(() => {
|
||||||
trackEvent("hyperlink", "delete");
|
trackEvent("hyperlink", "delete");
|
||||||
mutateElement(element, { link: null });
|
scene.mutateElement(element, { link: null });
|
||||||
setAppState({ showHyperlinkPopup: false });
|
setAppState({ showHyperlinkPopup: false });
|
||||||
}, [setAppState, element]);
|
}, [setAppState, element, scene]);
|
||||||
|
|
||||||
const onEdit = () => {
|
const onEdit = () => {
|
||||||
trackEvent("hyperlink", "edit", "popup-ui");
|
trackEvent("hyperlink", "edit", "popup-ui");
|
||||||
|
|
|
@ -78,7 +78,7 @@ const WelcomeScreenMenuItemLink = ({
|
||||||
className={`welcome-screen-menu-item ${className}`}
|
className={`welcome-screen-menu-item ${className}`}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
isFirefox,
|
isFirefox,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
cloneJSON,
|
cloneJSON,
|
||||||
|
SVG_DOCUMENT_PREAMBLE,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
@ -134,7 +135,11 @@ export const exportCanvas = async (
|
||||||
if (type === "svg") {
|
if (type === "svg") {
|
||||||
return fileSave(
|
return fileSave(
|
||||||
svgPromise.then((svg) => {
|
svgPromise.then((svg) => {
|
||||||
return new Blob([svg.outerHTML], { type: MIME_TYPES.svg });
|
// adding SVG preamble so that older software parse the SVG file
|
||||||
|
// properly
|
||||||
|
return new Blob([SVG_DOCUMENT_PREAMBLE + svg.outerHTML], {
|
||||||
|
type: MIME_TYPES.svg,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
description: "Export to SVG",
|
description: "Export to SVG",
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { bumpVersion } from "@excalidraw/element/mutateElement";
|
||||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||||
import { detectLineHeight } from "@excalidraw/element/textMeasurements";
|
import { detectLineHeight } from "@excalidraw/element/textMeasurements";
|
||||||
import {
|
import {
|
||||||
|
isArrowBoundToElement,
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
|
@ -439,7 +440,7 @@ const repairContainerElement = (
|
||||||
// if defined, lest boundElements is stale
|
// if defined, lest boundElements is stale
|
||||||
!boundElement.containerId
|
!boundElement.containerId
|
||||||
) {
|
) {
|
||||||
(boundElement as Mutable<ExcalidrawTextElement>).containerId =
|
(boundElement as Mutable<typeof boundElement>).containerId =
|
||||||
container.id;
|
container.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -464,6 +465,10 @@ const repairBoundElement = (
|
||||||
? elementsMap.get(boundElement.containerId)
|
? elementsMap.get(boundElement.containerId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
(boundElement as Mutable<typeof boundElement>).angle = (
|
||||||
|
isArrowElement(container) ? 0 : container?.angle ?? 0
|
||||||
|
) as Radians;
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
boundElement.containerId = null;
|
boundElement.containerId = null;
|
||||||
return;
|
return;
|
||||||
|
@ -590,8 +595,7 @@ export const restoreElements = (
|
||||||
return restoredElements.map((element) => {
|
return restoredElements.map((element) => {
|
||||||
if (
|
if (
|
||||||
isElbowArrow(element) &&
|
isElbowArrow(element) &&
|
||||||
element.startBinding == null &&
|
!isArrowBoundToElement(element) &&
|
||||||
element.endBinding == null &&
|
|
||||||
!validateElbowPoints(element.points)
|
!validateElbowPoints(element.points)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -38,10 +38,13 @@ import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
|
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||||
|
|
||||||
|
import Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
|
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
@ -63,8 +66,6 @@ import type {
|
||||||
|
|
||||||
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { getCommonBounds } from "..";
|
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
x: number;
|
x: number;
|
||||||
|
@ -221,7 +222,7 @@ const DEFAULT_DIMENSION = 100;
|
||||||
const bindTextToContainer = (
|
const bindTextToContainer = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
const textElement: ExcalidrawTextElement = newTextElement({
|
const textElement: ExcalidrawTextElement = newTextElement({
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -240,7 +241,8 @@ const bindTextToContainer = (
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
redrawTextBoundingBox(textElement, container, elementsMap);
|
redrawTextBoundingBox(textElement, container, scene);
|
||||||
|
|
||||||
return [container, textElement] as const;
|
return [container, textElement] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -249,7 +251,7 @@ const bindLinearElementToElement = (
|
||||||
start: ValidLinearElement["start"],
|
start: ValidLinearElement["start"],
|
||||||
end: ValidLinearElement["end"],
|
end: ValidLinearElement["end"],
|
||||||
elementStore: ElementStore,
|
elementStore: ElementStore,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
): {
|
): {
|
||||||
linearElement: ExcalidrawLinearElement;
|
linearElement: ExcalidrawLinearElement;
|
||||||
startBoundElement?: ExcalidrawElement;
|
startBoundElement?: ExcalidrawElement;
|
||||||
|
@ -335,7 +337,7 @@ const bindLinearElementToElement = (
|
||||||
linearElement,
|
linearElement,
|
||||||
startBoundElement as ExcalidrawBindableElement,
|
startBoundElement as ExcalidrawBindableElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -410,7 +412,7 @@ const bindLinearElementToElement = (
|
||||||
linearElement,
|
linearElement,
|
||||||
endBoundElement as ExcalidrawBindableElement,
|
endBoundElement as ExcalidrawBindableElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -651,6 +653,9 @@ export const convertToExcalidrawElements = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementsMap = elementStore.getElementsMap();
|
const elementsMap = elementStore.getElementsMap();
|
||||||
|
// we don't have a real scene, so we just use a temp scene to query and mutate elements
|
||||||
|
const scene = new Scene(elementsMap);
|
||||||
|
|
||||||
// Add labels and arrow bindings
|
// Add labels and arrow bindings
|
||||||
for (const [id, element] of elementsWithIds) {
|
for (const [id, element] of elementsWithIds) {
|
||||||
const excalidrawElement = elementStore.getElement(id)!;
|
const excalidrawElement = elementStore.getElement(id)!;
|
||||||
|
@ -664,7 +669,7 @@ export const convertToExcalidrawElements = (
|
||||||
let [container, text] = bindTextToContainer(
|
let [container, text] = bindTextToContainer(
|
||||||
excalidrawElement,
|
excalidrawElement,
|
||||||
element?.label,
|
element?.label,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
elementStore.add(container);
|
elementStore.add(container);
|
||||||
elementStore.add(text);
|
elementStore.add(text);
|
||||||
|
@ -692,7 +697,7 @@ export const convertToExcalidrawElements = (
|
||||||
originalStart,
|
originalStart,
|
||||||
originalEnd,
|
originalEnd,
|
||||||
elementStore,
|
elementStore,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
container = linearElement;
|
container = linearElement;
|
||||||
elementStore.add(linearElement);
|
elementStore.add(linearElement);
|
||||||
|
@ -717,7 +722,7 @@ export const convertToExcalidrawElements = (
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
elementStore,
|
elementStore,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
elementStore.add(linearElement);
|
elementStore.add(linearElement);
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { atom, createStore, type PrimitiveAtom } from "jotai";
|
import {
|
||||||
|
atom,
|
||||||
|
createStore,
|
||||||
|
type PrimitiveAtom,
|
||||||
|
type WritableAtom,
|
||||||
|
} from "jotai";
|
||||||
import { createIsolation } from "jotai-scope";
|
import { createIsolation } from "jotai-scope";
|
||||||
|
|
||||||
const jotai = createIsolation();
|
const jotai = createIsolation();
|
||||||
|
|
||||||
export { atom, PrimitiveAtom };
|
export { atom, PrimitiveAtom, WritableAtom };
|
||||||
export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
|
export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
|
||||||
export const EditorJotaiProvider: ReturnType<
|
export const EditorJotaiProvider: ReturnType<
|
||||||
typeof createIsolation
|
typeof createIsolation
|
||||||
|
|
243
packages/excalidraw/eraser/index.ts
Normal file
243
packages/excalidraw/eraser/index.ts
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
||||||
|
import { getElementLineSegments } from "@excalidraw/element/bounds";
|
||||||
|
import {
|
||||||
|
lineSegment,
|
||||||
|
lineSegmentIntersectionPoints,
|
||||||
|
pointFrom,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
import { getElementShape } from "@excalidraw/element/shapes";
|
||||||
|
import { shouldTestInside } from "@excalidraw/element/collision";
|
||||||
|
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||||
|
import {
|
||||||
|
hasBoundTextElement,
|
||||||
|
isBoundToContainer,
|
||||||
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
import { getBoundTextElementId } from "@excalidraw/element/textElement";
|
||||||
|
|
||||||
|
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||||
|
import type {
|
||||||
|
ElementsSegmentsMap,
|
||||||
|
GlobalPoint,
|
||||||
|
LineSegment,
|
||||||
|
} from "@excalidraw/math/types";
|
||||||
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import { AnimatedTrail } from "../animated-trail";
|
||||||
|
|
||||||
|
import type { AnimationFrameHandler } from "../animation-frame-handler";
|
||||||
|
|
||||||
|
import type App from "../components/App";
|
||||||
|
|
||||||
|
// just enough to form a segment; this is sufficient for eraser
|
||||||
|
const POINTS_ON_TRAIL = 2;
|
||||||
|
|
||||||
|
export class EraserTrail extends AnimatedTrail {
|
||||||
|
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
|
private groupsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||||
|
private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
|
||||||
|
private geometricShapesCache: Map<string, GeometricShape<GlobalPoint>> =
|
||||||
|
new Map();
|
||||||
|
|
||||||
|
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||||
|
super(animationFrameHandler, app, {
|
||||||
|
streamline: 0.2,
|
||||||
|
size: 5,
|
||||||
|
keepHead: true,
|
||||||
|
sizeMapping: (c) => {
|
||||||
|
const DECAY_TIME = 200;
|
||||||
|
const DECAY_LENGTH = 10;
|
||||||
|
const t = Math.max(
|
||||||
|
0,
|
||||||
|
1 - (performance.now() - c.pressure) / DECAY_TIME,
|
||||||
|
);
|
||||||
|
const l =
|
||||||
|
(DECAY_LENGTH -
|
||||||
|
Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
|
||||||
|
DECAY_LENGTH;
|
||||||
|
|
||||||
|
return Math.min(easeOut(l), easeOut(t));
|
||||||
|
},
|
||||||
|
fill: () =>
|
||||||
|
app.state.theme === THEME.LIGHT
|
||||||
|
? "rgba(0, 0, 0, 0.2)"
|
||||||
|
: "rgba(255, 255, 255, 0.2)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startPath(x: number, y: number): void {
|
||||||
|
this.endPath();
|
||||||
|
super.startPath(x, y);
|
||||||
|
this.elementsToErase.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
addPointToPath(x: number, y: number, restore = false) {
|
||||||
|
super.addPointToPath(x, y);
|
||||||
|
|
||||||
|
const elementsToEraser = this.updateElementsToBeErased(restore);
|
||||||
|
|
||||||
|
return elementsToEraser;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateElementsToBeErased(restoreToErase?: boolean) {
|
||||||
|
let eraserPath: GlobalPoint[] =
|
||||||
|
super
|
||||||
|
.getCurrentTrail()
|
||||||
|
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) || [];
|
||||||
|
|
||||||
|
// for efficiency and avoid unnecessary calculations,
|
||||||
|
// take only POINTS_ON_TRAIL points to form some number of segments
|
||||||
|
eraserPath = eraserPath?.slice(eraserPath.length - POINTS_ON_TRAIL);
|
||||||
|
|
||||||
|
const candidateElements = this.app.visibleElements.filter(
|
||||||
|
(el) => !el.locked,
|
||||||
|
);
|
||||||
|
|
||||||
|
const candidateElementsMap = arrayToMap(candidateElements);
|
||||||
|
|
||||||
|
const pathSegments = eraserPath.reduce((acc, point, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
acc.push(lineSegment(eraserPath[index - 1], point));
|
||||||
|
return acc;
|
||||||
|
}, [] as LineSegment<GlobalPoint>[]);
|
||||||
|
|
||||||
|
if (pathSegments.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const element of candidateElements) {
|
||||||
|
// restore only if already added to the to-be-erased set
|
||||||
|
if (restoreToErase && this.elementsToErase.has(element.id)) {
|
||||||
|
const intersects = eraserTest(
|
||||||
|
pathSegments,
|
||||||
|
element,
|
||||||
|
this.segmentsCache,
|
||||||
|
this.geometricShapesCache,
|
||||||
|
candidateElementsMap,
|
||||||
|
this.app,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intersects) {
|
||||||
|
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||||
|
|
||||||
|
if (this.groupsToErase.has(shallowestGroupId)) {
|
||||||
|
const elementsInGroup = getElementsInGroup(
|
||||||
|
this.app.scene.getNonDeletedElementsMap(),
|
||||||
|
shallowestGroupId,
|
||||||
|
);
|
||||||
|
for (const elementInGroup of elementsInGroup) {
|
||||||
|
this.elementsToErase.delete(elementInGroup.id);
|
||||||
|
}
|
||||||
|
this.groupsToErase.delete(shallowestGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBoundToContainer(element)) {
|
||||||
|
this.elementsToErase.delete(element.containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBoundTextElement(element)) {
|
||||||
|
const boundText = getBoundTextElementId(element);
|
||||||
|
|
||||||
|
if (boundText) {
|
||||||
|
this.elementsToErase.delete(boundText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elementsToErase.delete(element.id);
|
||||||
|
}
|
||||||
|
} else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
|
||||||
|
const intersects = eraserTest(
|
||||||
|
pathSegments,
|
||||||
|
element,
|
||||||
|
this.segmentsCache,
|
||||||
|
this.geometricShapesCache,
|
||||||
|
candidateElementsMap,
|
||||||
|
this.app,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (intersects) {
|
||||||
|
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||||
|
|
||||||
|
if (!this.groupsToErase.has(shallowestGroupId)) {
|
||||||
|
const elementsInGroup = getElementsInGroup(
|
||||||
|
this.app.scene.getNonDeletedElementsMap(),
|
||||||
|
shallowestGroupId,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const elementInGroup of elementsInGroup) {
|
||||||
|
this.elementsToErase.add(elementInGroup.id);
|
||||||
|
}
|
||||||
|
this.groupsToErase.add(shallowestGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBoundTextElement(element)) {
|
||||||
|
const boundText = getBoundTextElementId(element);
|
||||||
|
|
||||||
|
if (boundText) {
|
||||||
|
this.elementsToErase.add(boundText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBoundToContainer(element)) {
|
||||||
|
this.elementsToErase.add(element.containerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elementsToErase.add(element.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(this.elementsToErase);
|
||||||
|
}
|
||||||
|
|
||||||
|
endPath(): void {
|
||||||
|
super.endPath();
|
||||||
|
super.clearTrails();
|
||||||
|
this.elementsToErase.clear();
|
||||||
|
this.groupsToErase.clear();
|
||||||
|
this.segmentsCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eraserTest = (
|
||||||
|
pathSegments: LineSegment<GlobalPoint>[],
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsSegments: ElementsSegmentsMap,
|
||||||
|
shapesCache: Map<string, GeometricShape<GlobalPoint>>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
app: App,
|
||||||
|
): boolean => {
|
||||||
|
let shape = shapesCache.get(element.id);
|
||||||
|
|
||||||
|
if (!shape) {
|
||||||
|
shape = getElementShape<GlobalPoint>(element, elementsMap);
|
||||||
|
shapesCache.set(element.id, shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
||||||
|
if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elementSegments = elementsSegments.get(element.id);
|
||||||
|
|
||||||
|
if (!elementSegments) {
|
||||||
|
elementSegments = getElementLineSegments(element, elementsMap);
|
||||||
|
elementsSegments.set(element.id, elementSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathSegments.some((pathSegment) =>
|
||||||
|
elementSegments?.some(
|
||||||
|
(elementSegment) =>
|
||||||
|
lineSegmentIntersectionPoints(
|
||||||
|
pathSegment,
|
||||||
|
elementSegment,
|
||||||
|
app.getElementHitThreshold(),
|
||||||
|
) !== null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
|
@ -28,6 +28,8 @@ import type {
|
||||||
|
|
||||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { CascadiaFontFaces } from "./Cascadia";
|
import { CascadiaFontFaces } from "./Cascadia";
|
||||||
import { ComicShannsFontFaces } from "./ComicShanns";
|
import { ComicShannsFontFaces } from "./ComicShanns";
|
||||||
import { EmojiFontFaces } from "./Emoji";
|
import { EmojiFontFaces } from "./Emoji";
|
||||||
|
@ -40,8 +42,6 @@ import { NunitoFontFaces } from "./Nunito";
|
||||||
import { VirgilFontFaces } from "./Virgil";
|
import { VirgilFontFaces } from "./Virgil";
|
||||||
import { XiaolaiFontFaces } from "./Xiaolai";
|
import { XiaolaiFontFaces } from "./Xiaolai";
|
||||||
|
|
||||||
import type Scene from "../scene/Scene";
|
|
||||||
|
|
||||||
export class Fonts {
|
export class Fonts {
|
||||||
// it's ok to track fonts across multiple instances only once, so let's use
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
// a static member to reduce memory footprint
|
// a static member to reduce memory footprint
|
||||||
|
|
|
@ -53,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable,
|
renderEmbeddable,
|
||||||
aiEnabled,
|
aiEnabled,
|
||||||
showDeprecatedFonts,
|
showDeprecatedFonts,
|
||||||
|
renderScrollbars,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
|
@ -143,6 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable={renderEmbeddable}
|
renderEmbeddable={renderEmbeddable}
|
||||||
aiEnabled={aiEnabled !== false}
|
aiEnabled={aiEnabled !== false}
|
||||||
showDeprecatedFonts={showDeprecatedFonts}
|
showDeprecatedFonts={showDeprecatedFonts}
|
||||||
|
renderScrollbars={renderScrollbars}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</App>
|
</App>
|
||||||
|
|
|
@ -149,6 +149,7 @@ export class LassoTrail extends AnimatedTrail {
|
||||||
this.app.scene.getNonDeletedElement(
|
this.app.scene.getNonDeletedElement(
|
||||||
selectedIds[0],
|
selectedIds[0],
|
||||||
) as NonDeleted<ExcalidrawLinearElement>,
|
) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
this.app.scene.getNonDeletedElementsMap(),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,11 +7,13 @@ import {
|
||||||
polygonIncludesPointNonZero,
|
polygonIncludesPointNonZero,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
import type {
|
||||||
|
ElementsSegmentsMap,
|
||||||
|
GlobalPoint,
|
||||||
|
LineSegment,
|
||||||
|
} from "@excalidraw/math/types";
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
export type ElementsSegmentsMap = Map<string, LineSegment<GlobalPoint>[]>;
|
|
||||||
|
|
||||||
export const getLassoSelectedElementIds = (input: {
|
export const getLassoSelectedElementIds = (input: {
|
||||||
lassoPath: GlobalPoint[];
|
lassoPath: GlobalPoint[];
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|
|
@ -165,7 +165,9 @@
|
||||||
"unCroppedDimension": "Uncropped dimension",
|
"unCroppedDimension": "Uncropped dimension",
|
||||||
"copyElementLink": "Copy link to object",
|
"copyElementLink": "Copy link to object",
|
||||||
"linkToElement": "Link to object",
|
"linkToElement": "Link to object",
|
||||||
"wrapSelectionInFrame": "Wrap selection in frame"
|
"wrapSelectionInFrame": "Wrap selection in frame",
|
||||||
|
"tab": "Tab",
|
||||||
|
"shapeSwitch": "Switch shape"
|
||||||
},
|
},
|
||||||
"elementLink": {
|
"elementLink": {
|
||||||
"title": "Link to object",
|
"title": "Link to object",
|
||||||
|
|
|
@ -1182,7 +1182,7 @@ const _renderInteractiveScene = ({
|
||||||
let scrollBars;
|
let scrollBars;
|
||||||
if (renderConfig.renderScrollbars) {
|
if (renderConfig.renderScrollbars) {
|
||||||
scrollBars = getScrollBars(
|
scrollBars = getScrollBars(
|
||||||
visibleElements,
|
elementsMap,
|
||||||
normalizedWidth,
|
normalizedWidth,
|
||||||
normalizedHeight,
|
normalizedHeight,
|
||||||
appState,
|
appState,
|
||||||
|
|
|
@ -9,10 +9,11 @@ import type {
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
|
import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
|
||||||
import { renderStaticSceneThrottled } from "../renderer/staticScene";
|
import { renderStaticSceneThrottled } from "../renderer/staticScene";
|
||||||
|
|
||||||
import type Scene from "./Scene";
|
|
||||||
import type { RenderableElementsMap } from "./types";
|
import type { RenderableElementsMap } from "./types";
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
|
|
|
@ -2,24 +2,23 @@ import { getGlobalCSSVariable } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage } from "../i18n";
|
||||||
|
|
||||||
import type { InteractiveCanvasAppState } from "../types";
|
import type { InteractiveCanvasAppState } from "../types";
|
||||||
import type { ScrollBars } from "./types";
|
import type { RenderableElementsMap, ScrollBars } from "./types";
|
||||||
|
|
||||||
export const SCROLLBAR_MARGIN = 4;
|
export const SCROLLBAR_MARGIN = 4;
|
||||||
export const SCROLLBAR_WIDTH = 6;
|
export const SCROLLBAR_WIDTH = 6;
|
||||||
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||||
|
|
||||||
|
// The scrollbar represents where the viewport is in relationship to the scene
|
||||||
export const getScrollBars = (
|
export const getScrollBars = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: RenderableElementsMap,
|
||||||
viewportWidth: number,
|
viewportWidth: number,
|
||||||
viewportHeight: number,
|
viewportHeight: number,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
): ScrollBars => {
|
): ScrollBars => {
|
||||||
if (!elements.length) {
|
if (!elements.size) {
|
||||||
return {
|
return {
|
||||||
horizontal: null,
|
horizontal: null,
|
||||||
vertical: null,
|
vertical: null,
|
||||||
|
@ -33,9 +32,6 @@ export const getScrollBars = (
|
||||||
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||||
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||||
|
|
||||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
|
||||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
|
||||||
|
|
||||||
const safeArea = {
|
const safeArea = {
|
||||||
top: parseInt(getGlobalCSSVariable("sat")) || 0,
|
top: parseInt(getGlobalCSSVariable("sat")) || 0,
|
||||||
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
|
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
|
||||||
|
@ -46,10 +42,8 @@ export const getScrollBars = (
|
||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
|
|
||||||
// The viewport is the rectangle currently visible for the user
|
// The viewport is the rectangle currently visible for the user
|
||||||
const viewportMinX =
|
const viewportMinX = -appState.scrollX + safeArea.left;
|
||||||
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
const viewportMinY = -appState.scrollY + safeArea.top;
|
||||||
const viewportMinY =
|
|
||||||
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
|
||||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||||
|
|
||||||
|
@ -59,8 +53,43 @@ export const getScrollBars = (
|
||||||
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
|
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
|
||||||
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
|
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
|
||||||
|
|
||||||
// The scrollbar represents where the viewport is in relationship to the scene
|
// the elements-only bbox
|
||||||
|
const sceneWidth = elementsMaxX - elementsMinX;
|
||||||
|
const sceneHeight = elementsMaxY - elementsMinY;
|
||||||
|
|
||||||
|
// scene (elements) bbox + the viewport bbox that extends outside of it
|
||||||
|
const extendedSceneWidth = sceneMaxX - sceneMinX;
|
||||||
|
const extendedSceneHeight = sceneMaxY - sceneMinY;
|
||||||
|
|
||||||
|
const scrollWidthOffset =
|
||||||
|
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right) +
|
||||||
|
SCROLLBAR_WIDTH * 2;
|
||||||
|
|
||||||
|
const scrollbarWidth =
|
||||||
|
viewportWidth * (viewportWidthWithZoom / extendedSceneWidth) -
|
||||||
|
scrollWidthOffset;
|
||||||
|
|
||||||
|
const scrollbarHeightOffset =
|
||||||
|
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom) +
|
||||||
|
SCROLLBAR_WIDTH * 2;
|
||||||
|
|
||||||
|
const scrollbarHeight =
|
||||||
|
viewportHeight * (viewportHeightWithZoom / extendedSceneHeight) -
|
||||||
|
scrollbarHeightOffset;
|
||||||
|
// NOTE the delta multiplier calculation isn't quite correct when viewport
|
||||||
|
// is extended outside the scene (elements) bbox as there's some small
|
||||||
|
// accumulation error. I'll let this be an exercise for others to fix. ^^
|
||||||
|
const horizontalDeltaMultiplier =
|
||||||
|
extendedSceneWidth > sceneWidth
|
||||||
|
? (extendedSceneWidth * appState.zoom.value) /
|
||||||
|
(scrollbarWidth + scrollWidthOffset)
|
||||||
|
: viewportWidth / (scrollbarWidth + scrollWidthOffset);
|
||||||
|
|
||||||
|
const verticalDeltaMultiplier =
|
||||||
|
extendedSceneHeight > sceneHeight
|
||||||
|
? (extendedSceneHeight * appState.zoom.value) /
|
||||||
|
(scrollbarHeight + scrollbarHeightOffset)
|
||||||
|
: viewportHeight / (scrollbarHeight + scrollbarHeightOffset);
|
||||||
return {
|
return {
|
||||||
horizontal:
|
horizontal:
|
||||||
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
|
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
|
||||||
|
@ -68,18 +97,17 @@ export const getScrollBars = (
|
||||||
: {
|
: {
|
||||||
x:
|
x:
|
||||||
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
|
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
|
||||||
((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) *
|
SCROLLBAR_WIDTH +
|
||||||
viewportWidth,
|
((viewportMinX - sceneMinX) / extendedSceneWidth) * viewportWidth,
|
||||||
y:
|
y:
|
||||||
viewportHeight -
|
viewportHeight -
|
||||||
SCROLLBAR_WIDTH -
|
SCROLLBAR_WIDTH -
|
||||||
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
|
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
|
||||||
width:
|
width: scrollbarWidth,
|
||||||
((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) *
|
|
||||||
viewportWidth -
|
|
||||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
|
|
||||||
height: SCROLLBAR_WIDTH,
|
height: SCROLLBAR_WIDTH,
|
||||||
|
deltaMultiplier: horizontalDeltaMultiplier,
|
||||||
},
|
},
|
||||||
|
|
||||||
vertical:
|
vertical:
|
||||||
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
|
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
|
||||||
? null
|
? null
|
||||||
|
@ -90,14 +118,13 @@ export const getScrollBars = (
|
||||||
SCROLLBAR_WIDTH -
|
SCROLLBAR_WIDTH -
|
||||||
Math.max(safeArea.right, SCROLLBAR_MARGIN),
|
Math.max(safeArea.right, SCROLLBAR_MARGIN),
|
||||||
y:
|
y:
|
||||||
((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) *
|
Math.max(safeArea.top, SCROLLBAR_MARGIN) +
|
||||||
viewportHeight +
|
SCROLLBAR_WIDTH +
|
||||||
Math.max(safeArea.top, SCROLLBAR_MARGIN),
|
((viewportMinY - sceneMinY) / extendedSceneHeight) *
|
||||||
|
viewportHeight,
|
||||||
width: SCROLLBAR_WIDTH,
|
width: SCROLLBAR_WIDTH,
|
||||||
height:
|
height: scrollbarHeight,
|
||||||
((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) *
|
deltaMultiplier: verticalDeltaMultiplier,
|
||||||
viewportHeight -
|
|
||||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -130,12 +130,14 @@ export type ScrollBars = {
|
||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
deltaMultiplier: number;
|
||||||
} | null;
|
} | null;
|
||||||
vertical: {
|
vertical: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
deltaMultiplier: number;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||||
<a
|
<a
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="blog.excalidaw.com"
|
href="blog.excalidaw.com"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -392,7 +392,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
aria-label="GitHub"
|
aria-label="GitHub"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://github.com/excalidraw/excalidraw"
|
href="https://github.com/excalidraw/excalidraw"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="GitHub"
|
title="GitHub"
|
||||||
>
|
>
|
||||||
|
@ -426,7 +426,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
aria-label="X"
|
aria-label="X"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://x.com/excalidraw"
|
href="https://x.com/excalidraw"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="X"
|
title="X"
|
||||||
>
|
>
|
||||||
|
@ -472,7 +472,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
aria-label="Discord"
|
aria-label="Discord"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://discord.gg/UexuTaE"
|
href="https://discord.gg/UexuTaE"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Discord"
|
title="Discord"
|
||||||
>
|
>
|
||||||
|
|
|
@ -171,7 +171,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 19,
|
"version": 9,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": -50,
|
"y": -50,
|
||||||
|
@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "102.35417",
|
"height": "102.45605",
|
||||||
"id": "id172",
|
"id": "id172",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
|
@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"101.77517",
|
"102.80179",
|
||||||
"102.35417",
|
"102.45605",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
|
@ -227,9 +227,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 40,
|
"version": 37,
|
||||||
"width": "101.77517",
|
"width": "102.80179",
|
||||||
"x": "0.70711",
|
"x": "-0.42182",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -264,7 +264,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 6,
|
"version": 14,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 100,
|
"x": 100,
|
||||||
"y": 100,
|
"y": 100,
|
||||||
|
@ -291,22 +291,39 @@ History {
|
||||||
"added": Map {},
|
"added": Map {},
|
||||||
"removed": Map {},
|
"removed": Map {},
|
||||||
"updated": Map {
|
"updated": Map {
|
||||||
|
"id171" => Delta {
|
||||||
|
"deleted": {
|
||||||
|
"boundElements": [],
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id172",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
"id172" => Delta {
|
"id172" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id171",
|
"elementId": "id175",
|
||||||
"focus": "0.00990",
|
"fixedPoint": [
|
||||||
|
"0.50000",
|
||||||
|
1,
|
||||||
|
],
|
||||||
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.98586",
|
"height": "70.45017",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"100.70774",
|
||||||
"-0.98586",
|
"70.45017",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
|
@ -321,7 +338,7 @@ History {
|
||||||
"focus": "-0.02000",
|
"focus": "-0.02000",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"height": "0.00000",
|
"height": "0.09250",
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
|
@ -329,7 +346,7 @@ History {
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"98.58579",
|
||||||
"0.00000",
|
"0.09250",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
|
@ -339,6 +356,19 @@ History {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"id175" => Delta {
|
||||||
|
"deleted": {
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id172",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"boundElements": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -366,59 +396,32 @@ History {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id171" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"boundElements": [],
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id172",
|
|
||||||
"type": "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id172" => Delta {
|
"id172" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": {
|
"height": "102.45584",
|
||||||
"elementId": "id175",
|
|
||||||
"fixedPoint": [
|
|
||||||
"0.50000",
|
|
||||||
1,
|
|
||||||
],
|
|
||||||
"focus": 0,
|
|
||||||
"gap": 1,
|
|
||||||
},
|
|
||||||
"height": "102.35417",
|
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"101.77517",
|
"102.79971",
|
||||||
"102.35417",
|
"102.45584",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
"height": "70.33521",
|
||||||
"elementId": "id171",
|
|
||||||
"focus": "0.00990",
|
|
||||||
"gap": 1,
|
|
||||||
},
|
|
||||||
"height": "0.98586",
|
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"98.58579",
|
"100.78887",
|
||||||
"-0.98586",
|
"70.33521",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
|
@ -426,20 +429,7 @@ History {
|
||||||
"focus": "0.02970",
|
"focus": "0.02970",
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
"y": "0.99364",
|
"y": "35.20327",
|
||||||
},
|
|
||||||
},
|
|
||||||
"id175" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id172",
|
|
||||||
"type": "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"boundElements": [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -739,7 +729,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 9,
|
"version": 19,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 150,
|
"x": 150,
|
||||||
"y": -50,
|
"y": -50,
|
||||||
|
@ -819,8 +809,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 30,
|
"version": 33,
|
||||||
"width": 0,
|
"width": 100,
|
||||||
"x": "149.29289",
|
"x": "149.29289",
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
|
@ -846,20 +836,22 @@ History {
|
||||||
"added": Map {},
|
"added": Map {},
|
||||||
"removed": Map {},
|
"removed": Map {},
|
||||||
"updated": Map {
|
"updated": Map {
|
||||||
"id167" => Delta {
|
"id166" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"points": [
|
"boundElements": [],
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id167",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"id167" => Delta {
|
||||||
|
"deleted": {
|
||||||
|
"endBinding": null,
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
|
@ -871,6 +863,23 @@ History {
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"inserted": {
|
||||||
|
"endBinding": {
|
||||||
|
"elementId": "id166",
|
||||||
|
"focus": -0,
|
||||||
|
"gap": 1,
|
||||||
|
},
|
||||||
|
"points": [
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -899,22 +908,8 @@ History {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id166" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"boundElements": [],
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"boundElements": [
|
|
||||||
{
|
|
||||||
"id": "id167",
|
|
||||||
"type": "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id167" => Delta {
|
"id167" => Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"endBinding": null,
|
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
|
@ -928,18 +923,13 @@ History {
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"endBinding": {
|
|
||||||
"elementId": "id166",
|
|
||||||
"focus": -0,
|
|
||||||
"gap": 1,
|
|
||||||
},
|
|
||||||
"points": [
|
"points": [
|
||||||
[
|
[
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
0,
|
100,
|
||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -7348,8 +7338,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": 0,
|
||||||
"y": -10,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -7422,8 +7412,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": 0,
|
||||||
"y": -10,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -7490,7 +7480,7 @@ History {
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`;
|
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `10`;
|
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `9`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = `
|
exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
|
@ -10561,7 +10551,7 @@ History {
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`;
|
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `15`;
|
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `14`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = `
|
exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
|
@ -12138,8 +12128,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": -10,
|
||||||
"y": 10,
|
"y": -10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12192,8 +12182,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 60,
|
"x": 40,
|
||||||
"y": 0,
|
"y": -20,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12246,8 +12236,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 150,
|
"x": 130,
|
||||||
"y": -10,
|
"y": -30,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12301,8 +12291,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": -10,
|
||||||
"y": 10,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -12387,8 +12377,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 150,
|
"x": 130,
|
||||||
"y": -10,
|
"y": -30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -20188,4 +20178,4 @@ History {
|
||||||
|
|
||||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`;
|
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `21`;
|
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `20`;
|
||||||
|
|
|
@ -1,40 +1,6 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": null,
|
|
||||||
"customData": undefined,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 50,
|
|
||||||
"id": "id2",
|
|
||||||
"index": "Zz",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 6,
|
|
||||||
"versionNonce": 1604849351,
|
|
||||||
"width": 30,
|
|
||||||
"x": 30,
|
|
||||||
"y": 20,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
|
@ -54,13 +20,47 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1505387817,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 6,
|
"version": 5,
|
||||||
|
"versionNonce": 1505387817,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": null,
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 50,
|
||||||
|
"id": "id2",
|
||||||
|
"index": "a1",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": {
|
||||||
|
"type": 3,
|
||||||
|
},
|
||||||
|
"seed": 1604849351,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "rectangle",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 7,
|
||||||
"versionNonce": 915032327,
|
"versionNonce": 915032327,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
|
|
|
@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 400692809,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
@ -106,7 +106,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 400692809,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue