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",
|
||||
"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.
|
||||
|
||||
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**
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ All `props` are _optional_.
|
|||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
|
||||
|
||||
### Storing custom data on Excalidraw elements
|
||||
|
||||
|
|
|
@ -104,6 +104,7 @@ export default function ExampleApp({
|
|||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||
const [renderScrollbars, setRenderScrollbars] = useState(false);
|
||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
|
@ -192,6 +193,7 @@ export default function ExampleApp({
|
|||
}) => setPointerData(payload),
|
||||
viewModeEnabled,
|
||||
zenModeEnabled,
|
||||
renderScrollbars,
|
||||
gridModeEnabled,
|
||||
theme,
|
||||
name: "Custom name of drawing",
|
||||
|
@ -710,6 +712,14 @@ export default function ExampleApp({
|
|||
/>
|
||||
Grid mode
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={renderScrollbars}
|
||||
onChange={() => setRenderScrollbars(!renderScrollbars)}
|
||||
/>
|
||||
Render scrollbars
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
|
@ -73,7 +73,7 @@ export const AIComponents = ({
|
|||
</br>
|
||||
<div>You can also try <a href="${
|
||||
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>
|
||||
</body>
|
||||
</html>`,
|
||||
|
|
|
@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
|
|||
className="encrypted-icon tooltip"
|
||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||
|
|
|
@ -10,7 +10,7 @@ export const ExcalidrawPlusAppLink = () => {
|
|||
import.meta.env.VITE_APP_PLUS_APP
|
||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
className="plus-button"
|
||||
>
|
||||
Go to Excalidraw+
|
||||
|
|
|
@ -198,7 +198,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
|||
<a
|
||||
class="welcome-screen-menu-item "
|
||||
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -112,12 +112,14 @@ export const YOUTUBE_STATES = {
|
|||
export const ENV = {
|
||||
TEST: "test",
|
||||
DEVELOPMENT: "development",
|
||||
PRODUCTION: "production",
|
||||
};
|
||||
|
||||
export const CLASSES = {
|
||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||
ZOOM_ACTIONS: "zoom-actions",
|
||||
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||
};
|
||||
|
||||
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 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;
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { average } from "@excalidraw/math";
|
||||
import { average, pointFrom, type GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
FontFamilyValues,
|
||||
FontString,
|
||||
ExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type {
|
||||
|
@ -679,7 +680,7 @@ export const arrayToMap = <T extends { id: string } | string>(
|
|||
return items.reduce((acc: Map<string, T>, element) => {
|
||||
acc.set(typeof element === "string" ? element : element.id, element);
|
||||
return acc;
|
||||
}, new Map());
|
||||
}, new Map() as Map<string, T>);
|
||||
};
|
||||
|
||||
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 isProdEnv = () => import.meta.env.MODE === ENV.PRODUCTION;
|
||||
|
||||
export const isServerEnv = () =>
|
||||
typeof process !== "undefined" && !!process?.env?.NODE_ENV;
|
||||
|
||||
|
@ -1201,3 +1204,32 @@ export const escapeDoubleQuotes = (str: string) => {
|
|||
|
||||
export const castArray = <T>(value: T | T[]): T[] =>
|
||||
Array.isArray(value) ? value : [value];
|
||||
|
||||
export const elementCenterPoint = (
|
||||
element: ExcalidrawElement,
|
||||
xOffset: number = 0,
|
||||
yOffset: number = 0,
|
||||
) => {
|
||||
const { x, y, width, height } = element;
|
||||
|
||||
const centerXPoint = x + width / 2 + xOffset;
|
||||
|
||||
const centerYPoint = y + height / 2 + yOffset;
|
||||
|
||||
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
||||
};
|
||||
|
||||
/** 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,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
isReadonlyArray,
|
||||
} from "@excalidraw/common";
|
||||
import { isNonDeletedElement } from "@excalidraw/element";
|
||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
||||
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||
|
||||
import {
|
||||
orderByFractionalIndex,
|
||||
syncInvalidIndices,
|
||||
syncMovedIndices,
|
||||
validateFractionalIndices,
|
||||
|
@ -19,7 +21,11 @@ import {
|
|||
|
||||
import { getSelectedElements } from "@excalidraw/element/selection";
|
||||
|
||||
import type { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import {
|
||||
mutateElement,
|
||||
type ElementUpdate,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
|
@ -32,12 +38,13 @@ import type {
|
|||
Ordered,
|
||||
} 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";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
import type { AppState } from "../../excalidraw/types";
|
||||
|
||||
type SceneStateCallback = () => void;
|
||||
type SceneStateCallbackRemover = () => void;
|
||||
|
@ -102,44 +109,7 @@ const hashSelectionOpts = (
|
|||
// in our codebase
|
||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||
|
||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
||||
if (typeof elementKey === "string") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
class Scene {
|
||||
// ---------------------------------------------------------------------------
|
||||
// static methods/props
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
||||
private static sceneMapById = new Map<string, Scene>();
|
||||
|
||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
||||
if (isIdKey(elementKey)) {
|
||||
// for cases where we don't have access to the element object
|
||||
// (e.g. restore serialized appState with id references)
|
||||
this.sceneMapById.set(elementKey, scene);
|
||||
} else {
|
||||
this.sceneMapByElement.set(elementKey, scene);
|
||||
// if mapping element objects, also cache the id string when later
|
||||
// looking up by id alone
|
||||
this.sceneMapById.set(elementKey.id, scene);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated pass down `app.scene` and use it directly
|
||||
*/
|
||||
static getScene(elementKey: ElementKey): Scene | null {
|
||||
if (isIdKey(elementKey)) {
|
||||
return this.sceneMapById.get(elementKey) || null;
|
||||
}
|
||||
return this.sceneMapByElement.get(elementKey) || null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// instance methods/props
|
||||
// ---------------------------------------------------------------------------
|
||||
|
@ -198,6 +168,12 @@ class Scene {
|
|||
return this.frames;
|
||||
}
|
||||
|
||||
constructor(elements: ElementsMapOrArray | null = null) {
|
||||
if (elements) {
|
||||
this.replaceAllElements(elements);
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedElements(opts: {
|
||||
// NOTE can be ommitted by making Scene constructor require App instance
|
||||
selectedElementIds: AppState["selectedElementIds"];
|
||||
|
@ -292,23 +268,25 @@ class Scene {
|
|||
}
|
||||
|
||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||
const _nextElements =
|
||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||
nextElements instanceof Array
|
||||
? nextElements
|
||||
: Array.from(nextElements.values());
|
||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||
if (!isReadonlyArray(nextElements)) {
|
||||
// need to order by fractional indices to get the correct order
|
||||
nextElements = orderByFractionalIndex(
|
||||
Array.from(nextElements.values()) as OrderedExcalidrawElement[],
|
||||
);
|
||||
}
|
||||
|
||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||
|
||||
validateIndicesThrottled(_nextElements);
|
||||
validateIndicesThrottled(nextElements);
|
||||
|
||||
this.elements = syncInvalidIndices(_nextElements);
|
||||
this.elements = syncInvalidIndices(nextElements);
|
||||
this.elementsMap.clear();
|
||||
this.elements.forEach((element) => {
|
||||
if (isFrameLikeElement(element)) {
|
||||
nextFrameLikes.push(element);
|
||||
}
|
||||
this.elementsMap.set(element.id, element);
|
||||
Scene.mapElementToScene(element, this);
|
||||
});
|
||||
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||
this.nonDeletedElements = nonDeletedElements.elements;
|
||||
|
@ -353,12 +331,6 @@ class Scene {
|
|||
this.selectedElementsCache.elements = null;
|
||||
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
|
||||
// (I guess?)
|
||||
this.callbacks.clear();
|
||||
|
@ -455,6 +427,42 @@ class Scene {
|
|||
// then, check if the id is a group
|
||||
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;
|
|
@ -1,12 +1,11 @@
|
|||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import { updateBoundElements } from "./binding";
|
||||
import { getCommonBoundingBox } from "./bounds";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getMaximumGroups } from "./groups";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { BoundingBox } from "./bounds";
|
||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
export interface Alignment {
|
||||
position: "start" | "center" | "end";
|
||||
|
@ -15,10 +14,10 @@ export interface Alignment {
|
|||
|
||||
export const alignElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
alignment: Alignment,
|
||||
scene: Scene,
|
||||
): ExcalidrawElement[] => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
|
@ -33,12 +32,13 @@ export const alignElements = (
|
|||
);
|
||||
return group.map((element) => {
|
||||
// update element
|
||||
const updatedEle = mutateElement(element, {
|
||||
const updatedEle = scene.mutateElement(element, {
|
||||
x: element.x + translation.x,
|
||||
y: element.y + translation.y,
|
||||
});
|
||||
|
||||
// update bound elements
|
||||
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: group,
|
||||
});
|
||||
return updatedEle;
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
invariant,
|
||||
isDevEnv,
|
||||
isTestEnv,
|
||||
elementCenterPoint,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
|
@ -30,8 +31,6 @@ import { isPointOnShape } from "@excalidraw/utils/collision";
|
|||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
@ -55,7 +54,6 @@ import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
|||
import {
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
isBindingElement,
|
||||
isBoundToContainer,
|
||||
isElbowArrow,
|
||||
isFixedPointBinding,
|
||||
|
@ -68,6 +66,8 @@ import {
|
|||
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { ElementUpdate } from "./mutateElement";
|
||||
import type {
|
||||
|
@ -81,10 +81,8 @@ import type {
|
|||
NonDeletedSceneElementsMap,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawArrowElement,
|
||||
OrderedExcalidrawElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
FixedPoint,
|
||||
SceneElementsMap,
|
||||
FixedPointBinding,
|
||||
} from "./types";
|
||||
|
||||
|
@ -130,7 +128,6 @@ export const bindOrUnbindLinearElement = (
|
|||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): void => {
|
||||
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||
|
@ -142,7 +139,7 @@ export const bindOrUnbindLinearElement = (
|
|||
"start",
|
||||
boundToElementIds,
|
||||
unboundFromElementIds,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
bindOrUnbindLinearElementEdge(
|
||||
linearElement,
|
||||
|
@ -151,7 +148,7 @@ export const bindOrUnbindLinearElement = (
|
|||
"end",
|
||||
boundToElementIds,
|
||||
unboundFromElementIds,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
||||
|
@ -159,7 +156,7 @@ export const bindOrUnbindLinearElement = (
|
|||
);
|
||||
|
||||
getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
boundElements: element.boundElements?.filter(
|
||||
(element) =>
|
||||
element.type !== "arrow" || element.id !== linearElement.id,
|
||||
|
@ -177,7 +174,7 @@ const bindOrUnbindLinearElementEdge = (
|
|||
boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||
// Is mutated
|
||||
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): void => {
|
||||
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
||||
if (bindableElement === "keep") {
|
||||
|
@ -186,7 +183,7 @@ const bindOrUnbindLinearElementEdge = (
|
|||
|
||||
// null means break the bind, so nothing to consider here
|
||||
if (bindableElement === null) {
|
||||
const unbound = unbindLinearElement(linearElement, startOrEnd);
|
||||
const unbound = unbindLinearElement(linearElement, startOrEnd, scene);
|
||||
if (unbound != null) {
|
||||
unboundFromElementIds.add(unbound);
|
||||
}
|
||||
|
@ -209,16 +206,11 @@ const bindOrUnbindLinearElementEdge = (
|
|||
: startOrEnd === "start" ||
|
||||
otherEdgeBindableElement.id !== bindableElement.id)
|
||||
) {
|
||||
bindLinearElement(
|
||||
linearElement,
|
||||
bindableElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
);
|
||||
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||
boundToElementIds.add(bindableElement.id);
|
||||
}
|
||||
} else {
|
||||
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap);
|
||||
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||
boundToElementIds.add(bindableElement.id);
|
||||
}
|
||||
};
|
||||
|
@ -283,15 +275,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||
zoom,
|
||||
)
|
||||
: 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";
|
||||
const end = endDragged
|
||||
? isBindingEnabled
|
||||
|
@ -303,15 +286,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
|||
zoom,
|
||||
)
|
||||
: 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";
|
||||
|
||||
return [start, end];
|
||||
|
@ -362,11 +336,9 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
|||
|
||||
export const bindOrUnbindLinearElements = (
|
||||
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
isBindingEnabled: boolean,
|
||||
draggingPoints: readonly number[] | null,
|
||||
scene: Scene,
|
||||
zoom?: AppState["zoom"],
|
||||
): void => {
|
||||
selectedElements.forEach((selectedElement) => {
|
||||
|
@ -376,20 +348,20 @@ export const bindOrUnbindLinearElements = (
|
|||
selectedElement,
|
||||
isBindingEnabled,
|
||||
draggingPoints ?? [],
|
||||
elementsMap,
|
||||
elements,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene.getNonDeletedElements(),
|
||||
zoom,
|
||||
)
|
||||
: // The arrow itself (the shaft) or the inner joins are dragged
|
||||
getBindingStrategyForDraggingArrowOrJoints(
|
||||
selectedElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene.getNonDeletedElements(),
|
||||
isBindingEnabled,
|
||||
zoom,
|
||||
);
|
||||
|
||||
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
|
||||
bindOrUnbindLinearElement(selectedElement, start, end, scene);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -429,15 +401,17 @@ export const maybeBindLinearElement = (
|
|||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
pointerCoords: { x: number; y: number },
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
): void => {
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
if (appState.startBoundElement != null) {
|
||||
bindLinearElement(
|
||||
linearElement,
|
||||
appState.startBoundElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -458,7 +432,7 @@ export const maybeBindLinearElement = (
|
|||
"end",
|
||||
)
|
||||
) {
|
||||
bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
|
||||
bindLinearElement(linearElement, hoveredElement, "end", scene);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -487,7 +461,7 @@ export const bindLinearElement = (
|
|||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): void => {
|
||||
if (!isArrowElement(linearElement)) {
|
||||
return;
|
||||
|
@ -500,7 +474,7 @@ export const bindLinearElement = (
|
|||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
hoveredElement,
|
||||
),
|
||||
|
@ -513,18 +487,17 @@ export const bindLinearElement = (
|
|||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
mutateElement(linearElement, {
|
||||
scene.mutateElement(linearElement, {
|
||||
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
||||
});
|
||||
|
||||
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
|
||||
if (!boundElementsMap.has(linearElement.id)) {
|
||||
mutateElement(hoveredElement, {
|
||||
scene.mutateElement(hoveredElement, {
|
||||
boundElements: (hoveredElement.boundElements || []).concat({
|
||||
id: linearElement.id,
|
||||
type: "arrow",
|
||||
|
@ -566,13 +539,14 @@ const isLinearElementSimple = (
|
|||
const unbindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
scene: Scene,
|
||||
): ExcalidrawBindableElement["id"] | null => {
|
||||
const field = startOrEnd === "start" ? "startBinding" : "endBinding";
|
||||
const binding = linearElement[field];
|
||||
if (binding == null) {
|
||||
return null;
|
||||
}
|
||||
mutateElement(linearElement, { [field]: null });
|
||||
scene.mutateElement(linearElement, { [field]: null });
|
||||
return binding.elementId;
|
||||
};
|
||||
|
||||
|
@ -735,25 +709,30 @@ const calculateFocusAndGap = (
|
|||
|
||||
// Supports translating, rotating and scaling `changedElement` with bound
|
||||
// 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 = (
|
||||
changedElement: NonDeletedExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||
changedElements?: Map<string, ExcalidrawElement>;
|
||||
},
|
||||
) => {
|
||||
if (!isBindableElement(changedElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
|
||||
if (!isBindableElement(changedElement)) {
|
||||
return;
|
||||
let elementsMap: ElementsMap = scene.getNonDeletedElementsMap();
|
||||
if (options?.changedElements) {
|
||||
elementsMap = new Map(elementsMap) as typeof elementsMap;
|
||||
options.changedElements.forEach((element) => {
|
||||
elementsMap.set(element.id, element);
|
||||
});
|
||||
}
|
||||
|
||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||
|
@ -796,7 +775,7 @@ export const updateBoundElements = (
|
|||
|
||||
// `linearElement` is being moved/scaled already, just update the binding
|
||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||
mutateElement(element, bindings, true);
|
||||
scene.mutateElement(element, bindings);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -843,27 +822,41 @@ export const updateBoundElements = (
|
|||
}> => update !== null,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
updates,
|
||||
{
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
: {}),
|
||||
...(changedElement.id === element.endBinding?.elementId
|
||||
? { endBinding: bindings.endBinding }
|
||||
: {}),
|
||||
},
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
LinearElementEditor.movePoints(element, scene, updates, {
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
: {}),
|
||||
...(changedElement.id === element.endBinding?.elementId
|
||||
? { endBinding: bindings.endBinding }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
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 = (
|
||||
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
changedElement: ExcalidrawBindableElement,
|
||||
|
@ -885,7 +878,6 @@ export const getHeadingForElbowArrowSnap = (
|
|||
otherPoint: Readonly<GlobalPoint>,
|
||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||
aabb: Bounds | undefined | null,
|
||||
elementsMap: ElementsMap,
|
||||
origPoint: GlobalPoint,
|
||||
zoom?: AppState["zoom"],
|
||||
): Heading => {
|
||||
|
@ -895,22 +887,11 @@ export const getHeadingForElbowArrowSnap = (
|
|||
return otherPointHeading;
|
||||
}
|
||||
|
||||
const distance = getDistanceForBinding(
|
||||
origPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
zoom,
|
||||
);
|
||||
const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
|
||||
|
||||
if (!distance) {
|
||||
return vectorToHeading(
|
||||
vectorFromPoint(
|
||||
p,
|
||||
pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
),
|
||||
),
|
||||
vectorFromPoint(p, elementCenterPoint(bindableElement)),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -920,7 +901,6 @@ export const getHeadingForElbowArrowSnap = (
|
|||
const getDistanceForBinding = (
|
||||
point: Readonly<GlobalPoint>,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
) => {
|
||||
const distance = distanceToBindableElement(bindableElement, point);
|
||||
|
@ -1040,10 +1020,7 @@ export const avoidRectangularCorner = (
|
|||
element: ExcalidrawBindableElement,
|
||||
p: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||
|
@ -1140,10 +1117,9 @@ export const snapToMid = (
|
|||
tolerance: number = 0.05,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, height, angle } = element;
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
x + width / 2 - 0.1,
|
||||
y + height / 2 - 0.1,
|
||||
);
|
||||
|
||||
const center = elementCenterPoint(element, -0.1, -0.1);
|
||||
|
||||
const nonRotated = pointRotateRads(p, center, -angle as Radians);
|
||||
|
||||
// snap-to-center point is adaptive to element size, but we don't want to go
|
||||
|
@ -1226,12 +1202,8 @@ const updateBoundPoint = (
|
|||
linearElement,
|
||||
bindableElement,
|
||||
startOrEnd === "startBinding" ? "start" : "end",
|
||||
elementsMap,
|
||||
).fixedPoint;
|
||||
const globalMidPoint = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const globalMidPoint = elementCenterPoint(bindableElement);
|
||||
const global = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + fixedPoint[0] * bindableElement.width,
|
||||
bindableElement.y + fixedPoint[1] * bindableElement.height,
|
||||
|
@ -1275,10 +1247,7 @@ const updateBoundPoint = (
|
|||
elementsMap,
|
||||
);
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
bindableElement.x + bindableElement.width / 2,
|
||||
bindableElement.y + bindableElement.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(bindableElement);
|
||||
const interceptorLength =
|
||||
pointDistance(adjacentPoint, edgePointAbsolute) +
|
||||
pointDistance(adjacentPoint, center) +
|
||||
|
@ -1336,7 +1305,6 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
): { fixedPoint: FixedPoint } => {
|
||||
const bounds = [
|
||||
hoveredElement.x,
|
||||
|
@ -1424,19 +1392,19 @@ const getLinearElementEdgeCoors = (
|
|||
};
|
||||
|
||||
export const fixDuplicatedBindingsAfterDuplication = (
|
||||
newElements: ExcalidrawElement[],
|
||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||
duplicatedElementsMap: NonDeletedSceneElementsMap,
|
||||
duplicatedElements: ExcalidrawElement[],
|
||||
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||
duplicateElementsMap: NonDeletedSceneElementsMap,
|
||||
) => {
|
||||
for (const element of newElements) {
|
||||
if ("boundElements" in element && element.boundElements) {
|
||||
Object.assign(element, {
|
||||
boundElements: element.boundElements.reduce(
|
||||
for (const duplicateElement of duplicatedElements) {
|
||||
if ("boundElements" in duplicateElement && duplicateElement.boundElements) {
|
||||
Object.assign(duplicateElement, {
|
||||
boundElements: duplicateElement.boundElements.reduce(
|
||||
(
|
||||
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
||||
binding,
|
||||
) => {
|
||||
const newBindingId = oldIdToDuplicatedId.get(binding.id);
|
||||
const newBindingId = origIdToDuplicateId.get(binding.id);
|
||||
if (newBindingId) {
|
||||
acc.push({ ...binding, id: newBindingId });
|
||||
}
|
||||
|
@ -1447,46 +1415,47 @@ export const fixDuplicatedBindingsAfterDuplication = (
|
|||
});
|
||||
}
|
||||
|
||||
if ("containerId" in element && element.containerId) {
|
||||
Object.assign(element, {
|
||||
containerId: oldIdToDuplicatedId.get(element.containerId) ?? null,
|
||||
if ("containerId" in duplicateElement && duplicateElement.containerId) {
|
||||
Object.assign(duplicateElement, {
|
||||
containerId:
|
||||
origIdToDuplicateId.get(duplicateElement.containerId) ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
if ("endBinding" in element && element.endBinding) {
|
||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
||||
element.endBinding.elementId,
|
||||
if ("endBinding" in duplicateElement && duplicateElement.endBinding) {
|
||||
const newEndBindingId = origIdToDuplicateId.get(
|
||||
duplicateElement.endBinding.elementId,
|
||||
);
|
||||
Object.assign(element, {
|
||||
Object.assign(duplicateElement, {
|
||||
endBinding: newEndBindingId
|
||||
? {
|
||||
...element.endBinding,
|
||||
...duplicateElement.endBinding,
|
||||
elementId: newEndBindingId,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
if ("startBinding" in element && element.startBinding) {
|
||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
||||
element.startBinding.elementId,
|
||||
if ("startBinding" in duplicateElement && duplicateElement.startBinding) {
|
||||
const newEndBindingId = origIdToDuplicateId.get(
|
||||
duplicateElement.startBinding.elementId,
|
||||
);
|
||||
Object.assign(element, {
|
||||
Object.assign(duplicateElement, {
|
||||
startBinding: newEndBindingId
|
||||
? {
|
||||
...element.startBinding,
|
||||
...duplicateElement.startBinding,
|
||||
elementId: newEndBindingId,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
if (isElbowArrow(duplicateElement)) {
|
||||
Object.assign(
|
||||
element,
|
||||
updateElbowArrowPoints(element, duplicatedElementsMap, {
|
||||
duplicateElement,
|
||||
updateElbowArrowPoints(duplicateElement, duplicateElementsMap, {
|
||||
points: [
|
||||
element.points[0],
|
||||
element.points[element.points.length - 1],
|
||||
duplicateElement.points[0],
|
||||
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 = (
|
||||
sceneElements: readonly ExcalidrawElement[],
|
||||
deletedElements: readonly ExcalidrawElement[],
|
||||
|
@ -1691,8 +1470,12 @@ export const fixBindingsAfterDeletion = (
|
|||
const elements = arrayToMap(sceneElements);
|
||||
|
||||
for (const element of deletedElements) {
|
||||
BoundElement.unbindAffected(elements, element, mutateElement);
|
||||
BindableElement.unbindAffected(elements, element, mutateElement);
|
||||
BoundElement.unbindAffected(elements, element, (element, updates) =>
|
||||
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)
|
||||
b: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
if (pointsEqual(a, b)) {
|
||||
return 0;
|
||||
|
@ -1904,10 +1684,7 @@ const determineFocusPoint = (
|
|||
focus: number,
|
||||
adjacentPoint: GlobalPoint,
|
||||
): GlobalPoint => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
if (focus === 0) {
|
||||
return center;
|
||||
|
@ -2338,10 +2115,7 @@ export const getGlobalFixedPointForBindableElement = (
|
|||
element.x + element.width * fixedX,
|
||||
element.y + element.height * fixedY,
|
||||
),
|
||||
pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
),
|
||||
elementCenterPoint(element),
|
||||
element.angle,
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import rough from "roughjs/bin/rough";
|
||||
|
||||
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
|
||||
import {
|
||||
rescalePoints,
|
||||
arrayToMap,
|
||||
invariant,
|
||||
sizeOf,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
degreesToRadians,
|
||||
|
@ -57,6 +62,7 @@ import type {
|
|||
ElementsMap,
|
||||
ExcalidrawRectanguloidElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ElementsMapOrArray,
|
||||
} from "./types";
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
|
@ -938,10 +944,10 @@ export const getElementBounds = (
|
|||
};
|
||||
|
||||
export const getCommonBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: ElementsMapOrArray,
|
||||
elementsMap?: ElementsMap,
|
||||
): Bounds => {
|
||||
if (!elements.length) {
|
||||
if (!sizeOf(elements)) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isTransparent } from "@excalidraw/common";
|
||||
import { isTransparent, elementCenterPoint } from "@excalidraw/common";
|
||||
import {
|
||||
curveIntersectLineSegment,
|
||||
isPointWithinBounds,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
} from "@excalidraw/math/ellipse";
|
||||
|
||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||
import { getPolygonShape } from "@excalidraw/utils/shape";
|
||||
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import type {
|
||||
GlobalPoint,
|
||||
|
@ -26,8 +26,6 @@ import type {
|
|||
Radians,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
||||
|
@ -191,10 +189,7 @@ const intersectRectanguloidWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||
// instead. It's all the same distance-wise.
|
||||
const rotatedA = pointRotateRads<GlobalPoint>(
|
||||
|
@ -253,10 +248,7 @@ const intersectDiamondWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||
// points. It's all the same distance-wise.
|
||||
|
@ -304,10 +296,7 @@ const intersectEllipseWithLineSegment = (
|
|||
l: LineSegment<GlobalPoint>,
|
||||
offset: number = 0,
|
||||
): GlobalPoint[] => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians);
|
||||
const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians);
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
} from "@excalidraw/math";
|
||||
import { type Point } from "points-on-curve";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
getElementAbsoluteCoords,
|
||||
getResizedElementAbsoluteCoords,
|
||||
|
@ -61,7 +63,7 @@ export const cropElement = (
|
|||
|
||||
const rotatedPointer = pointRotateRads(
|
||||
pointFrom(pointerX, pointerY),
|
||||
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
|
||||
elementCenterPoint(element),
|
||||
-element.angle as Radians,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import {
|
||||
curvePointDistance,
|
||||
distanceToLineSegment,
|
||||
pointFrom,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { GlobalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
|
@ -53,10 +54,7 @@ const distanceToRectanguloidElement = (
|
|||
element: ExcalidrawRectanguloidElement,
|
||||
p: GlobalPoint,
|
||||
) => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
// To emulate a rotated rectangle we rotate the point in the inverse angle
|
||||
// instead. It's all the same distance-wise.
|
||||
const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians);
|
||||
|
@ -84,10 +82,7 @@ const distanceToDiamondElement = (
|
|||
element: ExcalidrawDiamondElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
// Rotate the point to the inverse direction to simulate the rotated diamond
|
||||
// points. It's all the same distance-wise.
|
||||
|
@ -115,10 +110,7 @@ const distanceToEllipseElement = (
|
|||
element: ExcalidrawEllipseElement,
|
||||
p: GlobalPoint,
|
||||
): number => {
|
||||
const center = pointFrom(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
return ellipseDistanceFromPoint(
|
||||
// Instead of rotating the ellipse, rotate the point to the inverse angle
|
||||
pointRotateRads(p, center, -element.angle as Radians),
|
||||
|
|
|
@ -11,13 +11,10 @@ import type {
|
|||
PointerDownState,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { updateBoundElements } from "./binding";
|
||||
import { getCommonBounds } from "./bounds";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
import { getMinTextElementWidth } from "./textMeasurements";
|
||||
|
@ -29,6 +26,8 @@ import {
|
|||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
|
@ -104,7 +103,7 @@ export const dragSelectedElements = (
|
|||
);
|
||||
|
||||
elementsToUpdate.forEach((element) => {
|
||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||
if (!isArrowElement(element)) {
|
||||
// skip arrow labels since we calculate its position during render
|
||||
const textElement = getBoundTextElement(
|
||||
|
@ -112,9 +111,14 @@ export const dragSelectedElements = (
|
|||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (textElement) {
|
||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||
updateElementCoords(
|
||||
pointerDownState,
|
||||
textElement,
|
||||
scene,
|
||||
adjustedOffset,
|
||||
);
|
||||
}
|
||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||
});
|
||||
}
|
||||
|
@ -155,6 +159,7 @@ const calculateOffset = (
|
|||
const updateElementCoords = (
|
||||
pointerDownState: PointerDownState,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
scene: Scene,
|
||||
dragOffset: { x: number; y: number },
|
||||
) => {
|
||||
const originalElement =
|
||||
|
@ -163,7 +168,7 @@ const updateElementCoords = (
|
|||
const nextX = originalElement.x + dragOffset.x;
|
||||
const nextY = originalElement.y + dragOffset.y;
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
});
|
||||
|
@ -190,6 +195,7 @@ export const dragNewElement = ({
|
|||
shouldMaintainAspectRatio,
|
||||
shouldResizeFromCenter,
|
||||
zoom,
|
||||
scene,
|
||||
widthAspectRatio = null,
|
||||
originOffset = null,
|
||||
informMutation = true,
|
||||
|
@ -205,6 +211,7 @@ export const dragNewElement = ({
|
|||
shouldMaintainAspectRatio: boolean;
|
||||
shouldResizeFromCenter: boolean;
|
||||
zoom: NormalizedZoomValue;
|
||||
scene: Scene;
|
||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||
true */
|
||||
widthAspectRatio?: number | null;
|
||||
|
@ -285,7 +292,7 @@ export const dragNewElement = ({
|
|||
};
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
x: newX + (originOffset?.x ?? 0),
|
||||
|
@ -295,7 +302,7 @@ export const dragNewElement = ({
|
|||
...textAutoResize,
|
||||
...imageInitialDimension,
|
||||
},
|
||||
informMutation,
|
||||
{ informMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -36,10 +36,7 @@ import {
|
|||
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
|
||||
import {
|
||||
fixDuplicatedBindingsAfterDuplication,
|
||||
fixReversedBindings,
|
||||
} from "./binding";
|
||||
import { fixDuplicatedBindingsAfterDuplication } from "./binding";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
|
@ -60,16 +57,14 @@ import type {
|
|||
* multiple elements at once, share this map
|
||||
* amongst all of them
|
||||
* @param element Element to duplicate
|
||||
* @param overrides Any element properties to override
|
||||
*/
|
||||
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||
editingGroupId: AppState["editingGroupId"],
|
||||
groupIdMapForOperation: Map<GroupId, GroupId>,
|
||||
element: TElement,
|
||||
overrides?: Partial<TElement>,
|
||||
randomizeSeed?: boolean,
|
||||
): Readonly<TElement> => {
|
||||
let copy = deepCopyElement(element);
|
||||
const copy = deepCopyElement(element);
|
||||
|
||||
if (isTestEnv()) {
|
||||
__test__defineOrigId(copy, element.id);
|
||||
|
@ -92,9 +87,6 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
|||
return groupIdMapForOperation.get(groupId)!;
|
||||
},
|
||||
);
|
||||
if (overrides) {
|
||||
copy = Object.assign(copy, overrides);
|
||||
}
|
||||
return copy;
|
||||
};
|
||||
|
||||
|
@ -102,9 +94,14 @@ export const duplicateElements = (
|
|||
opts: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
randomizeSeed?: boolean;
|
||||
overrides?: (
|
||||
originalElement: ExcalidrawElement,
|
||||
) => Partial<ExcalidrawElement>;
|
||||
overrides?: (data: {
|
||||
duplicateElement: ExcalidrawElement;
|
||||
origElement: ExcalidrawElement;
|
||||
origIdToDuplicateId: Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement["id"]
|
||||
>;
|
||||
}) => Partial<ExcalidrawElement>;
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
|
@ -132,14 +129,6 @@ export const duplicateElements = (
|
|||
editingGroupId: AppState["editingGroupId"];
|
||||
selectedGroupIds: AppState["selectedGroupIds"];
|
||||
};
|
||||
/**
|
||||
* If true, duplicated elements are inserted _before_ specified
|
||||
* elements. Case: alt-dragging elements to duplicate them.
|
||||
*
|
||||
* TODO: remove this once (if) we stop replacing the original element
|
||||
* with the duplicated one in the scene array.
|
||||
*/
|
||||
reverseOrder: boolean;
|
||||
}
|
||||
),
|
||||
) => {
|
||||
|
@ -153,8 +142,6 @@ export const duplicateElements = (
|
|||
selectedGroupIds: {},
|
||||
} as const);
|
||||
|
||||
const reverseOrder = opts.type === "in-place" ? opts.reverseOrder : false;
|
||||
|
||||
// Ids of elements that have already been processed so we don't push them
|
||||
// into the array twice if we end up backtracking when retrieving
|
||||
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||
|
@ -167,10 +154,17 @@ export const duplicateElements = (
|
|||
// loop over them.
|
||||
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||
const groupIdMap = new Map();
|
||||
const newElements: ExcalidrawElement[] = [];
|
||||
const oldElements: ExcalidrawElement[] = [];
|
||||
const oldIdToDuplicatedId = new Map();
|
||||
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
||||
const duplicatedElements: ExcalidrawElement[] = [];
|
||||
const origElements: ExcalidrawElement[] = [];
|
||||
const origIdToDuplicateId = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement["id"]
|
||||
>();
|
||||
const duplicateIdToOrigElement = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement
|
||||
>();
|
||||
const duplicateElementsMap = new Map<string, ExcalidrawElement>();
|
||||
const elementsMap = arrayToMap(elements) as ElementsMap;
|
||||
const _idsOfElementsToDuplicate =
|
||||
opts.type === "in-place"
|
||||
|
@ -188,7 +182,7 @@ export const duplicateElements = (
|
|||
|
||||
elements = normalizeElementOrder(elements);
|
||||
|
||||
const elementsWithClones: ExcalidrawElement[] = elements.slice();
|
||||
const elementsWithDuplicates: ExcalidrawElement[] = elements.slice();
|
||||
|
||||
// helper functions
|
||||
// -------------------------------------------------------------------------
|
||||
|
@ -214,17 +208,17 @@ export const duplicateElements = (
|
|||
appState.editingGroupId,
|
||||
groupIdMap,
|
||||
element,
|
||||
opts.overrides?.(element),
|
||||
opts.randomizeSeed,
|
||||
);
|
||||
|
||||
processedIds.set(newElement.id, true);
|
||||
|
||||
duplicatedElementsMap.set(newElement.id, newElement);
|
||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
||||
duplicateElementsMap.set(newElement.id, newElement);
|
||||
origIdToDuplicateId.set(element.id, newElement.id);
|
||||
duplicateIdToOrigElement.set(newElement.id, element);
|
||||
|
||||
oldElements.push(element);
|
||||
newElements.push(newElement);
|
||||
origElements.push(element);
|
||||
duplicatedElements.push(newElement);
|
||||
|
||||
acc.push(newElement);
|
||||
return acc;
|
||||
|
@ -248,21 +242,12 @@ export const duplicateElements = (
|
|||
return;
|
||||
}
|
||||
|
||||
if (reverseOrder && index < 1) {
|
||||
elementsWithClones.unshift(...castArray(elements));
|
||||
if (index > elementsWithDuplicates.length - 1) {
|
||||
elementsWithDuplicates.push(...castArray(elements));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reverseOrder && index > elementsWithClones.length - 1) {
|
||||
elementsWithClones.push(...castArray(elements));
|
||||
return;
|
||||
}
|
||||
|
||||
elementsWithClones.splice(
|
||||
index + (reverseOrder ? 0 : 1),
|
||||
0,
|
||||
...castArray(elements),
|
||||
);
|
||||
elementsWithDuplicates.splice(index + 1, 0, ...castArray(elements));
|
||||
};
|
||||
|
||||
const frameIdsToDuplicate = new Set(
|
||||
|
@ -294,13 +279,9 @@ export const duplicateElements = (
|
|||
: [element],
|
||||
);
|
||||
|
||||
const targetIndex = reverseOrder
|
||||
? elementsWithClones.findIndex((el) => {
|
||||
return el.groupIds?.includes(groupId);
|
||||
})
|
||||
: findLastIndex(elementsWithClones, (el) => {
|
||||
return el.groupIds?.includes(groupId);
|
||||
});
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.groupIds?.includes(groupId);
|
||||
});
|
||||
|
||||
insertBeforeOrAfterIndex(targetIndex, copyElements(groupElements));
|
||||
continue;
|
||||
|
@ -318,7 +299,7 @@ export const duplicateElements = (
|
|||
|
||||
const frameChildren = getFrameChildren(elements, frameId);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.frameId === frameId || el.id === frameId;
|
||||
});
|
||||
|
||||
|
@ -335,7 +316,7 @@ export const duplicateElements = (
|
|||
if (hasBoundTextElement(element)) {
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return (
|
||||
el.id === element.id ||
|
||||
("containerId" in el && el.containerId === element.id)
|
||||
|
@ -344,7 +325,7 @@ export const duplicateElements = (
|
|||
|
||||
if (boundTextElement) {
|
||||
insertBeforeOrAfterIndex(
|
||||
targetIndex + (reverseOrder ? -1 : 0),
|
||||
targetIndex,
|
||||
copyElements([element, boundTextElement]),
|
||||
);
|
||||
} else {
|
||||
|
@ -357,7 +338,7 @@ export const duplicateElements = (
|
|||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
|
||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
||||
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||
return el.id === element.id || el.id === container?.id;
|
||||
});
|
||||
|
||||
|
@ -377,7 +358,7 @@ export const duplicateElements = (
|
|||
// -------------------------------------------------------------------------
|
||||
|
||||
insertBeforeOrAfterIndex(
|
||||
findLastIndex(elementsWithClones, (el) => el.id === element.id),
|
||||
findLastIndex(elementsWithDuplicates, (el) => el.id === element.id),
|
||||
copyElements(element),
|
||||
);
|
||||
}
|
||||
|
@ -385,28 +366,38 @@ export const duplicateElements = (
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
fixDuplicatedBindingsAfterDuplication(
|
||||
newElements,
|
||||
oldIdToDuplicatedId,
|
||||
duplicatedElementsMap as NonDeletedSceneElementsMap,
|
||||
duplicatedElements,
|
||||
origIdToDuplicateId,
|
||||
duplicateElementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
|
||||
if (reverseOrder) {
|
||||
fixReversedBindings(
|
||||
_idsOfElementsToDuplicate,
|
||||
elementsWithClones,
|
||||
oldIdToDuplicatedId,
|
||||
);
|
||||
}
|
||||
|
||||
bindElementsToFramesAfterDuplication(
|
||||
elementsWithClones,
|
||||
oldElements,
|
||||
oldIdToDuplicatedId,
|
||||
elementsWithDuplicates,
|
||||
origElements,
|
||||
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 {
|
||||
newElements,
|
||||
elementsWithClones,
|
||||
duplicatedElements,
|
||||
duplicateElementsMap,
|
||||
elementsWithDuplicates,
|
||||
origIdToDuplicateId,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -50,7 +50,6 @@ import { isBindableElement } from "./typeChecks";
|
|||
import {
|
||||
type ExcalidrawElbowArrowElement,
|
||||
type NonDeletedSceneElementsMap,
|
||||
type SceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||
|
@ -887,7 +886,7 @@ export const updateElbowArrowPoints = (
|
|||
elementsMap: NonDeletedSceneElementsMap,
|
||||
updates: {
|
||||
points?: readonly LocalPoint[];
|
||||
fixedSegments?: FixedSegment[] | null;
|
||||
fixedSegments?: readonly FixedSegment[] | null;
|
||||
startBinding?: FixedPointBinding | null;
|
||||
endBinding?: FixedPointBinding | null;
|
||||
},
|
||||
|
@ -1273,14 +1272,12 @@ const getElbowArrowData = (
|
|||
const startHeading = getBindPointHeading(
|
||||
startGlobalPoint,
|
||||
endGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredStartElement,
|
||||
origStartGlobalPoint,
|
||||
);
|
||||
const endHeading = getBindPointHeading(
|
||||
endGlobalPoint,
|
||||
startGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredEndElement,
|
||||
origEndGlobalPoint,
|
||||
);
|
||||
|
@ -2250,7 +2247,6 @@ const getGlobalPoint = (
|
|||
const getBindPointHeading = (
|
||||
p: GlobalPoint,
|
||||
otherPoint: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||
origPoint: GlobalPoint,
|
||||
): Heading =>
|
||||
|
@ -2268,7 +2264,6 @@ const getBindPointHeading = (
|
|||
number,
|
||||
],
|
||||
),
|
||||
elementsMap,
|
||||
origPoint,
|
||||
);
|
||||
|
||||
|
|
|
@ -39,6 +39,8 @@ import {
|
|||
type OrderedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
type LinkDirection = "up" | "right" | "down" | "left";
|
||||
|
||||
const VERTICAL_OFFSET = 100;
|
||||
|
@ -236,10 +238,11 @@ const getOffsets = (
|
|||
|
||||
const addNewNode = (
|
||||
element: ExcalidrawFlowchartNodeElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: AppState,
|
||||
direction: LinkDirection,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const successors = getSuccessors(element, elementsMap, direction);
|
||||
const predeccessors = getPredecessors(element, elementsMap, direction);
|
||||
|
||||
|
@ -274,9 +277,9 @@ const addNewNode = (
|
|||
const bindingArrow = createBindingArrow(
|
||||
element,
|
||||
nextNode,
|
||||
elementsMap,
|
||||
direction,
|
||||
appState,
|
||||
scene,
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -287,9 +290,9 @@ const addNewNode = (
|
|||
|
||||
export const addNewNodes = (
|
||||
startNode: ExcalidrawFlowchartNodeElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: AppState,
|
||||
direction: LinkDirection,
|
||||
scene: Scene,
|
||||
numberOfNodes: number,
|
||||
) => {
|
||||
// always start from 0 and distribute evenly
|
||||
|
@ -352,9 +355,9 @@ export const addNewNodes = (
|
|||
const bindingArrow = createBindingArrow(
|
||||
startNode,
|
||||
nextNode,
|
||||
elementsMap,
|
||||
direction,
|
||||
appState,
|
||||
scene,
|
||||
);
|
||||
|
||||
newNodes.push(nextNode);
|
||||
|
@ -367,9 +370,9 @@ export const addNewNodes = (
|
|||
const createBindingArrow = (
|
||||
startBindingElement: ExcalidrawFlowchartNodeElement,
|
||||
endBindingElement: ExcalidrawFlowchartNodeElement,
|
||||
elementsMap: ElementsMap,
|
||||
direction: LinkDirection,
|
||||
appState: AppState,
|
||||
scene: Scene,
|
||||
) => {
|
||||
let startX: number;
|
||||
let startY: number;
|
||||
|
@ -440,18 +443,10 @@ const createBindingArrow = (
|
|||
elbowed: true,
|
||||
});
|
||||
|
||||
bindLinearElement(
|
||||
bindingArrow,
|
||||
startBindingElement,
|
||||
"start",
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
bindLinearElement(
|
||||
bindingArrow,
|
||||
endBindingElement,
|
||||
"end",
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
bindLinearElement(bindingArrow, startBindingElement, "start", scene);
|
||||
bindLinearElement(bindingArrow, endBindingElement, "end", scene);
|
||||
|
||||
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
||||
changedElements.set(
|
||||
|
@ -467,7 +462,7 @@ const createBindingArrow = (
|
|||
bindingArrow as OrderedExcalidrawElement,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(bindingArrow, [
|
||||
LinearElementEditor.movePoints(bindingArrow, scene, [
|
||||
{
|
||||
index: 1,
|
||||
point: bindingArrow.points[1],
|
||||
|
@ -632,16 +627,17 @@ export class FlowChartCreator {
|
|||
|
||||
createNodes(
|
||||
startNode: ExcalidrawFlowchartNodeElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: AppState,
|
||||
direction: LinkDirection,
|
||||
scene: Scene,
|
||||
) {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
if (direction !== this.direction) {
|
||||
const { nextNode, bindingArrow } = addNewNode(
|
||||
startNode,
|
||||
elementsMap,
|
||||
appState,
|
||||
direction,
|
||||
scene,
|
||||
);
|
||||
|
||||
this.numberOfNodes = 1;
|
||||
|
@ -652,9 +648,9 @@ export class FlowChartCreator {
|
|||
this.numberOfNodes += 1;
|
||||
const newNodes = addNewNodes(
|
||||
startNode,
|
||||
elementsMap,
|
||||
appState,
|
||||
direction,
|
||||
scene,
|
||||
this.numberOfNodes,
|
||||
);
|
||||
|
||||
|
@ -682,13 +678,9 @@ export class FlowChartCreator {
|
|||
)
|
||||
) {
|
||||
this.pendingNodes = this.pendingNodes.map((node) =>
|
||||
mutateElement(
|
||||
node,
|
||||
{
|
||||
frameId: startNode.frameId,
|
||||
},
|
||||
false,
|
||||
),
|
||||
mutateElement(node, elementsMap, {
|
||||
frameId: startNode.frameId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { getBoundTextElement } from "./textElement";
|
|||
import { hasBoundTextElement } from "./typeChecks";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
FractionalIndex,
|
||||
OrderedExcalidrawElement,
|
||||
|
@ -152,9 +153,10 @@ export const orderByFractionalIndex = (
|
|||
*/
|
||||
export const syncMovedIndices = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
movedElements: Map<string, ExcalidrawElement>,
|
||||
movedElements: ElementsMap,
|
||||
): OrderedExcalidrawElement[] => {
|
||||
try {
|
||||
const elementsMap = arrayToMap(elements);
|
||||
const indicesGroups = getMovedIndicesGroups(elements, 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
|
||||
for (const [element, update] of elementsUpdates) {
|
||||
mutateElement(element, update, false);
|
||||
mutateElement(element, elementsMap, update);
|
||||
}
|
||||
} catch (e) {
|
||||
// fallback to default sync
|
||||
|
@ -194,10 +196,12 @@ export const syncMovedIndices = (
|
|||
export const syncInvalidIndices = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): OrderedExcalidrawElement[] => {
|
||||
const elementsMap = arrayToMap(elements);
|
||||
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||
|
||||
for (const [element, update] of elementsUpdates) {
|
||||
mutateElement(element, update, false);
|
||||
mutateElement(element, elementsMap, update);
|
||||
}
|
||||
|
||||
return elements as OrderedExcalidrawElement[];
|
||||
|
@ -210,7 +214,7 @@ export const syncInvalidIndices = (
|
|||
*/
|
||||
const getMovedIndicesGroups = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
movedElements: Map<string, ExcalidrawElement>,
|
||||
movedElements: ElementsMap,
|
||||
) => {
|
||||
const indicesGroups: number[][] = [];
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
|
|||
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
||||
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
||||
|
||||
import type { ExcalidrawElementsIncludingDeleted } from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type {
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
|
@ -29,6 +27,8 @@ import {
|
|||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import type { ExcalidrawElementsIncludingDeleted } from "./Scene";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
|
@ -41,30 +41,24 @@ import type {
|
|||
// --------------------------- Frame State ------------------------------------
|
||||
export const bindElementsToFramesAfterDuplication = (
|
||||
nextElements: readonly ExcalidrawElement[],
|
||||
oldElements: readonly ExcalidrawElement[],
|
||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||
origElements: readonly ExcalidrawElement[],
|
||||
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||
) => {
|
||||
const nextElementMap = arrayToMap(nextElements) as Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement
|
||||
>;
|
||||
|
||||
for (const element of oldElements) {
|
||||
for (const element of origElements) {
|
||||
if (element.frameId) {
|
||||
// use its frameId to get the new frameId
|
||||
const nextElementId = oldIdToDuplicatedId.get(element.id);
|
||||
const nextFrameId = oldIdToDuplicatedId.get(element.frameId);
|
||||
if (nextElementId) {
|
||||
const nextElement = nextElementMap.get(nextElementId);
|
||||
if (nextElement) {
|
||||
mutateElement(
|
||||
nextElement,
|
||||
{
|
||||
frameId: nextFrameId ?? element.frameId,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
const nextElementId = origIdToDuplicateId.get(element.id);
|
||||
const nextFrameId = origIdToDuplicateId.get(element.frameId);
|
||||
const nextElement = nextElementId && nextElementMap.get(nextElementId);
|
||||
if (nextElement) {
|
||||
mutateElement(nextElement, nextElementMap, {
|
||||
frameId: nextFrameId ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -567,13 +561,9 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
|||
}
|
||||
|
||||
for (const element of finalElementsToAdd) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
frameId: frame.id,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(element, elementsMap, {
|
||||
frameId: frame.id,
|
||||
});
|
||||
}
|
||||
|
||||
return allElements;
|
||||
|
@ -611,13 +601,9 @@ export const removeElementsFromFrame = (
|
|||
}
|
||||
|
||||
for (const [, element] of _elementsToRemove) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
frameId: null,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(element, elementsMap, {
|
||||
frameId: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -20,10 +20,6 @@ import {
|
|||
tupleToCoors,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { Store } from "@excalidraw/excalidraw/store";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
@ -50,10 +46,8 @@ import {
|
|||
getMinMaxXYFromCurvePathOps,
|
||||
} from "./bounds";
|
||||
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
|
||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import {
|
||||
isBindingElement,
|
||||
|
@ -73,6 +67,8 @@ import {
|
|||
|
||||
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type {
|
||||
NonDeleted,
|
||||
|
@ -84,7 +80,6 @@ import type {
|
|||
ElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
FixedPointBinding,
|
||||
SceneElementsMap,
|
||||
FixedSegment,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
|
@ -127,15 +122,17 @@ export class LinearElementEditor {
|
|||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||
public readonly elbowed: boolean;
|
||||
|
||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||
constructor(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
this.elementId = element.id as string & {
|
||||
_brand: "excalidrawLinearElementId";
|
||||
};
|
||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||
console.error("Linear element is not normalized", Error().stack);
|
||||
LinearElementEditor.normalizePoints(element);
|
||||
LinearElementEditor.normalizePoints(element, elementsMap);
|
||||
}
|
||||
|
||||
this.selectedPointsIndices = null;
|
||||
this.lastUncommittedPoint = null;
|
||||
this.isDragging = false;
|
||||
|
@ -309,7 +306,7 @@ export class LinearElementEditor {
|
|||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, [
|
||||
LinearElementEditor.movePoints(element, scene, [
|
||||
{
|
||||
index: selectedIndex,
|
||||
point: pointFrom(
|
||||
|
@ -333,6 +330,7 @@ export class LinearElementEditor {
|
|||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
scene,
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
const newPointPosition: LocalPoint =
|
||||
pointIndex === lastClickedPoint
|
||||
|
@ -358,7 +356,7 @@ export class LinearElementEditor {
|
|||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
handleBindTextResize(element, elementsMap, false);
|
||||
handleBindTextResize(element, scene, false);
|
||||
}
|
||||
|
||||
// suggest bindings for first and last point if selected
|
||||
|
@ -453,7 +451,7 @@ export class LinearElementEditor {
|
|||
selectedPoint === element.points.length - 1
|
||||
) {
|
||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||
LinearElementEditor.movePoints(element, [
|
||||
LinearElementEditor.movePoints(element, scene, [
|
||||
{
|
||||
index: selectedPoint,
|
||||
point:
|
||||
|
@ -795,7 +793,7 @@ export class LinearElementEditor {
|
|||
);
|
||||
} else if (event.altKey && appState.editingLinearElement) {
|
||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
points: [
|
||||
...element.points,
|
||||
LinearElementEditor.createPointAt(
|
||||
|
@ -861,7 +859,6 @@ export class LinearElementEditor {
|
|||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
@ -934,13 +931,13 @@ export class LinearElementEditor {
|
|||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
app: AppClassProperties,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
): LinearElementEditor | null {
|
||||
const appState = app.state;
|
||||
if (!appState.editingLinearElement) {
|
||||
return null;
|
||||
}
|
||||
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
if (!element) {
|
||||
return appState.editingLinearElement;
|
||||
|
@ -951,7 +948,9 @@ export class LinearElementEditor {
|
|||
|
||||
if (!event.altKey) {
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
||||
LinearElementEditor.deletePoints(element, app.scene, [
|
||||
points.length - 1,
|
||||
]);
|
||||
}
|
||||
return {
|
||||
...appState.editingLinearElement,
|
||||
|
@ -989,14 +988,14 @@ export class LinearElementEditor {
|
|||
}
|
||||
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.movePoints(element, [
|
||||
LinearElementEditor.movePoints(element, app.scene, [
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: newPoint,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
|
||||
LinearElementEditor.addPoints(element, app.scene, [{ point: newPoint }]);
|
||||
}
|
||||
return {
|
||||
...appState.editingLinearElement,
|
||||
|
@ -1160,23 +1159,26 @@ export class LinearElementEditor {
|
|||
y: element.y + offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
// element-mutating methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
||||
static normalizePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
mutateElement(
|
||||
element,
|
||||
elementsMap,
|
||||
LinearElementEditor.getNormalizedPoints(element),
|
||||
);
|
||||
}
|
||||
|
||||
static duplicateSelectedPoints(
|
||||
appState: AppState,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
): AppState {
|
||||
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
|
||||
invariant(
|
||||
appState.editingLinearElement,
|
||||
"Not currently editing a linear element",
|
||||
);
|
||||
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
|
||||
|
@ -1219,13 +1221,13 @@ export class LinearElementEditor {
|
|||
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,
|
||||
// potentially expanding the bounding box
|
||||
if (pointAddedToEnd) {
|
||||
const lastPoint = element.points[element.points.length - 1];
|
||||
LinearElementEditor.movePoints(element, [
|
||||
LinearElementEditor.movePoints(element, scene, [
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||
|
@ -1244,6 +1246,7 @@ export class LinearElementEditor {
|
|||
|
||||
static deletePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
pointIndices: readonly number[],
|
||||
) {
|
||||
let offsetX = 0;
|
||||
|
@ -1274,28 +1277,41 @@ export class LinearElementEditor {
|
|||
return acc;
|
||||
}, []);
|
||||
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||
LinearElementEditor._updatePoints(
|
||||
element,
|
||||
scene,
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
);
|
||||
}
|
||||
|
||||
static addPoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
targetPoints: { point: LocalPoint }[],
|
||||
) {
|
||||
const offsetX = 0;
|
||||
const offsetY = 0;
|
||||
|
||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||
LinearElementEditor._updatePoints(
|
||||
element,
|
||||
scene,
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
);
|
||||
}
|
||||
|
||||
static movePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
||||
otherUpdates?: {
|
||||
startBinding?: PointBinding | null;
|
||||
endBinding?: PointBinding | null;
|
||||
},
|
||||
sceneElementsMap?: NonDeletedSceneElementsMap,
|
||||
) {
|
||||
const { points } = element;
|
||||
|
||||
|
@ -1329,6 +1345,7 @@ export class LinearElementEditor {
|
|||
|
||||
LinearElementEditor._updatePoints(
|
||||
element,
|
||||
scene,
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
|
@ -1339,7 +1356,6 @@ export class LinearElementEditor {
|
|||
dragging || targetPoint.isDragging === true,
|
||||
false,
|
||||
),
|
||||
sceneElementsMap,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1394,8 +1410,9 @@ export class LinearElementEditor {
|
|||
pointerCoords: PointerCoords,
|
||||
app: AppClassProperties,
|
||||
snapToGrid: boolean,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
elementsMap,
|
||||
|
@ -1425,9 +1442,7 @@ export class LinearElementEditor {
|
|||
...element.points.slice(segmentMidpoint.index!),
|
||||
];
|
||||
|
||||
mutateElement(element, {
|
||||
points,
|
||||
});
|
||||
scene.mutateElement(element, { points });
|
||||
|
||||
ret.pointerDownState = {
|
||||
...linearElementEditor.pointerDownState,
|
||||
|
@ -1443,6 +1458,7 @@ export class LinearElementEditor {
|
|||
|
||||
private static _updatePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
nextPoints: readonly LocalPoint[],
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
|
@ -1479,28 +1495,10 @@ export class LinearElementEditor {
|
|||
|
||||
updates.points = Array.from(nextPoints);
|
||||
|
||||
if (!options?.sceneElementsMap || Scene.getScene(element)) {
|
||||
mutateElement(element, updates, true, {
|
||||
isDragging: options?.isDragging,
|
||||
});
|
||||
} else {
|
||||
// The element is not in the scene, so we need to use the provided
|
||||
// scene map.
|
||||
Object.assign(element, {
|
||||
...updates,
|
||||
angle: 0 as Radians,
|
||||
|
||||
...updateElbowArrowPoints(
|
||||
element,
|
||||
options.sceneElementsMap,
|
||||
updates,
|
||||
{
|
||||
isDragging: options?.isDragging,
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
bumpVersion(element);
|
||||
scene.mutateElement(element, updates, {
|
||||
informMutation: true,
|
||||
isDragging: options?.isDragging ?? false,
|
||||
});
|
||||
} else {
|
||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||
const prevCoords = getElementPointsCoords(element, element.points);
|
||||
|
@ -1515,7 +1513,7 @@ export class LinearElementEditor {
|
|||
pointFrom(dX, dY),
|
||||
element.angle,
|
||||
);
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
...otherUpdates,
|
||||
points: nextPoints,
|
||||
x: element.x + rotated[0],
|
||||
|
@ -1574,7 +1572,7 @@ export class LinearElementEditor {
|
|||
elementsMap,
|
||||
);
|
||||
if (points.length < 2) {
|
||||
mutateElement(boundTextElement, { isDeleted: true });
|
||||
mutateElement(boundTextElement, elementsMap, { isDeleted: true });
|
||||
}
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
@ -1781,8 +1779,9 @@ export class LinearElementEditor {
|
|||
index: number,
|
||||
x: number,
|
||||
y: number,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
): LinearElementEditor {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElement.elementId,
|
||||
elementsMap,
|
||||
|
@ -1825,7 +1824,7 @@ export class LinearElementEditor {
|
|||
.map((segment) => segment.index)
|
||||
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
fixedSegments: nextFixedSegments,
|
||||
});
|
||||
|
||||
|
@ -1859,14 +1858,14 @@ export class LinearElementEditor {
|
|||
|
||||
static deleteFixedSegment(
|
||||
element: ExcalidrawElbowArrowElement,
|
||||
scene: Scene,
|
||||
index: number,
|
||||
): void {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
fixedSegments: element.fixedSegments?.filter(
|
||||
(segment) => segment.index !== index,
|
||||
),
|
||||
});
|
||||
mutateElement(element, {}, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,13 +2,8 @@ import {
|
|||
getSizeFromPoints,
|
||||
randomInteger,
|
||||
getUpdatedTimestamp,
|
||||
toBrandedType,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
@ -16,35 +11,42 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
|||
import { ShapeCache } from "./ShapeCache";
|
||||
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
|
||||
import { isElbowArrow } from "./typeChecks";
|
||||
|
||||
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types";
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElbowArrowElement,
|
||||
ExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
"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
|
||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||
/**
|
||||
* 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
|
||||
* 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>>(
|
||||
element: TElement,
|
||||
elementsMap: ElementsMap,
|
||||
updates: ElementUpdate<TElement>,
|
||||
informMutation = true,
|
||||
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;
|
||||
},
|
||||
): TElement => {
|
||||
) => {
|
||||
let didChange = false;
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
const { points, fixedSegments, fileId, startBinding, endBinding } =
|
||||
const { points, fixedSegments, startBinding, endBinding, fileId } =
|
||||
updates as any;
|
||||
|
||||
if (
|
||||
|
@ -55,10 +57,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||
typeof startBinding !== "undefined" ||
|
||||
typeof endBinding !== "undefined") // manual binding to element
|
||||
) {
|
||||
const elementsMap = toBrandedType<NonDeletedSceneElementsMap>(
|
||||
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
||||
);
|
||||
|
||||
updates = {
|
||||
...updates,
|
||||
angle: 0 as Radians,
|
||||
|
@ -68,16 +66,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||
x: updates.x || element.x,
|
||||
y: updates.y || element.y,
|
||||
},
|
||||
elementsMap,
|
||||
{
|
||||
fixedSegments,
|
||||
points,
|
||||
startBinding,
|
||||
endBinding,
|
||||
},
|
||||
{
|
||||
isDragging: options?.isDragging,
|
||||
},
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
updates as ElementUpdate<ExcalidrawElbowArrowElement>,
|
||||
options,
|
||||
),
|
||||
};
|
||||
} else if (typeof points !== "undefined") {
|
||||
|
@ -150,10 +141,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||
element.versionNonce = randomInteger();
|
||||
element.updated = getUpdatedTimestamp();
|
||||
|
||||
if (informMutation) {
|
||||
Scene.getScene(element)?.triggerUpdate();
|
||||
}
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@ import type {
|
|||
ExcalidrawIframeElement,
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
FixedSegment,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
|
||||
|
@ -478,7 +477,7 @@ export const newArrowElement = <T extends boolean>(
|
|||
endArrowhead?: Arrowhead | null;
|
||||
points?: ExcalidrawArrowElement["points"];
|
||||
elbowed?: T;
|
||||
fixedSegments?: FixedSegment[] | null;
|
||||
fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"] | null;
|
||||
} & ElementConstructorOpts,
|
||||
): T extends true
|
||||
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||
|
|
|
@ -17,8 +17,6 @@ import {
|
|||
|
||||
import type { GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
@ -32,7 +30,6 @@ import {
|
|||
getElementBounds,
|
||||
} from "./bounds";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getBoundTextElementId,
|
||||
|
@ -60,6 +57,8 @@ import {
|
|||
|
||||
import { isInGroup } from "./groups";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { BoundingBox } from "./bounds";
|
||||
import type {
|
||||
MaybeTransformHandleType,
|
||||
|
@ -74,7 +73,6 @@ import type {
|
|||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
SceneElementsMap,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
|
||||
|
@ -83,7 +81,6 @@ export const transformElements = (
|
|||
originalElements: PointerDownState["originalElements"],
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: SceneElementsMap,
|
||||
scene: Scene,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
|
@ -93,31 +90,31 @@ export const transformElements = (
|
|||
centerX: number,
|
||||
centerY: number,
|
||||
): boolean => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
if (transformHandleType === "rotation") {
|
||||
if (!isElbowArrow(element)) {
|
||||
rotateSingleElement(
|
||||
element,
|
||||
elementsMap,
|
||||
scene,
|
||||
pointerX,
|
||||
pointerY,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
);
|
||||
updateBoundElements(element, elementsMap);
|
||||
updateBoundElements(element, scene);
|
||||
}
|
||||
} else if (isTextElement(element) && transformHandleType) {
|
||||
resizeSingleTextElement(
|
||||
originalElements,
|
||||
element,
|
||||
elementsMap,
|
||||
scene,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
updateBoundElements(element, elementsMap);
|
||||
updateBoundElements(element, scene);
|
||||
return true;
|
||||
} else if (transformHandleType) {
|
||||
const elementId = selectedElements[0].id;
|
||||
|
@ -129,8 +126,6 @@ export const transformElements = (
|
|||
getNextSingleWidthAndHeightFromPointer(
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElements,
|
||||
transformHandleType,
|
||||
pointerX,
|
||||
pointerY,
|
||||
|
@ -145,8 +140,8 @@ export const transformElements = (
|
|||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElements,
|
||||
scene,
|
||||
transformHandleType,
|
||||
{
|
||||
shouldMaintainAspectRatio,
|
||||
|
@ -161,7 +156,6 @@ export const transformElements = (
|
|||
rotateMultipleElements(
|
||||
originalElements,
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
scene,
|
||||
pointerX,
|
||||
pointerY,
|
||||
|
@ -210,13 +204,15 @@ export const transformElements = (
|
|||
|
||||
const rotateSingleElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
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 cy = (y1 + y2) / 2;
|
||||
let angle: Radians;
|
||||
|
@ -233,13 +229,13 @@ const rotateSingleElement = (
|
|||
}
|
||||
const boundTextElementId = getBoundTextElementId(element);
|
||||
|
||||
mutateElement(element, { angle });
|
||||
scene.mutateElement(element, { angle });
|
||||
if (boundTextElementId) {
|
||||
const textElement =
|
||||
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
||||
|
||||
if (textElement && !isArrowElement(element)) {
|
||||
mutateElement(textElement, { angle });
|
||||
scene.mutateElement(textElement, { angle });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -289,12 +285,13 @@ export const measureFontSizeFromWidth = (
|
|||
const resizeSingleTextElement = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
transformHandleType: TransformHandleDirection,
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
|
@ -393,7 +390,7 @@ const resizeSingleTextElement = (
|
|||
);
|
||||
const [nextX, nextY] = newTopLeft;
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
fontSize: metrics.size,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
|
@ -508,14 +505,13 @@ const resizeSingleTextElement = (
|
|||
autoResize: false,
|
||||
};
|
||||
|
||||
mutateElement(element, resizedElement);
|
||||
scene.mutateElement(element, resizedElement);
|
||||
}
|
||||
};
|
||||
|
||||
const rotateMultipleElements = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: SceneElementsMap,
|
||||
scene: Scene,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
|
@ -523,6 +519,7 @@ const rotateMultipleElements = (
|
|||
centerX: number,
|
||||
centerY: number,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
let centerAngle =
|
||||
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
||||
if (shouldRotateWithDiscreteAngle) {
|
||||
|
@ -543,38 +540,30 @@ const rotateMultipleElements = (
|
|||
(centerAngle + origAngle - element.angle) as Radians,
|
||||
);
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
// Needed to re-route the arrow
|
||||
mutateElement(element, {
|
||||
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||
});
|
||||
} else {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
const updates = isElbowArrow(element)
|
||||
? {
|
||||
// Needed to re-route the arrow
|
||||
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||
}
|
||||
: {
|
||||
x: element.x + (rotatedCX - cx),
|
||||
y: element.y + (rotatedCY - cy),
|
||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
updateBoundElements(element, elementsMap, {
|
||||
scene.mutateElement(element, updates);
|
||||
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: elements,
|
||||
});
|
||||
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
if (boundText && !isArrowElement(element)) {
|
||||
mutateElement(
|
||||
boundText,
|
||||
{
|
||||
x: boundText.x + (rotatedCX - cx),
|
||||
y: boundText.y + (rotatedCY - cy),
|
||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(boundText, {
|
||||
x: boundText.x + (rotatedCX - cx),
|
||||
y: boundText.y + (rotatedCY - cy),
|
||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -819,8 +808,8 @@ export const resizeSingleElement = (
|
|||
nextHeight: number,
|
||||
latestElement: ExcalidrawElement,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
handleDirection: TransformHandleDirection,
|
||||
{
|
||||
shouldInformMutation = true,
|
||||
|
@ -833,6 +822,7 @@ export const resizeSingleElement = (
|
|||
} = {},
|
||||
) => {
|
||||
let boundTextFont: { fontSize?: number } = {};
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
|
||||
if (boundTextElement) {
|
||||
|
@ -932,7 +922,7 @@ export const resizeSingleElement = (
|
|||
}
|
||||
|
||||
if ("scale" in latestElement && "scale" in origElement) {
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
scale: [
|
||||
// defaulting because scaleX/Y can be 0/-0
|
||||
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
|
||||
|
@ -967,21 +957,24 @@ export const resizeSingleElement = (
|
|||
...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
|
||||
newSize: { width: nextWidth, height: nextHeight },
|
||||
});
|
||||
|
||||
if (boundTextElement && boundTextFont != null) {
|
||||
mutateElement(boundTextElement, {
|
||||
scene.mutateElement(boundTextElement, {
|
||||
fontSize: boundTextFont.fontSize,
|
||||
});
|
||||
}
|
||||
handleBindTextResize(
|
||||
latestElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
handleDirection,
|
||||
shouldMaintainAspectRatio,
|
||||
);
|
||||
|
@ -991,8 +984,6 @@ export const resizeSingleElement = (
|
|||
const getNextSingleWidthAndHeightFromPointer = (
|
||||
latestElement: ExcalidrawElement,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
handleDirection: TransformHandleDirection,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
|
@ -1527,27 +1518,24 @@ export const resizeMultipleElements = (
|
|||
} of elementsAndUpdates) {
|
||||
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
|
||||
isDragging: true,
|
||||
});
|
||||
|
||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
newSize: { width, height },
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement && boundTextFontSize) {
|
||||
mutateElement(
|
||||
boundTextElement,
|
||||
{
|
||||
fontSize: boundTextFontSize,
|
||||
angle: isLinearElement(element) ? undefined : angle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
handleBindTextResize(element, elementsMap, handleDirection, true);
|
||||
scene.mutateElement(boundTextElement, {
|
||||
fontSize: boundTextFontSize,
|
||||
angle: isLinearElement(element) ? undefined : angle,
|
||||
});
|
||||
handleBindTextResize(element, scene, handleDirection, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isShallowEqual } from "@excalidraw/common";
|
||||
import { arrayToMap, isShallowEqual } from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
AppState,
|
||||
|
@ -7,13 +7,20 @@ import type {
|
|||
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||
import { isElementInViewport } from "./sizeHelpers";
|
||||
import { isBoundToContainer, isFrameLikeElement } from "./typeChecks";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
} from "./typeChecks";
|
||||
import {
|
||||
elementOverlapsWithFrame,
|
||||
getContainingFrame,
|
||||
getFrameChildren,
|
||||
} from "./frame";
|
||||
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { selectGroupsForSelectedElements } from "./groups";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
|
@ -254,3 +261,49 @@ export const makeNextSelectedElementIds = (
|
|||
|
||||
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,
|
||||
ROUNDNESS,
|
||||
invariant,
|
||||
elementCenterPoint,
|
||||
} from "@excalidraw/common";
|
||||
import {
|
||||
isPoint,
|
||||
|
@ -297,7 +298,7 @@ export const aabbForElement = (
|
|||
midY: element.y + element.height / 2,
|
||||
};
|
||||
|
||||
const center = pointFrom(bbox.midX, bbox.midY);
|
||||
const center = elementCenterPoint(element);
|
||||
const [topLeftX, topLeftY] = pointRotateRads(
|
||||
pointFrom(bbox.minX, bbox.minY),
|
||||
center,
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||
|
@ -170,41 +169,6 @@ export const getLockedLinearCursorAlignSize = (
|
|||
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 = (
|
||||
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
|
||||
): {
|
||||
|
|
|
@ -6,18 +6,22 @@ import {
|
|||
TEXT_ALIGN,
|
||||
VERTICAL_ALIGN,
|
||||
getFontString,
|
||||
isProdEnv,
|
||||
invariant,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
|
||||
import { measureText } from "./textMeasurements";
|
||||
import { wrapText } from "./textWrapping";
|
||||
import {
|
||||
|
@ -26,6 +30,8 @@ import {
|
|||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||
import type {
|
||||
ElementsMap,
|
||||
|
@ -40,17 +46,30 @@ import type {
|
|||
export const redrawTextBoundingBox = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawElement | null,
|
||||
elementsMap: ElementsMap,
|
||||
informMutation = true,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
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 = {
|
||||
x: textElement.x,
|
||||
y: textElement.y,
|
||||
text: textElement.text,
|
||||
width: textElement.width,
|
||||
height: textElement.height,
|
||||
angle: container?.angle ?? textElement.angle,
|
||||
angle: (container
|
||||
? isArrowElement(container)
|
||||
? 0
|
||||
: container.angle
|
||||
: textElement.angle) as Radians,
|
||||
};
|
||||
|
||||
boundTextUpdates.text = textElement.text;
|
||||
|
@ -90,38 +109,43 @@ export const redrawTextBoundingBox = (
|
|||
metrics.height,
|
||||
container.type,
|
||||
);
|
||||
mutateElement(container, { height: nextHeight }, informMutation);
|
||||
scene.mutateElement(container, { height: nextHeight });
|
||||
updateOriginalContainerCache(container.id, nextHeight);
|
||||
}
|
||||
|
||||
if (metrics.width > maxContainerWidth) {
|
||||
const nextWidth = computeContainerDimensionForBoundText(
|
||||
metrics.width,
|
||||
container.type,
|
||||
);
|
||||
mutateElement(container, { width: nextWidth }, informMutation);
|
||||
scene.mutateElement(container, { width: nextWidth });
|
||||
}
|
||||
|
||||
const updatedTextElement = {
|
||||
...textElement,
|
||||
...boundTextUpdates,
|
||||
} as ExcalidrawTextElementWithContainer;
|
||||
|
||||
const { x, y } = computeBoundTextPosition(
|
||||
container,
|
||||
updatedTextElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
boundTextUpdates.x = x;
|
||||
boundTextUpdates.y = y;
|
||||
}
|
||||
|
||||
mutateElement(textElement, boundTextUpdates, informMutation);
|
||||
scene.mutateElement(textElement, boundTextUpdates);
|
||||
};
|
||||
|
||||
export const handleBindTextResize = (
|
||||
container: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
shouldMaintainAspectRatio = false,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const boundTextElementId = getBoundTextElementId(container);
|
||||
if (!boundTextElementId) {
|
||||
return;
|
||||
|
@ -174,20 +198,20 @@ export const handleBindTextResize = (
|
|||
transformHandleType === "n")
|
||||
? container.y - diff
|
||||
: container.y;
|
||||
mutateElement(container, {
|
||||
scene.mutateElement(container, {
|
||||
height: containerHeight,
|
||||
y: updatedY,
|
||||
});
|
||||
}
|
||||
|
||||
mutateElement(textElement, {
|
||||
scene.mutateElement(textElement, {
|
||||
text,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
});
|
||||
|
||||
if (!isArrowElement(container)) {
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
textElement,
|
||||
computeBoundTextPosition(container, textElement, elementsMap),
|
||||
);
|
||||
|
@ -335,7 +359,10 @@ export const getTextElementAngle = (
|
|||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
) => {
|
||||
if (!container || isArrowElement(container)) {
|
||||
if (isArrowElement(container)) {
|
||||
return 0;
|
||||
}
|
||||
if (!container) {
|
||||
return textElement.angle;
|
||||
}
|
||||
return container.angle;
|
||||
|
|
|
@ -119,6 +119,20 @@ export const isElbowArrow = (
|
|||
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 = (
|
||||
elementType: ElementOrToolType,
|
||||
): boolean => {
|
||||
|
@ -271,6 +285,10 @@ export const isBoundToContainer = (
|
|||
);
|
||||
};
|
||||
|
||||
export const isArrowBoundToElement = (element: ExcalidrawArrowElement) => {
|
||||
return !!element.startBinding || !!element.endBinding;
|
||||
};
|
||||
|
||||
export const isUsingAdaptiveRadius = (type: string) =>
|
||||
type === "rectangle" ||
|
||||
type === "embeddable" ||
|
||||
|
|
|
@ -412,3 +412,11 @@ export type NonDeletedSceneElementsMap = Map<
|
|||
export type ElementsMapOrArray =
|
||||
| readonly ExcalidrawElement[]
|
||||
| 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,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import { elementCenterPoint } from "@excalidraw/common";
|
||||
|
||||
import type { Curve, LineSegment } from "@excalidraw/math";
|
||||
|
||||
import { getCornerRadius } from "./shapes";
|
||||
|
@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
|
|||
return [sides, []];
|
||||
}
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const r = rectangle(
|
||||
pointFrom(element.x, element.y),
|
||||
|
@ -254,10 +253,7 @@ export function deconstructDiamondElement(
|
|||
return [[topRight, bottomRight, bottomLeft, topLeft], []];
|
||||
}
|
||||
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
element.x + element.width / 2,
|
||||
element.y + element.height / 2,
|
||||
);
|
||||
const center = elementCenterPoint(element);
|
||||
|
||||
const [top, right, bottom, left]: GlobalPoint[] = [
|
||||
pointFrom(element.x + topX, element.y + topY),
|
||||
|
|
|
@ -2,8 +2,6 @@ import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
|||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import { isFrameLikeElement } from "./typeChecks";
|
||||
|
||||
import { getElementsInGroup } from "./groups";
|
||||
|
@ -12,6 +10,8 @@ import { syncMovedIndices } from "./fractionalIndex";
|
|||
|
||||
import { getSelectedElements } from "./selection";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
||||
|
||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from "react";
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
|
@ -8,7 +7,7 @@ import {
|
|||
isPrimitive,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||
|
||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||
|
||||
|
@ -25,7 +24,6 @@ import {
|
|||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { mutateElement } from "../src/mutateElement";
|
||||
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
||||
|
||||
import type { ExcalidrawLinearElement } from "../src/types";
|
||||
|
@ -63,11 +61,11 @@ describe("duplicating single elements", () => {
|
|||
// @ts-ignore
|
||||
element.__proto__ = { hello: "world" };
|
||||
|
||||
mutateElement(element, {
|
||||
mutateElement(element, new Map(), {
|
||||
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);
|
||||
|
||||
|
@ -173,7 +171,7 @@ describe("duplicating multiple elements", () => {
|
|||
// -------------------------------------------------------------------------
|
||||
|
||||
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
||||
const { newElements: clonedElements } = duplicateElements({
|
||||
const { duplicatedElements } = duplicateElements({
|
||||
type: "everything",
|
||||
elements: origElements,
|
||||
});
|
||||
|
@ -181,10 +179,10 @@ describe("duplicating multiple elements", () => {
|
|||
// generic id in-equality checks
|
||||
// --------------------------------------------------------------------------
|
||||
expect(origElements.map((e) => e.type)).toEqual(
|
||||
clonedElements.map((e) => e.type),
|
||||
duplicatedElements.map((e) => e.type),
|
||||
);
|
||||
origElements.forEach((origElement, idx) => {
|
||||
const clonedElement = clonedElements[idx];
|
||||
const clonedElement = duplicatedElements[idx];
|
||||
expect(origElement).toEqual(
|
||||
expect.objectContaining({
|
||||
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",
|
||||
) as ExcalidrawLinearElement[];
|
||||
|
||||
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
|
||||
clonedElements as any as typeof origElements;
|
||||
duplicatedElements as any as typeof origElements;
|
||||
|
||||
expect(clonedText1.containerId).toBe(clonedRectangle.id);
|
||||
expect(
|
||||
|
@ -327,10 +325,10 @@ describe("duplicating multiple elements", () => {
|
|||
// -------------------------------------------------------------------------
|
||||
|
||||
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
||||
const { newElements: clonedElements } = duplicateElements({
|
||||
const duplicatedElements = duplicateElements({
|
||||
type: "everything",
|
||||
elements: origElements,
|
||||
}) as any as { newElements: typeof origElements };
|
||||
}).duplicatedElements as any as typeof origElements;
|
||||
|
||||
const [
|
||||
clonedRectangle,
|
||||
|
@ -338,7 +336,7 @@ describe("duplicating multiple elements", () => {
|
|||
clonedArrow1,
|
||||
clonedArrow2,
|
||||
clonedArrow3,
|
||||
] = clonedElements;
|
||||
] = duplicatedElements;
|
||||
|
||||
expect(clonedRectangle.boundElements).toEqual([
|
||||
{ id: clonedArrow1.id, type: "arrow" },
|
||||
|
@ -374,12 +372,12 @@ describe("duplicating multiple elements", () => {
|
|||
});
|
||||
|
||||
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
||||
const { newElements: clonedElements } = duplicateElements({
|
||||
const { duplicatedElements } = duplicateElements({
|
||||
type: "everything",
|
||||
elements: origElements,
|
||||
}) as any as { newElements: typeof origElements };
|
||||
});
|
||||
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
||||
clonedElements;
|
||||
duplicatedElements;
|
||||
|
||||
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
||||
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
|
||||
|
@ -399,7 +397,7 @@ describe("duplicating multiple elements", () => {
|
|||
});
|
||||
|
||||
const {
|
||||
newElements: [clonedRectangle1],
|
||||
duplicatedElements: [clonedRectangle1],
|
||||
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
||||
|
||||
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", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
|
@ -503,8 +612,8 @@ describe("duplication z-order", () => {
|
|||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ [ORIG_ID]: rectangle1.id },
|
||||
{ id: rectangle1.id, selected: true },
|
||||
{ id: rectangle1.id },
|
||||
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||
{ id: rectangle2.id },
|
||||
{ id: rectangle3.id },
|
||||
]);
|
||||
|
@ -538,8 +647,8 @@ describe("duplication z-order", () => {
|
|||
assertElements(h.elements, [
|
||||
{ id: rectangle1.id },
|
||||
{ id: rectangle2.id },
|
||||
{ [ORIG_ID]: rectangle3.id },
|
||||
{ id: rectangle3.id, selected: true },
|
||||
{ id: rectangle3.id },
|
||||
{ [ORIG_ID]: rectangle3.id, selected: true },
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -569,8 +678,8 @@ describe("duplication z-order", () => {
|
|||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ [ORIG_ID]: rectangle1.id },
|
||||
{ id: rectangle1.id, selected: true },
|
||||
{ id: rectangle1.id },
|
||||
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||
{ id: rectangle2.id },
|
||||
{ id: rectangle3.id },
|
||||
]);
|
||||
|
@ -605,19 +714,19 @@ describe("duplication z-order", () => {
|
|||
});
|
||||
|
||||
assertElements(h.elements, [
|
||||
{ [ORIG_ID]: rectangle1.id },
|
||||
{ [ORIG_ID]: rectangle2.id },
|
||||
{ [ORIG_ID]: rectangle3.id },
|
||||
{ id: rectangle1.id, selected: true },
|
||||
{ id: rectangle2.id, selected: true },
|
||||
{ id: rectangle3.id, selected: true },
|
||||
{ id: rectangle1.id },
|
||||
{ id: rectangle2.id },
|
||||
{ id: rectangle3.id },
|
||||
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||
{ [ORIG_ID]: rectangle2.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();
|
||||
API.setElements([rectangle, text]);
|
||||
API.setSelectedElements([rectangle, text]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||
|
@ -625,20 +734,20 @@ describe("duplication z-order", () => {
|
|||
});
|
||||
|
||||
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,
|
||||
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();
|
||||
API.setElements([text, rectangle]);
|
||||
API.setSelectedElements([rectangle, text]);
|
||||
API.setSelectedElements([rectangle]);
|
||||
|
||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||
|
@ -646,21 +755,21 @@ describe("duplication z-order", () => {
|
|||
});
|
||||
|
||||
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,
|
||||
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();
|
||||
|
||||
API.setElements([arrow, text]);
|
||||
API.setSelectedElements([arrow, text]);
|
||||
API.setSelectedElements([arrow]);
|
||||
|
||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||
|
@ -668,21 +777,24 @@ describe("duplication z-order", () => {
|
|||
});
|
||||
|
||||
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,
|
||||
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();
|
||||
|
||||
API.setElements([text, arrow]);
|
||||
API.setSelectedElements([arrow, text]);
|
||||
API.setSelectedElements([arrow]);
|
||||
|
||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||
|
@ -690,17 +802,17 @@ describe("duplication z-order", () => {
|
|||
});
|
||||
|
||||
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,
|
||||
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", {
|
||||
x: 0,
|
||||
y: 0,
|
||||
|
@ -722,11 +834,18 @@ describe("duplication z-order", () => {
|
|||
mouse.up(15, 15);
|
||||
});
|
||||
|
||||
expect(window.h.elements).toHaveLength(3);
|
||||
|
||||
const newRect = window.h.elements[0];
|
||||
|
||||
expect(arrow.endBinding?.elementId).toBe(newRect.id);
|
||||
expect(newRect.boundElements?.[0]?.id).toBe(arrow.id);
|
||||
assertElements(h.elements, [
|
||||
{
|
||||
id: rect.id,
|
||||
boundElements: expect.arrayContaining([
|
||||
expect.objectContaining({ id: 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 { 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 { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
||||
|
||||
|
@ -23,6 +22,8 @@ import type { LocalPoint } from "@excalidraw/math";
|
|||
|
||||
import { bindLinearElement } from "../src/binding";
|
||||
|
||||
import Scene from "../src/Scene";
|
||||
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
|
@ -142,7 +143,7 @@ describe("elbow arrow routing", () => {
|
|||
elbowed: true,
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(arrow);
|
||||
mutateElement(arrow, {
|
||||
h.app.scene.mutateElement(arrow, {
|
||||
points: [
|
||||
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
||||
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
||||
|
@ -187,14 +188,14 @@ describe("elbow arrow routing", () => {
|
|||
scene.insertElement(rectangle1);
|
||||
scene.insertElement(rectangle2);
|
||||
scene.insertElement(arrow);
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
bindLinearElement(arrow, rectangle1, "start", elementsMap);
|
||||
bindLinearElement(arrow, rectangle2, "end", elementsMap);
|
||||
|
||||
bindLinearElement(arrow, rectangle1, "start", scene);
|
||||
bindLinearElement(arrow, rectangle2, "end", scene);
|
||||
|
||||
expect(arrow.startBinding).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)],
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
|||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
FractionalIndex,
|
||||
} from "@excalidraw/element/types";
|
||||
|
@ -749,7 +750,7 @@ function testInvalidIndicesSync(args: {
|
|||
function prepareArguments(
|
||||
elementsLike: { id: string; index?: string }[],
|
||||
movedElementsIds?: string[],
|
||||
): [ExcalidrawElement[], Map<string, ExcalidrawElement> | undefined] {
|
||||
): [ExcalidrawElement[], ElementsMap | undefined] {
|
||||
const elements = elementsLike.map((x) =>
|
||||
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
|
||||
);
|
||||
|
@ -764,7 +765,7 @@ function prepareArguments(
|
|||
function test(
|
||||
name: string,
|
||||
elements: ExcalidrawElement[],
|
||||
movedElements: Map<string, ExcalidrawElement> | undefined,
|
||||
movedElements: ElementsMap | undefined,
|
||||
expectUnchangedElements: Map<string, { id: string }>,
|
||||
expectValidInput?: boolean,
|
||||
) {
|
||||
|
|
|
@ -333,7 +333,7 @@ describe("line element", () => {
|
|||
element,
|
||||
element,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene,
|
||||
"ne",
|
||||
);
|
||||
|
||||
|
@ -369,7 +369,7 @@ describe("line element", () => {
|
|||
element,
|
||||
element,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene,
|
||||
"se",
|
||||
);
|
||||
|
||||
|
@ -424,7 +424,7 @@ describe("line element", () => {
|
|||
element,
|
||||
element,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene,
|
||||
"e",
|
||||
{
|
||||
shouldResizeFromCenter: true,
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
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 type { ExcalidrawElement } from "../src/types";
|
||||
|
||||
const { h } = window;
|
||||
const assertOrder = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
expectedOrder: string[],
|
||||
|
@ -35,7 +37,7 @@ describe("normalizeElementsOrder", () => {
|
|||
boundElements: [],
|
||||
});
|
||||
|
||||
mutateElement(container, {
|
||||
mutateElement(container, new Map(), {
|
||||
boundElements: [{ type: "text", id: boundText.id }],
|
||||
});
|
||||
|
||||
|
@ -352,7 +354,7 @@ describe("normalizeElementsOrder", () => {
|
|||
containerId: container.id,
|
||||
});
|
||||
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [
|
||||
{ type: "text", id: boundText.id },
|
||||
{ type: "text", id: "xxx" },
|
||||
|
@ -387,7 +389,7 @@ describe("normalizeElementsOrder", () => {
|
|||
boundElements: [],
|
||||
groupIds: ["C", "A"],
|
||||
});
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: boundText.id }],
|
||||
});
|
||||
|
||||
|
|
|
@ -50,14 +50,8 @@ const alignSelectedElements = (
|
|||
alignment: Alignment,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const elementsMap = arrayToMap(elements);
|
||||
|
||||
const updatedElements = alignElements(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
alignment,
|
||||
app.scene,
|
||||
);
|
||||
const updatedElements = alignElements(selectedElements, alignment, app.scene);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
||||
|
|
|
@ -21,12 +21,12 @@ import {
|
|||
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isArrowElement,
|
||||
isTextBindableContainer,
|
||||
isTextElement,
|
||||
isUsingAdaptiveRadius,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { measureText } from "@excalidraw/element/textMeasurements";
|
||||
|
||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||
|
@ -42,6 +42,8 @@ import type {
|
|||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import { CaptureUpdateAction } from "../store";
|
||||
|
||||
import { register } from "./register";
|
||||
|
@ -77,7 +79,7 @@ export const actionUnbindText = register({
|
|||
boundTextElement,
|
||||
elementsMap,
|
||||
);
|
||||
mutateElement(boundTextElement as ExcalidrawTextElement, {
|
||||
app.scene.mutateElement(boundTextElement as ExcalidrawTextElement, {
|
||||
containerId: null,
|
||||
width,
|
||||
height,
|
||||
|
@ -85,7 +87,7 @@ export const actionUnbindText = register({
|
|||
x,
|
||||
y,
|
||||
});
|
||||
mutateElement(element, {
|
||||
app.scene.mutateElement(element, {
|
||||
boundElements: element.boundElements?.filter(
|
||||
(ele) => ele.id !== boundTextElement.id,
|
||||
),
|
||||
|
@ -150,24 +152,21 @@ export const actionBindText = register({
|
|||
textElement = selectedElements[1] as ExcalidrawTextElement;
|
||||
container = selectedElements[0] as ExcalidrawTextContainer;
|
||||
}
|
||||
mutateElement(textElement, {
|
||||
app.scene.mutateElement(textElement, {
|
||||
containerId: container.id,
|
||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
autoResize: true,
|
||||
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
||||
});
|
||||
mutateElement(container, {
|
||||
app.scene.mutateElement(container, {
|
||||
boundElements: (container.boundElements || []).concat({
|
||||
type: "text",
|
||||
id: textElement.id,
|
||||
}),
|
||||
});
|
||||
const originalContainerHeight = container.height;
|
||||
redrawTextBoundingBox(
|
||||
textElement,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
redrawTextBoundingBox(textElement, container, app.scene);
|
||||
// overwritting the cache with original container height so
|
||||
// it can be restored when unbind
|
||||
updateOriginalContainerCache(container.id, originalContainerHeight);
|
||||
|
@ -297,27 +296,23 @@ export const actionWrapTextInContainer = register({
|
|||
}
|
||||
|
||||
if (startBinding || endBinding) {
|
||||
mutateElement(ele, { startBinding, endBinding }, false);
|
||||
app.scene.mutateElement(ele, {
|
||||
startBinding,
|
||||
endBinding,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
textElement,
|
||||
{
|
||||
containerId: container.id,
|
||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
boundElements: null,
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
autoResize: true,
|
||||
},
|
||||
false,
|
||||
);
|
||||
redrawTextBoundingBox(
|
||||
textElement,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
app.scene.mutateElement(textElement, {
|
||||
containerId: container.id,
|
||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
boundElements: null,
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
autoResize: true,
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(textElement, container, app.scene);
|
||||
|
||||
updatedElements = pushContainerBelowText(
|
||||
[...updatedElements, container],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
|
||||
import { Excalidraw, mutateElement } from "../index";
|
||||
import { Excalidraw } from "../index";
|
||||
import { API } from "../tests/helpers/api";
|
||||
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,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
h.app.scene.mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
|
@ -94,7 +94,7 @@ describe("deleting selected elements when frame selected should keep children +
|
|||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
h.app.scene.mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
|
@ -132,7 +132,7 @@ describe("deleting selected elements when frame selected should keep children +
|
|||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
h.app.scene.mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
|
@ -170,7 +170,7 @@ describe("deleting selected elements when frame selected should keep children +
|
|||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(a1, {
|
||||
h.app.scene.mutateElement(a1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
|
|
|
@ -3,10 +3,7 @@ import { KEYS, updateActiveTool } from "@excalidraw/common";
|
|||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
import { fixBindingsAfterDeletion } from "@excalidraw/element/binding";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
|
@ -94,7 +91,7 @@ const deleteSelectedElements = (
|
|||
el.boundElements.forEach((candidate) => {
|
||||
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
||||
if (bound && isElbowArrow(bound)) {
|
||||
mutateElement(bound, {
|
||||
app.scene.mutateElement(bound, {
|
||||
startBinding:
|
||||
el.id === bound.startBinding?.elementId
|
||||
? null
|
||||
|
@ -102,7 +99,6 @@ const deleteSelectedElements = (
|
|||
endBinding:
|
||||
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
||||
});
|
||||
mutateElement(bound, { points: bound.points });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -261,7 +257,11 @@ export const actionDeleteSelected = register({
|
|||
: endBindingElement,
|
||||
};
|
||||
|
||||
LinearElementEditor.deletePoints(element, selectedPointsIndices);
|
||||
LinearElementEditor.deletePoints(
|
||||
element,
|
||||
app.scene,
|
||||
selectedPointsIndices,
|
||||
);
|
||||
|
||||
return {
|
||||
elements,
|
||||
|
|
|
@ -7,26 +7,17 @@ import {
|
|||
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isLinearElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||
|
||||
import {
|
||||
excludeElementsInFramesFromSelection,
|
||||
getSelectedElements,
|
||||
getSelectionStateForElements,
|
||||
} from "@excalidraw/element/selection";
|
||||
|
||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||
|
||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { DuplicateIcon } from "../components/icons";
|
||||
|
||||
|
@ -52,7 +43,7 @@ export const actionDuplicateSelection = register({
|
|||
try {
|
||||
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
||||
appState,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -65,52 +56,49 @@ export const actionDuplicateSelection = register({
|
|||
}
|
||||
}
|
||||
|
||||
let { newElements: duplicatedElements, elementsWithClones: nextElements } =
|
||||
duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
idsOfElementsToDuplicate: arrayToMap(
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}),
|
||||
),
|
||||
appState,
|
||||
randomizeSeed: true,
|
||||
overrides: (element) => ({
|
||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
||||
let { duplicatedElements, elementsWithDuplicates } = duplicateElements({
|
||||
type: "in-place",
|
||||
elements,
|
||||
idsOfElementsToDuplicate: arrayToMap(
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
}),
|
||||
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) {
|
||||
const mappedElements = app.props.onDuplicate(nextElements, elements);
|
||||
if (app.props.onDuplicate && elementsWithDuplicates) {
|
||||
const mappedElements = app.props.onDuplicate(
|
||||
elementsWithDuplicates,
|
||||
elements,
|
||||
);
|
||||
if (mappedElements) {
|
||||
nextElements = mappedElements;
|
||||
elementsWithDuplicates = mappedElements;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
|
||||
elements: syncMovedIndices(
|
||||
elementsWithDuplicates,
|
||||
arrayToMap(duplicatedElements),
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
...updateLinearElementEditors(duplicatedElements),
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: appState.editingGroupId,
|
||||
selectedElementIds: excludeElementsInFramesFromSelection(
|
||||
duplicatedElements,
|
||||
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
},
|
||||
getNonDeletedElements(nextElements),
|
||||
...getSelectionStateForElements(
|
||||
duplicatedElements,
|
||||
getNonDeletedElements(elementsWithDuplicates),
|
||||
appState,
|
||||
null,
|
||||
),
|
||||
},
|
||||
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,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
isBindingElement,
|
||||
isLinearElement,
|
||||
|
@ -46,7 +46,6 @@ export const actionFinalize = register({
|
|||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
@ -72,7 +71,11 @@ export const actionFinalize = register({
|
|||
scene.getElement(appState.pendingImageElementId);
|
||||
|
||||
if (pendingImageElement) {
|
||||
mutateElement(pendingImageElement, { isDeleted: true }, false);
|
||||
scene.mutateElement(
|
||||
pendingImageElement,
|
||||
{ isDeleted: true },
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
}
|
||||
|
||||
if (window.document.activeElement instanceof HTMLElement) {
|
||||
|
@ -96,7 +99,7 @@ export const actionFinalize = register({
|
|||
!lastCommittedPoint ||
|
||||
points[points.length - 1] !== lastCommittedPoint
|
||||
) {
|
||||
mutateElement(multiPointElement, {
|
||||
scene.mutateElement(multiPointElement, {
|
||||
points: multiPointElement.points.slice(0, -1),
|
||||
});
|
||||
}
|
||||
|
@ -120,7 +123,7 @@ export const actionFinalize = register({
|
|||
if (isLoop) {
|
||||
const linePoints = multiPointElement.points;
|
||||
const firstPoint = linePoints[0];
|
||||
mutateElement(multiPointElement, {
|
||||
scene.mutateElement(multiPointElement, {
|
||||
points: linePoints.map((p, index) =>
|
||||
index === linePoints.length - 1
|
||||
? pointFrom(firstPoint[0], firstPoint[1])
|
||||
|
@ -140,13 +143,7 @@ export const actionFinalize = register({
|
|||
-1,
|
||||
arrayToMap(elements),
|
||||
);
|
||||
maybeBindLinearElement(
|
||||
multiPointElement,
|
||||
appState,
|
||||
{ x, y },
|
||||
elementsMap,
|
||||
elements,
|
||||
);
|
||||
maybeBindLinearElement(multiPointElement, appState, { x, y }, scene);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,7 +199,10 @@ export const actionFinalize = register({
|
|||
// To select the linear element when user has finished mutipoint editing
|
||||
selectedLinearElement:
|
||||
multiPointElement && isLinearElement(multiPointElement)
|
||||
? new LinearElementEditor(multiPointElement)
|
||||
? new LinearElementEditor(
|
||||
multiPointElement,
|
||||
arrayToMap(newElements),
|
||||
)
|
||||
: appState.selectedLinearElement,
|
||||
pendingImageElementId: null,
|
||||
},
|
||||
|
|
|
@ -4,10 +4,7 @@ import {
|
|||
isBindingEnabled,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
||||
import {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
||||
import {
|
||||
|
@ -162,11 +159,9 @@ const flipElements = (
|
|||
|
||||
bindOrUnbindLinearElements(
|
||||
selectedElements.filter(isLinearElement),
|
||||
elementsMap,
|
||||
app.scene.getNonDeletedElements(),
|
||||
app.scene,
|
||||
isBindingEnabled(appState),
|
||||
[],
|
||||
app.scene,
|
||||
appState.zoom,
|
||||
);
|
||||
|
||||
|
@ -194,13 +189,13 @@ const flipElements = (
|
|||
getCommonBoundingBox(selectedElements);
|
||||
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||
otherElements.forEach((element) =>
|
||||
mutateElement(element, {
|
||||
app.scene.mutateElement(element, {
|
||||
x: element.x + diffX,
|
||||
y: element.y + diffY,
|
||||
}),
|
||||
);
|
||||
elbowArrows.forEach((element) =>
|
||||
mutateElement(element, {
|
||||
app.scene.mutateElement(element, {
|
||||
x: element.x + diffX,
|
||||
y: element.y + diffY,
|
||||
}),
|
||||
|
|
|
@ -173,11 +173,9 @@ export const actionWrapSelectionInFrame = register({
|
|||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
|
||||
const [x1, y1, x2, y2] = getCommonBounds(
|
||||
selectedElements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements, elementsMap);
|
||||
const PADDING = 16;
|
||||
const frame = newFrameElement({
|
||||
x: x1 - PADDING,
|
||||
|
@ -196,13 +194,9 @@ export const actionWrapSelectionInFrame = register({
|
|||
for (const elementInGroup of elementsInGroup) {
|
||||
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
||||
|
||||
mutateElement(
|
||||
elementInGroup,
|
||||
{
|
||||
groupIds: elementInGroup.groupIds.slice(0, index),
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(elementInGroup, elementsMap, {
|
||||
groupIds: elementInGroup.groupIds.slice(0, index),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
|||
|
||||
import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { arrayToMap } from "@excalidraw/common";
|
||||
|
||||
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
|
||||
|
||||
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
||||
|
@ -50,7 +52,7 @@ export const actionToggleLinearEditor = register({
|
|||
const editingLinearElement =
|
||||
appState.editingLinearElement?.elementId === selectedElement.id
|
||||
? null
|
||||
: new LinearElementEditor(selectedElement);
|
||||
: new LinearElementEditor(selectedElement, arrayToMap(elements));
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
|
|
|
@ -34,10 +34,7 @@ import {
|
|||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
getBoundTextElement,
|
||||
|
@ -61,6 +58,7 @@ import type { LocalPoint } from "@excalidraw/math";
|
|||
|
||||
import type {
|
||||
Arrowhead,
|
||||
ElementsMap,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
|
@ -68,9 +66,10 @@ import type {
|
|||
FontFamilyValues,
|
||||
TextAlign,
|
||||
VerticalAlign,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { trackEvent } from "../analytics";
|
||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
||||
|
@ -207,25 +206,22 @@ export const getFormValue = function <T extends Primitive>(
|
|||
const offsetElementAfterFontResize = (
|
||||
prevElement: ExcalidrawTextElement,
|
||||
nextElement: ExcalidrawTextElement,
|
||||
scene: Scene,
|
||||
) => {
|
||||
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
|
||||
return nextElement;
|
||||
}
|
||||
return mutateElement(
|
||||
nextElement,
|
||||
{
|
||||
x:
|
||||
prevElement.textAlign === "left"
|
||||
? prevElement.x
|
||||
: prevElement.x +
|
||||
(prevElement.width - nextElement.width) /
|
||||
(prevElement.textAlign === "center" ? 2 : 1),
|
||||
// centering vertically is non-standard, but for Excalidraw I think
|
||||
// it makes sense
|
||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
||||
},
|
||||
false,
|
||||
);
|
||||
return scene.mutateElement(nextElement, {
|
||||
x:
|
||||
prevElement.textAlign === "left"
|
||||
? prevElement.x
|
||||
: prevElement.x +
|
||||
(prevElement.width - nextElement.width) /
|
||||
(prevElement.textAlign === "center" ? 2 : 1),
|
||||
// centering vertically is non-standard, but for Excalidraw I think
|
||||
// it makes sense
|
||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
||||
});
|
||||
};
|
||||
|
||||
const changeFontSize = (
|
||||
|
@ -251,10 +247,14 @@ const changeFontSize = (
|
|||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
|
||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||
newElement = offsetElementAfterFontResize(
|
||||
oldElement,
|
||||
newElement,
|
||||
app.scene,
|
||||
);
|
||||
|
||||
return newElement;
|
||||
}
|
||||
|
@ -264,15 +264,11 @@ const changeFontSize = (
|
|||
);
|
||||
|
||||
// Update arrow elements after text elements have been updated
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
}).forEach((element) => {
|
||||
if (isTextElement(element)) {
|
||||
updateBoundElements(
|
||||
element,
|
||||
updatedElementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
updateBoundElements(element, app.scene);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -778,7 +774,7 @@ type ChangeFontFamilyData = Partial<
|
|||
>
|
||||
> & {
|
||||
/** cache of selected & editing elements populated on opened popup */
|
||||
cachedElements?: Map<string, ExcalidrawElement>;
|
||||
cachedElements?: ElementsMap;
|
||||
/** flag to reset all elements to their cached versions */
|
||||
resetAll?: true;
|
||||
/** flag to reset all containers to their cached versions */
|
||||
|
@ -919,7 +915,7 @@ export const actionChangeFontFamily = register({
|
|||
|
||||
if (resetContainers && container && cachedContainer) {
|
||||
// reset the container back to it's cached version
|
||||
mutateElement(container, { ...cachedContainer }, false);
|
||||
app.scene.mutateElement(container, { ...cachedContainer });
|
||||
}
|
||||
|
||||
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
|
||||
for (const [element, container] of elementContainerMapping) {
|
||||
// trigger synchronous redraw
|
||||
redrawTextBoundingBox(
|
||||
element,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
false,
|
||||
);
|
||||
redrawTextBoundingBox(element, container, app.scene);
|
||||
}
|
||||
} else {
|
||||
// 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(
|
||||
latestElement as ExcalidrawTextElement,
|
||||
latestContainer,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
false,
|
||||
app.scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -987,7 +977,7 @@ export const actionChangeFontFamily = register({
|
|||
return result;
|
||||
},
|
||||
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);
|
||||
// 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>({});
|
||||
|
@ -996,7 +986,7 @@ export const actionChangeFontFamily = register({
|
|||
const selectedFontFamily = useMemo(() => {
|
||||
const getFontFamily = (
|
||||
elementsArray: readonly ExcalidrawElement[],
|
||||
elementsMap: Map<string, ExcalidrawElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) =>
|
||||
getFormValue(
|
||||
elementsArray,
|
||||
|
@ -1179,7 +1169,7 @@ export const actionChangeTextAlign = register({
|
|||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
@ -1270,7 +1260,7 @@ export const actionChangeVerticalAlign = register({
|
|||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
@ -1670,10 +1660,10 @@ export const actionChangeArrowType = register({
|
|||
newElement,
|
||||
startHoveredElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
app.scene,
|
||||
);
|
||||
endHoveredElement &&
|
||||
bindLinearElement(newElement, endHoveredElement, "end", elementsMap);
|
||||
bindLinearElement(newElement, endHoveredElement, "end", app.scene);
|
||||
|
||||
const startBinding =
|
||||
startElement && newElement.startBinding
|
||||
|
@ -1684,7 +1674,6 @@ export const actionChangeArrowType = register({
|
|||
newElement,
|
||||
startElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
|
@ -1697,7 +1686,6 @@ export const actionChangeArrowType = register({
|
|||
newElement,
|
||||
endElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
|
@ -1729,7 +1717,7 @@ export const actionChangeArrowType = register({
|
|||
newElement.startBinding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (startElement) {
|
||||
bindLinearElement(newElement, startElement, "start", elementsMap);
|
||||
bindLinearElement(newElement, startElement, "start", app.scene);
|
||||
}
|
||||
}
|
||||
if (newElement.endBinding) {
|
||||
|
@ -1737,7 +1725,7 @@ export const actionChangeArrowType = register({
|
|||
newElement.endBinding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (endElement) {
|
||||
bindLinearElement(newElement, endElement, "end", elementsMap);
|
||||
bindLinearElement(newElement, endElement, "end", app.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1758,6 +1746,7 @@ export const actionChangeArrowType = register({
|
|||
if (selected) {
|
||||
newState.selectedLinearElement = new LinearElementEditor(
|
||||
selected as ExcalidrawLinearElement,
|
||||
arrayToMap(elements),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { getNonDeletedElements } from "@excalidraw/element";
|
|||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
import { arrayToMap, KEYS } from "@excalidraw/common";
|
||||
|
||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||
|
||||
|
@ -53,7 +53,7 @@ export const actionSelectAll = register({
|
|||
// single linear element selected
|
||||
Object.keys(selectedElementIds).length === 1 &&
|
||||
isLinearElement(elements[0])
|
||||
? new LinearElementEditor(elements[0])
|
||||
? new LinearElementEditor(elements[0], arrayToMap(elements))
|
||||
: null,
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
|
|
|
@ -139,11 +139,8 @@ export const actionPasteStyles = register({
|
|||
element.id === newElement.containerId,
|
||||
) || null;
|
||||
}
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
redrawTextBoundingBox(newElement, container, app.scene);
|
||||
}
|
||||
|
||||
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"
|
||||
| "cropEditor"
|
||||
| "wrapSelectionInFrame"
|
||||
| "toggleLassoTool";
|
||||
| "toggleLassoTool"
|
||||
| "toggleShapeSwitch";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
@ -195,7 +196,8 @@ export interface Action {
|
|||
| "menu"
|
||||
| "collab"
|
||||
| "hyperlink"
|
||||
| "search_menu";
|
||||
| "search_menu"
|
||||
| "shape_switch";
|
||||
action?: string;
|
||||
predicate?: (
|
||||
appState: Readonly<AppState>,
|
||||
|
|
|
@ -129,7 +129,6 @@ export class AnimatedTrail implements Trail {
|
|||
}
|
||||
|
||||
private update() {
|
||||
this.pastTrails = [];
|
||||
this.start();
|
||||
if (this.trailAnimation) {
|
||||
this.trailAnimation.setAttribute("begin", "indefinite");
|
||||
|
|
|
@ -37,6 +37,8 @@ import {
|
|||
syncMovedIndices,
|
||||
} from "@excalidraw/element/fractionalIndex";
|
||||
|
||||
import Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
|
||||
|
||||
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
||||
|
@ -490,6 +492,7 @@ export class AppStateChange implements Change<AppState> {
|
|||
nextElements.get(
|
||||
selectedLinearElementId,
|
||||
) as NonDeleted<ExcalidrawLinearElement>,
|
||||
nextElements,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
@ -499,6 +502,7 @@ export class AppStateChange implements Change<AppState> {
|
|||
nextElements.get(
|
||||
editingLinearElementId,
|
||||
) as NonDeleted<ExcalidrawLinearElement>,
|
||||
nextElements,
|
||||
)
|
||||
: null;
|
||||
|
||||
|
@ -1132,9 +1136,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||
}
|
||||
|
||||
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
|
||||
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
||||
nextElements = ElementsChange.reorderElements(
|
||||
|
@ -1143,8 +1144,14 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||
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
|
||||
ElementsChange.redrawBoundArrows(nextElements, changedElements);
|
||||
ElementsChange.redrawBoundArrows(tempScene, changedElements);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Couldn't mutate elements after applying elements change`,
|
||||
|
@ -1337,8 +1344,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||
} else {
|
||||
affectedElement = mutateElement(
|
||||
nextElement,
|
||||
nextElements,
|
||||
updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||
);
|
||||
) as OrderedExcalidrawElement;
|
||||
}
|
||||
|
||||
nextAffectedElements.set(affectedElement.id, affectedElement);
|
||||
|
@ -1456,9 +1464,10 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||
}
|
||||
|
||||
private static redrawTextBoundingBoxes(
|
||||
elements: SceneElementsMap,
|
||||
scene: Scene,
|
||||
changed: Map<string, OrderedExcalidrawElement>,
|
||||
) {
|
||||
const elements = scene.getNonDeletedElementsMap();
|
||||
const boxesToRedraw = new Map<
|
||||
string,
|
||||
{ container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
|
||||
|
@ -1498,17 +1507,17 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||
continue;
|
||||
}
|
||||
|
||||
redrawTextBoundingBox(boundText, container, elements, false);
|
||||
redrawTextBoundingBox(boundText, container, scene);
|
||||
}
|
||||
}
|
||||
|
||||
private static redrawBoundArrows(
|
||||
elements: SceneElementsMap,
|
||||
scene: Scene,
|
||||
changed: Map<string, OrderedExcalidrawElement>,
|
||||
) {
|
||||
for (const element of changed.values()) {
|
||||
if (!element.isDeleted && isBindableElement(element)) {
|
||||
updateBoundElements(element, elements, {
|
||||
updateBoundElements(element, scene, {
|
||||
changedElements: changed,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -172,7 +172,7 @@ export const serializeAsClipboardJSON = ({
|
|||
!framesToCopy.has(getContainingFrame(element, elementsMap)!)
|
||||
) {
|
||||
const copiedElement = deepCopyElement(element);
|
||||
mutateElement(copiedElement, {
|
||||
mutateElement(copiedElement, elementsMap, {
|
||||
frameId: null,
|
||||
});
|
||||
return copiedElement;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,8 @@ import {
|
|||
isWritableElement,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
|
||||
|
||||
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||
|
||||
import {
|
||||
|
@ -410,6 +412,14 @@ function CommandPaletteInner({
|
|||
actionManager.executeAction(actionToggleSearchMenu);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.shapeSwitch"),
|
||||
category: DEFAULT_CATEGORIES.elements,
|
||||
icon: boltIcon,
|
||||
perform: () => {
|
||||
actionManager.executeAction(actionToggleShapeSwitch);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.changeStroke"),
|
||||
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,
|
||||
getLinkIdAndTypeFromSelection,
|
||||
} 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 { getSelectedElements } from "../scene";
|
||||
|
@ -21,20 +22,20 @@ import { TrashIcon } from "./icons";
|
|||
import "./ElementLinkDialog.scss";
|
||||
|
||||
import type { AppProps, AppState, UIAppState } from "../types";
|
||||
|
||||
const ElementLinkDialog = ({
|
||||
sourceElementId,
|
||||
onClose,
|
||||
elementsMap,
|
||||
appState,
|
||||
scene,
|
||||
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
||||
}: {
|
||||
sourceElementId: ExcalidrawElement["id"];
|
||||
elementsMap: ElementsMap;
|
||||
appState: UIAppState;
|
||||
scene: Scene;
|
||||
onClose?: () => void;
|
||||
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
||||
|
||||
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
||||
|
@ -70,7 +71,7 @@ const ElementLinkDialog = ({
|
|||
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
|
||||
const elementToLink = elementsMap.get(sourceElementId);
|
||||
elementToLink &&
|
||||
mutateElement(elementToLink, {
|
||||
scene.mutateElement(elementToLink, {
|
||||
link: nextLink,
|
||||
});
|
||||
}
|
||||
|
@ -78,13 +79,13 @@ const ElementLinkDialog = ({
|
|||
if (!nextLink && linkEdited && sourceElementId) {
|
||||
const elementToLink = elementsMap.get(sourceElementId);
|
||||
elementToLink &&
|
||||
mutateElement(elementToLink, {
|
||||
scene.mutateElement(elementToLink, {
|
||||
link: null,
|
||||
});
|
||||
}
|
||||
|
||||
onClose?.();
|
||||
}, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
|
||||
}, [sourceElementId, nextLink, elementsMap, linkEdited, scene, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
|
@ -21,7 +21,7 @@ const Header = () => (
|
|||
className="HelpDialog__btn"
|
||||
href="https://docs.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||
{t("helpDialog.documentation")}
|
||||
|
@ -30,7 +30,7 @@ const Header = () => (
|
|||
className="HelpDialog__btn"
|
||||
href="https://plus.excalidraw.com/blog"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||
{t("helpDialog.blog")}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
CLASSES,
|
||||
DEFAULT_SIDEBAR,
|
||||
TOOL_TYPE,
|
||||
arrayToMap,
|
||||
capitalizeString,
|
||||
isShallowEqual,
|
||||
} from "@excalidraw/common";
|
||||
|
@ -17,7 +18,6 @@ import { ShapeCache } from "@excalidraw/element/ShapeCache";
|
|||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import Scene from "../scene/Scene";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { isHandToolActive } from "../appState";
|
||||
|
@ -446,22 +446,18 @@ const LayerUI = ({
|
|||
|
||||
if (selectedElements.length) {
|
||||
for (const element of selectedElements) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||
? colorPickerType === "elementBackground"
|
||||
? "strokeColor"
|
||||
: "backgroundColor"
|
||||
: colorPickerType === "elementBackground"
|
||||
? "backgroundColor"
|
||||
: "strokeColor"]: color,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(element, arrayToMap(elements), {
|
||||
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||
? colorPickerType === "elementBackground"
|
||||
? "strokeColor"
|
||||
: "backgroundColor"
|
||||
: colorPickerType === "elementBackground"
|
||||
? "backgroundColor"
|
||||
: "strokeColor"]: color,
|
||||
});
|
||||
ShapeCache.delete(element);
|
||||
}
|
||||
Scene.getScene(selectedElements[0])?.triggerUpdate();
|
||||
app.scene.triggerUpdate();
|
||||
} else if (colorPickerType === "elementBackground") {
|
||||
setAppState({
|
||||
currentItemBackgroundColor: color,
|
||||
|
@ -494,7 +490,7 @@ const LayerUI = ({
|
|||
openDialog: null,
|
||||
});
|
||||
}}
|
||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||
scene={app.scene}
|
||||
appState={appState}
|
||||
generateLinkForSelection={generateLinkForSelection}
|
||||
/>
|
||||
|
|
|
@ -166,7 +166,7 @@ export default function LibraryMenuItems({
|
|||
type: "everything",
|
||||
elements: item.elements,
|
||||
randomizeSeed: true,
|
||||
}).newElements,
|
||||
}).duplicatedElements,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
@ -389,7 +389,7 @@ const PublishLibrary = ({
|
|||
<a
|
||||
href="https://libraries.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
{el}
|
||||
</a>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
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 Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { angleIcon } from "../icons";
|
||||
|
||||
import { updateBindings } from "../../../element/src/binding";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface AngleProps {
|
||||
|
@ -35,7 +36,6 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
if (origElement && !isElbowArrow(origElement)) {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
|
@ -45,14 +45,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
|
||||
if (nextValue !== undefined) {
|
||||
const nextAngle = degreesToRadians(nextValue as Degrees);
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle });
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
|
||||
return;
|
||||
|
@ -71,14 +71,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||
|
||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
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 StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface PositionProps {
|
||||
|
|
|
@ -5,17 +5,17 @@ import {
|
|||
MINIMAL_CROP_SIZE,
|
||||
getUncroppedWidthAndHeight,
|
||||
} from "@excalidraw/element/cropElement";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { resizeSingleElement } from "@excalidraw/element/resizeElements";
|
||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface DimensionDragInputProps {
|
||||
|
@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
};
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||
|
@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
height: nextCropHeight,
|
||||
};
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||
|
@ -176,8 +176,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldMaintainAspectRatio: keepAspectRatio,
|
||||
|
@ -223,8 +223,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldMaintainAspectRatio: keepAspectRatio,
|
||||
|
|
|
@ -7,6 +7,8 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
|||
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { CaptureUpdateAction } from "../../store";
|
||||
import { useApp } from "../App";
|
||||
import { InlineIcon } from "../InlineIcon";
|
||||
|
@ -16,7 +18,6 @@ import { SMALLEST_DELTA } from "./utils";
|
|||
import "./DragInput.scss";
|
||||
|
||||
import type { StatsInputProperty } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
export type DragInputCallbackType<
|
||||
|
@ -216,13 +217,12 @@ const StatsDragInput = <
|
|||
y: number;
|
||||
} | null = null;
|
||||
|
||||
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
||||
app.scene
|
||||
.getNonDeletedElements()
|
||||
.reduce((acc: ElementsMap, element) => {
|
||||
acc.set(element.id, deepCopyElement(element));
|
||||
return acc;
|
||||
}, new Map());
|
||||
let originalElementsMap: ElementsMap | null = app.scene
|
||||
.getNonDeletedElements()
|
||||
.reduce((acc: ElementsMap, element) => {
|
||||
acc.set(element.id, deepCopyElement(element));
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
let originalElements: readonly E[] | null = elements.map(
|
||||
(element) => originalElementsMap!.get(element.id) as E,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
redrawTextBoundingBox,
|
||||
|
@ -13,13 +12,14 @@ import type {
|
|||
ExcalidrawTextElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { fontSizeIcon } from "../icons";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface FontSizeProps {
|
||||
|
@ -68,13 +68,13 @@ const handleFontSizeChange: DragInputCallbackType<
|
|||
}
|
||||
|
||||
if (nextFontSize) {
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
redrawTextBoundingBox(
|
||||
latestElement,
|
||||
scene.getContainerElement(latestElement),
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
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 Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { angleIcon } from "../icons";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiAngleProps {
|
||||
|
@ -54,17 +53,13 @@ const handleDegreeChange: DragInputCallbackType<
|
|||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
angle: nextAngle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(element, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
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);
|
||||
|
||||
mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
angle: nextAngle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
}
|
||||
scene.triggerUpdate();
|
||||
|
|
|
@ -3,7 +3,6 @@ import { useMemo } from "react";
|
|||
|
||||
import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
|
||||
import { updateBoundElements } from "@excalidraw/element/binding";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
rescalePointsInElement,
|
||||
resizeSingleElement,
|
||||
|
@ -23,13 +22,14 @@ import type {
|
|||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import { getElementsInAtomicUnit } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type { AtomicUnit } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiDimensionProps {
|
||||
|
@ -75,33 +75,31 @@ const resizeElementInGroup = (
|
|||
scale: number,
|
||||
latestElement: ExcalidrawElement,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||
|
||||
mutateElement(latestElement, updates, false);
|
||||
scene.mutateElement(latestElement, updates);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
origElement,
|
||||
originalElementsMap,
|
||||
);
|
||||
if (boundTextElement) {
|
||||
const newFontSize = boundTextElement.fontSize * scale;
|
||||
updateBoundElements(latestElement, elementsMap, {
|
||||
updateBoundElements(latestElement, scene, {
|
||||
newSize: { width: updates.width, height: updates.height },
|
||||
});
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||
mutateElement(
|
||||
latestBoundTextElement,
|
||||
{
|
||||
fontSize: newFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestBoundTextElement, {
|
||||
fontSize: newFontSize,
|
||||
});
|
||||
handleBindTextResize(
|
||||
latestElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
true,
|
||||
);
|
||||
|
@ -118,8 +116,8 @@ const resizeGroup = (
|
|||
property: MultiDimensionProps["property"],
|
||||
latestElements: ExcalidrawElement[],
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
// keep aspect ratio for groups
|
||||
if (property === "width") {
|
||||
|
@ -141,8 +139,8 @@ const resizeGroup = (
|
|||
scale,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -194,8 +192,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
property,
|
||||
latestElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
@ -237,8 +235,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldInformMutation: false,
|
||||
|
@ -301,8 +299,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
property,
|
||||
latestElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
@ -340,8 +338,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldInformMutation: false,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
redrawTextBoundingBox,
|
||||
|
@ -16,13 +15,14 @@ import type {
|
|||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { fontSizeIcon } from "../icons";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiFontSizeProps {
|
||||
|
@ -84,19 +84,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
|||
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||
|
||||
for (const textElement of latestTextElements) {
|
||||
mutateElement(
|
||||
textElement,
|
||||
{
|
||||
fontSize: nextFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(textElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(
|
||||
textElement,
|
||||
scene.getContainerElement(textElement),
|
||||
elementsMap,
|
||||
false,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -117,19 +112,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
|||
if (shouldChangeByStepSize) {
|
||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
||||
}
|
||||
mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
fontSize: nextFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(
|
||||
latestElement,
|
||||
scene.getContainerElement(latestElement),
|
||||
elementsMap,
|
||||
false,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,9 @@ import { isTextElement } from "@excalidraw/element/typeChecks";
|
|||
|
||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
@ -18,7 +15,6 @@ import { getElementsInAtomicUnit, moveElement } from "./utils";
|
|||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type { AtomicUnit } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiPositionProps {
|
||||
|
@ -36,13 +32,11 @@ const moveElements = (
|
|||
property: MultiPositionProps["property"],
|
||||
changeInTopX: number,
|
||||
changeInTopY: number,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
originalElements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
for (let i = 0; i < originalElements.length; i++) {
|
||||
const origElement = originalElements[i];
|
||||
|
||||
const [cx, cy] = [
|
||||
|
@ -65,8 +59,6 @@ const moveElements = (
|
|||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
|
@ -78,11 +70,10 @@ const moveGroupTo = (
|
|||
nextX: number,
|
||||
nextY: number,
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
||||
const offsetX = nextX - x1;
|
||||
const offsetY = nextY - y1;
|
||||
|
@ -112,8 +103,6 @@ const moveGroupTo = (
|
|||
topLeftX + offsetX,
|
||||
topLeftY + offsetY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
|
@ -135,7 +124,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of getAtomicUnits(
|
||||
|
@ -159,8 +147,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
elementsInUnit.map((el) => el.original),
|
||||
elementsMap,
|
||||
elements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
@ -188,8 +174,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
|
@ -214,8 +198,6 @@ const handlePositionChange: DragInputCallbackType<
|
|||
changeInTopX,
|
||||
changeInTopY,
|
||||
originalElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
|
|
@ -4,16 +4,16 @@ import {
|
|||
getFlipAdjustedCropPosition,
|
||||
getUncroppedWidthAndHeight,
|
||||
} from "@excalidraw/element/cropElement";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue, moveElement } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface PositionProps {
|
||||
|
@ -38,7 +38,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
const [cx, cy] = [
|
||||
origElement.x + origElement.width / 2,
|
||||
|
@ -101,7 +100,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
};
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
});
|
||||
|
||||
|
@ -119,7 +118,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
|
||||
};
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
});
|
||||
|
||||
|
@ -133,8 +132,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
|
@ -166,8 +163,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ import type {
|
|||
ExcalidrawTextElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { Excalidraw, getCommonBounds, mutateElement } from "../..";
|
||||
import { Excalidraw, getCommonBounds } from "../..";
|
||||
import { actionGroup } from "../../actions";
|
||||
import { t } from "../../i18n";
|
||||
import * as StaticScene from "../../renderer/staticScene";
|
||||
|
@ -478,7 +478,7 @@ describe("stats for a non-generic element", () => {
|
|||
containerId: container.id,
|
||||
fontSize: 20,
|
||||
});
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
});
|
||||
API.setElements([container, text]);
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
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 {
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
|
@ -24,10 +18,12 @@ import type {
|
|||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} 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";
|
||||
|
||||
export type StatsInputProperty =
|
||||
|
@ -119,12 +115,11 @@ export const moveElement = (
|
|||
newTopLeftX: number,
|
||||
newTopLeftY: number,
|
||||
originalElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
originalElementsMap: ElementsMap,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const latestElement = elementsMap.get(originalElement.id);
|
||||
if (!latestElement) {
|
||||
return;
|
||||
|
@ -148,15 +143,15 @@ export const moveElement = (
|
|||
-originalElement.angle as Radians,
|
||||
);
|
||||
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
x,
|
||||
y,
|
||||
},
|
||||
shouldInformMutation,
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
originalElement,
|
||||
|
@ -165,13 +160,13 @@ export const moveElement = (
|
|||
if (boundTextElement) {
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
latestBoundTextElement &&
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
latestBoundTextElement,
|
||||
{
|
||||
x: boundTextElement.x + changeInX,
|
||||
y: boundTextElement.y + changeInY,
|
||||
},
|
||||
shouldInformMutation,
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -196,29 +191,3 @@ export const getAtomicUnits = (
|
|||
});
|
||||
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;
|
||||
scale: number;
|
||||
appState: InteractiveCanvasAppState;
|
||||
renderScrollbars: boolean;
|
||||
device: Device;
|
||||
renderInteractiveSceneCallback: (
|
||||
data: RenderInteractiveSceneCallback,
|
||||
|
@ -143,7 +144,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||
remotePointerUsernames,
|
||||
remotePointerUserStates,
|
||||
selectionColor,
|
||||
renderScrollbars: false,
|
||||
renderScrollbars: props.renderScrollbars,
|
||||
},
|
||||
device: props.device,
|
||||
callback: props.renderInteractiveSceneCallback,
|
||||
|
@ -230,7 +231,8 @@ const areEqual = (
|
|||
// on appState)
|
||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||
prevProps.selectedElements !== nextProps.selectedElements
|
||||
prevProps.selectedElements !== nextProps.selectedElements ||
|
||||
prevProps.renderScrollbars !== nextProps.renderScrollbars
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ const DropdownMenuItemLink = ({
|
|||
onSelect,
|
||||
className = "",
|
||||
selected,
|
||||
rel = "noreferrer",
|
||||
rel = "noopener",
|
||||
...rest
|
||||
}: {
|
||||
href: string;
|
||||
|
@ -31,11 +31,12 @@ const DropdownMenuItemLink = ({
|
|||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-target-blank
|
||||
<a
|
||||
{...rest}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel={rel || "noopener"}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -21,8 +21,6 @@ import {
|
|||
embeddableURLValidator,
|
||||
} from "@excalidraw/element/embeddable";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
|
@ -33,6 +31,8 @@ import {
|
|||
|
||||
import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawEmbeddableElement,
|
||||
|
@ -70,14 +70,14 @@ const embeddableLinkCache = new Map<
|
|||
|
||||
export const Hyperlink = ({
|
||||
element,
|
||||
elementsMap,
|
||||
scene,
|
||||
setAppState,
|
||||
onLinkOpen,
|
||||
setToast,
|
||||
updateEmbedValidationStatus,
|
||||
}: {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
elementsMap: ElementsMap;
|
||||
scene: Scene;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||
setToast: (
|
||||
|
@ -88,6 +88,7 @@ export const Hyperlink = ({
|
|||
status: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const appState = useExcalidrawAppState();
|
||||
const appProps = useAppProps();
|
||||
const device = useDevice();
|
||||
|
@ -114,7 +115,7 @@ export const Hyperlink = ({
|
|||
setAppState({ activeEmbeddable: null });
|
||||
}
|
||||
if (!link) {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
link: null,
|
||||
});
|
||||
updateEmbedValidationStatus(element, false);
|
||||
|
@ -126,7 +127,7 @@ export const Hyperlink = ({
|
|||
setToast({ message: t("toast.unableToEmbed"), closable: true });
|
||||
}
|
||||
element.link && embeddableLinkCache.set(element.id, element.link);
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
link,
|
||||
});
|
||||
updateEmbedValidationStatus(element, false);
|
||||
|
@ -144,7 +145,7 @@ export const Hyperlink = ({
|
|||
: 1;
|
||||
const hasLinkChanged =
|
||||
embeddableLinkCache.get(element.id) !== element.link;
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
...(hasLinkChanged
|
||||
? {
|
||||
width:
|
||||
|
@ -169,10 +170,11 @@ export const Hyperlink = ({
|
|||
}
|
||||
}
|
||||
} else {
|
||||
mutateElement(element, { link });
|
||||
scene.mutateElement(element, { link });
|
||||
}
|
||||
}, [
|
||||
element,
|
||||
scene,
|
||||
setToast,
|
||||
appProps.validateEmbeddable,
|
||||
appState.activeEmbeddable,
|
||||
|
@ -229,9 +231,9 @@ export const Hyperlink = ({
|
|||
|
||||
const handleRemove = useCallback(() => {
|
||||
trackEvent("hyperlink", "delete");
|
||||
mutateElement(element, { link: null });
|
||||
scene.mutateElement(element, { link: null });
|
||||
setAppState({ showHyperlinkPopup: false });
|
||||
}, [setAppState, element]);
|
||||
}, [setAppState, element, scene]);
|
||||
|
||||
const onEdit = () => {
|
||||
trackEvent("hyperlink", "edit", "popup-ui");
|
||||
|
|
|
@ -78,7 +78,7 @@ const WelcomeScreenMenuItemLink = ({
|
|||
className={`welcome-screen-menu-item ${className}`}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
isFirefox,
|
||||
MIME_TYPES,
|
||||
cloneJSON,
|
||||
SVG_DOCUMENT_PREAMBLE,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
|
@ -134,7 +135,11 @@ export const exportCanvas = async (
|
|||
if (type === "svg") {
|
||||
return fileSave(
|
||||
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",
|
||||
|
|
|
@ -29,6 +29,7 @@ import { bumpVersion } from "@excalidraw/element/mutateElement";
|
|||
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||
import { detectLineHeight } from "@excalidraw/element/textMeasurements";
|
||||
import {
|
||||
isArrowBoundToElement,
|
||||
isArrowElement,
|
||||
isElbowArrow,
|
||||
isFixedPointBinding,
|
||||
|
@ -439,7 +440,7 @@ const repairContainerElement = (
|
|||
// if defined, lest boundElements is stale
|
||||
!boundElement.containerId
|
||||
) {
|
||||
(boundElement as Mutable<ExcalidrawTextElement>).containerId =
|
||||
(boundElement as Mutable<typeof boundElement>).containerId =
|
||||
container.id;
|
||||
}
|
||||
}
|
||||
|
@ -464,6 +465,10 @@ const repairBoundElement = (
|
|||
? elementsMap.get(boundElement.containerId)
|
||||
: null;
|
||||
|
||||
(boundElement as Mutable<typeof boundElement>).angle = (
|
||||
isArrowElement(container) ? 0 : container?.angle ?? 0
|
||||
) as Radians;
|
||||
|
||||
if (!container) {
|
||||
boundElement.containerId = null;
|
||||
return;
|
||||
|
@ -590,8 +595,7 @@ export const restoreElements = (
|
|||
return restoredElements.map((element) => {
|
||||
if (
|
||||
isElbowArrow(element) &&
|
||||
element.startBinding == null &&
|
||||
element.endBinding == null &&
|
||||
!isArrowBoundToElement(element) &&
|
||||
!validateElbowPoints(element.points)
|
||||
) {
|
||||
return {
|
||||
|
|
|
@ -38,10 +38,13 @@ import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
|
|||
|
||||
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 {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
|
@ -63,8 +66,6 @@ import type {
|
|||
|
||||
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { getCommonBounds } from "..";
|
||||
|
||||
export type ValidLinearElement = {
|
||||
type: "arrow" | "line";
|
||||
x: number;
|
||||
|
@ -221,7 +222,7 @@ const DEFAULT_DIMENSION = 100;
|
|||
const bindTextToContainer = (
|
||||
container: ExcalidrawElement,
|
||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const textElement: ExcalidrawTextElement = newTextElement({
|
||||
x: 0,
|
||||
|
@ -240,7 +241,8 @@ const bindTextToContainer = (
|
|||
}),
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(textElement, container, elementsMap);
|
||||
redrawTextBoundingBox(textElement, container, scene);
|
||||
|
||||
return [container, textElement] as const;
|
||||
};
|
||||
|
||||
|
@ -249,7 +251,7 @@ const bindLinearElementToElement = (
|
|||
start: ValidLinearElement["start"],
|
||||
end: ValidLinearElement["end"],
|
||||
elementStore: ElementStore,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): {
|
||||
linearElement: ExcalidrawLinearElement;
|
||||
startBoundElement?: ExcalidrawElement;
|
||||
|
@ -335,7 +337,7 @@ const bindLinearElementToElement = (
|
|||
linearElement,
|
||||
startBoundElement as ExcalidrawBindableElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -410,7 +412,7 @@ const bindLinearElementToElement = (
|
|||
linearElement,
|
||||
endBoundElement as ExcalidrawBindableElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -651,6 +653,9 @@ export const convertToExcalidrawElements = (
|
|||
}
|
||||
|
||||
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
|
||||
for (const [id, element] of elementsWithIds) {
|
||||
const excalidrawElement = elementStore.getElement(id)!;
|
||||
|
@ -664,7 +669,7 @@ export const convertToExcalidrawElements = (
|
|||
let [container, text] = bindTextToContainer(
|
||||
excalidrawElement,
|
||||
element?.label,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
elementStore.add(container);
|
||||
elementStore.add(text);
|
||||
|
@ -692,7 +697,7 @@ export const convertToExcalidrawElements = (
|
|||
originalStart,
|
||||
originalEnd,
|
||||
elementStore,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
container = linearElement;
|
||||
elementStore.add(linearElement);
|
||||
|
@ -717,7 +722,7 @@ export const convertToExcalidrawElements = (
|
|||
start,
|
||||
end,
|
||||
elementStore,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
elementStore.add(linearElement);
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
// 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";
|
||||
|
||||
const jotai = createIsolation();
|
||||
|
||||
export { atom, PrimitiveAtom };
|
||||
export { atom, PrimitiveAtom, WritableAtom };
|
||||
export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
|
||||
export const EditorJotaiProvider: ReturnType<
|
||||
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 Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { CascadiaFontFaces } from "./Cascadia";
|
||||
import { ComicShannsFontFaces } from "./ComicShanns";
|
||||
import { EmojiFontFaces } from "./Emoji";
|
||||
|
@ -40,8 +42,6 @@ import { NunitoFontFaces } from "./Nunito";
|
|||
import { VirgilFontFaces } from "./Virgil";
|
||||
import { XiaolaiFontFaces } from "./Xiaolai";
|
||||
|
||||
import type Scene from "../scene/Scene";
|
||||
|
||||
export class Fonts {
|
||||
// it's ok to track fonts across multiple instances only once, so let's use
|
||||
// a static member to reduce memory footprint
|
||||
|
|
|
@ -53,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
renderEmbeddable,
|
||||
aiEnabled,
|
||||
showDeprecatedFonts,
|
||||
renderScrollbars,
|
||||
} = props;
|
||||
|
||||
const canvasActions = props.UIOptions?.canvasActions;
|
||||
|
@ -143,6 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
renderEmbeddable={renderEmbeddable}
|
||||
aiEnabled={aiEnabled !== false}
|
||||
showDeprecatedFonts={showDeprecatedFonts}
|
||||
renderScrollbars={renderScrollbars}
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
|
|
|
@ -149,6 +149,7 @@ export class LassoTrail extends AnimatedTrail {
|
|||
this.app.scene.getNonDeletedElement(
|
||||
selectedIds[0],
|
||||
) as NonDeleted<ExcalidrawLinearElement>,
|
||||
this.app.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: null,
|
||||
};
|
||||
|
|
|
@ -7,11 +7,13 @@ import {
|
|||
polygonIncludesPointNonZero,
|
||||
} 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";
|
||||
|
||||
export type ElementsSegmentsMap = Map<string, LineSegment<GlobalPoint>[]>;
|
||||
|
||||
export const getLassoSelectedElementIds = (input: {
|
||||
lassoPath: GlobalPoint[];
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
|
|
@ -165,7 +165,9 @@
|
|||
"unCroppedDimension": "Uncropped dimension",
|
||||
"copyElementLink": "Copy link to object",
|
||||
"linkToElement": "Link to object",
|
||||
"wrapSelectionInFrame": "Wrap selection in frame"
|
||||
"wrapSelectionInFrame": "Wrap selection in frame",
|
||||
"tab": "Tab",
|
||||
"shapeSwitch": "Switch shape"
|
||||
},
|
||||
"elementLink": {
|
||||
"title": "Link to object",
|
||||
|
|
|
@ -1182,7 +1182,7 @@ const _renderInteractiveScene = ({
|
|||
let scrollBars;
|
||||
if (renderConfig.renderScrollbars) {
|
||||
scrollBars = getScrollBars(
|
||||
visibleElements,
|
||||
elementsMap,
|
||||
normalizedWidth,
|
||||
normalizedHeight,
|
||||
appState,
|
||||
|
|
|
@ -9,10 +9,11 @@ import type {
|
|||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
|
||||
import { renderStaticSceneThrottled } from "../renderer/staticScene";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
import type { RenderableElementsMap } from "./types";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
|
|
|
@ -2,24 +2,23 @@ import { getGlobalCSSVariable } from "@excalidraw/common";
|
|||
|
||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { getLanguage } from "../i18n";
|
||||
|
||||
import type { InteractiveCanvasAppState } from "../types";
|
||||
import type { ScrollBars } from "./types";
|
||||
import type { RenderableElementsMap, ScrollBars } from "./types";
|
||||
|
||||
export const SCROLLBAR_MARGIN = 4;
|
||||
export const SCROLLBAR_WIDTH = 6;
|
||||
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 = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: RenderableElementsMap,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
appState: InteractiveCanvasAppState,
|
||||
): ScrollBars => {
|
||||
if (!elements.length) {
|
||||
if (!elements.size) {
|
||||
return {
|
||||
horizontal: null,
|
||||
vertical: null,
|
||||
|
@ -33,9 +32,6 @@ export const getScrollBars = (
|
|||
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||
|
||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
||||
|
||||
const safeArea = {
|
||||
top: parseInt(getGlobalCSSVariable("sat")) || 0,
|
||||
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
|
||||
|
@ -46,10 +42,8 @@ export const getScrollBars = (
|
|||
const isRTL = getLanguage().rtl;
|
||||
|
||||
// The viewport is the rectangle currently visible for the user
|
||||
const viewportMinX =
|
||||
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
||||
const viewportMinY =
|
||||
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
||||
const viewportMinX = -appState.scrollX + safeArea.left;
|
||||
const viewportMinY = -appState.scrollY + safeArea.top;
|
||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||
|
||||
|
@ -59,8 +53,43 @@ export const getScrollBars = (
|
|||
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
|
||||
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 {
|
||||
horizontal:
|
||||
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
|
||||
|
@ -68,18 +97,17 @@ export const getScrollBars = (
|
|||
: {
|
||||
x:
|
||||
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
|
||||
((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) *
|
||||
viewportWidth,
|
||||
SCROLLBAR_WIDTH +
|
||||
((viewportMinX - sceneMinX) / extendedSceneWidth) * viewportWidth,
|
||||
y:
|
||||
viewportHeight -
|
||||
SCROLLBAR_WIDTH -
|
||||
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
|
||||
width:
|
||||
((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) *
|
||||
viewportWidth -
|
||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
|
||||
width: scrollbarWidth,
|
||||
height: SCROLLBAR_WIDTH,
|
||||
deltaMultiplier: horizontalDeltaMultiplier,
|
||||
},
|
||||
|
||||
vertical:
|
||||
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
|
||||
? null
|
||||
|
@ -90,14 +118,13 @@ export const getScrollBars = (
|
|||
SCROLLBAR_WIDTH -
|
||||
Math.max(safeArea.right, SCROLLBAR_MARGIN),
|
||||
y:
|
||||
((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) *
|
||||
viewportHeight +
|
||||
Math.max(safeArea.top, SCROLLBAR_MARGIN),
|
||||
Math.max(safeArea.top, SCROLLBAR_MARGIN) +
|
||||
SCROLLBAR_WIDTH +
|
||||
((viewportMinY - sceneMinY) / extendedSceneHeight) *
|
||||
viewportHeight,
|
||||
width: SCROLLBAR_WIDTH,
|
||||
height:
|
||||
((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) *
|
||||
viewportHeight -
|
||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
|
||||
height: scrollbarHeight,
|
||||
deltaMultiplier: verticalDeltaMultiplier,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -130,12 +130,14 @@ export type ScrollBars = {
|
|||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
deltaMultiplier: number;
|
||||
} | null;
|
||||
vertical: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
deltaMultiplier: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
|||
<a
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
href="blog.excalidaw.com"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
|
@ -392,7 +392,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
aria-label="GitHub"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
href="https://github.com/excalidraw/excalidraw"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
>
|
||||
|
@ -426,7 +426,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
aria-label="X"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
href="https://x.com/excalidraw"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="X"
|
||||
>
|
||||
|
@ -472,7 +472,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||
aria-label="Discord"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
href="https://discord.gg/UexuTaE"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="Discord"
|
||||
>
|
||||
|
|
|
@ -171,7 +171,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 19,
|
||||
"version": 9,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"y": -50,
|
||||
|
@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "102.35417",
|
||||
"height": "102.45605",
|
||||
"id": "id172",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
|
@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
0,
|
||||
],
|
||||
[
|
||||
"101.77517",
|
||||
"102.35417",
|
||||
"102.80179",
|
||||
"102.45605",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
|
@ -227,9 +227,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 40,
|
||||
"width": "101.77517",
|
||||
"x": "0.70711",
|
||||
"version": 37,
|
||||
"width": "102.80179",
|
||||
"x": "-0.42182",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
@ -264,7 +264,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 6,
|
||||
"version": 14,
|
||||
"width": 50,
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
|
@ -291,22 +291,39 @@ History {
|
|||
"added": Map {},
|
||||
"removed": Map {},
|
||||
"updated": Map {
|
||||
"id171" => Delta {
|
||||
"deleted": {
|
||||
"boundElements": [],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id172",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"id172" => Delta {
|
||||
"deleted": {
|
||||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "0.00990",
|
||||
"elementId": "id175",
|
||||
"fixedPoint": [
|
||||
"0.50000",
|
||||
1,
|
||||
],
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "0.98586",
|
||||
"height": "70.45017",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"-0.98586",
|
||||
"100.70774",
|
||||
"70.45017",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
|
@ -321,7 +338,7 @@ History {
|
|||
"focus": "-0.02000",
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "0.00000",
|
||||
"height": "0.09250",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
|
@ -329,7 +346,7 @@ History {
|
|||
],
|
||||
[
|
||||
"98.58579",
|
||||
"0.00000",
|
||||
"0.09250",
|
||||
],
|
||||
],
|
||||
"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 {
|
||||
"deleted": {
|
||||
"endBinding": {
|
||||
"elementId": "id175",
|
||||
"fixedPoint": [
|
||||
"0.50000",
|
||||
1,
|
||||
],
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "102.35417",
|
||||
"height": "102.45584",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"101.77517",
|
||||
"102.35417",
|
||||
"102.79971",
|
||||
"102.45584",
|
||||
],
|
||||
],
|
||||
"startBinding": null,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "0.00990",
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "0.98586",
|
||||
"height": "70.33521",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"-0.98586",
|
||||
"100.78887",
|
||||
"70.33521",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
|
@ -426,20 +429,7 @@ History {
|
|||
"focus": "0.02970",
|
||||
"gap": 1,
|
||||
},
|
||||
"y": "0.99364",
|
||||
},
|
||||
},
|
||||
"id175" => Delta {
|
||||
"deleted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id172",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
"y": "35.20327",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -739,7 +729,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 9,
|
||||
"version": 19,
|
||||
"width": 100,
|
||||
"x": 150,
|
||||
"y": -50,
|
||||
|
@ -819,8 +809,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 30,
|
||||
"width": 0,
|
||||
"version": 33,
|
||||
"width": 100,
|
||||
"x": "149.29289",
|
||||
"y": 0,
|
||||
}
|
||||
|
@ -846,20 +836,22 @@ History {
|
|||
"added": Map {},
|
||||
"removed": Map {},
|
||||
"updated": Map {
|
||||
"id167" => Delta {
|
||||
"id166" => Delta {
|
||||
"deleted": {
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"boundElements": [],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id167",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"id167" => Delta {
|
||||
"deleted": {
|
||||
"endBinding": null,
|
||||
"points": [
|
||||
[
|
||||
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 {
|
||||
"deleted": {
|
||||
"endBinding": null,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
|
@ -928,18 +923,13 @@ History {
|
|||
"startBinding": null,
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id166",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
},
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
0,
|
||||
100,
|
||||
0,
|
||||
],
|
||||
],
|
||||
|
@ -7348,8 +7338,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"updated": 1,
|
||||
"version": 7,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": -10,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -7422,8 +7412,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": -10,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"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 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`] = `
|
||||
{
|
||||
|
@ -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 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`] = `
|
||||
{
|
||||
|
@ -12138,8 +12128,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||
"updated": 1,
|
||||
"version": 3,
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"x": -10,
|
||||
"y": -10,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -12192,8 +12182,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||
"updated": 1,
|
||||
"version": 5,
|
||||
"width": 50,
|
||||
"x": 60,
|
||||
"y": 0,
|
||||
"x": 40,
|
||||
"y": -20,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -12246,8 +12236,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||
"updated": 1,
|
||||
"version": 4,
|
||||
"width": 50,
|
||||
"x": 150,
|
||||
"y": -10,
|
||||
"x": 130,
|
||||
"y": -30,
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -12301,8 +12291,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 10,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"x": -10,
|
||||
"y": -10,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
@ -12387,8 +12377,8 @@ History {
|
|||
"strokeWidth": 2,
|
||||
"type": "freedraw",
|
||||
"width": 50,
|
||||
"x": 150,
|
||||
"y": -10,
|
||||
"x": 130,
|
||||
"y": -30,
|
||||
},
|
||||
"inserted": {
|
||||
"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 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
|
||||
|
||||
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,
|
||||
"backgroundColor": "transparent",
|
||||
|
@ -54,13 +20,47 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1505387817,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"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,
|
||||
"width": 30,
|
||||
"x": -10,
|
||||
|
|
|
@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
|||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 400692809,
|
||||
"width": 70,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
|
@ -106,7 +106,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
|||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 400692809,
|
||||
"width": 70,
|
||||
"x": 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