mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge branch 'master' into update-binding-only-on-dragged-side
This commit is contained in:
commit
3ad0662036
84 changed files with 1546 additions and 1558 deletions
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
|
||||||
|
|
||||||
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
|
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should be a valid React Node.
|
||||||
|
|
||||||
**Usage**
|
**Usage**
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ function App() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This will only for `Desktop` devices.
|
This will only work for `Desktop` devices.
|
||||||
|
|
||||||
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ All `props` are _optional_.
|
||||||
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
| [`generateIdForFile`](#generateidforfile) | `function` | \_ | Allows you to override `id` generation for files added on canvas |
|
||||||
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
| [`validateEmbeddable`](#validateembeddable) | `string[]` \| `boolean` \| `RegExp` \| `RegExp[]` \| <code>((link: string) => boolean | undefined)</code> | \_ | use for custom src url validation |
|
||||||
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
| [`renderScrollbars`] | `boolean`| | `false` | Indicates whether scrollbars will be shown
|
||||||
|
|
||||||
### Storing custom data on Excalidraw elements
|
### Storing custom data on Excalidraw elements
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ export default function ExampleApp({
|
||||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||||
|
const [renderScrollbars, setRenderScrollbars] = useState(false);
|
||||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||||
|
@ -192,6 +193,7 @@ export default function ExampleApp({
|
||||||
}) => setPointerData(payload),
|
}) => setPointerData(payload),
|
||||||
viewModeEnabled,
|
viewModeEnabled,
|
||||||
zenModeEnabled,
|
zenModeEnabled,
|
||||||
|
renderScrollbars,
|
||||||
gridModeEnabled,
|
gridModeEnabled,
|
||||||
theme,
|
theme,
|
||||||
name: "Custom name of drawing",
|
name: "Custom name of drawing",
|
||||||
|
@ -710,6 +712,14 @@ export default function ExampleApp({
|
||||||
/>
|
/>
|
||||||
Grid mode
|
Grid mode
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={renderScrollbars}
|
||||||
|
onChange={() => setRenderScrollbars(!renderScrollbars)}
|
||||||
|
/>
|
||||||
|
Render scrollbars
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
|
@ -680,7 +680,7 @@ export const arrayToMap = <T extends { id: string } | string>(
|
||||||
return items.reduce((acc: Map<string, T>, element) => {
|
return items.reduce((acc: Map<string, T>, element) => {
|
||||||
acc.set(typeof element === "string" ? element : element.id, element);
|
acc.set(typeof element === "string" ? element : element.id, element);
|
||||||
return acc;
|
return acc;
|
||||||
}, new Map());
|
}, new Map() as Map<string, T>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const arrayToMapWithIndex = <T extends { id: string }>(
|
export const arrayToMapWithIndex = <T extends { id: string }>(
|
||||||
|
@ -1218,3 +1218,18 @@ export const elementCenterPoint = (
|
||||||
|
|
||||||
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
return pointFrom<GlobalPoint>(centerXPoint, centerYPoint);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** hack for Array.isArray type guard not working with readonly value[] */
|
||||||
|
export const isReadonlyArray = (value?: any): value is readonly any[] => {
|
||||||
|
return Array.isArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sizeOf = (
|
||||||
|
value: readonly number[] | Readonly<Map<any, any>> | Record<any, any>,
|
||||||
|
): number => {
|
||||||
|
return isReadonlyArray(value)
|
||||||
|
? value.length
|
||||||
|
: value instanceof Map
|
||||||
|
? value.size
|
||||||
|
: Object.keys(value).length;
|
||||||
|
};
|
||||||
|
|
|
@ -6,12 +6,14 @@ import {
|
||||||
toBrandedType,
|
toBrandedType,
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
|
isReadonlyArray,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { isNonDeletedElement } from "@excalidraw/element";
|
import { isNonDeletedElement } from "@excalidraw/element";
|
||||||
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
||||||
import { getElementsInGroup } from "@excalidraw/element/groups";
|
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
orderByFractionalIndex,
|
||||||
syncInvalidIndices,
|
syncInvalidIndices,
|
||||||
syncMovedIndices,
|
syncMovedIndices,
|
||||||
validateFractionalIndices,
|
validateFractionalIndices,
|
||||||
|
@ -19,7 +21,11 @@ import {
|
||||||
|
|
||||||
import { getSelectedElements } from "@excalidraw/element/selection";
|
import { getSelectedElements } from "@excalidraw/element/selection";
|
||||||
|
|
||||||
import type { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import {
|
||||||
|
mutateElement,
|
||||||
|
type ElementUpdate,
|
||||||
|
} from "@excalidraw/element/mutateElement";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
@ -32,12 +38,13 @@ import type {
|
||||||
Ordered,
|
Ordered,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Assert, SameType } from "@excalidraw/common/utility-types";
|
import type {
|
||||||
|
Assert,
|
||||||
|
Mutable,
|
||||||
|
SameType,
|
||||||
|
} from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../../excalidraw/types";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
|
||||||
|
|
||||||
type SceneStateCallback = () => void;
|
type SceneStateCallback = () => void;
|
||||||
type SceneStateCallbackRemover = () => void;
|
type SceneStateCallbackRemover = () => void;
|
||||||
|
@ -102,44 +109,7 @@ const hashSelectionOpts = (
|
||||||
// in our codebase
|
// in our codebase
|
||||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||||
|
|
||||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
|
||||||
if (typeof elementKey === "string") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Scene {
|
class Scene {
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// static methods/props
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
|
||||||
private static sceneMapById = new Map<string, Scene>();
|
|
||||||
|
|
||||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
|
||||||
if (isIdKey(elementKey)) {
|
|
||||||
// for cases where we don't have access to the element object
|
|
||||||
// (e.g. restore serialized appState with id references)
|
|
||||||
this.sceneMapById.set(elementKey, scene);
|
|
||||||
} else {
|
|
||||||
this.sceneMapByElement.set(elementKey, scene);
|
|
||||||
// if mapping element objects, also cache the id string when later
|
|
||||||
// looking up by id alone
|
|
||||||
this.sceneMapById.set(elementKey.id, scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated pass down `app.scene` and use it directly
|
|
||||||
*/
|
|
||||||
static getScene(elementKey: ElementKey): Scene | null {
|
|
||||||
if (isIdKey(elementKey)) {
|
|
||||||
return this.sceneMapById.get(elementKey) || null;
|
|
||||||
}
|
|
||||||
return this.sceneMapByElement.get(elementKey) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// instance methods/props
|
// instance methods/props
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -198,6 +168,12 @@ class Scene {
|
||||||
return this.frames;
|
return this.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(elements: ElementsMapOrArray | null = null) {
|
||||||
|
if (elements) {
|
||||||
|
this.replaceAllElements(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getSelectedElements(opts: {
|
getSelectedElements(opts: {
|
||||||
// NOTE can be ommitted by making Scene constructor require App instance
|
// NOTE can be ommitted by making Scene constructor require App instance
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
|
@ -292,23 +268,25 @@ class Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||||
const _nextElements =
|
|
||||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||||
nextElements instanceof Array
|
if (!isReadonlyArray(nextElements)) {
|
||||||
? nextElements
|
// need to order by fractional indices to get the correct order
|
||||||
: Array.from(nextElements.values());
|
nextElements = orderByFractionalIndex(
|
||||||
|
Array.from(nextElements.values()) as OrderedExcalidrawElement[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
||||||
validateIndicesThrottled(_nextElements);
|
validateIndicesThrottled(nextElements);
|
||||||
|
|
||||||
this.elements = syncInvalidIndices(_nextElements);
|
this.elements = syncInvalidIndices(nextElements);
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
this.elements.forEach((element) => {
|
this.elements.forEach((element) => {
|
||||||
if (isFrameLikeElement(element)) {
|
if (isFrameLikeElement(element)) {
|
||||||
nextFrameLikes.push(element);
|
nextFrameLikes.push(element);
|
||||||
}
|
}
|
||||||
this.elementsMap.set(element.id, element);
|
this.elementsMap.set(element.id, element);
|
||||||
Scene.mapElementToScene(element, this);
|
|
||||||
});
|
});
|
||||||
const nonDeletedElements = getNonDeletedElements(this.elements);
|
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||||
this.nonDeletedElements = nonDeletedElements.elements;
|
this.nonDeletedElements = nonDeletedElements.elements;
|
||||||
|
@ -353,12 +331,6 @@ class Scene {
|
||||||
this.selectedElementsCache.elements = null;
|
this.selectedElementsCache.elements = null;
|
||||||
this.selectedElementsCache.cache.clear();
|
this.selectedElementsCache.cache.clear();
|
||||||
|
|
||||||
Scene.sceneMapById.forEach((scene, elementKey) => {
|
|
||||||
if (scene === this) {
|
|
||||||
Scene.sceneMapById.delete(elementKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// done not for memory leaks, but to guard against possible late fires
|
// done not for memory leaks, but to guard against possible late fires
|
||||||
// (I guess?)
|
// (I guess?)
|
||||||
this.callbacks.clear();
|
this.callbacks.clear();
|
||||||
|
@ -455,6 +427,42 @@ class Scene {
|
||||||
// then, check if the id is a group
|
// then, check if the id is a group
|
||||||
return getElementsInGroup(elementsMap, id);
|
return getElementsInGroup(elementsMap, id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mutate an element with passed updates and trigger the component to update. Make sure you
|
||||||
|
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||||
|
mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||||
|
element: TElement,
|
||||||
|
updates: ElementUpdate<TElement>,
|
||||||
|
options: {
|
||||||
|
informMutation: boolean;
|
||||||
|
isDragging: boolean;
|
||||||
|
} = {
|
||||||
|
informMutation: true,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const elementsMap = this.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
const { version: prevVersion } = element;
|
||||||
|
const { version: nextVersion } = mutateElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
updates,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
// skip if the element is not in the scene (i.e. selection)
|
||||||
|
this.elementsMap.has(element.id) &&
|
||||||
|
// skip if the element's version hasn't changed, as mutateElement returned the same element
|
||||||
|
prevVersion !== nextVersion &&
|
||||||
|
options.informMutation
|
||||||
|
) {
|
||||||
|
this.triggerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Scene;
|
export default Scene;
|
|
@ -1,12 +1,11 @@
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBoundingBox } from "./bounds";
|
import { getCommonBoundingBox } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { BoundingBox } from "./bounds";
|
import type { BoundingBox } from "./bounds";
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
position: "start" | "center" | "end";
|
position: "start" | "center" | "end";
|
||||||
|
@ -15,10 +14,10 @@ export interface Alignment {
|
||||||
|
|
||||||
export const alignElements = (
|
export const alignElements = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -33,12 +32,13 @@ export const alignElements = (
|
||||||
);
|
);
|
||||||
return group.map((element) => {
|
return group.map((element) => {
|
||||||
// update element
|
// update element
|
||||||
const updatedEle = mutateElement(element, {
|
const updatedEle = scene.mutateElement(element, {
|
||||||
x: element.x + translation.x,
|
x: element.x + translation.x,
|
||||||
y: element.y + translation.y,
|
y: element.y + translation.y,
|
||||||
});
|
});
|
||||||
|
|
||||||
// update bound elements
|
// update bound elements
|
||||||
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: group,
|
simultaneouslyUpdated: group,
|
||||||
});
|
});
|
||||||
return updatedEle;
|
return updatedEle;
|
||||||
|
|
|
@ -31,8 +31,6 @@ import { isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
@ -56,7 +54,6 @@ import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isBindableElement,
|
isBindableElement,
|
||||||
isBindingElement,
|
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
|
@ -69,6 +66,8 @@ import {
|
||||||
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
import type {
|
import type {
|
||||||
|
@ -85,7 +84,6 @@ import type {
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
SceneElementsMap,
|
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
@ -131,7 +129,6 @@ export const bindOrUnbindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startBindingElement: ExcalidrawBindableElement | null | "keep",
|
startBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||||
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||||
|
@ -143,7 +140,7 @@ export const bindOrUnbindLinearElement = (
|
||||||
"start",
|
"start",
|
||||||
boundToElementIds,
|
boundToElementIds,
|
||||||
unboundFromElementIds,
|
unboundFromElementIds,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
bindOrUnbindLinearElementEdge(
|
bindOrUnbindLinearElementEdge(
|
||||||
linearElement,
|
linearElement,
|
||||||
|
@ -152,7 +149,7 @@ export const bindOrUnbindLinearElement = (
|
||||||
"end",
|
"end",
|
||||||
boundToElementIds,
|
boundToElementIds,
|
||||||
unboundFromElementIds,
|
unboundFromElementIds,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
||||||
|
@ -160,7 +157,7 @@ export const bindOrUnbindLinearElement = (
|
||||||
);
|
);
|
||||||
|
|
||||||
getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
|
getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
boundElements: element.boundElements?.filter(
|
boundElements: element.boundElements?.filter(
|
||||||
(element) =>
|
(element) =>
|
||||||
element.type !== "arrow" || element.id !== linearElement.id,
|
element.type !== "arrow" || element.id !== linearElement.id,
|
||||||
|
@ -178,7 +175,7 @@ const bindOrUnbindLinearElementEdge = (
|
||||||
boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
|
boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||||
// Is mutated
|
// Is mutated
|
||||||
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
||||||
if (bindableElement === "keep") {
|
if (bindableElement === "keep") {
|
||||||
|
@ -187,7 +184,7 @@ const bindOrUnbindLinearElementEdge = (
|
||||||
|
|
||||||
// null means break the bind, so nothing to consider here
|
// null means break the bind, so nothing to consider here
|
||||||
if (bindableElement === null) {
|
if (bindableElement === null) {
|
||||||
const unbound = unbindLinearElement(linearElement, startOrEnd);
|
const unbound = unbindLinearElement(linearElement, startOrEnd, scene);
|
||||||
if (unbound != null) {
|
if (unbound != null) {
|
||||||
unboundFromElementIds.add(unbound);
|
unboundFromElementIds.add(unbound);
|
||||||
}
|
}
|
||||||
|
@ -210,16 +207,11 @@ const bindOrUnbindLinearElementEdge = (
|
||||||
: startOrEnd === "start" ||
|
: startOrEnd === "start" ||
|
||||||
otherEdgeBindableElement.id !== bindableElement.id)
|
otherEdgeBindableElement.id !== bindableElement.id)
|
||||||
) {
|
) {
|
||||||
bindLinearElement(
|
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||||
linearElement,
|
|
||||||
bindableElement,
|
|
||||||
startOrEnd,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
boundToElementIds.add(bindableElement.id);
|
boundToElementIds.add(bindableElement.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap);
|
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||||
boundToElementIds.add(bindableElement.id);
|
boundToElementIds.add(bindableElement.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -345,11 +337,9 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
||||||
|
|
||||||
export const bindOrUnbindLinearElements = (
|
export const bindOrUnbindLinearElements = (
|
||||||
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
|
||||||
isBindingEnabled: boolean,
|
isBindingEnabled: boolean,
|
||||||
draggingPoints: readonly number[] | null,
|
draggingPoints: readonly number[] | null,
|
||||||
|
scene: Scene,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): void => {
|
): void => {
|
||||||
selectedElements.forEach((selectedElement) => {
|
selectedElements.forEach((selectedElement) => {
|
||||||
|
@ -359,20 +349,20 @@ export const bindOrUnbindLinearElements = (
|
||||||
selectedElement,
|
selectedElement,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
draggingPoints ?? [],
|
draggingPoints ?? [],
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
elements,
|
scene.getNonDeletedElements(),
|
||||||
zoom,
|
zoom,
|
||||||
)
|
)
|
||||||
: // The arrow itself (the shaft) or the inner joins are dragged
|
: // The arrow itself (the shaft) or the inner joins are dragged
|
||||||
getBindingStrategyForDraggingArrowOrJoints(
|
getBindingStrategyForDraggingArrowOrJoints(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
elements,
|
scene.getNonDeletedElements(),
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
zoom,
|
zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
|
bindOrUnbindLinearElement(selectedElement, start, end, scene);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -412,15 +402,17 @@ export const maybeBindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
pointerCoords: { x: number; y: number },
|
pointerCoords: { x: number; y: number },
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
): void => {
|
): void => {
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
if (appState.startBoundElement != null) {
|
if (appState.startBoundElement != null) {
|
||||||
bindLinearElement(
|
bindLinearElement(
|
||||||
linearElement,
|
linearElement,
|
||||||
appState.startBoundElement,
|
appState.startBoundElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,7 +433,7 @@ export const maybeBindLinearElement = (
|
||||||
"end",
|
"end",
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
|
bindLinearElement(linearElement, hoveredElement, "end", scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -470,7 +462,7 @@ export const bindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
): void => {
|
): void => {
|
||||||
if (!isArrowElement(linearElement)) {
|
if (!isArrowElement(linearElement)) {
|
||||||
return;
|
return;
|
||||||
|
@ -483,7 +475,7 @@ export const bindLinearElement = (
|
||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
elementsMap,
|
scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
),
|
),
|
||||||
|
@ -496,18 +488,17 @@ export const bindLinearElement = (
|
||||||
linearElement,
|
linearElement,
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
startOrEnd,
|
startOrEnd,
|
||||||
elementsMap,
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(linearElement, {
|
scene.mutateElement(linearElement, {
|
||||||
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
|
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
|
||||||
if (!boundElementsMap.has(linearElement.id)) {
|
if (!boundElementsMap.has(linearElement.id)) {
|
||||||
mutateElement(hoveredElement, {
|
scene.mutateElement(hoveredElement, {
|
||||||
boundElements: (hoveredElement.boundElements || []).concat({
|
boundElements: (hoveredElement.boundElements || []).concat({
|
||||||
id: linearElement.id,
|
id: linearElement.id,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
|
@ -549,13 +540,14 @@ const isLinearElementSimple = (
|
||||||
const unbindLinearElement = (
|
const unbindLinearElement = (
|
||||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
|
scene: Scene,
|
||||||
): ExcalidrawBindableElement["id"] | null => {
|
): ExcalidrawBindableElement["id"] | null => {
|
||||||
const field = startOrEnd === "start" ? "startBinding" : "endBinding";
|
const field = startOrEnd === "start" ? "startBinding" : "endBinding";
|
||||||
const binding = linearElement[field];
|
const binding = linearElement[field];
|
||||||
if (binding == null) {
|
if (binding == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
mutateElement(linearElement, { [field]: null });
|
scene.mutateElement(linearElement, { [field]: null });
|
||||||
return binding.elementId;
|
return binding.elementId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -723,7 +715,7 @@ const calculateFocusAndGap = (
|
||||||
// in explicitly.
|
// in explicitly.
|
||||||
export const updateBoundElements = (
|
export const updateBoundElements = (
|
||||||
changedElement: NonDeletedExcalidrawElement,
|
changedElement: NonDeletedExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
scene: Scene,
|
||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
|
@ -739,6 +731,8 @@ export const updateBoundElements = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||||
if (!isLinearElement(element) || element.isDeleted) {
|
if (!isLinearElement(element) || element.isDeleted) {
|
||||||
return;
|
return;
|
||||||
|
@ -779,7 +773,7 @@ export const updateBoundElements = (
|
||||||
|
|
||||||
// `linearElement` is being moved/scaled already, just update the binding
|
// `linearElement` is being moved/scaled already, just update the binding
|
||||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||||
mutateElement(element, bindings, true);
|
scene.mutateElement(element, bindings);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -826,23 +820,18 @@ export const updateBoundElements = (
|
||||||
}> => update !== null,
|
}> => update !== null,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(element, scene, updates, {
|
||||||
element,
|
|
||||||
updates,
|
|
||||||
{
|
|
||||||
...(changedElement.id === element.startBinding?.elementId
|
...(changedElement.id === element.startBinding?.elementId
|
||||||
? { startBinding: bindings.startBinding }
|
? { startBinding: bindings.startBinding }
|
||||||
: {}),
|
: {}),
|
||||||
...(changedElement.id === element.endBinding?.elementId
|
...(changedElement.id === element.endBinding?.elementId
|
||||||
? { endBinding: bindings.endBinding }
|
? { endBinding: bindings.endBinding }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
});
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !boundText.isDeleted) {
|
if (boundText && !boundText.isDeleted) {
|
||||||
handleBindTextResize(element, elementsMap, false);
|
handleBindTextResize(element, scene, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -868,7 +857,6 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
otherPoint: Readonly<GlobalPoint>,
|
otherPoint: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||||
aabb: Bounds | undefined | null,
|
aabb: Bounds | undefined | null,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
): Heading => {
|
): Heading => {
|
||||||
|
@ -878,12 +866,7 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
return otherPointHeading;
|
return otherPointHeading;
|
||||||
}
|
}
|
||||||
|
|
||||||
const distance = getDistanceForBinding(
|
const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
|
||||||
origPoint,
|
|
||||||
bindableElement,
|
|
||||||
elementsMap,
|
|
||||||
zoom,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!distance) {
|
if (!distance) {
|
||||||
return vectorToHeading(
|
return vectorToHeading(
|
||||||
|
@ -897,7 +880,6 @@ export const getHeadingForElbowArrowSnap = (
|
||||||
const getDistanceForBinding = (
|
const getDistanceForBinding = (
|
||||||
point: Readonly<GlobalPoint>,
|
point: Readonly<GlobalPoint>,
|
||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const distance = distanceToBindableElement(bindableElement, point);
|
const distance = distanceToBindableElement(bindableElement, point);
|
||||||
|
@ -1199,7 +1181,6 @@ const updateBoundPoint = (
|
||||||
linearElement,
|
linearElement,
|
||||||
bindableElement,
|
bindableElement,
|
||||||
startOrEnd === "startBinding" ? "start" : "end",
|
startOrEnd === "startBinding" ? "start" : "end",
|
||||||
elementsMap,
|
|
||||||
).fixedPoint;
|
).fixedPoint;
|
||||||
const globalMidPoint = elementCenterPoint(bindableElement);
|
const globalMidPoint = elementCenterPoint(bindableElement);
|
||||||
const global = pointFrom<GlobalPoint>(
|
const global = pointFrom<GlobalPoint>(
|
||||||
|
@ -1303,7 +1284,6 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||||
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
||||||
hoveredElement: ExcalidrawBindableElement,
|
hoveredElement: ExcalidrawBindableElement,
|
||||||
startOrEnd: "start" | "end",
|
startOrEnd: "start" | "end",
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): { fixedPoint: FixedPoint } => {
|
): { fixedPoint: FixedPoint } => {
|
||||||
const bounds = [
|
const bounds = [
|
||||||
hoveredElement.x,
|
hoveredElement.x,
|
||||||
|
@ -1391,19 +1371,19 @@ const getLinearElementEdgeCoors = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fixDuplicatedBindingsAfterDuplication = (
|
export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
newElements: ExcalidrawElement[],
|
duplicatedElements: ExcalidrawElement[],
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
duplicatedElementsMap: NonDeletedSceneElementsMap,
|
duplicateElementsMap: NonDeletedSceneElementsMap,
|
||||||
) => {
|
) => {
|
||||||
for (const element of newElements) {
|
for (const duplicateElement of duplicatedElements) {
|
||||||
if ("boundElements" in element && element.boundElements) {
|
if ("boundElements" in duplicateElement && duplicateElement.boundElements) {
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
boundElements: element.boundElements.reduce(
|
boundElements: duplicateElement.boundElements.reduce(
|
||||||
(
|
(
|
||||||
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
|
||||||
binding,
|
binding,
|
||||||
) => {
|
) => {
|
||||||
const newBindingId = oldIdToDuplicatedId.get(binding.id);
|
const newBindingId = origIdToDuplicateId.get(binding.id);
|
||||||
if (newBindingId) {
|
if (newBindingId) {
|
||||||
acc.push({ ...binding, id: newBindingId });
|
acc.push({ ...binding, id: newBindingId });
|
||||||
}
|
}
|
||||||
|
@ -1414,46 +1394,47 @@ export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("containerId" in element && element.containerId) {
|
if ("containerId" in duplicateElement && duplicateElement.containerId) {
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
containerId: oldIdToDuplicatedId.get(element.containerId) ?? null,
|
containerId:
|
||||||
|
origIdToDuplicateId.get(duplicateElement.containerId) ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("endBinding" in element && element.endBinding) {
|
if ("endBinding" in duplicateElement && duplicateElement.endBinding) {
|
||||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
const newEndBindingId = origIdToDuplicateId.get(
|
||||||
element.endBinding.elementId,
|
duplicateElement.endBinding.elementId,
|
||||||
);
|
);
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
endBinding: newEndBindingId
|
endBinding: newEndBindingId
|
||||||
? {
|
? {
|
||||||
...element.endBinding,
|
...duplicateElement.endBinding,
|
||||||
elementId: newEndBindingId,
|
elementId: newEndBindingId,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ("startBinding" in element && element.startBinding) {
|
if ("startBinding" in duplicateElement && duplicateElement.startBinding) {
|
||||||
const newEndBindingId = oldIdToDuplicatedId.get(
|
const newEndBindingId = origIdToDuplicateId.get(
|
||||||
element.startBinding.elementId,
|
duplicateElement.startBinding.elementId,
|
||||||
);
|
);
|
||||||
Object.assign(element, {
|
Object.assign(duplicateElement, {
|
||||||
startBinding: newEndBindingId
|
startBinding: newEndBindingId
|
||||||
? {
|
? {
|
||||||
...element.startBinding,
|
...duplicateElement.startBinding,
|
||||||
elementId: newEndBindingId,
|
elementId: newEndBindingId,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
if (isElbowArrow(duplicateElement)) {
|
||||||
Object.assign(
|
Object.assign(
|
||||||
element,
|
duplicateElement,
|
||||||
updateElbowArrowPoints(element, duplicatedElementsMap, {
|
updateElbowArrowPoints(duplicateElement, duplicateElementsMap, {
|
||||||
points: [
|
points: [
|
||||||
element.points[0],
|
duplicateElement.points[0],
|
||||||
element.points[element.points.length - 1],
|
duplicateElement.points[duplicateElement.points.length - 1],
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -1461,196 +1442,6 @@ export const fixDuplicatedBindingsAfterDuplication = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fixReversedBindingsForBindables = (
|
|
||||||
original: ExcalidrawBindableElement,
|
|
||||||
duplicate: ExcalidrawBindableElement,
|
|
||||||
originalElements: Map<string, ExcalidrawElement>,
|
|
||||||
elementsWithClones: ExcalidrawElement[],
|
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
|
||||||
) => {
|
|
||||||
original.boundElements?.forEach((binding, idx) => {
|
|
||||||
if (binding.type !== "arrow") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldArrow = elementsWithClones.find((el) => el.id === binding.id);
|
|
||||||
|
|
||||||
if (!isBindingElement(oldArrow)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalElements.has(binding.id)) {
|
|
||||||
// Linked arrow is in the selection, so find the duplicate pair
|
|
||||||
const newArrowId = oldIdToDuplicatedId.get(binding.id) ?? binding.id;
|
|
||||||
const newArrow = elementsWithClones.find(
|
|
||||||
(el) => el.id === newArrowId,
|
|
||||||
)! as ExcalidrawArrowElement;
|
|
||||||
|
|
||||||
mutateElement(newArrow, {
|
|
||||||
startBinding:
|
|
||||||
oldArrow.startBinding?.elementId === binding.id
|
|
||||||
? {
|
|
||||||
...oldArrow.startBinding,
|
|
||||||
elementId: duplicate.id,
|
|
||||||
}
|
|
||||||
: newArrow.startBinding,
|
|
||||||
endBinding:
|
|
||||||
oldArrow.endBinding?.elementId === binding.id
|
|
||||||
? {
|
|
||||||
...oldArrow.endBinding,
|
|
||||||
elementId: duplicate.id,
|
|
||||||
}
|
|
||||||
: newArrow.endBinding,
|
|
||||||
});
|
|
||||||
mutateElement(duplicate, {
|
|
||||||
boundElements: [
|
|
||||||
...(duplicate.boundElements ?? []).filter(
|
|
||||||
(el) => el.id !== binding.id && el.id !== newArrowId,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
id: newArrowId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Linked arrow is outside the selection,
|
|
||||||
// so we move the binding to the duplicate
|
|
||||||
mutateElement(oldArrow, {
|
|
||||||
startBinding:
|
|
||||||
oldArrow.startBinding?.elementId === original.id
|
|
||||||
? {
|
|
||||||
...oldArrow.startBinding,
|
|
||||||
elementId: duplicate.id,
|
|
||||||
}
|
|
||||||
: oldArrow.startBinding,
|
|
||||||
endBinding:
|
|
||||||
oldArrow.endBinding?.elementId === original.id
|
|
||||||
? {
|
|
||||||
...oldArrow.endBinding,
|
|
||||||
elementId: duplicate.id,
|
|
||||||
}
|
|
||||||
: oldArrow.endBinding,
|
|
||||||
});
|
|
||||||
mutateElement(duplicate, {
|
|
||||||
boundElements: [
|
|
||||||
...(duplicate.boundElements ?? []),
|
|
||||||
{
|
|
||||||
type: "arrow",
|
|
||||||
id: oldArrow.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
mutateElement(original, {
|
|
||||||
boundElements:
|
|
||||||
original.boundElements?.filter((_, i) => i !== idx) ?? null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fixReversedBindingsForArrows = (
|
|
||||||
original: ExcalidrawArrowElement,
|
|
||||||
duplicate: ExcalidrawArrowElement,
|
|
||||||
originalElements: Map<string, ExcalidrawElement>,
|
|
||||||
bindingProp: "startBinding" | "endBinding",
|
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
|
||||||
elementsWithClones: ExcalidrawElement[],
|
|
||||||
) => {
|
|
||||||
const oldBindableId = original[bindingProp]?.elementId;
|
|
||||||
|
|
||||||
if (oldBindableId) {
|
|
||||||
if (originalElements.has(oldBindableId)) {
|
|
||||||
// Linked element is in the selection
|
|
||||||
const newBindableId =
|
|
||||||
oldIdToDuplicatedId.get(oldBindableId) ?? oldBindableId;
|
|
||||||
const newBindable = elementsWithClones.find(
|
|
||||||
(el) => el.id === newBindableId,
|
|
||||||
) as ExcalidrawBindableElement;
|
|
||||||
mutateElement(duplicate, {
|
|
||||||
[bindingProp]: {
|
|
||||||
...original[bindingProp],
|
|
||||||
elementId: newBindableId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
mutateElement(newBindable, {
|
|
||||||
boundElements: [
|
|
||||||
...(newBindable.boundElements ?? []).filter(
|
|
||||||
(el) => el.id !== original.id && el.id !== duplicate.id,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
id: duplicate.id,
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Linked element is outside the selection
|
|
||||||
const originalBindable = elementsWithClones.find(
|
|
||||||
(el) => el.id === oldBindableId,
|
|
||||||
);
|
|
||||||
if (originalBindable) {
|
|
||||||
mutateElement(duplicate, {
|
|
||||||
[bindingProp]: original[bindingProp],
|
|
||||||
});
|
|
||||||
mutateElement(original, {
|
|
||||||
[bindingProp]: null,
|
|
||||||
});
|
|
||||||
mutateElement(originalBindable, {
|
|
||||||
boundElements: [
|
|
||||||
...(originalBindable.boundElements?.filter(
|
|
||||||
(el) => el.id !== original.id,
|
|
||||||
) ?? []),
|
|
||||||
{
|
|
||||||
id: duplicate.id,
|
|
||||||
type: "arrow",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fixReversedBindings = (
|
|
||||||
originalElements: Map<string, ExcalidrawElement>,
|
|
||||||
elementsWithClones: ExcalidrawElement[],
|
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
|
||||||
) => {
|
|
||||||
for (const original of originalElements.values()) {
|
|
||||||
const duplicate = elementsWithClones.find(
|
|
||||||
(el) => el.id === oldIdToDuplicatedId.get(original.id),
|
|
||||||
)!;
|
|
||||||
|
|
||||||
if (isBindableElement(original) && isBindableElement(duplicate)) {
|
|
||||||
fixReversedBindingsForBindables(
|
|
||||||
original,
|
|
||||||
duplicate,
|
|
||||||
originalElements,
|
|
||||||
elementsWithClones,
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
);
|
|
||||||
} else if (isArrowElement(original) && isArrowElement(duplicate)) {
|
|
||||||
fixReversedBindingsForArrows(
|
|
||||||
original,
|
|
||||||
duplicate,
|
|
||||||
originalElements,
|
|
||||||
"startBinding",
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
elementsWithClones,
|
|
||||||
);
|
|
||||||
fixReversedBindingsForArrows(
|
|
||||||
original,
|
|
||||||
duplicate,
|
|
||||||
originalElements,
|
|
||||||
"endBinding",
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
elementsWithClones,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fixBindingsAfterDeletion = (
|
export const fixBindingsAfterDeletion = (
|
||||||
sceneElements: readonly ExcalidrawElement[],
|
sceneElements: readonly ExcalidrawElement[],
|
||||||
deletedElements: readonly ExcalidrawElement[],
|
deletedElements: readonly ExcalidrawElement[],
|
||||||
|
@ -1658,8 +1449,12 @@ export const fixBindingsAfterDeletion = (
|
||||||
const elements = arrayToMap(sceneElements);
|
const elements = arrayToMap(sceneElements);
|
||||||
|
|
||||||
for (const element of deletedElements) {
|
for (const element of deletedElements) {
|
||||||
BoundElement.unbindAffected(elements, element, mutateElement);
|
BoundElement.unbindAffected(elements, element, (element, updates) =>
|
||||||
BindableElement.unbindAffected(elements, element, mutateElement);
|
mutateElement(element, elements, updates),
|
||||||
|
);
|
||||||
|
BindableElement.unbindAffected(elements, element, (element, updates) =>
|
||||||
|
mutateElement(element, elements, updates),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
|
|
||||||
import { rescalePoints, arrayToMap, invariant } from "@excalidraw/common";
|
import {
|
||||||
|
rescalePoints,
|
||||||
|
arrayToMap,
|
||||||
|
invariant,
|
||||||
|
sizeOf,
|
||||||
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
degreesToRadians,
|
||||||
|
@ -57,6 +62,7 @@ import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
|
ElementsMapOrArray,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Drawable, Op } from "roughjs/bin/core";
|
import type { Drawable, Op } from "roughjs/bin/core";
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
|
@ -938,10 +944,10 @@ export const getElementBounds = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommonBounds = (
|
export const getCommonBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: ElementsMapOrArray,
|
||||||
elementsMap?: ElementsMap,
|
elementsMap?: ElementsMap,
|
||||||
): Bounds => {
|
): Bounds => {
|
||||||
if (!elements.length) {
|
if (!sizeOf(elements)) {
|
||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,10 @@ import type {
|
||||||
PointerDownState,
|
PointerDownState,
|
||||||
} from "@excalidraw/excalidraw/types";
|
} from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { getMinTextElementWidth } from "./textMeasurements";
|
import { getMinTextElementWidth } from "./textMeasurements";
|
||||||
|
@ -29,6 +26,8 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type { ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
|
@ -104,7 +103,7 @@ export const dragSelectedElements = (
|
||||||
);
|
);
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||||
if (!isArrowElement(element)) {
|
if (!isArrowElement(element)) {
|
||||||
// skip arrow labels since we calculate its position during render
|
// skip arrow labels since we calculate its position during render
|
||||||
const textElement = getBoundTextElement(
|
const textElement = getBoundTextElement(
|
||||||
|
@ -112,9 +111,14 @@ export const dragSelectedElements = (
|
||||||
scene.getNonDeletedElementsMap(),
|
scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
if (textElement) {
|
if (textElement) {
|
||||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
updateElementCoords(
|
||||||
|
pointerDownState,
|
||||||
|
textElement,
|
||||||
|
scene,
|
||||||
|
adjustedOffset,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -155,6 +159,7 @@ const calculateOffset = (
|
||||||
const updateElementCoords = (
|
const updateElementCoords = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
scene: Scene,
|
||||||
dragOffset: { x: number; y: number },
|
dragOffset: { x: number; y: number },
|
||||||
) => {
|
) => {
|
||||||
const originalElement =
|
const originalElement =
|
||||||
|
@ -163,7 +168,7 @@ const updateElementCoords = (
|
||||||
const nextX = originalElement.x + dragOffset.x;
|
const nextX = originalElement.x + dragOffset.x;
|
||||||
const nextY = originalElement.y + dragOffset.y;
|
const nextY = originalElement.y + dragOffset.y;
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
x: nextX,
|
x: nextX,
|
||||||
y: nextY,
|
y: nextY,
|
||||||
});
|
});
|
||||||
|
@ -190,6 +195,7 @@ export const dragNewElement = ({
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
zoom,
|
zoom,
|
||||||
|
scene,
|
||||||
widthAspectRatio = null,
|
widthAspectRatio = null,
|
||||||
originOffset = null,
|
originOffset = null,
|
||||||
informMutation = true,
|
informMutation = true,
|
||||||
|
@ -205,6 +211,7 @@ export const dragNewElement = ({
|
||||||
shouldMaintainAspectRatio: boolean;
|
shouldMaintainAspectRatio: boolean;
|
||||||
shouldResizeFromCenter: boolean;
|
shouldResizeFromCenter: boolean;
|
||||||
zoom: NormalizedZoomValue;
|
zoom: NormalizedZoomValue;
|
||||||
|
scene: Scene;
|
||||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||||
true */
|
true */
|
||||||
widthAspectRatio?: number | null;
|
widthAspectRatio?: number | null;
|
||||||
|
@ -285,7 +292,7 @@ export const dragNewElement = ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
x: newX + (originOffset?.x ?? 0),
|
x: newX + (originOffset?.x ?? 0),
|
||||||
|
@ -295,7 +302,7 @@ export const dragNewElement = ({
|
||||||
...textAutoResize,
|
...textAutoResize,
|
||||||
...imageInitialDimension,
|
...imageInitialDimension,
|
||||||
},
|
},
|
||||||
informMutation,
|
{ informMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -36,10 +36,7 @@ import {
|
||||||
|
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
|
|
||||||
import {
|
import { fixDuplicatedBindingsAfterDuplication } from "./binding";
|
||||||
fixDuplicatedBindingsAfterDuplication,
|
|
||||||
fixReversedBindings,
|
|
||||||
} from "./binding";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
@ -60,16 +57,14 @@ import type {
|
||||||
* multiple elements at once, share this map
|
* multiple elements at once, share this map
|
||||||
* amongst all of them
|
* amongst all of them
|
||||||
* @param element Element to duplicate
|
* @param element Element to duplicate
|
||||||
* @param overrides Any element properties to override
|
|
||||||
*/
|
*/
|
||||||
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||||
editingGroupId: AppState["editingGroupId"],
|
editingGroupId: AppState["editingGroupId"],
|
||||||
groupIdMapForOperation: Map<GroupId, GroupId>,
|
groupIdMapForOperation: Map<GroupId, GroupId>,
|
||||||
element: TElement,
|
element: TElement,
|
||||||
overrides?: Partial<TElement>,
|
|
||||||
randomizeSeed?: boolean,
|
randomizeSeed?: boolean,
|
||||||
): Readonly<TElement> => {
|
): Readonly<TElement> => {
|
||||||
let copy = deepCopyElement(element);
|
const copy = deepCopyElement(element);
|
||||||
|
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
__test__defineOrigId(copy, element.id);
|
__test__defineOrigId(copy, element.id);
|
||||||
|
@ -92,9 +87,6 @@ export const duplicateElement = <TElement extends ExcalidrawElement>(
|
||||||
return groupIdMapForOperation.get(groupId)!;
|
return groupIdMapForOperation.get(groupId)!;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (overrides) {
|
|
||||||
copy = Object.assign(copy, overrides);
|
|
||||||
}
|
|
||||||
return copy;
|
return copy;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,9 +94,14 @@ export const duplicateElements = (
|
||||||
opts: {
|
opts: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
randomizeSeed?: boolean;
|
randomizeSeed?: boolean;
|
||||||
overrides?: (
|
overrides?: (data: {
|
||||||
originalElement: ExcalidrawElement,
|
duplicateElement: ExcalidrawElement;
|
||||||
) => Partial<ExcalidrawElement>;
|
origElement: ExcalidrawElement;
|
||||||
|
origIdToDuplicateId: Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement["id"]
|
||||||
|
>;
|
||||||
|
}) => Partial<ExcalidrawElement>;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
/**
|
/**
|
||||||
|
@ -132,14 +129,6 @@ export const duplicateElements = (
|
||||||
editingGroupId: AppState["editingGroupId"];
|
editingGroupId: AppState["editingGroupId"];
|
||||||
selectedGroupIds: AppState["selectedGroupIds"];
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* If true, duplicated elements are inserted _before_ specified
|
|
||||||
* elements. Case: alt-dragging elements to duplicate them.
|
|
||||||
*
|
|
||||||
* TODO: remove this once (if) we stop replacing the original element
|
|
||||||
* with the duplicated one in the scene array.
|
|
||||||
*/
|
|
||||||
reverseOrder: boolean;
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
) => {
|
) => {
|
||||||
|
@ -153,8 +142,6 @@ export const duplicateElements = (
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
} as const);
|
} as const);
|
||||||
|
|
||||||
const reverseOrder = opts.type === "in-place" ? opts.reverseOrder : false;
|
|
||||||
|
|
||||||
// Ids of elements that have already been processed so we don't push them
|
// Ids of elements that have already been processed so we don't push them
|
||||||
// into the array twice if we end up backtracking when retrieving
|
// into the array twice if we end up backtracking when retrieving
|
||||||
// discontiguous group of elements (can happen due to a bug, or in edge
|
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||||
|
@ -167,10 +154,17 @@ export const duplicateElements = (
|
||||||
// loop over them.
|
// loop over them.
|
||||||
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||||
const groupIdMap = new Map();
|
const groupIdMap = new Map();
|
||||||
const newElements: ExcalidrawElement[] = [];
|
const duplicatedElements: ExcalidrawElement[] = [];
|
||||||
const oldElements: ExcalidrawElement[] = [];
|
const origElements: ExcalidrawElement[] = [];
|
||||||
const oldIdToDuplicatedId = new Map();
|
const origIdToDuplicateId = new Map<
|
||||||
const duplicatedElementsMap = new Map<string, ExcalidrawElement>();
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement["id"]
|
||||||
|
>();
|
||||||
|
const duplicateIdToOrigElement = new Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement
|
||||||
|
>();
|
||||||
|
const duplicateElementsMap = new Map<string, ExcalidrawElement>();
|
||||||
const elementsMap = arrayToMap(elements) as ElementsMap;
|
const elementsMap = arrayToMap(elements) as ElementsMap;
|
||||||
const _idsOfElementsToDuplicate =
|
const _idsOfElementsToDuplicate =
|
||||||
opts.type === "in-place"
|
opts.type === "in-place"
|
||||||
|
@ -188,7 +182,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
elements = normalizeElementOrder(elements);
|
elements = normalizeElementOrder(elements);
|
||||||
|
|
||||||
const elementsWithClones: ExcalidrawElement[] = elements.slice();
|
const elementsWithDuplicates: ExcalidrawElement[] = elements.slice();
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
@ -214,17 +208,17 @@ export const duplicateElements = (
|
||||||
appState.editingGroupId,
|
appState.editingGroupId,
|
||||||
groupIdMap,
|
groupIdMap,
|
||||||
element,
|
element,
|
||||||
opts.overrides?.(element),
|
|
||||||
opts.randomizeSeed,
|
opts.randomizeSeed,
|
||||||
);
|
);
|
||||||
|
|
||||||
processedIds.set(newElement.id, true);
|
processedIds.set(newElement.id, true);
|
||||||
|
|
||||||
duplicatedElementsMap.set(newElement.id, newElement);
|
duplicateElementsMap.set(newElement.id, newElement);
|
||||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
origIdToDuplicateId.set(element.id, newElement.id);
|
||||||
|
duplicateIdToOrigElement.set(newElement.id, element);
|
||||||
|
|
||||||
oldElements.push(element);
|
origElements.push(element);
|
||||||
newElements.push(newElement);
|
duplicatedElements.push(newElement);
|
||||||
|
|
||||||
acc.push(newElement);
|
acc.push(newElement);
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -248,21 +242,12 @@ export const duplicateElements = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reverseOrder && index < 1) {
|
if (index > elementsWithDuplicates.length - 1) {
|
||||||
elementsWithClones.unshift(...castArray(elements));
|
elementsWithDuplicates.push(...castArray(elements));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reverseOrder && index > elementsWithClones.length - 1) {
|
elementsWithDuplicates.splice(index + 1, 0, ...castArray(elements));
|
||||||
elementsWithClones.push(...castArray(elements));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
elementsWithClones.splice(
|
|
||||||
index + (reverseOrder ? 0 : 1),
|
|
||||||
0,
|
|
||||||
...castArray(elements),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const frameIdsToDuplicate = new Set(
|
const frameIdsToDuplicate = new Set(
|
||||||
|
@ -294,11 +279,7 @@ export const duplicateElements = (
|
||||||
: [element],
|
: [element],
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetIndex = reverseOrder
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
? elementsWithClones.findIndex((el) => {
|
|
||||||
return el.groupIds?.includes(groupId);
|
|
||||||
})
|
|
||||||
: findLastIndex(elementsWithClones, (el) => {
|
|
||||||
return el.groupIds?.includes(groupId);
|
return el.groupIds?.includes(groupId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -318,7 +299,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
const frameChildren = getFrameChildren(elements, frameId);
|
const frameChildren = getFrameChildren(elements, frameId);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return el.frameId === frameId || el.id === frameId;
|
return el.frameId === frameId || el.id === frameId;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -335,7 +316,7 @@ export const duplicateElements = (
|
||||||
if (hasBoundTextElement(element)) {
|
if (hasBoundTextElement(element)) {
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return (
|
return (
|
||||||
el.id === element.id ||
|
el.id === element.id ||
|
||||||
("containerId" in el && el.containerId === element.id)
|
("containerId" in el && el.containerId === element.id)
|
||||||
|
@ -344,7 +325,7 @@ export const duplicateElements = (
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
insertBeforeOrAfterIndex(
|
insertBeforeOrAfterIndex(
|
||||||
targetIndex + (reverseOrder ? -1 : 0),
|
targetIndex,
|
||||||
copyElements([element, boundTextElement]),
|
copyElements([element, boundTextElement]),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -357,7 +338,7 @@ export const duplicateElements = (
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
const container = getContainerElement(element, elementsMap);
|
const container = getContainerElement(element, elementsMap);
|
||||||
|
|
||||||
const targetIndex = findLastIndex(elementsWithClones, (el) => {
|
const targetIndex = findLastIndex(elementsWithDuplicates, (el) => {
|
||||||
return el.id === element.id || el.id === container?.id;
|
return el.id === element.id || el.id === container?.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -377,7 +358,7 @@ export const duplicateElements = (
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
insertBeforeOrAfterIndex(
|
insertBeforeOrAfterIndex(
|
||||||
findLastIndex(elementsWithClones, (el) => el.id === element.id),
|
findLastIndex(elementsWithDuplicates, (el) => el.id === element.id),
|
||||||
copyElements(element),
|
copyElements(element),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -385,28 +366,38 @@ export const duplicateElements = (
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
fixDuplicatedBindingsAfterDuplication(
|
fixDuplicatedBindingsAfterDuplication(
|
||||||
newElements,
|
duplicatedElements,
|
||||||
oldIdToDuplicatedId,
|
origIdToDuplicateId,
|
||||||
duplicatedElementsMap as NonDeletedSceneElementsMap,
|
duplicateElementsMap as NonDeletedSceneElementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (reverseOrder) {
|
|
||||||
fixReversedBindings(
|
|
||||||
_idsOfElementsToDuplicate,
|
|
||||||
elementsWithClones,
|
|
||||||
oldIdToDuplicatedId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindElementsToFramesAfterDuplication(
|
bindElementsToFramesAfterDuplication(
|
||||||
elementsWithClones,
|
elementsWithDuplicates,
|
||||||
oldElements,
|
origElements,
|
||||||
oldIdToDuplicatedId,
|
origIdToDuplicateId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (opts.overrides) {
|
||||||
|
for (const duplicateElement of duplicatedElements) {
|
||||||
|
const origElement = duplicateIdToOrigElement.get(duplicateElement.id);
|
||||||
|
if (origElement) {
|
||||||
|
Object.assign(
|
||||||
|
duplicateElement,
|
||||||
|
opts.overrides({
|
||||||
|
duplicateElement,
|
||||||
|
origElement,
|
||||||
|
origIdToDuplicateId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newElements,
|
duplicatedElements,
|
||||||
elementsWithClones,
|
duplicateElementsMap,
|
||||||
|
elementsWithDuplicates,
|
||||||
|
origIdToDuplicateId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,6 @@ import { isBindableElement } from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
type ExcalidrawElbowArrowElement,
|
type ExcalidrawElbowArrowElement,
|
||||||
type NonDeletedSceneElementsMap,
|
type NonDeletedSceneElementsMap,
|
||||||
type SceneElementsMap,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||||
|
@ -887,7 +886,7 @@ export const updateElbowArrowPoints = (
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
updates: {
|
updates: {
|
||||||
points?: readonly LocalPoint[];
|
points?: readonly LocalPoint[];
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: readonly FixedSegment[] | null;
|
||||||
startBinding?: FixedPointBinding | null;
|
startBinding?: FixedPointBinding | null;
|
||||||
endBinding?: FixedPointBinding | null;
|
endBinding?: FixedPointBinding | null;
|
||||||
},
|
},
|
||||||
|
@ -1273,14 +1272,12 @@ const getElbowArrowData = (
|
||||||
const startHeading = getBindPointHeading(
|
const startHeading = getBindPointHeading(
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
elementsMap,
|
|
||||||
hoveredStartElement,
|
hoveredStartElement,
|
||||||
origStartGlobalPoint,
|
origStartGlobalPoint,
|
||||||
);
|
);
|
||||||
const endHeading = getBindPointHeading(
|
const endHeading = getBindPointHeading(
|
||||||
endGlobalPoint,
|
endGlobalPoint,
|
||||||
startGlobalPoint,
|
startGlobalPoint,
|
||||||
elementsMap,
|
|
||||||
hoveredEndElement,
|
hoveredEndElement,
|
||||||
origEndGlobalPoint,
|
origEndGlobalPoint,
|
||||||
);
|
);
|
||||||
|
@ -2250,7 +2247,6 @@ const getGlobalPoint = (
|
||||||
const getBindPointHeading = (
|
const getBindPointHeading = (
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
otherPoint: GlobalPoint,
|
otherPoint: GlobalPoint,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||||
origPoint: GlobalPoint,
|
origPoint: GlobalPoint,
|
||||||
): Heading =>
|
): Heading =>
|
||||||
|
@ -2268,7 +2264,6 @@ const getBindPointHeading = (
|
||||||
number,
|
number,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
elementsMap,
|
|
||||||
origPoint,
|
origPoint,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ import {
|
||||||
type OrderedExcalidrawElement,
|
type OrderedExcalidrawElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
type LinkDirection = "up" | "right" | "down" | "left";
|
type LinkDirection = "up" | "right" | "down" | "left";
|
||||||
|
|
||||||
const VERTICAL_OFFSET = 100;
|
const VERTICAL_OFFSET = 100;
|
||||||
|
@ -236,10 +238,11 @@ const getOffsets = (
|
||||||
|
|
||||||
const addNewNode = (
|
const addNewNode = (
|
||||||
element: ExcalidrawFlowchartNodeElement,
|
element: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const successors = getSuccessors(element, elementsMap, direction);
|
const successors = getSuccessors(element, elementsMap, direction);
|
||||||
const predeccessors = getPredecessors(element, elementsMap, direction);
|
const predeccessors = getPredecessors(element, elementsMap, direction);
|
||||||
|
|
||||||
|
@ -274,9 +277,9 @@ const addNewNode = (
|
||||||
const bindingArrow = createBindingArrow(
|
const bindingArrow = createBindingArrow(
|
||||||
element,
|
element,
|
||||||
nextNode,
|
nextNode,
|
||||||
elementsMap,
|
|
||||||
direction,
|
direction,
|
||||||
appState,
|
appState,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -287,9 +290,9 @@ const addNewNode = (
|
||||||
|
|
||||||
export const addNewNodes = (
|
export const addNewNodes = (
|
||||||
startNode: ExcalidrawFlowchartNodeElement,
|
startNode: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
numberOfNodes: number,
|
numberOfNodes: number,
|
||||||
) => {
|
) => {
|
||||||
// always start from 0 and distribute evenly
|
// always start from 0 and distribute evenly
|
||||||
|
@ -352,9 +355,9 @@ export const addNewNodes = (
|
||||||
const bindingArrow = createBindingArrow(
|
const bindingArrow = createBindingArrow(
|
||||||
startNode,
|
startNode,
|
||||||
nextNode,
|
nextNode,
|
||||||
elementsMap,
|
|
||||||
direction,
|
direction,
|
||||||
appState,
|
appState,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
newNodes.push(nextNode);
|
newNodes.push(nextNode);
|
||||||
|
@ -367,9 +370,9 @@ export const addNewNodes = (
|
||||||
const createBindingArrow = (
|
const createBindingArrow = (
|
||||||
startBindingElement: ExcalidrawFlowchartNodeElement,
|
startBindingElement: ExcalidrawFlowchartNodeElement,
|
||||||
endBindingElement: ExcalidrawFlowchartNodeElement,
|
endBindingElement: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
let startX: number;
|
let startX: number;
|
||||||
let startY: number;
|
let startY: number;
|
||||||
|
@ -440,18 +443,10 @@ const createBindingArrow = (
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
bindLinearElement(
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
bindingArrow,
|
|
||||||
startBindingElement,
|
bindLinearElement(bindingArrow, startBindingElement, "start", scene);
|
||||||
"start",
|
bindLinearElement(bindingArrow, endBindingElement, "end", scene);
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
bindLinearElement(
|
|
||||||
bindingArrow,
|
|
||||||
endBindingElement,
|
|
||||||
"end",
|
|
||||||
elementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
||||||
changedElements.set(
|
changedElements.set(
|
||||||
|
@ -467,7 +462,7 @@ const createBindingArrow = (
|
||||||
bindingArrow as OrderedExcalidrawElement,
|
bindingArrow as OrderedExcalidrawElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(bindingArrow, [
|
LinearElementEditor.movePoints(bindingArrow, scene, [
|
||||||
{
|
{
|
||||||
index: 1,
|
index: 1,
|
||||||
point: bindingArrow.points[1],
|
point: bindingArrow.points[1],
|
||||||
|
@ -632,16 +627,17 @@ export class FlowChartCreator {
|
||||||
|
|
||||||
createNodes(
|
createNodes(
|
||||||
startNode: ExcalidrawFlowchartNodeElement,
|
startNode: ExcalidrawFlowchartNodeElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
direction: LinkDirection,
|
direction: LinkDirection,
|
||||||
|
scene: Scene,
|
||||||
) {
|
) {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
if (direction !== this.direction) {
|
if (direction !== this.direction) {
|
||||||
const { nextNode, bindingArrow } = addNewNode(
|
const { nextNode, bindingArrow } = addNewNode(
|
||||||
startNode,
|
startNode,
|
||||||
elementsMap,
|
|
||||||
appState,
|
appState,
|
||||||
direction,
|
direction,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.numberOfNodes = 1;
|
this.numberOfNodes = 1;
|
||||||
|
@ -652,9 +648,9 @@ export class FlowChartCreator {
|
||||||
this.numberOfNodes += 1;
|
this.numberOfNodes += 1;
|
||||||
const newNodes = addNewNodes(
|
const newNodes = addNewNodes(
|
||||||
startNode,
|
startNode,
|
||||||
elementsMap,
|
|
||||||
appState,
|
appState,
|
||||||
direction,
|
direction,
|
||||||
|
scene,
|
||||||
this.numberOfNodes,
|
this.numberOfNodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -682,13 +678,9 @@ export class FlowChartCreator {
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this.pendingNodes = this.pendingNodes.map((node) =>
|
this.pendingNodes = this.pendingNodes.map((node) =>
|
||||||
mutateElement(
|
mutateElement(node, elementsMap, {
|
||||||
node,
|
|
||||||
{
|
|
||||||
frameId: startNode.frameId,
|
frameId: startNode.frameId,
|
||||||
},
|
}),
|
||||||
false,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { getBoundTextElement } from "./textElement";
|
||||||
import { hasBoundTextElement } from "./typeChecks";
|
import { hasBoundTextElement } from "./typeChecks";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
|
@ -152,9 +153,10 @@ export const orderByFractionalIndex = (
|
||||||
*/
|
*/
|
||||||
export const syncMovedIndices = (
|
export const syncMovedIndices = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement>,
|
movedElements: ElementsMap,
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
try {
|
try {
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
const indicesGroups = getMovedIndicesGroups(elements, movedElements);
|
const indicesGroups = getMovedIndicesGroups(elements, movedElements);
|
||||||
|
|
||||||
// try generatating indices, throws on invalid movedElements
|
// try generatating indices, throws on invalid movedElements
|
||||||
|
@ -176,7 +178,7 @@ export const syncMovedIndices = (
|
||||||
|
|
||||||
// split mutation so we don't end up in an incosistent state
|
// split mutation so we don't end up in an incosistent state
|
||||||
for (const [element, update] of elementsUpdates) {
|
for (const [element, update] of elementsUpdates) {
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, elementsMap, update);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// fallback to default sync
|
// fallback to default sync
|
||||||
|
@ -194,10 +196,12 @@ export const syncMovedIndices = (
|
||||||
export const syncInvalidIndices = (
|
export const syncInvalidIndices = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
const indicesGroups = getInvalidIndicesGroups(elements);
|
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
|
|
||||||
for (const [element, update] of elementsUpdates) {
|
for (const [element, update] of elementsUpdates) {
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, elementsMap, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements as OrderedExcalidrawElement[];
|
return elements as OrderedExcalidrawElement[];
|
||||||
|
@ -210,7 +214,7 @@ export const syncInvalidIndices = (
|
||||||
*/
|
*/
|
||||||
const getMovedIndicesGroups = (
|
const getMovedIndicesGroups = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement>,
|
movedElements: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
const indicesGroups: number[][] = [];
|
const indicesGroups: number[][] = [];
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
|
||||||
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
||||||
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
||||||
|
|
||||||
import type { ExcalidrawElementsIncludingDeleted } from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -29,6 +27,8 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
|
import type { ExcalidrawElementsIncludingDeleted } from "./Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
|
@ -41,30 +41,24 @@ import type {
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
nextElements: readonly ExcalidrawElement[],
|
nextElements: readonly ExcalidrawElement[],
|
||||||
oldElements: readonly ExcalidrawElement[],
|
origElements: readonly ExcalidrawElement[],
|
||||||
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
origIdToDuplicateId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
|
||||||
) => {
|
) => {
|
||||||
const nextElementMap = arrayToMap(nextElements) as Map<
|
const nextElementMap = arrayToMap(nextElements) as Map<
|
||||||
ExcalidrawElement["id"],
|
ExcalidrawElement["id"],
|
||||||
ExcalidrawElement
|
ExcalidrawElement
|
||||||
>;
|
>;
|
||||||
|
|
||||||
for (const element of oldElements) {
|
for (const element of origElements) {
|
||||||
if (element.frameId) {
|
if (element.frameId) {
|
||||||
// use its frameId to get the new frameId
|
// use its frameId to get the new frameId
|
||||||
const nextElementId = oldIdToDuplicatedId.get(element.id);
|
const nextElementId = origIdToDuplicateId.get(element.id);
|
||||||
const nextFrameId = oldIdToDuplicatedId.get(element.frameId);
|
const nextFrameId = origIdToDuplicateId.get(element.frameId);
|
||||||
if (nextElementId) {
|
const nextElement = nextElementId && nextElementMap.get(nextElementId);
|
||||||
const nextElement = nextElementMap.get(nextElementId);
|
|
||||||
if (nextElement) {
|
if (nextElement) {
|
||||||
mutateElement(
|
mutateElement(nextElement, nextElementMap, {
|
||||||
nextElement,
|
frameId: nextFrameId ?? null,
|
||||||
{
|
});
|
||||||
frameId: nextFrameId ?? element.frameId,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -567,13 +561,9 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const element of finalElementsToAdd) {
|
for (const element of finalElementsToAdd) {
|
||||||
mutateElement(
|
mutateElement(element, elementsMap, {
|
||||||
element,
|
|
||||||
{
|
|
||||||
frameId: frame.id,
|
frameId: frame.id,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allElements;
|
return allElements;
|
||||||
|
@ -611,13 +601,9 @@ export const removeElementsFromFrame = (
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [, element] of _elementsToRemove) {
|
for (const [, element] of _elementsToRemove) {
|
||||||
mutateElement(
|
mutateElement(element, elementsMap, {
|
||||||
element,
|
|
||||||
{
|
|
||||||
frameId: null,
|
frameId: null,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,6 @@ import {
|
||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { Store } from "@excalidraw/excalidraw/store";
|
import type { Store } from "@excalidraw/excalidraw/store";
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
@ -50,10 +46,8 @@ import {
|
||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
|
||||||
|
|
||||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
|
@ -73,6 +67,8 @@ import {
|
||||||
|
|
||||||
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
@ -84,7 +80,6 @@ import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
SceneElementsMap,
|
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
@ -127,15 +122,17 @@ export class LinearElementEditor {
|
||||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||||
public readonly elbowed: boolean;
|
public readonly elbowed: boolean;
|
||||||
|
|
||||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
constructor(
|
||||||
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) {
|
||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
_brand: "excalidrawLinearElementId";
|
_brand: "excalidrawLinearElementId";
|
||||||
};
|
};
|
||||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||||
console.error("Linear element is not normalized", Error().stack);
|
console.error("Linear element is not normalized", Error().stack);
|
||||||
LinearElementEditor.normalizePoints(element);
|
LinearElementEditor.normalizePoints(element, elementsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedPointsIndices = null;
|
this.selectedPointsIndices = null;
|
||||||
this.lastUncommittedPoint = null;
|
this.lastUncommittedPoint = null;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
|
@ -309,7 +306,7 @@ export class LinearElementEditor {
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, scene, [
|
||||||
{
|
{
|
||||||
index: selectedIndex,
|
index: selectedIndex,
|
||||||
point: pointFrom(
|
point: pointFrom(
|
||||||
|
@ -333,6 +330,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
LinearElementEditor.movePoints(
|
LinearElementEditor.movePoints(
|
||||||
element,
|
element,
|
||||||
|
scene,
|
||||||
selectedPointsIndices.map((pointIndex) => {
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
const newPointPosition: LocalPoint =
|
const newPointPosition: LocalPoint =
|
||||||
pointIndex === lastClickedPoint
|
pointIndex === lastClickedPoint
|
||||||
|
@ -358,7 +356,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
handleBindTextResize(element, elementsMap, false);
|
handleBindTextResize(element, scene, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// suggest bindings for first and last point if selected
|
// suggest bindings for first and last point if selected
|
||||||
|
@ -453,7 +451,7 @@ export class LinearElementEditor {
|
||||||
selectedPoint === element.points.length - 1
|
selectedPoint === element.points.length - 1
|
||||||
) {
|
) {
|
||||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, scene, [
|
||||||
{
|
{
|
||||||
index: selectedPoint,
|
index: selectedPoint,
|
||||||
point:
|
point:
|
||||||
|
@ -795,7 +793,7 @@ export class LinearElementEditor {
|
||||||
);
|
);
|
||||||
} else if (event.altKey && appState.editingLinearElement) {
|
} else if (event.altKey && appState.editingLinearElement) {
|
||||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
points: [
|
points: [
|
||||||
...element.points,
|
...element.points,
|
||||||
LinearElementEditor.createPointAt(
|
LinearElementEditor.createPointAt(
|
||||||
|
@ -861,7 +859,6 @@ export class LinearElementEditor {
|
||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -934,13 +931,13 @@ export class LinearElementEditor {
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
): LinearElementEditor | null {
|
): LinearElementEditor | null {
|
||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
if (!appState.editingLinearElement) {
|
if (!appState.editingLinearElement) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return appState.editingLinearElement;
|
return appState.editingLinearElement;
|
||||||
|
@ -951,7 +948,9 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
if (!event.altKey) {
|
if (!event.altKey) {
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
LinearElementEditor.deletePoints(element, app.scene, [
|
||||||
|
points.length - 1,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
|
@ -989,14 +988,14 @@ export class LinearElementEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, app.scene, [
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: newPoint,
|
point: newPoint,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
|
LinearElementEditor.addPoints(element, app.scene, [{ point: newPoint }]);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
|
@ -1160,23 +1159,26 @@ export class LinearElementEditor {
|
||||||
y: element.y + offsetY,
|
y: element.y + offsetY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// element-mutating methods
|
// element-mutating methods
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
static normalizePoints(
|
||||||
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) {
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
elementsMap: ElementsMap,
|
||||||
|
) {
|
||||||
|
mutateElement(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
LinearElementEditor.getNormalizedPoints(element),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static duplicateSelectedPoints(
|
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
|
||||||
appState: AppState,
|
|
||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
|
||||||
): AppState {
|
|
||||||
invariant(
|
invariant(
|
||||||
appState.editingLinearElement,
|
appState.editingLinearElement,
|
||||||
"Not currently editing a linear element",
|
"Not currently editing a linear element",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
|
|
||||||
|
@ -1219,13 +1221,13 @@ export class LinearElementEditor {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
mutateElement(element, { points: nextPoints });
|
scene.mutateElement(element, { points: nextPoints });
|
||||||
|
|
||||||
// temp hack to ensure the line doesn't move when adding point to the end,
|
// temp hack to ensure the line doesn't move when adding point to the end,
|
||||||
// potentially expanding the bounding box
|
// potentially expanding the bounding box
|
||||||
if (pointAddedToEnd) {
|
if (pointAddedToEnd) {
|
||||||
const lastPoint = element.points[element.points.length - 1];
|
const lastPoint = element.points[element.points.length - 1];
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(element, scene, [
|
||||||
{
|
{
|
||||||
index: element.points.length - 1,
|
index: element.points.length - 1,
|
||||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||||
|
@ -1244,6 +1246,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
static deletePoints(
|
static deletePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
pointIndices: readonly number[],
|
pointIndices: readonly number[],
|
||||||
) {
|
) {
|
||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
|
@ -1274,28 +1277,41 @@ export class LinearElementEditor {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
LinearElementEditor._updatePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
nextPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addPoints(
|
static addPoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
targetPoints: { point: LocalPoint }[],
|
targetPoints: { point: LocalPoint }[],
|
||||||
) {
|
) {
|
||||||
const offsetX = 0;
|
const offsetX = 0;
|
||||||
const offsetY = 0;
|
const offsetY = 0;
|
||||||
|
|
||||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
LinearElementEditor._updatePoints(
|
||||||
|
element,
|
||||||
|
scene,
|
||||||
|
nextPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static movePoints(
|
static movePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
||||||
otherUpdates?: {
|
otherUpdates?: {
|
||||||
startBinding?: PointBinding | null;
|
startBinding?: PointBinding | null;
|
||||||
endBinding?: PointBinding | null;
|
endBinding?: PointBinding | null;
|
||||||
},
|
},
|
||||||
sceneElementsMap?: NonDeletedSceneElementsMap,
|
|
||||||
) {
|
) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
|
@ -1329,6 +1345,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(
|
LinearElementEditor._updatePoints(
|
||||||
element,
|
element,
|
||||||
|
scene,
|
||||||
nextPoints,
|
nextPoints,
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
|
@ -1339,7 +1356,6 @@ export class LinearElementEditor {
|
||||||
dragging || targetPoint.isDragging === true,
|
dragging || targetPoint.isDragging === true,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
sceneElementsMap,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1394,8 +1410,9 @@ export class LinearElementEditor {
|
||||||
pointerCoords: PointerCoords,
|
pointerCoords: PointerCoords,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
snapToGrid: boolean,
|
snapToGrid: boolean,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
) {
|
) {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(
|
const element = LinearElementEditor.getElement(
|
||||||
linearElementEditor.elementId,
|
linearElementEditor.elementId,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -1425,9 +1442,7 @@ export class LinearElementEditor {
|
||||||
...element.points.slice(segmentMidpoint.index!),
|
...element.points.slice(segmentMidpoint.index!),
|
||||||
];
|
];
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, { points });
|
||||||
points,
|
|
||||||
});
|
|
||||||
|
|
||||||
ret.pointerDownState = {
|
ret.pointerDownState = {
|
||||||
...linearElementEditor.pointerDownState,
|
...linearElementEditor.pointerDownState,
|
||||||
|
@ -1443,6 +1458,7 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
private static _updatePoints(
|
private static _updatePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
scene: Scene,
|
||||||
nextPoints: readonly LocalPoint[],
|
nextPoints: readonly LocalPoint[],
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
|
@ -1479,28 +1495,10 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
updates.points = Array.from(nextPoints);
|
updates.points = Array.from(nextPoints);
|
||||||
|
|
||||||
if (!options?.sceneElementsMap || Scene.getScene(element)) {
|
scene.mutateElement(element, updates, {
|
||||||
mutateElement(element, updates, true, {
|
informMutation: true,
|
||||||
isDragging: options?.isDragging,
|
isDragging: options?.isDragging ?? false,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// The element is not in the scene, so we need to use the provided
|
|
||||||
// scene map.
|
|
||||||
Object.assign(element, {
|
|
||||||
...updates,
|
|
||||||
angle: 0 as Radians,
|
|
||||||
|
|
||||||
...updateElbowArrowPoints(
|
|
||||||
element,
|
|
||||||
options.sceneElementsMap,
|
|
||||||
updates,
|
|
||||||
{
|
|
||||||
isDragging: options?.isDragging,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
bumpVersion(element);
|
|
||||||
} else {
|
} else {
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
const prevCoords = getElementPointsCoords(element, element.points);
|
const prevCoords = getElementPointsCoords(element, element.points);
|
||||||
|
@ -1515,7 +1513,7 @@ export class LinearElementEditor {
|
||||||
pointFrom(dX, dY),
|
pointFrom(dX, dY),
|
||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
...otherUpdates,
|
...otherUpdates,
|
||||||
points: nextPoints,
|
points: nextPoints,
|
||||||
x: element.x + rotated[0],
|
x: element.x + rotated[0],
|
||||||
|
@ -1574,7 +1572,7 @@ export class LinearElementEditor {
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
mutateElement(boundTextElement, { isDeleted: true });
|
mutateElement(boundTextElement, elementsMap, { isDeleted: true });
|
||||||
}
|
}
|
||||||
let x = 0;
|
let x = 0;
|
||||||
let y = 0;
|
let y = 0;
|
||||||
|
@ -1781,8 +1779,9 @@ export class LinearElementEditor {
|
||||||
index: number,
|
index: number,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
): LinearElementEditor {
|
): LinearElementEditor {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(
|
const element = LinearElementEditor.getElement(
|
||||||
linearElement.elementId,
|
linearElement.elementId,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -1825,7 +1824,7 @@ export class LinearElementEditor {
|
||||||
.map((segment) => segment.index)
|
.map((segment) => segment.index)
|
||||||
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fixedSegments: nextFixedSegments,
|
fixedSegments: nextFixedSegments,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1859,14 +1858,14 @@ export class LinearElementEditor {
|
||||||
|
|
||||||
static deleteFixedSegment(
|
static deleteFixedSegment(
|
||||||
element: ExcalidrawElbowArrowElement,
|
element: ExcalidrawElbowArrowElement,
|
||||||
|
scene: Scene,
|
||||||
index: number,
|
index: number,
|
||||||
): void {
|
): void {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fixedSegments: element.fixedSegments?.filter(
|
fixedSegments: element.fixedSegments?.filter(
|
||||||
(segment) => segment.index !== index,
|
(segment) => segment.index !== index,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
mutateElement(element, {}, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,8 @@ import {
|
||||||
getSizeFromPoints,
|
getSizeFromPoints,
|
||||||
randomInteger,
|
randomInteger,
|
||||||
getUpdatedTimestamp,
|
getUpdatedTimestamp,
|
||||||
toBrandedType,
|
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { Radians } from "@excalidraw/math";
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
@ -16,35 +11,42 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
import { ShapeCache } from "./ShapeCache";
|
import { ShapeCache } from "./ShapeCache";
|
||||||
|
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
import { isElbowArrow } from "./typeChecks";
|
import { isElbowArrow } from "./typeChecks";
|
||||||
|
|
||||||
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types";
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
"id" | "version" | "versionNonce" | "updated"
|
"id" | "version" | "versionNonce" | "updated"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// This function tracks updates of text elements for the purposes for collaboration.
|
/**
|
||||||
// The version is used to compare updates when more than one user is working in
|
* This function tracks updates of text elements for the purposes for collaboration.
|
||||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
* The version is used to compare updates when more than one user is working in
|
||||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
* the same drawing.
|
||||||
|
*
|
||||||
|
* WARNING: this won't trigger the component to update, so if you need to trigger component update,
|
||||||
|
* use `scene.mutateElement` or `ExcalidrawImperativeAPI.mutateElement` instead.
|
||||||
|
*/
|
||||||
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
informMutation = true,
|
|
||||||
options?: {
|
options?: {
|
||||||
// Currently only for elbow arrows.
|
|
||||||
// If true, the elbow arrow tries to bind to the nearest element. If false
|
|
||||||
// it tries to keep the same bound element, if any.
|
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
},
|
},
|
||||||
): TElement => {
|
) => {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
|
|
||||||
// casting to any because can't use `in` operator
|
// casting to any because can't use `in` operator
|
||||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||||
const { points, fixedSegments, fileId, startBinding, endBinding } =
|
const { points, fixedSegments, startBinding, endBinding, fileId } =
|
||||||
updates as any;
|
updates as any;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -55,10 +57,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
typeof startBinding !== "undefined" ||
|
typeof startBinding !== "undefined" ||
|
||||||
typeof endBinding !== "undefined") // manual binding to element
|
typeof endBinding !== "undefined") // manual binding to element
|
||||||
) {
|
) {
|
||||||
const elementsMap = toBrandedType<NonDeletedSceneElementsMap>(
|
|
||||||
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
|
||||||
);
|
|
||||||
|
|
||||||
updates = {
|
updates = {
|
||||||
...updates,
|
...updates,
|
||||||
angle: 0 as Radians,
|
angle: 0 as Radians,
|
||||||
|
@ -68,16 +66,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
x: updates.x || element.x,
|
x: updates.x || element.x,
|
||||||
y: updates.y || element.y,
|
y: updates.y || element.y,
|
||||||
},
|
},
|
||||||
elementsMap,
|
elementsMap as NonDeletedSceneElementsMap,
|
||||||
{
|
updates as ElementUpdate<ExcalidrawElbowArrowElement>,
|
||||||
fixedSegments,
|
options,
|
||||||
points,
|
|
||||||
startBinding,
|
|
||||||
endBinding,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isDragging: options?.isDragging,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (typeof points !== "undefined") {
|
} else if (typeof points !== "undefined") {
|
||||||
|
@ -150,10 +141,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element.versionNonce = randomInteger();
|
element.versionNonce = randomInteger();
|
||||||
element.updated = getUpdatedTimestamp();
|
element.updated = getUpdatedTimestamp();
|
||||||
|
|
||||||
if (informMutation) {
|
|
||||||
Scene.getScene(element)?.triggerUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,6 @@ import {
|
||||||
|
|
||||||
import type { GlobalPoint } from "@excalidraw/math";
|
import type { GlobalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
@ -32,7 +30,6 @@ import {
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
|
@ -60,6 +57,8 @@ import {
|
||||||
|
|
||||||
import { isInGroup } from "./groups";
|
import { isInGroup } from "./groups";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { BoundingBox } from "./bounds";
|
import type { BoundingBox } from "./bounds";
|
||||||
import type {
|
import type {
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
|
@ -74,7 +73,6 @@ import type {
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
SceneElementsMap,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
@ -83,7 +81,6 @@ export const transformElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: SceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
|
@ -93,31 +90,31 @@ export const transformElements = (
|
||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
if (transformHandleType === "rotation") {
|
if (transformHandleType === "rotation") {
|
||||||
if (!isElbowArrow(element)) {
|
if (!isElbowArrow(element)) {
|
||||||
rotateSingleElement(
|
rotateSingleElement(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
shouldRotateWithDiscreteAngle,
|
shouldRotateWithDiscreteAngle,
|
||||||
);
|
);
|
||||||
updateBoundElements(element, elementsMap);
|
updateBoundElements(element, scene);
|
||||||
}
|
}
|
||||||
} else if (isTextElement(element) && transformHandleType) {
|
} else if (isTextElement(element) && transformHandleType) {
|
||||||
resizeSingleTextElement(
|
resizeSingleTextElement(
|
||||||
originalElements,
|
originalElements,
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
scene,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
shouldResizeFromCenter,
|
shouldResizeFromCenter,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
);
|
);
|
||||||
updateBoundElements(element, elementsMap);
|
updateBoundElements(element, scene);
|
||||||
return true;
|
return true;
|
||||||
} else if (transformHandleType) {
|
} else if (transformHandleType) {
|
||||||
const elementId = selectedElements[0].id;
|
const elementId = selectedElements[0].id;
|
||||||
|
@ -129,8 +126,6 @@ export const transformElements = (
|
||||||
getNextSingleWidthAndHeightFromPointer(
|
getNextSingleWidthAndHeightFromPointer(
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElements,
|
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
|
@ -145,8 +140,8 @@ export const transformElements = (
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElements,
|
originalElements,
|
||||||
|
scene,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
{
|
{
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
|
@ -161,7 +156,6 @@ export const transformElements = (
|
||||||
rotateMultipleElements(
|
rotateMultipleElements(
|
||||||
originalElements,
|
originalElements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
pointerX,
|
pointerX,
|
||||||
pointerY,
|
pointerY,
|
||||||
|
@ -210,13 +204,15 @@ export const transformElements = (
|
||||||
|
|
||||||
const rotateSingleElement = (
|
const rotateSingleElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
let angle: Radians;
|
let angle: Radians;
|
||||||
|
@ -233,13 +229,13 @@ const rotateSingleElement = (
|
||||||
}
|
}
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
mutateElement(element, { angle });
|
scene.mutateElement(element, { angle });
|
||||||
if (boundTextElementId) {
|
if (boundTextElementId) {
|
||||||
const textElement =
|
const textElement =
|
||||||
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
||||||
|
|
||||||
if (textElement && !isArrowElement(element)) {
|
if (textElement && !isArrowElement(element)) {
|
||||||
mutateElement(textElement, { angle });
|
scene.mutateElement(textElement, { angle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -289,12 +285,13 @@ export const measureFontSizeFromWidth = (
|
||||||
const resizeSingleTextElement = (
|
const resizeSingleTextElement = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
transformHandleType: TransformHandleDirection,
|
transformHandleType: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
@ -393,7 +390,7 @@ const resizeSingleTextElement = (
|
||||||
);
|
);
|
||||||
const [nextX, nextY] = newTopLeft;
|
const [nextX, nextY] = newTopLeft;
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
fontSize: metrics.size,
|
fontSize: metrics.size,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
|
@ -508,14 +505,13 @@ const resizeSingleTextElement = (
|
||||||
autoResize: false,
|
autoResize: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(element, resizedElement);
|
scene.mutateElement(element, resizedElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateMultipleElements = (
|
const rotateMultipleElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: SceneElementsMap,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
|
@ -523,6 +519,7 @@ const rotateMultipleElements = (
|
||||||
centerX: number,
|
centerX: number,
|
||||||
centerY: number,
|
centerY: number,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
let centerAngle =
|
let centerAngle =
|
||||||
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
||||||
if (shouldRotateWithDiscreteAngle) {
|
if (shouldRotateWithDiscreteAngle) {
|
||||||
|
@ -543,38 +540,30 @@ const rotateMultipleElements = (
|
||||||
(centerAngle + origAngle - element.angle) as Radians,
|
(centerAngle + origAngle - element.angle) as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isElbowArrow(element)) {
|
const updates = isElbowArrow(element)
|
||||||
|
? {
|
||||||
// Needed to re-route the arrow
|
// Needed to re-route the arrow
|
||||||
mutateElement(element, {
|
|
||||||
points: getArrowLocalFixedPoints(element, elementsMap),
|
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||||
});
|
}
|
||||||
} else {
|
: {
|
||||||
mutateElement(
|
|
||||||
element,
|
|
||||||
{
|
|
||||||
x: element.x + (rotatedCX - cx),
|
x: element.x + (rotatedCX - cx),
|
||||||
y: element.y + (rotatedCY - cy),
|
y: element.y + (rotatedCY - cy),
|
||||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||||
},
|
};
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
scene.mutateElement(element, updates);
|
||||||
|
|
||||||
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: elements,
|
simultaneouslyUpdated: elements,
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundText = getBoundTextElement(element, elementsMap);
|
const boundText = getBoundTextElement(element, elementsMap);
|
||||||
if (boundText && !isArrowElement(element)) {
|
if (boundText && !isArrowElement(element)) {
|
||||||
mutateElement(
|
scene.mutateElement(boundText, {
|
||||||
boundText,
|
|
||||||
{
|
|
||||||
x: boundText.x + (rotatedCX - cx),
|
x: boundText.x + (rotatedCX - cx),
|
||||||
y: boundText.y + (rotatedCY - cy),
|
y: boundText.y + (rotatedCY - cy),
|
||||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -819,8 +808,8 @@ export const resizeSingleElement = (
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
handleDirection: TransformHandleDirection,
|
handleDirection: TransformHandleDirection,
|
||||||
{
|
{
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
|
@ -833,6 +822,7 @@ export const resizeSingleElement = (
|
||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
let boundTextFont: { fontSize?: number } = {};
|
let boundTextFont: { fontSize?: number } = {};
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
|
@ -932,7 +922,7 @@ export const resizeSingleElement = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("scale" in latestElement && "scale" in origElement) {
|
if ("scale" in latestElement && "scale" in origElement) {
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
scale: [
|
scale: [
|
||||||
// defaulting because scaleX/Y can be 0/-0
|
// defaulting because scaleX/Y can be 0/-0
|
||||||
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
|
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
|
||||||
|
@ -967,21 +957,24 @@ export const resizeSingleElement = (
|
||||||
...rescaledPoints,
|
...rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(latestElement, updates, shouldInformMutation);
|
scene.mutateElement(latestElement, updates, {
|
||||||
|
informMutation: shouldInformMutation,
|
||||||
|
isDragging: false,
|
||||||
|
});
|
||||||
|
|
||||||
updateBoundElements(latestElement, elementsMap as SceneElementsMap, {
|
updateBoundElements(latestElement, scene, {
|
||||||
// TODO: confirm with MARK if this actually makes sense
|
// TODO: confirm with MARK if this actually makes sense
|
||||||
newSize: { width: nextWidth, height: nextHeight },
|
newSize: { width: nextWidth, height: nextHeight },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (boundTextElement && boundTextFont != null) {
|
if (boundTextElement && boundTextFont != null) {
|
||||||
mutateElement(boundTextElement, {
|
scene.mutateElement(boundTextElement, {
|
||||||
fontSize: boundTextFont.fontSize,
|
fontSize: boundTextFont.fontSize,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleBindTextResize(
|
handleBindTextResize(
|
||||||
latestElement,
|
latestElement,
|
||||||
elementsMap,
|
scene,
|
||||||
handleDirection,
|
handleDirection,
|
||||||
shouldMaintainAspectRatio,
|
shouldMaintainAspectRatio,
|
||||||
);
|
);
|
||||||
|
@ -991,8 +984,6 @@ export const resizeSingleElement = (
|
||||||
const getNextSingleWidthAndHeightFromPointer = (
|
const getNextSingleWidthAndHeightFromPointer = (
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
|
||||||
handleDirection: TransformHandleDirection,
|
handleDirection: TransformHandleDirection,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
|
@ -1527,27 +1518,24 @@ export const resizeMultipleElements = (
|
||||||
} of elementsAndUpdates) {
|
} of elementsAndUpdates) {
|
||||||
const { width, height, angle } = update;
|
const { width, height, angle } = update;
|
||||||
|
|
||||||
mutateElement(element, update, false, {
|
scene.mutateElement(element, update, {
|
||||||
|
informMutation: true,
|
||||||
// needed for the fixed binding point udpate to take effect
|
// needed for the fixed binding point udpate to take effect
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
updateBoundElements(element, scene, {
|
||||||
simultaneouslyUpdated: elementsToUpdate,
|
simultaneouslyUpdated: elementsToUpdate,
|
||||||
newSize: { width, height },
|
newSize: { width, height },
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement && boundTextFontSize) {
|
if (boundTextElement && boundTextFontSize) {
|
||||||
mutateElement(
|
scene.mutateElement(boundTextElement, {
|
||||||
boundTextElement,
|
|
||||||
{
|
|
||||||
fontSize: boundTextFontSize,
|
fontSize: boundTextFontSize,
|
||||||
angle: isLinearElement(element) ? undefined : angle,
|
angle: isLinearElement(element) ? undefined : angle,
|
||||||
},
|
});
|
||||||
false,
|
handleBindTextResize(element, scene, handleDirection, true);
|
||||||
);
|
|
||||||
handleBindTextResize(element, elementsMap, handleDirection, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { isShallowEqual } from "@excalidraw/common";
|
import { arrayToMap, isShallowEqual } from "@excalidraw/common";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
|
@ -7,13 +7,20 @@ import type {
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
|
||||||
import { isElementInViewport } from "./sizeHelpers";
|
import { isElementInViewport } from "./sizeHelpers";
|
||||||
import { isBoundToContainer, isFrameLikeElement } from "./typeChecks";
|
import {
|
||||||
|
isBoundToContainer,
|
||||||
|
isFrameLikeElement,
|
||||||
|
isLinearElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
getContainingFrame,
|
getContainingFrame,
|
||||||
getFrameChildren,
|
getFrameChildren,
|
||||||
} from "./frame";
|
} from "./frame";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { selectGroupsForSelectedElements } from "./groups";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ElementsMapOrArray,
|
ElementsMapOrArray,
|
||||||
|
@ -254,3 +261,49 @@ export const makeNextSelectedElementIds = (
|
||||||
|
|
||||||
return nextSelectedElementIds;
|
return nextSelectedElementIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _getLinearElementEditor = (
|
||||||
|
targetElements: readonly ExcalidrawElement[],
|
||||||
|
allElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const linears = targetElements.filter(isLinearElement);
|
||||||
|
if (linears.length === 1) {
|
||||||
|
const linear = linears[0];
|
||||||
|
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
||||||
|
const onlySingleLinearSelected = targetElements.every(
|
||||||
|
(el) => el.id === linear.id || boundElements.includes(el.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onlySingleLinearSelected) {
|
||||||
|
return new LinearElementEditor(linear, arrayToMap(allElements));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSelectionStateForElements = (
|
||||||
|
targetElements: readonly ExcalidrawElement[],
|
||||||
|
allElements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
selectedLinearElement: _getLinearElementEditor(targetElements, allElements),
|
||||||
|
...selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
editingGroupId: appState.editingGroupId,
|
||||||
|
selectedElementIds: excludeElementsInFramesFromSelection(
|
||||||
|
targetElements,
|
||||||
|
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
|
if (!isBoundToContainer(element)) {
|
||||||
|
acc[element.id] = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
},
|
||||||
|
allElements,
|
||||||
|
appState,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||||
|
@ -170,41 +169,6 @@ export const getLockedLinearCursorAlignSize = (
|
||||||
return { width, height };
|
return { width, height };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resizePerfectLineForNWHandler = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
) => {
|
|
||||||
const anchorX = element.x + element.width;
|
|
||||||
const anchorY = element.y + element.height;
|
|
||||||
const distanceToAnchorX = x - anchorX;
|
|
||||||
const distanceToAnchorY = y - anchorY;
|
|
||||||
if (Math.abs(distanceToAnchorX) < Math.abs(distanceToAnchorY) / 2) {
|
|
||||||
mutateElement(element, {
|
|
||||||
x: anchorX,
|
|
||||||
width: 0,
|
|
||||||
y,
|
|
||||||
height: -distanceToAnchorY,
|
|
||||||
});
|
|
||||||
} else if (Math.abs(distanceToAnchorY) < Math.abs(element.width) / 2) {
|
|
||||||
mutateElement(element, {
|
|
||||||
y: anchorY,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const nextHeight =
|
|
||||||
Math.sign(distanceToAnchorY) *
|
|
||||||
Math.sign(distanceToAnchorX) *
|
|
||||||
element.width;
|
|
||||||
mutateElement(element, {
|
|
||||||
x,
|
|
||||||
y: anchorY - nextHeight,
|
|
||||||
width: -distanceToAnchorX,
|
|
||||||
height: nextHeight,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNormalizedDimensions = (
|
export const getNormalizedDimensions = (
|
||||||
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
|
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
|
||||||
): {
|
): {
|
||||||
|
|
|
@ -14,12 +14,14 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { measureText } from "./textMeasurements";
|
import { measureText } from "./textMeasurements";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import {
|
import {
|
||||||
|
@ -28,7 +30,7 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import type { Radians } from "../../math/src";
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import type {
|
import type {
|
||||||
|
@ -44,9 +46,10 @@ import type {
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
informMutation = true,
|
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
|
|
||||||
if (!isProdEnv()) {
|
if (!isProdEnv()) {
|
||||||
|
@ -106,38 +109,43 @@ export const redrawTextBoundingBox = (
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: nextHeight }, informMutation);
|
scene.mutateElement(container, { height: nextHeight });
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metrics.width > maxContainerWidth) {
|
if (metrics.width > maxContainerWidth) {
|
||||||
const nextWidth = computeContainerDimensionForBoundText(
|
const nextWidth = computeContainerDimensionForBoundText(
|
||||||
metrics.width,
|
metrics.width,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { width: nextWidth }, informMutation);
|
scene.mutateElement(container, { width: nextWidth });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTextElement = {
|
const updatedTextElement = {
|
||||||
...textElement,
|
...textElement,
|
||||||
...boundTextUpdates,
|
...boundTextUpdates,
|
||||||
} as ExcalidrawTextElementWithContainer;
|
} as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
const { x, y } = computeBoundTextPosition(
|
const { x, y } = computeBoundTextPosition(
|
||||||
container,
|
container,
|
||||||
updatedTextElement,
|
updatedTextElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
boundTextUpdates.x = x;
|
boundTextUpdates.x = x;
|
||||||
boundTextUpdates.y = y;
|
boundTextUpdates.y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(textElement, boundTextUpdates, informMutation);
|
scene.mutateElement(textElement, boundTextUpdates);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
container: NonDeletedExcalidrawElement,
|
container: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
shouldMaintainAspectRatio = false,
|
shouldMaintainAspectRatio = false,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId) {
|
if (!boundTextElementId) {
|
||||||
return;
|
return;
|
||||||
|
@ -190,20 +198,20 @@ export const handleBindTextResize = (
|
||||||
transformHandleType === "n")
|
transformHandleType === "n")
|
||||||
? container.y - diff
|
? container.y - diff
|
||||||
: container.y;
|
: container.y;
|
||||||
mutateElement(container, {
|
scene.mutateElement(container, {
|
||||||
height: containerHeight,
|
height: containerHeight,
|
||||||
y: updatedY,
|
y: updatedY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(textElement, {
|
scene.mutateElement(textElement, {
|
||||||
text,
|
text,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
textElement,
|
textElement,
|
||||||
computeBoundTextPosition(container, textElement, elementsMap),
|
computeBoundTextPosition(container, textElement, elementsMap),
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import { isFrameLikeElement } from "./typeChecks";
|
import { isFrameLikeElement } from "./typeChecks";
|
||||||
|
|
||||||
import { getElementsInGroup } from "./groups";
|
import { getElementsInGroup } from "./groups";
|
||||||
|
@ -12,6 +10,8 @@ import { syncMovedIndices } from "./fractionalIndex";
|
||||||
|
|
||||||
import { getSelectedElements } from "./selection";
|
import { getSelectedElements } from "./selection";
|
||||||
|
|
||||||
|
import type Scene from "./Scene";
|
||||||
|
|
||||||
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
||||||
|
|
||||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import React from "react";
|
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -8,7 +7,7 @@ import {
|
||||||
isPrimitive,
|
isPrimitive,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||||
|
|
||||||
|
@ -25,7 +24,6 @@ import {
|
||||||
|
|
||||||
import type { LocalPoint } from "@excalidraw/math";
|
import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { mutateElement } from "../src/mutateElement";
|
|
||||||
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
||||||
|
|
||||||
import type { ExcalidrawLinearElement } from "../src/types";
|
import type { ExcalidrawLinearElement } from "../src/types";
|
||||||
|
@ -63,11 +61,11 @@ describe("duplicating single elements", () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
element.__proto__ = { hello: "world" };
|
element.__proto__ = { hello: "world" };
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, new Map(), {
|
||||||
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const copy = duplicateElement(null, new Map(), element, undefined, true);
|
const copy = duplicateElement(null, new Map(), element, true);
|
||||||
|
|
||||||
assertCloneObjects(element, copy);
|
assertCloneObjects(element, copy);
|
||||||
|
|
||||||
|
@ -173,7 +171,7 @@ describe("duplicating multiple elements", () => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const { duplicatedElements } = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
});
|
});
|
||||||
|
@ -181,10 +179,10 @@ describe("duplicating multiple elements", () => {
|
||||||
// generic id in-equality checks
|
// generic id in-equality checks
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
expect(origElements.map((e) => e.type)).toEqual(
|
expect(origElements.map((e) => e.type)).toEqual(
|
||||||
clonedElements.map((e) => e.type),
|
duplicatedElements.map((e) => e.type),
|
||||||
);
|
);
|
||||||
origElements.forEach((origElement, idx) => {
|
origElements.forEach((origElement, idx) => {
|
||||||
const clonedElement = clonedElements[idx];
|
const clonedElement = duplicatedElements[idx];
|
||||||
expect(origElement).toEqual(
|
expect(origElement).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.not.stringMatching(clonedElement.id),
|
id: expect.not.stringMatching(clonedElement.id),
|
||||||
|
@ -217,12 +215,12 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
const clonedArrows = clonedElements.filter(
|
const clonedArrows = duplicatedElements.filter(
|
||||||
(e) => e.type === "arrow",
|
(e) => e.type === "arrow",
|
||||||
) as ExcalidrawLinearElement[];
|
) as ExcalidrawLinearElement[];
|
||||||
|
|
||||||
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
|
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
|
||||||
clonedElements as any as typeof origElements;
|
duplicatedElements as any as typeof origElements;
|
||||||
|
|
||||||
expect(clonedText1.containerId).toBe(clonedRectangle.id);
|
expect(clonedText1.containerId).toBe(clonedRectangle.id);
|
||||||
expect(
|
expect(
|
||||||
|
@ -327,10 +325,10 @@ describe("duplicating multiple elements", () => {
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const duplicatedElements = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
}) as any as { newElements: typeof origElements };
|
}).duplicatedElements as any as typeof origElements;
|
||||||
|
|
||||||
const [
|
const [
|
||||||
clonedRectangle,
|
clonedRectangle,
|
||||||
|
@ -338,7 +336,7 @@ describe("duplicating multiple elements", () => {
|
||||||
clonedArrow1,
|
clonedArrow1,
|
||||||
clonedArrow2,
|
clonedArrow2,
|
||||||
clonedArrow3,
|
clonedArrow3,
|
||||||
] = clonedElements;
|
] = duplicatedElements;
|
||||||
|
|
||||||
expect(clonedRectangle.boundElements).toEqual([
|
expect(clonedRectangle.boundElements).toEqual([
|
||||||
{ id: clonedArrow1.id, type: "arrow" },
|
{ id: clonedArrow1.id, type: "arrow" },
|
||||||
|
@ -374,12 +372,12 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
const origElements = [rectangle1, rectangle2, rectangle3] as const;
|
||||||
const { newElements: clonedElements } = duplicateElements({
|
const { duplicatedElements } = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: origElements,
|
elements: origElements,
|
||||||
}) as any as { newElements: typeof origElements };
|
});
|
||||||
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
|
||||||
clonedElements;
|
duplicatedElements;
|
||||||
|
|
||||||
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
|
||||||
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
|
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
|
||||||
|
@ -399,7 +397,7 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
newElements: [clonedRectangle1],
|
duplicatedElements: [clonedRectangle1],
|
||||||
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
} = duplicateElements({ type: "everything", elements: [rectangle1] });
|
||||||
|
|
||||||
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
|
||||||
|
@ -408,6 +406,117 @@ describe("duplicating multiple elements", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("group-related duplication", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("action-duplicating within group", async () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
h.app.actionManager.executeAction(actionDuplicateSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: ["group1"] },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe("group1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("alt-duplicating within group", async () => {
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle2.x + 5, rectangle2.y + 5);
|
||||||
|
mouse.up(rectangle2.x + 50, rectangle2.y + 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: rectangle1.id },
|
||||||
|
{ id: rectangle2.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: ["group1"] },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe("group1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("alt-duplicating within group away outside frame", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
groupIds: ["group1"],
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([frame, rectangle1, rectangle2]);
|
||||||
|
API.setSelectedElements([rectangle2], "group1");
|
||||||
|
|
||||||
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
|
mouse.down(rectangle2.x + 5, rectangle2.y + 5);
|
||||||
|
mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log(h.elements);
|
||||||
|
|
||||||
|
assertElements(h.elements, [
|
||||||
|
{ id: frame.id },
|
||||||
|
{ id: rectangle1.id, frameId: frame.id },
|
||||||
|
{ id: rectangle2.id, frameId: frame.id },
|
||||||
|
{ [ORIG_ID]: rectangle2.id, selected: true, groupIds: [], frameId: null },
|
||||||
|
]);
|
||||||
|
expect(h.state.editingGroupId).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("duplication z-order", () => {
|
describe("duplication z-order", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<Excalidraw />);
|
await render(<Excalidraw />);
|
||||||
|
@ -503,8 +612,8 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ id: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
]);
|
]);
|
||||||
|
@ -538,8 +647,8 @@ describe("duplication z-order", () => {
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ id: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ [ORIG_ID]: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
{ id: rectangle3.id, selected: true },
|
{ [ORIG_ID]: rectangle3.id, selected: true },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -569,8 +678,8 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ id: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
]);
|
]);
|
||||||
|
@ -605,19 +714,19 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle1.id },
|
{ id: rectangle1.id },
|
||||||
{ [ORIG_ID]: rectangle2.id },
|
{ id: rectangle2.id },
|
||||||
{ [ORIG_ID]: rectangle3.id },
|
{ id: rectangle3.id },
|
||||||
{ id: rectangle1.id, selected: true },
|
{ [ORIG_ID]: rectangle1.id, selected: true },
|
||||||
{ id: rectangle2.id, selected: true },
|
{ [ORIG_ID]: rectangle2.id, selected: true },
|
||||||
{ id: rectangle3.id, selected: true },
|
{ [ORIG_ID]: rectangle3.id, selected: true },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating text container (in-order)", async () => {
|
it("alt-duplicating text container (in-order)", async () => {
|
||||||
const [rectangle, text] = API.createTextContainer();
|
const [rectangle, text] = API.createTextContainer();
|
||||||
API.setElements([rectangle, text]);
|
API.setElements([rectangle, text]);
|
||||||
API.setSelectedElements([rectangle, text]);
|
API.setSelectedElements([rectangle]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||||
|
@ -625,20 +734,20 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle.id },
|
{ id: rectangle.id },
|
||||||
|
{ id: text.id, containerId: rectangle.id },
|
||||||
|
{ [ORIG_ID]: rectangle.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: rectangle.id, selected: true },
|
|
||||||
{ id: text.id, containerId: rectangle.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating text container (out-of-order)", async () => {
|
it("alt-duplicating text container (out-of-order)", async () => {
|
||||||
const [rectangle, text] = API.createTextContainer();
|
const [rectangle, text] = API.createTextContainer();
|
||||||
API.setElements([text, rectangle]);
|
API.setElements([text, rectangle]);
|
||||||
API.setSelectedElements([rectangle, text]);
|
API.setSelectedElements([rectangle]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
mouse.down(rectangle.x + 5, rectangle.y + 5);
|
||||||
|
@ -646,21 +755,21 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: rectangle.id },
|
{ id: rectangle.id },
|
||||||
|
{ id: text.id, containerId: rectangle.id },
|
||||||
|
{ [ORIG_ID]: rectangle.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(rectangle.id)?.id,
|
containerId: getCloneByOrigId(rectangle.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: rectangle.id, selected: true },
|
|
||||||
{ id: text.id, containerId: rectangle.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating labeled arrows (in-order)", async () => {
|
it("alt-duplicating labeled arrows (in-order)", async () => {
|
||||||
const [arrow, text] = API.createLabeledArrow();
|
const [arrow, text] = API.createLabeledArrow();
|
||||||
|
|
||||||
API.setElements([arrow, text]);
|
API.setElements([arrow, text]);
|
||||||
API.setSelectedElements([arrow, text]);
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(arrow.x + 5, arrow.y + 5);
|
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||||
|
@ -668,21 +777,24 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: arrow.id },
|
{ id: arrow.id },
|
||||||
|
{ id: text.id, containerId: arrow.id },
|
||||||
|
{ [ORIG_ID]: arrow.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(arrow.id)?.id,
|
containerId: getCloneByOrigId(arrow.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: arrow.id, selected: true },
|
|
||||||
{ id: text.id, containerId: arrow.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
|
expect(h.state.selectedLinearElement).toEqual(
|
||||||
|
expect.objectContaining({ elementId: getCloneByOrigId(arrow.id)?.id }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating labeled arrows (out-of-order)", async () => {
|
it("alt-duplicating labeled arrows (out-of-order)", async () => {
|
||||||
const [arrow, text] = API.createLabeledArrow();
|
const [arrow, text] = API.createLabeledArrow();
|
||||||
|
|
||||||
API.setElements([text, arrow]);
|
API.setElements([text, arrow]);
|
||||||
API.setSelectedElements([arrow, text]);
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ alt: true }, () => {
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
||||||
mouse.down(arrow.x + 5, arrow.y + 5);
|
mouse.down(arrow.x + 5, arrow.y + 5);
|
||||||
|
@ -690,17 +802,17 @@ describe("duplication z-order", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
assertElements(h.elements, [
|
assertElements(h.elements, [
|
||||||
{ [ORIG_ID]: arrow.id },
|
{ id: arrow.id },
|
||||||
|
{ id: text.id, containerId: arrow.id },
|
||||||
|
{ [ORIG_ID]: arrow.id, selected: true },
|
||||||
{
|
{
|
||||||
[ORIG_ID]: text.id,
|
[ORIG_ID]: text.id,
|
||||||
containerId: getCloneByOrigId(arrow.id)?.id,
|
containerId: getCloneByOrigId(arrow.id)?.id,
|
||||||
},
|
},
|
||||||
{ id: arrow.id, selected: true },
|
|
||||||
{ id: text.id, containerId: arrow.id, selected: true },
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reverse-duplicating bindable element with bound arrow should keep the arrow on the duplicate", () => {
|
it("alt-duplicating bindable element with bound arrow should keep the arrow on the duplicate", async () => {
|
||||||
const rect = UI.createElement("rectangle", {
|
const rect = UI.createElement("rectangle", {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -722,11 +834,18 @@ describe("duplication z-order", () => {
|
||||||
mouse.up(15, 15);
|
mouse.up(15, 15);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(window.h.elements).toHaveLength(3);
|
assertElements(h.elements, [
|
||||||
|
{
|
||||||
const newRect = window.h.elements[0];
|
id: rect.id,
|
||||||
|
boundElements: expect.arrayContaining([
|
||||||
expect(arrow.endBinding?.elementId).toBe(newRect.id);
|
expect.objectContaining({ id: arrow.id }),
|
||||||
expect(newRect.boundElements?.[0]?.id).toBe(arrow.id);
|
]),
|
||||||
|
},
|
||||||
|
{ [ORIG_ID]: rect.id, boundElements: [], selected: true },
|
||||||
|
{
|
||||||
|
id: arrow.id,
|
||||||
|
endBinding: expect.objectContaining({ elementId: rect.id }),
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { ARROW_TYPE } from "@excalidraw/common";
|
import { ARROW_TYPE } from "@excalidraw/common";
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
|
|
||||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
||||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
||||||
|
|
||||||
|
@ -23,6 +22,8 @@ import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import { bindLinearElement } from "../src/binding";
|
import { bindLinearElement } from "../src/binding";
|
||||||
|
|
||||||
|
import Scene from "../src/Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
|
@ -142,7 +143,7 @@ describe("elbow arrow routing", () => {
|
||||||
elbowed: true,
|
elbowed: true,
|
||||||
}) as ExcalidrawElbowArrowElement;
|
}) as ExcalidrawElbowArrowElement;
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
mutateElement(arrow, {
|
h.app.scene.mutateElement(arrow, {
|
||||||
points: [
|
points: [
|
||||||
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
||||||
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
||||||
|
@ -187,14 +188,14 @@ describe("elbow arrow routing", () => {
|
||||||
scene.insertElement(rectangle1);
|
scene.insertElement(rectangle1);
|
||||||
scene.insertElement(rectangle2);
|
scene.insertElement(rectangle2);
|
||||||
scene.insertElement(arrow);
|
scene.insertElement(arrow);
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
bindLinearElement(arrow, rectangle1, "start", elementsMap);
|
bindLinearElement(arrow, rectangle1, "start", scene);
|
||||||
bindLinearElement(arrow, rectangle2, "end", elementsMap);
|
bindLinearElement(arrow, rectangle2, "end", scene);
|
||||||
|
|
||||||
expect(arrow.startBinding).not.toBe(null);
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
expect(arrow.endBinding).not.toBe(null);
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
mutateElement(arrow, {
|
h.app.scene.mutateElement(arrow, {
|
||||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
@ -749,7 +750,7 @@ function testInvalidIndicesSync(args: {
|
||||||
function prepareArguments(
|
function prepareArguments(
|
||||||
elementsLike: { id: string; index?: string }[],
|
elementsLike: { id: string; index?: string }[],
|
||||||
movedElementsIds?: string[],
|
movedElementsIds?: string[],
|
||||||
): [ExcalidrawElement[], Map<string, ExcalidrawElement> | undefined] {
|
): [ExcalidrawElement[], ElementsMap | undefined] {
|
||||||
const elements = elementsLike.map((x) =>
|
const elements = elementsLike.map((x) =>
|
||||||
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
|
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
|
||||||
);
|
);
|
||||||
|
@ -764,7 +765,7 @@ function prepareArguments(
|
||||||
function test(
|
function test(
|
||||||
name: string,
|
name: string,
|
||||||
elements: ExcalidrawElement[],
|
elements: ExcalidrawElement[],
|
||||||
movedElements: Map<string, ExcalidrawElement> | undefined,
|
movedElements: ElementsMap | undefined,
|
||||||
expectUnchangedElements: Map<string, { id: string }>,
|
expectUnchangedElements: Map<string, { id: string }>,
|
||||||
expectValidInput?: boolean,
|
expectValidInput?: boolean,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -333,7 +333,7 @@ describe("line element", () => {
|
||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"ne",
|
"ne",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -369,7 +369,7 @@ describe("line element", () => {
|
||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"se",
|
"se",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -424,7 +424,7 @@ describe("line element", () => {
|
||||||
element,
|
element,
|
||||||
element,
|
element,
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene.getNonDeletedElementsMap(),
|
||||||
h.app.scene.getNonDeletedElementsMap(),
|
h.app.scene,
|
||||||
"e",
|
"e",
|
||||||
{
|
{
|
||||||
shouldResizeFromCenter: true,
|
shouldResizeFromCenter: true,
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import { mutateElement } from "../src/mutateElement";
|
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||||
|
|
||||||
import { normalizeElementOrder } from "../src/sortElements";
|
import { normalizeElementOrder } from "../src/sortElements";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "../src/types";
|
import type { ExcalidrawElement } from "../src/types";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
const assertOrder = (
|
const assertOrder = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
expectedOrder: string[],
|
expectedOrder: string[],
|
||||||
|
@ -35,7 +37,7 @@ describe("normalizeElementsOrder", () => {
|
||||||
boundElements: [],
|
boundElements: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(container, {
|
mutateElement(container, new Map(), {
|
||||||
boundElements: [{ type: "text", id: boundText.id }],
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -352,7 +354,7 @@ describe("normalizeElementsOrder", () => {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [
|
boundElements: [
|
||||||
{ type: "text", id: boundText.id },
|
{ type: "text", id: boundText.id },
|
||||||
{ type: "text", id: "xxx" },
|
{ type: "text", id: "xxx" },
|
||||||
|
@ -387,7 +389,7 @@ describe("normalizeElementsOrder", () => {
|
||||||
boundElements: [],
|
boundElements: [],
|
||||||
groupIds: ["C", "A"],
|
groupIds: ["C", "A"],
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [{ type: "text", id: boundText.id }],
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -50,14 +50,8 @@ const alignSelectedElements = (
|
||||||
alignment: Alignment,
|
alignment: Alignment,
|
||||||
) => {
|
) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
const elementsMap = arrayToMap(elements);
|
|
||||||
|
|
||||||
const updatedElements = alignElements(
|
const updatedElements = alignElements(selectedElements, alignment, app.scene);
|
||||||
selectedElements,
|
|
||||||
elementsMap,
|
|
||||||
alignment,
|
|
||||||
app.scene,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,6 @@ import {
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import { measureText } from "@excalidraw/element/textMeasurements";
|
import { measureText } from "@excalidraw/element/textMeasurements";
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
@ -43,12 +42,12 @@ import type {
|
||||||
|
|
||||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type { Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
import type { Radians } from "../../math/src";
|
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
|
|
||||||
export const actionUnbindText = register({
|
export const actionUnbindText = register({
|
||||||
|
@ -80,7 +79,7 @@ export const actionUnbindText = register({
|
||||||
boundTextElement,
|
boundTextElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
mutateElement(boundTextElement as ExcalidrawTextElement, {
|
app.scene.mutateElement(boundTextElement as ExcalidrawTextElement, {
|
||||||
containerId: null,
|
containerId: null,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
@ -88,7 +87,7 @@ export const actionUnbindText = register({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
boundElements: element.boundElements?.filter(
|
boundElements: element.boundElements?.filter(
|
||||||
(ele) => ele.id !== boundTextElement.id,
|
(ele) => ele.id !== boundTextElement.id,
|
||||||
),
|
),
|
||||||
|
@ -153,25 +152,21 @@ export const actionBindText = register({
|
||||||
textElement = selectedElements[1] as ExcalidrawTextElement;
|
textElement = selectedElements[1] as ExcalidrawTextElement;
|
||||||
container = selectedElements[0] as ExcalidrawTextContainer;
|
container = selectedElements[0] as ExcalidrawTextContainer;
|
||||||
}
|
}
|
||||||
mutateElement(textElement, {
|
app.scene.mutateElement(textElement, {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
app.scene.mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
type: "text",
|
type: "text",
|
||||||
id: textElement.id,
|
id: textElement.id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const originalContainerHeight = container.height;
|
const originalContainerHeight = container.height;
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(textElement, container, app.scene);
|
||||||
textElement,
|
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
// overwritting the cache with original container height so
|
// overwritting the cache with original container height so
|
||||||
// it can be restored when unbind
|
// it can be restored when unbind
|
||||||
updateOriginalContainerCache(container.id, originalContainerHeight);
|
updateOriginalContainerCache(container.id, originalContainerHeight);
|
||||||
|
@ -301,27 +296,23 @@ export const actionWrapTextInContainer = register({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startBinding || endBinding) {
|
if (startBinding || endBinding) {
|
||||||
mutateElement(ele, { startBinding, endBinding }, false);
|
app.scene.mutateElement(ele, {
|
||||||
|
startBinding,
|
||||||
|
endBinding,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(
|
app.scene.mutateElement(textElement, {
|
||||||
textElement,
|
|
||||||
{
|
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
boundElements: null,
|
boundElements: null,
|
||||||
textAlign: TEXT_ALIGN.CENTER,
|
textAlign: TEXT_ALIGN.CENTER,
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
redrawTextBoundingBox(textElement, container, app.scene);
|
||||||
redrawTextBoundingBox(
|
|
||||||
textElement,
|
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
updatedElements = pushContainerBelowText(
|
updatedElements = pushContainerBelowText(
|
||||||
[...updatedElements, container],
|
[...updatedElements, container],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Excalidraw, mutateElement } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { act, assertElements, render } from "../tests/test-utils";
|
import { act, assertElements, render } from "../tests/test-utils";
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||||
frameId: f1.id,
|
frameId: f1.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(r1, {
|
h.app.scene.mutateElement(r1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(a1, {
|
h.app.scene.mutateElement(a1, {
|
||||||
boundElements: [{ type: "text", id: t1.id }],
|
boundElements: [{ type: "text", id: t1.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,7 @@ import { KEYS, updateActiveTool } from "@excalidraw/common";
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import { fixBindingsAfterDeletion } from "@excalidraw/element/binding";
|
import { fixBindingsAfterDeletion } from "@excalidraw/element/binding";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||||
import {
|
import {
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
|
@ -94,7 +91,7 @@ const deleteSelectedElements = (
|
||||||
el.boundElements.forEach((candidate) => {
|
el.boundElements.forEach((candidate) => {
|
||||||
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
||||||
if (bound && isElbowArrow(bound)) {
|
if (bound && isElbowArrow(bound)) {
|
||||||
mutateElement(bound, {
|
app.scene.mutateElement(bound, {
|
||||||
startBinding:
|
startBinding:
|
||||||
el.id === bound.startBinding?.elementId
|
el.id === bound.startBinding?.elementId
|
||||||
? null
|
? null
|
||||||
|
@ -102,7 +99,6 @@ const deleteSelectedElements = (
|
||||||
endBinding:
|
endBinding:
|
||||||
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
||||||
});
|
});
|
||||||
mutateElement(bound, { points: bound.points });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -261,7 +257,11 @@ export const actionDeleteSelected = register({
|
||||||
: endBindingElement,
|
: endBindingElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
LinearElementEditor.deletePoints(element, selectedPointsIndices);
|
LinearElementEditor.deletePoints(
|
||||||
|
element,
|
||||||
|
app.scene,
|
||||||
|
selectedPointsIndices,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements,
|
elements,
|
||||||
|
|
|
@ -7,26 +7,17 @@ import {
|
||||||
|
|
||||||
import { getNonDeletedElements } from "@excalidraw/element";
|
import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
|
||||||
isBoundToContainer,
|
|
||||||
isLinearElement,
|
|
||||||
} from "@excalidraw/element/typeChecks";
|
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
excludeElementsInFramesFromSelection,
|
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
|
getSelectionStateForElements,
|
||||||
} from "@excalidraw/element/selection";
|
} from "@excalidraw/element/selection";
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
import { duplicateElements } from "@excalidraw/element/duplicate";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
|
||||||
|
@ -52,7 +43,7 @@ export const actionDuplicateSelection = register({
|
||||||
try {
|
try {
|
||||||
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
||||||
appState,
|
appState,
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -65,8 +56,7 @@ export const actionDuplicateSelection = register({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let { newElements: duplicatedElements, elementsWithClones: nextElements } =
|
let { duplicatedElements, elementsWithDuplicates } = duplicateElements({
|
||||||
duplicateElements({
|
|
||||||
type: "in-place",
|
type: "in-place",
|
||||||
elements,
|
elements,
|
||||||
idsOfElementsToDuplicate: arrayToMap(
|
idsOfElementsToDuplicate: arrayToMap(
|
||||||
|
@ -77,40 +67,38 @@ export const actionDuplicateSelection = register({
|
||||||
),
|
),
|
||||||
appState,
|
appState,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
overrides: (element) => ({
|
overrides: ({ origElement, origIdToDuplicateId }) => {
|
||||||
x: element.x + DEFAULT_GRID_SIZE / 2,
|
const duplicateFrameId =
|
||||||
y: element.y + DEFAULT_GRID_SIZE / 2,
|
origElement.frameId && origIdToDuplicateId.get(origElement.frameId);
|
||||||
}),
|
return {
|
||||||
reverseOrder: false,
|
x: origElement.x + DEFAULT_GRID_SIZE / 2,
|
||||||
|
y: origElement.y + DEFAULT_GRID_SIZE / 2,
|
||||||
|
frameId: duplicateFrameId ?? origElement.frameId,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (app.props.onDuplicate && nextElements) {
|
if (app.props.onDuplicate && elementsWithDuplicates) {
|
||||||
const mappedElements = app.props.onDuplicate(nextElements, elements);
|
const mappedElements = app.props.onDuplicate(
|
||||||
|
elementsWithDuplicates,
|
||||||
|
elements,
|
||||||
|
);
|
||||||
if (mappedElements) {
|
if (mappedElements) {
|
||||||
nextElements = mappedElements;
|
elementsWithDuplicates = mappedElements;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
|
elements: syncMovedIndices(
|
||||||
|
elementsWithDuplicates,
|
||||||
|
arrayToMap(duplicatedElements),
|
||||||
|
),
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...updateLinearElementEditors(duplicatedElements),
|
...getSelectionStateForElements(
|
||||||
...selectGroupsForSelectedElements(
|
|
||||||
{
|
|
||||||
editingGroupId: appState.editingGroupId,
|
|
||||||
selectedElementIds: excludeElementsInFramesFromSelection(
|
|
||||||
duplicatedElements,
|
duplicatedElements,
|
||||||
).reduce((acc: Record<ExcalidrawElement["id"], true>, element) => {
|
getNonDeletedElements(elementsWithDuplicates),
|
||||||
if (!isBoundToContainer(element)) {
|
|
||||||
acc[element.id] = true;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
},
|
|
||||||
getNonDeletedElements(nextElements),
|
|
||||||
appState,
|
appState,
|
||||||
null,
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
@ -130,24 +118,3 @@ export const actionDuplicateSelection = register({
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
|
|
||||||
const linears = clonedElements.filter(isLinearElement);
|
|
||||||
if (linears.length === 1) {
|
|
||||||
const linear = linears[0];
|
|
||||||
const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
|
|
||||||
const onlySingleLinearSelected = clonedElements.every(
|
|
||||||
(el) => el.id === linear.id || boundElements.includes(el.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (onlySingleLinearSelected) {
|
|
||||||
return {
|
|
||||||
selectedLinearElement: new LinearElementEditor(linear),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedLinearElement: null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
|
@ -46,7 +46,6 @@ export const actionFinalize = register({
|
||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -72,7 +71,11 @@ export const actionFinalize = register({
|
||||||
scene.getElement(appState.pendingImageElementId);
|
scene.getElement(appState.pendingImageElementId);
|
||||||
|
|
||||||
if (pendingImageElement) {
|
if (pendingImageElement) {
|
||||||
mutateElement(pendingImageElement, { isDeleted: true }, false);
|
scene.mutateElement(
|
||||||
|
pendingImageElement,
|
||||||
|
{ isDeleted: true },
|
||||||
|
{ informMutation: false, isDragging: false },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.document.activeElement instanceof HTMLElement) {
|
if (window.document.activeElement instanceof HTMLElement) {
|
||||||
|
@ -96,7 +99,7 @@ export const actionFinalize = register({
|
||||||
!lastCommittedPoint ||
|
!lastCommittedPoint ||
|
||||||
points[points.length - 1] !== lastCommittedPoint
|
points[points.length - 1] !== lastCommittedPoint
|
||||||
) {
|
) {
|
||||||
mutateElement(multiPointElement, {
|
scene.mutateElement(multiPointElement, {
|
||||||
points: multiPointElement.points.slice(0, -1),
|
points: multiPointElement.points.slice(0, -1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -120,7 +123,7 @@ export const actionFinalize = register({
|
||||||
if (isLoop) {
|
if (isLoop) {
|
||||||
const linePoints = multiPointElement.points;
|
const linePoints = multiPointElement.points;
|
||||||
const firstPoint = linePoints[0];
|
const firstPoint = linePoints[0];
|
||||||
mutateElement(multiPointElement, {
|
scene.mutateElement(multiPointElement, {
|
||||||
points: linePoints.map((p, index) =>
|
points: linePoints.map((p, index) =>
|
||||||
index === linePoints.length - 1
|
index === linePoints.length - 1
|
||||||
? pointFrom(firstPoint[0], firstPoint[1])
|
? pointFrom(firstPoint[0], firstPoint[1])
|
||||||
|
@ -140,13 +143,7 @@ export const actionFinalize = register({
|
||||||
-1,
|
-1,
|
||||||
arrayToMap(elements),
|
arrayToMap(elements),
|
||||||
);
|
);
|
||||||
maybeBindLinearElement(
|
maybeBindLinearElement(multiPointElement, appState, { x, y }, scene);
|
||||||
multiPointElement,
|
|
||||||
appState,
|
|
||||||
{ x, y },
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +199,10 @@ export const actionFinalize = register({
|
||||||
// To select the linear element when user has finished mutipoint editing
|
// To select the linear element when user has finished mutipoint editing
|
||||||
selectedLinearElement:
|
selectedLinearElement:
|
||||||
multiPointElement && isLinearElement(multiPointElement)
|
multiPointElement && isLinearElement(multiPointElement)
|
||||||
? new LinearElementEditor(multiPointElement)
|
? new LinearElementEditor(
|
||||||
|
multiPointElement,
|
||||||
|
arrayToMap(newElements),
|
||||||
|
)
|
||||||
: appState.selectedLinearElement,
|
: appState.selectedLinearElement,
|
||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,10 +4,7 @@ import {
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||||
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
||||||
import {
|
import {
|
||||||
|
@ -162,11 +159,9 @@ const flipElements = (
|
||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isLinearElement),
|
||||||
elementsMap,
|
|
||||||
app.scene.getNonDeletedElements(),
|
|
||||||
app.scene,
|
|
||||||
isBindingEnabled(appState),
|
isBindingEnabled(appState),
|
||||||
[],
|
[],
|
||||||
|
app.scene,
|
||||||
appState.zoom,
|
appState.zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -194,13 +189,13 @@ const flipElements = (
|
||||||
getCommonBoundingBox(selectedElements);
|
getCommonBoundingBox(selectedElements);
|
||||||
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||||
otherElements.forEach((element) =>
|
otherElements.forEach((element) =>
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
x: element.x + diffX,
|
x: element.x + diffX,
|
||||||
y: element.y + diffY,
|
y: element.y + diffY,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
elbowArrows.forEach((element) =>
|
elbowArrows.forEach((element) =>
|
||||||
mutateElement(element, {
|
app.scene.mutateElement(element, {
|
||||||
x: element.x + diffX,
|
x: element.x + diffX,
|
||||||
y: element.y + diffY,
|
y: element.y + diffY,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -173,11 +173,9 @@ export const actionWrapSelectionInFrame = register({
|
||||||
},
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getCommonBounds(
|
const [x1, y1, x2, y2] = getCommonBounds(selectedElements, elementsMap);
|
||||||
selectedElements,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
const PADDING = 16;
|
const PADDING = 16;
|
||||||
const frame = newFrameElement({
|
const frame = newFrameElement({
|
||||||
x: x1 - PADDING,
|
x: x1 - PADDING,
|
||||||
|
@ -196,13 +194,9 @@ export const actionWrapSelectionInFrame = register({
|
||||||
for (const elementInGroup of elementsInGroup) {
|
for (const elementInGroup of elementsInGroup) {
|
||||||
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
||||||
|
|
||||||
mutateElement(
|
mutateElement(elementInGroup, elementsMap, {
|
||||||
elementInGroup,
|
|
||||||
{
|
|
||||||
groupIds: elementInGroup.groupIds.slice(0, index),
|
groupIds: elementInGroup.groupIds.slice(0, index),
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
|
import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
|
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
||||||
|
@ -50,7 +52,7 @@ export const actionToggleLinearEditor = register({
|
||||||
const editingLinearElement =
|
const editingLinearElement =
|
||||||
appState.editingLinearElement?.elementId === selectedElement.id
|
appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
? null
|
? null
|
||||||
: new LinearElementEditor(selectedElement);
|
: new LinearElementEditor(selectedElement, arrayToMap(elements));
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
|
|
@ -34,10 +34,7 @@ import {
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
|
@ -61,6 +58,7 @@ import type { LocalPoint } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
ElementsMap,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
|
@ -68,9 +66,10 @@ import type {
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
TextAlign,
|
TextAlign,
|
||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||||
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
||||||
|
@ -207,13 +206,12 @@ export const getFormValue = function <T extends Primitive>(
|
||||||
const offsetElementAfterFontResize = (
|
const offsetElementAfterFontResize = (
|
||||||
prevElement: ExcalidrawTextElement,
|
prevElement: ExcalidrawTextElement,
|
||||||
nextElement: ExcalidrawTextElement,
|
nextElement: ExcalidrawTextElement,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
|
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
|
||||||
return nextElement;
|
return nextElement;
|
||||||
}
|
}
|
||||||
return mutateElement(
|
return scene.mutateElement(nextElement, {
|
||||||
nextElement,
|
|
||||||
{
|
|
||||||
x:
|
x:
|
||||||
prevElement.textAlign === "left"
|
prevElement.textAlign === "left"
|
||||||
? prevElement.x
|
? prevElement.x
|
||||||
|
@ -223,9 +221,7 @@ const offsetElementAfterFontResize = (
|
||||||
// centering vertically is non-standard, but for Excalidraw I think
|
// centering vertically is non-standard, but for Excalidraw I think
|
||||||
// it makes sense
|
// it makes sense
|
||||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeFontSize = (
|
const changeFontSize = (
|
||||||
|
@ -251,10 +247,14 @@ const changeFontSize = (
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
newElement,
|
newElement,
|
||||||
app.scene.getContainerElement(oldElement),
|
app.scene.getContainerElement(oldElement),
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
newElement = offsetElementAfterFontResize(
|
||||||
|
oldElement,
|
||||||
|
newElement,
|
||||||
|
app.scene,
|
||||||
|
);
|
||||||
|
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
@ -264,15 +264,11 @@ const changeFontSize = (
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update arrow elements after text elements have been updated
|
// Update arrow elements after text elements have been updated
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
|
||||||
getSelectedElements(elements, appState, {
|
getSelectedElements(elements, appState, {
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
}).forEach((element) => {
|
}).forEach((element) => {
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
updateBoundElements(
|
updateBoundElements(element, app.scene);
|
||||||
element,
|
|
||||||
updatedElementsMap as NonDeletedSceneElementsMap,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -778,7 +774,7 @@ type ChangeFontFamilyData = Partial<
|
||||||
>
|
>
|
||||||
> & {
|
> & {
|
||||||
/** cache of selected & editing elements populated on opened popup */
|
/** cache of selected & editing elements populated on opened popup */
|
||||||
cachedElements?: Map<string, ExcalidrawElement>;
|
cachedElements?: ElementsMap;
|
||||||
/** flag to reset all elements to their cached versions */
|
/** flag to reset all elements to their cached versions */
|
||||||
resetAll?: true;
|
resetAll?: true;
|
||||||
/** flag to reset all containers to their cached versions */
|
/** flag to reset all containers to their cached versions */
|
||||||
|
@ -919,7 +915,7 @@ export const actionChangeFontFamily = register({
|
||||||
|
|
||||||
if (resetContainers && container && cachedContainer) {
|
if (resetContainers && container && cachedContainer) {
|
||||||
// reset the container back to it's cached version
|
// reset the container back to it's cached version
|
||||||
mutateElement(container, { ...cachedContainer }, false);
|
app.scene.mutateElement(container, { ...cachedContainer });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipFontFaceCheck) {
|
if (!skipFontFaceCheck) {
|
||||||
|
@ -950,12 +946,7 @@ export const actionChangeFontFamily = register({
|
||||||
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
||||||
for (const [element, container] of elementContainerMapping) {
|
for (const [element, container] of elementContainerMapping) {
|
||||||
// trigger synchronous redraw
|
// trigger synchronous redraw
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(element, container, app.scene);
|
||||||
element,
|
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
|
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
|
||||||
|
@ -972,8 +963,7 @@ export const actionChangeFontFamily = register({
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
latestElement as ExcalidrawTextElement,
|
latestElement as ExcalidrawTextElement,
|
||||||
latestContainer,
|
latestContainer,
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -987,7 +977,7 @@ export const actionChangeFontFamily = register({
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, app, updateData }) => {
|
PanelComponent: ({ elements, appState, app, updateData }) => {
|
||||||
const cachedElementsRef = useRef<Map<string, ExcalidrawElement>>(new Map());
|
const cachedElementsRef = useRef<ElementsMap>(new Map());
|
||||||
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
||||||
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
|
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
|
||||||
const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
|
const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
|
||||||
|
@ -996,7 +986,7 @@ export const actionChangeFontFamily = register({
|
||||||
const selectedFontFamily = useMemo(() => {
|
const selectedFontFamily = useMemo(() => {
|
||||||
const getFontFamily = (
|
const getFontFamily = (
|
||||||
elementsArray: readonly ExcalidrawElement[],
|
elementsArray: readonly ExcalidrawElement[],
|
||||||
elementsMap: Map<string, ExcalidrawElement>,
|
elementsMap: ElementsMap,
|
||||||
) =>
|
) =>
|
||||||
getFormValue(
|
getFormValue(
|
||||||
elementsArray,
|
elementsArray,
|
||||||
|
@ -1179,7 +1169,7 @@ export const actionChangeTextAlign = register({
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
newElement,
|
newElement,
|
||||||
app.scene.getContainerElement(oldElement),
|
app.scene.getContainerElement(oldElement),
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
@ -1270,7 +1260,7 @@ export const actionChangeVerticalAlign = register({
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
newElement,
|
newElement,
|
||||||
app.scene.getContainerElement(oldElement),
|
app.scene.getContainerElement(oldElement),
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene,
|
||||||
);
|
);
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
|
@ -1670,10 +1660,10 @@ export const actionChangeArrowType = register({
|
||||||
newElement,
|
newElement,
|
||||||
startHoveredElement,
|
startHoveredElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
app.scene,
|
||||||
);
|
);
|
||||||
endHoveredElement &&
|
endHoveredElement &&
|
||||||
bindLinearElement(newElement, endHoveredElement, "end", elementsMap);
|
bindLinearElement(newElement, endHoveredElement, "end", app.scene);
|
||||||
|
|
||||||
const startBinding =
|
const startBinding =
|
||||||
startElement && newElement.startBinding
|
startElement && newElement.startBinding
|
||||||
|
@ -1684,7 +1674,6 @@ export const actionChangeArrowType = register({
|
||||||
newElement,
|
newElement,
|
||||||
startElement,
|
startElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
@ -1697,7 +1686,6 @@ export const actionChangeArrowType = register({
|
||||||
newElement,
|
newElement,
|
||||||
endElement,
|
endElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
@ -1729,7 +1717,7 @@ export const actionChangeArrowType = register({
|
||||||
newElement.startBinding.elementId,
|
newElement.startBinding.elementId,
|
||||||
) as ExcalidrawBindableElement;
|
) as ExcalidrawBindableElement;
|
||||||
if (startElement) {
|
if (startElement) {
|
||||||
bindLinearElement(newElement, startElement, "start", elementsMap);
|
bindLinearElement(newElement, startElement, "start", app.scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newElement.endBinding) {
|
if (newElement.endBinding) {
|
||||||
|
@ -1737,7 +1725,7 @@ export const actionChangeArrowType = register({
|
||||||
newElement.endBinding.elementId,
|
newElement.endBinding.elementId,
|
||||||
) as ExcalidrawBindableElement;
|
) as ExcalidrawBindableElement;
|
||||||
if (endElement) {
|
if (endElement) {
|
||||||
bindLinearElement(newElement, endElement, "end", elementsMap);
|
bindLinearElement(newElement, endElement, "end", app.scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1758,6 +1746,7 @@ export const actionChangeArrowType = register({
|
||||||
if (selected) {
|
if (selected) {
|
||||||
newState.selectedLinearElement = new LinearElementEditor(
|
newState.selectedLinearElement = new LinearElementEditor(
|
||||||
selected as ExcalidrawLinearElement,
|
selected as ExcalidrawLinearElement,
|
||||||
|
arrayToMap(elements),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { getNonDeletedElements } from "@excalidraw/element";
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks";
|
import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { KEYS } from "@excalidraw/common";
|
import { arrayToMap, KEYS } from "@excalidraw/common";
|
||||||
|
|
||||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ export const actionSelectAll = register({
|
||||||
// single linear element selected
|
// single linear element selected
|
||||||
Object.keys(selectedElementIds).length === 1 &&
|
Object.keys(selectedElementIds).length === 1 &&
|
||||||
isLinearElement(elements[0])
|
isLinearElement(elements[0])
|
||||||
? new LinearElementEditor(elements[0])
|
? new LinearElementEditor(elements[0], arrayToMap(elements))
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
|
|
@ -139,11 +139,8 @@ export const actionPasteStyles = register({
|
||||||
element.id === newElement.containerId,
|
element.id === newElement.containerId,
|
||||||
) || null;
|
) || null;
|
||||||
}
|
}
|
||||||
redrawTextBoundingBox(
|
|
||||||
newElement,
|
redrawTextBoundingBox(newElement, container, app.scene);
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -129,7 +129,6 @@ export class AnimatedTrail implements Trail {
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.pastTrails = [];
|
|
||||||
this.start();
|
this.start();
|
||||||
if (this.trailAnimation) {
|
if (this.trailAnimation) {
|
||||||
this.trailAnimation.setAttribute("begin", "indefinite");
|
this.trailAnimation.setAttribute("begin", "indefinite");
|
||||||
|
|
|
@ -37,6 +37,8 @@ import {
|
||||||
syncMovedIndices,
|
syncMovedIndices,
|
||||||
} from "@excalidraw/element/fractionalIndex";
|
} from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
|
import Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
|
import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
|
||||||
|
|
||||||
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
||||||
|
@ -490,6 +492,7 @@ export class AppStateChange implements Change<AppState> {
|
||||||
nextElements.get(
|
nextElements.get(
|
||||||
selectedLinearElementId,
|
selectedLinearElementId,
|
||||||
) as NonDeleted<ExcalidrawLinearElement>,
|
) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
nextElements,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -499,6 +502,7 @@ export class AppStateChange implements Change<AppState> {
|
||||||
nextElements.get(
|
nextElements.get(
|
||||||
editingLinearElementId,
|
editingLinearElementId,
|
||||||
) as NonDeleted<ExcalidrawLinearElement>,
|
) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
nextElements,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
@ -1132,9 +1136,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
|
|
||||||
ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements);
|
|
||||||
|
|
||||||
// the following reorder performs also mutations, but only on new instances of changed elements
|
// the following reorder performs also mutations, but only on new instances of changed elements
|
||||||
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
||||||
nextElements = ElementsChange.reorderElements(
|
nextElements = ElementsChange.reorderElements(
|
||||||
|
@ -1143,8 +1144,14 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
flags,
|
flags,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
||||||
|
// we also don't have a scene on the server
|
||||||
|
// so we are creating a temp scene just to query and mutate elements
|
||||||
|
const tempScene = new Scene(nextElements);
|
||||||
|
|
||||||
|
ElementsChange.redrawTextBoundingBoxes(tempScene, changedElements);
|
||||||
// Need ordered nextElements to avoid z-index binding issues
|
// Need ordered nextElements to avoid z-index binding issues
|
||||||
ElementsChange.redrawBoundArrows(nextElements, changedElements);
|
ElementsChange.redrawBoundArrows(tempScene, changedElements);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Couldn't mutate elements after applying elements change`,
|
`Couldn't mutate elements after applying elements change`,
|
||||||
|
@ -1337,8 +1344,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
} else {
|
} else {
|
||||||
affectedElement = mutateElement(
|
affectedElement = mutateElement(
|
||||||
nextElement,
|
nextElement,
|
||||||
|
nextElements,
|
||||||
updates as ElementUpdate<OrderedExcalidrawElement>,
|
updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||||
);
|
) as OrderedExcalidrawElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAffectedElements.set(affectedElement.id, affectedElement);
|
nextAffectedElements.set(affectedElement.id, affectedElement);
|
||||||
|
@ -1456,9 +1464,10 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static redrawTextBoundingBoxes(
|
private static redrawTextBoundingBoxes(
|
||||||
elements: SceneElementsMap,
|
scene: Scene,
|
||||||
changed: Map<string, OrderedExcalidrawElement>,
|
changed: Map<string, OrderedExcalidrawElement>,
|
||||||
) {
|
) {
|
||||||
|
const elements = scene.getNonDeletedElementsMap();
|
||||||
const boxesToRedraw = new Map<
|
const boxesToRedraw = new Map<
|
||||||
string,
|
string,
|
||||||
{ container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
|
{ container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
|
||||||
|
@ -1498,17 +1507,17 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
redrawTextBoundingBox(boundText, container, elements, false);
|
redrawTextBoundingBox(boundText, container, scene);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static redrawBoundArrows(
|
private static redrawBoundArrows(
|
||||||
elements: SceneElementsMap,
|
scene: Scene,
|
||||||
changed: Map<string, OrderedExcalidrawElement>,
|
changed: Map<string, OrderedExcalidrawElement>,
|
||||||
) {
|
) {
|
||||||
for (const element of changed.values()) {
|
for (const element of changed.values()) {
|
||||||
if (!element.isDeleted && isBindableElement(element)) {
|
if (!element.isDeleted && isBindableElement(element)) {
|
||||||
updateBoundElements(element, elements, {
|
updateBoundElements(element, scene, {
|
||||||
changedElements: changed,
|
changedElements: changed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,7 +172,7 @@ export const serializeAsClipboardJSON = ({
|
||||||
!framesToCopy.has(getContainingFrame(element, elementsMap)!)
|
!framesToCopy.has(getContainingFrame(element, elementsMap)!)
|
||||||
) {
|
) {
|
||||||
const copiedElement = deepCopyElement(element);
|
const copiedElement = deepCopyElement(element);
|
||||||
mutateElement(copiedElement, {
|
mutateElement(copiedElement, elementsMap, {
|
||||||
frameId: null,
|
frameId: null,
|
||||||
});
|
});
|
||||||
return copiedElement;
|
return copiedElement;
|
||||||
|
|
|
@ -122,10 +122,7 @@ import {
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
import {
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
mutateElement,
|
|
||||||
newElementWith,
|
|
||||||
} from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
newFrameElement,
|
newFrameElement,
|
||||||
|
@ -279,6 +276,7 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
excludeElementsInFramesFromSelection,
|
excludeElementsInFramesFromSelection,
|
||||||
|
getSelectionStateForElements,
|
||||||
makeNextSelectedElementIds,
|
makeNextSelectedElementIds,
|
||||||
} from "@excalidraw/element/selection";
|
} from "@excalidraw/element/selection";
|
||||||
|
|
||||||
|
@ -302,6 +300,10 @@ import {
|
||||||
|
|
||||||
import { isNonDeletedElement } from "@excalidraw/element";
|
import { isNonDeletedElement } from "@excalidraw/element";
|
||||||
|
|
||||||
|
import Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
|
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -327,9 +329,10 @@ import type {
|
||||||
MagicGenerationData,
|
MagicGenerationData,
|
||||||
ExcalidrawNonSelectionElement,
|
ExcalidrawNonSelectionElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
actionAddToLibrary,
|
actionAddToLibrary,
|
||||||
|
@ -402,7 +405,6 @@ import {
|
||||||
hasBackground,
|
hasBackground,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import {
|
import {
|
||||||
dataURLToFile,
|
dataURLToFile,
|
||||||
|
@ -759,6 +761,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (excalidrawAPI) {
|
if (excalidrawAPI) {
|
||||||
const api: ExcalidrawImperativeAPI = {
|
const api: ExcalidrawImperativeAPI = {
|
||||||
updateScene: this.updateScene,
|
updateScene: this.updateScene,
|
||||||
|
mutateElement: this.mutateElement,
|
||||||
updateLibrary: this.library.updateLibrary,
|
updateLibrary: this.library.updateLibrary,
|
||||||
addFiles: this.addFiles,
|
addFiles: this.addFiles,
|
||||||
resetScene: this.resetScene,
|
resetScene: this.resetScene,
|
||||||
|
@ -1386,7 +1389,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => {
|
private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => {
|
||||||
if (frame) {
|
if (frame) {
|
||||||
mutateElement(frame, { name: frame.name?.trim() || null });
|
this.scene.mutateElement(frame, { name: frame.name?.trim() || null });
|
||||||
}
|
}
|
||||||
this.setState({ editingFrame: null });
|
this.setState({ editingFrame: null });
|
||||||
};
|
};
|
||||||
|
@ -1443,7 +1446,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
autoFocus
|
autoFocus
|
||||||
value={frameNameInEdit}
|
value={frameNameInEdit}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
mutateElement(f, {
|
this.scene.mutateElement(f, {
|
||||||
name: e.target.value,
|
name: e.target.value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -1669,7 +1672,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
key={firstSelectedElement.id}
|
key={firstSelectedElement.id}
|
||||||
element={firstSelectedElement}
|
element={firstSelectedElement}
|
||||||
elementsMap={allElementsMap}
|
scene={this.scene}
|
||||||
setAppState={this.setAppState}
|
setAppState={this.setAppState}
|
||||||
onLinkOpen={this.props.onLinkOpen}
|
onLinkOpen={this.props.onLinkOpen}
|
||||||
setToast={this.setToast}
|
setToast={this.setToast}
|
||||||
|
@ -1829,6 +1832,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
scale={window.devicePixelRatio}
|
scale={window.devicePixelRatio}
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
|
renderScrollbars={
|
||||||
|
this.props.renderScrollbars === true
|
||||||
|
}
|
||||||
device={this.device}
|
device={this.device}
|
||||||
renderInteractiveSceneCallback={
|
renderInteractiveSceneCallback={
|
||||||
this.renderInteractiveSceneCallback
|
this.renderInteractiveSceneCallback
|
||||||
|
@ -1934,16 +1940,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// state only.
|
// state only.
|
||||||
// Thus reset so that we prefer local cache (if there was some
|
// Thus reset so that we prefer local cache (if there was some
|
||||||
// generationData set previously)
|
// generationData set previously)
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
frameElement,
|
frameElement,
|
||||||
{ customData: { generationData: undefined } },
|
{
|
||||||
false,
|
customData: { generationData: undefined },
|
||||||
|
},
|
||||||
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
frameElement,
|
frameElement,
|
||||||
{ customData: { generationData: data } },
|
{
|
||||||
false,
|
customData: { generationData: data },
|
||||||
|
},
|
||||||
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.magicGenerations.set(frameElement.id, data);
|
this.magicGenerations.set(frameElement.id, data);
|
||||||
|
@ -2115,7 +2125,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scene.insertElement(frame);
|
this.scene.insertElement(frame);
|
||||||
|
|
||||||
for (const child of selectedElements) {
|
for (const child of selectedElements) {
|
||||||
mutateElement(child, { frameId: frame.id });
|
this.scene.mutateElement(child, { frameId: frame.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -2914,8 +2924,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
nonDeletedElementsMap,
|
nonDeletedElementsMap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene,
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3267,7 +3276,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
|
||||||
|
|
||||||
const { newElements } = duplicateElements({
|
const { duplicatedElements } = duplicateElements({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
return newElementWith(element, {
|
return newElementWith(element, {
|
||||||
|
@ -3279,7 +3288,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
const prevElements = this.scene.getElementsIncludingDeleted();
|
const prevElements = this.scene.getElementsIncludingDeleted();
|
||||||
let nextElements = [...prevElements, ...newElements];
|
let nextElements = [...prevElements, ...duplicatedElements];
|
||||||
|
|
||||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||||
nextElements,
|
nextElements,
|
||||||
|
@ -3288,13 +3297,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
nextElements = mappedNewSceneElements || nextElements;
|
nextElements = mappedNewSceneElements || nextElements;
|
||||||
|
|
||||||
syncMovedIndices(nextElements, arrayToMap(newElements));
|
syncMovedIndices(nextElements, arrayToMap(duplicatedElements));
|
||||||
|
|
||||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||||
|
|
||||||
if (topLayerFrame) {
|
if (topLayerFrame) {
|
||||||
const eligibleElements = filterElementsEligibleAsFrameChildren(
|
const eligibleElements = filterElementsEligibleAsFrameChildren(
|
||||||
newElements,
|
duplicatedElements,
|
||||||
topLayerFrame,
|
topLayerFrame,
|
||||||
);
|
);
|
||||||
addElementsToFrame(
|
addElementsToFrame(
|
||||||
|
@ -3307,23 +3316,19 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextElements);
|
this.scene.replaceAllElements(nextElements);
|
||||||
|
|
||||||
newElements.forEach((newElement) => {
|
duplicatedElements.forEach((newElement) => {
|
||||||
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||||
const container = getContainerElement(
|
const container = getContainerElement(
|
||||||
newElement,
|
newElement,
|
||||||
this.scene.getElementsMapIncludingDeleted(),
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
);
|
);
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(newElement, container, this.scene);
|
||||||
newElement,
|
|
||||||
container,
|
|
||||||
this.scene.getElementsMapIncludingDeleted(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// paste event may not fire FontFace loadingdone event in Safari, hence loading font faces manually
|
// paste event may not fire FontFace loadingdone event in Safari, hence loading font faces manually
|
||||||
if (isSafari) {
|
if (isSafari) {
|
||||||
Fonts.loadElementsFonts(newElements).then((fontFaces) => {
|
Fonts.loadElementsFonts(duplicatedElements).then((fontFaces) => {
|
||||||
this.fonts.onLoaded(fontFaces);
|
this.fonts.onLoaded(fontFaces);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3335,7 +3340,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.store.shouldCaptureIncrement();
|
this.store.shouldCaptureIncrement();
|
||||||
|
|
||||||
const nextElementsToSelect =
|
const nextElementsToSelect =
|
||||||
excludeElementsInFramesFromSelection(newElements);
|
excludeElementsInFramesFromSelection(duplicatedElements);
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
|
@ -3378,7 +3383,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.setActiveTool({ type: "selection" });
|
this.setActiveTool({ type: "selection" });
|
||||||
|
|
||||||
if (opts.fitToContent) {
|
if (opts.fitToContent) {
|
||||||
this.scrollToContent(newElements, {
|
this.scrollToContent(duplicatedElements, {
|
||||||
fitToContent: true,
|
fitToContent: true,
|
||||||
canvasOffsets: this.getEditorUIOffsets(),
|
canvasOffsets: this.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
|
@ -3440,7 +3445,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
// hack to reset the `y` coord because we vertically center during
|
// hack to reset the `y` coord because we vertically center during
|
||||||
// insertImageElement
|
// insertImageElement
|
||||||
mutateElement(initializedImageElement, { y }, false);
|
this.scene.mutateElement(
|
||||||
|
initializedImageElement,
|
||||||
|
{ y },
|
||||||
|
{ informMutation: false, isDragging: false },
|
||||||
|
);
|
||||||
|
|
||||||
y = imageElement.y + imageElement.height + 25;
|
y = imageElement.y + imageElement.height + 25;
|
||||||
|
|
||||||
|
@ -3994,6 +4003,17 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
|
element: TElement,
|
||||||
|
updates: ElementUpdate<TElement>,
|
||||||
|
informMutation = true,
|
||||||
|
) => {
|
||||||
|
return this.scene.mutateElement(element, updates, {
|
||||||
|
informMutation,
|
||||||
|
isDragging: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private triggerRender = (
|
private triggerRender = (
|
||||||
/** force always re-renders canvas even if no change */
|
/** force always re-renders canvas even if no change */
|
||||||
force?: boolean,
|
force?: boolean,
|
||||||
|
@ -4162,9 +4182,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
) {
|
) {
|
||||||
this.flowChartCreator.createNodes(
|
this.flowChartCreator.createNodes(
|
||||||
selectedElements[0],
|
selectedElements[0],
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
this.state,
|
this.state,
|
||||||
getLinkDirectionFromKey(event.key),
|
getLinkDirectionFromKey(event.key),
|
||||||
|
this.scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4406,16 +4426,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
x: element.x + offsetX,
|
x: element.x + offsetX,
|
||||||
y: element.y + offsetY,
|
y: element.y + offsetY,
|
||||||
},
|
},
|
||||||
false,
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
updateBoundElements(element, this.scene, {
|
||||||
simultaneouslyUpdated: selectedElements,
|
simultaneouslyUpdated: selectedElements,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4449,6 +4469,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.setState({
|
this.setState({
|
||||||
editingLinearElement: new LinearElementEditor(
|
editingLinearElement: new LinearElementEditor(
|
||||||
selectedElement,
|
selectedElement,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4642,11 +4663,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (isArrowKey(event.key)) {
|
if (isArrowKey(event.key)) {
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
this.scene.getSelectedElements(this.state).filter(isLinearElement),
|
this.scene.getSelectedElements(this.state).filter(isLinearElement),
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.scene,
|
|
||||||
isBindingEnabled(this.state),
|
isBindingEnabled(this.state),
|
||||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||||
|
this.scene,
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
);
|
);
|
||||||
this.setState({ suggestedBindings: [] });
|
this.setState({ suggestedBindings: [] });
|
||||||
|
@ -4953,7 +4972,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
onChange: withBatchedUpdates((nextOriginalText) => {
|
onChange: withBatchedUpdates((nextOriginalText) => {
|
||||||
updateElement(nextOriginalText, false);
|
updateElement(nextOriginalText, false);
|
||||||
if (isNonDeletedElement(element)) {
|
if (isNonDeletedElement(element)) {
|
||||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap());
|
updateBoundElements(element, this.scene);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||||
|
@ -5316,7 +5335,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
||||||
const newHeight = Math.max(container.height, minHeight);
|
const newHeight = Math.max(container.height, minHeight);
|
||||||
const newWidth = Math.max(container.width, minWidth);
|
const newWidth = Math.max(container.width, minWidth);
|
||||||
mutateElement(container, { height: newHeight, width: newWidth });
|
this.scene.mutateElement(container, {
|
||||||
|
height: newHeight,
|
||||||
|
width: newWidth,
|
||||||
|
});
|
||||||
sceneX = container.x + newWidth / 2;
|
sceneX = container.x + newWidth / 2;
|
||||||
sceneY = container.y + newHeight / 2;
|
sceneY = container.y + newHeight / 2;
|
||||||
if (parentCenterPosition) {
|
if (parentCenterPosition) {
|
||||||
|
@ -5367,7 +5389,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingTextElement && shouldBindToContainer && container) {
|
if (!existingTextElement && shouldBindToContainer && container) {
|
||||||
mutateElement(container, {
|
this.scene.mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
type: "text",
|
type: "text",
|
||||||
id: element.id,
|
id: element.id,
|
||||||
|
@ -5443,7 +5465,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
) {
|
) {
|
||||||
this.store.shouldCaptureIncrement();
|
this.store.shouldCaptureIncrement();
|
||||||
this.setState({
|
this.setState({
|
||||||
editingLinearElement: new LinearElementEditor(selectedElements[0]),
|
editingLinearElement: new LinearElementEditor(
|
||||||
|
selectedElements[0],
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -5467,7 +5492,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
if (midPoint && midPoint > -1) {
|
if (midPoint && midPoint > -1) {
|
||||||
this.store.shouldCaptureIncrement();
|
this.store.shouldCaptureIncrement();
|
||||||
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
|
LinearElementEditor.deleteFixedSegment(
|
||||||
|
selectedElements[0],
|
||||||
|
this.scene,
|
||||||
|
midPoint,
|
||||||
|
);
|
||||||
|
|
||||||
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||||
{
|
{
|
||||||
|
@ -5850,7 +5879,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
scenePointerX,
|
scenePointerX,
|
||||||
scenePointerY,
|
scenePointerY,
|
||||||
this,
|
this,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -5912,7 +5940,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
lastPoint,
|
lastPoint,
|
||||||
) >= LINE_CONFIRM_THRESHOLD
|
) >= LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
multiElement,
|
multiElement,
|
||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
|
@ -5920,7 +5948,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
false,
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
|
@ -5936,12 +5964,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
multiElement,
|
multiElement,
|
||||||
{
|
{
|
||||||
points: points.slice(0, -1),
|
points: points.slice(0, -1),
|
||||||
},
|
},
|
||||||
false,
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
@ -5973,8 +6001,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
if (isPathALoop(points, this.state.zoom.value)) {
|
if (isPathALoop(points, this.state.zoom.value)) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update last uncommitted point
|
// update last uncommitted point
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
multiElement,
|
multiElement,
|
||||||
{
|
{
|
||||||
points: [
|
points: [
|
||||||
|
@ -5985,9 +6014,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
false,
|
|
||||||
{
|
{
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
|
informMutation: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -6574,7 +6603,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||||
|
|
||||||
mutateElement(pendingImageElement, {
|
this.scene.mutateElement(pendingImageElement, {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
frameId: frame ? frame.id : null,
|
frameId: frame ? frame.id : null,
|
||||||
|
@ -6942,6 +6971,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
drag: {
|
drag: {
|
||||||
hasOccurred: false,
|
hasOccurred: false,
|
||||||
offset: null,
|
offset: null,
|
||||||
|
origin: { ...origin },
|
||||||
},
|
},
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
onMove: null,
|
onMove: null,
|
||||||
|
@ -7628,7 +7658,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
multiElement.type === "line" &&
|
multiElement.type === "line" &&
|
||||||
isPathALoop(multiElement.points, this.state.zoom.value)
|
isPathALoop(multiElement.points, this.state.zoom.value)
|
||||||
) {
|
) {
|
||||||
mutateElement(multiElement, {
|
this.scene.mutateElement(multiElement, {
|
||||||
lastCommittedPoint:
|
lastCommittedPoint:
|
||||||
multiElement.points[multiElement.points.length - 1],
|
multiElement.points[multiElement.points.length - 1],
|
||||||
});
|
});
|
||||||
|
@ -7639,7 +7669,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// Elbow arrows cannot be created by putting down points
|
// Elbow arrows cannot be created by putting down points
|
||||||
// only the start and end points can be defined
|
// only the start and end points can be defined
|
||||||
if (isElbowArrow(multiElement) && multiElement.points.length > 1) {
|
if (isElbowArrow(multiElement) && multiElement.points.length > 1) {
|
||||||
mutateElement(multiElement, {
|
this.scene.mutateElement(multiElement, {
|
||||||
lastCommittedPoint:
|
lastCommittedPoint:
|
||||||
multiElement.points[multiElement.points.length - 1],
|
multiElement.points[multiElement.points.length - 1],
|
||||||
});
|
});
|
||||||
|
@ -7676,7 +7706,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}));
|
}));
|
||||||
// clicking outside commit zone → update reference for last committed
|
// clicking outside commit zone → update reference for last committed
|
||||||
// point
|
// point
|
||||||
mutateElement(multiElement, {
|
this.scene.mutateElement(multiElement, {
|
||||||
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
||||||
});
|
});
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
|
@ -7762,7 +7792,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
this.scene.mutateElement(element, {
|
||||||
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
||||||
});
|
});
|
||||||
const boundElement = getHoveredElementForBinding(
|
const boundElement = getHoveredElementForBinding(
|
||||||
|
@ -8012,7 +8042,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
index,
|
index,
|
||||||
gridX,
|
gridX,
|
||||||
gridY,
|
gridY,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
|
@ -8117,7 +8147,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
pointerCoords,
|
pointerCoords,
|
||||||
this,
|
this,
|
||||||
!event[KEYS.CTRL_OR_CMD],
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
elementsMap,
|
this.scene,
|
||||||
);
|
);
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
return;
|
return;
|
||||||
|
@ -8236,8 +8266,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state.activeEmbeddable?.state !== "active"
|
this.state.activeEmbeddable?.state !== "active"
|
||||||
) {
|
) {
|
||||||
const dragOffset = {
|
const dragOffset = {
|
||||||
x: pointerCoords.x - pointerDownState.origin.x,
|
x: pointerCoords.x - pointerDownState.drag.origin.x,
|
||||||
y: pointerCoords.y - pointerDownState.origin.y,
|
y: pointerCoords.y - pointerDownState.drag.origin.y,
|
||||||
};
|
};
|
||||||
|
|
||||||
const originalElements = [
|
const originalElements = [
|
||||||
|
@ -8344,7 +8374,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(croppingElement, {
|
this.scene.mutateElement(croppingElement, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8432,52 +8462,112 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
selectedElements.map((el) => [el.id, el]),
|
selectedElements.map((el) => [el.id, el]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { newElements: clonedElements, elementsWithClones } =
|
const {
|
||||||
duplicateElements({
|
duplicatedElements,
|
||||||
|
duplicateElementsMap,
|
||||||
|
elementsWithDuplicates,
|
||||||
|
origIdToDuplicateId,
|
||||||
|
} = duplicateElements({
|
||||||
type: "in-place",
|
type: "in-place",
|
||||||
elements,
|
elements,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
idsOfElementsToDuplicate,
|
idsOfElementsToDuplicate,
|
||||||
overrides: (el) => {
|
overrides: ({ duplicateElement, origElement }) => {
|
||||||
|
return {
|
||||||
|
// reset to the original element's frameId (unless we've
|
||||||
|
// duplicated alongside a frame in which case we need to
|
||||||
|
// keep the duplicate frame's id) so that the element
|
||||||
|
// frame membership is refreshed on pointerup
|
||||||
|
// NOTE this is a hacky solution and should be done
|
||||||
|
// differently
|
||||||
|
frameId: duplicateElement.frameId ?? origElement.frameId,
|
||||||
|
seed: randomInteger(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
duplicatedElements.forEach((element) => {
|
||||||
|
pointerDownState.originalElements.set(
|
||||||
|
element.id,
|
||||||
|
deepCopyElement(element),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mappedClonedElements = elementsWithDuplicates.map((el) => {
|
||||||
|
if (idsOfElementsToDuplicate.has(el.id)) {
|
||||||
const origEl = pointerDownState.originalElements.get(el.id);
|
const origEl = pointerDownState.originalElements.get(el.id);
|
||||||
|
|
||||||
if (origEl) {
|
if (origEl) {
|
||||||
return {
|
return newElementWith(el, {
|
||||||
x: origEl.x,
|
x: origEl.x,
|
||||||
y: origEl.y,
|
y: origEl.y,
|
||||||
seed: origEl.seed,
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
reverseOrder: true,
|
|
||||||
});
|
|
||||||
clonedElements.forEach((element) => {
|
|
||||||
pointerDownState.originalElements.set(element.id, element);
|
|
||||||
});
|
|
||||||
|
|
||||||
const mappedNewSceneElements = this.props.onDuplicate?.(
|
|
||||||
elementsWithClones,
|
|
||||||
elements,
|
|
||||||
);
|
|
||||||
|
|
||||||
const nextSceneElements = syncMovedIndices(
|
|
||||||
mappedNewSceneElements || elementsWithClones,
|
|
||||||
arrayToMap(clonedElements),
|
|
||||||
).map((el) => {
|
|
||||||
if (idsOfElementsToDuplicate.has(el.id)) {
|
|
||||||
return newElementWith(el, {
|
|
||||||
seed: randomInteger(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextSceneElements);
|
const mappedNewSceneElements = this.props.onDuplicate?.(
|
||||||
|
mappedClonedElements,
|
||||||
|
elements,
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsWithIndices = syncMovedIndices(
|
||||||
|
mappedNewSceneElements || mappedClonedElements,
|
||||||
|
arrayToMap(duplicatedElements),
|
||||||
|
);
|
||||||
|
|
||||||
|
// we need to update synchronously so as to keep pointerDownState,
|
||||||
|
// appState, and scene elements in sync
|
||||||
|
flushSync(() => {
|
||||||
|
// swap hit element with the duplicated one
|
||||||
|
if (pointerDownState.hit.element) {
|
||||||
|
const cloneId = origIdToDuplicateId.get(
|
||||||
|
pointerDownState.hit.element.id,
|
||||||
|
);
|
||||||
|
const clonedElement =
|
||||||
|
cloneId && duplicateElementsMap.get(cloneId);
|
||||||
|
pointerDownState.hit.element = clonedElement || null;
|
||||||
|
}
|
||||||
|
// swap hit elements with the duplicated ones
|
||||||
|
pointerDownState.hit.allHitElements =
|
||||||
|
pointerDownState.hit.allHitElements.reduce(
|
||||||
|
(
|
||||||
|
acc: typeof pointerDownState.hit.allHitElements,
|
||||||
|
origHitElement,
|
||||||
|
) => {
|
||||||
|
const cloneId = origIdToDuplicateId.get(origHitElement.id);
|
||||||
|
const clonedElement =
|
||||||
|
cloneId && duplicateElementsMap.get(cloneId);
|
||||||
|
if (clonedElement) {
|
||||||
|
acc.push(clonedElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// update drag origin to the position at which we started
|
||||||
|
// the duplication so that the drag offset is correct
|
||||||
|
pointerDownState.drag.origin = viewportCoordsToSceneCoords(
|
||||||
|
event,
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
// switch selected elements to the duplicated ones
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
...getSelectionStateForElements(
|
||||||
|
duplicatedElements,
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.scene.replaceAllElements(elementsWithIndices);
|
||||||
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
this.maybeCacheVisibleGaps(event, selectedElements, true);
|
||||||
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -8541,13 +8631,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
? newElement.pressures
|
? newElement.pressures
|
||||||
: [...newElement.pressures, event.pressure];
|
: [...newElement.pressures, event.pressure];
|
||||||
|
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||||
pressures,
|
pressures,
|
||||||
},
|
},
|
||||||
false,
|
{
|
||||||
|
informMutation: false,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -8570,24 +8663,23 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (points.length === 1) {
|
if (points.length === 1) {
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
points.length === 2 ||
|
points.length === 2 ||
|
||||||
(points.length > 1 && isElbowArrow(newElement))
|
(points.length > 1 && isElbowArrow(newElement))
|
||||||
) {
|
) {
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
newElement,
|
newElement,
|
||||||
{
|
{
|
||||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||||
},
|
},
|
||||||
false,
|
{ isDragging: true, informMutation: false },
|
||||||
{ isDragging: true },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8698,7 +8790,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
selectedLinearElement:
|
selectedLinearElement:
|
||||||
elementsWithinSelection.length === 1 &&
|
elementsWithinSelection.length === 1 &&
|
||||||
isLinearElement(elementsWithinSelection[0])
|
isLinearElement(elementsWithinSelection[0])
|
||||||
? new LinearElementEditor(elementsWithinSelection[0])
|
? new LinearElementEditor(
|
||||||
|
elementsWithinSelection[0],
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
showHyperlinkPopup:
|
showHyperlinkPopup:
|
||||||
elementsWithinSelection.length === 1 &&
|
elementsWithinSelection.length === 1 &&
|
||||||
|
@ -8722,7 +8817,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
const dx = x - pointerDownState.lastCoords.x;
|
const dx = x - pointerDownState.lastCoords.x;
|
||||||
this.translateCanvas({
|
this.translateCanvas({
|
||||||
scrollX: this.state.scrollX - dx / this.state.zoom.value,
|
scrollX:
|
||||||
|
this.state.scrollX -
|
||||||
|
(dx * (currentScrollBars.horizontal?.deltaMultiplier || 1)) /
|
||||||
|
this.state.zoom.value,
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.x = x;
|
pointerDownState.lastCoords.x = x;
|
||||||
return true;
|
return true;
|
||||||
|
@ -8732,7 +8830,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const y = event.clientY;
|
const y = event.clientY;
|
||||||
const dy = y - pointerDownState.lastCoords.y;
|
const dy = y - pointerDownState.lastCoords.y;
|
||||||
this.translateCanvas({
|
this.translateCanvas({
|
||||||
scrollY: this.state.scrollY - dy / this.state.zoom.value,
|
scrollY:
|
||||||
|
this.state.scrollY -
|
||||||
|
(dy * (currentScrollBars.vertical?.deltaMultiplier || 1)) /
|
||||||
|
this.state.zoom.value,
|
||||||
});
|
});
|
||||||
pointerDownState.lastCoords.y = y;
|
pointerDownState.lastCoords.y = y;
|
||||||
return true;
|
return true;
|
||||||
|
@ -8798,7 +8899,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
.map((e) => elementsMap.get(e.id))
|
.map((e) => elementsMap.get(e.id))
|
||||||
.filter((e) => isElbowArrow(e))
|
.filter((e) => isElbowArrow(e))
|
||||||
.forEach((e) => {
|
.forEach((e) => {
|
||||||
!!e && mutateElement(e, {}, true);
|
!!e && this.scene.mutateElement(e, {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8834,7 +8935,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
if (element) {
|
if (element) {
|
||||||
mutateElement(element, {}, true);
|
this.scene.mutateElement(
|
||||||
|
element as ExcalidrawElbowArrowElement,
|
||||||
|
{},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8863,7 +8967,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
element,
|
element,
|
||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
|
||||||
this.scene,
|
this.scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8930,7 +9033,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
? []
|
? []
|
||||||
: [...newElement.pressures, childEvent.pressure];
|
: [...newElement.pressures, childEvent.pressure];
|
||||||
|
|
||||||
mutateElement(newElement, {
|
this.scene.mutateElement(newElement, {
|
||||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||||
pressures,
|
pressures,
|
||||||
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
||||||
|
@ -8977,7 +9080,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
|
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
|
||||||
mutateElement(newElement, {
|
this.scene.mutateElement(
|
||||||
|
newElement,
|
||||||
|
{
|
||||||
points: [
|
points: [
|
||||||
...newElement.points,
|
...newElement.points,
|
||||||
pointFrom<LocalPoint>(
|
pointFrom<LocalPoint>(
|
||||||
|
@ -8985,7 +9090,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
pointerCoords.y - newElement.y,
|
pointerCoords.y - newElement.y,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
});
|
},
|
||||||
|
{ informMutation: false, isDragging: false },
|
||||||
|
);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
multiElement: newElement,
|
multiElement: newElement,
|
||||||
newElement,
|
newElement,
|
||||||
|
@ -8999,8 +9107,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
newElement,
|
newElement,
|
||||||
this.state,
|
this.state,
|
||||||
pointerCoords,
|
pointerCoords,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene,
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||||
|
@ -9018,7 +9125,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
},
|
},
|
||||||
prevState,
|
prevState,
|
||||||
),
|
),
|
||||||
selectedLinearElement: new LinearElementEditor(newElement),
|
selectedLinearElement: new LinearElementEditor(
|
||||||
|
newElement,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
|
@ -9041,7 +9151,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newElement.width < minWidth) {
|
if (newElement.width < minWidth) {
|
||||||
mutateElement(newElement, {
|
this.scene.mutateElement(newElement, {
|
||||||
autoResize: true,
|
autoResize: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9091,7 +9201,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newElement) {
|
if (newElement) {
|
||||||
mutateElement(newElement, getNormalizedDimensions(newElement));
|
this.scene.mutateElement(
|
||||||
|
newElement,
|
||||||
|
getNormalizedDimensions(newElement),
|
||||||
|
{
|
||||||
|
informMutation: false,
|
||||||
|
isDragging: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
// the above does not guarantee the scene to be rendered again, hence the trigger below
|
// the above does not guarantee the scene to be rendered again, hence the trigger below
|
||||||
this.scene.triggerUpdate();
|
this.scene.triggerUpdate();
|
||||||
}
|
}
|
||||||
|
@ -9123,7 +9240,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
) {
|
) {
|
||||||
// remove the linear element from all groups
|
// remove the linear element from all groups
|
||||||
// before removing it from the frame as well
|
// before removing it from the frame as well
|
||||||
mutateElement(linearElement, {
|
this.scene.mutateElement(linearElement, {
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9152,12 +9269,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.state.editingGroupId!,
|
this.state.editingGroupId!,
|
||||||
);
|
);
|
||||||
|
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
groupIds: element.groupIds.slice(0, index),
|
groupIds: element.groupIds.slice(0, index),
|
||||||
},
|
},
|
||||||
false,
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9169,12 +9286,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
element.groupIds[element.groupIds.length - 1],
|
element.groupIds[element.groupIds.length - 1],
|
||||||
).length < 2
|
).length < 2
|
||||||
) {
|
) {
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
},
|
},
|
||||||
false,
|
{ informMutation: false, isDragging: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -9284,7 +9401,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// the one we've hit
|
// the one we've hit
|
||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedLinearElement: new LinearElementEditor(hitElement),
|
selectedLinearElement: new LinearElementEditor(
|
||||||
|
hitElement,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9409,7 +9529,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
selectedLinearElement:
|
selectedLinearElement:
|
||||||
newSelectedElements.length === 1 &&
|
newSelectedElements.length === 1 &&
|
||||||
isLinearElement(newSelectedElements[0])
|
isLinearElement(newSelectedElements[0])
|
||||||
? new LinearElementEditor(newSelectedElements[0])
|
? new LinearElementEditor(
|
||||||
|
newSelectedElements[0],
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
: prevState.selectedLinearElement,
|
: prevState.selectedLinearElement,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -9483,7 +9606,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
|
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
|
||||||
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
|
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
|
||||||
prevState.selectedLinearElement?.elementId !== hitElement.id
|
prevState.selectedLinearElement?.elementId !== hitElement.id
|
||||||
? new LinearElementEditor(hitElement)
|
? new LinearElementEditor(
|
||||||
|
hitElement,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
: prevState.selectedLinearElement,
|
: prevState.selectedLinearElement,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -9576,11 +9702,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
linearElements,
|
linearElements,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.scene,
|
|
||||||
isBindingEnabled(this.state),
|
isBindingEnabled(this.state),
|
||||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||||
|
this.scene,
|
||||||
this.state.zoom,
|
this.state.zoom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9737,12 +9861,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const dataURL =
|
const dataURL =
|
||||||
this.files[fileId]?.dataURL || (await getDataURL(imageFile));
|
this.files[fileId]?.dataURL || (await getDataURL(imageFile));
|
||||||
|
|
||||||
const imageElement = mutateElement(
|
const imageElement = this.scene.mutateElement(
|
||||||
_imageElement,
|
_imageElement,
|
||||||
{
|
{
|
||||||
fileId,
|
fileId,
|
||||||
},
|
},
|
||||||
false,
|
{ informMutation: false, isDragging: false },
|
||||||
) as NonDeleted<InitializedExcalidrawImageElement>;
|
) as NonDeleted<InitializedExcalidrawImageElement>;
|
||||||
|
|
||||||
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
|
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
|
||||||
|
@ -9808,7 +9932,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
showCursorImagePreview,
|
showCursorImagePreview,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
mutateElement(imageElement, {
|
this.scene.mutateElement(imageElement, {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
});
|
});
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
|
@ -9954,7 +10078,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value
|
imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value
|
||||||
) {
|
) {
|
||||||
const placeholderSize = 100 / this.state.zoom.value;
|
const placeholderSize = 100 / this.state.zoom.value;
|
||||||
mutateElement(imageElement, {
|
this.scene.mutateElement(imageElement, {
|
||||||
x: imageElement.x - placeholderSize / 2,
|
x: imageElement.x - placeholderSize / 2,
|
||||||
y: imageElement.y - placeholderSize / 2,
|
y: imageElement.y - placeholderSize / 2,
|
||||||
width: placeholderSize,
|
width: placeholderSize,
|
||||||
|
@ -9988,7 +10112,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
||||||
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
||||||
|
|
||||||
mutateElement(imageElement, {
|
this.scene.mutateElement(imageElement, {
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
|
@ -10419,7 +10543,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this,
|
this,
|
||||||
),
|
),
|
||||||
selectedLinearElement: isLinearElement(element)
|
selectedLinearElement: isLinearElement(element)
|
||||||
? new LinearElementEditor(element)
|
? new LinearElementEditor(
|
||||||
|
element,
|
||||||
|
this.scene.getNonDeletedElementsMap(),
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
: this.state),
|
: this.state),
|
||||||
|
@ -10452,8 +10579,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
||||||
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
||||||
shouldResizeFromCenter: false,
|
shouldResizeFromCenter: false,
|
||||||
|
scene: this.scene,
|
||||||
zoom: this.state.zoom.value,
|
zoom: this.state.zoom.value,
|
||||||
informMutation,
|
informMutation: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -10517,6 +10645,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
: shouldMaintainAspectRatio(event),
|
: shouldMaintainAspectRatio(event),
|
||||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||||
zoom: this.state.zoom.value,
|
zoom: this.state.zoom.value,
|
||||||
|
scene: this.scene,
|
||||||
widthAspectRatio: aspectRatio,
|
widthAspectRatio: aspectRatio,
|
||||||
originOffset: this.state.originSnapOffset,
|
originOffset: this.state.originSnapOffset,
|
||||||
informMutation,
|
informMutation,
|
||||||
|
@ -10604,7 +10733,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
);
|
);
|
||||||
|
|
||||||
mutateElement(
|
this.scene.mutateElement(
|
||||||
croppingElement,
|
croppingElement,
|
||||||
cropElement(
|
cropElement(
|
||||||
croppingElement,
|
croppingElement,
|
||||||
|
@ -10619,16 +10748,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
updateBoundElements(
|
updateBoundElements(croppingElement, this.scene, {
|
||||||
croppingElement,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
{
|
|
||||||
newSize: {
|
newSize: {
|
||||||
width: croppingElement.width,
|
width: croppingElement.width,
|
||||||
height: croppingElement.height,
|
height: croppingElement.height,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isCropping: transformHandleType && transformHandleType !== "rotation",
|
isCropping: transformHandleType && transformHandleType !== "rotation",
|
||||||
|
@ -10742,7 +10867,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
pointerDownState.originalElements,
|
pointerDownState.originalElements,
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene.getElementsMapIncludingDeleted(),
|
|
||||||
this.scene,
|
this.scene,
|
||||||
shouldRotateWithDiscreteAngle(event),
|
shouldRotateWithDiscreteAngle(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
|
|
|
@ -6,9 +6,10 @@ import {
|
||||||
defaultGetElementLinkFromSelection,
|
defaultGetElementLinkFromSelection,
|
||||||
getLinkIdAndTypeFromSelection,
|
getLinkIdAndTypeFromSelection,
|
||||||
} from "@excalidraw/element/elementLink";
|
} from "@excalidraw/element/elementLink";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
@ -21,20 +22,20 @@ import { TrashIcon } from "./icons";
|
||||||
import "./ElementLinkDialog.scss";
|
import "./ElementLinkDialog.scss";
|
||||||
|
|
||||||
import type { AppProps, AppState, UIAppState } from "../types";
|
import type { AppProps, AppState, UIAppState } from "../types";
|
||||||
|
|
||||||
const ElementLinkDialog = ({
|
const ElementLinkDialog = ({
|
||||||
sourceElementId,
|
sourceElementId,
|
||||||
onClose,
|
onClose,
|
||||||
elementsMap,
|
|
||||||
appState,
|
appState,
|
||||||
|
scene,
|
||||||
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
||||||
}: {
|
}: {
|
||||||
sourceElementId: ExcalidrawElement["id"];
|
sourceElementId: ExcalidrawElement["id"];
|
||||||
elementsMap: ElementsMap;
|
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
|
scene: Scene;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
||||||
}) => {
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
||||||
|
|
||||||
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
||||||
|
@ -70,7 +71,7 @@ const ElementLinkDialog = ({
|
||||||
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
|
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
|
||||||
const elementToLink = elementsMap.get(sourceElementId);
|
const elementToLink = elementsMap.get(sourceElementId);
|
||||||
elementToLink &&
|
elementToLink &&
|
||||||
mutateElement(elementToLink, {
|
scene.mutateElement(elementToLink, {
|
||||||
link: nextLink,
|
link: nextLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -78,13 +79,13 @@ const ElementLinkDialog = ({
|
||||||
if (!nextLink && linkEdited && sourceElementId) {
|
if (!nextLink && linkEdited && sourceElementId) {
|
||||||
const elementToLink = elementsMap.get(sourceElementId);
|
const elementToLink = elementsMap.get(sourceElementId);
|
||||||
elementToLink &&
|
elementToLink &&
|
||||||
mutateElement(elementToLink, {
|
scene.mutateElement(elementToLink, {
|
||||||
link: null,
|
link: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose?.();
|
onClose?.();
|
||||||
}, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
|
}, [sourceElementId, nextLink, elementsMap, linkEdited, scene, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
CLASSES,
|
CLASSES,
|
||||||
DEFAULT_SIDEBAR,
|
DEFAULT_SIDEBAR,
|
||||||
TOOL_TYPE,
|
TOOL_TYPE,
|
||||||
|
arrayToMap,
|
||||||
capitalizeString,
|
capitalizeString,
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
@ -17,7 +18,6 @@ import { ShapeCache } from "@excalidraw/element/ShapeCache";
|
||||||
|
|
||||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { actionToggleStats } from "../actions";
|
import { actionToggleStats } from "../actions";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { isHandToolActive } from "../appState";
|
import { isHandToolActive } from "../appState";
|
||||||
|
@ -446,9 +446,7 @@ const LayerUI = ({
|
||||||
|
|
||||||
if (selectedElements.length) {
|
if (selectedElements.length) {
|
||||||
for (const element of selectedElements) {
|
for (const element of selectedElements) {
|
||||||
mutateElement(
|
mutateElement(element, arrayToMap(elements), {
|
||||||
element,
|
|
||||||
{
|
|
||||||
[altKey && eyeDropperState.swapPreviewOnAlt
|
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||||
? colorPickerType === "elementBackground"
|
? colorPickerType === "elementBackground"
|
||||||
? "strokeColor"
|
? "strokeColor"
|
||||||
|
@ -456,12 +454,10 @@ const LayerUI = ({
|
||||||
: colorPickerType === "elementBackground"
|
: colorPickerType === "elementBackground"
|
||||||
? "backgroundColor"
|
? "backgroundColor"
|
||||||
: "strokeColor"]: color,
|
: "strokeColor"]: color,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
ShapeCache.delete(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
Scene.getScene(selectedElements[0])?.triggerUpdate();
|
app.scene.triggerUpdate();
|
||||||
} else if (colorPickerType === "elementBackground") {
|
} else if (colorPickerType === "elementBackground") {
|
||||||
setAppState({
|
setAppState({
|
||||||
currentItemBackgroundColor: color,
|
currentItemBackgroundColor: color,
|
||||||
|
@ -494,7 +490,7 @@ const LayerUI = ({
|
||||||
openDialog: null,
|
openDialog: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
scene={app.scene}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
generateLinkForSelection={generateLinkForSelection}
|
generateLinkForSelection={generateLinkForSelection}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -166,7 +166,7 @@ export default function LibraryMenuItems({
|
||||||
type: "everything",
|
type: "everything",
|
||||||
elements: item.elements,
|
elements: item.elements,
|
||||||
randomizeSeed: true,
|
randomizeSeed: true,
|
||||||
}).newElements,
|
}).duplicatedElements,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
@ -9,13 +7,14 @@ import type { Degrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface AngleProps {
|
interface AngleProps {
|
||||||
|
@ -35,7 +34,6 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
scene,
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement && !isElbowArrow(origElement)) {
|
if (origElement && !isElbowArrow(origElement)) {
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
|
@ -45,14 +43,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextAngle = degreesToRadians(nextValue as Degrees);
|
const nextAngle = degreesToRadians(nextValue as Degrees);
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
updateBindings(latestElement, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -71,14 +69,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||||
|
|
||||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||||
|
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
updateBindings(latestElement, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle });
|
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { getNormalizedGridStep } from "../../scene";
|
import { getNormalizedGridStep } from "../../scene";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
|
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface PositionProps {
|
interface PositionProps {
|
||||||
|
|
|
@ -5,17 +5,17 @@ import {
|
||||||
MINIMAL_CROP_SIZE,
|
MINIMAL_CROP_SIZE,
|
||||||
getUncroppedWidthAndHeight,
|
getUncroppedWidthAndHeight,
|
||||||
} from "@excalidraw/element/cropElement";
|
} from "@excalidraw/element/cropElement";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import { resizeSingleElement } from "@excalidraw/element/resizeElements";
|
import { resizeSingleElement } from "@excalidraw/element/resizeElements";
|
||||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface DimensionDragInputProps {
|
interface DimensionDragInputProps {
|
||||||
|
@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||||
|
@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
height: nextCropHeight,
|
height: nextCropHeight,
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||||
|
@ -176,8 +176,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldMaintainAspectRatio: keepAspectRatio,
|
shouldMaintainAspectRatio: keepAspectRatio,
|
||||||
|
@ -223,8 +223,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldMaintainAspectRatio: keepAspectRatio,
|
shouldMaintainAspectRatio: keepAspectRatio,
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { CaptureUpdateAction } from "../../store";
|
import { CaptureUpdateAction } from "../../store";
|
||||||
import { useApp } from "../App";
|
import { useApp } from "../App";
|
||||||
import { InlineIcon } from "../InlineIcon";
|
import { InlineIcon } from "../InlineIcon";
|
||||||
|
@ -16,7 +18,6 @@ import { SMALLEST_DELTA } from "./utils";
|
||||||
import "./DragInput.scss";
|
import "./DragInput.scss";
|
||||||
|
|
||||||
import type { StatsInputProperty } from "./utils";
|
import type { StatsInputProperty } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
export type DragInputCallbackType<
|
export type DragInputCallbackType<
|
||||||
|
@ -216,8 +217,7 @@ const StatsDragInput = <
|
||||||
y: number;
|
y: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
let originalElementsMap: ElementsMap | null = app.scene
|
||||||
app.scene
|
|
||||||
.getNonDeletedElements()
|
.getNonDeletedElements()
|
||||||
.reduce((acc: ElementsMap, element) => {
|
.reduce((acc: ElementsMap, element) => {
|
||||||
acc.set(element.id, deepCopyElement(element));
|
acc.set(element.id, deepCopyElement(element));
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
|
@ -13,13 +12,14 @@ import type {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { fontSizeIcon } from "../icons";
|
import { fontSizeIcon } from "../icons";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface FontSizeProps {
|
interface FontSizeProps {
|
||||||
|
@ -68,13 +68,13 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextFontSize) {
|
if (nextFontSize) {
|
||||||
mutateElement(latestElement, {
|
scene.mutateElement(latestElement, {
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
});
|
});
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
latestElement,
|
latestElement,
|
||||||
scene.getContainerElement(latestElement),
|
scene.getContainerElement(latestElement),
|
||||||
scene.getNonDeletedElementsMap(),
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import { isArrowElement } from "@excalidraw/element/typeChecks";
|
import { isArrowElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
@ -11,13 +9,14 @@ import type { Degrees } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiAngleProps {
|
interface MultiAngleProps {
|
||||||
|
@ -54,17 +53,13 @@ const handleDegreeChange: DragInputCallbackType<
|
||||||
if (!element) {
|
if (!element) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
mutateElement(
|
scene.mutateElement(element, {
|
||||||
element,
|
|
||||||
{
|
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(element)) {
|
if (boundTextElement && !isArrowElement(element)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,17 +87,13 @@ const handleDegreeChange: DragInputCallbackType<
|
||||||
|
|
||||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||||
|
|
||||||
mutateElement(
|
scene.mutateElement(latestElement, {
|
||||||
latestElement,
|
|
||||||
{
|
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { useMemo } from "react";
|
||||||
|
|
||||||
import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
|
import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
|
||||||
import { updateBoundElements } from "@excalidraw/element/binding";
|
import { updateBoundElements } from "@excalidraw/element/binding";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
rescalePointsInElement,
|
rescalePointsInElement,
|
||||||
resizeSingleElement,
|
resizeSingleElement,
|
||||||
|
@ -23,13 +22,14 @@ import type {
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
import { getElementsInAtomicUnit } from "./utils";
|
import { getElementsInAtomicUnit } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiDimensionProps {
|
interface MultiDimensionProps {
|
||||||
|
@ -75,33 +75,31 @@ const resizeElementInGroup = (
|
||||||
scale: number,
|
scale: number,
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||||
|
|
||||||
mutateElement(latestElement, updates, false);
|
scene.mutateElement(latestElement, updates);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
origElement,
|
origElement,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const newFontSize = boundTextElement.fontSize * scale;
|
const newFontSize = boundTextElement.fontSize * scale;
|
||||||
updateBoundElements(latestElement, elementsMap, {
|
updateBoundElements(latestElement, scene, {
|
||||||
newSize: { width: updates.width, height: updates.height },
|
newSize: { width: updates.width, height: updates.height },
|
||||||
});
|
});
|
||||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||||
mutateElement(
|
scene.mutateElement(latestBoundTextElement, {
|
||||||
latestBoundTextElement,
|
|
||||||
{
|
|
||||||
fontSize: newFontSize,
|
fontSize: newFontSize,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
handleBindTextResize(
|
handleBindTextResize(
|
||||||
latestElement,
|
latestElement,
|
||||||
elementsMap,
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
@ -118,8 +116,8 @@ const resizeGroup = (
|
||||||
property: MultiDimensionProps["property"],
|
property: MultiDimensionProps["property"],
|
||||||
latestElements: ExcalidrawElement[],
|
latestElements: ExcalidrawElement[],
|
||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
// keep aspect ratio for groups
|
// keep aspect ratio for groups
|
||||||
if (property === "width") {
|
if (property === "width") {
|
||||||
|
@ -141,8 +139,8 @@ const resizeGroup = (
|
||||||
scale,
|
scale,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -194,8 +192,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [el] = elementsInUnit;
|
const [el] = elementsInUnit;
|
||||||
|
@ -237,8 +235,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldInformMutation: false,
|
shouldInformMutation: false,
|
||||||
|
@ -301,8 +299,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
property,
|
property,
|
||||||
latestElements,
|
latestElements,
|
||||||
originalElements,
|
originalElements,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const [el] = elementsInUnit;
|
const [el] = elementsInUnit;
|
||||||
|
@ -340,8 +338,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||||
nextHeight,
|
nextHeight,
|
||||||
latestElement,
|
latestElement,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldInformMutation: false,
|
shouldInformMutation: false,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
|
@ -16,13 +15,14 @@ import type {
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { fontSizeIcon } from "../icons";
|
import { fontSizeIcon } from "../icons";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiFontSizeProps {
|
interface MultiFontSizeProps {
|
||||||
|
@ -84,19 +84,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||||
|
|
||||||
for (const textElement of latestTextElements) {
|
for (const textElement of latestTextElements) {
|
||||||
mutateElement(
|
scene.mutateElement(textElement, {
|
||||||
textElement,
|
|
||||||
{
|
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
textElement,
|
textElement,
|
||||||
scene.getContainerElement(textElement),
|
scene.getContainerElement(textElement),
|
||||||
elementsMap,
|
scene,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,19 +112,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
||||||
}
|
}
|
||||||
mutateElement(
|
scene.mutateElement(latestElement, {
|
||||||
latestElement,
|
|
||||||
{
|
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
},
|
});
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
redrawTextBoundingBox(
|
redrawTextBoundingBox(
|
||||||
latestElement,
|
latestElement,
|
||||||
scene.getContainerElement(latestElement),
|
scene.getContainerElement(latestElement),
|
||||||
elementsMap,
|
scene,
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,9 @@ import { isTextElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||||
|
|
||||||
import type {
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawElement,
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
@ -18,7 +15,6 @@ import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type { AtomicUnit } from "./utils";
|
import type { AtomicUnit } from "./utils";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface MultiPositionProps {
|
interface MultiPositionProps {
|
||||||
|
@ -36,13 +32,11 @@ const moveElements = (
|
||||||
property: MultiPositionProps["property"],
|
property: MultiPositionProps["property"],
|
||||||
changeInTopX: number,
|
changeInTopX: number,
|
||||||
changeInTopY: number,
|
changeInTopY: number,
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
originalElements: readonly ExcalidrawElement[],
|
originalElements: readonly ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < originalElements.length; i++) {
|
||||||
const origElement = originalElements[i];
|
const origElement = originalElements[i];
|
||||||
|
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
|
@ -65,8 +59,6 @@ const moveElements = (
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
|
@ -78,11 +70,10 @@ const moveGroupTo = (
|
||||||
nextX: number,
|
nextX: number,
|
||||||
nextY: number,
|
nextY: number,
|
||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
const [x1, y1, ,] = getCommonBounds(originalElements);
|
||||||
const offsetX = nextX - x1;
|
const offsetX = nextX - x1;
|
||||||
const offsetY = nextY - y1;
|
const offsetY = nextY - y1;
|
||||||
|
@ -112,8 +103,6 @@ const moveGroupTo = (
|
||||||
topLeftX + offsetX,
|
topLeftX + offsetX,
|
||||||
topLeftY + offsetY,
|
topLeftY + offsetY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
|
@ -135,7 +124,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
originalAppState,
|
originalAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
for (const atomicUnit of getAtomicUnits(
|
for (const atomicUnit of getAtomicUnits(
|
||||||
|
@ -159,8 +147,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
elementsInUnit.map((el) => el.original),
|
elementsInUnit.map((el) => el.original),
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
|
@ -188,8 +174,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
|
@ -214,8 +198,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||||
changeInTopX,
|
changeInTopX,
|
||||||
changeInTopY,
|
changeInTopY,
|
||||||
originalElements,
|
originalElements,
|
||||||
originalElements,
|
|
||||||
elementsMap,
|
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,16 +4,16 @@ import {
|
||||||
getFlipAdjustedCropPosition,
|
getFlipAdjustedCropPosition,
|
||||||
getUncroppedWidthAndHeight,
|
getUncroppedWidthAndHeight,
|
||||||
} from "@excalidraw/element/cropElement";
|
} from "@excalidraw/element/cropElement";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import { getStepSizedValue, moveElement } from "./utils";
|
import { getStepSizedValue, moveElement } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type Scene from "../../scene/Scene";
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
interface PositionProps {
|
interface PositionProps {
|
||||||
|
@ -38,7 +38,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
originalAppState,
|
originalAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const elements = scene.getNonDeletedElements();
|
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
|
@ -101,7 +100,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -119,7 +118,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
|
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
|
||||||
};
|
};
|
||||||
|
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
crop: nextCrop,
|
crop: nextCrop,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -133,8 +132,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
|
@ -166,8 +163,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||||
newTopLeftX,
|
newTopLeftX,
|
||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,7 @@ import type {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { Excalidraw, getCommonBounds, mutateElement } from "../..";
|
import { Excalidraw, getCommonBounds } from "../..";
|
||||||
import { actionGroup } from "../../actions";
|
import { actionGroup } from "../../actions";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import * as StaticScene from "../../renderer/staticScene";
|
import * as StaticScene from "../../renderer/staticScene";
|
||||||
|
@ -478,7 +478,7 @@ describe("stats for a non-generic element", () => {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [{ type: "text", id: text.id }],
|
boundElements: [{ type: "text", id: text.id }],
|
||||||
});
|
});
|
||||||
API.setElements([container, text]);
|
API.setElements([container, text]);
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
bindOrUnbindLinearElements,
|
bindOrUnbindLinearElements,
|
||||||
updateBoundElements,
|
updateBoundElements,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import {
|
import {
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
|
@ -24,10 +23,10 @@ import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
export type StatsInputProperty =
|
export type StatsInputProperty =
|
||||||
|
@ -119,12 +118,11 @@ export const moveElement = (
|
||||||
newTopLeftX: number,
|
newTopLeftX: number,
|
||||||
newTopLeftY: number,
|
newTopLeftY: number,
|
||||||
originalElement: ExcalidrawElement,
|
originalElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const latestElement = elementsMap.get(originalElement.id);
|
const latestElement = elementsMap.get(originalElement.id);
|
||||||
if (!latestElement) {
|
if (!latestElement) {
|
||||||
return;
|
return;
|
||||||
|
@ -148,15 +146,15 @@ export const moveElement = (
|
||||||
-originalElement.angle as Radians,
|
-originalElement.angle as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
latestElement,
|
latestElement,
|
||||||
{
|
{
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
},
|
},
|
||||||
shouldInformMutation,
|
{ informMutation: shouldInformMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
updateBindings(latestElement, elementsMap, elements, scene);
|
updateBindings(latestElement, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
originalElement,
|
originalElement,
|
||||||
|
@ -165,13 +163,13 @@ export const moveElement = (
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||||
latestBoundTextElement &&
|
latestBoundTextElement &&
|
||||||
mutateElement(
|
scene.mutateElement(
|
||||||
latestBoundTextElement,
|
latestBoundTextElement,
|
||||||
{
|
{
|
||||||
x: boundTextElement.x + changeInX,
|
x: boundTextElement.x + changeInX,
|
||||||
y: boundTextElement.y + changeInY,
|
y: boundTextElement.y + changeInY,
|
||||||
},
|
},
|
||||||
shouldInformMutation,
|
{ informMutation: shouldInformMutation, isDragging: false },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -199,8 +197,6 @@ export const getAtomicUnits = (
|
||||||
|
|
||||||
export const updateBindings = (
|
export const updateBindings = (
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
|
@ -209,16 +205,8 @@ export const updateBindings = (
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (isLinearElement(latestElement)) {
|
if (isLinearElement(latestElement)) {
|
||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
||||||
[latestElement],
|
|
||||||
elementsMap,
|
|
||||||
elements,
|
|
||||||
scene,
|
|
||||||
true,
|
|
||||||
[],
|
|
||||||
options?.zoom,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
updateBoundElements(latestElement, elementsMap, options);
|
updateBoundElements(latestElement, scene, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,6 +34,7 @@ type InteractiveCanvasProps = {
|
||||||
selectionNonce: number | undefined;
|
selectionNonce: number | undefined;
|
||||||
scale: number;
|
scale: number;
|
||||||
appState: InteractiveCanvasAppState;
|
appState: InteractiveCanvasAppState;
|
||||||
|
renderScrollbars: boolean;
|
||||||
device: Device;
|
device: Device;
|
||||||
renderInteractiveSceneCallback: (
|
renderInteractiveSceneCallback: (
|
||||||
data: RenderInteractiveSceneCallback,
|
data: RenderInteractiveSceneCallback,
|
||||||
|
@ -143,7 +144,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||||
remotePointerUsernames,
|
remotePointerUsernames,
|
||||||
remotePointerUserStates,
|
remotePointerUserStates,
|
||||||
selectionColor,
|
selectionColor,
|
||||||
renderScrollbars: false,
|
renderScrollbars: props.renderScrollbars,
|
||||||
},
|
},
|
||||||
device: props.device,
|
device: props.device,
|
||||||
callback: props.renderInteractiveSceneCallback,
|
callback: props.renderInteractiveSceneCallback,
|
||||||
|
@ -230,7 +231,8 @@ const areEqual = (
|
||||||
// on appState)
|
// on appState)
|
||||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||||
prevProps.selectedElements !== nextProps.selectedElements
|
prevProps.selectedElements !== nextProps.selectedElements ||
|
||||||
|
prevProps.renderScrollbars !== nextProps.renderScrollbars
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,6 @@ import {
|
||||||
embeddableURLValidator,
|
embeddableURLValidator,
|
||||||
} from "@excalidraw/element/embeddable";
|
} from "@excalidraw/element/embeddable";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
|
@ -33,6 +31,8 @@ import {
|
||||||
|
|
||||||
import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawEmbeddableElement,
|
ExcalidrawEmbeddableElement,
|
||||||
|
@ -70,14 +70,14 @@ const embeddableLinkCache = new Map<
|
||||||
|
|
||||||
export const Hyperlink = ({
|
export const Hyperlink = ({
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
scene,
|
||||||
setAppState,
|
setAppState,
|
||||||
onLinkOpen,
|
onLinkOpen,
|
||||||
setToast,
|
setToast,
|
||||||
updateEmbedValidationStatus,
|
updateEmbedValidationStatus,
|
||||||
}: {
|
}: {
|
||||||
element: NonDeletedExcalidrawElement;
|
element: NonDeletedExcalidrawElement;
|
||||||
elementsMap: ElementsMap;
|
scene: Scene;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||||
setToast: (
|
setToast: (
|
||||||
|
@ -88,6 +88,7 @@ export const Hyperlink = ({
|
||||||
status: boolean,
|
status: boolean,
|
||||||
) => void;
|
) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const appProps = useAppProps();
|
const appProps = useAppProps();
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
|
@ -114,7 +115,7 @@ export const Hyperlink = ({
|
||||||
setAppState({ activeEmbeddable: null });
|
setAppState({ activeEmbeddable: null });
|
||||||
}
|
}
|
||||||
if (!link) {
|
if (!link) {
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
link: null,
|
link: null,
|
||||||
});
|
});
|
||||||
updateEmbedValidationStatus(element, false);
|
updateEmbedValidationStatus(element, false);
|
||||||
|
@ -126,7 +127,7 @@ export const Hyperlink = ({
|
||||||
setToast({ message: t("toast.unableToEmbed"), closable: true });
|
setToast({ message: t("toast.unableToEmbed"), closable: true });
|
||||||
}
|
}
|
||||||
element.link && embeddableLinkCache.set(element.id, element.link);
|
element.link && embeddableLinkCache.set(element.id, element.link);
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
link,
|
link,
|
||||||
});
|
});
|
||||||
updateEmbedValidationStatus(element, false);
|
updateEmbedValidationStatus(element, false);
|
||||||
|
@ -144,7 +145,7 @@ export const Hyperlink = ({
|
||||||
: 1;
|
: 1;
|
||||||
const hasLinkChanged =
|
const hasLinkChanged =
|
||||||
embeddableLinkCache.get(element.id) !== element.link;
|
embeddableLinkCache.get(element.id) !== element.link;
|
||||||
mutateElement(element, {
|
scene.mutateElement(element, {
|
||||||
...(hasLinkChanged
|
...(hasLinkChanged
|
||||||
? {
|
? {
|
||||||
width:
|
width:
|
||||||
|
@ -169,10 +170,11 @@ export const Hyperlink = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mutateElement(element, { link });
|
scene.mutateElement(element, { link });
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
element,
|
element,
|
||||||
|
scene,
|
||||||
setToast,
|
setToast,
|
||||||
appProps.validateEmbeddable,
|
appProps.validateEmbeddable,
|
||||||
appState.activeEmbeddable,
|
appState.activeEmbeddable,
|
||||||
|
@ -229,9 +231,9 @@ export const Hyperlink = ({
|
||||||
|
|
||||||
const handleRemove = useCallback(() => {
|
const handleRemove = useCallback(() => {
|
||||||
trackEvent("hyperlink", "delete");
|
trackEvent("hyperlink", "delete");
|
||||||
mutateElement(element, { link: null });
|
scene.mutateElement(element, { link: null });
|
||||||
setAppState({ showHyperlinkPopup: false });
|
setAppState({ showHyperlinkPopup: false });
|
||||||
}, [setAppState, element]);
|
}, [setAppState, element, scene]);
|
||||||
|
|
||||||
const onEdit = () => {
|
const onEdit = () => {
|
||||||
trackEvent("hyperlink", "edit", "popup-ui");
|
trackEvent("hyperlink", "edit", "popup-ui");
|
||||||
|
|
|
@ -38,10 +38,13 @@ import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
|
|
||||||
|
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||||
|
|
||||||
|
import Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
|
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
@ -63,8 +66,6 @@ import type {
|
||||||
|
|
||||||
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { getCommonBounds } from "..";
|
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
x: number;
|
x: number;
|
||||||
|
@ -221,7 +222,7 @@ const DEFAULT_DIMENSION = 100;
|
||||||
const bindTextToContainer = (
|
const bindTextToContainer = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||||
elementsMap: ElementsMap,
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
const textElement: ExcalidrawTextElement = newTextElement({
|
const textElement: ExcalidrawTextElement = newTextElement({
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -240,7 +241,8 @@ const bindTextToContainer = (
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
redrawTextBoundingBox(textElement, container, elementsMap);
|
redrawTextBoundingBox(textElement, container, scene);
|
||||||
|
|
||||||
return [container, textElement] as const;
|
return [container, textElement] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -249,7 +251,7 @@ const bindLinearElementToElement = (
|
||||||
start: ValidLinearElement["start"],
|
start: ValidLinearElement["start"],
|
||||||
end: ValidLinearElement["end"],
|
end: ValidLinearElement["end"],
|
||||||
elementStore: ElementStore,
|
elementStore: ElementStore,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
): {
|
): {
|
||||||
linearElement: ExcalidrawLinearElement;
|
linearElement: ExcalidrawLinearElement;
|
||||||
startBoundElement?: ExcalidrawElement;
|
startBoundElement?: ExcalidrawElement;
|
||||||
|
@ -335,7 +337,7 @@ const bindLinearElementToElement = (
|
||||||
linearElement,
|
linearElement,
|
||||||
startBoundElement as ExcalidrawBindableElement,
|
startBoundElement as ExcalidrawBindableElement,
|
||||||
"start",
|
"start",
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -410,7 +412,7 @@ const bindLinearElementToElement = (
|
||||||
linearElement,
|
linearElement,
|
||||||
endBoundElement as ExcalidrawBindableElement,
|
endBoundElement as ExcalidrawBindableElement,
|
||||||
"end",
|
"end",
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -651,6 +653,9 @@ export const convertToExcalidrawElements = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementsMap = elementStore.getElementsMap();
|
const elementsMap = elementStore.getElementsMap();
|
||||||
|
// we don't have a real scene, so we just use a temp scene to query and mutate elements
|
||||||
|
const scene = new Scene(elementsMap);
|
||||||
|
|
||||||
// Add labels and arrow bindings
|
// Add labels and arrow bindings
|
||||||
for (const [id, element] of elementsWithIds) {
|
for (const [id, element] of elementsWithIds) {
|
||||||
const excalidrawElement = elementStore.getElement(id)!;
|
const excalidrawElement = elementStore.getElement(id)!;
|
||||||
|
@ -664,7 +669,7 @@ export const convertToExcalidrawElements = (
|
||||||
let [container, text] = bindTextToContainer(
|
let [container, text] = bindTextToContainer(
|
||||||
excalidrawElement,
|
excalidrawElement,
|
||||||
element?.label,
|
element?.label,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
elementStore.add(container);
|
elementStore.add(container);
|
||||||
elementStore.add(text);
|
elementStore.add(text);
|
||||||
|
@ -692,7 +697,7 @@ export const convertToExcalidrawElements = (
|
||||||
originalStart,
|
originalStart,
|
||||||
originalEnd,
|
originalEnd,
|
||||||
elementStore,
|
elementStore,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
container = linearElement;
|
container = linearElement;
|
||||||
elementStore.add(linearElement);
|
elementStore.add(linearElement);
|
||||||
|
@ -717,7 +722,7 @@ export const convertToExcalidrawElements = (
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
elementStore,
|
elementStore,
|
||||||
elementsMap,
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
elementStore.add(linearElement);
|
elementStore.add(linearElement);
|
||||||
|
|
|
@ -28,6 +28,8 @@ import type {
|
||||||
|
|
||||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { CascadiaFontFaces } from "./Cascadia";
|
import { CascadiaFontFaces } from "./Cascadia";
|
||||||
import { ComicShannsFontFaces } from "./ComicShanns";
|
import { ComicShannsFontFaces } from "./ComicShanns";
|
||||||
import { EmojiFontFaces } from "./Emoji";
|
import { EmojiFontFaces } from "./Emoji";
|
||||||
|
@ -40,8 +42,6 @@ import { NunitoFontFaces } from "./Nunito";
|
||||||
import { VirgilFontFaces } from "./Virgil";
|
import { VirgilFontFaces } from "./Virgil";
|
||||||
import { XiaolaiFontFaces } from "./Xiaolai";
|
import { XiaolaiFontFaces } from "./Xiaolai";
|
||||||
|
|
||||||
import type Scene from "../scene/Scene";
|
|
||||||
|
|
||||||
export class Fonts {
|
export class Fonts {
|
||||||
// it's ok to track fonts across multiple instances only once, so let's use
|
// it's ok to track fonts across multiple instances only once, so let's use
|
||||||
// a static member to reduce memory footprint
|
// a static member to reduce memory footprint
|
||||||
|
|
|
@ -53,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable,
|
renderEmbeddable,
|
||||||
aiEnabled,
|
aiEnabled,
|
||||||
showDeprecatedFonts,
|
showDeprecatedFonts,
|
||||||
|
renderScrollbars,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
|
@ -143,6 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable={renderEmbeddable}
|
renderEmbeddable={renderEmbeddable}
|
||||||
aiEnabled={aiEnabled !== false}
|
aiEnabled={aiEnabled !== false}
|
||||||
showDeprecatedFonts={showDeprecatedFonts}
|
showDeprecatedFonts={showDeprecatedFonts}
|
||||||
|
renderScrollbars={renderScrollbars}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</App>
|
</App>
|
||||||
|
|
|
@ -149,6 +149,7 @@ export class LassoTrail extends AnimatedTrail {
|
||||||
this.app.scene.getNonDeletedElement(
|
this.app.scene.getNonDeletedElement(
|
||||||
selectedIds[0],
|
selectedIds[0],
|
||||||
) as NonDeleted<ExcalidrawLinearElement>,
|
) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
|
this.app.scene.getNonDeletedElementsMap(),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1182,7 +1182,7 @@ const _renderInteractiveScene = ({
|
||||||
let scrollBars;
|
let scrollBars;
|
||||||
if (renderConfig.renderScrollbars) {
|
if (renderConfig.renderScrollbars) {
|
||||||
scrollBars = getScrollBars(
|
scrollBars = getScrollBars(
|
||||||
visibleElements,
|
elementsMap,
|
||||||
normalizedWidth,
|
normalizedWidth,
|
||||||
normalizedHeight,
|
normalizedHeight,
|
||||||
appState,
|
appState,
|
||||||
|
|
|
@ -9,10 +9,11 @@ import type {
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
|
import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
|
||||||
import { renderStaticSceneThrottled } from "../renderer/staticScene";
|
import { renderStaticSceneThrottled } from "../renderer/staticScene";
|
||||||
|
|
||||||
import type Scene from "./Scene";
|
|
||||||
import type { RenderableElementsMap } from "./types";
|
import type { RenderableElementsMap } from "./types";
|
||||||
|
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
|
|
|
@ -2,24 +2,23 @@ import { getGlobalCSSVariable } from "@excalidraw/common";
|
||||||
|
|
||||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
|
||||||
|
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage } from "../i18n";
|
||||||
|
|
||||||
import type { InteractiveCanvasAppState } from "../types";
|
import type { InteractiveCanvasAppState } from "../types";
|
||||||
import type { ScrollBars } from "./types";
|
import type { RenderableElementsMap, ScrollBars } from "./types";
|
||||||
|
|
||||||
export const SCROLLBAR_MARGIN = 4;
|
export const SCROLLBAR_MARGIN = 4;
|
||||||
export const SCROLLBAR_WIDTH = 6;
|
export const SCROLLBAR_WIDTH = 6;
|
||||||
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||||
|
|
||||||
|
// The scrollbar represents where the viewport is in relationship to the scene
|
||||||
export const getScrollBars = (
|
export const getScrollBars = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: RenderableElementsMap,
|
||||||
viewportWidth: number,
|
viewportWidth: number,
|
||||||
viewportHeight: number,
|
viewportHeight: number,
|
||||||
appState: InteractiveCanvasAppState,
|
appState: InteractiveCanvasAppState,
|
||||||
): ScrollBars => {
|
): ScrollBars => {
|
||||||
if (!elements.length) {
|
if (!elements.size) {
|
||||||
return {
|
return {
|
||||||
horizontal: null,
|
horizontal: null,
|
||||||
vertical: null,
|
vertical: null,
|
||||||
|
@ -33,9 +32,6 @@ export const getScrollBars = (
|
||||||
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||||
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||||
|
|
||||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
|
||||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
|
||||||
|
|
||||||
const safeArea = {
|
const safeArea = {
|
||||||
top: parseInt(getGlobalCSSVariable("sat")) || 0,
|
top: parseInt(getGlobalCSSVariable("sat")) || 0,
|
||||||
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
|
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
|
||||||
|
@ -46,10 +42,8 @@ export const getScrollBars = (
|
||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
|
|
||||||
// The viewport is the rectangle currently visible for the user
|
// The viewport is the rectangle currently visible for the user
|
||||||
const viewportMinX =
|
const viewportMinX = -appState.scrollX + safeArea.left;
|
||||||
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
const viewportMinY = -appState.scrollY + safeArea.top;
|
||||||
const viewportMinY =
|
|
||||||
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
|
||||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||||
|
|
||||||
|
@ -59,8 +53,43 @@ export const getScrollBars = (
|
||||||
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
|
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
|
||||||
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
|
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
|
||||||
|
|
||||||
// The scrollbar represents where the viewport is in relationship to the scene
|
// the elements-only bbox
|
||||||
|
const sceneWidth = elementsMaxX - elementsMinX;
|
||||||
|
const sceneHeight = elementsMaxY - elementsMinY;
|
||||||
|
|
||||||
|
// scene (elements) bbox + the viewport bbox that extends outside of it
|
||||||
|
const extendedSceneWidth = sceneMaxX - sceneMinX;
|
||||||
|
const extendedSceneHeight = sceneMaxY - sceneMinY;
|
||||||
|
|
||||||
|
const scrollWidthOffset =
|
||||||
|
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right) +
|
||||||
|
SCROLLBAR_WIDTH * 2;
|
||||||
|
|
||||||
|
const scrollbarWidth =
|
||||||
|
viewportWidth * (viewportWidthWithZoom / extendedSceneWidth) -
|
||||||
|
scrollWidthOffset;
|
||||||
|
|
||||||
|
const scrollbarHeightOffset =
|
||||||
|
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom) +
|
||||||
|
SCROLLBAR_WIDTH * 2;
|
||||||
|
|
||||||
|
const scrollbarHeight =
|
||||||
|
viewportHeight * (viewportHeightWithZoom / extendedSceneHeight) -
|
||||||
|
scrollbarHeightOffset;
|
||||||
|
// NOTE the delta multiplier calculation isn't quite correct when viewport
|
||||||
|
// is extended outside the scene (elements) bbox as there's some small
|
||||||
|
// accumulation error. I'll let this be an exercise for others to fix. ^^
|
||||||
|
const horizontalDeltaMultiplier =
|
||||||
|
extendedSceneWidth > sceneWidth
|
||||||
|
? (extendedSceneWidth * appState.zoom.value) /
|
||||||
|
(scrollbarWidth + scrollWidthOffset)
|
||||||
|
: viewportWidth / (scrollbarWidth + scrollWidthOffset);
|
||||||
|
|
||||||
|
const verticalDeltaMultiplier =
|
||||||
|
extendedSceneHeight > sceneHeight
|
||||||
|
? (extendedSceneHeight * appState.zoom.value) /
|
||||||
|
(scrollbarHeight + scrollbarHeightOffset)
|
||||||
|
: viewportHeight / (scrollbarHeight + scrollbarHeightOffset);
|
||||||
return {
|
return {
|
||||||
horizontal:
|
horizontal:
|
||||||
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
|
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
|
||||||
|
@ -68,18 +97,17 @@ export const getScrollBars = (
|
||||||
: {
|
: {
|
||||||
x:
|
x:
|
||||||
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
|
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
|
||||||
((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) *
|
SCROLLBAR_WIDTH +
|
||||||
viewportWidth,
|
((viewportMinX - sceneMinX) / extendedSceneWidth) * viewportWidth,
|
||||||
y:
|
y:
|
||||||
viewportHeight -
|
viewportHeight -
|
||||||
SCROLLBAR_WIDTH -
|
SCROLLBAR_WIDTH -
|
||||||
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
|
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
|
||||||
width:
|
width: scrollbarWidth,
|
||||||
((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) *
|
|
||||||
viewportWidth -
|
|
||||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
|
|
||||||
height: SCROLLBAR_WIDTH,
|
height: SCROLLBAR_WIDTH,
|
||||||
|
deltaMultiplier: horizontalDeltaMultiplier,
|
||||||
},
|
},
|
||||||
|
|
||||||
vertical:
|
vertical:
|
||||||
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
|
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
|
||||||
? null
|
? null
|
||||||
|
@ -90,14 +118,13 @@ export const getScrollBars = (
|
||||||
SCROLLBAR_WIDTH -
|
SCROLLBAR_WIDTH -
|
||||||
Math.max(safeArea.right, SCROLLBAR_MARGIN),
|
Math.max(safeArea.right, SCROLLBAR_MARGIN),
|
||||||
y:
|
y:
|
||||||
((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) *
|
Math.max(safeArea.top, SCROLLBAR_MARGIN) +
|
||||||
viewportHeight +
|
SCROLLBAR_WIDTH +
|
||||||
Math.max(safeArea.top, SCROLLBAR_MARGIN),
|
((viewportMinY - sceneMinY) / extendedSceneHeight) *
|
||||||
|
viewportHeight,
|
||||||
width: SCROLLBAR_WIDTH,
|
width: SCROLLBAR_WIDTH,
|
||||||
height:
|
height: scrollbarHeight,
|
||||||
((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) *
|
deltaMultiplier: verticalDeltaMultiplier,
|
||||||
viewportHeight -
|
|
||||||
Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -130,12 +130,14 @@ export type ScrollBars = {
|
||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
deltaMultiplier: number;
|
||||||
} | null;
|
} | null;
|
||||||
vertical: {
|
vertical: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
deltaMultiplier: number;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7338,8 +7338,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": 0,
|
||||||
"y": -10,
|
"y": 0,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -7412,8 +7412,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": -10,
|
"x": 0,
|
||||||
"y": -10,
|
"y": 0,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -7480,7 +7480,7 @@ History {
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`;
|
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `10`;
|
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `9`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = `
|
exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
|
@ -10551,7 +10551,7 @@ History {
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`;
|
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `15`;
|
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `14`;
|
||||||
|
|
||||||
exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = `
|
exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
|
@ -12128,8 +12128,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": -10,
|
||||||
"y": 10,
|
"y": -10,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12182,8 +12182,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 60,
|
"x": 40,
|
||||||
"y": 0,
|
"y": -20,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12236,8 +12236,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 150,
|
"x": 130,
|
||||||
"y": -10,
|
"y": -30,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -12291,8 +12291,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": -10,
|
||||||
"y": 10,
|
"y": -10,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -12377,8 +12377,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "freedraw",
|
"type": "freedraw",
|
||||||
"width": 50,
|
"width": 50,
|
||||||
"x": 150,
|
"x": 130,
|
||||||
"y": -10,
|
"y": -30,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -20178,4 +20178,4 @@ History {
|
||||||
|
|
||||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`;
|
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`;
|
||||||
|
|
||||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `21`;
|
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `20`;
|
||||||
|
|
|
@ -1,40 +1,6 @@
|
||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
||||||
{
|
|
||||||
"angle": 0,
|
|
||||||
"backgroundColor": "transparent",
|
|
||||||
"boundElements": null,
|
|
||||||
"customData": undefined,
|
|
||||||
"fillStyle": "solid",
|
|
||||||
"frameId": null,
|
|
||||||
"groupIds": [],
|
|
||||||
"height": 50,
|
|
||||||
"id": "id2",
|
|
||||||
"index": "Zz",
|
|
||||||
"isDeleted": false,
|
|
||||||
"link": null,
|
|
||||||
"locked": false,
|
|
||||||
"opacity": 100,
|
|
||||||
"roughness": 1,
|
|
||||||
"roundness": {
|
|
||||||
"type": 3,
|
|
||||||
},
|
|
||||||
"seed": 1278240551,
|
|
||||||
"strokeColor": "#1e1e1e",
|
|
||||||
"strokeStyle": "solid",
|
|
||||||
"strokeWidth": 2,
|
|
||||||
"type": "rectangle",
|
|
||||||
"updated": 1,
|
|
||||||
"version": 6,
|
|
||||||
"versionNonce": 1604849351,
|
|
||||||
"width": 30,
|
|
||||||
"x": 30,
|
|
||||||
"y": 20,
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
|
||||||
{
|
{
|
||||||
"angle": 0,
|
"angle": 0,
|
||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
|
@ -54,13 +20,47 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 1505387817,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 6,
|
"version": 5,
|
||||||
|
"versionNonce": 1505387817,
|
||||||
|
"width": 30,
|
||||||
|
"x": 30,
|
||||||
|
"y": 20,
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||||
|
{
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": null,
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 50,
|
||||||
|
"id": "id2",
|
||||||
|
"index": "a1",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": {
|
||||||
|
"type": 3,
|
||||||
|
},
|
||||||
|
"seed": 1604849351,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "rectangle",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 7,
|
||||||
"versionNonce": 915032327,
|
"versionNonce": 915032327,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
|
|
|
@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 400692809,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
@ -106,7 +106,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"versionNonce": 1604849351,
|
"versionNonce": 400692809,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
|
|
@ -2038,7 +2038,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"searchMatches": [],
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id2": true,
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
|
@ -2128,8 +2128,16 @@ History {
|
||||||
HistoryEntry {
|
HistoryEntry {
|
||||||
"appStateChange": AppStateChange {
|
"appStateChange": AppStateChange {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {},
|
"deleted": {
|
||||||
"inserted": {},
|
"selectedElementIds": {
|
||||||
|
"id2": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"selectedElementIds": {
|
||||||
|
"id0": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"elementsChange": ElementsChange {
|
"elementsChange": ElementsChange {
|
||||||
|
@ -2145,7 +2153,7 @@ History {
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"index": "Zz",
|
"index": "a1",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
@ -2159,26 +2167,15 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 20,
|
||||||
"y": 10,
|
"y": 20,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": Map {
|
"updated": Map {},
|
||||||
"id0" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"x": 20,
|
|
||||||
"y": 20,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"x": 10,
|
|
||||||
"y": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -6835,7 +6832,7 @@ History {
|
||||||
|
|
||||||
exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`;
|
exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`;
|
||||||
|
|
||||||
exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `33`;
|
exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `31`;
|
||||||
|
|
||||||
exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = `
|
exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
|
@ -10378,13 +10375,13 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
"searchMatches": [],
|
"searchMatches": [],
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id6": true,
|
||||||
"id1": true,
|
"id8": true,
|
||||||
"id2": true,
|
"id9": true,
|
||||||
},
|
},
|
||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {
|
"selectedGroupIds": {
|
||||||
"id4": true,
|
"id7": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElement": null,
|
"selectedLinearElement": null,
|
||||||
"selectionElement": null,
|
"selectionElement": null,
|
||||||
|
@ -10648,8 +10645,26 @@ History {
|
||||||
HistoryEntry {
|
HistoryEntry {
|
||||||
"appStateChange": AppStateChange {
|
"appStateChange": AppStateChange {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {},
|
"deleted": {
|
||||||
"inserted": {},
|
"selectedElementIds": {
|
||||||
|
"id6": true,
|
||||||
|
"id8": true,
|
||||||
|
"id9": true,
|
||||||
|
},
|
||||||
|
"selectedGroupIds": {
|
||||||
|
"id7": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"inserted": {
|
||||||
|
"selectedElementIds": {
|
||||||
|
"id0": true,
|
||||||
|
"id1": true,
|
||||||
|
"id2": true,
|
||||||
|
},
|
||||||
|
"selectedGroupIds": {
|
||||||
|
"id4": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"elementsChange": ElementsChange {
|
"elementsChange": ElementsChange {
|
||||||
|
@ -10667,7 +10682,7 @@ History {
|
||||||
"id7",
|
"id7",
|
||||||
],
|
],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"index": "Zx",
|
"index": "a3",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
@ -10681,8 +10696,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 10,
|
"x": 20,
|
||||||
"y": 10,
|
"y": 20,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -10700,7 +10715,7 @@ History {
|
||||||
"id7",
|
"id7",
|
||||||
],
|
],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"index": "Zy",
|
"index": "a4",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
@ -10714,8 +10729,8 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 30,
|
"x": 40,
|
||||||
"y": 10,
|
"y": 20,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
|
@ -10733,7 +10748,7 @@ History {
|
||||||
"id7",
|
"id7",
|
||||||
],
|
],
|
||||||
"height": 10,
|
"height": 10,
|
||||||
"index": "Zz",
|
"index": "a5",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"link": null,
|
"link": null,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
@ -10747,46 +10762,15 @@ History {
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"width": 10,
|
"width": 10,
|
||||||
"x": 50,
|
"x": 60,
|
||||||
"y": 10,
|
"y": 20,
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"isDeleted": true,
|
"isDeleted": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": Map {
|
"updated": Map {},
|
||||||
"id0" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"x": 20,
|
|
||||||
"y": 20,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"x": 10,
|
|
||||||
"y": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id1" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"x": 40,
|
|
||||||
"y": 20,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"x": 30,
|
|
||||||
"y": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id2" => Delta {
|
|
||||||
"deleted": {
|
|
||||||
"x": 60,
|
|
||||||
"y": 20,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"x": 50,
|
|
||||||
"y": 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -14566,7 +14550,7 @@ History {
|
||||||
|
|
||||||
exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`;
|
exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`;
|
||||||
|
|
||||||
exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `20`;
|
exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `19`;
|
||||||
|
|
||||||
exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = `
|
exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = `
|
||||||
{
|
{
|
||||||
|
|
|
@ -307,6 +307,41 @@ describe("pasting & frames", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should remove element from frame when pasted outside", async () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
frameId: frame.id,
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([frame]);
|
||||||
|
|
||||||
|
const clipboardJSON = await serializeAsClipboardJSON({
|
||||||
|
elements: [rect],
|
||||||
|
files: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
mouse.moveTo(150, 150);
|
||||||
|
|
||||||
|
pasteWithCtrlCmdV(clipboardJSON);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(h.elements.length).toBe(2);
|
||||||
|
expect(h.elements[1].type).toBe(rect.type);
|
||||||
|
expect(h.elements[1].frameId).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should filter out elements not overlapping frame", async () => {
|
it("should filter out elements not overlapping frame", async () => {
|
||||||
const frame = API.createElement({
|
const frame = API.createElement({
|
||||||
type: "frame",
|
type: "frame",
|
||||||
|
|
|
@ -218,7 +218,7 @@ describe("Cropping and other features", async () => {
|
||||||
initialHeight / 2,
|
initialHeight / 2,
|
||||||
]);
|
]);
|
||||||
Keyboard.keyDown(KEYS.ESCAPE);
|
Keyboard.keyDown(KEYS.ESCAPE);
|
||||||
const duplicatedImage = duplicateElement(null, new Map(), image, {});
|
const duplicatedImage = duplicateElement(null, new Map(), image);
|
||||||
act(() => {
|
act(() => {
|
||||||
h.app.scene.insertElement(duplicatedImage);
|
h.app.scene.insertElement(duplicatedImage);
|
||||||
});
|
});
|
||||||
|
|
|
@ -313,7 +313,7 @@ describe("Test dragCreate", () => {
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`6`,
|
`6`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
@ -342,7 +342,7 @@ describe("Test dragCreate", () => {
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||||
`6`,
|
`6`,
|
||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
|
|
||||||
import { KEYS } from "@excalidraw/common";
|
import { KEYS } from "@excalidraw/common";
|
||||||
|
|
||||||
import { actionSelectAll } from "../actions";
|
import { actionSelectAll } from "../actions";
|
||||||
|
@ -298,7 +296,7 @@ describe("element locking", () => {
|
||||||
height: textSize,
|
height: textSize,
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [{ id: text.id, type: "text" }],
|
boundElements: [{ id: text.id, type: "text" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -339,7 +337,7 @@ describe("element locking", () => {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
locked: true,
|
locked: true,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [{ id: text.id, type: "text" }],
|
boundElements: [{ id: text.id, type: "text" }],
|
||||||
});
|
});
|
||||||
API.setElements([container, text]);
|
API.setElements([container, text]);
|
||||||
|
@ -373,7 +371,7 @@ describe("element locking", () => {
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
locked: true,
|
locked: true,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
h.app.scene.mutateElement(container, {
|
||||||
boundElements: [{ id: text.id, type: "text" }],
|
boundElements: [{ id: text.id, type: "text" }],
|
||||||
});
|
});
|
||||||
API.setElements([container, text]);
|
API.setElements([container, text]);
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
|
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
|
||||||
|
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
newArrowElement,
|
newArrowElement,
|
||||||
newElement,
|
newElement,
|
||||||
|
@ -100,10 +99,10 @@ export class API {
|
||||||
|
|
||||||
// eslint-disable-next-line prettier/prettier
|
// eslint-disable-next-line prettier/prettier
|
||||||
static updateElement = <T extends ExcalidrawElement>(
|
static updateElement = <T extends ExcalidrawElement>(
|
||||||
...args: Parameters<typeof mutateElement<T>>
|
...args: Parameters<typeof h.app.scene.mutateElement<T>>
|
||||||
) => {
|
) => {
|
||||||
act(() => {
|
act(() => {
|
||||||
mutateElement<T>(...args);
|
h.app.scene.mutateElement(...args);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -419,12 +418,11 @@ export class API {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(
|
h.app.scene.mutateElement(
|
||||||
rectangle,
|
rectangle,
|
||||||
{
|
{
|
||||||
boundElements: [{ type: "text", id: text.id }],
|
boundElements: [{ type: "text", id: text.id }],
|
||||||
},
|
},
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return [rectangle, text];
|
return [rectangle, text];
|
||||||
|
@ -444,7 +442,6 @@ export class API {
|
||||||
|
|
||||||
const text = API.createElement({
|
const text = API.createElement({
|
||||||
type: "text",
|
type: "text",
|
||||||
id: "text2",
|
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 20,
|
height: 20,
|
||||||
containerId: arrow.id,
|
containerId: arrow.id,
|
||||||
|
@ -454,12 +451,11 @@ export class API {
|
||||||
: opts?.label?.frameId ?? null,
|
: opts?.label?.frameId ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
mutateElement(
|
h.app.scene.mutateElement(
|
||||||
arrow,
|
arrow,
|
||||||
{
|
{
|
||||||
boundElements: [{ type: "text", id: text.id }],
|
boundElements: [{ type: "text", id: text.id }],
|
||||||
},
|
},
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return [arrow, text];
|
return [arrow, text];
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {
|
||||||
getElementPointsCoords,
|
getElementPointsCoords,
|
||||||
} from "@excalidraw/element/bounds";
|
} from "@excalidraw/element/bounds";
|
||||||
import { cropElement } from "@excalidraw/element/cropElement";
|
import { cropElement } from "@excalidraw/element/cropElement";
|
||||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
|
||||||
import {
|
import {
|
||||||
getTransformHandles,
|
getTransformHandles,
|
||||||
getTransformHandlesFromCoords,
|
getTransformHandlesFromCoords,
|
||||||
|
@ -180,10 +179,17 @@ export class Pointer {
|
||||||
public clientX = 0;
|
public clientX = 0;
|
||||||
public clientY = 0;
|
public clientY = 0;
|
||||||
|
|
||||||
|
static activePointers: Pointer[] = [];
|
||||||
|
static resetAll() {
|
||||||
|
Pointer.activePointers.forEach((pointer) => pointer.reset());
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly pointerType: "mouse" | "touch" | "pen",
|
private readonly pointerType: "mouse" | "touch" | "pen",
|
||||||
private readonly pointerId = 1,
|
private readonly pointerId = 1,
|
||||||
) {}
|
) {
|
||||||
|
Pointer.activePointers.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.clientX = 0;
|
this.clientX = 0;
|
||||||
|
@ -519,7 +525,7 @@ export class UI {
|
||||||
|
|
||||||
if (angle !== 0) {
|
if (angle !== 0) {
|
||||||
act(() => {
|
act(() => {
|
||||||
mutateElement(origElement, { angle });
|
h.app.scene.mutateElement(origElement, { angle });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ import type {
|
||||||
FontString,
|
FontString,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { Excalidraw, mutateElement } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
||||||
import * as StaticScene from "../renderer/staticScene";
|
import * as StaticScene from "../renderer/staticScene";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
|
@ -118,7 +118,7 @@ describe("Test Linear Elements", () => {
|
||||||
],
|
],
|
||||||
roundness,
|
roundness,
|
||||||
});
|
});
|
||||||
mutateElement(line, { points: line.points });
|
h.app.scene.mutateElement(line, { points: line.points });
|
||||||
API.setElements([line]);
|
API.setElements([line]);
|
||||||
mouse.clickAt(p1[0], p1[1]);
|
mouse.clickAt(p1[0], p1[1]);
|
||||||
return line;
|
return line;
|
||||||
|
@ -177,7 +177,7 @@ describe("Test Linear Elements", () => {
|
||||||
pointFrom<LocalPoint>(0.5, 0),
|
pointFrom<LocalPoint>(0.5, 0),
|
||||||
pointFrom<LocalPoint>(100, 100),
|
pointFrom<LocalPoint>(100, 100),
|
||||||
]);
|
]);
|
||||||
new LinearElementEditor(element);
|
new LinearElementEditor(element, arrayToMap(h.elements));
|
||||||
expect(element.points).toEqual([
|
expect(element.points).toEqual([
|
||||||
pointFrom<LocalPoint>(0, 0),
|
pointFrom<LocalPoint>(0, 0),
|
||||||
pointFrom<LocalPoint>(99.5, 100),
|
pointFrom<LocalPoint>(99.5, 100),
|
||||||
|
@ -1271,7 +1271,7 @@ describe("Test Linear Elements", () => {
|
||||||
expect(rect.y).toBe(0);
|
expect(rect.y).toBe(0);
|
||||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||||
h.elements[0],
|
h.elements[0],
|
||||||
arrayToMap(h.elements),
|
h.app.scene,
|
||||||
"nw",
|
"nw",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
@ -1384,7 +1384,7 @@ describe("Test Linear Elements", () => {
|
||||||
const [origStartX, origStartY] = [line.x, line.y];
|
const [origStartX, origStartY] = [line.x, line.y];
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
LinearElementEditor.movePoints(line, [
|
LinearElementEditor.movePoints(line, h.app.scene, [
|
||||||
{
|
{
|
||||||
index: 0,
|
index: 0,
|
||||||
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
|
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
|
||||||
|
|
|
@ -13,8 +13,6 @@ import type {
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
|
||||||
|
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
||||||
import * as StaticScene from "../renderer/staticScene";
|
import * as StaticScene from "../renderer/staticScene";
|
||||||
|
@ -85,15 +83,13 @@ describe("move element", () => {
|
||||||
const rectA = UI.createElement("rectangle", { size: 100 });
|
const rectA = UI.createElement("rectangle", { size: 100 });
|
||||||
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
|
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
|
||||||
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
|
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
|
||||||
const elementsMap = h.app.scene.getNonDeletedElementsMap();
|
|
||||||
act(() => {
|
act(() => {
|
||||||
// bind line to two rectangles
|
// bind line to two rectangles
|
||||||
bindOrUnbindLinearElement(
|
bindOrUnbindLinearElement(
|
||||||
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
|
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
|
||||||
rectA.get() as ExcalidrawRectangleElement,
|
rectA.get() as ExcalidrawRectangleElement,
|
||||||
rectB.get() as ExcalidrawRectangleElement,
|
rectB.get() as ExcalidrawRectangleElement,
|
||||||
elementsMap,
|
h.app.scene,
|
||||||
{} as Scene,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -170,8 +166,6 @@ describe("duplicate element on move when ALT is clicked", () => {
|
||||||
fireEvent.pointerMove(canvas, { clientX: 10, clientY: 60 });
|
fireEvent.pointerMove(canvas, { clientX: 10, clientY: 60 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
// TODO: This used to be 4, but binding made it go up to 5. Do we need
|
|
||||||
// that additional render?
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`4`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`4`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`3`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`3`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
|
@ -119,7 +119,7 @@ describe("multi point mode in linear elements", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
@ -162,7 +162,7 @@ describe("multi point mode in linear elements", () => {
|
||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import type { AllPossibleKeys } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
|
import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
|
||||||
|
|
||||||
import { UI } from "./helpers/ui";
|
import { Pointer, UI } from "./helpers/ui";
|
||||||
import * as toolQueries from "./queries/toolQueries";
|
import * as toolQueries from "./queries/toolQueries";
|
||||||
|
|
||||||
import type { RenderResult, RenderOptions } from "@testing-library/react";
|
import type { RenderResult, RenderOptions } from "@testing-library/react";
|
||||||
|
@ -42,6 +42,10 @@ type TestRenderFn = (
|
||||||
) => Promise<RenderResult<typeof customQueries>>;
|
) => Promise<RenderResult<typeof customQueries>>;
|
||||||
|
|
||||||
const renderApp: TestRenderFn = async (ui, options) => {
|
const renderApp: TestRenderFn = async (ui, options) => {
|
||||||
|
// when tests reuse Pointer instances let's reset the last
|
||||||
|
// pointer poisitions so there's no leak between tests
|
||||||
|
Pointer.resetAll();
|
||||||
|
|
||||||
if (options?.localStorageData) {
|
if (options?.localStorageData) {
|
||||||
initLocalStorage(options.localStorageData);
|
initLocalStorage(options.localStorageData);
|
||||||
delete options.localStorageData;
|
delete options.localStorageData;
|
||||||
|
|
|
@ -601,6 +601,7 @@ export interface ExcalidrawProps {
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
aiEnabled?: boolean;
|
aiEnabled?: boolean;
|
||||||
showDeprecatedFonts?: boolean;
|
showDeprecatedFonts?: boolean;
|
||||||
|
renderScrollbars?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SceneData = {
|
export type SceneData = {
|
||||||
|
@ -724,7 +725,8 @@ export type PointerDownState = Readonly<{
|
||||||
scrollbars: ReturnType<typeof isOverScrollBars>;
|
scrollbars: ReturnType<typeof isOverScrollBars>;
|
||||||
// The previous pointer position
|
// The previous pointer position
|
||||||
lastCoords: { x: number; y: number };
|
lastCoords: { x: number; y: number };
|
||||||
// map of original elements data
|
// original element frozen snapshots so we can access the original
|
||||||
|
// element attribute values at time of pointerdown
|
||||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
originalElements: Map<string, NonDeleted<ExcalidrawElement>>;
|
||||||
resize: {
|
resize: {
|
||||||
// Handle when resizing, might change during the pointer interaction
|
// Handle when resizing, might change during the pointer interaction
|
||||||
|
@ -758,6 +760,9 @@ export type PointerDownState = Readonly<{
|
||||||
hasOccurred: boolean;
|
hasOccurred: boolean;
|
||||||
// Might change during the pointer interaction
|
// Might change during the pointer interaction
|
||||||
offset: { x: number; y: number } | null;
|
offset: { x: number; y: number } | null;
|
||||||
|
// by default same as PointerDownState.origin. On alt-duplication, reset
|
||||||
|
// to current pointer position at time of duplication.
|
||||||
|
origin: { x: number; y: number };
|
||||||
};
|
};
|
||||||
// We need to have these in the state so that we can unsubscribe them
|
// We need to have these in the state so that we can unsubscribe them
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
|
@ -779,6 +784,7 @@ export type UnsubscribeCallback = () => void;
|
||||||
|
|
||||||
export interface ExcalidrawImperativeAPI {
|
export interface ExcalidrawImperativeAPI {
|
||||||
updateScene: InstanceType<typeof App>["updateScene"];
|
updateScene: InstanceType<typeof App>["updateScene"];
|
||||||
|
mutateElement: InstanceType<typeof App>["mutateElement"];
|
||||||
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
||||||
resetScene: InstanceType<typeof App>["resetScene"];
|
resetScene: InstanceType<typeof App>["resetScene"];
|
||||||
getSceneElementsIncludingDeleted: InstanceType<
|
getSceneElementsIncludingDeleted: InstanceType<
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from "@excalidraw/element/containerCache";
|
} from "@excalidraw/element/containerCache";
|
||||||
|
|
||||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||||
import { bumpVersion, mutateElement } from "@excalidraw/element/mutateElement";
|
import { bumpVersion } from "@excalidraw/element/mutateElement";
|
||||||
import {
|
import {
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
|
@ -45,7 +45,6 @@ import type {
|
||||||
|
|
||||||
import { actionSaveToActiveFile } from "../actions";
|
import { actionSaveToActiveFile } from "../actions";
|
||||||
|
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
import {
|
import {
|
||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
|
@ -130,8 +129,7 @@ export const textWysiwyg = ({
|
||||||
|
|
||||||
const updateWysiwygStyle = () => {
|
const updateWysiwygStyle = () => {
|
||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
const updatedTextElement =
|
const updatedTextElement = app.scene.getElement<ExcalidrawTextElement>(id);
|
||||||
Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
|
|
||||||
|
|
||||||
if (!updatedTextElement) {
|
if (!updatedTextElement) {
|
||||||
return;
|
return;
|
||||||
|
@ -201,7 +199,7 @@ export const textWysiwyg = ({
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
|
|
||||||
mutateElement(container, { height: targetContainerHeight });
|
app.scene.mutateElement(container, { height: targetContainerHeight });
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
// autoshrink container height until original container height
|
// autoshrink container height until original container height
|
||||||
|
@ -214,7 +212,7 @@ export const textWysiwyg = ({
|
||||||
height,
|
height,
|
||||||
container.type,
|
container.type,
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: targetContainerHeight });
|
app.scene.mutateElement(container, { height: targetContainerHeight });
|
||||||
} else {
|
} else {
|
||||||
const { y } = computeBoundTextPosition(
|
const { y } = computeBoundTextPosition(
|
||||||
container,
|
container,
|
||||||
|
@ -287,7 +285,7 @@ export const textWysiwyg = ({
|
||||||
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
|
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
mutateElement(updatedTextElement, { x: coordX, y: coordY });
|
app.scene.mutateElement(updatedTextElement, { x: coordX, y: coordY });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -544,7 +542,7 @@ export const textWysiwyg = ({
|
||||||
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
||||||
// wysiwyg on update
|
// wysiwyg on update
|
||||||
cleanup();
|
cleanup();
|
||||||
const updateElement = Scene.getScene(element)?.getElement(
|
const updateElement = app.scene.getElement(
|
||||||
element.id,
|
element.id,
|
||||||
) as ExcalidrawTextElement;
|
) as ExcalidrawTextElement;
|
||||||
if (!updateElement) {
|
if (!updateElement) {
|
||||||
|
@ -559,7 +557,7 @@ export const textWysiwyg = ({
|
||||||
if (editable.value.trim()) {
|
if (editable.value.trim()) {
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId || boundTextElementId !== element.id) {
|
if (!boundTextElementId || boundTextElementId !== element.id) {
|
||||||
mutateElement(container, {
|
app.scene.mutateElement(container, {
|
||||||
boundElements: (container.boundElements || []).concat({
|
boundElements: (container.boundElements || []).concat({
|
||||||
type: "text",
|
type: "text",
|
||||||
id: element.id,
|
id: element.id,
|
||||||
|
@ -570,7 +568,7 @@ export const textWysiwyg = ({
|
||||||
bumpVersion(container);
|
bumpVersion(container);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mutateElement(container, {
|
app.scene.mutateElement(container, {
|
||||||
boundElements: container.boundElements?.filter(
|
boundElements: container.boundElements?.filter(
|
||||||
(ele) =>
|
(ele) =>
|
||||||
!isTextElement(
|
!isTextElement(
|
||||||
|
@ -579,11 +577,8 @@ export const textWysiwyg = ({
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
redrawTextBoundingBox(
|
|
||||||
updateElement,
|
redrawTextBoundingBox(updateElement, container, app.scene);
|
||||||
container,
|
|
||||||
app.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
|
|
|
@ -94,11 +94,6 @@ vi.mock(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mock("nanoid", () => {
|
|
||||||
return {
|
|
||||||
nanoid: vi.fn(() => "test-id"),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
// ReactDOM is located inside index.tsx file
|
// ReactDOM is located inside index.tsx file
|
||||||
// as a result, we need a place for it to render into
|
// as a result, we need a place for it to render into
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue