Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4eb1bd8036 | |||
| e19fd1332a | |||
| 6e655cdb24 | |||
| 8d7ffa21d1 | |||
| 192c4e7658 | |||
| 195a743874 | |||
| 82cef23c3d | |||
| 4a60fe3d22 | |||
| 541725ff5a | |||
| 28066034d7 | |||
| 7d0d6aec7a | |||
| e6ade3b627 | |||
| 9a2bd18904 | |||
| c7c6a4c3f1 | |||
| 9c27f936de | |||
| b8fdd7ef23 | |||
| ece841326b | |||
| 2a0d15799c | |||
| a18b139a60 | |||
| 1913599594 | |||
| 41711af210 | |||
| 230e47fd52 | |||
| 52445aeb68 | |||
| bc9f34e71e | |||
| 22aade07b3 | |||
| c2de1304b7 | |||
| 25fb43f5b7 | |||
| 6dfa5de66c | |||
| 7abbb2afa3 | |||
| aa91a3d610 | |||
| 25d6e517c9 | |||
| d5e33730ab | |||
| c06b78c1b2 | |||
| eaa869620e | |||
| a8338cdb5a | |||
| 1ee3676784 | |||
| f12f7e4b50 |
@@ -32,6 +32,12 @@
|
||||
"name": "jotai",
|
||||
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
||||
}
|
||||
],
|
||||
"react/jsx-no-target-blank": [
|
||||
"error",
|
||||
{
|
||||
"allowReferrer": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export const AIComponents = ({
|
||||
</br>
|
||||
<div>You can also try <a href="${
|
||||
import.meta.env.VITE_APP_PLUS_LP
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noopener">Excalidraw+</a> to get more requests.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "@excalidraw/math";
|
||||
import { isCurve } from "@excalidraw/math/curve";
|
||||
|
||||
import type { DebugElement } from "@excalidraw/excalidraw/visualdebug";
|
||||
import type { DebugElement } from "@excalidraw/utils/visualdebug";
|
||||
|
||||
import type { Curve } from "@excalidraw/math";
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
|
||||
className="encrypted-icon tooltip"
|
||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
aria-label={t("encrypted.link")}
|
||||
>
|
||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||
|
||||
@@ -10,7 +10,7 @@ export const ExcalidrawPlusAppLink = () => {
|
||||
import.meta.env.VITE_APP_PLUS_APP
|
||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
className="plus-button"
|
||||
>
|
||||
Go to Excalidraw+
|
||||
|
||||
@@ -198,7 +198,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
||||
<a
|
||||
class="welcome-screen-menu-item "
|
||||
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -119,6 +119,7 @@ export const CLASSES = {
|
||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||
ZOOM_ACTIONS: "zoom-actions",
|
||||
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||
};
|
||||
|
||||
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
||||
|
||||
@@ -680,7 +680,7 @@ export const arrayToMap = <T extends { id: string } | string>(
|
||||
return items.reduce((acc: Map<string, T>, element) => {
|
||||
acc.set(typeof element === "string" ? element : element.id, element);
|
||||
return acc;
|
||||
}, new Map());
|
||||
}, new Map() as Map<string, T>);
|
||||
};
|
||||
|
||||
export const arrayToMapWithIndex = <T extends { id: string }>(
|
||||
|
||||
@@ -13,6 +13,7 @@ import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
|
||||
import { getElementsInGroup } from "@excalidraw/element/groups";
|
||||
|
||||
import {
|
||||
orderByFractionalIndex,
|
||||
syncInvalidIndices,
|
||||
syncMovedIndices,
|
||||
validateFractionalIndices,
|
||||
@@ -20,7 +21,11 @@ import {
|
||||
|
||||
import { getSelectedElements } from "@excalidraw/element/selection";
|
||||
|
||||
import type { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import {
|
||||
mutateElement,
|
||||
type ElementUpdate,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
@@ -33,12 +38,13 @@ import type {
|
||||
Ordered,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { Assert, SameType } from "@excalidraw/common/utility-types";
|
||||
import type {
|
||||
Assert,
|
||||
Mutable,
|
||||
SameType,
|
||||
} from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
import type { AppState } from "../../excalidraw/types";
|
||||
|
||||
type SceneStateCallback = () => void;
|
||||
type SceneStateCallbackRemover = () => void;
|
||||
@@ -103,44 +109,7 @@ const hashSelectionOpts = (
|
||||
// in our codebase
|
||||
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
|
||||
|
||||
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
||||
if (typeof elementKey === "string") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
class Scene {
|
||||
// ---------------------------------------------------------------------------
|
||||
// static methods/props
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private static sceneMapByElement = new WeakMap<ExcalidrawElement, Scene>();
|
||||
private static sceneMapById = new Map<string, Scene>();
|
||||
|
||||
static mapElementToScene(elementKey: ElementKey, scene: Scene) {
|
||||
if (isIdKey(elementKey)) {
|
||||
// for cases where we don't have access to the element object
|
||||
// (e.g. restore serialized appState with id references)
|
||||
this.sceneMapById.set(elementKey, scene);
|
||||
} else {
|
||||
this.sceneMapByElement.set(elementKey, scene);
|
||||
// if mapping element objects, also cache the id string when later
|
||||
// looking up by id alone
|
||||
this.sceneMapById.set(elementKey.id, scene);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated pass down `app.scene` and use it directly
|
||||
*/
|
||||
static getScene(elementKey: ElementKey): Scene | null {
|
||||
if (isIdKey(elementKey)) {
|
||||
return this.sceneMapById.get(elementKey) || null;
|
||||
}
|
||||
return this.sceneMapByElement.get(elementKey) || null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// instance methods/props
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -199,6 +168,12 @@ class Scene {
|
||||
return this.frames;
|
||||
}
|
||||
|
||||
constructor(elements: ElementsMapOrArray | null = null) {
|
||||
if (elements) {
|
||||
this.replaceAllElements(elements);
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedElements(opts: {
|
||||
// NOTE can be ommitted by making Scene constructor require App instance
|
||||
selectedElementIds: AppState["selectedElementIds"];
|
||||
@@ -293,21 +268,25 @@ class Scene {
|
||||
}
|
||||
|
||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
||||
const _nextElements = isReadonlyArray(nextElements)
|
||||
? nextElements
|
||||
: Array.from(nextElements.values());
|
||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||
if (!isReadonlyArray(nextElements)) {
|
||||
// need to order by fractional indices to get the correct order
|
||||
nextElements = orderByFractionalIndex(
|
||||
Array.from(nextElements.values()) as OrderedExcalidrawElement[],
|
||||
);
|
||||
}
|
||||
|
||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||
|
||||
validateIndicesThrottled(_nextElements);
|
||||
validateIndicesThrottled(nextElements);
|
||||
|
||||
this.elements = syncInvalidIndices(_nextElements);
|
||||
this.elements = syncInvalidIndices(nextElements);
|
||||
this.elementsMap.clear();
|
||||
this.elements.forEach((element) => {
|
||||
if (isFrameLikeElement(element)) {
|
||||
nextFrameLikes.push(element);
|
||||
}
|
||||
this.elementsMap.set(element.id, element);
|
||||
Scene.mapElementToScene(element, this);
|
||||
});
|
||||
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||
this.nonDeletedElements = nonDeletedElements.elements;
|
||||
@@ -352,12 +331,6 @@ class Scene {
|
||||
this.selectedElementsCache.elements = null;
|
||||
this.selectedElementsCache.cache.clear();
|
||||
|
||||
Scene.sceneMapById.forEach((scene, elementKey) => {
|
||||
if (scene === this) {
|
||||
Scene.sceneMapById.delete(elementKey);
|
||||
}
|
||||
});
|
||||
|
||||
// done not for memory leaks, but to guard against possible late fires
|
||||
// (I guess?)
|
||||
this.callbacks.clear();
|
||||
@@ -454,6 +427,42 @@ class Scene {
|
||||
// then, check if the id is a group
|
||||
return getElementsInGroup(elementsMap, id);
|
||||
};
|
||||
|
||||
// Mutate an element with passed updates and trigger the component to update. Make sure you
|
||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||
mutateElement<TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
options: {
|
||||
informMutation: boolean;
|
||||
isDragging: boolean;
|
||||
} = {
|
||||
informMutation: true,
|
||||
isDragging: false,
|
||||
},
|
||||
) {
|
||||
const elementsMap = this.getNonDeletedElementsMap();
|
||||
|
||||
const { version: prevVersion } = element;
|
||||
const { version: nextVersion } = mutateElement(
|
||||
element,
|
||||
elementsMap,
|
||||
updates,
|
||||
options,
|
||||
);
|
||||
|
||||
if (
|
||||
// skip if the element is not in the scene (i.e. selection)
|
||||
this.elementsMap.has(element.id) &&
|
||||
// skip if the element's version hasn't changed, as mutateElement returned the same element
|
||||
prevVersion !== nextVersion &&
|
||||
options.informMutation
|
||||
) {
|
||||
this.triggerUpdate();
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
export default Scene;
|
||||
@@ -1,12 +1,11 @@
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import { updateBoundElements } from "./binding";
|
||||
import { getCommonBoundingBox } from "./bounds";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getMaximumGroups } from "./groups";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { BoundingBox } from "./bounds";
|
||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
export interface Alignment {
|
||||
position: "start" | "center" | "end";
|
||||
@@ -15,10 +14,10 @@ export interface Alignment {
|
||||
|
||||
export const alignElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
alignment: Alignment,
|
||||
scene: Scene,
|
||||
): ExcalidrawElement[] => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
@@ -33,12 +32,13 @@ export const alignElements = (
|
||||
);
|
||||
return group.map((element) => {
|
||||
// update element
|
||||
const updatedEle = mutateElement(element, {
|
||||
const updatedEle = scene.mutateElement(element, {
|
||||
x: element.x + translation.x,
|
||||
y: element.y + translation.y,
|
||||
});
|
||||
|
||||
// update bound elements
|
||||
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: group,
|
||||
});
|
||||
return updatedEle;
|
||||
|
||||
+120
-93
@@ -31,8 +31,6 @@ import { isPointOnShape } from "@excalidraw/utils/collision";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
@@ -45,6 +43,12 @@ import {
|
||||
import { intersectElementWithLineSegment } from "./collision";
|
||||
import { distanceToBindableElement } from "./distance";
|
||||
import {
|
||||
compareHeading,
|
||||
HEADING_DOWN,
|
||||
HEADING_LEFT,
|
||||
HEADING_RIGHT,
|
||||
HEADING_UP,
|
||||
headingForPoint,
|
||||
headingForPointFromElement,
|
||||
headingIsHorizontal,
|
||||
vectorToHeading,
|
||||
@@ -68,6 +72,8 @@ import {
|
||||
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { ElementUpdate } from "./mutateElement";
|
||||
import type {
|
||||
@@ -81,10 +87,8 @@ import type {
|
||||
NonDeletedSceneElementsMap,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawArrowElement,
|
||||
OrderedExcalidrawElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
FixedPoint,
|
||||
SceneElementsMap,
|
||||
FixedPointBinding,
|
||||
} from "./types";
|
||||
|
||||
@@ -130,7 +134,6 @@ export const bindOrUnbindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||
endBindingElement: ExcalidrawBindableElement | null | "keep",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): void => {
|
||||
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
|
||||
@@ -142,7 +145,7 @@ export const bindOrUnbindLinearElement = (
|
||||
"start",
|
||||
boundToElementIds,
|
||||
unboundFromElementIds,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
bindOrUnbindLinearElementEdge(
|
||||
linearElement,
|
||||
@@ -151,7 +154,7 @@ export const bindOrUnbindLinearElement = (
|
||||
"end",
|
||||
boundToElementIds,
|
||||
unboundFromElementIds,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
||||
@@ -159,7 +162,7 @@ export const bindOrUnbindLinearElement = (
|
||||
);
|
||||
|
||||
getNonDeletedElements(scene, onlyUnbound).forEach((element) => {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
boundElements: element.boundElements?.filter(
|
||||
(element) =>
|
||||
element.type !== "arrow" || element.id !== linearElement.id,
|
||||
@@ -177,7 +180,7 @@ const bindOrUnbindLinearElementEdge = (
|
||||
boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||
// Is mutated
|
||||
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): void => {
|
||||
// "keep" is for method chaining convenience, a "no-op", so just bail out
|
||||
if (bindableElement === "keep") {
|
||||
@@ -186,7 +189,7 @@ const bindOrUnbindLinearElementEdge = (
|
||||
|
||||
// null means break the bind, so nothing to consider here
|
||||
if (bindableElement === null) {
|
||||
const unbound = unbindLinearElement(linearElement, startOrEnd);
|
||||
const unbound = unbindLinearElement(linearElement, startOrEnd, scene);
|
||||
if (unbound != null) {
|
||||
unboundFromElementIds.add(unbound);
|
||||
}
|
||||
@@ -209,16 +212,11 @@ const bindOrUnbindLinearElementEdge = (
|
||||
: startOrEnd === "start" ||
|
||||
otherEdgeBindableElement.id !== bindableElement.id)
|
||||
) {
|
||||
bindLinearElement(
|
||||
linearElement,
|
||||
bindableElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
);
|
||||
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||
boundToElementIds.add(bindableElement.id);
|
||||
}
|
||||
} else {
|
||||
bindLinearElement(linearElement, bindableElement, startOrEnd, elementsMap);
|
||||
bindLinearElement(linearElement, bindableElement, startOrEnd, scene);
|
||||
boundToElementIds.add(bindableElement.id);
|
||||
}
|
||||
};
|
||||
@@ -283,15 +281,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
||||
zoom,
|
||||
)
|
||||
: null // If binding is disabled and start is dragged, break all binds
|
||||
: !isElbowArrow(selectedElement)
|
||||
? // We have to update the focus and gap of the binding, so let's rebind
|
||||
getElligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
elements,
|
||||
zoom,
|
||||
)
|
||||
: "keep";
|
||||
const end = endDragged
|
||||
? isBindingEnabled
|
||||
@@ -303,15 +292,6 @@ const getBindingStrategyForDraggingArrowEndpoints = (
|
||||
zoom,
|
||||
)
|
||||
: null // If binding is disabled and end is dragged, break all binds
|
||||
: !isElbowArrow(selectedElement)
|
||||
? // We have to update the focus and gap of the binding, so let's rebind
|
||||
getElligibleElementForBindingElement(
|
||||
selectedElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
elements,
|
||||
zoom,
|
||||
)
|
||||
: "keep";
|
||||
|
||||
return [start, end];
|
||||
@@ -362,11 +342,9 @@ const getBindingStrategyForDraggingArrowOrJoints = (
|
||||
|
||||
export const bindOrUnbindLinearElements = (
|
||||
selectedElements: NonDeleted<ExcalidrawLinearElement>[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
isBindingEnabled: boolean,
|
||||
draggingPoints: readonly number[] | null,
|
||||
scene: Scene,
|
||||
zoom?: AppState["zoom"],
|
||||
): void => {
|
||||
selectedElements.forEach((selectedElement) => {
|
||||
@@ -376,20 +354,20 @@ export const bindOrUnbindLinearElements = (
|
||||
selectedElement,
|
||||
isBindingEnabled,
|
||||
draggingPoints ?? [],
|
||||
elementsMap,
|
||||
elements,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene.getNonDeletedElements(),
|
||||
zoom,
|
||||
)
|
||||
: // The arrow itself (the shaft) or the inner joins are dragged
|
||||
getBindingStrategyForDraggingArrowOrJoints(
|
||||
selectedElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene.getNonDeletedElements(),
|
||||
isBindingEnabled,
|
||||
zoom,
|
||||
);
|
||||
|
||||
bindOrUnbindLinearElement(selectedElement, start, end, elementsMap, scene);
|
||||
bindOrUnbindLinearElement(selectedElement, start, end, scene);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -429,15 +407,17 @@ export const maybeBindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
pointerCoords: { x: number; y: number },
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
): void => {
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
if (appState.startBoundElement != null) {
|
||||
bindLinearElement(
|
||||
linearElement,
|
||||
appState.startBoundElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -458,7 +438,7 @@ export const maybeBindLinearElement = (
|
||||
"end",
|
||||
)
|
||||
) {
|
||||
bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
|
||||
bindLinearElement(linearElement, hoveredElement, "end", scene);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -487,7 +467,7 @@ export const bindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): void => {
|
||||
if (!isArrowElement(linearElement)) {
|
||||
return;
|
||||
@@ -500,7 +480,7 @@ export const bindLinearElement = (
|
||||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
hoveredElement,
|
||||
),
|
||||
@@ -513,18 +493,17 @@ export const bindLinearElement = (
|
||||
linearElement,
|
||||
hoveredElement,
|
||||
startOrEnd,
|
||||
elementsMap,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
mutateElement(linearElement, {
|
||||
scene.mutateElement(linearElement, {
|
||||
[startOrEnd === "start" ? "startBinding" : "endBinding"]: binding,
|
||||
});
|
||||
|
||||
const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
|
||||
if (!boundElementsMap.has(linearElement.id)) {
|
||||
mutateElement(hoveredElement, {
|
||||
scene.mutateElement(hoveredElement, {
|
||||
boundElements: (hoveredElement.boundElements || []).concat({
|
||||
id: linearElement.id,
|
||||
type: "arrow",
|
||||
@@ -566,13 +545,14 @@ const isLinearElementSimple = (
|
||||
const unbindLinearElement = (
|
||||
linearElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
startOrEnd: "start" | "end",
|
||||
scene: Scene,
|
||||
): ExcalidrawBindableElement["id"] | null => {
|
||||
const field = startOrEnd === "start" ? "startBinding" : "endBinding";
|
||||
const binding = linearElement[field];
|
||||
if (binding == null) {
|
||||
return null;
|
||||
}
|
||||
mutateElement(linearElement, { [field]: null });
|
||||
scene.mutateElement(linearElement, { [field]: null });
|
||||
return binding.elementId;
|
||||
};
|
||||
|
||||
@@ -735,25 +715,30 @@ const calculateFocusAndGap = (
|
||||
|
||||
// Supports translating, rotating and scaling `changedElement` with bound
|
||||
// linear elements.
|
||||
// Because scaling involves moving the focus points as well, it is
|
||||
// done before the `changedElement` is updated, and the `newSize` is passed
|
||||
// in explicitly.
|
||||
export const updateBoundElements = (
|
||||
changedElement: NonDeletedExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||
changedElements?: Map<string, ExcalidrawElement>;
|
||||
},
|
||||
) => {
|
||||
if (!isBindableElement(changedElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
|
||||
if (!isBindableElement(changedElement)) {
|
||||
return;
|
||||
let elementsMap: ElementsMap = scene.getNonDeletedElementsMap();
|
||||
if (options?.changedElements) {
|
||||
elementsMap = new Map(elementsMap) as typeof elementsMap;
|
||||
options.changedElements.forEach((element) => {
|
||||
elementsMap.set(element.id, element);
|
||||
});
|
||||
}
|
||||
|
||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||
@@ -796,7 +781,7 @@ export const updateBoundElements = (
|
||||
|
||||
// `linearElement` is being moved/scaled already, just update the binding
|
||||
if (simultaneouslyUpdatedElementIds.has(element.id)) {
|
||||
mutateElement(element, bindings, true);
|
||||
scene.mutateElement(element, bindings);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -843,27 +828,41 @@ export const updateBoundElements = (
|
||||
}> => update !== null,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
updates,
|
||||
{
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
: {}),
|
||||
...(changedElement.id === element.endBinding?.elementId
|
||||
? { endBinding: bindings.endBinding }
|
||||
: {}),
|
||||
},
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
LinearElementEditor.movePoints(element, scene, updates, {
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
: {}),
|
||||
...(changedElement.id === element.endBinding?.elementId
|
||||
? { endBinding: bindings.endBinding }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
if (boundText && !boundText.isDeleted) {
|
||||
handleBindTextResize(element, elementsMap, false);
|
||||
handleBindTextResize(element, scene, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const updateBindings = (
|
||||
latestElement: ExcalidrawElement,
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
zoom?: AppState["zoom"];
|
||||
},
|
||||
) => {
|
||||
if (isLinearElement(latestElement)) {
|
||||
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
||||
} else {
|
||||
updateBoundElements(latestElement, scene, {
|
||||
...options,
|
||||
changedElements: new Map([[latestElement.id, latestElement]]),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const doesNeedUpdate = (
|
||||
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
||||
changedElement: ExcalidrawBindableElement,
|
||||
@@ -885,7 +884,6 @@ export const getHeadingForElbowArrowSnap = (
|
||||
otherPoint: Readonly<GlobalPoint>,
|
||||
bindableElement: ExcalidrawBindableElement | undefined | null,
|
||||
aabb: Bounds | undefined | null,
|
||||
elementsMap: ElementsMap,
|
||||
origPoint: GlobalPoint,
|
||||
zoom?: AppState["zoom"],
|
||||
): Heading => {
|
||||
@@ -895,12 +893,7 @@ export const getHeadingForElbowArrowSnap = (
|
||||
return otherPointHeading;
|
||||
}
|
||||
|
||||
const distance = getDistanceForBinding(
|
||||
origPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
zoom,
|
||||
);
|
||||
const distance = getDistanceForBinding(origPoint, bindableElement, zoom);
|
||||
|
||||
if (!distance) {
|
||||
return vectorToHeading(
|
||||
@@ -914,7 +907,6 @@ export const getHeadingForElbowArrowSnap = (
|
||||
const getDistanceForBinding = (
|
||||
point: Readonly<GlobalPoint>,
|
||||
bindableElement: ExcalidrawBindableElement,
|
||||
elementsMap: ElementsMap,
|
||||
zoom?: AppState["zoom"],
|
||||
) => {
|
||||
const distance = distanceToBindableElement(bindableElement, point);
|
||||
@@ -1039,7 +1031,14 @@ export const avoidRectangularCorner = (
|
||||
|
||||
if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) {
|
||||
// Top left
|
||||
if (nonRotatedPoint[1] - element.y > -FIXED_BINDING_DISTANCE) {
|
||||
const heading = headingForPoint(
|
||||
nonRotatedPoint,
|
||||
pointFrom(element.x, element.y),
|
||||
);
|
||||
if (
|
||||
compareHeading(heading, HEADING_DOWN) ||
|
||||
compareHeading(heading, HEADING_LEFT)
|
||||
) {
|
||||
return pointRotateRads<GlobalPoint>(
|
||||
pointFrom(element.x - FIXED_BINDING_DISTANCE, element.y),
|
||||
center,
|
||||
@@ -1056,7 +1055,14 @@ export const avoidRectangularCorner = (
|
||||
nonRotatedPoint[1] > element.y + element.height
|
||||
) {
|
||||
// Bottom left
|
||||
if (nonRotatedPoint[0] - element.x > -FIXED_BINDING_DISTANCE) {
|
||||
const heading = headingForPoint(
|
||||
nonRotatedPoint,
|
||||
pointFrom(element.x, element.y + element.height),
|
||||
);
|
||||
if (
|
||||
compareHeading(heading, HEADING_DOWN) ||
|
||||
compareHeading(heading, HEADING_RIGHT)
|
||||
) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
element.x,
|
||||
@@ -1076,9 +1082,13 @@ export const avoidRectangularCorner = (
|
||||
nonRotatedPoint[1] > element.y + element.height
|
||||
) {
|
||||
// Bottom right
|
||||
const heading = headingForPoint(
|
||||
nonRotatedPoint,
|
||||
pointFrom(element.x + element.width, element.y + element.height),
|
||||
);
|
||||
if (
|
||||
nonRotatedPoint[0] - element.x <
|
||||
element.width + FIXED_BINDING_DISTANCE
|
||||
compareHeading(heading, HEADING_DOWN) ||
|
||||
compareHeading(heading, HEADING_LEFT)
|
||||
) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
@@ -1102,9 +1112,13 @@ export const avoidRectangularCorner = (
|
||||
nonRotatedPoint[1] < element.y
|
||||
) {
|
||||
// Top right
|
||||
const heading = headingForPoint(
|
||||
nonRotatedPoint,
|
||||
pointFrom(element.x + element.width, element.y),
|
||||
);
|
||||
if (
|
||||
nonRotatedPoint[0] - element.x <
|
||||
element.width + FIXED_BINDING_DISTANCE
|
||||
compareHeading(heading, HEADING_UP) ||
|
||||
compareHeading(heading, HEADING_LEFT)
|
||||
) {
|
||||
return pointRotateRads(
|
||||
pointFrom(
|
||||
@@ -1122,6 +1136,17 @@ export const avoidRectangularCorner = (
|
||||
);
|
||||
}
|
||||
|
||||
// Break up explicit border bindings to have better elbow arrow routing
|
||||
if (p[0] === element.x) {
|
||||
return pointFrom(p[0] - FIXED_BINDING_DISTANCE, p[1]);
|
||||
} else if (p[0] === element.x + element.width) {
|
||||
return pointFrom(p[0] + FIXED_BINDING_DISTANCE, p[1]);
|
||||
} else if (p[1] === element.y) {
|
||||
return pointFrom(p[0], p[1] - FIXED_BINDING_DISTANCE);
|
||||
} else if (p[1] === element.y + element.height) {
|
||||
return pointFrom(p[0], p[1] + FIXED_BINDING_DISTANCE);
|
||||
}
|
||||
|
||||
return p;
|
||||
};
|
||||
|
||||
@@ -1216,7 +1241,6 @@ const updateBoundPoint = (
|
||||
linearElement,
|
||||
bindableElement,
|
||||
startOrEnd === "startBinding" ? "start" : "end",
|
||||
elementsMap,
|
||||
).fixedPoint;
|
||||
const globalMidPoint = elementCenterPoint(bindableElement);
|
||||
const global = pointFrom<GlobalPoint>(
|
||||
@@ -1320,7 +1344,6 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||
linearElement: NonDeleted<ExcalidrawElbowArrowElement>,
|
||||
hoveredElement: ExcalidrawBindableElement,
|
||||
startOrEnd: "start" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
): { fixedPoint: FixedPoint } => {
|
||||
const bounds = [
|
||||
hoveredElement.x,
|
||||
@@ -1486,8 +1509,12 @@ export const fixBindingsAfterDeletion = (
|
||||
const elements = arrayToMap(sceneElements);
|
||||
|
||||
for (const element of deletedElements) {
|
||||
BoundElement.unbindAffected(elements, element, mutateElement);
|
||||
BindableElement.unbindAffected(elements, element, mutateElement);
|
||||
BoundElement.unbindAffected(elements, element, (element, updates) =>
|
||||
mutateElement(element, elements, updates),
|
||||
);
|
||||
BindableElement.unbindAffected(elements, element, (element, updates) =>
|
||||
mutateElement(element, elements, updates),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import rough from "roughjs/bin/rough";
|
||||
|
||||
import {
|
||||
rescalePoints,
|
||||
arrayToMap,
|
||||
invariant,
|
||||
rescalePoints,
|
||||
sizeOf,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
degreesToRadians,
|
||||
lineSegment,
|
||||
pointFrom,
|
||||
pointDistance,
|
||||
pointFrom,
|
||||
pointFromArray,
|
||||
pointRotateRads,
|
||||
} from "@excalidraw/math";
|
||||
@@ -33,8 +33,8 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
import { generateRoughOptions } from "./Shape";
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import {
|
||||
@@ -52,20 +52,20 @@ import {
|
||||
deconstructRectanguloidElement,
|
||||
} from "./utils";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
Arrowhead,
|
||||
ExcalidrawFreeDrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
ExcalidrawRectanguloidElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ElementsMapOrArray,
|
||||
} from "./types";
|
||||
import type { Drawable, Op } from "roughjs/bin/core";
|
||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||
import type {
|
||||
Arrowhead,
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
NonDeleted,
|
||||
} from "./types";
|
||||
|
||||
export type RectangleBox = {
|
||||
x: number;
|
||||
|
||||
@@ -11,13 +11,10 @@ import type {
|
||||
PointerDownState,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { updateBoundElements } from "./binding";
|
||||
import { getCommonBounds } from "./bounds";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getPerfectElementSize } from "./sizeHelpers";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
import { getMinTextElementWidth } from "./textMeasurements";
|
||||
@@ -29,6 +26,8 @@ import {
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
@@ -104,7 +103,7 @@ export const dragSelectedElements = (
|
||||
);
|
||||
|
||||
elementsToUpdate.forEach((element) => {
|
||||
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||
updateElementCoords(pointerDownState, element, scene, adjustedOffset);
|
||||
if (!isArrowElement(element)) {
|
||||
// skip arrow labels since we calculate its position during render
|
||||
const textElement = getBoundTextElement(
|
||||
@@ -112,9 +111,14 @@ export const dragSelectedElements = (
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (textElement) {
|
||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||
updateElementCoords(
|
||||
pointerDownState,
|
||||
textElement,
|
||||
scene,
|
||||
adjustedOffset,
|
||||
);
|
||||
}
|
||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||
});
|
||||
}
|
||||
@@ -155,6 +159,7 @@ const calculateOffset = (
|
||||
const updateElementCoords = (
|
||||
pointerDownState: PointerDownState,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
scene: Scene,
|
||||
dragOffset: { x: number; y: number },
|
||||
) => {
|
||||
const originalElement =
|
||||
@@ -163,7 +168,7 @@ const updateElementCoords = (
|
||||
const nextX = originalElement.x + dragOffset.x;
|
||||
const nextY = originalElement.y + dragOffset.y;
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
});
|
||||
@@ -190,6 +195,7 @@ export const dragNewElement = ({
|
||||
shouldMaintainAspectRatio,
|
||||
shouldResizeFromCenter,
|
||||
zoom,
|
||||
scene,
|
||||
widthAspectRatio = null,
|
||||
originOffset = null,
|
||||
informMutation = true,
|
||||
@@ -205,6 +211,7 @@ export const dragNewElement = ({
|
||||
shouldMaintainAspectRatio: boolean;
|
||||
shouldResizeFromCenter: boolean;
|
||||
zoom: NormalizedZoomValue;
|
||||
scene: Scene;
|
||||
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
|
||||
true */
|
||||
widthAspectRatio?: number | null;
|
||||
@@ -285,7 +292,7 @@ export const dragNewElement = ({
|
||||
};
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
x: newX + (originOffset?.x ?? 0),
|
||||
@@ -295,7 +302,7 @@ export const dragNewElement = ({
|
||||
...textAutoResize,
|
||||
...imageInitialDimension,
|
||||
},
|
||||
informMutation,
|
||||
{ informMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -50,10 +50,9 @@ import { isBindableElement } from "./typeChecks";
|
||||
import {
|
||||
type ExcalidrawElbowArrowElement,
|
||||
type NonDeletedSceneElementsMap,
|
||||
type SceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import { aabbForElement, pointInsideBounds } from "./shapes";
|
||||
import { aabbForElement, aabbForPoints, pointInsideBounds } from "./shapes";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type { Heading } from "./heading";
|
||||
@@ -66,6 +65,8 @@ import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
import { debugDrawBounds } from "@excalidraw/utils/visualdebug";
|
||||
|
||||
type GridAddress = [number, number] & { _brand: "gridaddress" };
|
||||
|
||||
type Node = {
|
||||
@@ -107,8 +108,32 @@ type ElbowArrowData = {
|
||||
hoveredEndElement: ExcalidrawBindableElement | null;
|
||||
};
|
||||
|
||||
const DEDUP_TRESHOLD = 1;
|
||||
export const BASE_PADDING = 40;
|
||||
const calculateDedupTreshhold = <Point extends GlobalPoint | LocalPoint>(
|
||||
a: Point,
|
||||
b: Point,
|
||||
) => 1 + pointDistance(a, b) / 300;
|
||||
|
||||
const calculatePadding = (
|
||||
aabb: Bounds,
|
||||
startBoundingBox: Bounds,
|
||||
endBoundingBox: Bounds,
|
||||
) => {
|
||||
return Math.max(
|
||||
Math.min(
|
||||
Math.hypot(
|
||||
startBoundingBox[2] - startBoundingBox[0],
|
||||
startBoundingBox[3] - startBoundingBox[1],
|
||||
) / 4,
|
||||
Math.hypot(
|
||||
endBoundingBox[2] - endBoundingBox[0],
|
||||
endBoundingBox[3] - endBoundingBox[1],
|
||||
) / 4,
|
||||
Math.hypot(aabb[2] - aabb[0], aabb[3] - aabb[1]) / 4,
|
||||
40,
|
||||
),
|
||||
30,
|
||||
);
|
||||
};
|
||||
|
||||
const handleSegmentRenormalization = (
|
||||
arrow: ExcalidrawElbowArrowElement,
|
||||
@@ -184,7 +209,11 @@ const handleSegmentRenormalization = (
|
||||
|
||||
if (
|
||||
// Remove segments that are too short
|
||||
pointDistance(points[i - 2], points[i - 1]) < DEDUP_TRESHOLD
|
||||
pointDistance(points[i - 2], points[i - 1]) <
|
||||
calculateDedupTreshhold(
|
||||
points[i - 3] ?? points[i - 3],
|
||||
points[i] ?? points[i - 1],
|
||||
)
|
||||
) {
|
||||
const prevPrevSegmentIdx =
|
||||
nextFixedSegments?.findIndex((segment) => segment.index === i - 2) ??
|
||||
@@ -360,6 +389,10 @@ const handleSegmentRelease = (
|
||||
null,
|
||||
);
|
||||
|
||||
if (!restoredPoints) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const nextPoints: GlobalPoint[] = [];
|
||||
|
||||
// First part of the arrow are the old points
|
||||
@@ -464,6 +497,13 @@ const handleSegmentMove = (
|
||||
hoveredStartElement: ExcalidrawBindableElement | null,
|
||||
hoveredEndElement: ExcalidrawBindableElement | null,
|
||||
): ElementUpdate<ExcalidrawElbowArrowElement> => {
|
||||
const BASE_PADDING = calculatePadding(
|
||||
aabbForElement(arrow),
|
||||
hoveredStartElement
|
||||
? aabbForElement(hoveredStartElement)
|
||||
: [10, 10, 10, 10],
|
||||
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
|
||||
);
|
||||
const activelyModifiedSegmentIdx = fixedSegments
|
||||
.map((segment, i) => {
|
||||
if (
|
||||
@@ -708,6 +748,13 @@ const handleEndpointDrag = (
|
||||
hoveredStartElement: ExcalidrawBindableElement | null,
|
||||
hoveredEndElement: ExcalidrawBindableElement | null,
|
||||
) => {
|
||||
const BASE_PADDING = calculatePadding(
|
||||
aabbForPoints([startGlobalPoint, endGlobalPoint]),
|
||||
hoveredStartElement
|
||||
? aabbForElement(hoveredStartElement)
|
||||
: [10, 10, 10, 10],
|
||||
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
|
||||
);
|
||||
let startIsSpecial = arrow.startIsSpecial ?? null;
|
||||
let endIsSpecial = arrow.endIsSpecial ?? null;
|
||||
const globalUpdatedPoints = updatedPoints.map((p, i) =>
|
||||
@@ -742,6 +789,7 @@ const handleEndpointDrag = (
|
||||
|
||||
// Calculate the moving second point connection and add the start point
|
||||
{
|
||||
startIsSpecial = arrow.startIsSpecial && globalUpdatedPoints.length > 2;
|
||||
const secondPoint = globalUpdatedPoints[startIsSpecial ? 2 : 1];
|
||||
const thirdPoint = globalUpdatedPoints[startIsSpecial ? 3 : 2];
|
||||
const startIsHorizontal = headingIsHorizontal(startHeading);
|
||||
@@ -802,6 +850,7 @@ const handleEndpointDrag = (
|
||||
|
||||
// Calculate the moving second to last point connection
|
||||
{
|
||||
endIsSpecial = arrow.endIsSpecial && globalUpdatedPoints.length > 2;
|
||||
const secondToLastPoint =
|
||||
globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 3 : 2)];
|
||||
const thirdToLastPoint =
|
||||
@@ -887,7 +936,7 @@ export const updateElbowArrowPoints = (
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
updates: {
|
||||
points?: readonly LocalPoint[];
|
||||
fixedSegments?: FixedSegment[] | null;
|
||||
fixedSegments?: readonly FixedSegment[] | null;
|
||||
startBinding?: FixedPointBinding | null;
|
||||
endBinding?: FixedPointBinding | null;
|
||||
},
|
||||
@@ -1273,14 +1322,12 @@ const getElbowArrowData = (
|
||||
const startHeading = getBindPointHeading(
|
||||
startGlobalPoint,
|
||||
endGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredStartElement,
|
||||
origStartGlobalPoint,
|
||||
);
|
||||
const endHeading = getBindPointHeading(
|
||||
endGlobalPoint,
|
||||
startGlobalPoint,
|
||||
elementsMap,
|
||||
hoveredEndElement,
|
||||
origEndGlobalPoint,
|
||||
);
|
||||
@@ -1296,29 +1343,28 @@ const getElbowArrowData = (
|
||||
endGlobalPoint[0] + 2,
|
||||
endGlobalPoint[1] + 2,
|
||||
] as Bounds;
|
||||
const BASE_PADDING = calculatePadding(
|
||||
aabbForPoints([startGlobalPoint, endGlobalPoint]),
|
||||
hoveredStartElement
|
||||
? aabbForElement(hoveredStartElement)
|
||||
: [10, 10, 10, 10],
|
||||
hoveredEndElement ? aabbForElement(hoveredEndElement) : [10, 10, 10, 10],
|
||||
);
|
||||
const startOffsets = offsetFromHeading(
|
||||
startHeading,
|
||||
arrow.startArrowhead ? FIXED_BINDING_DISTANCE * 4 : FIXED_BINDING_DISTANCE,
|
||||
1,
|
||||
);
|
||||
const endOffsets = offsetFromHeading(
|
||||
endHeading,
|
||||
arrow.endArrowhead ? FIXED_BINDING_DISTANCE * 4 : FIXED_BINDING_DISTANCE,
|
||||
1,
|
||||
);
|
||||
const startElementBounds = hoveredStartElement
|
||||
? aabbForElement(
|
||||
hoveredStartElement,
|
||||
offsetFromHeading(
|
||||
startHeading,
|
||||
arrow.startArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2,
|
||||
1,
|
||||
),
|
||||
)
|
||||
? aabbForElement(hoveredStartElement, startOffsets)
|
||||
: startPointBounds;
|
||||
const endElementBounds = hoveredEndElement
|
||||
? aabbForElement(
|
||||
hoveredEndElement,
|
||||
offsetFromHeading(
|
||||
endHeading,
|
||||
arrow.endArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2,
|
||||
1,
|
||||
),
|
||||
)
|
||||
? aabbForElement(hoveredEndElement, endOffsets)
|
||||
: endPointBounds;
|
||||
const boundsOverlap =
|
||||
pointInsideBounds(
|
||||
@@ -1361,7 +1407,7 @@ const getElbowArrowData = (
|
||||
: BASE_PADDING -
|
||||
(arrow.startArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2),
|
||||
: FIXED_BINDING_DISTANCE),
|
||||
BASE_PADDING,
|
||||
),
|
||||
boundsOverlap
|
||||
@@ -1377,13 +1423,29 @@ const getElbowArrowData = (
|
||||
: BASE_PADDING -
|
||||
(arrow.endArrowhead
|
||||
? FIXED_BINDING_DISTANCE * 6
|
||||
: FIXED_BINDING_DISTANCE * 2),
|
||||
: FIXED_BINDING_DISTANCE),
|
||||
BASE_PADDING,
|
||||
),
|
||||
boundsOverlap,
|
||||
hoveredStartElement && aabbForElement(hoveredStartElement),
|
||||
hoveredEndElement && aabbForElement(hoveredEndElement),
|
||||
hoveredStartElement
|
||||
? aabbForElement(hoveredStartElement)
|
||||
: startPointBounds,
|
||||
hoveredEndElement ? aabbForElement(hoveredEndElement) : endPointBounds,
|
||||
);
|
||||
|
||||
debugDrawBounds(startElementBounds, {
|
||||
permanent: false,
|
||||
color: "red",
|
||||
});
|
||||
debugDrawBounds(endElementBounds, {
|
||||
permanent: false,
|
||||
color: "green",
|
||||
});
|
||||
debugDrawBounds(dynamicAABBs, {
|
||||
permanent: false,
|
||||
color: "blue",
|
||||
});
|
||||
|
||||
const startDonglePosition = getDonglePosition(
|
||||
dynamicAABBs[0],
|
||||
startHeading,
|
||||
@@ -1654,11 +1716,11 @@ const generateDynamicAABBs = (
|
||||
a: Bounds,
|
||||
b: Bounds,
|
||||
common: Bounds,
|
||||
startDifference?: [number, number, number, number],
|
||||
endDifference?: [number, number, number, number],
|
||||
disableSideHack?: boolean,
|
||||
startElementBounds?: Bounds | null,
|
||||
endElementBounds?: Bounds | null,
|
||||
startDifference: [number, number, number, number],
|
||||
endDifference: [number, number, number, number],
|
||||
disableSideHack: boolean,
|
||||
startElementBounds: Bounds,
|
||||
endElementBounds: Bounds,
|
||||
): Bounds[] => {
|
||||
const startEl = startElementBounds ?? a;
|
||||
const endEl = endElementBounds ?? b;
|
||||
@@ -1738,15 +1800,24 @@ const generateDynamicAABBs = (
|
||||
(second[0] + second[2]) / 2,
|
||||
(second[1] + second[3]) / 2,
|
||||
];
|
||||
if (b[0] > a[2] && a[1] > b[3]) {
|
||||
if (
|
||||
endElementBounds[0] > startElementBounds[2] &&
|
||||
startElementBounds[1] > endElementBounds[3]
|
||||
) {
|
||||
// BOTTOM LEFT
|
||||
const cX = first[2] + (second[0] - first[2]) / 2;
|
||||
const cY = second[3] + (first[1] - second[3]) / 2;
|
||||
|
||||
if (
|
||||
vectorCross(
|
||||
vector(a[2] - endCenterX, a[1] - endCenterY),
|
||||
vector(a[0] - endCenterX, a[3] - endCenterY),
|
||||
vector(
|
||||
startElementBounds[2] - endCenterX,
|
||||
startElementBounds[1] - endCenterY,
|
||||
),
|
||||
vector(
|
||||
startElementBounds[0] - endCenterX,
|
||||
startElementBounds[3] - endCenterY,
|
||||
),
|
||||
) > 0
|
||||
) {
|
||||
return [
|
||||
@@ -1759,15 +1830,24 @@ const generateDynamicAABBs = (
|
||||
[first[0], cY, first[2], first[3]],
|
||||
[second[0], second[1], second[2], cY],
|
||||
];
|
||||
} else if (a[2] < b[0] && a[3] < b[1]) {
|
||||
} else if (
|
||||
startElementBounds[2] < endElementBounds[0] &&
|
||||
startElementBounds[3] < endElementBounds[1]
|
||||
) {
|
||||
// TOP LEFT
|
||||
const cX = first[2] + (second[0] - first[2]) / 2;
|
||||
const cY = first[3] + (second[1] - first[3]) / 2;
|
||||
|
||||
if (
|
||||
vectorCross(
|
||||
vector(a[0] - endCenterX, a[1] - endCenterY),
|
||||
vector(a[2] - endCenterX, a[3] - endCenterY),
|
||||
vector(
|
||||
startElementBounds[0] - endCenterX,
|
||||
startElementBounds[1] - endCenterY,
|
||||
),
|
||||
vector(
|
||||
startElementBounds[2] - endCenterX,
|
||||
startElementBounds[3] - endCenterY,
|
||||
),
|
||||
) > 0
|
||||
) {
|
||||
return [
|
||||
@@ -1780,15 +1860,24 @@ const generateDynamicAABBs = (
|
||||
[first[0], first[1], cX, first[3]],
|
||||
[cX, second[1], second[2], second[3]],
|
||||
];
|
||||
} else if (a[0] > b[2] && a[3] < b[1]) {
|
||||
} else if (
|
||||
startElementBounds[0] > endElementBounds[2] &&
|
||||
startElementBounds[3] < endElementBounds[1]
|
||||
) {
|
||||
// TOP RIGHT
|
||||
const cX = second[2] + (first[0] - second[2]) / 2;
|
||||
const cY = first[3] + (second[1] - first[3]) / 2;
|
||||
|
||||
if (
|
||||
vectorCross(
|
||||
vector(a[2] - endCenterX, a[1] - endCenterY),
|
||||
vector(a[0] - endCenterX, a[3] - endCenterY),
|
||||
vector(
|
||||
startElementBounds[2] - endCenterX,
|
||||
startElementBounds[1] - endCenterY,
|
||||
),
|
||||
vector(
|
||||
startElementBounds[0] - endCenterX,
|
||||
startElementBounds[3] - endCenterY,
|
||||
),
|
||||
) > 0
|
||||
) {
|
||||
return [
|
||||
@@ -1801,15 +1890,24 @@ const generateDynamicAABBs = (
|
||||
[first[0], first[1], first[2], cY],
|
||||
[second[0], cY, second[2], second[3]],
|
||||
];
|
||||
} else if (a[0] > b[2] && a[1] > b[3]) {
|
||||
} else if (
|
||||
startElementBounds[0] > endElementBounds[2] &&
|
||||
startElementBounds[1] > endElementBounds[3]
|
||||
) {
|
||||
// BOTTOM RIGHT
|
||||
const cX = second[2] + (first[0] - second[2]) / 2;
|
||||
const cY = second[3] + (first[1] - second[3]) / 2;
|
||||
|
||||
if (
|
||||
vectorCross(
|
||||
vector(a[0] - endCenterX, a[1] - endCenterY),
|
||||
vector(a[2] - endCenterX, a[3] - endCenterY),
|
||||
vector(
|
||||
startElementBounds[0] - endCenterX,
|
||||
startElementBounds[1] - endCenterY,
|
||||
),
|
||||
vector(
|
||||
startElementBounds[2] - endCenterX,
|
||||
startElementBounds[3] - endCenterY,
|
||||
),
|
||||
) > 0
|
||||
) {
|
||||
return [
|
||||
@@ -2091,16 +2189,11 @@ const normalizeArrowElementUpdate = (
|
||||
nextFixedSegments: readonly FixedSegment[] | null,
|
||||
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
|
||||
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"],
|
||||
): {
|
||||
points: LocalPoint[];
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fixedSegments: readonly FixedSegment[] | null;
|
||||
startIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"];
|
||||
endIsSpecial?: ExcalidrawElbowArrowElement["startIsSpecial"];
|
||||
} => {
|
||||
): ElementUpdate<ExcalidrawElbowArrowElement> => {
|
||||
if (global.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const offsetX = global[0][0];
|
||||
const offsetY = global[0][1];
|
||||
let points = global.map((p) =>
|
||||
@@ -2188,7 +2281,10 @@ const removeElbowArrowShortSegments = (
|
||||
|
||||
const prev = points[idx - 1];
|
||||
const prevDist = pointDistance(prev, p);
|
||||
return prevDist > DEDUP_TRESHOLD;
|
||||
return (
|
||||
prevDist >
|
||||
calculateDedupTreshhold(points[idx - 2] ?? prev, points[idx + 1] ?? p)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2250,7 +2346,6 @@ const getGlobalPoint = (
|
||||
const getBindPointHeading = (
|
||||
p: GlobalPoint,
|
||||
otherPoint: GlobalPoint,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||
origPoint: GlobalPoint,
|
||||
): Heading =>
|
||||
@@ -2268,7 +2363,6 @@ const getBindPointHeading = (
|
||||
number,
|
||||
],
|
||||
),
|
||||
elementsMap,
|
||||
origPoint,
|
||||
);
|
||||
|
||||
@@ -2293,13 +2387,16 @@ const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean =>
|
||||
|
||||
export const validateElbowPoints = <P extends GlobalPoint | LocalPoint>(
|
||||
points: readonly P[],
|
||||
tolerance: number = DEDUP_TRESHOLD,
|
||||
tolerance?: number,
|
||||
) =>
|
||||
points
|
||||
.slice(1)
|
||||
.map(
|
||||
(p, i) =>
|
||||
Math.abs(p[0] - points[i][0]) < tolerance ||
|
||||
Math.abs(p[1] - points[i][1]) < tolerance,
|
||||
)
|
||||
.map((p, i) => {
|
||||
const t =
|
||||
tolerance ??
|
||||
calculateDedupTreshhold(points[i - 1] ?? points[i], points[i + 2] ?? p);
|
||||
return (
|
||||
Math.abs(p[0] - points[i][0]) < t || Math.abs(p[1] - points[i][1]) < t
|
||||
);
|
||||
})
|
||||
.every(Boolean);
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
type OrderedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
type LinkDirection = "up" | "right" | "down" | "left";
|
||||
|
||||
const VERTICAL_OFFSET = 100;
|
||||
@@ -236,10 +238,11 @@ const getOffsets = (
|
||||
|
||||
const addNewNode = (
|
||||
element: ExcalidrawFlowchartNodeElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: AppState,
|
||||
direction: LinkDirection,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const successors = getSuccessors(element, elementsMap, direction);
|
||||
const predeccessors = getPredecessors(element, elementsMap, direction);
|
||||
|
||||
@@ -274,9 +277,9 @@ const addNewNode = (
|
||||
const bindingArrow = createBindingArrow(
|
||||
element,
|
||||
nextNode,
|
||||
elementsMap,
|
||||
direction,
|
||||
appState,
|
||||
scene,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -287,9 +290,9 @@ const addNewNode = (
|
||||
|
||||
export const addNewNodes = (
|
||||
startNode: ExcalidrawFlowchartNodeElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: AppState,
|
||||
direction: LinkDirection,
|
||||
scene: Scene,
|
||||
numberOfNodes: number,
|
||||
) => {
|
||||
// always start from 0 and distribute evenly
|
||||
@@ -352,9 +355,9 @@ export const addNewNodes = (
|
||||
const bindingArrow = createBindingArrow(
|
||||
startNode,
|
||||
nextNode,
|
||||
elementsMap,
|
||||
direction,
|
||||
appState,
|
||||
scene,
|
||||
);
|
||||
|
||||
newNodes.push(nextNode);
|
||||
@@ -367,9 +370,9 @@ export const addNewNodes = (
|
||||
const createBindingArrow = (
|
||||
startBindingElement: ExcalidrawFlowchartNodeElement,
|
||||
endBindingElement: ExcalidrawFlowchartNodeElement,
|
||||
elementsMap: ElementsMap,
|
||||
direction: LinkDirection,
|
||||
appState: AppState,
|
||||
scene: Scene,
|
||||
) => {
|
||||
let startX: number;
|
||||
let startY: number;
|
||||
@@ -440,18 +443,10 @@ const createBindingArrow = (
|
||||
elbowed: true,
|
||||
});
|
||||
|
||||
bindLinearElement(
|
||||
bindingArrow,
|
||||
startBindingElement,
|
||||
"start",
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
bindLinearElement(
|
||||
bindingArrow,
|
||||
endBindingElement,
|
||||
"end",
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
bindLinearElement(bindingArrow, startBindingElement, "start", scene);
|
||||
bindLinearElement(bindingArrow, endBindingElement, "end", scene);
|
||||
|
||||
const changedElements = new Map<string, OrderedExcalidrawElement>();
|
||||
changedElements.set(
|
||||
@@ -467,7 +462,7 @@ const createBindingArrow = (
|
||||
bindingArrow as OrderedExcalidrawElement,
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(bindingArrow, [
|
||||
LinearElementEditor.movePoints(bindingArrow, scene, [
|
||||
{
|
||||
index: 1,
|
||||
point: bindingArrow.points[1],
|
||||
@@ -632,16 +627,17 @@ export class FlowChartCreator {
|
||||
|
||||
createNodes(
|
||||
startNode: ExcalidrawFlowchartNodeElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: AppState,
|
||||
direction: LinkDirection,
|
||||
scene: Scene,
|
||||
) {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
if (direction !== this.direction) {
|
||||
const { nextNode, bindingArrow } = addNewNode(
|
||||
startNode,
|
||||
elementsMap,
|
||||
appState,
|
||||
direction,
|
||||
scene,
|
||||
);
|
||||
|
||||
this.numberOfNodes = 1;
|
||||
@@ -652,9 +648,9 @@ export class FlowChartCreator {
|
||||
this.numberOfNodes += 1;
|
||||
const newNodes = addNewNodes(
|
||||
startNode,
|
||||
elementsMap,
|
||||
appState,
|
||||
direction,
|
||||
scene,
|
||||
this.numberOfNodes,
|
||||
);
|
||||
|
||||
@@ -682,13 +678,9 @@ export class FlowChartCreator {
|
||||
)
|
||||
) {
|
||||
this.pendingNodes = this.pendingNodes.map((node) =>
|
||||
mutateElement(
|
||||
node,
|
||||
{
|
||||
frameId: startNode.frameId,
|
||||
},
|
||||
false,
|
||||
),
|
||||
mutateElement(node, elementsMap, {
|
||||
frameId: startNode.frameId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getBoundTextElement } from "./textElement";
|
||||
import { hasBoundTextElement } from "./typeChecks";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
FractionalIndex,
|
||||
OrderedExcalidrawElement,
|
||||
@@ -152,9 +153,10 @@ export const orderByFractionalIndex = (
|
||||
*/
|
||||
export const syncMovedIndices = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
movedElements: Map<string, ExcalidrawElement>,
|
||||
movedElements: ElementsMap,
|
||||
): OrderedExcalidrawElement[] => {
|
||||
try {
|
||||
const elementsMap = arrayToMap(elements);
|
||||
const indicesGroups = getMovedIndicesGroups(elements, movedElements);
|
||||
|
||||
// try generatating indices, throws on invalid movedElements
|
||||
@@ -176,7 +178,7 @@ export const syncMovedIndices = (
|
||||
|
||||
// split mutation so we don't end up in an incosistent state
|
||||
for (const [element, update] of elementsUpdates) {
|
||||
mutateElement(element, update, false);
|
||||
mutateElement(element, elementsMap, update);
|
||||
}
|
||||
} catch (e) {
|
||||
// fallback to default sync
|
||||
@@ -194,10 +196,12 @@ export const syncMovedIndices = (
|
||||
export const syncInvalidIndices = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): OrderedExcalidrawElement[] => {
|
||||
const elementsMap = arrayToMap(elements);
|
||||
const indicesGroups = getInvalidIndicesGroups(elements);
|
||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||
|
||||
for (const [element, update] of elementsUpdates) {
|
||||
mutateElement(element, update, false);
|
||||
mutateElement(element, elementsMap, update);
|
||||
}
|
||||
|
||||
return elements as OrderedExcalidrawElement[];
|
||||
@@ -210,7 +214,7 @@ export const syncInvalidIndices = (
|
||||
*/
|
||||
const getMovedIndicesGroups = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
movedElements: Map<string, ExcalidrawElement>,
|
||||
movedElements: ElementsMap,
|
||||
) => {
|
||||
const indicesGroups: number[][] = [];
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
|
||||
import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
|
||||
import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
|
||||
|
||||
import type { ExcalidrawElementsIncludingDeleted } from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type {
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
@@ -29,6 +27,8 @@ import {
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import type { ExcalidrawElementsIncludingDeleted } from "./Scene";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
@@ -56,13 +56,9 @@ export const bindElementsToFramesAfterDuplication = (
|
||||
const nextFrameId = origIdToDuplicateId.get(element.frameId);
|
||||
const nextElement = nextElementId && nextElementMap.get(nextElementId);
|
||||
if (nextElement) {
|
||||
mutateElement(
|
||||
nextElement,
|
||||
{
|
||||
frameId: nextFrameId ?? null,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(nextElement, nextElementMap, {
|
||||
frameId: nextFrameId ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -565,13 +561,9 @@ export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
||||
}
|
||||
|
||||
for (const element of finalElementsToAdd) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
frameId: frame.id,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(element, elementsMap, {
|
||||
frameId: frame.id,
|
||||
});
|
||||
}
|
||||
|
||||
return allElements;
|
||||
@@ -609,13 +601,9 @@ export const removeElementsFromFrame = (
|
||||
}
|
||||
|
||||
for (const [, element] of _elementsToRemove) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
frameId: null,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(element, elementsMap, {
|
||||
frameId: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ import {
|
||||
tupleToCoors,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { Store } from "@excalidraw/excalidraw/store";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
@@ -50,10 +46,8 @@ import {
|
||||
getMinMaxXYFromCurvePathOps,
|
||||
} from "./bounds";
|
||||
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
|
||||
import { headingIsHorizontal, vectorToHeading } from "./heading";
|
||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import {
|
||||
isBindingElement,
|
||||
@@ -73,6 +67,8 @@ import {
|
||||
|
||||
import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
import type {
|
||||
NonDeleted,
|
||||
@@ -84,7 +80,6 @@ import type {
|
||||
ElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
FixedPointBinding,
|
||||
SceneElementsMap,
|
||||
FixedSegment,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
@@ -127,15 +122,17 @@ export class LinearElementEditor {
|
||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||
public readonly elbowed: boolean;
|
||||
|
||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||
constructor(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
this.elementId = element.id as string & {
|
||||
_brand: "excalidrawLinearElementId";
|
||||
};
|
||||
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
|
||||
console.error("Linear element is not normalized", Error().stack);
|
||||
LinearElementEditor.normalizePoints(element);
|
||||
LinearElementEditor.normalizePoints(element, elementsMap);
|
||||
}
|
||||
|
||||
this.selectedPointsIndices = null;
|
||||
this.lastUncommittedPoint = null;
|
||||
this.isDragging = false;
|
||||
@@ -309,7 +306,7 @@ export class LinearElementEditor {
|
||||
event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
|
||||
);
|
||||
|
||||
LinearElementEditor.movePoints(element, [
|
||||
LinearElementEditor.movePoints(element, scene, [
|
||||
{
|
||||
index: selectedIndex,
|
||||
point: pointFrom(
|
||||
@@ -333,6 +330,7 @@ export class LinearElementEditor {
|
||||
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
scene,
|
||||
selectedPointsIndices.map((pointIndex) => {
|
||||
const newPointPosition: LocalPoint =
|
||||
pointIndex === lastClickedPoint
|
||||
@@ -358,7 +356,7 @@ export class LinearElementEditor {
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
handleBindTextResize(element, elementsMap, false);
|
||||
handleBindTextResize(element, scene, false);
|
||||
}
|
||||
|
||||
// suggest bindings for first and last point if selected
|
||||
@@ -453,7 +451,7 @@ export class LinearElementEditor {
|
||||
selectedPoint === element.points.length - 1
|
||||
) {
|
||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||
LinearElementEditor.movePoints(element, [
|
||||
LinearElementEditor.movePoints(element, scene, [
|
||||
{
|
||||
index: selectedPoint,
|
||||
point:
|
||||
@@ -795,7 +793,7 @@ export class LinearElementEditor {
|
||||
);
|
||||
} else if (event.altKey && appState.editingLinearElement) {
|
||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
points: [
|
||||
...element.points,
|
||||
LinearElementEditor.createPointAt(
|
||||
@@ -861,7 +859,6 @@ export class LinearElementEditor {
|
||||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
@@ -934,13 +931,13 @@ export class LinearElementEditor {
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
app: AppClassProperties,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
): LinearElementEditor | null {
|
||||
const appState = app.state;
|
||||
if (!appState.editingLinearElement) {
|
||||
return null;
|
||||
}
|
||||
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
if (!element) {
|
||||
return appState.editingLinearElement;
|
||||
@@ -951,7 +948,9 @@ export class LinearElementEditor {
|
||||
|
||||
if (!event.altKey) {
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
||||
LinearElementEditor.deletePoints(element, app.scene, [
|
||||
points.length - 1,
|
||||
]);
|
||||
}
|
||||
return {
|
||||
...appState.editingLinearElement,
|
||||
@@ -989,14 +988,14 @@ export class LinearElementEditor {
|
||||
}
|
||||
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.movePoints(element, [
|
||||
LinearElementEditor.movePoints(element, app.scene, [
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: newPoint,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
LinearElementEditor.addPoints(element, [{ point: newPoint }]);
|
||||
LinearElementEditor.addPoints(element, app.scene, [{ point: newPoint }]);
|
||||
}
|
||||
return {
|
||||
...appState.editingLinearElement,
|
||||
@@ -1160,23 +1159,26 @@ export class LinearElementEditor {
|
||||
y: element.y + offsetY,
|
||||
};
|
||||
}
|
||||
|
||||
// element-mutating methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static normalizePoints(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
||||
static normalizePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
mutateElement(
|
||||
element,
|
||||
elementsMap,
|
||||
LinearElementEditor.getNormalizedPoints(element),
|
||||
);
|
||||
}
|
||||
|
||||
static duplicateSelectedPoints(
|
||||
appState: AppState,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
): AppState {
|
||||
static duplicateSelectedPoints(appState: AppState, scene: Scene): AppState {
|
||||
invariant(
|
||||
appState.editingLinearElement,
|
||||
"Not currently editing a linear element",
|
||||
);
|
||||
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
|
||||
@@ -1219,13 +1221,13 @@ export class LinearElementEditor {
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
mutateElement(element, { points: nextPoints });
|
||||
scene.mutateElement(element, { points: nextPoints });
|
||||
|
||||
// temp hack to ensure the line doesn't move when adding point to the end,
|
||||
// potentially expanding the bounding box
|
||||
if (pointAddedToEnd) {
|
||||
const lastPoint = element.points[element.points.length - 1];
|
||||
LinearElementEditor.movePoints(element, [
|
||||
LinearElementEditor.movePoints(element, scene, [
|
||||
{
|
||||
index: element.points.length - 1,
|
||||
point: pointFrom(lastPoint[0] + 30, lastPoint[1] + 30),
|
||||
@@ -1244,6 +1246,7 @@ export class LinearElementEditor {
|
||||
|
||||
static deletePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
pointIndices: readonly number[],
|
||||
) {
|
||||
let offsetX = 0;
|
||||
@@ -1274,28 +1277,41 @@ export class LinearElementEditor {
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||
LinearElementEditor._updatePoints(
|
||||
element,
|
||||
scene,
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
);
|
||||
}
|
||||
|
||||
static addPoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
targetPoints: { point: LocalPoint }[],
|
||||
) {
|
||||
const offsetX = 0;
|
||||
const offsetY = 0;
|
||||
|
||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||
LinearElementEditor._updatePoints(
|
||||
element,
|
||||
scene,
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
);
|
||||
}
|
||||
|
||||
static movePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
targetPoints: { index: number; point: LocalPoint; isDragging?: boolean }[],
|
||||
otherUpdates?: {
|
||||
startBinding?: PointBinding | null;
|
||||
endBinding?: PointBinding | null;
|
||||
},
|
||||
sceneElementsMap?: NonDeletedSceneElementsMap,
|
||||
) {
|
||||
const { points } = element;
|
||||
|
||||
@@ -1329,6 +1345,7 @@ export class LinearElementEditor {
|
||||
|
||||
LinearElementEditor._updatePoints(
|
||||
element,
|
||||
scene,
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
@@ -1339,7 +1356,6 @@ export class LinearElementEditor {
|
||||
dragging || targetPoint.isDragging === true,
|
||||
false,
|
||||
),
|
||||
sceneElementsMap,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1394,8 +1410,9 @@ export class LinearElementEditor {
|
||||
pointerCoords: PointerCoords,
|
||||
app: AppClassProperties,
|
||||
snapToGrid: boolean,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
elementsMap,
|
||||
@@ -1425,9 +1442,7 @@ export class LinearElementEditor {
|
||||
...element.points.slice(segmentMidpoint.index!),
|
||||
];
|
||||
|
||||
mutateElement(element, {
|
||||
points,
|
||||
});
|
||||
scene.mutateElement(element, { points });
|
||||
|
||||
ret.pointerDownState = {
|
||||
...linearElementEditor.pointerDownState,
|
||||
@@ -1443,6 +1458,7 @@ export class LinearElementEditor {
|
||||
|
||||
private static _updatePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
scene: Scene,
|
||||
nextPoints: readonly LocalPoint[],
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
@@ -1479,28 +1495,10 @@ export class LinearElementEditor {
|
||||
|
||||
updates.points = Array.from(nextPoints);
|
||||
|
||||
if (!options?.sceneElementsMap || Scene.getScene(element)) {
|
||||
mutateElement(element, updates, true, {
|
||||
isDragging: options?.isDragging,
|
||||
});
|
||||
} else {
|
||||
// The element is not in the scene, so we need to use the provided
|
||||
// scene map.
|
||||
Object.assign(element, {
|
||||
...updates,
|
||||
angle: 0 as Radians,
|
||||
|
||||
...updateElbowArrowPoints(
|
||||
element,
|
||||
options.sceneElementsMap,
|
||||
updates,
|
||||
{
|
||||
isDragging: options?.isDragging,
|
||||
},
|
||||
),
|
||||
});
|
||||
}
|
||||
bumpVersion(element);
|
||||
scene.mutateElement(element, updates, {
|
||||
informMutation: true,
|
||||
isDragging: options?.isDragging ?? false,
|
||||
});
|
||||
} else {
|
||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||
const prevCoords = getElementPointsCoords(element, element.points);
|
||||
@@ -1515,7 +1513,7 @@ export class LinearElementEditor {
|
||||
pointFrom(dX, dY),
|
||||
element.angle,
|
||||
);
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
...otherUpdates,
|
||||
points: nextPoints,
|
||||
x: element.x + rotated[0],
|
||||
@@ -1574,7 +1572,7 @@ export class LinearElementEditor {
|
||||
elementsMap,
|
||||
);
|
||||
if (points.length < 2) {
|
||||
mutateElement(boundTextElement, { isDeleted: true });
|
||||
mutateElement(boundTextElement, elementsMap, { isDeleted: true });
|
||||
}
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
@@ -1781,8 +1779,9 @@ export class LinearElementEditor {
|
||||
index: number,
|
||||
x: number,
|
||||
y: number,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
): LinearElementEditor {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElement.elementId,
|
||||
elementsMap,
|
||||
@@ -1825,7 +1824,7 @@ export class LinearElementEditor {
|
||||
.map((segment) => segment.index)
|
||||
.reduce((count, idx) => (idx < index ? count + 1 : count), 0);
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
fixedSegments: nextFixedSegments,
|
||||
});
|
||||
|
||||
@@ -1859,14 +1858,14 @@ export class LinearElementEditor {
|
||||
|
||||
static deleteFixedSegment(
|
||||
element: ExcalidrawElbowArrowElement,
|
||||
scene: Scene,
|
||||
index: number,
|
||||
): void {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
fixedSegments: element.fixedSegments?.filter(
|
||||
(segment) => segment.index !== index,
|
||||
),
|
||||
});
|
||||
mutateElement(element, {}, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,8 @@ import {
|
||||
getSizeFromPoints,
|
||||
randomInteger,
|
||||
getUpdatedTimestamp,
|
||||
toBrandedType,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
// TODO: remove direct dependency on the scene, should be passed in or injected instead
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
@@ -16,35 +11,42 @@ import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
|
||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||
|
||||
import { isElbowArrow } from "./typeChecks";
|
||||
|
||||
import type { ExcalidrawElement, NonDeletedSceneElementsMap } from "./types";
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElbowArrowElement,
|
||||
ExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
export type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
"id" | "version" | "versionNonce" | "updated"
|
||||
>;
|
||||
|
||||
// This function tracks updates of text elements for the purposes for collaboration.
|
||||
// The version is used to compare updates when more than one user is working in
|
||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||
// are calling it either from a React event handler or within unstable_batchedUpdates().
|
||||
/**
|
||||
* This function tracks updates of text elements for the purposes for collaboration.
|
||||
* The version is used to compare updates when more than one user is working in
|
||||
* the same drawing.
|
||||
*
|
||||
* WARNING: this won't trigger the component to update, so if you need to trigger component update,
|
||||
* use `scene.mutateElement` or `ExcalidrawImperativeAPI.mutateElement` instead.
|
||||
*/
|
||||
export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
elementsMap: ElementsMap,
|
||||
updates: ElementUpdate<TElement>,
|
||||
informMutation = true,
|
||||
options?: {
|
||||
// Currently only for elbow arrows.
|
||||
// If true, the elbow arrow tries to bind to the nearest element. If false
|
||||
// it tries to keep the same bound element, if any.
|
||||
isDragging?: boolean;
|
||||
},
|
||||
): TElement => {
|
||||
) => {
|
||||
let didChange = false;
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
const { points, fixedSegments, fileId, startBinding, endBinding } =
|
||||
const { points, fixedSegments, startBinding, endBinding, fileId } =
|
||||
updates as any;
|
||||
|
||||
if (
|
||||
@@ -55,10 +57,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
typeof startBinding !== "undefined" ||
|
||||
typeof endBinding !== "undefined") // manual binding to element
|
||||
) {
|
||||
const elementsMap = toBrandedType<NonDeletedSceneElementsMap>(
|
||||
Scene.getScene(element)?.getNonDeletedElementsMap() ?? new Map(),
|
||||
);
|
||||
|
||||
updates = {
|
||||
...updates,
|
||||
angle: 0 as Radians,
|
||||
@@ -68,16 +66,9 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
x: updates.x || element.x,
|
||||
y: updates.y || element.y,
|
||||
},
|
||||
elementsMap,
|
||||
{
|
||||
fixedSegments,
|
||||
points,
|
||||
startBinding,
|
||||
endBinding,
|
||||
},
|
||||
{
|
||||
isDragging: options?.isDragging,
|
||||
},
|
||||
elementsMap as NonDeletedSceneElementsMap,
|
||||
updates as ElementUpdate<ExcalidrawElbowArrowElement>,
|
||||
options,
|
||||
),
|
||||
};
|
||||
} else if (typeof points !== "undefined") {
|
||||
@@ -150,10 +141,6 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
element.versionNonce = randomInteger();
|
||||
element.updated = getUpdatedTimestamp();
|
||||
|
||||
if (informMutation) {
|
||||
Scene.getScene(element)?.triggerUpdate();
|
||||
}
|
||||
|
||||
return element;
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ import type {
|
||||
ExcalidrawIframeElement,
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
FixedSegment,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
|
||||
@@ -478,7 +477,7 @@ export const newArrowElement = <T extends boolean>(
|
||||
endArrowhead?: Arrowhead | null;
|
||||
points?: ExcalidrawArrowElement["points"];
|
||||
elbowed?: T;
|
||||
fixedSegments?: FixedSegment[] | null;
|
||||
fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"] | null;
|
||||
} & ElementConstructorOpts,
|
||||
): T extends true
|
||||
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||
|
||||
@@ -17,8 +17,6 @@ import {
|
||||
|
||||
import type { GlobalPoint } from "@excalidraw/math";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import type { PointerDownState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
@@ -32,7 +30,6 @@ import {
|
||||
getElementBounds,
|
||||
} from "./bounds";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getBoundTextElementId,
|
||||
@@ -60,6 +57,8 @@ import {
|
||||
|
||||
import { isInGroup } from "./groups";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { BoundingBox } from "./bounds";
|
||||
import type {
|
||||
MaybeTransformHandleType,
|
||||
@@ -74,7 +73,6 @@ import type {
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
SceneElementsMap,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "./types";
|
||||
|
||||
@@ -83,7 +81,6 @@ export const transformElements = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: SceneElementsMap,
|
||||
scene: Scene,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
@@ -93,31 +90,31 @@ export const transformElements = (
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
): boolean => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
if (transformHandleType === "rotation") {
|
||||
if (!isElbowArrow(element)) {
|
||||
rotateSingleElement(
|
||||
element,
|
||||
elementsMap,
|
||||
scene,
|
||||
pointerX,
|
||||
pointerY,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
);
|
||||
updateBoundElements(element, elementsMap);
|
||||
updateBoundElements(element, scene);
|
||||
}
|
||||
} else if (isTextElement(element) && transformHandleType) {
|
||||
resizeSingleTextElement(
|
||||
originalElements,
|
||||
element,
|
||||
elementsMap,
|
||||
scene,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
updateBoundElements(element, elementsMap);
|
||||
updateBoundElements(element, scene);
|
||||
return true;
|
||||
} else if (transformHandleType) {
|
||||
const elementId = selectedElements[0].id;
|
||||
@@ -129,8 +126,6 @@ export const transformElements = (
|
||||
getNextSingleWidthAndHeightFromPointer(
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElements,
|
||||
transformHandleType,
|
||||
pointerX,
|
||||
pointerY,
|
||||
@@ -145,8 +140,8 @@ export const transformElements = (
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElements,
|
||||
scene,
|
||||
transformHandleType,
|
||||
{
|
||||
shouldMaintainAspectRatio,
|
||||
@@ -161,7 +156,6 @@ export const transformElements = (
|
||||
rotateMultipleElements(
|
||||
originalElements,
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
scene,
|
||||
pointerX,
|
||||
pointerY,
|
||||
@@ -210,13 +204,15 @@ export const transformElements = (
|
||||
|
||||
const rotateSingleElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(
|
||||
element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
let angle: Radians;
|
||||
@@ -233,13 +229,13 @@ const rotateSingleElement = (
|
||||
}
|
||||
const boundTextElementId = getBoundTextElementId(element);
|
||||
|
||||
mutateElement(element, { angle });
|
||||
scene.mutateElement(element, { angle });
|
||||
if (boundTextElementId) {
|
||||
const textElement =
|
||||
scene.getElement<ExcalidrawTextElementWithContainer>(boundTextElementId);
|
||||
|
||||
if (textElement && !isArrowElement(element)) {
|
||||
mutateElement(textElement, { angle });
|
||||
scene.mutateElement(textElement, { angle });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -289,12 +285,13 @@ export const measureFontSizeFromWidth = (
|
||||
const resizeSingleTextElement = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
transformHandleType: TransformHandleDirection,
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
@@ -393,7 +390,7 @@ const resizeSingleTextElement = (
|
||||
);
|
||||
const [nextX, nextY] = newTopLeft;
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
fontSize: metrics.size,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
@@ -508,14 +505,13 @@ const resizeSingleTextElement = (
|
||||
autoResize: false,
|
||||
};
|
||||
|
||||
mutateElement(element, resizedElement);
|
||||
scene.mutateElement(element, resizedElement);
|
||||
}
|
||||
};
|
||||
|
||||
const rotateMultipleElements = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: SceneElementsMap,
|
||||
scene: Scene,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
@@ -523,6 +519,7 @@ const rotateMultipleElements = (
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
let centerAngle =
|
||||
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
||||
if (shouldRotateWithDiscreteAngle) {
|
||||
@@ -543,38 +540,30 @@ const rotateMultipleElements = (
|
||||
(centerAngle + origAngle - element.angle) as Radians,
|
||||
);
|
||||
|
||||
if (isElbowArrow(element)) {
|
||||
// Needed to re-route the arrow
|
||||
mutateElement(element, {
|
||||
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||
});
|
||||
} else {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
const updates = isElbowArrow(element)
|
||||
? {
|
||||
// Needed to re-route the arrow
|
||||
points: getArrowLocalFixedPoints(element, elementsMap),
|
||||
}
|
||||
: {
|
||||
x: element.x + (rotatedCX - cx),
|
||||
y: element.y + (rotatedCY - cy),
|
||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
updateBoundElements(element, elementsMap, {
|
||||
scene.mutateElement(element, updates);
|
||||
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: elements,
|
||||
});
|
||||
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
if (boundText && !isArrowElement(element)) {
|
||||
mutateElement(
|
||||
boundText,
|
||||
{
|
||||
x: boundText.x + (rotatedCX - cx),
|
||||
y: boundText.y + (rotatedCY - cy),
|
||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(boundText, {
|
||||
x: boundText.x + (rotatedCX - cx),
|
||||
y: boundText.y + (rotatedCY - cy),
|
||||
angle: normalizeRadians((centerAngle + origAngle) as Radians),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -819,8 +808,8 @@ export const resizeSingleElement = (
|
||||
nextHeight: number,
|
||||
latestElement: ExcalidrawElement,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
handleDirection: TransformHandleDirection,
|
||||
{
|
||||
shouldInformMutation = true,
|
||||
@@ -833,6 +822,7 @@ export const resizeSingleElement = (
|
||||
} = {},
|
||||
) => {
|
||||
let boundTextFont: { fontSize?: number } = {};
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
|
||||
if (boundTextElement) {
|
||||
@@ -932,7 +922,7 @@ export const resizeSingleElement = (
|
||||
}
|
||||
|
||||
if ("scale" in latestElement && "scale" in origElement) {
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
scale: [
|
||||
// defaulting because scaleX/Y can be 0/-0
|
||||
(Math.sign(nextWidth) || origElement.scale[0]) * origElement.scale[0],
|
||||
@@ -967,21 +957,24 @@ export const resizeSingleElement = (
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
mutateElement(latestElement, updates, shouldInformMutation);
|
||||
scene.mutateElement(latestElement, updates, {
|
||||
informMutation: shouldInformMutation,
|
||||
isDragging: false,
|
||||
});
|
||||
|
||||
updateBoundElements(latestElement, elementsMap as SceneElementsMap, {
|
||||
updateBoundElements(latestElement, scene, {
|
||||
// TODO: confirm with MARK if this actually makes sense
|
||||
newSize: { width: nextWidth, height: nextHeight },
|
||||
});
|
||||
|
||||
if (boundTextElement && boundTextFont != null) {
|
||||
mutateElement(boundTextElement, {
|
||||
scene.mutateElement(boundTextElement, {
|
||||
fontSize: boundTextFont.fontSize,
|
||||
});
|
||||
}
|
||||
handleBindTextResize(
|
||||
latestElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
handleDirection,
|
||||
shouldMaintainAspectRatio,
|
||||
);
|
||||
@@ -991,8 +984,6 @@ export const resizeSingleElement = (
|
||||
const getNextSingleWidthAndHeightFromPointer = (
|
||||
latestElement: ExcalidrawElement,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
handleDirection: TransformHandleDirection,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
@@ -1527,27 +1518,24 @@ export const resizeMultipleElements = (
|
||||
} of elementsAndUpdates) {
|
||||
const { width, height, angle } = update;
|
||||
|
||||
mutateElement(element, update, false, {
|
||||
scene.mutateElement(element, update, {
|
||||
informMutation: true,
|
||||
// needed for the fixed binding point udpate to take effect
|
||||
isDragging: true,
|
||||
});
|
||||
|
||||
updateBoundElements(element, elementsMap as SceneElementsMap, {
|
||||
updateBoundElements(element, scene, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
newSize: { width, height },
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement && boundTextFontSize) {
|
||||
mutateElement(
|
||||
boundTextElement,
|
||||
{
|
||||
fontSize: boundTextFontSize,
|
||||
angle: isLinearElement(element) ? undefined : angle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
handleBindTextResize(element, elementsMap, handleDirection, true);
|
||||
scene.mutateElement(boundTextElement, {
|
||||
fontSize: boundTextFontSize,
|
||||
angle: isLinearElement(element) ? undefined : angle,
|
||||
});
|
||||
handleBindTextResize(element, scene, handleDirection, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isShallowEqual } from "@excalidraw/common";
|
||||
import { arrayToMap, isShallowEqual } from "@excalidraw/common";
|
||||
|
||||
import type {
|
||||
AppState,
|
||||
@@ -264,6 +264,7 @@ export const makeNextSelectedElementIds = (
|
||||
|
||||
const _getLinearElementEditor = (
|
||||
targetElements: readonly ExcalidrawElement[],
|
||||
allElements: readonly NonDeletedExcalidrawElement[],
|
||||
) => {
|
||||
const linears = targetElements.filter(isLinearElement);
|
||||
if (linears.length === 1) {
|
||||
@@ -274,7 +275,7 @@ const _getLinearElementEditor = (
|
||||
);
|
||||
|
||||
if (onlySingleLinearSelected) {
|
||||
return new LinearElementEditor(linear);
|
||||
return new LinearElementEditor(linear, arrayToMap(allElements));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +288,7 @@ export const getSelectionStateForElements = (
|
||||
appState: AppState,
|
||||
) => {
|
||||
return {
|
||||
selectedLinearElement: _getLinearElementEditor(targetElements),
|
||||
selectedLinearElement: _getLinearElementEditor(targetElements, allElements),
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: appState.editingGroupId,
|
||||
|
||||
@@ -282,6 +282,15 @@ export const mapIntervalToBezierT = <P extends GlobalPoint | LocalPoint>(
|
||||
);
|
||||
};
|
||||
|
||||
export const aabbForPoints = <Point extends GlobalPoint | LocalPoint>(
|
||||
points: Point[],
|
||||
): Bounds => [
|
||||
Math.min(...points.map((point) => point[0])),
|
||||
Math.min(...points.map((point) => point[1])),
|
||||
Math.max(...points.map((point) => point[0])),
|
||||
Math.max(...points.map((point) => point[1])),
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the axis-aligned bounding box for a given element
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
import type { AppState, Offsets, Zoom } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import { getCommonBounds, getElementBounds } from "./bounds";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "./types";
|
||||
@@ -170,41 +169,6 @@ export const getLockedLinearCursorAlignSize = (
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
export const resizePerfectLineForNWHandler = (
|
||||
element: ExcalidrawElement,
|
||||
x: number,
|
||||
y: number,
|
||||
) => {
|
||||
const anchorX = element.x + element.width;
|
||||
const anchorY = element.y + element.height;
|
||||
const distanceToAnchorX = x - anchorX;
|
||||
const distanceToAnchorY = y - anchorY;
|
||||
if (Math.abs(distanceToAnchorX) < Math.abs(distanceToAnchorY) / 2) {
|
||||
mutateElement(element, {
|
||||
x: anchorX,
|
||||
width: 0,
|
||||
y,
|
||||
height: -distanceToAnchorY,
|
||||
});
|
||||
} else if (Math.abs(distanceToAnchorY) < Math.abs(element.width) / 2) {
|
||||
mutateElement(element, {
|
||||
y: anchorY,
|
||||
height: 0,
|
||||
});
|
||||
} else {
|
||||
const nextHeight =
|
||||
Math.sign(distanceToAnchorY) *
|
||||
Math.sign(distanceToAnchorX) *
|
||||
element.width;
|
||||
mutateElement(element, {
|
||||
x,
|
||||
y: anchorY - nextHeight,
|
||||
width: -distanceToAnchorX,
|
||||
height: nextHeight,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getNormalizedDimensions = (
|
||||
element: Pick<ExcalidrawElement, "width" | "height" | "x" | "y">,
|
||||
): {
|
||||
|
||||
@@ -14,12 +14,14 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type { ExtractSetType } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
|
||||
import { measureText } from "./textMeasurements";
|
||||
import { wrapText } from "./textWrapping";
|
||||
import {
|
||||
@@ -28,7 +30,7 @@ import {
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
import type { Radians } from "../../math/src";
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { MaybeTransformHandleType } from "./transformHandles";
|
||||
import type {
|
||||
@@ -44,9 +46,10 @@ import type {
|
||||
export const redrawTextBoundingBox = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawElement | null,
|
||||
elementsMap: ElementsMap,
|
||||
informMutation = true,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
|
||||
let maxWidth = undefined;
|
||||
|
||||
if (!isProdEnv()) {
|
||||
@@ -106,38 +109,43 @@ export const redrawTextBoundingBox = (
|
||||
metrics.height,
|
||||
container.type,
|
||||
);
|
||||
mutateElement(container, { height: nextHeight }, informMutation);
|
||||
scene.mutateElement(container, { height: nextHeight });
|
||||
updateOriginalContainerCache(container.id, nextHeight);
|
||||
}
|
||||
|
||||
if (metrics.width > maxContainerWidth) {
|
||||
const nextWidth = computeContainerDimensionForBoundText(
|
||||
metrics.width,
|
||||
container.type,
|
||||
);
|
||||
mutateElement(container, { width: nextWidth }, informMutation);
|
||||
scene.mutateElement(container, { width: nextWidth });
|
||||
}
|
||||
|
||||
const updatedTextElement = {
|
||||
...textElement,
|
||||
...boundTextUpdates,
|
||||
} as ExcalidrawTextElementWithContainer;
|
||||
|
||||
const { x, y } = computeBoundTextPosition(
|
||||
container,
|
||||
updatedTextElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
boundTextUpdates.x = x;
|
||||
boundTextUpdates.y = y;
|
||||
}
|
||||
|
||||
mutateElement(textElement, boundTextUpdates, informMutation);
|
||||
scene.mutateElement(textElement, boundTextUpdates);
|
||||
};
|
||||
|
||||
export const handleBindTextResize = (
|
||||
container: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
shouldMaintainAspectRatio = false,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const boundTextElementId = getBoundTextElementId(container);
|
||||
if (!boundTextElementId) {
|
||||
return;
|
||||
@@ -190,20 +198,20 @@ export const handleBindTextResize = (
|
||||
transformHandleType === "n")
|
||||
? container.y - diff
|
||||
: container.y;
|
||||
mutateElement(container, {
|
||||
scene.mutateElement(container, {
|
||||
height: containerHeight,
|
||||
y: updatedY,
|
||||
});
|
||||
}
|
||||
|
||||
mutateElement(textElement, {
|
||||
scene.mutateElement(textElement, {
|
||||
text,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
});
|
||||
|
||||
if (!isArrowElement(container)) {
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
textElement,
|
||||
computeBoundTextPosition(container, textElement, elementsMap),
|
||||
);
|
||||
|
||||
@@ -119,6 +119,20 @@ export const isElbowArrow = (
|
||||
return isArrowElement(element) && element.elbowed;
|
||||
};
|
||||
|
||||
export const isSharpArrow = (
|
||||
element?: ExcalidrawElement,
|
||||
): element is ExcalidrawArrowElement => {
|
||||
return isArrowElement(element) && !element.elbowed && !element.roundness;
|
||||
};
|
||||
|
||||
export const isCurvedArrow = (
|
||||
element?: ExcalidrawElement,
|
||||
): element is ExcalidrawArrowElement => {
|
||||
return (
|
||||
isArrowElement(element) && !element.elbowed && element.roundness !== null
|
||||
);
|
||||
};
|
||||
|
||||
export const isLinearElementType = (
|
||||
elementType: ElementOrToolType,
|
||||
): boolean => {
|
||||
@@ -271,6 +285,10 @@ export const isBoundToContainer = (
|
||||
);
|
||||
};
|
||||
|
||||
export const isArrowBoundToElement = (element: ExcalidrawArrowElement) => {
|
||||
return !!element.startBinding || !!element.endBinding;
|
||||
};
|
||||
|
||||
export const isUsingAdaptiveRadius = (type: string) =>
|
||||
type === "rectangle" ||
|
||||
type === "embeddable" ||
|
||||
|
||||
@@ -412,3 +412,11 @@ export type NonDeletedSceneElementsMap = Map<
|
||||
export type ElementsMapOrArray =
|
||||
| readonly ExcalidrawElement[]
|
||||
| Readonly<ElementsMap>;
|
||||
|
||||
export type ConvertibleGenericTypes = "rectangle" | "diamond" | "ellipse";
|
||||
export type ConvertibleLinearTypes =
|
||||
| "line"
|
||||
| "sharpArrow"
|
||||
| "curvedArrow"
|
||||
| "elbowArrow";
|
||||
export type ConvertibleTypes = ConvertibleGenericTypes | ConvertibleLinearTypes;
|
||||
|
||||
@@ -2,8 +2,6 @@ import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
|
||||
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import { isFrameLikeElement } from "./typeChecks";
|
||||
|
||||
import { getElementsInGroup } from "./groups";
|
||||
@@ -12,6 +10,8 @@ import { syncMovedIndices } from "./fractionalIndex";
|
||||
|
||||
import { getSelectedElements } from "./selection";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
|
||||
import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
|
||||
|
||||
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
isPrimitive,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||
|
||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
|
||||
import type { LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { mutateElement } from "../src/mutateElement";
|
||||
import { duplicateElement, duplicateElements } from "../src/duplicate";
|
||||
|
||||
import type { ExcalidrawLinearElement } from "../src/types";
|
||||
@@ -62,7 +61,7 @@ describe("duplicating single elements", () => {
|
||||
// @ts-ignore
|
||||
element.__proto__ = { hello: "world" };
|
||||
|
||||
mutateElement(element, {
|
||||
mutateElement(element, new Map(), {
|
||||
points: [pointFrom<LocalPoint>(1, 2), pointFrom<LocalPoint>(3, 4)],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ARROW_TYPE } from "@excalidraw/common";
|
||||
import { pointFrom } from "@excalidraw/math";
|
||||
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
|
||||
import Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
import { actionSelectAll } from "@excalidraw/excalidraw/actions";
|
||||
import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
|
||||
|
||||
@@ -23,6 +22,8 @@ import type { LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import { bindLinearElement } from "../src/binding";
|
||||
|
||||
import Scene from "../src/Scene";
|
||||
|
||||
import type {
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
@@ -142,7 +143,7 @@ describe("elbow arrow routing", () => {
|
||||
elbowed: true,
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(arrow);
|
||||
mutateElement(arrow, {
|
||||
h.app.scene.mutateElement(arrow, {
|
||||
points: [
|
||||
pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y),
|
||||
pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y),
|
||||
@@ -187,14 +188,14 @@ describe("elbow arrow routing", () => {
|
||||
scene.insertElement(rectangle1);
|
||||
scene.insertElement(rectangle2);
|
||||
scene.insertElement(arrow);
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
bindLinearElement(arrow, rectangle1, "start", elementsMap);
|
||||
bindLinearElement(arrow, rectangle2, "end", elementsMap);
|
||||
|
||||
bindLinearElement(arrow, rectangle1, "start", scene);
|
||||
bindLinearElement(arrow, rectangle2, "end", scene);
|
||||
|
||||
expect(arrow.startBinding).not.toBe(null);
|
||||
expect(arrow.endBinding).not.toBe(null);
|
||||
|
||||
mutateElement(arrow, {
|
||||
scene.mutateElement(arrow, {
|
||||
points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)],
|
||||
});
|
||||
|
||||
@@ -294,11 +295,11 @@ describe("elbow arrow ui", () => {
|
||||
) as HTMLInputElement;
|
||||
UI.updateInput(inputAngle, String("40"));
|
||||
|
||||
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
||||
expect(arrow.points).toCloselyEqualPoints([
|
||||
[0, 0],
|
||||
[35, 0],
|
||||
[35, 165],
|
||||
[103, 165],
|
||||
[34.7791, 0],
|
||||
[34.7791, 164.67],
|
||||
[102.931, 164.67],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
FractionalIndex,
|
||||
} from "@excalidraw/element/types";
|
||||
@@ -749,7 +750,7 @@ function testInvalidIndicesSync(args: {
|
||||
function prepareArguments(
|
||||
elementsLike: { id: string; index?: string }[],
|
||||
movedElementsIds?: string[],
|
||||
): [ExcalidrawElement[], Map<string, ExcalidrawElement> | undefined] {
|
||||
): [ExcalidrawElement[], ElementsMap | undefined] {
|
||||
const elements = elementsLike.map((x) =>
|
||||
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
|
||||
);
|
||||
@@ -764,7 +765,7 @@ function prepareArguments(
|
||||
function test(
|
||||
name: string,
|
||||
elements: ExcalidrawElement[],
|
||||
movedElements: Map<string, ExcalidrawElement> | undefined,
|
||||
movedElements: ElementsMap | undefined,
|
||||
expectUnchangedElements: Map<string, { id: string }>,
|
||||
expectValidInput?: boolean,
|
||||
) {
|
||||
|
||||
@@ -333,7 +333,7 @@ describe("line element", () => {
|
||||
element,
|
||||
element,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene,
|
||||
"ne",
|
||||
);
|
||||
|
||||
@@ -369,7 +369,7 @@ describe("line element", () => {
|
||||
element,
|
||||
element,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene,
|
||||
"se",
|
||||
);
|
||||
|
||||
@@ -424,7 +424,7 @@ describe("line element", () => {
|
||||
element,
|
||||
element,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.app.scene,
|
||||
"e",
|
||||
{
|
||||
shouldResizeFromCenter: true,
|
||||
@@ -510,12 +510,12 @@ describe("arrow element", () => {
|
||||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
|
||||
UI.resize(rectangle, "se", [-200, -150]);
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
});
|
||||
|
||||
@@ -538,11 +538,11 @@ describe("arrow element", () => {
|
||||
h.state,
|
||||
)[0] as ExcalidrawElbowArrowElement;
|
||||
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||
|
||||
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0);
|
||||
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.05);
|
||||
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||
|
||||
import { mutateElement } from "../src/mutateElement";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { normalizeElementOrder } from "../src/sortElements";
|
||||
|
||||
import type { ExcalidrawElement } from "../src/types";
|
||||
|
||||
const { h } = window;
|
||||
const assertOrder = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
expectedOrder: string[],
|
||||
@@ -35,7 +37,7 @@ describe("normalizeElementsOrder", () => {
|
||||
boundElements: [],
|
||||
});
|
||||
|
||||
mutateElement(container, {
|
||||
mutateElement(container, new Map(), {
|
||||
boundElements: [{ type: "text", id: boundText.id }],
|
||||
});
|
||||
|
||||
@@ -352,7 +354,7 @@ describe("normalizeElementsOrder", () => {
|
||||
containerId: container.id,
|
||||
});
|
||||
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [
|
||||
{ type: "text", id: boundText.id },
|
||||
{ type: "text", id: "xxx" },
|
||||
@@ -387,7 +389,7 @@ describe("normalizeElementsOrder", () => {
|
||||
boundElements: [],
|
||||
groupIds: ["C", "A"],
|
||||
});
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: boundText.id }],
|
||||
});
|
||||
|
||||
|
||||
@@ -50,14 +50,8 @@ const alignSelectedElements = (
|
||||
alignment: Alignment,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const elementsMap = arrayToMap(elements);
|
||||
|
||||
const updatedElements = alignElements(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
alignment,
|
||||
app.scene,
|
||||
);
|
||||
const updatedElements = alignElements(selectedElements, alignment, app.scene);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
isUsingAdaptiveRadius,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { measureText } from "@excalidraw/element/textMeasurements";
|
||||
|
||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||
@@ -43,12 +42,12 @@ import type {
|
||||
|
||||
import type { Mutable } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import { CaptureUpdateAction } from "../store";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { Radians } from "../../math/src";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
|
||||
export const actionUnbindText = register({
|
||||
@@ -80,7 +79,7 @@ export const actionUnbindText = register({
|
||||
boundTextElement,
|
||||
elementsMap,
|
||||
);
|
||||
mutateElement(boundTextElement as ExcalidrawTextElement, {
|
||||
app.scene.mutateElement(boundTextElement as ExcalidrawTextElement, {
|
||||
containerId: null,
|
||||
width,
|
||||
height,
|
||||
@@ -88,7 +87,7 @@ export const actionUnbindText = register({
|
||||
x,
|
||||
y,
|
||||
});
|
||||
mutateElement(element, {
|
||||
app.scene.mutateElement(element, {
|
||||
boundElements: element.boundElements?.filter(
|
||||
(ele) => ele.id !== boundTextElement.id,
|
||||
),
|
||||
@@ -153,25 +152,21 @@ export const actionBindText = register({
|
||||
textElement = selectedElements[1] as ExcalidrawTextElement;
|
||||
container = selectedElements[0] as ExcalidrawTextContainer;
|
||||
}
|
||||
mutateElement(textElement, {
|
||||
app.scene.mutateElement(textElement, {
|
||||
containerId: container.id,
|
||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
autoResize: true,
|
||||
angle: (isArrowElement(container) ? 0 : container?.angle ?? 0) as Radians,
|
||||
});
|
||||
mutateElement(container, {
|
||||
app.scene.mutateElement(container, {
|
||||
boundElements: (container.boundElements || []).concat({
|
||||
type: "text",
|
||||
id: textElement.id,
|
||||
}),
|
||||
});
|
||||
const originalContainerHeight = container.height;
|
||||
redrawTextBoundingBox(
|
||||
textElement,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
redrawTextBoundingBox(textElement, container, app.scene);
|
||||
// overwritting the cache with original container height so
|
||||
// it can be restored when unbind
|
||||
updateOriginalContainerCache(container.id, originalContainerHeight);
|
||||
@@ -301,27 +296,23 @@ export const actionWrapTextInContainer = register({
|
||||
}
|
||||
|
||||
if (startBinding || endBinding) {
|
||||
mutateElement(ele, { startBinding, endBinding }, false);
|
||||
app.scene.mutateElement(ele, {
|
||||
startBinding,
|
||||
endBinding,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
textElement,
|
||||
{
|
||||
containerId: container.id,
|
||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
boundElements: null,
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
autoResize: true,
|
||||
},
|
||||
false,
|
||||
);
|
||||
redrawTextBoundingBox(
|
||||
textElement,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
app.scene.mutateElement(textElement, {
|
||||
containerId: container.id,
|
||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
boundElements: null,
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
autoResize: true,
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(textElement, container, app.scene);
|
||||
|
||||
updatedElements = pushContainerBelowText(
|
||||
[...updatedElements, container],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { Excalidraw, mutateElement } from "../index";
|
||||
import { Excalidraw } from "../index";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { act, assertElements, render } from "../tests/test-utils";
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||
frameId: f1.id,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
h.app.scene.mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
h.app.scene.mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(r1, {
|
||||
h.app.scene.mutateElement(r1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("deleting selected elements when frame selected should keep children +
|
||||
frameId: null,
|
||||
});
|
||||
|
||||
mutateElement(a1, {
|
||||
h.app.scene.mutateElement(a1, {
|
||||
boundElements: [{ type: "text", id: t1.id }],
|
||||
});
|
||||
|
||||
|
||||
@@ -3,10 +3,7 @@ import { KEYS, updateActiveTool } from "@excalidraw/common";
|
||||
import { getNonDeletedElements } from "@excalidraw/element";
|
||||
import { fixBindingsAfterDeletion } from "@excalidraw/element/binding";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
@@ -94,7 +91,7 @@ const deleteSelectedElements = (
|
||||
el.boundElements.forEach((candidate) => {
|
||||
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
|
||||
if (bound && isElbowArrow(bound)) {
|
||||
mutateElement(bound, {
|
||||
app.scene.mutateElement(bound, {
|
||||
startBinding:
|
||||
el.id === bound.startBinding?.elementId
|
||||
? null
|
||||
@@ -102,7 +99,6 @@ const deleteSelectedElements = (
|
||||
endBinding:
|
||||
el.id === bound.endBinding?.elementId ? null : bound.endBinding,
|
||||
});
|
||||
mutateElement(bound, { points: bound.points });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -261,7 +257,11 @@ export const actionDeleteSelected = register({
|
||||
: endBindingElement,
|
||||
};
|
||||
|
||||
LinearElementEditor.deletePoints(element, selectedPointsIndices);
|
||||
LinearElementEditor.deletePoints(
|
||||
element,
|
||||
app.scene,
|
||||
selectedPointsIndices,
|
||||
);
|
||||
|
||||
return {
|
||||
elements,
|
||||
|
||||
@@ -43,7 +43,7 @@ export const actionDuplicateSelection = register({
|
||||
try {
|
||||
const newAppState = LinearElementEditor.duplicateSelectedPoints(
|
||||
appState,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
bindOrUnbindLinearElement,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
isBindingElement,
|
||||
isLinearElement,
|
||||
@@ -46,7 +46,6 @@ export const actionFinalize = register({
|
||||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
@@ -72,7 +71,11 @@ export const actionFinalize = register({
|
||||
scene.getElement(appState.pendingImageElementId);
|
||||
|
||||
if (pendingImageElement) {
|
||||
mutateElement(pendingImageElement, { isDeleted: true }, false);
|
||||
scene.mutateElement(
|
||||
pendingImageElement,
|
||||
{ isDeleted: true },
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
}
|
||||
|
||||
if (window.document.activeElement instanceof HTMLElement) {
|
||||
@@ -96,7 +99,7 @@ export const actionFinalize = register({
|
||||
!lastCommittedPoint ||
|
||||
points[points.length - 1] !== lastCommittedPoint
|
||||
) {
|
||||
mutateElement(multiPointElement, {
|
||||
scene.mutateElement(multiPointElement, {
|
||||
points: multiPointElement.points.slice(0, -1),
|
||||
});
|
||||
}
|
||||
@@ -120,7 +123,7 @@ export const actionFinalize = register({
|
||||
if (isLoop) {
|
||||
const linePoints = multiPointElement.points;
|
||||
const firstPoint = linePoints[0];
|
||||
mutateElement(multiPointElement, {
|
||||
scene.mutateElement(multiPointElement, {
|
||||
points: linePoints.map((p, index) =>
|
||||
index === linePoints.length - 1
|
||||
? pointFrom(firstPoint[0], firstPoint[1])
|
||||
@@ -140,13 +143,7 @@ export const actionFinalize = register({
|
||||
-1,
|
||||
arrayToMap(elements),
|
||||
);
|
||||
maybeBindLinearElement(
|
||||
multiPointElement,
|
||||
appState,
|
||||
{ x, y },
|
||||
elementsMap,
|
||||
elements,
|
||||
);
|
||||
maybeBindLinearElement(multiPointElement, appState, { x, y }, scene);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +199,10 @@ export const actionFinalize = register({
|
||||
// To select the linear element when user has finished mutipoint editing
|
||||
selectedLinearElement:
|
||||
multiPointElement && isLinearElement(multiPointElement)
|
||||
? new LinearElementEditor(multiPointElement)
|
||||
? new LinearElementEditor(
|
||||
multiPointElement,
|
||||
arrayToMap(newElements),
|
||||
)
|
||||
: appState.selectedLinearElement,
|
||||
pendingImageElementId: null,
|
||||
},
|
||||
|
||||
@@ -73,12 +73,12 @@ describe("flipping re-centers selection", () => {
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
|
||||
const rec1 = h.elements.find((el) => el.id === "rec1")!;
|
||||
expect(rec1.x).toBeCloseTo(100, 0);
|
||||
expect(rec1.y).toBeCloseTo(100, 0);
|
||||
expect(rec1.x).toBeCloseTo(97.8678, 0);
|
||||
expect(rec1.y).toBeCloseTo(97.444, 0);
|
||||
|
||||
const rec2 = h.elements.find((el) => el.id === "rec2")!;
|
||||
expect(rec2.x).toBeCloseTo(220, 0);
|
||||
expect(rec2.y).toBeCloseTo(250, 0);
|
||||
expect(rec2.x).toBeCloseTo(218, 0);
|
||||
expect(rec2.y).toBeCloseTo(247, 0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
isBindingEnabled,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
|
||||
import {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||
import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
|
||||
import {
|
||||
@@ -162,11 +159,9 @@ const flipElements = (
|
||||
|
||||
bindOrUnbindLinearElements(
|
||||
selectedElements.filter(isLinearElement),
|
||||
elementsMap,
|
||||
app.scene.getNonDeletedElements(),
|
||||
app.scene,
|
||||
isBindingEnabled(appState),
|
||||
[],
|
||||
app.scene,
|
||||
appState.zoom,
|
||||
);
|
||||
|
||||
@@ -194,13 +189,13 @@ const flipElements = (
|
||||
getCommonBoundingBox(selectedElements);
|
||||
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||
otherElements.forEach((element) =>
|
||||
mutateElement(element, {
|
||||
app.scene.mutateElement(element, {
|
||||
x: element.x + diffX,
|
||||
y: element.y + diffY,
|
||||
}),
|
||||
);
|
||||
elbowArrows.forEach((element) =>
|
||||
mutateElement(element, {
|
||||
app.scene.mutateElement(element, {
|
||||
x: element.x + diffX,
|
||||
y: element.y + diffY,
|
||||
}),
|
||||
|
||||
@@ -173,11 +173,9 @@ export const actionWrapSelectionInFrame = register({
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
|
||||
const [x1, y1, x2, y2] = getCommonBounds(
|
||||
selectedElements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements, elementsMap);
|
||||
const PADDING = 16;
|
||||
const frame = newFrameElement({
|
||||
x: x1 - PADDING,
|
||||
@@ -196,13 +194,9 @@ export const actionWrapSelectionInFrame = register({
|
||||
for (const elementInGroup of elementsInGroup) {
|
||||
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
|
||||
|
||||
mutateElement(
|
||||
elementInGroup,
|
||||
{
|
||||
groupIds: elementInGroup.groupIds.slice(0, index),
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(elementInGroup, elementsMap, {
|
||||
groupIds: elementInGroup.groupIds.slice(0, index),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { arrayToMap } from "@excalidraw/common";
|
||||
|
||||
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
|
||||
|
||||
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
||||
@@ -50,7 +52,7 @@ export const actionToggleLinearEditor = register({
|
||||
const editingLinearElement =
|
||||
appState.editingLinearElement?.elementId === selectedElement.id
|
||||
? null
|
||||
: new LinearElementEditor(selectedElement);
|
||||
: new LinearElementEditor(selectedElement, arrayToMap(elements));
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
|
||||
@@ -34,10 +34,7 @@ import {
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
getBoundTextElement,
|
||||
@@ -61,6 +58,7 @@ import type { LocalPoint } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
Arrowhead,
|
||||
ElementsMap,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
@@ -68,9 +66,10 @@ import type {
|
||||
FontFamilyValues,
|
||||
TextAlign,
|
||||
VerticalAlign,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { trackEvent } from "../analytics";
|
||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
|
||||
@@ -207,25 +206,22 @@ export const getFormValue = function <T extends Primitive>(
|
||||
const offsetElementAfterFontResize = (
|
||||
prevElement: ExcalidrawTextElement,
|
||||
nextElement: ExcalidrawTextElement,
|
||||
scene: Scene,
|
||||
) => {
|
||||
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
|
||||
return nextElement;
|
||||
}
|
||||
return mutateElement(
|
||||
nextElement,
|
||||
{
|
||||
x:
|
||||
prevElement.textAlign === "left"
|
||||
? prevElement.x
|
||||
: prevElement.x +
|
||||
(prevElement.width - nextElement.width) /
|
||||
(prevElement.textAlign === "center" ? 2 : 1),
|
||||
// centering vertically is non-standard, but for Excalidraw I think
|
||||
// it makes sense
|
||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
||||
},
|
||||
false,
|
||||
);
|
||||
return scene.mutateElement(nextElement, {
|
||||
x:
|
||||
prevElement.textAlign === "left"
|
||||
? prevElement.x
|
||||
: prevElement.x +
|
||||
(prevElement.width - nextElement.width) /
|
||||
(prevElement.textAlign === "center" ? 2 : 1),
|
||||
// centering vertically is non-standard, but for Excalidraw I think
|
||||
// it makes sense
|
||||
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
|
||||
});
|
||||
};
|
||||
|
||||
const changeFontSize = (
|
||||
@@ -251,10 +247,14 @@ const changeFontSize = (
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
|
||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||
newElement = offsetElementAfterFontResize(
|
||||
oldElement,
|
||||
newElement,
|
||||
app.scene,
|
||||
);
|
||||
|
||||
return newElement;
|
||||
}
|
||||
@@ -264,15 +264,11 @@ const changeFontSize = (
|
||||
);
|
||||
|
||||
// Update arrow elements after text elements have been updated
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
getSelectedElements(elements, appState, {
|
||||
includeBoundTextElement: true,
|
||||
}).forEach((element) => {
|
||||
if (isTextElement(element)) {
|
||||
updateBoundElements(
|
||||
element,
|
||||
updatedElementsMap as NonDeletedSceneElementsMap,
|
||||
);
|
||||
updateBoundElements(element, app.scene);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -778,7 +774,7 @@ type ChangeFontFamilyData = Partial<
|
||||
>
|
||||
> & {
|
||||
/** cache of selected & editing elements populated on opened popup */
|
||||
cachedElements?: Map<string, ExcalidrawElement>;
|
||||
cachedElements?: ElementsMap;
|
||||
/** flag to reset all elements to their cached versions */
|
||||
resetAll?: true;
|
||||
/** flag to reset all containers to their cached versions */
|
||||
@@ -919,7 +915,7 @@ export const actionChangeFontFamily = register({
|
||||
|
||||
if (resetContainers && container && cachedContainer) {
|
||||
// reset the container back to it's cached version
|
||||
mutateElement(container, { ...cachedContainer }, false);
|
||||
app.scene.mutateElement(container, { ...cachedContainer });
|
||||
}
|
||||
|
||||
if (!skipFontFaceCheck) {
|
||||
@@ -950,12 +946,7 @@ export const actionChangeFontFamily = register({
|
||||
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
||||
for (const [element, container] of elementContainerMapping) {
|
||||
// trigger synchronous redraw
|
||||
redrawTextBoundingBox(
|
||||
element,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
false,
|
||||
);
|
||||
redrawTextBoundingBox(element, container, app.scene);
|
||||
}
|
||||
} else {
|
||||
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
|
||||
@@ -972,8 +963,7 @@ export const actionChangeFontFamily = register({
|
||||
redrawTextBoundingBox(
|
||||
latestElement as ExcalidrawTextElement,
|
||||
latestContainer,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
false,
|
||||
app.scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -987,7 +977,7 @@ export const actionChangeFontFamily = register({
|
||||
return result;
|
||||
},
|
||||
PanelComponent: ({ elements, appState, app, updateData }) => {
|
||||
const cachedElementsRef = useRef<Map<string, ExcalidrawElement>>(new Map());
|
||||
const cachedElementsRef = useRef<ElementsMap>(new Map());
|
||||
const prevSelectedFontFamilyRef = useRef<number | null>(null);
|
||||
// relying on state batching as multiple `FontPicker` handlers could be called in rapid succession and we want to combine them
|
||||
const [batchedData, setBatchedData] = useState<ChangeFontFamilyData>({});
|
||||
@@ -996,7 +986,7 @@ export const actionChangeFontFamily = register({
|
||||
const selectedFontFamily = useMemo(() => {
|
||||
const getFontFamily = (
|
||||
elementsArray: readonly ExcalidrawElement[],
|
||||
elementsMap: Map<string, ExcalidrawElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) =>
|
||||
getFormValue(
|
||||
elementsArray,
|
||||
@@ -1179,7 +1169,7 @@ export const actionChangeTextAlign = register({
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
@@ -1270,7 +1260,7 @@ export const actionChangeVerticalAlign = register({
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
@@ -1670,10 +1660,10 @@ export const actionChangeArrowType = register({
|
||||
newElement,
|
||||
startHoveredElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
app.scene,
|
||||
);
|
||||
endHoveredElement &&
|
||||
bindLinearElement(newElement, endHoveredElement, "end", elementsMap);
|
||||
bindLinearElement(newElement, endHoveredElement, "end", app.scene);
|
||||
|
||||
const startBinding =
|
||||
startElement && newElement.startBinding
|
||||
@@ -1684,7 +1674,6 @@ export const actionChangeArrowType = register({
|
||||
newElement,
|
||||
startElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
@@ -1697,7 +1686,6 @@ export const actionChangeArrowType = register({
|
||||
newElement,
|
||||
endElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
@@ -1729,7 +1717,7 @@ export const actionChangeArrowType = register({
|
||||
newElement.startBinding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (startElement) {
|
||||
bindLinearElement(newElement, startElement, "start", elementsMap);
|
||||
bindLinearElement(newElement, startElement, "start", app.scene);
|
||||
}
|
||||
}
|
||||
if (newElement.endBinding) {
|
||||
@@ -1737,7 +1725,7 @@ export const actionChangeArrowType = register({
|
||||
newElement.endBinding.elementId,
|
||||
) as ExcalidrawBindableElement;
|
||||
if (endElement) {
|
||||
bindLinearElement(newElement, endElement, "end", elementsMap);
|
||||
bindLinearElement(newElement, endElement, "end", app.scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1758,6 +1746,7 @@ export const actionChangeArrowType = register({
|
||||
if (selected) {
|
||||
newState.selectedLinearElement = new LinearElementEditor(
|
||||
selected as ExcalidrawLinearElement,
|
||||
arrayToMap(elements),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getNonDeletedElements } from "@excalidraw/element";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
import { arrayToMap, KEYS } from "@excalidraw/common";
|
||||
|
||||
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
|
||||
|
||||
@@ -53,7 +53,7 @@ export const actionSelectAll = register({
|
||||
// single linear element selected
|
||||
Object.keys(selectedElementIds).length === 1 &&
|
||||
isLinearElement(elements[0])
|
||||
? new LinearElementEditor(elements[0])
|
||||
? new LinearElementEditor(elements[0], arrayToMap(elements))
|
||||
: null,
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
|
||||
@@ -139,11 +139,8 @@ export const actionPasteStyles = register({
|
||||
element.id === newElement.containerId,
|
||||
) || null;
|
||||
}
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
redrawTextBoundingBox(newElement, container, app.scene);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import {
|
||||
getConversionTypeFromElements,
|
||||
convertElementTypePopupAtom,
|
||||
} from "../components/ConvertElementTypePopup";
|
||||
import { editorJotaiStore } from "../editor-jotai";
|
||||
import { CaptureUpdateAction } from "../store";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionToggleShapeSwitch = register({
|
||||
name: "toggleShapeSwitch",
|
||||
label: "labels.shapeSwitch",
|
||||
icon: () => null,
|
||||
viewMode: true,
|
||||
trackEvent: {
|
||||
category: "shape_switch",
|
||||
action: "toggle",
|
||||
},
|
||||
keywords: ["change", "switch", "swap"],
|
||||
perform(elements, appState, _, app) {
|
||||
editorJotaiStore.set(convertElementTypePopupAtom, {
|
||||
type: "panel",
|
||||
});
|
||||
|
||||
return {
|
||||
captureUpdate: CaptureUpdateAction.NEVER,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.gridModeEnabled,
|
||||
predicate: (elements, appState, props) =>
|
||||
getConversionTypeFromElements(elements as ExcalidrawElement[]) !== null,
|
||||
});
|
||||
@@ -140,7 +140,8 @@ export type ActionName =
|
||||
| "linkToElement"
|
||||
| "cropEditor"
|
||||
| "wrapSelectionInFrame"
|
||||
| "toggleLassoTool";
|
||||
| "toggleLassoTool"
|
||||
| "toggleShapeSwitch";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
@@ -195,7 +196,8 @@ export interface Action {
|
||||
| "menu"
|
||||
| "collab"
|
||||
| "hyperlink"
|
||||
| "search_menu";
|
||||
| "search_menu"
|
||||
| "shape_switch";
|
||||
action?: string;
|
||||
predicate?: (
|
||||
appState: Readonly<AppState>,
|
||||
|
||||
@@ -129,7 +129,6 @@ export class AnimatedTrail implements Trail {
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.pastTrails = [];
|
||||
this.start();
|
||||
if (this.trailAnimation) {
|
||||
this.trailAnimation.setAttribute("begin", "indefinite");
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
syncMovedIndices,
|
||||
} from "@excalidraw/element/fractionalIndex";
|
||||
|
||||
import Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
|
||||
|
||||
import type { ElementUpdate } from "@excalidraw/element/mutateElement";
|
||||
@@ -490,6 +492,7 @@ export class AppStateChange implements Change<AppState> {
|
||||
nextElements.get(
|
||||
selectedLinearElementId,
|
||||
) as NonDeleted<ExcalidrawLinearElement>,
|
||||
nextElements,
|
||||
)
|
||||
: null;
|
||||
|
||||
@@ -499,6 +502,7 @@ export class AppStateChange implements Change<AppState> {
|
||||
nextElements.get(
|
||||
editingLinearElementId,
|
||||
) as NonDeleted<ExcalidrawLinearElement>,
|
||||
nextElements,
|
||||
)
|
||||
: null;
|
||||
|
||||
@@ -1132,9 +1136,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
|
||||
ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements);
|
||||
|
||||
// the following reorder performs also mutations, but only on new instances of changed elements
|
||||
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
||||
nextElements = ElementsChange.reorderElements(
|
||||
@@ -1143,8 +1144,14 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||
flags,
|
||||
);
|
||||
|
||||
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
||||
// we also don't have a scene on the server
|
||||
// so we are creating a temp scene just to query and mutate elements
|
||||
const tempScene = new Scene(nextElements);
|
||||
|
||||
ElementsChange.redrawTextBoundingBoxes(tempScene, changedElements);
|
||||
// Need ordered nextElements to avoid z-index binding issues
|
||||
ElementsChange.redrawBoundArrows(nextElements, changedElements);
|
||||
ElementsChange.redrawBoundArrows(tempScene, changedElements);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Couldn't mutate elements after applying elements change`,
|
||||
@@ -1337,8 +1344,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||
} else {
|
||||
affectedElement = mutateElement(
|
||||
nextElement,
|
||||
nextElements,
|
||||
updates as ElementUpdate<OrderedExcalidrawElement>,
|
||||
);
|
||||
) as OrderedExcalidrawElement;
|
||||
}
|
||||
|
||||
nextAffectedElements.set(affectedElement.id, affectedElement);
|
||||
@@ -1456,9 +1464,10 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||
}
|
||||
|
||||
private static redrawTextBoundingBoxes(
|
||||
elements: SceneElementsMap,
|
||||
scene: Scene,
|
||||
changed: Map<string, OrderedExcalidrawElement>,
|
||||
) {
|
||||
const elements = scene.getNonDeletedElementsMap();
|
||||
const boxesToRedraw = new Map<
|
||||
string,
|
||||
{ container: OrderedExcalidrawElement; boundText: ExcalidrawTextElement }
|
||||
@@ -1498,17 +1507,17 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
||||
continue;
|
||||
}
|
||||
|
||||
redrawTextBoundingBox(boundText, container, elements, false);
|
||||
redrawTextBoundingBox(boundText, container, scene);
|
||||
}
|
||||
}
|
||||
|
||||
private static redrawBoundArrows(
|
||||
elements: SceneElementsMap,
|
||||
scene: Scene,
|
||||
changed: Map<string, OrderedExcalidrawElement>,
|
||||
) {
|
||||
for (const element of changed.values()) {
|
||||
if (!element.isDeleted && isBindableElement(element)) {
|
||||
updateBoundElements(element, elements, {
|
||||
updateBoundElements(element, scene, {
|
||||
changedElements: changed,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export const serializeAsClipboardJSON = ({
|
||||
!framesToCopy.has(getContainingFrame(element, elementsMap)!)
|
||||
) {
|
||||
const copiedElement = deepCopyElement(element);
|
||||
mutateElement(copiedElement, {
|
||||
mutateElement(copiedElement, elementsMap, {
|
||||
frameId: null,
|
||||
});
|
||||
return copiedElement;
|
||||
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
arrayToMap,
|
||||
type EXPORT_IMAGE_TYPES,
|
||||
randomInteger,
|
||||
CLASSES,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
@@ -122,10 +123,7 @@ import {
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import {
|
||||
mutateElement,
|
||||
newElementWith,
|
||||
} from "@excalidraw/element/mutateElement";
|
||||
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
newFrameElement,
|
||||
@@ -303,6 +301,10 @@ import {
|
||||
|
||||
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 {
|
||||
@@ -328,9 +330,10 @@ import type {
|
||||
MagicGenerationData,
|
||||
ExcalidrawNonSelectionElement,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import {
|
||||
actionAddToLibrary,
|
||||
@@ -403,7 +406,6 @@ import {
|
||||
hasBackground,
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { getStateForZoom } from "../scene/zoom";
|
||||
import {
|
||||
dataURLToFile,
|
||||
@@ -430,7 +432,7 @@ import {
|
||||
} from "../components/hyperlink/Hyperlink";
|
||||
|
||||
import { Fonts } from "../fonts";
|
||||
import { editorJotaiStore } from "../editor-jotai";
|
||||
import { editorJotaiStore, type WritableAtom } from "../editor-jotai";
|
||||
import { ImageSceneDataError } from "../errors";
|
||||
import {
|
||||
getSnapLinesAtPointer,
|
||||
@@ -466,6 +468,12 @@ import { LassoTrail } from "../lasso";
|
||||
|
||||
import { EraserTrail } from "../eraser";
|
||||
|
||||
import ConvertElementTypePopup, {
|
||||
getConversionTypeFromElements,
|
||||
convertElementTypePopupAtom,
|
||||
convertElementTypes,
|
||||
} from "./ConvertElementTypePopup";
|
||||
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||
@@ -497,7 +505,6 @@ import type { ExportedElements } from "../data";
|
||||
import type { ContextMenuItems } from "./ContextMenu";
|
||||
import type { FileSystemHandle } from "../data/filesystem";
|
||||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
||||
|
||||
import type {
|
||||
AppClassProperties,
|
||||
AppProps,
|
||||
@@ -760,6 +767,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (excalidrawAPI) {
|
||||
const api: ExcalidrawImperativeAPI = {
|
||||
updateScene: this.updateScene,
|
||||
mutateElement: this.mutateElement,
|
||||
updateLibrary: this.library.updateLibrary,
|
||||
addFiles: this.addFiles,
|
||||
resetScene: this.resetScene,
|
||||
@@ -813,6 +821,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
}
|
||||
|
||||
updateEditorAtom = <Value, Args extends unknown[], Result>(
|
||||
atom: WritableAtom<Value, Args, Result>,
|
||||
...args: Args
|
||||
): Result => {
|
||||
const result = editorJotaiStore.set(atom, ...args);
|
||||
this.triggerRender();
|
||||
return result;
|
||||
};
|
||||
|
||||
private onWindowMessage(event: MessageEvent) {
|
||||
if (
|
||||
event.origin !== "https://player.vimeo.com" &&
|
||||
@@ -1387,7 +1404,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
private resetEditingFrame = (frame: ExcalidrawFrameLikeElement | null) => {
|
||||
if (frame) {
|
||||
mutateElement(frame, { name: frame.name?.trim() || null });
|
||||
this.scene.mutateElement(frame, { name: frame.name?.trim() || null });
|
||||
}
|
||||
this.setState({ editingFrame: null });
|
||||
};
|
||||
@@ -1444,7 +1461,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
autoFocus
|
||||
value={frameNameInEdit}
|
||||
onChange={(e) => {
|
||||
mutateElement(f, {
|
||||
this.scene.mutateElement(f, {
|
||||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
@@ -1581,6 +1598,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const firstSelectedElement = selectedElements[0];
|
||||
|
||||
const showShapeSwitchPanel =
|
||||
editorJotaiStore.get(convertElementTypePopupAtom)?.type === "panel";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("excalidraw excalidraw-container", {
|
||||
@@ -1670,7 +1690,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
<Hyperlink
|
||||
key={firstSelectedElement.id}
|
||||
element={firstSelectedElement}
|
||||
elementsMap={allElementsMap}
|
||||
scene={this.scene}
|
||||
setAppState={this.setAppState}
|
||||
onLinkOpen={this.props.onLinkOpen}
|
||||
setToast={this.setToast}
|
||||
@@ -1855,6 +1875,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/>
|
||||
)}
|
||||
{this.renderFrameNames()}
|
||||
{showShapeSwitchPanel && (
|
||||
<ConvertElementTypePopup app={this} />
|
||||
)}
|
||||
</ExcalidrawActionManagerContext.Provider>
|
||||
{this.renderEmbeddables()}
|
||||
</ExcalidrawElementsContext.Provider>
|
||||
@@ -1938,16 +1961,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// state only.
|
||||
// Thus reset so that we prefer local cache (if there was some
|
||||
// generationData set previously)
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
frameElement,
|
||||
{ customData: { generationData: undefined } },
|
||||
false,
|
||||
{
|
||||
customData: { generationData: undefined },
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
} else {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
frameElement,
|
||||
{ customData: { generationData: data } },
|
||||
false,
|
||||
{
|
||||
customData: { generationData: data },
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
}
|
||||
this.magicGenerations.set(frameElement.id, data);
|
||||
@@ -2119,7 +2146,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.insertElement(frame);
|
||||
|
||||
for (const child of selectedElements) {
|
||||
mutateElement(child, { frameId: frame.id });
|
||||
this.scene.mutateElement(child, { frameId: frame.id });
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -2132,7 +2159,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
||||
editorJotaiStore.set(activeEyeDropperAtom, {
|
||||
this.updateEditorAtom(activeEyeDropperAtom, {
|
||||
swapPreviewOnAlt: true,
|
||||
colorPickerType:
|
||||
type === "stroke" ? "elementStroke" : "elementBackground",
|
||||
@@ -2918,8 +2945,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
nonDeletedElementsMap,
|
||||
),
|
||||
),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3317,11 +3343,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
newElement,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
container,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
redrawTextBoundingBox(newElement, container, this.scene);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3444,7 +3466,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
// hack to reset the `y` coord because we vertically center during
|
||||
// insertImageElement
|
||||
mutateElement(initializedImageElement, { y }, false);
|
||||
this.scene.mutateElement(
|
||||
initializedImageElement,
|
||||
{ y },
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
|
||||
y = imageElement.y + imageElement.height + 25;
|
||||
|
||||
@@ -3998,6 +4024,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 = (
|
||||
/** force always re-renders canvas even if no change */
|
||||
force?: boolean,
|
||||
@@ -4141,6 +4178,40 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Shape switching
|
||||
if (event.key === KEYS.ESCAPE) {
|
||||
this.updateEditorAtom(convertElementTypePopupAtom, null);
|
||||
} else if (
|
||||
event.key === KEYS.TAB &&
|
||||
(document.activeElement === this.excalidrawContainerRef?.current ||
|
||||
document.activeElement?.classList.contains(
|
||||
CLASSES.CONVERT_ELEMENT_TYPE_POPUP,
|
||||
))
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
const conversionType =
|
||||
getConversionTypeFromElements(selectedElements);
|
||||
|
||||
if (
|
||||
editorJotaiStore.get(convertElementTypePopupAtom)?.type === "panel"
|
||||
) {
|
||||
if (
|
||||
convertElementTypes(this, {
|
||||
conversionType,
|
||||
direction: event.shiftKey ? "left" : "right",
|
||||
})
|
||||
) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
}
|
||||
}
|
||||
if (conversionType) {
|
||||
this.updateEditorAtom(convertElementTypePopupAtom, {
|
||||
type: "panel",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === KEYS.ESCAPE &&
|
||||
this.flowChartCreator.isCreatingChart
|
||||
@@ -4166,9 +4237,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
this.flowChartCreator.createNodes(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
getLinkDirectionFromKey(event.key),
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4410,16 +4481,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
selectedElements.forEach((element) => {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
element,
|
||||
{
|
||||
x: element.x + offsetX,
|
||||
y: element.y + offsetY,
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
|
||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
|
||||
updateBoundElements(element, this.scene, {
|
||||
simultaneouslyUpdated: selectedElements,
|
||||
});
|
||||
});
|
||||
@@ -4453,6 +4524,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -4598,7 +4670,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
event[KEYS.CTRL_OR_CMD] &&
|
||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
||||
) {
|
||||
editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
||||
this.updateEditorAtom(activeConfirmDialogAtom, "clearCanvas");
|
||||
}
|
||||
|
||||
// eye dropper
|
||||
@@ -4646,11 +4718,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (isArrowKey(event.key)) {
|
||||
bindOrUnbindLinearElements(
|
||||
this.scene.getSelectedElements(this.state).filter(isLinearElement),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
isBindingEnabled(this.state),
|
||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
this.scene,
|
||||
this.state.zoom,
|
||||
);
|
||||
this.setState({ suggestedBindings: [] });
|
||||
@@ -4957,7 +5027,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
onChange: withBatchedUpdates((nextOriginalText) => {
|
||||
updateElement(nextOriginalText, false);
|
||||
if (isNonDeletedElement(element)) {
|
||||
updateBoundElements(element, this.scene.getNonDeletedElementsMap());
|
||||
updateBoundElements(element, this.scene);
|
||||
}
|
||||
}),
|
||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||
@@ -5320,7 +5390,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
|
||||
const newHeight = Math.max(container.height, minHeight);
|
||||
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;
|
||||
sceneY = container.y + newHeight / 2;
|
||||
if (parentCenterPosition) {
|
||||
@@ -5371,7 +5444,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
|
||||
if (!existingTextElement && shouldBindToContainer && container) {
|
||||
mutateElement(container, {
|
||||
this.scene.mutateElement(container, {
|
||||
boundElements: (container.boundElements || []).concat({
|
||||
type: "text",
|
||||
id: element.id,
|
||||
@@ -5447,7 +5520,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
this.setState({
|
||||
editingLinearElement: new LinearElementEditor(selectedElements[0]),
|
||||
editingLinearElement: new LinearElementEditor(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
return;
|
||||
} else if (
|
||||
@@ -5471,7 +5547,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
if (midPoint && midPoint > -1) {
|
||||
this.store.shouldCaptureIncrement();
|
||||
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
|
||||
LinearElementEditor.deleteFixedSegment(
|
||||
selectedElements[0],
|
||||
this.scene,
|
||||
midPoint,
|
||||
);
|
||||
|
||||
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
|
||||
{
|
||||
@@ -5854,7 +5934,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -5916,7 +5995,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
lastPoint,
|
||||
) >= LINE_CONFIRM_THRESHOLD
|
||||
) {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
multiElement,
|
||||
{
|
||||
points: [
|
||||
@@ -5924,7 +6003,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointFrom<LocalPoint>(scenePointerX - rx, scenePointerY - ry),
|
||||
],
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
} else {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
@@ -5940,12 +6019,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) < LINE_CONFIRM_THRESHOLD
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
multiElement,
|
||||
{
|
||||
points: points.slice(0, -1),
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
} else {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
@@ -5977,8 +6056,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (isPathALoop(points, this.state.zoom.value)) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
}
|
||||
|
||||
// update last uncommitted point
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
multiElement,
|
||||
{
|
||||
points: [
|
||||
@@ -5989,9 +6069,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
],
|
||||
},
|
||||
false,
|
||||
{
|
||||
isDragging: true,
|
||||
informMutation: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6339,7 +6419,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
focus: false,
|
||||
})),
|
||||
}));
|
||||
editorJotaiStore.set(searchItemInFocusAtom, null);
|
||||
this.updateEditorAtom(searchItemInFocusAtom, null);
|
||||
}
|
||||
|
||||
if (editorJotaiStore.get(convertElementTypePopupAtom)) {
|
||||
this.updateEditorAtom(convertElementTypePopupAtom, null);
|
||||
}
|
||||
|
||||
// since contextMenu options are potentially evaluated on each render,
|
||||
@@ -6578,7 +6662,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||
|
||||
mutateElement(pendingImageElement, {
|
||||
this.scene.mutateElement(pendingImageElement, {
|
||||
x,
|
||||
y,
|
||||
frameId: frame ? frame.id : null,
|
||||
@@ -7633,7 +7717,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
multiElement.type === "line" &&
|
||||
isPathALoop(multiElement.points, this.state.zoom.value)
|
||||
) {
|
||||
mutateElement(multiElement, {
|
||||
this.scene.mutateElement(multiElement, {
|
||||
lastCommittedPoint:
|
||||
multiElement.points[multiElement.points.length - 1],
|
||||
});
|
||||
@@ -7644,7 +7728,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// Elbow arrows cannot be created by putting down points
|
||||
// only the start and end points can be defined
|
||||
if (isElbowArrow(multiElement) && multiElement.points.length > 1) {
|
||||
mutateElement(multiElement, {
|
||||
this.scene.mutateElement(multiElement, {
|
||||
lastCommittedPoint:
|
||||
multiElement.points[multiElement.points.length - 1],
|
||||
});
|
||||
@@ -7681,7 +7765,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}));
|
||||
// clicking outside commit zone → update reference for last committed
|
||||
// point
|
||||
mutateElement(multiElement, {
|
||||
this.scene.mutateElement(multiElement, {
|
||||
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
||||
});
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
@@ -7767,7 +7851,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
};
|
||||
});
|
||||
mutateElement(element, {
|
||||
this.scene.mutateElement(element, {
|
||||
points: [...element.points, pointFrom<LocalPoint>(0, 0)],
|
||||
});
|
||||
const boundElement = getHoveredElementForBinding(
|
||||
@@ -8017,7 +8101,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
index,
|
||||
gridX,
|
||||
gridY,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene,
|
||||
);
|
||||
|
||||
flushSync(() => {
|
||||
@@ -8122,7 +8206,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerCoords,
|
||||
this,
|
||||
!event[KEYS.CTRL_OR_CMD],
|
||||
elementsMap,
|
||||
this.scene,
|
||||
);
|
||||
if (!ret) {
|
||||
return;
|
||||
@@ -8349,7 +8433,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
};
|
||||
|
||||
mutateElement(croppingElement, {
|
||||
this.scene.mutateElement(croppingElement, {
|
||||
crop: nextCrop,
|
||||
});
|
||||
|
||||
@@ -8606,13 +8690,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? newElement.pressures
|
||||
: [...newElement.pressures, event.pressure];
|
||||
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
pressures,
|
||||
},
|
||||
false,
|
||||
{
|
||||
informMutation: false,
|
||||
isDragging: false,
|
||||
},
|
||||
);
|
||||
|
||||
this.setState({
|
||||
@@ -8635,24 +8722,23 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
if (points.length === 1) {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
} else if (
|
||||
points.length === 2 ||
|
||||
(points.length > 1 && isElbowArrow(newElement))
|
||||
) {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [...points.slice(0, -1), pointFrom<LocalPoint>(dx, dy)],
|
||||
},
|
||||
false,
|
||||
{ isDragging: true },
|
||||
{ isDragging: true, informMutation: false },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8763,7 +8849,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
selectedLinearElement:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
isLinearElement(elementsWithinSelection[0])
|
||||
? new LinearElementEditor(elementsWithinSelection[0])
|
||||
? new LinearElementEditor(
|
||||
elementsWithinSelection[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: null,
|
||||
showHyperlinkPopup:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
@@ -8869,7 +8958,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
.map((e) => elementsMap.get(e.id))
|
||||
.filter((e) => isElbowArrow(e))
|
||||
.forEach((e) => {
|
||||
!!e && mutateElement(e, {}, true);
|
||||
!!e && this.scene.mutateElement(e, {});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8905,7 +8994,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (element) {
|
||||
mutateElement(element, {}, true);
|
||||
this.scene.mutateElement(
|
||||
element as ExcalidrawElbowArrowElement,
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8934,7 +9026,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element,
|
||||
startBindingElement,
|
||||
endBindingElement,
|
||||
elementsMap,
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
@@ -9001,7 +9092,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
? []
|
||||
: [...newElement.pressures, childEvent.pressure];
|
||||
|
||||
mutateElement(newElement, {
|
||||
this.scene.mutateElement(newElement, {
|
||||
points: [...points, pointFrom<LocalPoint>(dx, dy)],
|
||||
pressures,
|
||||
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
||||
@@ -9048,15 +9139,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
if (!pointerDownState.drag.hasOccurred && newElement && !multiElement) {
|
||||
mutateElement(newElement, {
|
||||
points: [
|
||||
...newElement.points,
|
||||
pointFrom<LocalPoint>(
|
||||
pointerCoords.x - newElement.x,
|
||||
pointerCoords.y - newElement.y,
|
||||
),
|
||||
],
|
||||
});
|
||||
this.scene.mutateElement(
|
||||
newElement,
|
||||
{
|
||||
points: [
|
||||
...newElement.points,
|
||||
pointFrom<LocalPoint>(
|
||||
pointerCoords.x - newElement.x,
|
||||
pointerCoords.y - newElement.y,
|
||||
),
|
||||
],
|
||||
},
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
|
||||
this.setState({
|
||||
multiElement: newElement,
|
||||
newElement,
|
||||
@@ -9070,8 +9166,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
newElement,
|
||||
this.state,
|
||||
pointerCoords,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
);
|
||||
}
|
||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||
@@ -9089,7 +9184,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
prevState,
|
||||
),
|
||||
selectedLinearElement: new LinearElementEditor(newElement),
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
newElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
this.setState((prevState) => ({
|
||||
@@ -9112,7 +9210,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
if (newElement.width < minWidth) {
|
||||
mutateElement(newElement, {
|
||||
this.scene.mutateElement(newElement, {
|
||||
autoResize: true,
|
||||
});
|
||||
}
|
||||
@@ -9162,7 +9260,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
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
|
||||
this.scene.triggerUpdate();
|
||||
}
|
||||
@@ -9194,7 +9299,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
) {
|
||||
// remove the linear element from all groups
|
||||
// before removing it from the frame as well
|
||||
mutateElement(linearElement, {
|
||||
this.scene.mutateElement(linearElement, {
|
||||
groupIds: [],
|
||||
});
|
||||
|
||||
@@ -9223,12 +9328,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.editingGroupId!,
|
||||
);
|
||||
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
element,
|
||||
{
|
||||
groupIds: element.groupIds.slice(0, index),
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9240,12 +9345,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
element.groupIds[element.groupIds.length - 1],
|
||||
).length < 2
|
||||
) {
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
element,
|
||||
{
|
||||
groupIds: [],
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -9355,7 +9460,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// the one we've hit
|
||||
if (selectedElements.length === 1) {
|
||||
this.setState({
|
||||
selectedLinearElement: new LinearElementEditor(hitElement),
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
hitElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9480,7 +9588,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
selectedLinearElement:
|
||||
newSelectedElements.length === 1 &&
|
||||
isLinearElement(newSelectedElements[0])
|
||||
? new LinearElementEditor(newSelectedElements[0])
|
||||
? new LinearElementEditor(
|
||||
newSelectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
};
|
||||
});
|
||||
@@ -9554,7 +9665,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.
|
||||
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
|
||||
prevState.selectedLinearElement?.elementId !== hitElement.id
|
||||
? new LinearElementEditor(hitElement)
|
||||
? new LinearElementEditor(
|
||||
hitElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: prevState.selectedLinearElement,
|
||||
}));
|
||||
}
|
||||
@@ -9647,11 +9761,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
bindOrUnbindLinearElements(
|
||||
linearElements,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene,
|
||||
isBindingEnabled(this.state),
|
||||
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
|
||||
this.scene,
|
||||
this.state.zoom,
|
||||
);
|
||||
}
|
||||
@@ -9808,12 +9920,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const dataURL =
|
||||
this.files[fileId]?.dataURL || (await getDataURL(imageFile));
|
||||
|
||||
const imageElement = mutateElement(
|
||||
const imageElement = this.scene.mutateElement(
|
||||
_imageElement,
|
||||
{
|
||||
fileId,
|
||||
},
|
||||
false,
|
||||
{ informMutation: false, isDragging: false },
|
||||
) as NonDeleted<InitializedExcalidrawImageElement>;
|
||||
|
||||
return new Promise<NonDeleted<InitializedExcalidrawImageElement>>(
|
||||
@@ -9879,7 +9991,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
showCursorImagePreview,
|
||||
});
|
||||
} catch (error: any) {
|
||||
mutateElement(imageElement, {
|
||||
this.scene.mutateElement(imageElement, {
|
||||
isDeleted: true,
|
||||
});
|
||||
this.actionManager.executeAction(actionFinalize);
|
||||
@@ -10025,7 +10137,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value
|
||||
) {
|
||||
const placeholderSize = 100 / this.state.zoom.value;
|
||||
mutateElement(imageElement, {
|
||||
this.scene.mutateElement(imageElement, {
|
||||
x: imageElement.x - placeholderSize / 2,
|
||||
y: imageElement.y - placeholderSize / 2,
|
||||
width: placeholderSize,
|
||||
@@ -10059,7 +10171,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const x = imageElement.x + imageElement.width / 2 - width / 2;
|
||||
const y = imageElement.y + imageElement.height / 2 - height / 2;
|
||||
|
||||
mutateElement(imageElement, {
|
||||
this.scene.mutateElement(imageElement, {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
@@ -10490,7 +10602,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this,
|
||||
),
|
||||
selectedLinearElement: isLinearElement(element)
|
||||
? new LinearElementEditor(element)
|
||||
? new LinearElementEditor(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: null,
|
||||
}
|
||||
: this.state),
|
||||
@@ -10523,8 +10638,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
||||
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter: false,
|
||||
scene: this.scene,
|
||||
zoom: this.state.zoom.value,
|
||||
informMutation,
|
||||
informMutation: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -10588,6 +10704,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
: shouldMaintainAspectRatio(event),
|
||||
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||
zoom: this.state.zoom.value,
|
||||
scene: this.scene,
|
||||
widthAspectRatio: aspectRatio,
|
||||
originOffset: this.state.originSnapOffset,
|
||||
informMutation,
|
||||
@@ -10675,7 +10792,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
transformHandleType,
|
||||
);
|
||||
|
||||
mutateElement(
|
||||
this.scene.mutateElement(
|
||||
croppingElement,
|
||||
cropElement(
|
||||
croppingElement,
|
||||
@@ -10690,16 +10807,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
),
|
||||
);
|
||||
|
||||
updateBoundElements(
|
||||
croppingElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
{
|
||||
newSize: {
|
||||
width: croppingElement.width,
|
||||
height: croppingElement.height,
|
||||
},
|
||||
updateBoundElements(croppingElement, this.scene, {
|
||||
newSize: {
|
||||
width: croppingElement.width,
|
||||
height: croppingElement.height,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
isCropping: transformHandleType && transformHandleType !== "rotation",
|
||||
@@ -10813,7 +10926,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerDownState.originalElements,
|
||||
transformHandleType,
|
||||
selectedElements,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
this.scene,
|
||||
shouldRotateWithDiscreteAngle(event),
|
||||
shouldResizeFromCenter(event),
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
isWritableElement,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
|
||||
|
||||
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||
|
||||
import {
|
||||
@@ -410,6 +412,14 @@ function CommandPaletteInner({
|
||||
actionManager.executeAction(actionToggleSearchMenu);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.shapeSwitch"),
|
||||
category: DEFAULT_CATEGORIES.elements,
|
||||
icon: boltIcon,
|
||||
perform: () => {
|
||||
actionManager.executeAction(actionToggleShapeSwitch);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("labels.changeStroke"),
|
||||
keywords: ["color", "outline"],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
@import "../css//variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
.ConvertElementTypePopup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--island-bg-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
padding: 0.5rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,10 @@ import {
|
||||
defaultGetElementLinkFromSelection,
|
||||
getLinkIdAndTypeFromSelection,
|
||||
} from "@excalidraw/element/elementLink";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { t } from "../i18n";
|
||||
import { getSelectedElements } from "../scene";
|
||||
@@ -21,20 +22,20 @@ import { TrashIcon } from "./icons";
|
||||
import "./ElementLinkDialog.scss";
|
||||
|
||||
import type { AppProps, AppState, UIAppState } from "../types";
|
||||
|
||||
const ElementLinkDialog = ({
|
||||
sourceElementId,
|
||||
onClose,
|
||||
elementsMap,
|
||||
appState,
|
||||
scene,
|
||||
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
||||
}: {
|
||||
sourceElementId: ExcalidrawElement["id"];
|
||||
elementsMap: ElementsMap;
|
||||
appState: UIAppState;
|
||||
scene: Scene;
|
||||
onClose?: () => void;
|
||||
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
||||
|
||||
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
||||
@@ -70,7 +71,7 @@ const ElementLinkDialog = ({
|
||||
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
|
||||
const elementToLink = elementsMap.get(sourceElementId);
|
||||
elementToLink &&
|
||||
mutateElement(elementToLink, {
|
||||
scene.mutateElement(elementToLink, {
|
||||
link: nextLink,
|
||||
});
|
||||
}
|
||||
@@ -78,13 +79,13 @@ const ElementLinkDialog = ({
|
||||
if (!nextLink && linkEdited && sourceElementId) {
|
||||
const elementToLink = elementsMap.get(sourceElementId);
|
||||
elementToLink &&
|
||||
mutateElement(elementToLink, {
|
||||
scene.mutateElement(elementToLink, {
|
||||
link: null,
|
||||
});
|
||||
}
|
||||
|
||||
onClose?.();
|
||||
}, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
|
||||
}, [sourceElementId, nextLink, elementsMap, linkEdited, scene, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ const Header = () => (
|
||||
className="HelpDialog__btn"
|
||||
href="https://docs.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||
{t("helpDialog.documentation")}
|
||||
@@ -30,7 +30,7 @@ const Header = () => (
|
||||
className="HelpDialog__btn"
|
||||
href="https://plus.excalidraw.com/blog"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||
{t("helpDialog.blog")}
|
||||
@@ -247,6 +247,11 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
label={t("toolBar.link")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.convertElementType")}
|
||||
shortcuts={["Tab", "Shift+Tab"]}
|
||||
isOr={true}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
<ShortcutIsland
|
||||
className="HelpDialog__island--view"
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CLASSES,
|
||||
DEFAULT_SIDEBAR,
|
||||
TOOL_TYPE,
|
||||
arrayToMap,
|
||||
capitalizeString,
|
||||
isShallowEqual,
|
||||
} from "@excalidraw/common";
|
||||
@@ -17,7 +18,6 @@ import { ShapeCache } from "@excalidraw/element/ShapeCache";
|
||||
|
||||
import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import Scene from "../scene/Scene";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { isHandToolActive } from "../appState";
|
||||
@@ -446,22 +446,18 @@ const LayerUI = ({
|
||||
|
||||
if (selectedElements.length) {
|
||||
for (const element of selectedElements) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||
? colorPickerType === "elementBackground"
|
||||
? "strokeColor"
|
||||
: "backgroundColor"
|
||||
: colorPickerType === "elementBackground"
|
||||
? "backgroundColor"
|
||||
: "strokeColor"]: color,
|
||||
},
|
||||
false,
|
||||
);
|
||||
mutateElement(element, arrayToMap(elements), {
|
||||
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||
? colorPickerType === "elementBackground"
|
||||
? "strokeColor"
|
||||
: "backgroundColor"
|
||||
: colorPickerType === "elementBackground"
|
||||
? "backgroundColor"
|
||||
: "strokeColor"]: color,
|
||||
});
|
||||
ShapeCache.delete(element);
|
||||
}
|
||||
Scene.getScene(selectedElements[0])?.triggerUpdate();
|
||||
app.scene.triggerUpdate();
|
||||
} else if (colorPickerType === "elementBackground") {
|
||||
setAppState({
|
||||
currentItemBackgroundColor: color,
|
||||
@@ -494,7 +490,7 @@ const LayerUI = ({
|
||||
openDialog: null,
|
||||
});
|
||||
}}
|
||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||
scene={app.scene}
|
||||
appState={appState}
|
||||
generateLinkForSelection={generateLinkForSelection}
|
||||
/>
|
||||
|
||||
@@ -389,7 +389,7 @@ const PublishLibrary = ({
|
||||
<a
|
||||
href="https://libraries.excalidraw.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
{el}
|
||||
</a>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import { isArrowElement, isElbowArrow } from "@excalidraw/element/typeChecks";
|
||||
|
||||
@@ -9,13 +7,16 @@ import type { Degrees } from "@excalidraw/math";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { angleIcon } from "../icons";
|
||||
|
||||
import { updateBindings } from "../../../element/src/binding";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface AngleProps {
|
||||
@@ -35,7 +36,6 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
scene,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
if (origElement && !isElbowArrow(origElement)) {
|
||||
const latestElement = elementsMap.get(origElement.id);
|
||||
@@ -45,14 +45,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
const nextAngle = degreesToRadians(nextValue as Degrees);
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle });
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -71,14 +71,14 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
||||
|
||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle });
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { getNormalizedGridStep } from "../../scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface PositionProps {
|
||||
|
||||
@@ -5,17 +5,17 @@ import {
|
||||
MINIMAL_CROP_SIZE,
|
||||
getUncroppedWidthAndHeight,
|
||||
} from "@excalidraw/element/cropElement";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { resizeSingleElement } from "@excalidraw/element/resizeElements";
|
||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface DimensionDragInputProps {
|
||||
@@ -113,7 +113,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
};
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||
@@ -144,7 +144,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
height: nextCropHeight,
|
||||
};
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
|
||||
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight),
|
||||
@@ -176,8 +176,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldMaintainAspectRatio: keepAspectRatio,
|
||||
@@ -223,8 +223,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldMaintainAspectRatio: keepAspectRatio,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { deepCopyElement } from "@excalidraw/element/duplicate";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { CaptureUpdateAction } from "../../store";
|
||||
import { useApp } from "../App";
|
||||
import { InlineIcon } from "../InlineIcon";
|
||||
@@ -16,7 +18,6 @@ import { SMALLEST_DELTA } from "./utils";
|
||||
import "./DragInput.scss";
|
||||
|
||||
import type { StatsInputProperty } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
export type DragInputCallbackType<
|
||||
@@ -216,13 +217,12 @@ const StatsDragInput = <
|
||||
y: number;
|
||||
} | null = null;
|
||||
|
||||
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
||||
app.scene
|
||||
.getNonDeletedElements()
|
||||
.reduce((acc: ElementsMap, element) => {
|
||||
acc.set(element.id, deepCopyElement(element));
|
||||
return acc;
|
||||
}, new Map());
|
||||
let originalElementsMap: ElementsMap | null = app.scene
|
||||
.getNonDeletedElements()
|
||||
.reduce((acc: ElementsMap, element) => {
|
||||
acc.set(element.id, deepCopyElement(element));
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
let originalElements: readonly E[] | null = elements.map(
|
||||
(element) => originalElementsMap!.get(element.id) as E,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@@ -13,13 +12,14 @@ import type {
|
||||
ExcalidrawTextElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { fontSizeIcon } from "../icons";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface FontSizeProps {
|
||||
@@ -68,13 +68,13 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||
}
|
||||
|
||||
if (nextFontSize) {
|
||||
mutateElement(latestElement, {
|
||||
scene.mutateElement(latestElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
redrawTextBoundingBox(
|
||||
latestElement,
|
||||
scene.getContainerElement(latestElement),
|
||||
scene.getNonDeletedElementsMap(),
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import { isArrowElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
@@ -11,13 +9,14 @@ import type { Degrees } from "@excalidraw/math";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { angleIcon } from "../icons";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiAngleProps {
|
||||
@@ -54,17 +53,13 @@ const handleDegreeChange: DragInputCallbackType<
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
angle: nextAngle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(element, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(element)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,17 +87,13 @@ const handleDegreeChange: DragInputCallbackType<
|
||||
|
||||
const nextAngle = degreesToRadians(nextAngleInDegrees as Degrees);
|
||||
|
||||
mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
angle: nextAngle,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestElement, {
|
||||
angle: nextAngle,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||
mutateElement(boundTextElement, { angle: nextAngle }, false);
|
||||
scene.mutateElement(boundTextElement, { angle: nextAngle });
|
||||
}
|
||||
}
|
||||
scene.triggerUpdate();
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useMemo } from "react";
|
||||
|
||||
import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
|
||||
import { updateBoundElements } from "@excalidraw/element/binding";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
rescalePointsInElement,
|
||||
resizeSingleElement,
|
||||
@@ -23,13 +22,14 @@ import type {
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import DragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import { getElementsInAtomicUnit } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type { AtomicUnit } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiDimensionProps {
|
||||
@@ -75,33 +75,31 @@ const resizeElementInGroup = (
|
||||
scale: number,
|
||||
latestElement: ExcalidrawElement,
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||
|
||||
mutateElement(latestElement, updates, false);
|
||||
scene.mutateElement(latestElement, updates);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
origElement,
|
||||
originalElementsMap,
|
||||
);
|
||||
if (boundTextElement) {
|
||||
const newFontSize = boundTextElement.fontSize * scale;
|
||||
updateBoundElements(latestElement, elementsMap, {
|
||||
updateBoundElements(latestElement, scene, {
|
||||
newSize: { width: updates.width, height: updates.height },
|
||||
});
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||
mutateElement(
|
||||
latestBoundTextElement,
|
||||
{
|
||||
fontSize: newFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestBoundTextElement, {
|
||||
fontSize: newFontSize,
|
||||
});
|
||||
handleBindTextResize(
|
||||
latestElement,
|
||||
elementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
true,
|
||||
);
|
||||
@@ -118,8 +116,8 @@ const resizeGroup = (
|
||||
property: MultiDimensionProps["property"],
|
||||
latestElements: ExcalidrawElement[],
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
// keep aspect ratio for groups
|
||||
if (property === "width") {
|
||||
@@ -141,8 +139,8 @@ const resizeGroup = (
|
||||
scale,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -194,8 +192,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
property,
|
||||
latestElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
@@ -237,8 +235,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldInformMutation: false,
|
||||
@@ -301,8 +299,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
property,
|
||||
latestElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
@@ -340,8 +338,8 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
nextHeight,
|
||||
latestElement,
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
property === "width" ? "e" : "s",
|
||||
{
|
||||
shouldInformMutation: false,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@@ -16,13 +15,14 @@ import type {
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { fontSizeIcon } from "../icons";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiFontSizeProps {
|
||||
@@ -84,19 +84,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||
|
||||
for (const textElement of latestTextElements) {
|
||||
mutateElement(
|
||||
textElement,
|
||||
{
|
||||
fontSize: nextFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(textElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(
|
||||
textElement,
|
||||
scene.getContainerElement(textElement),
|
||||
elementsMap,
|
||||
false,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,19 +112,14 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||
if (shouldChangeByStepSize) {
|
||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
||||
}
|
||||
mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
fontSize: nextFontSize,
|
||||
},
|
||||
false,
|
||||
);
|
||||
scene.mutateElement(latestElement, {
|
||||
fontSize: nextFontSize,
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(
|
||||
latestElement,
|
||||
scene.getContainerElement(latestElement),
|
||||
elementsMap,
|
||||
false,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,20 +5,21 @@ import { isTextElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
|
||||
import {
|
||||
getAtomicUnits,
|
||||
getStepSizedValue,
|
||||
isPropertyEditable,
|
||||
STEP_SIZE,
|
||||
} from "./utils";
|
||||
import { getElementsInAtomicUnit, moveElement } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type { AtomicUnit } from "./utils";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface MultiPositionProps {
|
||||
@@ -30,19 +31,15 @@ interface MultiPositionProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
const STEP_SIZE = 10;
|
||||
|
||||
const moveElements = (
|
||||
property: MultiPositionProps["property"],
|
||||
changeInTopX: number,
|
||||
changeInTopY: number,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
originalElements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
for (let i = 0; i < originalElements.length; i++) {
|
||||
const origElement = originalElements[i];
|
||||
|
||||
const [cx, cy] = [
|
||||
@@ -65,8 +62,6 @@ const moveElements = (
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
@@ -78,11 +73,10 @@ const moveGroupTo = (
|
||||
nextX: number,
|
||||
nextY: number,
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const [x1, y1, ,] = getCommonBounds(originalElements);
|
||||
const offsetX = nextX - x1;
|
||||
const offsetY = nextY - y1;
|
||||
@@ -112,8 +106,6 @@ const moveGroupTo = (
|
||||
topLeftX + offsetX,
|
||||
topLeftY + offsetY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
@@ -135,7 +127,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
|
||||
if (nextValue !== undefined) {
|
||||
for (const atomicUnit of getAtomicUnits(
|
||||
@@ -159,8 +150,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
elementsInUnit.map((el) => el.original),
|
||||
elementsMap,
|
||||
elements,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
@@ -188,8 +177,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
false,
|
||||
@@ -214,8 +201,6 @@ const handlePositionChange: DragInputCallbackType<
|
||||
changeInTopX,
|
||||
changeInTopY,
|
||||
originalElements,
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
@@ -4,16 +4,16 @@ import {
|
||||
getFlipAdjustedCropPosition,
|
||||
getUncroppedWidthAndHeight,
|
||||
} from "@excalidraw/element/cropElement";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { isImageElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import StatsDragInput from "./DragInput";
|
||||
import { getStepSizedValue, moveElement } from "./utils";
|
||||
import { getStepSizedValue, moveElement, STEP_SIZE } from "./utils";
|
||||
|
||||
import type { DragInputCallbackType } from "./DragInput";
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
interface PositionProps {
|
||||
@@ -24,8 +24,6 @@ interface PositionProps {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
const STEP_SIZE = 10;
|
||||
|
||||
const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
accumulatedChange,
|
||||
instantChange,
|
||||
@@ -38,7 +36,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
originalAppState,
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const elements = scene.getNonDeletedElements();
|
||||
const origElement = originalElements[0];
|
||||
const [cx, cy] = [
|
||||
origElement.x + origElement.width / 2,
|
||||
@@ -101,7 +98,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
};
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
});
|
||||
|
||||
@@ -119,7 +116,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height),
|
||||
};
|
||||
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
crop: nextCrop,
|
||||
});
|
||||
|
||||
@@ -133,8 +130,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
@@ -166,8 +161,6 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
||||
newTopLeftX,
|
||||
newTopLeftY,
|
||||
origElement,
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
originalElementsMap,
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
ExcalidrawTextElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { Excalidraw, getCommonBounds, mutateElement } from "../..";
|
||||
import { Excalidraw, getCommonBounds } from "../..";
|
||||
import { actionGroup } from "../../actions";
|
||||
import { t } from "../../i18n";
|
||||
import * as StaticScene from "../../renderer/staticScene";
|
||||
@@ -478,7 +478,7 @@ describe("stats for a non-generic element", () => {
|
||||
containerId: container.id,
|
||||
fontSize: 20,
|
||||
});
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
});
|
||||
API.setElements([container, text]);
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
bindOrUnbindLinearElements,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||
import {
|
||||
isFrameLikeElement,
|
||||
isLinearElement,
|
||||
isTextElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
@@ -18,16 +12,20 @@ import {
|
||||
isInGroup,
|
||||
} from "@excalidraw/element/groups";
|
||||
|
||||
import { getFrameChildren } from "@excalidraw/element/frame";
|
||||
|
||||
import type { Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "../../scene/Scene";
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { updateBindings } from "../../../element/src/binding";
|
||||
|
||||
import type { AppState } from "../../types";
|
||||
|
||||
export type StatsInputProperty =
|
||||
@@ -40,6 +38,7 @@ export type StatsInputProperty =
|
||||
| "gridStep";
|
||||
|
||||
export const SMALLEST_DELTA = 0.01;
|
||||
export const STEP_SIZE = 10;
|
||||
|
||||
export const isPropertyEditable = (
|
||||
element: ExcalidrawElement,
|
||||
@@ -119,12 +118,11 @@ export const moveElement = (
|
||||
newTopLeftX: number,
|
||||
newTopLeftY: number,
|
||||
originalElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
originalElementsMap: ElementsMap,
|
||||
shouldInformMutation = true,
|
||||
) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const latestElement = elementsMap.get(originalElement.id);
|
||||
if (!latestElement) {
|
||||
return;
|
||||
@@ -148,15 +146,15 @@ export const moveElement = (
|
||||
-originalElement.angle as Radians,
|
||||
);
|
||||
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
latestElement,
|
||||
{
|
||||
x,
|
||||
y,
|
||||
},
|
||||
shouldInformMutation,
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
updateBindings(latestElement, elementsMap, elements, scene);
|
||||
updateBindings(latestElement, scene);
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
originalElement,
|
||||
@@ -165,15 +163,77 @@ export const moveElement = (
|
||||
if (boundTextElement) {
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
latestBoundTextElement &&
|
||||
mutateElement(
|
||||
scene.mutateElement(
|
||||
latestBoundTextElement,
|
||||
{
|
||||
x: boundTextElement.x + changeInX,
|
||||
y: boundTextElement.y + changeInY,
|
||||
},
|
||||
shouldInformMutation,
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
|
||||
if (isFrameLikeElement(originalElement)) {
|
||||
const originalChildren = getFrameChildren(
|
||||
originalElementsMap,
|
||||
originalElement.id,
|
||||
);
|
||||
originalChildren.forEach((child) => {
|
||||
const latestChildElement = elementsMap.get(child.id);
|
||||
|
||||
if (!latestChildElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [childCX, childCY] = [
|
||||
child.x + child.width / 2,
|
||||
child.y + child.height / 2,
|
||||
];
|
||||
const [childTopLeftX, childTopLeftY] = pointRotateRads(
|
||||
pointFrom(child.x, child.y),
|
||||
pointFrom(childCX, childCY),
|
||||
child.angle,
|
||||
);
|
||||
|
||||
const childNewTopLeftX = Math.round(childTopLeftX + changeInX);
|
||||
const childNewTopLeftY = Math.round(childTopLeftY + changeInY);
|
||||
|
||||
const [childX, childY] = pointRotateRads(
|
||||
pointFrom(childNewTopLeftX, childNewTopLeftY),
|
||||
pointFrom(childCX + changeInX, childCY + changeInY),
|
||||
-child.angle as Radians,
|
||||
);
|
||||
|
||||
scene.mutateElement(
|
||||
latestChildElement,
|
||||
{
|
||||
x: childX,
|
||||
y: childY,
|
||||
},
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
updateBindings(latestChildElement, scene, {
|
||||
simultaneouslyUpdated: originalChildren,
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(
|
||||
latestChildElement,
|
||||
originalElementsMap,
|
||||
);
|
||||
if (boundTextElement) {
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
latestBoundTextElement &&
|
||||
scene.mutateElement(
|
||||
latestBoundTextElement,
|
||||
{
|
||||
x: boundTextElement.x + changeInX,
|
||||
y: boundTextElement.y + changeInY,
|
||||
},
|
||||
{ informMutation: shouldInformMutation, isDragging: false },
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAtomicUnits = (
|
||||
@@ -196,29 +256,3 @@ export const getAtomicUnits = (
|
||||
});
|
||||
return _atomicUnits;
|
||||
};
|
||||
|
||||
export const updateBindings = (
|
||||
latestElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
newSize?: { width: number; height: number };
|
||||
zoom?: AppState["zoom"];
|
||||
},
|
||||
) => {
|
||||
if (isLinearElement(latestElement)) {
|
||||
bindOrUnbindLinearElements(
|
||||
[latestElement],
|
||||
elementsMap,
|
||||
elements,
|
||||
scene,
|
||||
true,
|
||||
[],
|
||||
options?.zoom,
|
||||
);
|
||||
} else {
|
||||
updateBoundElements(latestElement, elementsMap, options);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ const DropdownMenuItemLink = ({
|
||||
onSelect,
|
||||
className = "",
|
||||
selected,
|
||||
rel = "noreferrer",
|
||||
rel = "noopener",
|
||||
...rest
|
||||
}: {
|
||||
href: string;
|
||||
@@ -31,11 +31,12 @@ const DropdownMenuItemLink = ({
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-no-target-blank
|
||||
<a
|
||||
{...rest}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel={rel || "noopener"}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -21,8 +21,6 @@ import {
|
||||
embeddableURLValidator,
|
||||
} from "@excalidraw/element/embeddable";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import {
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
@@ -33,6 +31,8 @@ import {
|
||||
|
||||
import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawEmbeddableElement,
|
||||
@@ -70,14 +70,14 @@ const embeddableLinkCache = new Map<
|
||||
|
||||
export const Hyperlink = ({
|
||||
element,
|
||||
elementsMap,
|
||||
scene,
|
||||
setAppState,
|
||||
onLinkOpen,
|
||||
setToast,
|
||||
updateEmbedValidationStatus,
|
||||
}: {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
elementsMap: ElementsMap;
|
||||
scene: Scene;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
onLinkOpen: ExcalidrawProps["onLinkOpen"];
|
||||
setToast: (
|
||||
@@ -88,6 +88,7 @@ export const Hyperlink = ({
|
||||
status: boolean,
|
||||
) => void;
|
||||
}) => {
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const appState = useExcalidrawAppState();
|
||||
const appProps = useAppProps();
|
||||
const device = useDevice();
|
||||
@@ -114,7 +115,7 @@ export const Hyperlink = ({
|
||||
setAppState({ activeEmbeddable: null });
|
||||
}
|
||||
if (!link) {
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
link: null,
|
||||
});
|
||||
updateEmbedValidationStatus(element, false);
|
||||
@@ -126,7 +127,7 @@ export const Hyperlink = ({
|
||||
setToast({ message: t("toast.unableToEmbed"), closable: true });
|
||||
}
|
||||
element.link && embeddableLinkCache.set(element.id, element.link);
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
link,
|
||||
});
|
||||
updateEmbedValidationStatus(element, false);
|
||||
@@ -144,7 +145,7 @@ export const Hyperlink = ({
|
||||
: 1;
|
||||
const hasLinkChanged =
|
||||
embeddableLinkCache.get(element.id) !== element.link;
|
||||
mutateElement(element, {
|
||||
scene.mutateElement(element, {
|
||||
...(hasLinkChanged
|
||||
? {
|
||||
width:
|
||||
@@ -169,10 +170,11 @@ export const Hyperlink = ({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mutateElement(element, { link });
|
||||
scene.mutateElement(element, { link });
|
||||
}
|
||||
}, [
|
||||
element,
|
||||
scene,
|
||||
setToast,
|
||||
appProps.validateEmbeddable,
|
||||
appState.activeEmbeddable,
|
||||
@@ -229,9 +231,9 @@ export const Hyperlink = ({
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
trackEvent("hyperlink", "delete");
|
||||
mutateElement(element, { link: null });
|
||||
scene.mutateElement(element, { link: null });
|
||||
setAppState({ showHyperlinkPopup: false });
|
||||
}, [setAppState, element]);
|
||||
}, [setAppState, element, scene]);
|
||||
|
||||
const onEdit = () => {
|
||||
trackEvent("hyperlink", "edit", "popup-ui");
|
||||
|
||||
@@ -78,7 +78,7 @@ const WelcomeScreenMenuItemLink = ({
|
||||
className={`welcome-screen-menu-item ${className}`}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
>
|
||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||
{children}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { bumpVersion } from "@excalidraw/element/mutateElement";
|
||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||
import { detectLineHeight } from "@excalidraw/element/textMeasurements";
|
||||
import {
|
||||
isArrowBoundToElement,
|
||||
isArrowElement,
|
||||
isElbowArrow,
|
||||
isFixedPointBinding,
|
||||
@@ -594,8 +595,7 @@ export const restoreElements = (
|
||||
return restoredElements.map((element) => {
|
||||
if (
|
||||
isElbowArrow(element) &&
|
||||
element.startBinding == null &&
|
||||
element.endBinding == null &&
|
||||
!isArrowBoundToElement(element) &&
|
||||
!validateElbowPoints(element.points)
|
||||
) {
|
||||
return {
|
||||
|
||||
@@ -38,10 +38,13 @@ import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
|
||||
import { getCommonBounds } from "@excalidraw/element/bounds";
|
||||
|
||||
import Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
@@ -63,8 +66,6 @@ import type {
|
||||
|
||||
import type { MarkOptional } from "@excalidraw/common/utility-types";
|
||||
|
||||
import { getCommonBounds } from "..";
|
||||
|
||||
export type ValidLinearElement = {
|
||||
type: "arrow" | "line";
|
||||
x: number;
|
||||
@@ -221,7 +222,7 @@ const DEFAULT_DIMENSION = 100;
|
||||
const bindTextToContainer = (
|
||||
container: ExcalidrawElement,
|
||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const textElement: ExcalidrawTextElement = newTextElement({
|
||||
x: 0,
|
||||
@@ -240,7 +241,8 @@ const bindTextToContainer = (
|
||||
}),
|
||||
});
|
||||
|
||||
redrawTextBoundingBox(textElement, container, elementsMap);
|
||||
redrawTextBoundingBox(textElement, container, scene);
|
||||
|
||||
return [container, textElement] as const;
|
||||
};
|
||||
|
||||
@@ -249,7 +251,7 @@ const bindLinearElementToElement = (
|
||||
start: ValidLinearElement["start"],
|
||||
end: ValidLinearElement["end"],
|
||||
elementStore: ElementStore,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
scene: Scene,
|
||||
): {
|
||||
linearElement: ExcalidrawLinearElement;
|
||||
startBoundElement?: ExcalidrawElement;
|
||||
@@ -335,7 +337,7 @@ const bindLinearElementToElement = (
|
||||
linearElement,
|
||||
startBoundElement as ExcalidrawBindableElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -410,7 +412,7 @@ const bindLinearElementToElement = (
|
||||
linearElement,
|
||||
endBoundElement as ExcalidrawBindableElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -651,6 +653,9 @@ export const convertToExcalidrawElements = (
|
||||
}
|
||||
|
||||
const elementsMap = elementStore.getElementsMap();
|
||||
// we don't have a real scene, so we just use a temp scene to query and mutate elements
|
||||
const scene = new Scene(elementsMap);
|
||||
|
||||
// Add labels and arrow bindings
|
||||
for (const [id, element] of elementsWithIds) {
|
||||
const excalidrawElement = elementStore.getElement(id)!;
|
||||
@@ -664,7 +669,7 @@ export const convertToExcalidrawElements = (
|
||||
let [container, text] = bindTextToContainer(
|
||||
excalidrawElement,
|
||||
element?.label,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
elementStore.add(container);
|
||||
elementStore.add(text);
|
||||
@@ -692,7 +697,7 @@ export const convertToExcalidrawElements = (
|
||||
originalStart,
|
||||
originalEnd,
|
||||
elementStore,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
container = linearElement;
|
||||
elementStore.add(linearElement);
|
||||
@@ -717,7 +722,7 @@ export const convertToExcalidrawElements = (
|
||||
start,
|
||||
end,
|
||||
elementStore,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
elementStore.add(linearElement);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { atom, createStore, type PrimitiveAtom } from "jotai";
|
||||
import {
|
||||
atom,
|
||||
createStore,
|
||||
type PrimitiveAtom,
|
||||
type WritableAtom,
|
||||
} from "jotai";
|
||||
import { createIsolation } from "jotai-scope";
|
||||
|
||||
const jotai = createIsolation();
|
||||
|
||||
export { atom, PrimitiveAtom };
|
||||
export { atom, PrimitiveAtom, WritableAtom };
|
||||
export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
|
||||
export const EditorJotaiProvider: ReturnType<
|
||||
typeof createIsolation
|
||||
|
||||
@@ -28,6 +28,8 @@ import type {
|
||||
|
||||
import type { ValueOf } from "@excalidraw/common/utility-types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { CascadiaFontFaces } from "./Cascadia";
|
||||
import { ComicShannsFontFaces } from "./ComicShanns";
|
||||
import { EmojiFontFaces } from "./Emoji";
|
||||
@@ -40,8 +42,6 @@ import { NunitoFontFaces } from "./Nunito";
|
||||
import { VirgilFontFaces } from "./Virgil";
|
||||
import { XiaolaiFontFaces } from "./Xiaolai";
|
||||
|
||||
import type Scene from "../scene/Scene";
|
||||
|
||||
export class Fonts {
|
||||
// it's ok to track fonts across multiple instances only once, so let's use
|
||||
// a static member to reduce memory footprint
|
||||
|
||||
@@ -149,6 +149,7 @@ export class LassoTrail extends AnimatedTrail {
|
||||
this.app.scene.getNonDeletedElement(
|
||||
selectedIds[0],
|
||||
) as NonDeleted<ExcalidrawLinearElement>,
|
||||
this.app.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
: null,
|
||||
};
|
||||
|
||||
@@ -165,7 +165,9 @@
|
||||
"unCroppedDimension": "Uncropped dimension",
|
||||
"copyElementLink": "Copy link to object",
|
||||
"linkToElement": "Link to object",
|
||||
"wrapSelectionInFrame": "Wrap selection in frame"
|
||||
"wrapSelectionInFrame": "Wrap selection in frame",
|
||||
"tab": "Tab",
|
||||
"shapeSwitch": "Switch shape"
|
||||
},
|
||||
"elementLink": {
|
||||
"title": "Link to object",
|
||||
@@ -296,7 +298,8 @@
|
||||
"laser": "Laser pointer",
|
||||
"hand": "Hand (panning tool)",
|
||||
"extraTools": "More tools",
|
||||
"mermaidToExcalidraw": "Mermaid to Excalidraw"
|
||||
"mermaidToExcalidraw": "Mermaid to Excalidraw",
|
||||
"convertElementType": "Toggle shape type"
|
||||
},
|
||||
"element": {
|
||||
"rectangle": "Rectangle",
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
import { THEME, THEME_FILTER } from "@excalidraw/common";
|
||||
import { elementCenterPoint, THEME, THEME_FILTER } from "@excalidraw/common";
|
||||
|
||||
import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
|
||||
import { getDiamondPoints } from "@excalidraw/element/bounds";
|
||||
import { getCornerRadius } from "@excalidraw/element/shapes";
|
||||
|
||||
import {
|
||||
bezierEquation,
|
||||
curve,
|
||||
curveTangent,
|
||||
type GlobalPoint,
|
||||
pointFrom,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
vector,
|
||||
vectorNormal,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawRectanguloidElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type { StaticCanvasRenderConfig } from "../scene/types";
|
||||
import type { StaticCanvasAppState, AppState } from "../types";
|
||||
import type { AppState, StaticCanvasAppState } from "../types";
|
||||
|
||||
export const fillCircle = (
|
||||
context: CanvasRenderingContext2D,
|
||||
@@ -72,3 +95,399 @@ export const bootstrapCanvas = ({
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
function drawCatmullRomQuadraticApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
segments = 20,
|
||||
) {
|
||||
ctx.lineTo(points[0][0], points[0][1]);
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
|
||||
for (let t = 0; t <= 1; t += 1 / segments) {
|
||||
const t2 = t * t;
|
||||
|
||||
const x =
|
||||
(1 - t) * (1 - t) * p0[0] + 2 * (1 - t) * t * p1[0] + t2 * p2[0];
|
||||
|
||||
const y =
|
||||
(1 - t) * (1 - t) * p0[1] + 2 * (1 - t) * t * p1[1] + t2 * p2[1];
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawCatmullRomCubicApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
segments = 20,
|
||||
) {
|
||||
ctx.lineTo(points[0][0], points[0][1]);
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2];
|
||||
|
||||
for (let t = 0; t <= 1; t += 1 / segments) {
|
||||
const t2 = t * t;
|
||||
const t3 = t2 * t;
|
||||
|
||||
const x =
|
||||
0.5 *
|
||||
(2 * p1[0] +
|
||||
(-p0[0] + p2[0]) * t +
|
||||
(2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
|
||||
(-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3);
|
||||
|
||||
const y =
|
||||
0.5 *
|
||||
(2 * p1[1] +
|
||||
(-p0[1] + p2[1]) * t +
|
||||
(2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
|
||||
(-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3);
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const drawHighlightForRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawRectanguloidElement,
|
||||
padding: number,
|
||||
) => {
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
elementCenterPoint(element),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.rotate(element.angle);
|
||||
|
||||
let radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
if (radius === 0) {
|
||||
radius = 0.01;
|
||||
}
|
||||
|
||||
context.beginPath();
|
||||
|
||||
{
|
||||
const topLeftApprox = offsetQuadraticBezier(
|
||||
pointFrom(0, 0 + radius),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0 + radius, 0),
|
||||
padding,
|
||||
);
|
||||
const topRightApprox = offsetQuadraticBezier(
|
||||
pointFrom(element.width - radius, 0),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width, radius),
|
||||
padding,
|
||||
);
|
||||
const bottomRightApprox = offsetQuadraticBezier(
|
||||
pointFrom(element.width, element.height - radius),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width - radius, element.height),
|
||||
padding,
|
||||
);
|
||||
const bottomLeftApprox = offsetQuadraticBezier(
|
||||
pointFrom(radius, element.height),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(0, element.height - radius),
|
||||
padding,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topLeftApprox[topLeftApprox.length - 1][0],
|
||||
topLeftApprox[topLeftApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(topRightApprox[0][0], topRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topRightApprox);
|
||||
context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomRightApprox);
|
||||
context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomLeftApprox);
|
||||
context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topLeftApprox);
|
||||
}
|
||||
|
||||
// Counter-clockwise for the cutout in the middle. We need to have an "inverse
|
||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||
// sharp inset edges on line joins < 90 degrees.
|
||||
{
|
||||
const topLeftApprox = offsetQuadraticBezier(
|
||||
pointFrom(0 + radius, 0),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0, 0 + radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const topRightApprox = offsetQuadraticBezier(
|
||||
pointFrom(element.width, radius),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width - radius, 0),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomRightApprox = offsetQuadraticBezier(
|
||||
pointFrom(element.width - radius, element.height),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width, element.height - radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomLeftApprox = offsetQuadraticBezier(
|
||||
pointFrom(0, element.height - radius),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(radius, element.height),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topLeftApprox[topLeftApprox.length - 1][0],
|
||||
topLeftApprox[topLeftApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(bottomLeftApprox[0][0], bottomLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomLeftApprox);
|
||||
context.lineTo(bottomRightApprox[0][0], bottomRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, bottomRightApprox);
|
||||
context.lineTo(topRightApprox[0][0], topRightApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topRightApprox);
|
||||
context.lineTo(topLeftApprox[0][0], topLeftApprox[0][1]);
|
||||
drawCatmullRomQuadraticApprox(context, topLeftApprox);
|
||||
}
|
||||
|
||||
context.closePath();
|
||||
context.fill();
|
||||
|
||||
context.restore();
|
||||
};
|
||||
|
||||
export const strokeEllipseWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
) => {
|
||||
context.beginPath();
|
||||
context.ellipse(cx, cy, width / 2, height / 2, angle, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
export const strokeRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
fill: boolean = false,
|
||||
/** should account for zoom */
|
||||
radius: number = 0,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(angle);
|
||||
if (fill) {
|
||||
context.fillRect(x - cx, y - cy, width, height);
|
||||
}
|
||||
if (radius && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(x - cx, y - cy, width, height, radius);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
} else {
|
||||
context.strokeRect(x - cx, y - cy, width, height);
|
||||
}
|
||||
context.restore();
|
||||
};
|
||||
|
||||
export const drawHighlightForDiamondWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
padding: number,
|
||||
element: ExcalidrawDiamondElement,
|
||||
) => {
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
elementCenterPoint(element),
|
||||
element.angle,
|
||||
);
|
||||
context.save();
|
||||
context.translate(x, y);
|
||||
context.rotate(element.angle);
|
||||
|
||||
{
|
||||
context.beginPath();
|
||||
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = offsetCubicBezier(
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
padding,
|
||||
);
|
||||
const rightApprox = offsetCubicBezier(
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
padding,
|
||||
);
|
||||
const bottomApprox = offsetCubicBezier(
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
padding,
|
||||
);
|
||||
const leftApprox = offsetCubicBezier(
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
padding,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
|
||||
// Counter-clockwise for the cutout in the middle. We need to have an "inverse
|
||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||
// sharp inset edges on line joins < 90 degrees.
|
||||
{
|
||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||
getDiamondPoints(element);
|
||||
const verticalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(topX - leftX), element)
|
||||
: (topX - leftX) * 0.01;
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = offsetCubicBezier(
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const rightApprox = offsetCubicBezier(
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomApprox = offsetCubicBezier(
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const leftApprox = offsetCubicBezier(
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
|
||||
context.moveTo(
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
function offsetCubicBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
p3: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 20,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const c = curve(p0, p1, p2, p3);
|
||||
const point = bezierEquation(c, t);
|
||||
const tangent = vectorNormalize(curveTangent(c, t));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
function offsetQuadraticBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 20,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const t1 = 1 - t;
|
||||
const point = pointFrom<GlobalPoint>(
|
||||
t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0],
|
||||
t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1],
|
||||
);
|
||||
const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]);
|
||||
const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]);
|
||||
const tangent = vectorNormalize(vector(tangentX, tangentY));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import oc from "open-color";
|
||||
import {
|
||||
pointFrom,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
type Radians,
|
||||
} from "@excalidraw/math";
|
||||
import oc from "open-color";
|
||||
|
||||
import {
|
||||
arrayToMap,
|
||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||
FRAME_STYLE,
|
||||
THEME,
|
||||
arrayToMap,
|
||||
invariant,
|
||||
THEME,
|
||||
throttleRAF,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import {
|
||||
BINDING_HIGHLIGHT_OFFSET,
|
||||
BINDING_HIGHLIGHT_THICKNESS,
|
||||
FIXED_BINDING_DISTANCE,
|
||||
maxBindingGap,
|
||||
} from "@excalidraw/element/binding";
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
@@ -35,14 +34,12 @@ import {
|
||||
isTextElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { getCornerRadius } from "@excalidraw/element/shapes";
|
||||
|
||||
import { renderSelectionElement } from "@excalidraw/element/renderElement";
|
||||
|
||||
import {
|
||||
isSelectedViaGroup,
|
||||
getSelectedGroupIds,
|
||||
getElementsInGroup,
|
||||
getSelectedGroupIds,
|
||||
isSelectedViaGroup,
|
||||
selectGroupsFromGivenElements,
|
||||
} from "@excalidraw/element/groups";
|
||||
|
||||
@@ -86,8 +83,12 @@ import { getClientColor, renderRemoteCursors } from "../clients";
|
||||
|
||||
import {
|
||||
bootstrapCanvas,
|
||||
drawHighlightForDiamondWithRotation,
|
||||
drawHighlightForRectWithRotation,
|
||||
fillCircle,
|
||||
getNormalizedCanvasDimensions,
|
||||
strokeEllipseWithRotation,
|
||||
strokeRectWithRotation,
|
||||
} from "./helpers";
|
||||
|
||||
import type {
|
||||
@@ -160,57 +161,6 @@ const highlightPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||
);
|
||||
};
|
||||
|
||||
const strokeRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
fill: boolean = false,
|
||||
/** should account for zoom */
|
||||
radius: number = 0,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(angle);
|
||||
if (fill) {
|
||||
context.fillRect(x - cx, y - cy, width, height);
|
||||
}
|
||||
if (radius && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(x - cx, y - cy, width, height, radius);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
} else {
|
||||
context.strokeRect(x - cx, y - cy, width, height);
|
||||
}
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const strokeDiamondWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(angle);
|
||||
context.beginPath();
|
||||
context.moveTo(0, height / 2);
|
||||
context.lineTo(width / 2, 0);
|
||||
context.lineTo(0, -height / 2);
|
||||
context.lineTo(-width / 2, 0);
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
@@ -237,19 +187,6 @@ const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
);
|
||||
};
|
||||
|
||||
const strokeEllipseWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cx: number,
|
||||
cy: number,
|
||||
angle: number,
|
||||
) => {
|
||||
context.beginPath();
|
||||
context.ellipse(cx, cy, width / 2, height / 2, angle, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
};
|
||||
|
||||
const renderBindingHighlightForBindableElement = (
|
||||
context: CanvasRenderingContext2D,
|
||||
element: ExcalidrawBindableElement,
|
||||
@@ -261,16 +198,10 @@ const renderBindingHighlightForBindableElement = (
|
||||
const height = y2 - y1;
|
||||
|
||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||
// When zooming out, make line width greater for visibility
|
||||
const zoomValue = zoom.value < 1 ? zoom.value : 1;
|
||||
context.lineWidth = BINDING_HIGHLIGHT_THICKNESS / zoomValue;
|
||||
// To ensure the binding highlight doesn't overlap the element itself
|
||||
const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET;
|
||||
context.fillStyle = "rgba(0,0,0,.05)";
|
||||
|
||||
const radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
// To ensure the binding highlight doesn't overlap the element itself
|
||||
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
@@ -280,37 +211,20 @@ const renderBindingHighlightForBindableElement = (
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
strokeRectWithRotation(
|
||||
context,
|
||||
x1 - padding,
|
||||
y1 - padding,
|
||||
width + padding * 2,
|
||||
height + padding * 2,
|
||||
x1 + width / 2,
|
||||
y1 + height / 2,
|
||||
element.angle,
|
||||
undefined,
|
||||
radius,
|
||||
);
|
||||
drawHighlightForRectWithRotation(context, element, padding);
|
||||
break;
|
||||
case "diamond":
|
||||
const side = Math.hypot(width, height);
|
||||
const wPadding = (padding * side) / height;
|
||||
const hPadding = (padding * side) / width;
|
||||
strokeDiamondWithRotation(
|
||||
context,
|
||||
width + wPadding * 2,
|
||||
height + hPadding * 2,
|
||||
x1 + width / 2,
|
||||
y1 + height / 2,
|
||||
element.angle,
|
||||
);
|
||||
drawHighlightForDiamondWithRotation(context, padding, element);
|
||||
break;
|
||||
case "ellipse":
|
||||
context.lineWidth =
|
||||
maxBindingGap(element, element.width, element.height, zoom) -
|
||||
FIXED_BINDING_DISTANCE;
|
||||
|
||||
strokeEllipseWithRotation(
|
||||
context,
|
||||
width + padding * 2,
|
||||
height + padding * 2,
|
||||
width + padding + FIXED_BINDING_DISTANCE,
|
||||
height + padding + FIXED_BINDING_DISTANCE,
|
||||
x1 + width / 2,
|
||||
y1 + height / 2,
|
||||
element.angle,
|
||||
|
||||
@@ -9,10 +9,11 @@ import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/element/Scene";
|
||||
|
||||
import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
|
||||
import { renderStaticSceneThrottled } from "../renderer/staticScene";
|
||||
|
||||
import type Scene from "./Scene";
|
||||
import type { RenderableElementsMap } from "./types";
|
||||
|
||||
import type { AppState } from "../types";
|
||||
|
||||
@@ -21,7 +21,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||
<a
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
href="blog.excalidaw.com"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
@@ -392,7 +392,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
aria-label="GitHub"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
href="https://github.com/excalidraw/excalidraw"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
>
|
||||
@@ -426,7 +426,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
aria-label="X"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
href="https://x.com/excalidraw"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="X"
|
||||
>
|
||||
@@ -472,7 +472,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||
aria-label="Discord"
|
||||
class="dropdown-menu-item dropdown-menu-item-base"
|
||||
href="https://discord.gg/UexuTaE"
|
||||
rel="noreferrer"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
title="Discord"
|
||||
>
|
||||
|
||||
@@ -171,7 +171,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 19,
|
||||
"version": 9,
|
||||
"width": 100,
|
||||
"x": 100,
|
||||
"y": -50,
|
||||
@@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "102.35417",
|
||||
"height": "102.45605",
|
||||
"id": "id172",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
@@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
0,
|
||||
],
|
||||
[
|
||||
"101.77517",
|
||||
"102.35417",
|
||||
"102.80179",
|
||||
"102.45605",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -227,9 +227,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 40,
|
||||
"width": "101.77517",
|
||||
"x": "0.70711",
|
||||
"version": 37,
|
||||
"width": "102.80179",
|
||||
"x": "-0.42182",
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
@@ -264,7 +264,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 6,
|
||||
"version": 14,
|
||||
"width": 50,
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
@@ -291,22 +291,39 @@ History {
|
||||
"added": Map {},
|
||||
"removed": Map {},
|
||||
"updated": Map {
|
||||
"id171" => Delta {
|
||||
"deleted": {
|
||||
"boundElements": [],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id172",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"id172" => Delta {
|
||||
"deleted": {
|
||||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "0.00990",
|
||||
"elementId": "id175",
|
||||
"fixedPoint": [
|
||||
"0.50000",
|
||||
1,
|
||||
],
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "0.98586",
|
||||
"height": "70.45017",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"-0.98586",
|
||||
"100.70774",
|
||||
"70.45017",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
@@ -321,7 +338,7 @@ History {
|
||||
"focus": "-0.02000",
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "0.00000",
|
||||
"height": "0.09250",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
@@ -329,7 +346,7 @@ History {
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"0.00000",
|
||||
"0.09250",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
@@ -339,6 +356,19 @@ History {
|
||||
},
|
||||
},
|
||||
},
|
||||
"id175" => Delta {
|
||||
"deleted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id172",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -366,59 +396,32 @@ History {
|
||||
],
|
||||
},
|
||||
},
|
||||
"id171" => Delta {
|
||||
"deleted": {
|
||||
"boundElements": [],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id172",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"id172" => Delta {
|
||||
"deleted": {
|
||||
"endBinding": {
|
||||
"elementId": "id175",
|
||||
"fixedPoint": [
|
||||
"0.50000",
|
||||
1,
|
||||
],
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "102.35417",
|
||||
"height": "102.45584",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"101.77517",
|
||||
"102.35417",
|
||||
"102.79971",
|
||||
"102.45584",
|
||||
],
|
||||
],
|
||||
"startBinding": null,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id171",
|
||||
"focus": "0.00990",
|
||||
"gap": 1,
|
||||
},
|
||||
"height": "0.98586",
|
||||
"height": "70.33521",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
"98.58579",
|
||||
"-0.98586",
|
||||
"100.78887",
|
||||
"70.33521",
|
||||
],
|
||||
],
|
||||
"startBinding": {
|
||||
@@ -426,20 +429,7 @@ History {
|
||||
"focus": "0.02970",
|
||||
"gap": 1,
|
||||
},
|
||||
"y": "0.99364",
|
||||
},
|
||||
},
|
||||
"id175" => Delta {
|
||||
"deleted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id172",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [],
|
||||
"y": "35.20327",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -739,7 +729,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 9,
|
||||
"version": 19,
|
||||
"width": 100,
|
||||
"x": 150,
|
||||
"y": -50,
|
||||
@@ -819,8 +809,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 30,
|
||||
"width": 0,
|
||||
"version": 33,
|
||||
"width": 100,
|
||||
"x": "149.29289",
|
||||
"y": 0,
|
||||
}
|
||||
@@ -846,20 +836,22 @@ History {
|
||||
"added": Map {},
|
||||
"removed": Map {},
|
||||
"updated": Map {
|
||||
"id167" => Delta {
|
||||
"id166" => Delta {
|
||||
"deleted": {
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"boundElements": [],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id167",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"id167" => Delta {
|
||||
"deleted": {
|
||||
"endBinding": null,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
@@ -871,6 +863,23 @@ History {
|
||||
],
|
||||
],
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id166",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
},
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -899,22 +908,8 @@ History {
|
||||
],
|
||||
},
|
||||
},
|
||||
"id166" => Delta {
|
||||
"deleted": {
|
||||
"boundElements": [],
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id167",
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"id167" => Delta {
|
||||
"deleted": {
|
||||
"endBinding": null,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
@@ -928,18 +923,13 @@ History {
|
||||
"startBinding": null,
|
||||
},
|
||||
"inserted": {
|
||||
"endBinding": {
|
||||
"elementId": "id166",
|
||||
"focus": -0,
|
||||
"gap": 1,
|
||||
},
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
],
|
||||
[
|
||||
0,
|
||||
100,
|
||||
0,
|
||||
],
|
||||
],
|
||||
@@ -7490,7 +7480,7 @@ History {
|
||||
|
||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `10`;
|
||||
exports[`history > multiplayer undo/redo > should iterate through the history when selected or editing linear element was remotely deleted > [end of test] number of renders 1`] = `9`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > should iterate through the history when when element change relates to remotely deleted element > [end of test] appState 1`] = `
|
||||
{
|
||||
@@ -10561,7 +10551,7 @@ History {
|
||||
|
||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `15`;
|
||||
exports[`history > multiplayer undo/redo > should override remotely added points on undo, but restore them on redo > [end of test] number of renders 1`] = `14`;
|
||||
|
||||
exports[`history > multiplayer undo/redo > should redistribute deltas when element gets removed locally but is restored remotely > [end of test] appState 1`] = `
|
||||
{
|
||||
@@ -20188,4 +20178,4 @@ History {
|
||||
|
||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of elements 1`] = `1`;
|
||||
|
||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `21`;
|
||||
exports[`history > singleplayer undo/redo > should support linear element creation and points manipulation through the editor > [end of test] number of renders 1`] = `20`;
|
||||
|
||||
@@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 400692809,
|
||||
"width": 70,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
@@ -106,7 +106,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
||||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 8,
|
||||
"versionNonce": 1604849351,
|
||||
"versionNonce": 400692809,
|
||||
"width": 70,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
|
||||
@@ -6832,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 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`] = `
|
||||
{
|
||||
@@ -14550,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 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`] = `
|
||||
{
|
||||
|
||||
@@ -313,7 +313,7 @@ describe("Test dragCreate", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`6`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@@ -342,7 +342,7 @@ describe("Test dragCreate", () => {
|
||||
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
||||
`6`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
|
||||
import { KEYS } from "@excalidraw/common";
|
||||
|
||||
import { actionSelectAll } from "../actions";
|
||||
@@ -298,7 +296,7 @@ describe("element locking", () => {
|
||||
height: textSize,
|
||||
containerId: container.id,
|
||||
});
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
});
|
||||
|
||||
@@ -339,7 +337,7 @@ describe("element locking", () => {
|
||||
containerId: container.id,
|
||||
locked: true,
|
||||
});
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
});
|
||||
API.setElements([container, text]);
|
||||
@@ -373,7 +371,7 @@ describe("element locking", () => {
|
||||
containerId: container.id,
|
||||
locked: true,
|
||||
});
|
||||
mutateElement(container, {
|
||||
h.app.scene.mutateElement(container, {
|
||||
boundElements: [{ id: text.id, type: "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 { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
newArrowElement,
|
||||
newElement,
|
||||
@@ -100,10 +99,10 @@ export class API {
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
static updateElement = <T extends ExcalidrawElement>(
|
||||
...args: Parameters<typeof mutateElement<T>>
|
||||
...args: Parameters<typeof h.app.scene.mutateElement<T>>
|
||||
) => {
|
||||
act(() => {
|
||||
mutateElement<T>(...args);
|
||||
h.app.scene.mutateElement(...args);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -419,12 +418,11 @@ export class API {
|
||||
|
||||
});
|
||||
|
||||
mutateElement(
|
||||
h.app.scene.mutateElement(
|
||||
rectangle,
|
||||
{
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
return [rectangle, text];
|
||||
@@ -453,12 +451,11 @@ export class API {
|
||||
: opts?.label?.frameId ?? null,
|
||||
});
|
||||
|
||||
mutateElement(
|
||||
h.app.scene.mutateElement(
|
||||
arrow,
|
||||
{
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
return [arrow, text];
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
getElementPointsCoords,
|
||||
} from "@excalidraw/element/bounds";
|
||||
import { cropElement } from "@excalidraw/element/cropElement";
|
||||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
getTransformHandles,
|
||||
getTransformHandlesFromCoords,
|
||||
@@ -526,7 +525,7 @@ export class UI {
|
||||
|
||||
if (angle !== 0) {
|
||||
act(() => {
|
||||
mutateElement(origElement, { angle });
|
||||
h.app.scene.mutateElement(origElement, { angle });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import type {
|
||||
FontString,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { Excalidraw, mutateElement } from "../index";
|
||||
import { Excalidraw } from "../index";
|
||||
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
||||
import * as StaticScene from "../renderer/staticScene";
|
||||
import { API } from "../tests/helpers/api";
|
||||
@@ -118,7 +118,7 @@ describe("Test Linear Elements", () => {
|
||||
],
|
||||
roundness,
|
||||
});
|
||||
mutateElement(line, { points: line.points });
|
||||
h.app.scene.mutateElement(line, { points: line.points });
|
||||
API.setElements([line]);
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
return line;
|
||||
@@ -177,7 +177,7 @@ describe("Test Linear Elements", () => {
|
||||
pointFrom<LocalPoint>(0.5, 0),
|
||||
pointFrom<LocalPoint>(100, 100),
|
||||
]);
|
||||
new LinearElementEditor(element);
|
||||
new LinearElementEditor(element, arrayToMap(h.elements));
|
||||
expect(element.points).toEqual([
|
||||
pointFrom<LocalPoint>(0, 0),
|
||||
pointFrom<LocalPoint>(99.5, 100),
|
||||
@@ -1271,7 +1271,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(rect.y).toBe(0);
|
||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||
h.elements[0],
|
||||
arrayToMap(h.elements),
|
||||
h.app.scene,
|
||||
"nw",
|
||||
false,
|
||||
);
|
||||
@@ -1384,7 +1384,7 @@ describe("Test Linear Elements", () => {
|
||||
const [origStartX, origStartY] = [line.x, line.y];
|
||||
|
||||
act(() => {
|
||||
LinearElementEditor.movePoints(line, [
|
||||
LinearElementEditor.movePoints(line, h.app.scene, [
|
||||
{
|
||||
index: 0,
|
||||
point: pointFrom(line.points[0][0] + 10, line.points[0][1] + 10),
|
||||
|
||||
@@ -13,8 +13,6 @@ import type {
|
||||
ExcalidrawRectangleElement,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import type Scene from "@excalidraw/excalidraw/scene/Scene";
|
||||
|
||||
import { Excalidraw } from "../index";
|
||||
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
||||
import * as StaticScene from "../renderer/staticScene";
|
||||
@@ -85,15 +83,13 @@ describe("move element", () => {
|
||||
const rectA = UI.createElement("rectangle", { size: 100 });
|
||||
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
|
||||
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
|
||||
const elementsMap = h.app.scene.getNonDeletedElementsMap();
|
||||
act(() => {
|
||||
// bind line to two rectangles
|
||||
bindOrUnbindLinearElement(
|
||||
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
|
||||
rectA.get() as ExcalidrawRectangleElement,
|
||||
rectB.get() as ExcalidrawRectangleElement,
|
||||
elementsMap,
|
||||
{} as Scene,
|
||||
h.app.scene,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -170,8 +166,6 @@ describe("duplicate element on move when ALT is clicked", () => {
|
||||
fireEvent.pointerMove(canvas, { clientX: 10, clientY: 60 });
|
||||
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(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`3`);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("multi point mode in linear elements", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
@@ -162,7 +162,7 @@ describe("multi point mode in linear elements", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
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);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
||||
@@ -714,6 +714,7 @@ export type AppClassProperties = {
|
||||
excalidrawContainerValue: App["excalidrawContainerValue"];
|
||||
|
||||
onPointerUpEmitter: App["onPointerUpEmitter"];
|
||||
updateEditorAtom: App["updateEditorAtom"];
|
||||
};
|
||||
|
||||
export type PointerDownState = Readonly<{
|
||||
@@ -784,6 +785,7 @@ export type UnsubscribeCallback = () => void;
|
||||
|
||||
export interface ExcalidrawImperativeAPI {
|
||||
updateScene: InstanceType<typeof App>["updateScene"];
|
||||
mutateElement: InstanceType<typeof App>["mutateElement"];
|
||||
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
||||
resetScene: InstanceType<typeof App>["resetScene"];
|
||||
getSceneElementsIncludingDeleted: InstanceType<
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "@excalidraw/element/containerCache";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
|
||||
import { bumpVersion, mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import { bumpVersion } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
getBoundTextElementId,
|
||||
getContainerElement,
|
||||
@@ -45,7 +45,6 @@ import type {
|
||||
|
||||
import { actionSaveToActiveFile } from "../actions";
|
||||
|
||||
import Scene from "../scene/Scene";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import {
|
||||
actionDecreaseFontSize,
|
||||
@@ -81,6 +80,8 @@ const getTransform = (
|
||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||
};
|
||||
|
||||
type SubmitHandler = () => void;
|
||||
|
||||
export const textWysiwyg = ({
|
||||
id,
|
||||
onChange,
|
||||
@@ -107,7 +108,7 @@ export const textWysiwyg = ({
|
||||
excalidrawContainer: HTMLDivElement | null;
|
||||
app: App;
|
||||
autoSelect?: boolean;
|
||||
}) => {
|
||||
}): SubmitHandler => {
|
||||
const textPropertiesUpdated = (
|
||||
updatedTextElement: ExcalidrawTextElement,
|
||||
editable: HTMLTextAreaElement,
|
||||
@@ -130,8 +131,7 @@ export const textWysiwyg = ({
|
||||
|
||||
const updateWysiwygStyle = () => {
|
||||
const appState = app.state;
|
||||
const updatedTextElement =
|
||||
Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
|
||||
const updatedTextElement = app.scene.getElement<ExcalidrawTextElement>(id);
|
||||
|
||||
if (!updatedTextElement) {
|
||||
return;
|
||||
@@ -188,7 +188,6 @@ export const textWysiwyg = ({
|
||||
}
|
||||
|
||||
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
||||
|
||||
maxHeight = getBoundTextMaxHeight(
|
||||
container,
|
||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||
@@ -201,7 +200,7 @@ export const textWysiwyg = ({
|
||||
container.type,
|
||||
);
|
||||
|
||||
mutateElement(container, { height: targetContainerHeight });
|
||||
app.scene.mutateElement(container, { height: targetContainerHeight });
|
||||
return;
|
||||
} else if (
|
||||
// autoshrink container height until original container height
|
||||
@@ -214,7 +213,7 @@ export const textWysiwyg = ({
|
||||
height,
|
||||
container.type,
|
||||
);
|
||||
mutateElement(container, { height: targetContainerHeight });
|
||||
app.scene.mutateElement(container, { height: targetContainerHeight });
|
||||
} else {
|
||||
const { y } = computeBoundTextPosition(
|
||||
container,
|
||||
@@ -287,7 +286,7 @@ export const textWysiwyg = ({
|
||||
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
|
||||
}
|
||||
|
||||
mutateElement(updatedTextElement, { x: coordX, y: coordY });
|
||||
app.scene.mutateElement(updatedTextElement, { x: coordX, y: coordY });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -544,7 +543,7 @@ export const textWysiwyg = ({
|
||||
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
||||
// wysiwyg on update
|
||||
cleanup();
|
||||
const updateElement = Scene.getScene(element)?.getElement(
|
||||
const updateElement = app.scene.getElement(
|
||||
element.id,
|
||||
) as ExcalidrawTextElement;
|
||||
if (!updateElement) {
|
||||
@@ -559,7 +558,7 @@ export const textWysiwyg = ({
|
||||
if (editable.value.trim()) {
|
||||
const boundTextElementId = getBoundTextElementId(container);
|
||||
if (!boundTextElementId || boundTextElementId !== element.id) {
|
||||
mutateElement(container, {
|
||||
app.scene.mutateElement(container, {
|
||||
boundElements: (container.boundElements || []).concat({
|
||||
type: "text",
|
||||
id: element.id,
|
||||
@@ -570,7 +569,7 @@ export const textWysiwyg = ({
|
||||
bumpVersion(container);
|
||||
}
|
||||
} else {
|
||||
mutateElement(container, {
|
||||
app.scene.mutateElement(container, {
|
||||
boundElements: container.boundElements?.filter(
|
||||
(ele) =>
|
||||
!isTextElement(
|
||||
@@ -579,11 +578,8 @@ export const textWysiwyg = ({
|
||||
),
|
||||
});
|
||||
}
|
||||
redrawTextBoundingBox(
|
||||
updateElement,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
redrawTextBoundingBox(updateElement, container, app.scene);
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
@@ -740,4 +736,6 @@ export const textWysiwyg = ({
|
||||
excalidrawContainer
|
||||
?.querySelector(".excalidraw-textEditorContainer")!
|
||||
.appendChild(editable);
|
||||
|
||||
return handleSubmit;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Bounds } from "@excalidraw/element/bounds";
|
||||
|
||||
import { isPoint, pointDistance, pointFrom } from "./point";
|
||||
import { rectangle, rectangleIntersectLineSegment } from "./rectangle";
|
||||
import { vector } from "./vector";
|
||||
|
||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||
|
||||
@@ -82,7 +83,7 @@ function solve(
|
||||
return [t0, s0];
|
||||
}
|
||||
|
||||
const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||
export const bezierEquation = <Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
t: number,
|
||||
) =>
|
||||
@@ -274,6 +275,26 @@ export function isCurve<P extends GlobalPoint | LocalPoint>(
|
||||
);
|
||||
}
|
||||
|
||||
export function curveTangent<Point extends GlobalPoint | LocalPoint>(
|
||||
[p0, p1, p2, p3]: Curve<Point>,
|
||||
t: number,
|
||||
) {
|
||||
return vector(
|
||||
-3 * (1 - t) * (1 - t) * p0[0] +
|
||||
3 * (1 - t) * (1 - t) * p1[0] -
|
||||
6 * t * (1 - t) * p1[0] -
|
||||
3 * t * t * p2[0] +
|
||||
6 * t * (1 - t) * p2[0] +
|
||||
3 * t * t * p3[0],
|
||||
-3 * (1 - t) * (1 - t) * p0[1] +
|
||||
3 * (1 - t) * (1 - t) * p1[1] -
|
||||
6 * t * (1 - t) * p1[1] -
|
||||
3 * t * t * p2[1] +
|
||||
6 * t * (1 - t) * p2[1] +
|
||||
3 * t * t * p3[1],
|
||||
);
|
||||
}
|
||||
|
||||
function curveBounds<Point extends GlobalPoint | LocalPoint>(
|
||||
c: Curve<Point>,
|
||||
): Bounds {
|
||||
|
||||
@@ -143,3 +143,8 @@ export const vectorNormalize = (v: Vector): Vector => {
|
||||
|
||||
return vector(v[0] / m, v[1] / m);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the right-hand normal of the vector.
|
||||
*/
|
||||
export const vectorNormal = (v: Vector): Vector => vector(v[1], -v[0]);
|
||||
|
||||
Reference in New Issue
Block a user