);
acc.push({
relative,
diff --git a/packages/excalidraw/fractionalIndex.ts b/packages/element/src/fractionalIndex.ts
similarity index 97%
rename from packages/excalidraw/fractionalIndex.ts
rename to packages/element/src/fractionalIndex.ts
index 8a1459ddd..bbe739f29 100644
--- a/packages/excalidraw/fractionalIndex.ts
+++ b/packages/element/src/fractionalIndex.ts
@@ -1,16 +1,20 @@
import { generateNKeysBetween } from "fractional-indexing";
-import { mutateElement } from "./element/mutateElement";
-import { getBoundTextElement } from "./element/textElement";
-import { hasBoundTextElement } from "./element/typeChecks";
-import { InvalidFractionalIndexError } from "./errors";
-import { arrayToMap } from "./utils";
+import { arrayToMap } from "@excalidraw/common";
+
+import { mutateElement } from "./mutateElement";
+import { getBoundTextElement } from "./textElement";
+import { hasBoundTextElement } from "./typeChecks";
import type {
ExcalidrawElement,
FractionalIndex,
OrderedExcalidrawElement,
-} from "./element/types";
+} from "./types";
+
+export class InvalidFractionalIndexError extends Error {
+ public code = "ELEMENT_HAS_INVALID_INDEX" as const;
+}
/**
* Envisioned relation between array order and fractional indices:
diff --git a/packages/excalidraw/frame.ts b/packages/element/src/frame.ts
similarity index 97%
rename from packages/excalidraw/frame.ts
rename to packages/element/src/frame.ts
index 758bc273e..7f40148b7 100644
--- a/packages/excalidraw/frame.ts
+++ b/packages/element/src/frame.ts
@@ -1,24 +1,33 @@
+import { arrayToMap } from "@excalidraw/common";
import { isPointWithinBounds, pointFrom } from "@excalidraw/math";
-import {
- doLineSegmentsIntersect,
- elementsOverlappingBBox,
-} from "@excalidraw/utils";
+import { doLineSegmentsIntersect } from "@excalidraw/utils/bbox";
+import { elementsOverlappingBBox } from "@excalidraw/utils/withinBounds";
+
+import type { ExcalidrawElementsIncludingDeleted } from "@excalidraw/excalidraw/scene/Scene";
+
+import type {
+ AppClassProperties,
+ AppState,
+ StaticCanvasAppState,
+} from "@excalidraw/excalidraw/types";
+
+import type { ReadonlySetLike } from "@excalidraw/common/utility-types";
+
+import { getElementsWithinSelection, getSelectedElements } from "./selection";
+import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
import {
+ getElementLineSegments,
getCommonBounds,
getElementAbsoluteCoords,
- isTextElement,
-} from "./element";
-import { getElementLineSegments } from "./element/bounds";
-import { mutateElement } from "./element/mutateElement";
+} from "./bounds";
+import { mutateElement } from "./mutateElement";
+import { getBoundTextElement, getContainerElement } from "./textElement";
import {
- getBoundTextElement,
- getContainerElement,
-} from "./element/textElement";
-import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
-import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
-import { getElementsWithinSelection, getSelectedElements } from "./scene";
-import { arrayToMap } from "./utils";
+ isFrameElement,
+ isFrameLikeElement,
+ isTextElement,
+} from "./typeChecks";
import type {
ElementsMap,
@@ -27,14 +36,7 @@ import type {
ExcalidrawFrameLikeElement,
NonDeleted,
NonDeletedExcalidrawElement,
-} from "./element/types";
-import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
-import type {
- AppClassProperties,
- AppState,
- StaticCanvasAppState,
} from "./types";
-import type { ReadonlySetLike } from "./utility-types";
// --------------------------- Frame State ------------------------------------
export const bindElementsToFramesAfterDuplication = (
diff --git a/packages/excalidraw/groups.ts b/packages/element/src/groups.ts
similarity index 97%
rename from packages/excalidraw/groups.ts
rename to packages/element/src/groups.ts
index cedc4af0f..1cd1536e1 100644
--- a/packages/excalidraw/groups.ts
+++ b/packages/element/src/groups.ts
@@ -1,6 +1,13 @@
-import { getBoundTextElement } from "./element/textElement";
-import { getSelectedElements } from "./scene";
-import { makeNextSelectedElementIds } from "./scene/selection";
+import type {
+ AppClassProperties,
+ AppState,
+ InteractiveCanvasAppState,
+} from "@excalidraw/excalidraw/types";
+import type { Mutable } from "@excalidraw/common/utility-types";
+
+import { getBoundTextElement } from "./textElement";
+
+import { makeNextSelectedElementIds, getSelectedElements } from "./selection";
import type {
GroupId,
@@ -9,13 +16,7 @@ import type {
NonDeletedExcalidrawElement,
ElementsMapOrArray,
ElementsMap,
-} from "./element/types";
-import type {
- AppClassProperties,
- AppState,
- InteractiveCanvasAppState,
} from "./types";
-import type { Mutable } from "./utility-types";
export const selectGroup = (
groupId: GroupId,
@@ -214,7 +215,10 @@ export const isSelectedViaGroup = (
) => getSelectedGroupForElement(appState, element) != null;
export const getSelectedGroupForElement = (
- appState: InteractiveCanvasAppState,
+ appState: Pick<
+ InteractiveCanvasAppState,
+ "editingGroupId" | "selectedGroupIds"
+ >,
element: ExcalidrawElement,
) =>
element.groupIds
@@ -294,24 +298,6 @@ export const getSelectedGroupIdForElement = (
selectedGroupIds: { [groupId: string]: boolean },
) => element.groupIds.find((groupId) => selectedGroupIds[groupId]);
-export const getNewGroupIdsForDuplication = (
- groupIds: ExcalidrawElement["groupIds"],
- editingGroupId: AppState["editingGroupId"],
- mapper: (groupId: GroupId) => GroupId,
-) => {
- const copy = [...groupIds];
- const positionOfEditingGroupId = editingGroupId
- ? groupIds.indexOf(editingGroupId)
- : -1;
- const endIndex =
- positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
- for (let index = 0; index < endIndex; index++) {
- copy[index] = mapper(copy[index]);
- }
-
- return copy;
-};
-
export const addToGroup = (
prevGroupIds: ExcalidrawElement["groupIds"],
newGroupId: GroupId,
@@ -398,3 +384,21 @@ export const elementsAreInSameGroup = (
export const isInGroup = (element: NonDeletedExcalidrawElement) => {
return element.groupIds.length > 0;
};
+
+export const getNewGroupIdsForDuplication = (
+ groupIds: ExcalidrawElement["groupIds"],
+ editingGroupId: AppState["editingGroupId"],
+ mapper: (groupId: GroupId) => GroupId,
+) => {
+ const copy = [...groupIds];
+ const positionOfEditingGroupId = editingGroupId
+ ? groupIds.indexOf(editingGroupId)
+ : -1;
+ const endIndex =
+ positionOfEditingGroupId > -1 ? positionOfEditingGroupId : groupIds.length;
+ for (let index = 0; index < endIndex; index++) {
+ copy[index] = mapper(copy[index]);
+ }
+
+ return copy;
+};
diff --git a/packages/element/src/heading.ts b/packages/element/src/heading.ts
new file mode 100644
index 000000000..1e9ab3713
--- /dev/null
+++ b/packages/element/src/heading.ts
@@ -0,0 +1,282 @@
+import { invariant, isDevEnv, isTestEnv } from "@excalidraw/common";
+
+import {
+ pointFrom,
+ pointFromVector,
+ pointRotateRads,
+ pointScaleFromOrigin,
+ pointsEqual,
+ triangleIncludesPoint,
+ vectorCross,
+ vectorFromPoint,
+ vectorScale,
+} from "@excalidraw/math";
+
+import type {
+ LocalPoint,
+ GlobalPoint,
+ Triangle,
+ Vector,
+} from "@excalidraw/math";
+
+import { getCenterForBounds, type Bounds } from "./bounds";
+
+import type { ExcalidrawBindableElement } from "./types";
+
+export const HEADING_RIGHT = [1, 0] as Heading;
+export const HEADING_DOWN = [0, 1] as Heading;
+export const HEADING_LEFT = [-1, 0] as Heading;
+export const HEADING_UP = [0, -1] as Heading;
+export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
+
+export const vectorToHeading = (vec: Vector): Heading => {
+ const [x, y] = vec;
+ const absX = Math.abs(x);
+ const absY = Math.abs(y);
+ if (x > absY) {
+ return HEADING_RIGHT;
+ } else if (x <= -absY) {
+ return HEADING_LEFT;
+ } else if (y > absX) {
+ return HEADING_DOWN;
+ }
+ return HEADING_UP;
+};
+
+export const headingForPoint = (
+ p: P,
+ o: P,
+) => vectorToHeading(vectorFromPoint
(p, o));
+
+export const headingForPointIsHorizontal =
(
+ p: P,
+ o: P,
+) => headingIsHorizontal(headingForPoint
(p, o));
+
+export const compareHeading = (a: Heading, b: Heading) =>
+ a[0] === b[0] && a[1] === b[1];
+
+export const headingIsHorizontal = (a: Heading) =>
+ compareHeading(a, HEADING_RIGHT) || compareHeading(a, HEADING_LEFT);
+
+export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
+
+const headingForPointFromDiamondElement = (
+ element: Readonly,
+ aabb: Readonly,
+ point: Readonly,
+): Heading => {
+ const midPoint = getCenterForBounds(aabb);
+
+ if (isDevEnv() || isTestEnv()) {
+ invariant(
+ element.width > 0 && element.height > 0,
+ "Diamond element has no width or height",
+ );
+ invariant(
+ !pointsEqual(midPoint, point),
+ "The point is too close to the element mid point to determine heading",
+ );
+ }
+
+ const SHRINK = 0.95; // Rounded elements tolerance
+ const top = pointFromVector(
+ vectorScale(
+ vectorFromPoint(
+ pointRotateRads(
+ pointFrom(element.x + element.width / 2, element.y),
+ midPoint,
+ element.angle,
+ ),
+ midPoint,
+ ),
+ SHRINK,
+ ),
+ midPoint,
+ );
+ const right = pointFromVector(
+ vectorScale(
+ vectorFromPoint(
+ pointRotateRads(
+ pointFrom(
+ element.x + element.width,
+ element.y + element.height / 2,
+ ),
+ midPoint,
+ element.angle,
+ ),
+ midPoint,
+ ),
+ SHRINK,
+ ),
+ midPoint,
+ );
+ const bottom = pointFromVector(
+ vectorScale(
+ vectorFromPoint(
+ pointRotateRads(
+ pointFrom(
+ element.x + element.width / 2,
+ element.y + element.height,
+ ),
+ midPoint,
+ element.angle,
+ ),
+ midPoint,
+ ),
+ SHRINK,
+ ),
+ midPoint,
+ );
+ const left = pointFromVector(
+ vectorScale(
+ vectorFromPoint(
+ pointRotateRads(
+ pointFrom(element.x, element.y + element.height / 2),
+ midPoint,
+ element.angle,
+ ),
+ midPoint,
+ ),
+ SHRINK,
+ ),
+ midPoint,
+ );
+
+ // Corners
+ if (
+ vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, right)) <=
+ 0 &&
+ vectorCross(vectorFromPoint(point, top), vectorFromPoint(top, left)) > 0
+ ) {
+ return headingForPoint(top, midPoint);
+ } else if (
+ vectorCross(
+ vectorFromPoint(point, right),
+ vectorFromPoint(right, bottom),
+ ) <= 0 &&
+ vectorCross(vectorFromPoint(point, right), vectorFromPoint(right, top)) > 0
+ ) {
+ return headingForPoint(right, midPoint);
+ } else if (
+ vectorCross(
+ vectorFromPoint(point, bottom),
+ vectorFromPoint(bottom, left),
+ ) <= 0 &&
+ vectorCross(
+ vectorFromPoint(point, bottom),
+ vectorFromPoint(bottom, right),
+ ) > 0
+ ) {
+ return headingForPoint(bottom, midPoint);
+ } else if (
+ vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, top)) <=
+ 0 &&
+ vectorCross(vectorFromPoint(point, left), vectorFromPoint(left, bottom)) > 0
+ ) {
+ return headingForPoint(left, midPoint);
+ }
+
+ // Sides
+ if (
+ vectorCross(
+ vectorFromPoint(point, midPoint),
+ vectorFromPoint(top, midPoint),
+ ) <= 0 &&
+ vectorCross(
+ vectorFromPoint(point, midPoint),
+ vectorFromPoint(right, midPoint),
+ ) > 0
+ ) {
+ const p = element.width > element.height ? top : right;
+ return headingForPoint(p, midPoint);
+ } else if (
+ vectorCross(
+ vectorFromPoint(point, midPoint),
+ vectorFromPoint(right, midPoint),
+ ) <= 0 &&
+ vectorCross(
+ vectorFromPoint(point, midPoint),
+ vectorFromPoint(bottom, midPoint),
+ ) > 0
+ ) {
+ const p = element.width > element.height ? bottom : right;
+ return headingForPoint(p, midPoint);
+ } else if (
+ vectorCross(
+ vectorFromPoint(point, midPoint),
+ vectorFromPoint(bottom, midPoint),
+ ) <= 0 &&
+ vectorCross(
+ vectorFromPoint(point, midPoint),
+ vectorFromPoint(left, midPoint),
+ ) > 0
+ ) {
+ const p = element.width > element.height ? bottom : left;
+ return headingForPoint(p, midPoint);
+ }
+
+ const p = element.width > element.height ? top : left;
+ return headingForPoint(p, midPoint);
+};
+
+// Gets the heading for the point by creating a bounding box around the rotated
+// close fitting bounding box, then creating 4 search cones around the center of
+// the external bbox.
+export const headingForPointFromElement = (
+ element: Readonly,
+ aabb: Readonly,
+ p: Readonly,
+): Heading => {
+ const SEARCH_CONE_MULTIPLIER = 2;
+
+ const midPoint = getCenterForBounds(aabb);
+
+ if (element.type === "diamond") {
+ return headingForPointFromDiamondElement(element, aabb, p);
+ }
+
+ const topLeft = pointScaleFromOrigin(
+ pointFrom(aabb[0], aabb[1]),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ) as Point;
+ const topRight = pointScaleFromOrigin(
+ pointFrom(aabb[2], aabb[1]),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ) as Point;
+ const bottomLeft = pointScaleFromOrigin(
+ pointFrom(aabb[0], aabb[3]),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ) as Point;
+ const bottomRight = pointScaleFromOrigin(
+ pointFrom(aabb[2], aabb[3]),
+ midPoint,
+ SEARCH_CONE_MULTIPLIER,
+ ) as Point;
+
+ return triangleIncludesPoint(
+ [topLeft, topRight, midPoint] as Triangle,
+ p,
+ )
+ ? HEADING_UP
+ : triangleIncludesPoint(
+ [topRight, bottomRight, midPoint] as Triangle,
+ p,
+ )
+ ? HEADING_RIGHT
+ : triangleIncludesPoint(
+ [bottomRight, bottomLeft, midPoint] as Triangle,
+ p,
+ )
+ ? HEADING_DOWN
+ : HEADING_LEFT;
+};
+
+export const flipHeading = (h: Heading): Heading =>
+ [
+ h[0] === 0 ? 0 : h[0] > 0 ? -1 : 1,
+ h[1] === 0 ? 0 : h[1] > 0 ? -1 : 1,
+ ] as Heading;
diff --git a/packages/excalidraw/element/image.ts b/packages/element/src/image.ts
similarity index 96%
rename from packages/excalidraw/element/image.ts
rename to packages/element/src/image.ts
index 0d5f9fb5a..562b48904 100644
--- a/packages/excalidraw/element/image.ts
+++ b/packages/element/src/image.ts
@@ -2,11 +2,16 @@
// ExcalidrawImageElement & related helpers
// -----------------------------------------------------------------------------
-import { MIME_TYPES, SVG_NS } from "../constants";
+import { MIME_TYPES, SVG_NS } from "@excalidraw/common";
+
+import type {
+ AppClassProperties,
+ DataURL,
+ BinaryFiles,
+} from "@excalidraw/excalidraw/types";
import { isInitializedImageElement } from "./typeChecks";
-import type { AppClassProperties, DataURL, BinaryFiles } from "../types";
import type {
ExcalidrawElement,
FileId,
diff --git a/packages/excalidraw/element/index.ts b/packages/element/src/index.ts
similarity index 65%
rename from packages/excalidraw/element/index.ts
rename to packages/element/src/index.ts
index abe84e031..d7edec8ae 100644
--- a/packages/excalidraw/element/index.ts
+++ b/packages/element/src/index.ts
@@ -7,56 +7,6 @@ import type {
NonDeleted,
} from "./types";
-export {
- newElement,
- newTextElement,
- refreshTextDimensions,
- newLinearElement,
- newArrowElement,
- newImageElement,
- duplicateElement,
-} from "./newElement";
-export {
- getElementAbsoluteCoords,
- getElementBounds,
- getCommonBounds,
- getDiamondPoints,
- getArrowheadPoints,
- getClosestElementBounds,
-} from "./bounds";
-
-export {
- OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
- getTransformHandlesFromCoords,
- getTransformHandles,
-} from "./transformHandles";
-export {
- resizeTest,
- getCursorForResizingElement,
- getElementWithTransformHandleType,
- getTransformHandleTypeFromCoords,
-} from "./resizeTest";
-export {
- transformElements,
- getResizeOffsetXY,
- getResizeArrowDirection,
-} from "./resizeElements";
-export {
- dragSelectedElements,
- getDragOffsetXY,
- dragNewElement,
-} from "./dragElements";
-export { isTextElement, isExcalidrawElement } from "./typeChecks";
-export { redrawTextBoundingBox, getTextFromElements } from "./textElement";
-export {
- getPerfectElementSize,
- getLockedLinearCursorAlignSize,
- isInvisiblySmallElement,
- resizePerfectLineForNWHandler,
- getNormalizedDimensions,
-} from "./sizeHelpers";
-export { showSelectedShapeActions } from "./showSelectedShapeActions";
-
/**
* @deprecated unsafe, use hashElementsVersion instead
*/
diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts
similarity index 94%
rename from packages/excalidraw/element/linearElementEditor.ts
rename to packages/element/src/linearElementEditor.ts
index f9b23f048..8a9117bf8 100644
--- a/packages/excalidraw/element/linearElementEditor.ts
+++ b/packages/element/src/linearElementEditor.ts
@@ -8,31 +8,52 @@ import {
pointDistance,
vectorFromPoint,
} from "@excalidraw/math";
-import { getCurvePathOps } from "@excalidraw/utils/geometry/shape";
+
+import { getCurvePathOps } from "@excalidraw/utils/shape";
+
+import {
+ DRAGGING_THRESHOLD,
+ KEYS,
+ shouldRotateWithDiscreteAngle,
+ getGridPoint,
+ invariant,
+ 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";
-import { DRAGGING_THRESHOLD } from "../constants";
-import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
-import { ShapeCache } from "../scene/ShapeCache";
-import {
- getBezierCurveLength,
- getBezierXY,
- getControlPointsForBezierCurve,
- isPathALoop,
- mapIntervalToBezierT,
-} from "../shapes";
-import { getGridPoint } from "../snapping";
-import { invariant, tupleToCoors } from "../utils";
+import type {
+ AppState,
+ PointerCoords,
+ InteractiveCanvasAppState,
+ AppClassProperties,
+ NullableGridSize,
+ Zoom,
+} from "@excalidraw/excalidraw/types";
+
+import type { Mutable } from "@excalidraw/common/utility-types";
import {
bindOrUnbindLinearElement,
getHoveredElementForBinding,
isBindingEnabled,
} from "./binding";
-import { getElementPointsCoords, getMinMaxXYFromCurvePathOps } from "./bounds";
+import {
+ getElementAbsoluteCoords,
+ getElementPointsCoords,
+ getMinMaxXYFromCurvePathOps,
+} from "./bounds";
+
+import { updateElbowArrowPoints } from "./elbowArrow";
+
import { headingIsHorizontal, vectorToHeading } from "./heading";
-import { mutateElement } from "./mutateElement";
+import { bumpVersion, mutateElement } from "./mutateElement";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import {
isBindingElement,
@@ -40,7 +61,17 @@ import {
isFixedPointBinding,
} from "./typeChecks";
-import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
+import { ShapeCache } from "./ShapeCache";
+
+import {
+ isPathALoop,
+ getBezierCurveLength,
+ getControlPointsForBezierCurve,
+ mapIntervalToBezierT,
+ getBezierXY,
+} from "./shapes";
+
+import { getLockedLinearCursorAlignSize } from "./sizeHelpers";
import type { Bounds } from "./bounds";
import type {
@@ -57,17 +88,6 @@ import type {
FixedSegment,
ExcalidrawElbowArrowElement,
} from "./types";
-import type Scene from "../scene/Scene";
-import type { Store } from "../store";
-import type {
- AppState,
- PointerCoords,
- InteractiveCanvasAppState,
- AppClassProperties,
- NullableGridSize,
- Zoom,
-} from "../types";
-import type { Mutable } from "../utility-types";
const editorMidPointsCache: {
version: number | null;
@@ -113,6 +133,7 @@ export class LinearElementEditor {
};
if (!pointsEqual(element.points[0], pointFrom(0, 0))) {
console.error("Linear element is not normalized", Error().stack);
+ LinearElementEditor.normalizePoints(element);
}
this.selectedPointsIndices = null;
@@ -232,15 +253,15 @@ export class LinearElementEditor {
) => void,
linearElementEditor: LinearElementEditor,
scene: Scene,
- ): boolean {
+ ): LinearElementEditor | null {
if (!linearElementEditor) {
- return false;
+ return null;
}
const { elementId } = linearElementEditor;
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
- return false;
+ return null;
}
if (
@@ -248,24 +269,18 @@ export class LinearElementEditor {
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
linearElementEditor.pointerDownState.lastClickedPoint !== 0
) {
- return false;
+ return null;
}
const selectedPointsIndices = isElbowArrow(element)
- ? linearElementEditor.selectedPointsIndices
- ?.reduce(
- (startEnd, index) =>
- (index === 0
- ? [0, startEnd[1]]
- : [startEnd[0], element.points.length - 1]) as [
- boolean | number,
- boolean | number,
- ],
- [false, false] as [number | boolean, number | boolean],
- )
- .filter(
- (idx: number | boolean): idx is number => typeof idx === "number",
- )
+ ? [
+ !!linearElementEditor.selectedPointsIndices?.includes(0)
+ ? 0
+ : undefined,
+ !!linearElementEditor.selectedPointsIndices?.find((idx) => idx > 0)
+ ? element.points.length - 1
+ : undefined,
+ ].filter((idx): idx is number => idx !== undefined)
: linearElementEditor.selectedPointsIndices;
const lastClickedPoint = isElbowArrow(element)
? linearElementEditor.pointerDownState.lastClickedPoint > 0
@@ -274,9 +289,7 @@ export class LinearElementEditor {
: linearElementEditor.pointerDownState.lastClickedPoint;
// point that's being dragged (out of all selected points)
- const draggingPoint = element.points[lastClickedPoint] as
- | [number, number]
- | undefined;
+ const draggingPoint = element.points[lastClickedPoint];
if (selectedPointsIndices && draggingPoint) {
if (
@@ -384,10 +397,28 @@ export class LinearElementEditor {
}
}
- return true;
+ return {
+ ...linearElementEditor,
+ selectedPointsIndices,
+ segmentMidPointHoveredCoords:
+ lastClickedPoint !== 0 &&
+ lastClickedPoint !== element.points.length - 1
+ ? this.getPointGlobalCoordinates(
+ element,
+ draggingPoint,
+ elementsMap,
+ )
+ : null,
+ hoverPointIndex:
+ lastClickedPoint === 0 ||
+ lastClickedPoint === element.points.length - 1
+ ? lastClickedPoint
+ : -1,
+ isDragging: true,
+ };
}
- return false;
+ return null;
}
static handlePointerUp(
@@ -1264,6 +1295,7 @@ export class LinearElementEditor {
startBinding?: PointBinding | null;
endBinding?: PointBinding | null;
},
+ sceneElementsMap?: NonDeletedSceneElementsMap,
) {
const { points } = element;
@@ -1307,6 +1339,7 @@ export class LinearElementEditor {
dragging || targetPoint.isDragging === true,
false,
),
+ sceneElementsMap,
},
);
}
@@ -1420,6 +1453,7 @@ export class LinearElementEditor {
options?: {
isDragging?: boolean;
zoom?: AppState["zoom"];
+ sceneElementsMap?: NonDeletedSceneElementsMap;
},
) {
if (isElbowArrow(element)) {
@@ -1445,9 +1479,28 @@ export class LinearElementEditor {
updates.points = Array.from(nextPoints);
- mutateElement(element, updates, true, {
- isDragging: options?.isDragging,
- });
+ 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);
} else {
const nextCoords = getElementPointsCoords(element, nextPoints);
const prevCoords = getElementPointsCoords(element, element.points);
diff --git a/packages/excalidraw/element/mutateElement.ts b/packages/element/src/mutateElement.ts
similarity index 92%
rename from packages/excalidraw/element/mutateElement.ts
rename to packages/element/src/mutateElement.ts
index fc96ec312..d87007369 100644
--- a/packages/excalidraw/element/mutateElement.ts
+++ b/packages/element/src/mutateElement.ts
@@ -1,16 +1,24 @@
+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 { getSizeFromPoints } from "../points";
-import { randomInteger } from "../random";
-import Scene from "../scene/Scene";
-import { ShapeCache } from "../scene/ShapeCache";
-import { getUpdatedTimestamp, toBrandedType } from "../utils";
+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 { Mutable } from "../utility-types";
export type ElementUpdate = Omit<
Partial,
diff --git a/packages/excalidraw/element/newElement.ts b/packages/element/src/newElement.ts
similarity index 60%
rename from packages/excalidraw/element/newElement.ts
rename to packages/element/src/newElement.ts
index d11c4c20f..53a2f05ae 100644
--- a/packages/excalidraw/element/newElement.ts
+++ b/packages/element/src/newElement.ts
@@ -1,32 +1,30 @@
-import type { Radians } from "@excalidraw/math";
-
import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
- ORIG_ID,
VERTICAL_ALIGN,
-} from "../constants";
-import { getLineHeight } from "../fonts";
-import { getNewGroupIdsForDuplication } from "../groups";
-import { randomInteger, randomId } from "../random";
-import {
- arrayToMap,
+ randomInteger,
+ randomId,
getFontString,
getUpdatedTimestamp,
- isTestEnv,
-} from "../utils";
+ getLineHeight,
+} from "@excalidraw/common";
-import { getResizedElementAbsoluteCoords } from "./bounds";
-import { bumpVersion, newElementWith } from "./mutateElement";
+import type { Radians } from "@excalidraw/math";
+
+import type { MarkOptional, Merge } from "@excalidraw/common/utility-types";
+
+import {
+ getElementAbsoluteCoords,
+ getResizedElementAbsoluteCoords,
+} from "./bounds";
+import { newElementWith } from "./mutateElement";
import { getBoundTextMaxWidth } from "./textElement";
import { normalizeText, measureText } from "./textMeasurements";
import { wrapText } from "./textWrapping";
-import { getElementAbsoluteCoords } from ".";
-
import type {
ExcalidrawElement,
ExcalidrawImageElement,
@@ -35,7 +33,6 @@ import type {
ExcalidrawGenericElement,
NonDeleted,
TextAlign,
- GroupId,
VerticalAlign,
Arrowhead,
ExcalidrawFreeDrawElement,
@@ -50,8 +47,6 @@ import type {
FixedSegment,
ExcalidrawElbowArrowElement,
} from "./types";
-import type { AppState } from "../types";
-import type { MarkOptional, Merge, Mutable } from "../utility-types";
export type ElementConstructorOpts = MarkOptional<
Omit,
@@ -538,260 +533,3 @@ export const newImageElement = (
crop: opts.crop ?? null,
};
};
-
-// Simplified deep clone for the purpose of cloning ExcalidrawElement.
-//
-// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
-// Typed arrays and other non-null objects.
-//
-// Adapted from https://github.com/lukeed/klona
-//
-// The reason for `deepCopyElement()` wrapper is type safety (only allow
-// passing ExcalidrawElement as the top-level argument).
-const _deepCopyElement = (val: any, depth: number = 0) => {
- // only clone non-primitives
- if (val == null || typeof val !== "object") {
- return val;
- }
-
- const objectType = Object.prototype.toString.call(val);
-
- if (objectType === "[object Object]") {
- const tmp =
- typeof val.constructor === "function"
- ? Object.create(Object.getPrototypeOf(val))
- : {};
- for (const key in val) {
- if (val.hasOwnProperty(key)) {
- // don't copy non-serializable objects like these caches. They'll be
- // populated when the element is rendered.
- if (depth === 0 && (key === "shape" || key === "canvas")) {
- continue;
- }
- tmp[key] = _deepCopyElement(val[key], depth + 1);
- }
- }
- return tmp;
- }
-
- if (Array.isArray(val)) {
- let k = val.length;
- const arr = new Array(k);
- while (k--) {
- arr[k] = _deepCopyElement(val[k], depth + 1);
- }
- return arr;
- }
-
- // we're not cloning non-array & non-plain-object objects because we
- // don't support them on excalidraw elements yet. If we do, we need to make
- // sure we start cloning them, so let's warn about it.
- if (import.meta.env.DEV) {
- if (
- objectType !== "[object Object]" &&
- objectType !== "[object Array]" &&
- objectType.startsWith("[object ")
- ) {
- console.warn(
- `_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`,
- );
- }
- }
-
- return val;
-};
-
-/**
- * Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or
- * any value. The purpose is to to break object references for immutability
- * reasons, whenever we want to keep the original element, but ensure it's not
- * mutated.
- *
- * Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
- * Typed arrays and other non-null objects.
- */
-export const deepCopyElement = (
- val: T,
-): Mutable => {
- return _deepCopyElement(val);
-};
-
-const __test__defineOrigId = (clonedObj: object, origId: string) => {
- Object.defineProperty(clonedObj, ORIG_ID, {
- value: origId,
- writable: false,
- enumerable: false,
- });
-};
-
-/**
- * utility wrapper to generate new id.
- */
-const regenerateId = () => {
- return randomId();
-};
-
-/**
- * Duplicate an element, often used in the alt-drag operation.
- * Note that this method has gotten a bit complicated since the
- * introduction of gruoping/ungrouping elements.
- * @param editingGroupId The current group being edited. The new
- * element will inherit this group and its
- * parents.
- * @param groupIdMapForOperation A Map that maps old group IDs to
- * duplicated ones. If you are duplicating
- * multiple elements at once, share this map
- * amongst all of them
- * @param element Element to duplicate
- * @param overrides Any element properties to override
- */
-export const duplicateElement = (
- editingGroupId: AppState["editingGroupId"],
- groupIdMapForOperation: Map,
- element: TElement,
- overrides?: Partial,
-): Readonly => {
- let copy = deepCopyElement(element);
-
- if (isTestEnv()) {
- __test__defineOrigId(copy, element.id);
- }
-
- copy.id = regenerateId();
- copy.boundElements = null;
- copy.updated = getUpdatedTimestamp();
- copy.seed = randomInteger();
- copy.groupIds = getNewGroupIdsForDuplication(
- copy.groupIds,
- editingGroupId,
- (groupId) => {
- if (!groupIdMapForOperation.has(groupId)) {
- groupIdMapForOperation.set(groupId, regenerateId());
- }
- return groupIdMapForOperation.get(groupId)!;
- },
- );
- if (overrides) {
- copy = Object.assign(copy, overrides);
- }
- return copy;
-};
-
-/**
- * Clones elements, regenerating their ids (including bindings) and group ids.
- *
- * If bindings don't exist in the elements array, they are removed. Therefore,
- * it's advised to supply the whole elements array, or sets of elements that
- * are encapsulated (such as library items), if the purpose is to retain
- * bindings to the cloned elements intact.
- *
- * NOTE by default does not randomize or regenerate anything except the id.
- */
-export const duplicateElements = (
- elements: readonly ExcalidrawElement[],
- opts?: {
- /** NOTE also updates version flags and `updated` */
- randomizeSeed: boolean;
- },
-) => {
- const clonedElements: ExcalidrawElement[] = [];
-
- const origElementsMap = arrayToMap(elements);
-
- // used for for migrating old ids to new ids
- const elementNewIdsMap = new Map<
- /* orig */ ExcalidrawElement["id"],
- /* new */ ExcalidrawElement["id"]
- >();
-
- const maybeGetNewId = (id: ExcalidrawElement["id"]) => {
- // if we've already migrated the element id, return the new one directly
- if (elementNewIdsMap.has(id)) {
- return elementNewIdsMap.get(id)!;
- }
- // if we haven't migrated the element id, but an old element with the same
- // id exists, generate a new id for it and return it
- if (origElementsMap.has(id)) {
- const newId = regenerateId();
- elementNewIdsMap.set(id, newId);
- return newId;
- }
- // if old element doesn't exist, return null to mark it for removal
- return null;
- };
-
- const groupNewIdsMap = new Map* orig */ GroupId, /* new */ GroupId>();
-
- for (const element of elements) {
- const clonedElement: Mutable = _deepCopyElement(element);
-
- clonedElement.id = maybeGetNewId(element.id)!;
- if (isTestEnv()) {
- __test__defineOrigId(clonedElement, element.id);
- }
-
- if (opts?.randomizeSeed) {
- clonedElement.seed = randomInteger();
- bumpVersion(clonedElement);
- }
-
- if (clonedElement.groupIds) {
- clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
- if (!groupNewIdsMap.has(groupId)) {
- groupNewIdsMap.set(groupId, regenerateId());
- }
- return groupNewIdsMap.get(groupId)!;
- });
- }
-
- if ("containerId" in clonedElement && clonedElement.containerId) {
- const newContainerId = maybeGetNewId(clonedElement.containerId);
- clonedElement.containerId = newContainerId;
- }
-
- if ("boundElements" in clonedElement && clonedElement.boundElements) {
- clonedElement.boundElements = clonedElement.boundElements.reduce(
- (
- acc: Mutable>,
- binding,
- ) => {
- const newBindingId = maybeGetNewId(binding.id);
- if (newBindingId) {
- acc.push({ ...binding, id: newBindingId });
- }
- return acc;
- },
- [],
- );
- }
-
- if ("endBinding" in clonedElement && clonedElement.endBinding) {
- const newEndBindingId = maybeGetNewId(clonedElement.endBinding.elementId);
- clonedElement.endBinding = newEndBindingId
- ? {
- ...clonedElement.endBinding,
- elementId: newEndBindingId,
- }
- : null;
- }
- if ("startBinding" in clonedElement && clonedElement.startBinding) {
- const newEndBindingId = maybeGetNewId(
- clonedElement.startBinding.elementId,
- );
- clonedElement.startBinding = newEndBindingId
- ? {
- ...clonedElement.startBinding,
- elementId: newEndBindingId,
- }
- : null;
- }
-
- if (clonedElement.frameId) {
- clonedElement.frameId = maybeGetNewId(clonedElement.frameId);
- }
-
- clonedElements.push(clonedElement);
- }
-
- return clonedElements;
-};
diff --git a/packages/excalidraw/renderer/renderElement.ts b/packages/element/src/renderElement.ts
similarity index 97%
rename from packages/excalidraw/renderer/renderElement.ts
rename to packages/element/src/renderElement.ts
index 9209c46d6..c8091e8ed 100644
--- a/packages/excalidraw/renderer/renderElement.ts
+++ b/packages/element/src/renderElement.ts
@@ -1,8 +1,8 @@
-import { isRightAngleRads } from "@excalidraw/math";
-import { getStroke } from "perfect-freehand";
import rough from "roughjs/bin/rough";
+import { getStroke } from "perfect-freehand";
+
+import { isRightAngleRads } from "@excalidraw/math";
-import { getDefaultAppState } from "../appState";
import {
BOUND_TEXT_PADDING,
DEFAULT_REDUCED_GLOBAL_ALPHA,
@@ -10,18 +10,39 @@ import {
FRAME_STYLE,
MIME_TYPES,
THEME,
-} from "../constants";
-import { getElementAbsoluteCoords } from "../element/bounds";
-import { getUncroppedImageElement } from "../element/cropElement";
-import { LinearElementEditor } from "../element/linearElementEditor";
+ distance,
+ getFontString,
+ isRTL,
+ getVerticalOffset,
+} from "@excalidraw/common";
+
+import type {
+ AppState,
+ StaticCanvasAppState,
+ Zoom,
+ InteractiveCanvasAppState,
+ ElementsPendingErasure,
+ PendingExcalidrawElements,
+ NormalizedZoomValue,
+} from "@excalidraw/excalidraw/types";
+
+import type {
+ StaticCanvasRenderConfig,
+ RenderableElementsMap,
+ InteractiveCanvasRenderConfig,
+} from "@excalidraw/excalidraw/scene/types";
+
+import { getElementAbsoluteCoords } from "./bounds";
+import { getUncroppedImageElement } from "./cropElement";
+import { LinearElementEditor } from "./linearElementEditor";
import {
getBoundTextElement,
getContainerCoords,
getContainerElement,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
-} from "../element/textElement";
-import { getLineHeightInPx } from "../element/textMeasurements";
+} from "./textElement";
+import { getLineHeightInPx } from "./textMeasurements";
import {
isTextElement,
isLinearElement,
@@ -31,12 +52,11 @@ import {
hasBoundTextElement,
isMagicFrameElement,
isImageElement,
-} from "../element/typeChecks";
-import { getVerticalOffset } from "../fonts";
-import { getContainingFrame } from "../frame";
-import { ShapeCache } from "../scene/ShapeCache";
-import { getCornerRadius } from "../shapes";
-import { distance, getFontString, isRTL } from "../utils";
+} from "./typeChecks";
+import { getContainingFrame } from "./frame";
+import { getCornerRadius } from "./shapes";
+
+import { ShapeCache } from "./ShapeCache";
import type {
ExcalidrawElement,
@@ -48,20 +68,8 @@ import type {
ExcalidrawFrameLikeElement,
NonDeletedSceneElementsMap,
ElementsMap,
-} from "../element/types";
-import type {
- StaticCanvasRenderConfig,
- RenderableElementsMap,
- InteractiveCanvasRenderConfig,
-} from "../scene/types";
-import type {
- AppState,
- StaticCanvasAppState,
- Zoom,
- InteractiveCanvasAppState,
- ElementsPendingErasure,
- PendingExcalidrawElements,
-} from "../types";
+} from "./types";
+
import type { StrokeOptions } from "perfect-freehand";
import type { RoughCanvas } from "roughjs/bin/canvas";
@@ -72,8 +80,6 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
export const IMAGE_INVERT_FILTER =
"invert(100%) hue-rotate(180deg) saturate(1.25)";
-const defaultAppState = getDefaultAppState();
-
const isPendingImageElement = (
element: ExcalidrawElement,
renderConfig: StaticCanvasRenderConfig,
@@ -533,7 +539,11 @@ const generateElementWithCanvas = (
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
) => {
- const zoom: Zoom = renderConfig ? appState.zoom : defaultAppState.zoom;
+ const zoom: Zoom = renderConfig
+ ? appState.zoom
+ : {
+ value: 1 as NormalizedZoomValue,
+ };
const prevElementWithCanvas = elementWithCanvasCache.get(element);
const shouldRegenerateBecauseZoom =
prevElementWithCanvas &&
diff --git a/packages/excalidraw/element/resizeElements.ts b/packages/element/src/resizeElements.ts
similarity index 99%
rename from packages/excalidraw/element/resizeElements.ts
rename to packages/element/src/resizeElements.ts
index cd51b8bd5..3ff405603 100644
--- a/packages/excalidraw/element/resizeElements.ts
+++ b/packages/element/src/resizeElements.ts
@@ -8,12 +8,20 @@ import {
type LocalPoint,
} from "@excalidraw/math";
+import {
+ MIN_FONT_SIZE,
+ SHIFT_LOCKING_ANGLE,
+ rescalePoints,
+ getFontString,
+} from "@excalidraw/common";
+
import type { GlobalPoint } from "@excalidraw/math";
-import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
-import { isInGroup } from "../groups";
-import { rescalePoints } from "../points";
-import { getFontString } from "../utils";
+import type Scene from "@excalidraw/excalidraw/scene/Scene";
+
+import type { PointerDownState } from "@excalidraw/excalidraw/types";
+
+import type { Mutable } from "@excalidraw/common/utility-types";
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
import {
@@ -50,6 +58,8 @@ import {
isTextElement,
} from "./typeChecks";
+import { isInGroup } from "./groups";
+
import type { BoundingBox } from "./bounds";
import type {
MaybeTransformHandleType,
@@ -67,9 +77,6 @@ import type {
SceneElementsMap,
ExcalidrawElbowArrowElement,
} from "./types";
-import type Scene from "../scene/Scene";
-import type { PointerDownState } from "../types";
-import type { Mutable } from "../utility-types";
// Returns true when transform (resizing/rotation) happened
export const transformElements = (
diff --git a/packages/excalidraw/element/resizeTest.ts b/packages/element/src/resizeTest.ts
similarity index 98%
rename from packages/excalidraw/element/resizeTest.ts
rename to packages/element/src/resizeTest.ts
index 1eb36d0b2..411dcf9a7 100644
--- a/packages/excalidraw/element/resizeTest.ts
+++ b/packages/element/src/resizeTest.ts
@@ -5,9 +5,11 @@ import {
type Radians,
} from "@excalidraw/math";
+import { SIDE_RESIZING_THRESHOLD } from "@excalidraw/common";
+
import type { GlobalPoint, LineSegment, LocalPoint } from "@excalidraw/math";
-import { SIDE_RESIZING_THRESHOLD } from "../constants";
+import type { AppState, Device, Zoom } from "@excalidraw/excalidraw/types";
import { getElementAbsoluteCoords } from "./bounds";
import {
@@ -18,7 +20,6 @@ import {
} from "./transformHandles";
import { isImageElement, isLinearElement } from "./typeChecks";
-import type { AppState, Device, Zoom } from "../types";
import type { Bounds } from "./bounds";
import type {
TransformHandleType,
diff --git a/packages/excalidraw/scene/selection.ts b/packages/element/src/selection.ts
similarity index 94%
rename from packages/excalidraw/scene/selection.ts
rename to packages/element/src/selection.ts
index 02f8f05e7..a76703920 100644
--- a/packages/excalidraw/scene/selection.ts
+++ b/packages/element/src/selection.ts
@@ -1,20 +1,25 @@
-import { getElementAbsoluteCoords, getElementBounds } from "../element";
-import { isElementInViewport } from "../element/sizeHelpers";
-import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks";
+import { isShallowEqual } from "@excalidraw/common";
+
+import type {
+ AppState,
+ InteractiveCanvasAppState,
+} from "@excalidraw/excalidraw/types";
+
+import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
+import { isElementInViewport } from "./sizeHelpers";
+import { isBoundToContainer, isFrameLikeElement } from "./typeChecks";
import {
elementOverlapsWithFrame,
getContainingFrame,
getFrameChildren,
-} from "../frame";
-import { isShallowEqual } from "../utils";
+} from "./frame";
import type {
ElementsMap,
ElementsMapOrArray,
ExcalidrawElement,
NonDeletedExcalidrawElement,
-} from "../element/types";
-import type { AppState, InteractiveCanvasAppState } from "../types";
+} from "./types";
/**
* Frames and their containing elements are not to be selected at the same time.
diff --git a/packages/excalidraw/shapes.tsx b/packages/element/src/shapes.ts
similarity index 81%
rename from packages/excalidraw/shapes.tsx
rename to packages/element/src/shapes.ts
index b2c391a42..96542c538 100644
--- a/packages/excalidraw/shapes.tsx
+++ b/packages/element/src/shapes.ts
@@ -1,3 +1,11 @@
+import {
+ DEFAULT_ADAPTIVE_RADIUS,
+ DEFAULT_PROPORTIONAL_RADIUS,
+ LINE_CONFIRM_THRESHOLD,
+ ROUNDNESS,
+ invariant,
+ elementCenterPoint,
+} from "@excalidraw/common";
import {
isPoint,
pointFrom,
@@ -16,131 +24,26 @@ import {
getFreedrawShape,
getPolygonShape,
type GeometricShape,
-} from "@excalidraw/utils/geometry/shape";
+} from "@excalidraw/utils/shape";
-import {
- ArrowIcon,
- DiamondIcon,
- EllipseIcon,
- EraserIcon,
- FreedrawIcon,
- ImageIcon,
- LineIcon,
- RectangleIcon,
- SelectionIcon,
- TextIcon,
-} from "./components/icons";
-import {
- DEFAULT_ADAPTIVE_RADIUS,
- DEFAULT_PROPORTIONAL_RADIUS,
- LINE_CONFIRM_THRESHOLD,
- ROUNDNESS,
-} from "./constants";
-import { getElementAbsoluteCoords } from "./element";
-import { shouldTestInside } from "./element/collision";
-import { LinearElementEditor } from "./element/linearElementEditor";
-import { getBoundTextElement } from "./element/textElement";
-import { KEYS } from "./keys";
-import { ShapeCache } from "./scene/ShapeCache";
-import { invariant } from "./utils";
+import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types";
+
+import { shouldTestInside } from "./collision";
+import { LinearElementEditor } from "./linearElementEditor";
+import { getBoundTextElement } from "./textElement";
+import { ShapeCache } from "./ShapeCache";
+
+import { getElementAbsoluteCoords, type Bounds } from "./bounds";
-import type { Bounds } from "./element/bounds";
import type {
ElementsMap,
ExcalidrawElement,
ExcalidrawLinearElement,
NonDeleted,
-} from "./element/types";
-import type { NormalizedZoomValue, Zoom } from "./types";
-
-export const SHAPES = [
- {
- icon: SelectionIcon,
- value: "selection",
- key: KEYS.V,
- numericKey: KEYS["1"],
- fillable: true,
- },
- {
- icon: RectangleIcon,
- value: "rectangle",
- key: KEYS.R,
- numericKey: KEYS["2"],
- fillable: true,
- },
- {
- icon: DiamondIcon,
- value: "diamond",
- key: KEYS.D,
- numericKey: KEYS["3"],
- fillable: true,
- },
- {
- icon: EllipseIcon,
- value: "ellipse",
- key: KEYS.O,
- numericKey: KEYS["4"],
- fillable: true,
- },
- {
- icon: ArrowIcon,
- value: "arrow",
- key: KEYS.A,
- numericKey: KEYS["5"],
- fillable: true,
- },
- {
- icon: LineIcon,
- value: "line",
- key: KEYS.L,
- numericKey: KEYS["6"],
- fillable: true,
- },
- {
- icon: FreedrawIcon,
- value: "freedraw",
- key: [KEYS.P, KEYS.X],
- numericKey: KEYS["7"],
- fillable: false,
- },
- {
- icon: TextIcon,
- value: "text",
- key: KEYS.T,
- numericKey: KEYS["8"],
- fillable: false,
- },
- {
- icon: ImageIcon,
- value: "image",
- key: null,
- numericKey: KEYS["9"],
- fillable: false,
- },
- {
- icon: EraserIcon,
- value: "eraser",
- key: KEYS.E,
- numericKey: KEYS["0"],
- fillable: false,
- },
-] as const;
-
-export const findShapeByKey = (key: string) => {
- const shape = SHAPES.find((shape, index) => {
- return (
- (shape.numericKey != null && key === shape.numericKey.toString()) ||
- (shape.key &&
- (typeof shape.key === "string"
- ? shape.key === key
- : (shape.key as readonly string[]).includes(key)))
- );
- });
- return shape?.value || null;
-};
+} from "./types";
/**
- * get the pure geometric shape of an excalidraw element
+ * get the pure geometric shape of an excalidraw elementw
* which is then used for hit detection
*/
export const getElementShape = (
@@ -395,7 +298,7 @@ export const aabbForElement = (
midY: element.y + element.height / 2,
};
- const center = pointFrom(bbox.midX, bbox.midY);
+ const center = elementCenterPoint(element);
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(bbox.minX, bbox.minY),
center,
diff --git a/packages/excalidraw/element/showSelectedShapeActions.ts b/packages/element/src/showSelectedShapeActions.ts
similarity index 79%
rename from packages/excalidraw/element/showSelectedShapeActions.ts
rename to packages/element/src/showSelectedShapeActions.ts
index 44c2e75c3..6f918cfbd 100644
--- a/packages/excalidraw/element/showSelectedShapeActions.ts
+++ b/packages/element/src/showSelectedShapeActions.ts
@@ -1,6 +1,7 @@
-import { getSelectedElements } from "../scene";
+import type { UIAppState } from "@excalidraw/excalidraw/types";
+
+import { getSelectedElements } from "./selection";
-import type { UIAppState } from "../types";
import type { NonDeletedExcalidrawElement } from "./types";
export const showSelectedShapeActions = (
@@ -13,6 +14,7 @@ export const showSelectedShapeActions = (
((appState.activeTool.type !== "custom" &&
(appState.editingTextElement ||
(appState.activeTool.type !== "selection" &&
+ appState.activeTool.type !== "lasso" &&
appState.activeTool.type !== "eraser" &&
appState.activeTool.type !== "hand" &&
appState.activeTool.type !== "laser"))) ||
diff --git a/packages/excalidraw/element/sizeHelpers.ts b/packages/element/src/sizeHelpers.ts
similarity index 97%
rename from packages/excalidraw/element/sizeHelpers.ts
rename to packages/element/src/sizeHelpers.ts
index 33b13e188..7a84dadba 100644
--- a/packages/excalidraw/element/sizeHelpers.ts
+++ b/packages/element/src/sizeHelpers.ts
@@ -1,12 +1,15 @@
-import { SHIFT_LOCKING_ANGLE } from "../constants";
-import { viewportCoordsToSceneCoords } from "../utils";
+import {
+ SHIFT_LOCKING_ANGLE,
+ viewportCoordsToSceneCoords,
+} from "@excalidraw/common";
+
+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";
-import type { AppState, Offsets, Zoom } from "../types";
// TODO: remove invisible elements consistently actions, so that invisible elements are not recorded by the store, exported, broadcasted or persisted
// - perhaps could be as part of a standalone 'cleanup' action, in addition to 'finalize'
diff --git a/packages/excalidraw/element/sortElements.ts b/packages/element/src/sortElements.ts
similarity index 98%
rename from packages/excalidraw/element/sortElements.ts
rename to packages/element/src/sortElements.ts
index d395adf2f..c98ff9d52 100644
--- a/packages/excalidraw/element/sortElements.ts
+++ b/packages/element/src/sortElements.ts
@@ -1,4 +1,4 @@
-import { arrayToMapWithIndex } from "../utils";
+import { arrayToMapWithIndex } from "@excalidraw/common";
import type { ExcalidrawElement } from "./types";
diff --git a/packages/excalidraw/element/textElement.ts b/packages/element/src/textElement.ts
similarity index 89%
rename from packages/excalidraw/element/textElement.ts
rename to packages/element/src/textElement.ts
index 9893ba5d6..ea27c318f 100644
--- a/packages/excalidraw/element/textElement.ts
+++ b/packages/element/src/textElement.ts
@@ -5,8 +5,12 @@ import {
DEFAULT_FONT_SIZE,
TEXT_ALIGN,
VERTICAL_ALIGN,
-} from "../constants";
-import { getFontString, arrayToMap } from "../utils";
+ getFontString,
+} from "@excalidraw/common";
+
+import type { AppState } from "@excalidraw/excalidraw/types";
+
+import type { ExtractSetType } from "@excalidraw/common/utility-types";
import {
resetOriginalContainerCache,
@@ -16,9 +20,11 @@ import { LinearElementEditor } from "./linearElementEditor";
import { mutateElement } from "./mutateElement";
import { measureText } from "./textMeasurements";
import { wrapText } from "./textWrapping";
-import { isBoundToContainer, isArrowElement } from "./typeChecks";
-
-import { isTextElement } from ".";
+import {
+ isBoundToContainer,
+ isArrowElement,
+ isTextElement,
+} from "./typeChecks";
import type { MaybeTransformHandleType } from "./transformHandles";
import type {
@@ -30,8 +36,6 @@ import type {
ExcalidrawTextElementWithContainer,
NonDeletedExcalidrawElement,
} from "./types";
-import type { AppState } from "../types";
-import type { ExtractSetType } from "../utility-types";
export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement,
@@ -112,48 +116,6 @@ export const redrawTextBoundingBox = (
mutateElement(textElement, boundTextUpdates, informMutation);
};
-export const bindTextToShapeAfterDuplication = (
- newElements: ExcalidrawElement[],
- oldElements: ExcalidrawElement[],
- oldIdToDuplicatedId: Map,
-): void => {
- const newElementsMap = arrayToMap(newElements) as Map<
- ExcalidrawElement["id"],
- ExcalidrawElement
- >;
- oldElements.forEach((element) => {
- const newElementId = oldIdToDuplicatedId.get(element.id) as string;
- const boundTextElementId = getBoundTextElementId(element);
-
- if (boundTextElementId) {
- const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
- if (newTextElementId) {
- const newContainer = newElementsMap.get(newElementId);
- if (newContainer) {
- mutateElement(newContainer, {
- boundElements: (element.boundElements || [])
- .filter(
- (boundElement) =>
- boundElement.id !== newTextElementId &&
- boundElement.id !== boundTextElementId,
- )
- .concat({
- type: "text",
- id: newTextElementId,
- }),
- });
- }
- const newTextElement = newElementsMap.get(newTextElementId);
- if (newTextElement && isTextElement(newTextElement)) {
- mutateElement(newTextElement, {
- containerId: newContainer ? newElementId : null,
- });
- }
- }
- }
- });
-};
-
export const handleBindTextResize = (
container: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
diff --git a/packages/excalidraw/element/textMeasurements.ts b/packages/element/src/textMeasurements.ts
similarity index 98%
rename from packages/excalidraw/element/textMeasurements.ts
rename to packages/element/src/textMeasurements.ts
index 840896cfc..5e790261d 100644
--- a/packages/excalidraw/element/textMeasurements.ts
+++ b/packages/element/src/textMeasurements.ts
@@ -2,8 +2,10 @@ import {
BOUND_TEXT_PADDING,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_FAMILY,
-} from "../constants";
-import { getFontString, isTestEnv, normalizeEOL } from "../utils";
+ getFontString,
+ isTestEnv,
+ normalizeEOL,
+} from "@excalidraw/common";
import type { FontString, ExcalidrawTextElement } from "./types";
diff --git a/packages/excalidraw/element/textWrapping.ts b/packages/element/src/textWrapping.ts
similarity index 99%
rename from packages/excalidraw/element/textWrapping.ts
rename to packages/element/src/textWrapping.ts
index 5df7051c0..5ec9bb42a 100644
--- a/packages/excalidraw/element/textWrapping.ts
+++ b/packages/element/src/textWrapping.ts
@@ -1,4 +1,4 @@
-import { ENV } from "../constants";
+import { isDevEnv, isTestEnv } from "@excalidraw/common";
import { charWidth, getLineWidth } from "./textMeasurements";
@@ -562,7 +562,7 @@ const isSingleCharacter = (maybeSingleCharacter: string) => {
* Invariant for the word wrapping algorithm.
*/
const satisfiesWordInvariant = (word: string) => {
- if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
+ if (isTestEnv() || isDevEnv()) {
if (/\s/.test(word)) {
throw new Error("Word should not contain any whitespaces!");
}
diff --git a/packages/excalidraw/element/transformHandles.ts b/packages/element/src/transformHandles.ts
similarity index 98%
rename from packages/excalidraw/element/transformHandles.ts
rename to packages/element/src/transformHandles.ts
index ab5691df8..f2b0cd278 100644
--- a/packages/excalidraw/element/transformHandles.ts
+++ b/packages/element/src/transformHandles.ts
@@ -1,12 +1,18 @@
-import { pointFrom, pointRotateRads } from "@excalidraw/math";
-
-import type { Radians } from "@excalidraw/math";
-
import {
DEFAULT_TRANSFORM_HANDLE_SPACING,
isAndroid,
isIOS,
-} from "../constants";
+} from "@excalidraw/common";
+
+import { pointFrom, pointRotateRads } from "@excalidraw/math";
+
+import type { Radians } from "@excalidraw/math";
+
+import type {
+ Device,
+ InteractiveCanvasAppState,
+ Zoom,
+} from "@excalidraw/excalidraw/types";
import { getElementAbsoluteCoords } from "./bounds";
import {
@@ -16,7 +22,6 @@ import {
isLinearElement,
} from "./typeChecks";
-import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
import type { Bounds } from "./bounds";
import type {
ElementsMap,
diff --git a/packages/excalidraw/element/typeChecks.ts b/packages/element/src/typeChecks.ts
similarity index 97%
rename from packages/excalidraw/element/typeChecks.ts
rename to packages/element/src/typeChecks.ts
index 77ac38f9d..54619726d 100644
--- a/packages/excalidraw/element/typeChecks.ts
+++ b/packages/element/src/typeChecks.ts
@@ -1,8 +1,9 @@
-import { ROUNDNESS } from "../constants";
-import { assertNever } from "../utils";
+import { ROUNDNESS, assertNever } from "@excalidraw/common";
+
+import type { ElementOrToolType } from "@excalidraw/excalidraw/types";
+
+import type { MarkNonNullable } from "@excalidraw/common/utility-types";
-import type { ElementOrToolType } from "../types";
-import type { MarkNonNullable } from "../utility-types";
import type { Bounds } from "./bounds";
import type {
ExcalidrawElement,
diff --git a/packages/excalidraw/element/types.ts b/packages/element/src/types.ts
similarity index 99%
rename from packages/excalidraw/element/types.ts
rename to packages/element/src/types.ts
index 49ad800af..3b40135d5 100644
--- a/packages/excalidraw/element/types.ts
+++ b/packages/element/src/types.ts
@@ -6,13 +6,14 @@ import type {
TEXT_ALIGN,
THEME,
VERTICAL_ALIGN,
-} from "../constants";
+} from "@excalidraw/common";
+
import type {
MakeBrand,
MarkNonNullable,
Merge,
ValueOf,
-} from "../utility-types";
+} from "@excalidraw/common/utility-types";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
diff --git a/packages/excalidraw/element/utils.ts b/packages/element/src/utils.ts
similarity index 97%
rename from packages/excalidraw/element/utils.ts
rename to packages/element/src/utils.ts
index 8992850dc..57b1e4346 100644
--- a/packages/excalidraw/element/utils.ts
+++ b/packages/element/src/utils.ts
@@ -10,11 +10,13 @@ import {
type GlobalPoint,
} from "@excalidraw/math";
+import { elementCenterPoint } from "@excalidraw/common";
+
import type { Curve, LineSegment } from "@excalidraw/math";
-import { getCornerRadius } from "../shapes";
+import { getCornerRadius } from "./shapes";
-import { getDiamondPoints } from ".";
+import { getDiamondPoints } from "./bounds";
import type {
ExcalidrawDiamondElement,
@@ -68,10 +70,7 @@ export function deconstructRectanguloidElement(
return [sides, []];
}
- const center = pointFrom(
- element.x + element.width / 2,
- element.y + element.height / 2,
- );
+ const center = elementCenterPoint(element);
const r = rectangle(
pointFrom(element.x, element.y),
@@ -254,10 +253,7 @@ export function deconstructDiamondElement(
return [[topRight, bottomRight, bottomLeft, topLeft], []];
}
- const center = pointFrom(
- element.x + element.width / 2,
- element.y + element.height / 2,
- );
+ const center = elementCenterPoint(element);
const [top, right, bottom, left]: GlobalPoint[] = [
pointFrom(element.x + topX, element.y + topY),
diff --git a/packages/excalidraw/zindex.ts b/packages/element/src/zindex.ts
similarity index 92%
rename from packages/excalidraw/zindex.ts
rename to packages/element/src/zindex.ts
index 8ffcec5d1..e09142e4a 100644
--- a/packages/excalidraw/zindex.ts
+++ b/packages/element/src/zindex.ts
@@ -1,15 +1,18 @@
-import { isFrameLikeElement } from "./element/typeChecks";
-import { syncMovedIndices } from "./fractionalIndex";
-import { getElementsInGroup } from "./groups";
-import { getSelectedElements } from "./scene";
-import Scene from "./scene/Scene";
-import { arrayToMap, findIndex, findLastIndex } from "./utils";
+import { arrayToMap, findIndex, findLastIndex } from "@excalidraw/common";
-import type {
- ExcalidrawElement,
- ExcalidrawFrameLikeElement,
-} from "./element/types";
-import type { AppState } from "./types";
+import type { AppState } from "@excalidraw/excalidraw/types";
+
+import type Scene from "@excalidraw/excalidraw/scene/Scene";
+
+import { isFrameLikeElement } from "./typeChecks";
+
+import { getElementsInGroup } from "./groups";
+
+import { syncMovedIndices } from "./fractionalIndex";
+
+import { getSelectedElements } from "./selection";
+
+import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "./types";
const isOfTargetFrame = (element: ExcalidrawElement, frameId: string) => {
return element.frameId === frameId || element.id === frameId;
@@ -79,11 +82,11 @@ const getTargetIndexAccountingForBinding = (
nextElement: ExcalidrawElement,
elements: readonly ExcalidrawElement[],
direction: "left" | "right",
+ scene: Scene,
) => {
if ("containerId" in nextElement && nextElement.containerId) {
- const containerElement = Scene.getScene(nextElement)!.getElement(
- nextElement.containerId,
- );
+ // TODO: why not to get the container from the nextElements?
+ const containerElement = scene.getElement(nextElement.containerId);
if (containerElement) {
return direction === "left"
? Math.min(
@@ -100,8 +103,7 @@ const getTargetIndexAccountingForBinding = (
(binding) => binding.type !== "arrow",
)?.id;
if (boundElementId) {
- const boundTextElement =
- Scene.getScene(nextElement)!.getElement(boundElementId);
+ const boundTextElement = scene.getElement(boundElementId);
if (boundTextElement) {
return direction === "left"
? Math.min(
@@ -151,6 +153,7 @@ const getTargetIndex = (
* If whole frame (including all children) is being moved, supply `null`.
*/
containingFrame: ExcalidrawFrameLikeElement["id"] | null,
+ scene: Scene,
) => {
const sourceElement = elements[boundaryIndex];
@@ -190,8 +193,12 @@ const getTargetIndex = (
sourceElement?.groupIds.join("") === nextElement?.groupIds.join("")
) {
return (
- getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
- candidateIndex
+ getTargetIndexAccountingForBinding(
+ nextElement,
+ elements,
+ direction,
+ scene,
+ ) ?? candidateIndex
);
} else if (!nextElement?.groupIds.includes(appState.editingGroupId)) {
// candidate element is outside current editing group → prevent
@@ -214,8 +221,12 @@ const getTargetIndex = (
if (!nextElement.groupIds.length) {
return (
- getTargetIndexAccountingForBinding(nextElement, elements, direction) ??
- candidateIndex
+ getTargetIndexAccountingForBinding(
+ nextElement,
+ elements,
+ direction,
+ scene,
+ ) ?? candidateIndex
);
}
@@ -255,6 +266,7 @@ const shiftElementsByOne = (
elements: readonly ExcalidrawElement[],
appState: AppState,
direction: "left" | "right",
+ scene: Scene,
) => {
const indicesToMove = getIndicesToMove(elements, appState);
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
@@ -289,6 +301,7 @@ const shiftElementsByOne = (
boundaryIndex,
direction,
containingFrame,
+ scene,
);
if (targetIndex === -1 || boundaryIndex === targetIndex) {
@@ -502,15 +515,17 @@ function shiftElementsAccountingForFrames(
export const moveOneLeft = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
+ scene: Scene,
) => {
- return shiftElementsByOne(allElements, appState, "left");
+ return shiftElementsByOne(allElements, appState, "left", scene);
};
export const moveOneRight = (
allElements: readonly ExcalidrawElement[],
appState: AppState,
+ scene: Scene,
) => {
- return shiftElementsByOne(allElements, appState, "right");
+ return shiftElementsByOne(allElements, appState, "right", scene);
};
export const moveAllLeft = (
diff --git a/packages/excalidraw/tests/align.test.tsx b/packages/element/tests/align.test.tsx
similarity index 97%
rename from packages/excalidraw/tests/align.test.tsx
rename to packages/element/tests/align.test.tsx
index d29e497ec..2dcafc65b 100644
--- a/packages/excalidraw/tests/align.test.tsx
+++ b/packages/element/tests/align.test.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import { KEYS } from "@excalidraw/common";
import {
actionAlignVerticallyCentered,
@@ -8,14 +8,17 @@ import {
actionAlignBottom,
actionAlignLeft,
actionAlignRight,
-} from "../actions";
-import { defaultLang, setLanguage } from "../i18n";
-import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
+} from "@excalidraw/excalidraw/actions";
+import { defaultLang, setLanguage } from "@excalidraw/excalidraw/i18n";
+import { Excalidraw } from "@excalidraw/excalidraw";
-import { API } from "./helpers/api";
-import { UI, Pointer, Keyboard } from "./helpers/ui";
-import { act, unmountComponent, render } from "./test-utils";
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
+import {
+ act,
+ unmountComponent,
+ render,
+} from "@excalidraw/excalidraw/tests/test-utils";
const mouse = new Pointer("mouse");
diff --git a/packages/excalidraw/tests/binding.test.tsx b/packages/element/tests/binding.test.tsx
similarity index 96%
rename from packages/excalidraw/tests/binding.test.tsx
rename to packages/element/tests/binding.test.tsx
index 1c50062c1..f57d7793a 100644
--- a/packages/excalidraw/tests/binding.test.tsx
+++ b/packages/element/tests/binding.test.tsx
@@ -1,15 +1,16 @@
+import { KEYS, arrayToMap } from "@excalidraw/common";
+
import { pointFrom } from "@excalidraw/math";
-import React from "react";
-import { actionWrapTextInContainer } from "../actions/actionBoundText";
-import { getTransformHandles } from "../element/transformHandles";
-import { Excalidraw, isLinearElement } from "../index";
-import { KEYS } from "../keys";
-import { arrayToMap } from "../utils";
+import { actionWrapTextInContainer } from "@excalidraw/excalidraw/actions/actionBoundText";
-import { API } from "./helpers/api";
-import { UI, Pointer, Keyboard } from "./helpers/ui";
-import { fireEvent, render } from "./test-utils";
+import { Excalidraw, isLinearElement } from "@excalidraw/excalidraw";
+
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui";
+import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils";
+
+import { getTransformHandles } from "../src/transformHandles";
const { h } = window;
diff --git a/packages/excalidraw/element/bounds.test.ts b/packages/element/tests/bounds.test.ts
similarity index 95%
rename from packages/excalidraw/element/bounds.test.ts
rename to packages/element/tests/bounds.test.ts
index 936ebf797..22c669f28 100644
--- a/packages/excalidraw/element/bounds.test.ts
+++ b/packages/element/tests/bounds.test.ts
@@ -1,13 +1,12 @@
import { pointFrom } from "@excalidraw/math";
+import { arrayToMap, ROUNDNESS } from "@excalidraw/common";
+
import type { LocalPoint } from "@excalidraw/math";
-import { ROUNDNESS } from "../constants";
-import { arrayToMap } from "../utils";
+import { getElementAbsoluteCoords, getElementBounds } from "../src/bounds";
-import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
-
-import type { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
+import type { ExcalidrawElement, ExcalidrawLinearElement } from "../src/types";
const _ce = ({
x,
diff --git a/packages/element/tests/duplicate.test.tsx b/packages/element/tests/duplicate.test.tsx
new file mode 100644
index 000000000..7492bcc58
--- /dev/null
+++ b/packages/element/tests/duplicate.test.tsx
@@ -0,0 +1,732 @@
+import React from "react";
+import { pointFrom } from "@excalidraw/math";
+
+import {
+ FONT_FAMILY,
+ ORIG_ID,
+ ROUNDNESS,
+ isPrimitive,
+} from "@excalidraw/common";
+
+import { Excalidraw } from "@excalidraw/excalidraw";
+
+import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
+
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+
+import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
+
+import {
+ act,
+ assertElements,
+ getCloneByOrigId,
+ render,
+} from "@excalidraw/excalidraw/tests/test-utils";
+
+import type { LocalPoint } from "@excalidraw/math";
+
+import { mutateElement } from "../src/mutateElement";
+import { duplicateElement, duplicateElements } from "../src/duplicate";
+
+import type { ExcalidrawLinearElement } from "../src/types";
+
+const { h } = window;
+const mouse = new Pointer("mouse");
+
+const assertCloneObjects = (source: any, clone: any) => {
+ for (const key in clone) {
+ if (clone.hasOwnProperty(key) && !isPrimitive(clone[key])) {
+ expect(clone[key]).not.toBe(source[key]);
+ if (source[key]) {
+ assertCloneObjects(source[key], clone[key]);
+ }
+ }
+ }
+};
+
+describe("duplicating single elements", () => {
+ it("clones arrow element", () => {
+ const element = API.createElement({
+ type: "arrow",
+ x: 0,
+ y: 0,
+ strokeColor: "#000000",
+ backgroundColor: "transparent",
+ fillStyle: "hachure",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
+ roughness: 1,
+ opacity: 100,
+ });
+
+ // @ts-ignore
+ element.__proto__ = { hello: "world" };
+
+ mutateElement(element, {
+ points: [pointFrom(1, 2), pointFrom(3, 4)],
+ });
+
+ const copy = duplicateElement(null, new Map(), element, undefined, true);
+
+ assertCloneObjects(element, copy);
+
+ // assert we clone the object's prototype
+ // @ts-ignore
+ expect(copy.__proto__).toEqual({ hello: "world" });
+ expect(copy.hasOwnProperty("hello")).toBe(false);
+
+ expect(copy.points).not.toBe(element.points);
+ expect(copy).not.toHaveProperty("shape");
+ expect(copy.id).not.toBe(element.id);
+ expect(typeof copy.id).toBe("string");
+ expect(copy.seed).not.toBe(element.seed);
+ expect(typeof copy.seed).toBe("number");
+ expect(copy).toEqual({
+ ...element,
+ id: copy.id,
+ seed: copy.seed,
+ version: copy.version,
+ versionNonce: copy.versionNonce,
+ });
+ });
+
+ it("clones text element", () => {
+ const element = API.createElement({
+ type: "text",
+ x: 0,
+ y: 0,
+ strokeColor: "#000000",
+ backgroundColor: "transparent",
+ fillStyle: "hachure",
+ strokeWidth: 1,
+ strokeStyle: "solid",
+ roundness: null,
+ roughness: 1,
+ opacity: 100,
+ text: "hello",
+ fontSize: 20,
+ fontFamily: FONT_FAMILY.Virgil,
+ textAlign: "left",
+ verticalAlign: "top",
+ });
+
+ const copy = duplicateElement(null, new Map(), element);
+
+ assertCloneObjects(element, copy);
+
+ expect(copy).not.toHaveProperty("points");
+ expect(copy).not.toHaveProperty("shape");
+ expect(copy.id).not.toBe(element.id);
+ expect(typeof copy.id).toBe("string");
+ expect(typeof copy.seed).toBe("number");
+ });
+});
+
+describe("duplicating multiple elements", () => {
+ it("duplicateElements should clone bindings", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ id: "rectangle1",
+ boundElements: [
+ { id: "arrow1", type: "arrow" },
+ { id: "arrow2", type: "arrow" },
+ { id: "text1", type: "text" },
+ ],
+ });
+
+ const text1 = API.createElement({
+ type: "text",
+ id: "text1",
+ containerId: "rectangle1",
+ });
+
+ const arrow1 = API.createElement({
+ type: "arrow",
+ id: "arrow1",
+ startBinding: {
+ elementId: "rectangle1",
+ focus: 0.2,
+ gap: 7,
+ fixedPoint: [0.5, 1],
+ },
+ });
+
+ const arrow2 = API.createElement({
+ type: "arrow",
+ id: "arrow2",
+ endBinding: {
+ elementId: "rectangle1",
+ focus: 0.2,
+ gap: 7,
+ fixedPoint: [0.5, 1],
+ },
+ boundElements: [{ id: "text2", type: "text" }],
+ });
+
+ const text2 = API.createElement({
+ type: "text",
+ id: "text2",
+ containerId: "arrow2",
+ });
+
+ // -------------------------------------------------------------------------
+
+ const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
+ const { newElements: clonedElements } = duplicateElements({
+ type: "everything",
+ elements: origElements,
+ });
+
+ // generic id in-equality checks
+ // --------------------------------------------------------------------------
+ expect(origElements.map((e) => e.type)).toEqual(
+ clonedElements.map((e) => e.type),
+ );
+ origElements.forEach((origElement, idx) => {
+ const clonedElement = clonedElements[idx];
+ expect(origElement).toEqual(
+ expect.objectContaining({
+ id: expect.not.stringMatching(clonedElement.id),
+ type: clonedElement.type,
+ }),
+ );
+ if ("containerId" in origElement) {
+ expect(origElement.containerId).not.toBe(
+ (clonedElement as any).containerId,
+ );
+ }
+ if ("endBinding" in origElement) {
+ if (origElement.endBinding) {
+ expect(origElement.endBinding.elementId).not.toBe(
+ (clonedElement as any).endBinding?.elementId,
+ );
+ } else {
+ expect((clonedElement as any).endBinding).toBeNull();
+ }
+ }
+ if ("startBinding" in origElement) {
+ if (origElement.startBinding) {
+ expect(origElement.startBinding.elementId).not.toBe(
+ (clonedElement as any).startBinding?.elementId,
+ );
+ } else {
+ expect((clonedElement as any).startBinding).toBeNull();
+ }
+ }
+ });
+ // --------------------------------------------------------------------------
+
+ const clonedArrows = clonedElements.filter(
+ (e) => e.type === "arrow",
+ ) as ExcalidrawLinearElement[];
+
+ const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
+ clonedElements as any as typeof origElements;
+
+ expect(clonedText1.containerId).toBe(clonedRectangle.id);
+ expect(
+ clonedRectangle.boundElements!.find((e) => e.id === clonedText1.id),
+ ).toEqual(
+ expect.objectContaining({
+ id: clonedText1.id,
+ type: clonedText1.type,
+ }),
+ );
+ expect(clonedRectangle.type).toBe("rectangle");
+
+ clonedArrows.forEach((arrow) => {
+ expect(
+ clonedRectangle.boundElements!.find((e) => e.id === arrow.id),
+ ).toEqual(
+ expect.objectContaining({
+ id: arrow.id,
+ type: arrow.type,
+ }),
+ );
+
+ if (arrow.endBinding) {
+ expect(arrow.endBinding.elementId).toBe(clonedRectangle.id);
+ }
+ if (arrow.startBinding) {
+ expect(arrow.startBinding.elementId).toBe(clonedRectangle.id);
+ }
+ });
+
+ expect(clonedArrow2.boundElements).toEqual([
+ { type: "text", id: clonedArrowLabel.id },
+ ]);
+ expect(clonedArrowLabel.containerId).toBe(clonedArrow2.id);
+ });
+
+ it("should remove id references of elements that aren't found", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ id: "rectangle1",
+ boundElements: [
+ // should keep
+ { id: "arrow1", type: "arrow" },
+ // should drop
+ { id: "arrow-not-exists", type: "arrow" },
+ // should drop
+ { id: "text-not-exists", type: "text" },
+ ],
+ });
+
+ const arrow1 = API.createElement({
+ type: "arrow",
+ id: "arrow1",
+ startBinding: {
+ elementId: "rectangle1",
+ focus: 0.2,
+ gap: 7,
+ fixedPoint: [0.5, 1],
+ },
+ });
+
+ const text1 = API.createElement({
+ type: "text",
+ id: "text1",
+ containerId: "rectangle-not-exists",
+ });
+
+ const arrow2 = API.createElement({
+ type: "arrow",
+ id: "arrow2",
+ startBinding: {
+ elementId: "rectangle1",
+ focus: 0.2,
+ gap: 7,
+ fixedPoint: [0.5, 1],
+ },
+ endBinding: {
+ elementId: "rectangle-not-exists",
+ focus: 0.2,
+ gap: 7,
+ fixedPoint: [0.5, 1],
+ },
+ });
+
+ const arrow3 = API.createElement({
+ type: "arrow",
+ id: "arrow3",
+ startBinding: {
+ elementId: "rectangle-not-exists",
+ focus: 0.2,
+ gap: 7,
+ fixedPoint: [0.5, 1],
+ },
+ endBinding: {
+ elementId: "rectangle1",
+ focus: 0.2,
+ gap: 7,
+ fixedPoint: [0.5, 1],
+ },
+ });
+
+ // -------------------------------------------------------------------------
+
+ const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
+ const { newElements: clonedElements } = duplicateElements({
+ type: "everything",
+ elements: origElements,
+ }) as any as { newElements: typeof origElements };
+
+ const [
+ clonedRectangle,
+ clonedText1,
+ clonedArrow1,
+ clonedArrow2,
+ clonedArrow3,
+ ] = clonedElements;
+
+ expect(clonedRectangle.boundElements).toEqual([
+ { id: clonedArrow1.id, type: "arrow" },
+ ]);
+
+ expect(clonedText1.containerId).toBe(null);
+
+ expect(clonedArrow2.startBinding).toEqual({
+ ...arrow2.startBinding,
+ elementId: clonedRectangle.id,
+ });
+ expect(clonedArrow2.endBinding).toBe(null);
+ expect(clonedArrow3.startBinding).toBe(null);
+ expect(clonedArrow3.endBinding).toEqual({
+ ...arrow3.endBinding,
+ elementId: clonedRectangle.id,
+ });
+ });
+
+ describe("should duplicate all group ids", () => {
+ it("should regenerate all group ids and keep them consistent across elements", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ groupIds: ["g1"],
+ });
+ const rectangle2 = API.createElement({
+ type: "rectangle",
+ groupIds: ["g2", "g1"],
+ });
+ const rectangle3 = API.createElement({
+ type: "rectangle",
+ groupIds: ["g2", "g1"],
+ });
+
+ const origElements = [rectangle1, rectangle2, rectangle3] as const;
+ const { newElements: clonedElements } = duplicateElements({
+ type: "everything",
+ elements: origElements,
+ }) as any as { newElements: typeof origElements };
+ const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
+ clonedElements;
+
+ expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
+ expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
+ expect(rectangle2.groupIds[1]).not.toBe(clonedRectangle2.groupIds[1]);
+
+ expect(clonedRectangle1.groupIds[0]).toBe(clonedRectangle2.groupIds[1]);
+ expect(clonedRectangle2.groupIds[0]).toBe(clonedRectangle3.groupIds[0]);
+ expect(clonedRectangle2.groupIds[1]).toBe(clonedRectangle3.groupIds[1]);
+ });
+
+ it("should keep and regenerate ids of groups even if invalid", () => {
+ // lone element shouldn't be able to be grouped with itself,
+ // but hard to check against in a performant way so we ignore it
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ groupIds: ["g1"],
+ });
+
+ const {
+ newElements: [clonedRectangle1],
+ } = duplicateElements({ type: "everything", elements: [rectangle1] });
+
+ expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
+ expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
+ });
+ });
+});
+
+describe("duplication z-order", () => {
+ beforeEach(async () => {
+ await render( );
+ });
+
+ it("duplication z order with Cmd+D for the lowest z-ordered element should be +1 for the clone", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ x: 0,
+ y: 0,
+ });
+ const rectangle2 = API.createElement({
+ type: "rectangle",
+ x: 10,
+ y: 10,
+ });
+ const rectangle3 = API.createElement({
+ type: "rectangle",
+ x: 20,
+ y: 20,
+ });
+
+ API.setElements([rectangle1, rectangle2, rectangle3]);
+ API.setSelectedElements([rectangle1]);
+
+ act(() => {
+ h.app.actionManager.executeAction(actionDuplicateSelection);
+ });
+
+ assertElements(h.elements, [
+ { id: rectangle1.id },
+ { [ORIG_ID]: rectangle1.id, selected: true },
+ { id: rectangle2.id },
+ { id: rectangle3.id },
+ ]);
+ });
+
+ it("duplication z order with Cmd+D for the highest z-ordered element should be +1 for the clone", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ x: 0,
+ y: 0,
+ });
+ const rectangle2 = API.createElement({
+ type: "rectangle",
+ x: 10,
+ y: 10,
+ });
+ const rectangle3 = API.createElement({
+ type: "rectangle",
+ x: 20,
+ y: 20,
+ });
+
+ API.setElements([rectangle1, rectangle2, rectangle3]);
+ API.setSelectedElements([rectangle3]);
+
+ act(() => {
+ h.app.actionManager.executeAction(actionDuplicateSelection);
+ });
+
+ assertElements(h.elements, [
+ { id: rectangle1.id },
+ { id: rectangle2.id },
+ { id: rectangle3.id },
+ { [ORIG_ID]: rectangle3.id, selected: true },
+ ]);
+ });
+
+ it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ x: 0,
+ y: 0,
+ });
+ const rectangle2 = API.createElement({
+ type: "rectangle",
+ x: 10,
+ y: 10,
+ });
+ const rectangle3 = API.createElement({
+ type: "rectangle",
+ x: 20,
+ y: 20,
+ });
+
+ API.setElements([rectangle1, rectangle2, rectangle3]);
+
+ mouse.select(rectangle1);
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(rectangle1.x + 5, rectangle1.y + 5);
+ mouse.up(rectangle1.x + 5, rectangle1.y + 5);
+ });
+
+ assertElements(h.elements, [
+ { [ORIG_ID]: rectangle1.id },
+ { id: rectangle1.id, selected: true },
+ { id: rectangle2.id },
+ { id: rectangle3.id },
+ ]);
+ });
+
+ it("duplication z order with alt+drag for the highest z-ordered element should be +1 for the clone", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ x: 0,
+ y: 0,
+ });
+ const rectangle2 = API.createElement({
+ type: "rectangle",
+ x: 10,
+ y: 10,
+ });
+ const rectangle3 = API.createElement({
+ type: "rectangle",
+ x: 20,
+ y: 20,
+ });
+
+ API.setElements([rectangle1, rectangle2, rectangle3]);
+
+ mouse.select(rectangle3);
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(rectangle3.x + 5, rectangle3.y + 5);
+ mouse.up(rectangle3.x + 5, rectangle3.y + 5);
+ });
+
+ assertElements(h.elements, [
+ { id: rectangle1.id },
+ { id: rectangle2.id },
+ { [ORIG_ID]: rectangle3.id },
+ { id: rectangle3.id, selected: true },
+ ]);
+ });
+
+ it("duplication z order with alt+drag for the lowest z-ordered element should be +1 for the clone", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ x: 0,
+ y: 0,
+ });
+ const rectangle2 = API.createElement({
+ type: "rectangle",
+ x: 10,
+ y: 10,
+ });
+ const rectangle3 = API.createElement({
+ type: "rectangle",
+ x: 20,
+ y: 20,
+ });
+
+ API.setElements([rectangle1, rectangle2, rectangle3]);
+
+ mouse.select(rectangle1);
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(rectangle1.x + 5, rectangle1.y + 5);
+ mouse.up(rectangle1.x + 5, rectangle1.y + 5);
+ });
+
+ assertElements(h.elements, [
+ { [ORIG_ID]: rectangle1.id },
+ { id: rectangle1.id, selected: true },
+ { id: rectangle2.id },
+ { id: rectangle3.id },
+ ]);
+ });
+
+ it("duplication z order with alt+drag with grouped elements should consider the group together when determining z-index", () => {
+ const rectangle1 = API.createElement({
+ type: "rectangle",
+ x: 0,
+ y: 0,
+ groupIds: ["group1"],
+ });
+ const rectangle2 = API.createElement({
+ type: "rectangle",
+ x: 10,
+ y: 10,
+ groupIds: ["group1"],
+ });
+ const rectangle3 = API.createElement({
+ type: "rectangle",
+ x: 20,
+ y: 20,
+ groupIds: ["group1"],
+ });
+
+ API.setElements([rectangle1, rectangle2, rectangle3]);
+
+ mouse.select(rectangle1);
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(rectangle1.x + 5, rectangle1.y + 5);
+ mouse.up(rectangle1.x + 15, rectangle1.y + 15);
+ });
+
+ assertElements(h.elements, [
+ { [ORIG_ID]: rectangle1.id },
+ { [ORIG_ID]: rectangle2.id },
+ { [ORIG_ID]: rectangle3.id },
+ { id: rectangle1.id, selected: true },
+ { id: rectangle2.id, selected: true },
+ { id: rectangle3.id, selected: true },
+ ]);
+ });
+
+ it("reverse-duplicating text container (in-order)", async () => {
+ const [rectangle, text] = API.createTextContainer();
+ API.setElements([rectangle, text]);
+ API.setSelectedElements([rectangle, text]);
+
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(rectangle.x + 5, rectangle.y + 5);
+ mouse.up(rectangle.x + 15, rectangle.y + 15);
+ });
+
+ assertElements(h.elements, [
+ { [ORIG_ID]: rectangle.id },
+ {
+ [ORIG_ID]: text.id,
+ containerId: getCloneByOrigId(rectangle.id)?.id,
+ },
+ { id: rectangle.id, selected: true },
+ { id: text.id, containerId: rectangle.id, selected: true },
+ ]);
+ });
+
+ it("reverse-duplicating text container (out-of-order)", async () => {
+ const [rectangle, text] = API.createTextContainer();
+ API.setElements([text, rectangle]);
+ API.setSelectedElements([rectangle, text]);
+
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(rectangle.x + 5, rectangle.y + 5);
+ mouse.up(rectangle.x + 15, rectangle.y + 15);
+ });
+
+ assertElements(h.elements, [
+ { [ORIG_ID]: rectangle.id },
+ {
+ [ORIG_ID]: text.id,
+ containerId: getCloneByOrigId(rectangle.id)?.id,
+ },
+ { id: rectangle.id, selected: true },
+ { id: text.id, containerId: rectangle.id, selected: true },
+ ]);
+ });
+
+ it("reverse-duplicating labeled arrows (in-order)", async () => {
+ const [arrow, text] = API.createLabeledArrow();
+
+ API.setElements([arrow, text]);
+ API.setSelectedElements([arrow, text]);
+
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(arrow.x + 5, arrow.y + 5);
+ mouse.up(arrow.x + 15, arrow.y + 15);
+ });
+
+ assertElements(h.elements, [
+ { [ORIG_ID]: arrow.id },
+ {
+ [ORIG_ID]: text.id,
+ containerId: getCloneByOrigId(arrow.id)?.id,
+ },
+ { id: arrow.id, selected: true },
+ { id: text.id, containerId: arrow.id, selected: true },
+ ]);
+ });
+
+ it("reverse-duplicating labeled arrows (out-of-order)", async () => {
+ const [arrow, text] = API.createLabeledArrow();
+
+ API.setElements([text, arrow]);
+ API.setSelectedElements([arrow, text]);
+
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(arrow.x + 5, arrow.y + 5);
+ mouse.up(arrow.x + 15, arrow.y + 15);
+ });
+
+ assertElements(h.elements, [
+ { [ORIG_ID]: arrow.id },
+ {
+ [ORIG_ID]: text.id,
+ containerId: getCloneByOrigId(arrow.id)?.id,
+ },
+ { id: arrow.id, selected: true },
+ { id: text.id, containerId: arrow.id, selected: true },
+ ]);
+ });
+
+ it("reverse-duplicating bindable element with bound arrow should keep the arrow on the duplicate", () => {
+ const rect = UI.createElement("rectangle", {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ });
+
+ const arrow = UI.createElement("arrow", {
+ x: -100,
+ y: 50,
+ width: 95,
+ height: 0,
+ });
+
+ expect(arrow.endBinding?.elementId).toBe(rect.id);
+
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(5, 5);
+ mouse.up(15, 15);
+ });
+
+ expect(window.h.elements).toHaveLength(3);
+
+ const newRect = window.h.elements[0];
+
+ expect(arrow.endBinding?.elementId).toBe(newRect.id);
+ expect(newRect.boundElements?.[0]?.id).toBe(arrow.id);
+ });
+});
diff --git a/packages/excalidraw/element/elbowArrow.test.tsx b/packages/element/tests/elbowArrow.test.tsx
similarity index 92%
rename from packages/excalidraw/element/elbowArrow.test.tsx
rename to packages/element/tests/elbowArrow.test.tsx
index 91e280d30..b8b5a8b85 100644
--- a/packages/excalidraw/element/elbowArrow.test.tsx
+++ b/packages/element/tests/elbowArrow.test.tsx
@@ -1,31 +1,33 @@
+import { ARROW_TYPE } from "@excalidraw/common";
import { pointFrom } from "@excalidraw/math";
-import React from "react";
+import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
-import type { LocalPoint } from "@excalidraw/math";
+import Scene from "@excalidraw/excalidraw/scene/Scene";
+import { actionSelectAll } from "@excalidraw/excalidraw/actions";
+import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
+
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
-import "../../utils/test-utils";
-import { actionSelectAll } from "../actions";
-import { actionDuplicateSelection } from "../actions/actionDuplicateSelection";
-import { ARROW_TYPE } from "../constants";
-import { Excalidraw, mutateElement } from "../index";
-import Scene from "../scene/Scene";
-import { API } from "../tests/helpers/api";
-import { Pointer, UI } from "../tests/helpers/ui";
import {
act,
fireEvent,
GlobalTestState,
queryByTestId,
render,
-} from "../tests/test-utils";
+} from "@excalidraw/excalidraw/tests/test-utils";
-import { bindLinearElement } from "./binding";
+import "@excalidraw/utils/test-utils";
+
+import type { LocalPoint } from "@excalidraw/math";
+
+import { bindLinearElement } from "../src/binding";
import type {
ExcalidrawArrowElement,
ExcalidrawBindableElement,
ExcalidrawElbowArrowElement,
-} from "./types";
+} from "../src/types";
const { h } = window;
@@ -358,7 +360,7 @@ describe("elbow arrow ui", () => {
expect(arrow.endBinding).not.toBe(null);
});
- it("keeps arrow shape when only the bound arrow is duplicated", async () => {
+ it("changes arrow shape to unbind variant if only the connected elbow arrow is duplicated", async () => {
UI.createElement("rectangle", {
x: -150,
y: -150,
@@ -404,8 +406,8 @@ describe("elbow arrow ui", () => {
expect(duplicatedArrow.elbowed).toBe(true);
expect(duplicatedArrow.points).toEqual([
[0, 0],
- [45, 0],
- [45, 200],
+ [0, 100],
+ [90, 100],
[90, 200],
]);
});
diff --git a/packages/excalidraw/element/flowchart.test.tsx b/packages/element/tests/flowchart.test.tsx
similarity index 97%
rename from packages/excalidraw/element/flowchart.test.tsx
rename to packages/element/tests/flowchart.test.tsx
index bc026e7d7..0130fb4c8 100644
--- a/packages/excalidraw/element/flowchart.test.tsx
+++ b/packages/element/tests/flowchart.test.tsx
@@ -1,9 +1,13 @@
-import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
-import { reseed } from "../random";
-import { API } from "../tests/helpers/api";
-import { UI, Keyboard, Pointer } from "../tests/helpers/ui";
-import { render, unmountComponent } from "../tests/test-utils";
+import { KEYS, reseed } from "@excalidraw/common";
+
+import { Excalidraw } from "@excalidraw/excalidraw";
+
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
+import {
+ render,
+ unmountComponent,
+} from "@excalidraw/excalidraw/tests/test-utils";
unmountComponent();
diff --git a/packages/excalidraw/tests/fractionalIndex.test.ts b/packages/element/tests/fractionalIndex.test.ts
similarity index 98%
rename from packages/excalidraw/tests/fractionalIndex.test.ts
rename to packages/element/tests/fractionalIndex.test.ts
index dbd55bd92..5d040e29f 100644
--- a/packages/excalidraw/tests/fractionalIndex.test.ts
+++ b/packages/element/tests/fractionalIndex.test.ts
@@ -1,18 +1,24 @@
/* eslint-disable no-lone-blocks */
import { generateKeyBetween } from "fractional-indexing";
-import { deepCopyElement } from "../element/newElement";
-import { InvalidFractionalIndexError } from "../errors";
+import { arrayToMap } from "@excalidraw/common";
+
import {
syncInvalidIndices,
syncMovedIndices,
validateFractionalIndices,
-} from "../fractionalIndex";
-import { arrayToMap } from "../utils";
+} from "@excalidraw/element/fractionalIndex";
-import { API } from "./helpers/api";
+import { deepCopyElement } from "@excalidraw/element/duplicate";
-import type { ExcalidrawElement, FractionalIndex } from "../element/types";
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+
+import type {
+ ExcalidrawElement,
+ FractionalIndex,
+} from "@excalidraw/element/types";
+
+import { InvalidFractionalIndexError } from "../src/fractionalIndex";
describe("sync invalid indices with array order", () => {
describe("should NOT sync empty array", () => {
diff --git a/packages/excalidraw/frame.test.tsx b/packages/element/tests/frame.test.tsx
similarity index 97%
rename from packages/excalidraw/frame.test.tsx
rename to packages/element/tests/frame.test.tsx
index fce420c02..47f2160ac 100644
--- a/packages/excalidraw/frame.test.tsx
+++ b/packages/element/tests/frame.test.tsx
@@ -1,10 +1,16 @@
-import { API } from "./tests/helpers/api";
-import { Keyboard, Pointer } from "./tests/helpers/ui";
-import { getCloneByOrigId, render } from "./tests/test-utils";
+import {
+ convertToExcalidrawElements,
+ Excalidraw,
+} from "@excalidraw/excalidraw";
-import { convertToExcalidrawElements, Excalidraw } from "./index";
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
+import {
+ getCloneByOrigId,
+ render,
+} from "@excalidraw/excalidraw/tests/test-utils";
-import type { ExcalidrawElement } from "./element/types";
+import type { ExcalidrawElement } from "../src/types";
const { h } = window;
const mouse = new Pointer("mouse");
diff --git a/packages/excalidraw/tests/resize.test.tsx b/packages/element/tests/resize.test.tsx
similarity index 98%
rename from packages/excalidraw/tests/resize.test.tsx
rename to packages/element/tests/resize.test.tsx
index 055b097b3..f3804e2a2 100644
--- a/packages/excalidraw/tests/resize.test.tsx
+++ b/packages/element/tests/resize.test.tsx
@@ -1,28 +1,33 @@
import { pointFrom } from "@excalidraw/math";
-import React from "react";
+
+import { Excalidraw } from "@excalidraw/excalidraw";
+import {
+ KEYS,
+ getSizeFromPoints,
+ reseed,
+ arrayToMap,
+} from "@excalidraw/common";
+
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
+import {
+ render,
+ unmountComponent,
+} from "@excalidraw/excalidraw/tests/test-utils";
import type { LocalPoint } from "@excalidraw/math";
-import { getElementPointsCoords } from "../element/bounds";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import { resizeSingleElement } from "../element/resizeElements";
-import { isLinearElement } from "../element/typeChecks";
-import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
-import { getSizeFromPoints } from "../points";
-import { reseed } from "../random";
-import { arrayToMap } from "../utils";
+import { isLinearElement } from "../src/typeChecks";
+import { resizeSingleElement } from "../src/resizeElements";
+import { LinearElementEditor } from "../src/linearElementEditor";
+import { getElementPointsCoords } from "../src/bounds";
-import { API } from "./helpers/api";
-import { UI, Keyboard, Pointer } from "./helpers/ui";
-import { render, unmountComponent } from "./test-utils";
-
-import type { Bounds } from "../element/bounds";
+import type { Bounds } from "../src/bounds";
import type {
ExcalidrawElbowArrowElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
-} from "../element/types";
+} from "../src/types";
unmountComponent();
diff --git a/packages/excalidraw/scene/selection.test.ts b/packages/element/tests/selection.test.ts
similarity index 95%
rename from packages/excalidraw/scene/selection.test.ts
rename to packages/element/tests/selection.test.ts
index 644d2129f..fbcdb2693 100644
--- a/packages/excalidraw/scene/selection.test.ts
+++ b/packages/element/tests/selection.test.ts
@@ -1,4 +1,4 @@
-import { makeNextSelectedElementIds } from "./selection";
+import { makeNextSelectedElementIds } from "../src/selection";
describe("makeNextSelectedElementIds", () => {
const _makeNextSelectedElementIds = (
diff --git a/packages/excalidraw/element/sizeHelpers.test.ts b/packages/element/tests/sizeHelpers.test.ts
similarity index 95%
rename from packages/excalidraw/element/sizeHelpers.test.ts
rename to packages/element/tests/sizeHelpers.test.ts
index c882e1f3c..168a9a2ad 100644
--- a/packages/excalidraw/element/sizeHelpers.test.ts
+++ b/packages/element/tests/sizeHelpers.test.ts
@@ -1,15 +1,15 @@
import { vi } from "vitest";
-import * as constants from "../constants";
+import * as constants from "@excalidraw/common";
-import { getPerfectElementSize } from "./sizeHelpers";
+import { getPerfectElementSize } from "../src/sizeHelpers";
const EPSILON_DIGITS = 3;
// Needed so that we can mock the value of constants which is done in
// below tests. In Jest this wasn't needed as global override was possible
// but vite doesn't allow that hence we need to mock
vi.mock(
- "../constants.ts",
+ "@excalidraw/common",
//@ts-ignore
async (importOriginal) => {
const module: any = await importOriginal();
diff --git a/packages/excalidraw/element/sortElements.test.ts b/packages/element/tests/sortElements.test.ts
similarity index 97%
rename from packages/excalidraw/element/sortElements.test.ts
rename to packages/element/tests/sortElements.test.ts
index 5f7c4b2e6..509e5e9d0 100644
--- a/packages/excalidraw/element/sortElements.test.ts
+++ b/packages/element/tests/sortElements.test.ts
@@ -1,9 +1,9 @@
-import { API } from "../tests/helpers/api";
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
-import { mutateElement } from "./mutateElement";
-import { normalizeElementOrder } from "./sortElements";
+import { mutateElement } from "../src/mutateElement";
+import { normalizeElementOrder } from "../src/sortElements";
-import type { ExcalidrawElement } from "./types";
+import type { ExcalidrawElement } from "../src/types";
const assertOrder = (
elements: readonly ExcalidrawElement[],
diff --git a/packages/excalidraw/element/textElement.test.ts b/packages/element/tests/textElement.test.ts
similarity index 94%
rename from packages/excalidraw/element/textElement.test.ts
rename to packages/element/tests/textElement.test.ts
index 41531a738..5c10681a7 100644
--- a/packages/excalidraw/element/textElement.test.ts
+++ b/packages/element/tests/textElement.test.ts
@@ -1,16 +1,17 @@
-import { FONT_FAMILY } from "../constants";
-import { getLineHeight } from "../fonts";
-import { API } from "../tests/helpers/api";
+import { getLineHeight } from "@excalidraw/common";
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+
+import { FONT_FAMILY } from "@excalidraw/common";
import {
computeContainerDimensionForBoundText,
getContainerCoords,
getBoundTextMaxWidth,
getBoundTextMaxHeight,
-} from "./textElement";
-import { detectLineHeight, getLineHeightInPx } from "./textMeasurements";
+} from "../src/textElement";
+import { detectLineHeight, getLineHeightInPx } from "../src/textMeasurements";
-import type { ExcalidrawTextElementWithContainer } from "./types";
+import type { ExcalidrawTextElementWithContainer } from "../src/types";
describe("Test measureText", () => {
describe("Test getContainerCoords", () => {
diff --git a/packages/excalidraw/element/textWrapping.test.ts b/packages/element/tests/textWrapping.test.ts
similarity index 99%
rename from packages/excalidraw/element/textWrapping.test.ts
rename to packages/element/tests/textWrapping.test.ts
index 357736a2e..87c96a4c9 100644
--- a/packages/excalidraw/element/textWrapping.test.ts
+++ b/packages/element/tests/textWrapping.test.ts
@@ -1,6 +1,6 @@
-import { wrapText, parseTokens } from "./textWrapping";
+import { wrapText, parseTokens } from "../src/textWrapping";
-import type { FontString } from "./types";
+import type { FontString } from "../src/types";
describe("Test wrapText", () => {
// font is irrelevant as jsdom does not support FontFace API
diff --git a/packages/excalidraw/element/typeChecks.test.ts b/packages/element/tests/typeChecks.test.ts
similarity index 93%
rename from packages/excalidraw/element/typeChecks.test.ts
rename to packages/element/tests/typeChecks.test.ts
index 44e4dd755..6be81aaa6 100644
--- a/packages/excalidraw/element/typeChecks.test.ts
+++ b/packages/element/tests/typeChecks.test.ts
@@ -1,6 +1,6 @@
-import { API } from "../tests/helpers/api";
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
-import { hasBoundTextElement } from "./typeChecks";
+import { hasBoundTextElement } from "../src/typeChecks";
describe("Test TypeChecks", () => {
describe("Test hasBoundTextElement", () => {
diff --git a/packages/excalidraw/tests/zindex.test.tsx b/packages/element/tests/zindex.test.tsx
similarity index 98%
rename from packages/excalidraw/tests/zindex.test.tsx
rename to packages/element/tests/zindex.test.tsx
index c8e166aff..997cb56f8 100644
--- a/packages/excalidraw/tests/zindex.test.tsx
+++ b/packages/element/tests/zindex.test.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import { reseed } from "@excalidraw/common";
import {
actionSendBackward,
@@ -6,20 +6,27 @@ import {
actionBringToFront,
actionSendToBack,
actionDuplicateSelection,
-} from "../actions";
-import { selectGroupsForSelectedElements } from "../groups";
-import { Excalidraw } from "../index";
-import { reseed } from "../random";
+} from "@excalidraw/excalidraw/actions";
-import { API } from "./helpers/api";
-import { act, getCloneByOrigId, render, unmountComponent } from "./test-utils";
+import { Excalidraw } from "@excalidraw/excalidraw";
+
+import { API } from "@excalidraw/excalidraw/tests/helpers/api";
+import {
+ act,
+ getCloneByOrigId,
+ render,
+ unmountComponent,
+} from "@excalidraw/excalidraw/tests/test-utils";
+
+import type { AppState } from "@excalidraw/excalidraw/types";
+
+import { selectGroupsForSelectedElements } from "../src/groups";
import type {
ExcalidrawElement,
ExcalidrawFrameElement,
ExcalidrawSelectionElement,
-} from "../element/types";
-import type { AppState } from "../types";
+} from "../src/types";
unmountComponent();
diff --git a/packages/element/tsconfig.json b/packages/element/tsconfig.json
new file mode 100644
index 000000000..6450145b1
--- /dev/null
+++ b/packages/element/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./dist/types"
+ },
+ "include": ["src/**/*", "global.d.ts"],
+ "exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
+}
diff --git a/packages/eslintrc.base.json b/packages/eslintrc.base.json
new file mode 100644
index 000000000..88d006a09
--- /dev/null
+++ b/packages/eslintrc.base.json
@@ -0,0 +1,21 @@
+{
+ "overrides": [
+ {
+ "files": ["src/**/*.{ts,tsx}"],
+ "rules": {
+ "@typescript-eslint/no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": ["../../excalidraw", "../../../packages/excalidraw", "@excalidraw/excalidraw"],
+ "message": "Do not import from '@excalidraw/excalidraw' package anything but types, as this package must be independent.",
+ "allowTypeImports": true
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/packages/excalidraw/actions/actionAddToLibrary.ts b/packages/excalidraw/actions/actionAddToLibrary.ts
index b246918a0..9216e52c2 100644
--- a/packages/excalidraw/actions/actionAddToLibrary.ts
+++ b/packages/excalidraw/actions/actionAddToLibrary.ts
@@ -1,7 +1,7 @@
-import { LIBRARY_DISABLED_TYPES } from "../constants";
-import { deepCopyElement } from "../element/newElement";
+import { LIBRARY_DISABLED_TYPES, randomId } from "@excalidraw/common";
+import { deepCopyElement } from "@excalidraw/element/duplicate";
+
import { t } from "../i18n";
-import { randomId } from "../random";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
diff --git a/packages/excalidraw/actions/actionAlign.tsx b/packages/excalidraw/actions/actionAlign.tsx
index b42122a50..46023d61e 100644
--- a/packages/excalidraw/actions/actionAlign.tsx
+++ b/packages/excalidraw/actions/actionAlign.tsx
@@ -1,4 +1,17 @@
-import { alignElements } from "../align";
+import { getNonDeletedElements } from "@excalidraw/element";
+
+import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
+
+import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
+
+import { KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
+
+import { alignElements } from "@excalidraw/element/align";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import type { Alignment } from "@excalidraw/element/align";
+
import { ToolButton } from "../components/ToolButton";
import {
AlignBottomIcon,
@@ -8,19 +21,14 @@ import {
CenterHorizontallyIcon,
CenterVerticallyIcon,
} from "../components/icons";
-import { getNonDeletedElements } from "../element";
-import { isFrameLikeElement } from "../element/typeChecks";
-import { updateFrameMembershipOfSelectedElements } from "../frame";
+
import { t } from "../i18n";
-import { KEYS } from "../keys";
+
import { isSomeElementSelected } from "../scene";
import { CaptureUpdateAction } from "../store";
-import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
-import type { Alignment } from "../align";
-import type { ExcalidrawElement } from "../element/types";
import type { AppClassProperties, AppState, UIAppState } from "../types";
export const alignActionsPredicate = (
diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx
index 4da3f99a1..ae18c0d98 100644
--- a/packages/excalidraw/actions/actionBoundText.tsx
+++ b/packages/excalidraw/actions/actionBoundText.tsx
@@ -3,40 +3,50 @@ import {
ROUNDNESS,
TEXT_ALIGN,
VERTICAL_ALIGN,
-} from "../constants";
-import { isTextElement, newElement } from "../element";
+ arrayToMap,
+ getFontString,
+} from "@excalidraw/common";
import {
getOriginalContainerHeightFromCache,
resetOriginalContainerCache,
updateOriginalContainerCache,
-} from "../element/containerCache";
-import { mutateElement } from "../element/mutateElement";
+} from "@excalidraw/element/containerCache";
+
import {
computeBoundTextPosition,
computeContainerDimensionForBoundText,
getBoundTextElement,
redrawTextBoundingBox,
-} from "../element/textElement";
-import { measureText } from "../element/textMeasurements";
+} from "@excalidraw/element/textElement";
+
import {
hasBoundTextElement,
isTextBindableContainer,
+ isTextElement,
isUsingAdaptiveRadius,
-} from "../element/typeChecks";
-import { syncMovedIndices } from "../fractionalIndex";
-import { CaptureUpdateAction } from "../store";
-import { arrayToMap, getFontString } from "../utils";
+} from "@excalidraw/element/typeChecks";
-import { register } from "./register";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+import { measureText } from "@excalidraw/element/textMeasurements";
+
+import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
+
+import { newElement } from "@excalidraw/element/newElement";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextContainer,
ExcalidrawTextElement,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import type { Mutable } from "@excalidraw/common/utility-types";
+
+import { CaptureUpdateAction } from "../store";
+
+import { register } from "./register";
+
import type { AppState } from "../types";
-import type { Mutable } from "../utility-types";
export const actionUnbindText = register({
name: "unbindText",
@@ -216,8 +226,8 @@ export const actionWrapTextInContainer = register({
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
- const areTextElements = selectedElements.every((el) => isTextElement(el));
- return selectedElements.length > 0 && areTextElements;
+ const someTextElements = selectedElements.some((el) => isTextElement(el));
+ return selectedElements.length > 0 && someTextElements;
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx
index f3e0a8a32..a8bd56e82 100644
--- a/packages/excalidraw/actions/actionCanvas.tsx
+++ b/packages/excalidraw/actions/actionCanvas.tsx
@@ -1,16 +1,35 @@
import { clamp, roundToStep } from "@excalidraw/math";
+import {
+ DEFAULT_CANVAS_BACKGROUND_PICKS,
+ CURSOR_TYPE,
+ MAX_ZOOM,
+ MIN_ZOOM,
+ THEME,
+ ZOOM_STEP,
+ getShortcutKey,
+ updateActiveTool,
+ CODES,
+ KEYS,
+} from "@excalidraw/common";
+
+import { getNonDeletedElements } from "@excalidraw/element";
+import { newElementWith } from "@excalidraw/element/mutateElement";
+import { getCommonBounds, type SceneBounds } from "@excalidraw/element/bounds";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import {
getDefaultAppState,
isEraserActive,
isHandToolActive,
} from "../appState";
-import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
import { ToolButton } from "../components/ToolButton";
import { Tooltip } from "../components/Tooltip";
import {
handIcon,
+ LassoIcon,
MoonIcon,
SunIcon,
TrashIcon,
@@ -19,34 +38,21 @@ import {
ZoomOutIcon,
ZoomResetIcon,
} from "../components/icons";
-import {
- CURSOR_TYPE,
- MAX_ZOOM,
- MIN_ZOOM,
- THEME,
- ZOOM_STEP,
-} from "../constants";
import { setCursor } from "../cursor";
-import { getCommonBounds, getNonDeletedElements } from "../element";
-import { newElementWith } from "../element/mutateElement";
+
import { t } from "../i18n";
-import { CODES, KEYS } from "../keys";
import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom";
import { CaptureUpdateAction } from "../store";
-import { getShortcutKey, updateActiveTool } from "../utils";
import { register } from "./register";
-import type { SceneBounds } from "../element/bounds";
-import type { ExcalidrawElement } from "../element/types";
import type { AppState, Offsets } from "../types";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
label: "labels.canvasBackground",
- paletteName: "Change canvas background color",
trackEvent: false,
predicate: (elements, appState, props, app) => {
return (
@@ -84,7 +90,6 @@ export const actionChangeViewBackgroundColor = register({
export const actionClearCanvas = register({
name: "clearCanvas",
label: "labels.clearCanvas",
- paletteName: "Clear canvas",
icon: TrashIcon,
trackEvent: { category: "canvas" },
predicate: (elements, appState, props, app) => {
@@ -519,10 +524,42 @@ export const actionToggleEraserTool = register({
keyTest: (event) => event.key === KEYS.E,
});
+export const actionToggleLassoTool = register({
+ name: "toggleLassoTool",
+ label: "toolBar.lasso",
+ icon: LassoIcon,
+ trackEvent: { category: "toolbar" },
+ perform: (elements, appState, _, app) => {
+ let activeTool: AppState["activeTool"];
+
+ if (appState.activeTool.type !== "lasso") {
+ activeTool = updateActiveTool(appState, {
+ type: "lasso",
+ fromSelection: false,
+ });
+ setCursor(app.interactiveCanvas, CURSOR_TYPE.CROSSHAIR);
+ } else {
+ activeTool = updateActiveTool(appState, {
+ type: "selection",
+ });
+ }
+
+ return {
+ appState: {
+ ...appState,
+ selectedElementIds: {},
+ selectedGroupIds: {},
+ activeEmbeddable: null,
+ activeTool,
+ },
+ captureUpdate: CaptureUpdateAction.NEVER,
+ };
+ },
+});
+
export const actionToggleHandTool = register({
name: "toggleHandTool",
label: "toolBar.hand",
- paletteName: "Toggle hand tool",
trackEvent: { category: "toolbar" },
icon: handIcon,
viewMode: false,
diff --git a/packages/excalidraw/actions/actionClipboard.tsx b/packages/excalidraw/actions/actionClipboard.tsx
index 4ce3c39dd..9de6d70f4 100644
--- a/packages/excalidraw/actions/actionClipboard.tsx
+++ b/packages/excalidraw/actions/actionClipboard.tsx
@@ -1,3 +1,8 @@
+import { isTextElement } from "@excalidraw/element/typeChecks";
+import { getTextFromElements } from "@excalidraw/element/textElement";
+
+import { CODES, KEYS, isFirefox } from "@excalidraw/common";
+
import {
copyTextToSystemClipboard,
copyToClipboard,
@@ -7,11 +12,9 @@ import {
readSystemClipboard,
} from "../clipboard";
import { DuplicateIcon, cutIcon, pngIcon, svgIcon } from "../components/icons";
-import { isFirefox } from "../constants";
import { exportCanvas, prepareElementsForExport } from "../data/index";
-import { getTextFromElements, isTextElement } from "../element";
import { t } from "../i18n";
-import { CODES, KEYS } from "../keys";
+
import { CaptureUpdateAction } from "../store";
import { actionDeleteSelected } from "./actionDeleteSelected";
diff --git a/packages/excalidraw/actions/actionCropEditor.tsx b/packages/excalidraw/actions/actionCropEditor.tsx
index c377b3a02..1a7b6da69 100644
--- a/packages/excalidraw/actions/actionCropEditor.tsx
+++ b/packages/excalidraw/actions/actionCropEditor.tsx
@@ -1,13 +1,14 @@
+import { isImageElement } from "@excalidraw/element/typeChecks";
+
+import type { ExcalidrawImageElement } from "@excalidraw/element/types";
+
import { ToolButton } from "../components/ToolButton";
import { cropIcon } from "../components/icons";
-import { isImageElement } from "../element/typeChecks";
import { t } from "../i18n";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
-import type { ExcalidrawImageElement } from "../element/types";
-
export const actionToggleCropEditor = register({
name: "cropEditor",
label: "helpDialog.cropStart",
diff --git a/packages/excalidraw/actions/actionDeleteSelected.tsx b/packages/excalidraw/actions/actionDeleteSelected.tsx
index fe3f8c5d7..75e666df0 100644
--- a/packages/excalidraw/actions/actionDeleteSelected.tsx
+++ b/packages/excalidraw/actions/actionDeleteSelected.tsx
@@ -1,26 +1,35 @@
-import { ToolButton } from "../components/ToolButton";
-import { TrashIcon } from "../components/icons";
-import { getNonDeletedElements } from "../element";
-import { fixBindingsAfterDeletion } from "../element/binding";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import { mutateElement, newElementWith } from "../element/mutateElement";
-import { getContainerElement } from "../element/textElement";
+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 { getContainerElement } from "@excalidraw/element/textElement";
import {
isBoundToContainer,
isElbowArrow,
isFrameLikeElement,
-} from "../element/typeChecks";
-import { getFrameChildren } from "../frame";
-import { getElementsInGroup, selectGroupsForSelectedElements } from "../groups";
+} from "@excalidraw/element/typeChecks";
+import { getFrameChildren } from "@excalidraw/element/frame";
+
+import {
+ getElementsInGroup,
+ selectGroupsForSelectedElements,
+} from "@excalidraw/element/groups";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { t } from "../i18n";
-import { KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { CaptureUpdateAction } from "../store";
-import { updateActiveTool } from "../utils";
+import { TrashIcon } from "../components/icons";
+import { ToolButton } from "../components/ToolButton";
import { register } from "./register";
-import type { ExcalidrawElement } from "../element/types";
import type { AppClassProperties, AppState } from "../types";
const deleteSelectedElements = (
@@ -288,6 +297,7 @@ export const actionDeleteSelected = register({
activeTool: updateActiveTool(appState, { type: "selection" }),
multiElement: null,
activeEmbeddable: null,
+ selectedLinearElement: null,
},
captureUpdate: isSomeElementSelected(
getNonDeletedElements(elements),
diff --git a/packages/excalidraw/actions/actionDistribute.tsx b/packages/excalidraw/actions/actionDistribute.tsx
index c96bdaa3e..9f05ab6bf 100644
--- a/packages/excalidraw/actions/actionDistribute.tsx
+++ b/packages/excalidraw/actions/actionDistribute.tsx
@@ -1,22 +1,30 @@
+import { getNonDeletedElements } from "@excalidraw/element";
+
+import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
+
+import { CODES, KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
+
+import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
+
+import { distributeElements } from "@excalidraw/element/distribute";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import type { Distribution } from "@excalidraw/element/distribute";
+
import { ToolButton } from "../components/ToolButton";
import {
DistributeHorizontallyIcon,
DistributeVerticallyIcon,
} from "../components/icons";
-import { distributeElements } from "../distribute";
-import { getNonDeletedElements } from "../element";
-import { isFrameLikeElement } from "../element/typeChecks";
-import { updateFrameMembershipOfSelectedElements } from "../frame";
+
import { t } from "../i18n";
-import { CODES, KEYS } from "../keys";
+
import { isSomeElementSelected } from "../scene";
import { CaptureUpdateAction } from "../store";
-import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
-import type { Distribution } from "../distribute";
-import type { ExcalidrawElement } from "../element/types";
import type { AppClassProperties, AppState } from "../types";
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
diff --git a/packages/excalidraw/actions/actionDuplicateSelection.test.tsx b/packages/excalidraw/actions/actionDuplicateSelection.test.tsx
index 2c1d44e92..71559fe47 100644
--- a/packages/excalidraw/actions/actionDuplicateSelection.test.tsx
+++ b/packages/excalidraw/actions/actionDuplicateSelection.test.tsx
@@ -1,6 +1,5 @@
-import React from "react";
+import { ORIG_ID } from "@excalidraw/common";
-import { ORIG_ID } from "../constants";
import { Excalidraw } from "../index";
import { API } from "../tests/helpers/api";
import {
@@ -258,7 +257,7 @@ describe("actionDuplicateSelection", () => {
assertElements(h.elements, [
{ id: frame.id },
{ id: text.id, frameId: frame.id },
- { [ORIG_ID]: text.id, frameId: frame.id },
+ { [ORIG_ID]: text.id, frameId: frame.id, selected: true },
]);
});
diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx
index 0125f0288..231fe1913 100644
--- a/packages/excalidraw/actions/actionDuplicateSelection.tsx
+++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx
@@ -1,51 +1,41 @@
-import { ToolButton } from "../components/ToolButton";
-import { DuplicateIcon } from "../components/icons";
-import { DEFAULT_GRID_SIZE } from "../constants";
-import { duplicateElement, getNonDeletedElements } from "../element";
-import { fixBindingsAfterDuplication } from "../element/binding";
import {
- bindTextToShapeAfterDuplication,
- getBoundTextElement,
- getContainerElement,
-} from "../element/textElement";
+ DEFAULT_GRID_SIZE,
+ KEYS,
+ arrayToMap,
+ getShortcutKey,
+} from "@excalidraw/common";
+
+import { getNonDeletedElements } from "@excalidraw/element";
+
import {
- hasBoundTextElement,
isBoundToContainer,
- isFrameLikeElement,
-} from "../element/typeChecks";
-import { normalizeElementOrder } from "../element/sortElements";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import {
- bindElementsToFramesAfterDuplication,
- getFrameChildren,
-} from "../frame";
-import {
- selectGroupsForSelectedElements,
- getSelectedGroupForElement,
- getElementsInGroup,
-} from "../groups";
-import { t } from "../i18n";
-import { KEYS } from "../keys";
-import { isSomeElementSelected } from "../scene";
+ isLinearElement,
+} from "@excalidraw/element/typeChecks";
+
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+
+import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
+
import {
excludeElementsInFramesFromSelection,
getSelectedElements,
-} from "../scene/selection";
+} from "@excalidraw/element/selection";
+
+import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
+
+import { duplicateElements } from "@excalidraw/element/duplicate";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import { ToolButton } from "../components/ToolButton";
+import { DuplicateIcon } from "../components/icons";
+
+import { t } from "../i18n";
+import { isSomeElementSelected } from "../scene";
import { CaptureUpdateAction } from "../store";
-import {
- arrayToMap,
- castArray,
- findLastIndex,
- getShortcutKey,
- invariant,
-} from "../utils";
import { register } from "./register";
-import type { ActionResult } from "./types";
-import type { ExcalidrawElement } from "../element/types";
-import type { AppState } from "../types";
-
export const actionDuplicateSelection = register({
name: "duplicateSelection",
label: "labels.duplicateSelection",
@@ -75,20 +65,54 @@ export const actionDuplicateSelection = register({
}
}
- const nextState = duplicateElements(elements, appState);
-
- if (app.props.onDuplicate && nextState.elements) {
- const mappedElements = app.props.onDuplicate(
- nextState.elements,
+ let { newElements: duplicatedElements, elementsWithClones: nextElements } =
+ duplicateElements({
+ type: "in-place",
elements,
- );
+ idsOfElementsToDuplicate: arrayToMap(
+ getSelectedElements(elements, appState, {
+ includeBoundTextElement: true,
+ includeElementsInFrames: true,
+ }),
+ ),
+ appState,
+ randomizeSeed: true,
+ overrides: (element) => ({
+ x: element.x + DEFAULT_GRID_SIZE / 2,
+ y: element.y + DEFAULT_GRID_SIZE / 2,
+ }),
+ reverseOrder: false,
+ });
+
+ if (app.props.onDuplicate && nextElements) {
+ const mappedElements = app.props.onDuplicate(nextElements, elements);
if (mappedElements) {
- nextState.elements = mappedElements;
+ nextElements = mappedElements;
}
}
return {
- ...nextState,
+ elements: syncMovedIndices(nextElements, arrayToMap(duplicatedElements)),
+ appState: {
+ ...appState,
+ ...updateLinearElementEditors(duplicatedElements),
+ ...selectGroupsForSelectedElements(
+ {
+ editingGroupId: appState.editingGroupId,
+ selectedElementIds: excludeElementsInFramesFromSelection(
+ duplicatedElements,
+ ).reduce((acc: Record, element) => {
+ if (!isBoundToContainer(element)) {
+ acc[element.id] = true;
+ }
+ return acc;
+ }, {}),
+ },
+ getNonDeletedElements(nextElements),
+ appState,
+ null,
+ ),
+ },
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
@@ -107,259 +131,23 @@ export const actionDuplicateSelection = register({
),
});
-const duplicateElements = (
- elements: readonly ExcalidrawElement[],
- appState: AppState,
-): Partial> => {
- // ---------------------------------------------------------------------------
-
- const groupIdMap = new Map();
- const newElements: ExcalidrawElement[] = [];
- const oldElements: ExcalidrawElement[] = [];
- const oldIdToDuplicatedId = new Map();
- const duplicatedElementsMap = new Map();
-
- const elementsMap = arrayToMap(elements);
-
- const duplicateAndOffsetElement = <
- T extends ExcalidrawElement | ExcalidrawElement[],
- >(
- element: T,
- ): T extends ExcalidrawElement[]
- ? ExcalidrawElement[]
- : ExcalidrawElement | null => {
- const elements = castArray(element);
-
- const _newElements = elements.reduce(
- (acc: ExcalidrawElement[], element) => {
- if (processedIds.has(element.id)) {
- return acc;
- }
-
- processedIds.set(element.id, true);
-
- const newElement = duplicateElement(
- appState.editingGroupId,
- groupIdMap,
- element,
- {
- x: element.x + DEFAULT_GRID_SIZE / 2,
- y: element.y + DEFAULT_GRID_SIZE / 2,
- },
- );
-
- processedIds.set(newElement.id, true);
-
- duplicatedElementsMap.set(newElement.id, newElement);
- oldIdToDuplicatedId.set(element.id, newElement.id);
-
- oldElements.push(element);
- newElements.push(newElement);
-
- acc.push(newElement);
- return acc;
- },
- [],
+const updateLinearElementEditors = (clonedElements: ExcalidrawElement[]) => {
+ const linears = clonedElements.filter(isLinearElement);
+ if (linears.length === 1) {
+ const linear = linears[0];
+ const boundElements = linear.boundElements?.map((def) => def.id) ?? [];
+ const onlySingleLinearSelected = clonedElements.every(
+ (el) => el.id === linear.id || boundElements.includes(el.id),
);
- return (
- Array.isArray(element) ? _newElements : _newElements[0] || null
- ) as T extends ExcalidrawElement[]
- ? ExcalidrawElement[]
- : ExcalidrawElement | null;
- };
-
- elements = normalizeElementOrder(elements);
-
- const idsOfElementsToDuplicate = arrayToMap(
- getSelectedElements(elements, appState, {
- includeBoundTextElement: true,
- includeElementsInFrames: true,
- }),
- );
-
- // Ids of elements that have already been processed so we don't push them
- // into the array twice if we end up backtracking when retrieving
- // discontiguous group of elements (can happen due to a bug, or in edge
- // cases such as a group containing deleted elements which were not selected).
- //
- // This is not enough to prevent duplicates, so we do a second loop afterwards
- // to remove them.
- //
- // For convenience we mark even the newly created ones even though we don't
- // loop over them.
- const processedIds = new Map();
-
- const elementsWithClones: ExcalidrawElement[] = elements.slice();
-
- const insertAfterIndex = (
- index: number,
- elements: ExcalidrawElement | null | ExcalidrawElement[],
- ) => {
- invariant(index !== -1, "targetIndex === -1 ");
-
- if (!Array.isArray(elements) && !elements) {
- return;
+ if (onlySingleLinearSelected) {
+ return {
+ selectedLinearElement: new LinearElementEditor(linear),
+ };
}
-
- elementsWithClones.splice(index + 1, 0, ...castArray(elements));
- };
-
- const frameIdsToDuplicate = new Set(
- elements
- .filter(
- (el) => idsOfElementsToDuplicate.has(el.id) && isFrameLikeElement(el),
- )
- .map((el) => el.id),
- );
-
- for (const element of elements) {
- if (processedIds.has(element.id)) {
- continue;
- }
-
- if (!idsOfElementsToDuplicate.has(element.id)) {
- continue;
- }
-
- // groups
- // -------------------------------------------------------------------------
-
- const groupId = getSelectedGroupForElement(appState, element);
- if (groupId) {
- const groupElements = getElementsInGroup(elements, groupId).flatMap(
- (element) =>
- isFrameLikeElement(element)
- ? [...getFrameChildren(elements, element.id), element]
- : [element],
- );
-
- const targetIndex = findLastIndex(elementsWithClones, (el) => {
- return el.groupIds?.includes(groupId);
- });
-
- insertAfterIndex(targetIndex, duplicateAndOffsetElement(groupElements));
- continue;
- }
-
- // frame duplication
- // -------------------------------------------------------------------------
-
- if (element.frameId && frameIdsToDuplicate.has(element.frameId)) {
- continue;
- }
-
- if (isFrameLikeElement(element)) {
- const frameId = element.id;
-
- const frameChildren = getFrameChildren(elements, frameId);
-
- const targetIndex = findLastIndex(elementsWithClones, (el) => {
- return el.frameId === frameId || el.id === frameId;
- });
-
- insertAfterIndex(
- targetIndex,
- duplicateAndOffsetElement([...frameChildren, element]),
- );
- continue;
- }
-
- // text container
- // -------------------------------------------------------------------------
-
- if (hasBoundTextElement(element)) {
- const boundTextElement = getBoundTextElement(element, elementsMap);
-
- const targetIndex = findLastIndex(elementsWithClones, (el) => {
- return (
- el.id === element.id ||
- ("containerId" in el && el.containerId === element.id)
- );
- });
-
- if (boundTextElement) {
- insertAfterIndex(
- targetIndex,
- duplicateAndOffsetElement([element, boundTextElement]),
- );
- } else {
- insertAfterIndex(targetIndex, duplicateAndOffsetElement(element));
- }
-
- continue;
- }
-
- if (isBoundToContainer(element)) {
- const container = getContainerElement(element, elementsMap);
-
- const targetIndex = findLastIndex(elementsWithClones, (el) => {
- return el.id === element.id || el.id === container?.id;
- });
-
- if (container) {
- insertAfterIndex(
- targetIndex,
- duplicateAndOffsetElement([container, element]),
- );
- } else {
- insertAfterIndex(targetIndex, duplicateAndOffsetElement(element));
- }
-
- continue;
- }
-
- // default duplication (regular elements)
- // -------------------------------------------------------------------------
-
- insertAfterIndex(
- findLastIndex(elementsWithClones, (el) => el.id === element.id),
- duplicateAndOffsetElement(element),
- );
}
- // ---------------------------------------------------------------------------
-
- bindTextToShapeAfterDuplication(
- elementsWithClones,
- oldElements,
- oldIdToDuplicatedId,
- );
- fixBindingsAfterDuplication(
- elementsWithClones,
- oldElements,
- oldIdToDuplicatedId,
- );
- bindElementsToFramesAfterDuplication(
- elementsWithClones,
- oldElements,
- oldIdToDuplicatedId,
- );
-
- const nextElementsToSelect =
- excludeElementsInFramesFromSelection(newElements);
-
return {
- elements: elementsWithClones,
- appState: {
- ...appState,
- ...selectGroupsForSelectedElements(
- {
- editingGroupId: appState.editingGroupId,
- selectedElementIds: nextElementsToSelect.reduce(
- (acc: Record, element) => {
- if (!isBoundToContainer(element)) {
- acc[element.id] = true;
- }
- return acc;
- },
- {},
- ),
- },
- getNonDeletedElements(elementsWithClones),
- appState,
- null,
- ),
- },
+ selectedLinearElement: null,
};
};
diff --git a/packages/excalidraw/actions/actionElementLink.ts b/packages/excalidraw/actions/actionElementLink.ts
index 69fc67dd3..24ea8bbd6 100644
--- a/packages/excalidraw/actions/actionElementLink.ts
+++ b/packages/excalidraw/actions/actionElementLink.ts
@@ -1,10 +1,11 @@
-import { copyTextToSystemClipboard } from "../clipboard";
-import { copyIcon, elementLinkIcon } from "../components/icons";
import {
canCreateLinkFromElements,
defaultGetElementLinkFromSelection,
getLinkIdAndTypeFromSelection,
-} from "../element/elementLink";
+} from "@excalidraw/element/elementLink";
+
+import { copyTextToSystemClipboard } from "../clipboard";
+import { copyIcon, elementLinkIcon } from "../components/icons";
import { t } from "../i18n";
import { getSelectedElements } from "../scene";
import { CaptureUpdateAction } from "../store";
diff --git a/packages/excalidraw/actions/actionElementLock.ts b/packages/excalidraw/actions/actionElementLock.ts
index 57ca1dded..6bc238a59 100644
--- a/packages/excalidraw/actions/actionElementLock.ts
+++ b/packages/excalidraw/actions/actionElementLock.ts
@@ -1,15 +1,18 @@
+import { KEYS, arrayToMap } from "@excalidraw/common";
+
+import { newElementWith } from "@excalidraw/element/mutateElement";
+
+import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { LockedIcon, UnlockedIcon } from "../components/icons";
-import { newElementWith } from "../element/mutateElement";
-import { isFrameLikeElement } from "../element/typeChecks";
-import { KEYS } from "../keys";
+
import { getSelectedElements } from "../scene";
import { CaptureUpdateAction } from "../store";
-import { arrayToMap } from "../utils";
import { register } from "./register";
-import type { ExcalidrawElement } from "../element/types";
-
const shouldLock = (elements: readonly ExcalidrawElement[]) =>
elements.every((el) => !el.locked);
@@ -87,7 +90,6 @@ export const actionToggleElementLock = register({
export const actionUnlockAllElements = register({
name: "unlockAllElements",
- paletteName: "Unlock all elements",
trackEvent: { category: "canvas" },
viewMode: false,
icon: UnlockedIcon,
diff --git a/packages/excalidraw/actions/actionEmbeddable.ts b/packages/excalidraw/actions/actionEmbeddable.ts
new file mode 100644
index 000000000..556652240
--- /dev/null
+++ b/packages/excalidraw/actions/actionEmbeddable.ts
@@ -0,0 +1,34 @@
+import { updateActiveTool } from "@excalidraw/common";
+
+import { setCursorForShape } from "../cursor";
+import { CaptureUpdateAction } from "../store";
+
+import { register } from "./register";
+
+export const actionSetEmbeddableAsActiveTool = register({
+ name: "setEmbeddableAsActiveTool",
+ trackEvent: { category: "toolbar" },
+ target: "Tool",
+ label: "toolBar.embeddable",
+ perform: (elements, appState, _, app) => {
+ const nextActiveTool = updateActiveTool(appState, {
+ type: "embeddable",
+ });
+
+ setCursorForShape(app.canvas, {
+ ...appState,
+ activeTool: nextActiveTool,
+ });
+
+ return {
+ elements,
+ appState: {
+ ...appState,
+ activeTool: updateActiveTool(appState, {
+ type: "embeddable",
+ }),
+ },
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ };
+ },
+});
diff --git a/packages/excalidraw/actions/actionExport.tsx b/packages/excalidraw/actions/actionExport.tsx
index c9d70fc46..8fcaea21b 100644
--- a/packages/excalidraw/actions/actionExport.tsx
+++ b/packages/excalidraw/actions/actionExport.tsx
@@ -1,3 +1,14 @@
+import {
+ KEYS,
+ DEFAULT_EXPORT_PADDING,
+ EXPORT_SCALES,
+ THEME,
+} from "@excalidraw/common";
+
+import { getNonDeletedElements } from "@excalidraw/element";
+
+import type { Theme } from "@excalidraw/element/types";
+
import { useDevice } from "../components/App";
import { CheckboxItem } from "../components/CheckboxItem";
import { DarkModeToggle } from "../components/DarkModeToggle";
@@ -5,14 +16,12 @@ import { ProjectName } from "../components/ProjectName";
import { ToolButton } from "../components/ToolButton";
import { Tooltip } from "../components/Tooltip";
import { ExportIcon, questionCircle, saveAs } from "../components/icons";
-import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
import { loadFromJSON, saveAsJSON } from "../data";
import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { resaveAsImageWithScene } from "../data/resave";
-import { getNonDeletedElements } from "../element";
+
import { t } from "../i18n";
-import { KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getExportSize } from "../scene/export";
import { CaptureUpdateAction } from "../store";
@@ -21,8 +30,6 @@ import "../components/ToolIcon.scss";
import { register } from "./register";
-import type { Theme } from "../element/types";
-
export const actionChangeProjectName = register({
name: "changeProjectName",
label: "labels.fileTitle",
diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx
index ac8a4e036..984961656 100644
--- a/packages/excalidraw/actions/actionFinalize.tsx
+++ b/packages/excalidraw/actions/actionFinalize.tsx
@@ -1,21 +1,26 @@
import { pointFrom } from "@excalidraw/math";
-import { ToolButton } from "../components/ToolButton";
-import { done } from "../components/icons";
-import { resetCursor } from "../cursor";
-import { isInvisiblySmallElement } from "../element";
import {
maybeBindLinearElement,
bindOrUnbindLinearElement,
-} from "../element/binding";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import { mutateElement } from "../element/mutateElement";
-import { isBindingElement, isLinearElement } from "../element/typeChecks";
+} from "@excalidraw/element/binding";
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+import {
+ isBindingElement,
+ isLinearElement,
+} from "@excalidraw/element/typeChecks";
+
+import { KEYS, arrayToMap, updateActiveTool } from "@excalidraw/common";
+import { isPathALoop } from "@excalidraw/element/shapes";
+
+import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
+
import { t } from "../i18n";
-import { KEYS } from "../keys";
-import { isPathALoop } from "../shapes";
+import { resetCursor } from "../cursor";
+import { done } from "../components/icons";
+import { ToolButton } from "../components/ToolButton";
import { CaptureUpdateAction } from "../store";
-import { arrayToMap, updateActiveTool } from "../utils";
import { register } from "./register";
diff --git a/packages/excalidraw/actions/actionFlip.test.tsx b/packages/excalidraw/actions/actionFlip.test.tsx
index e9953e02d..23e4ffc12 100644
--- a/packages/excalidraw/actions/actionFlip.test.tsx
+++ b/packages/excalidraw/actions/actionFlip.test.tsx
@@ -1,5 +1,4 @@
import { pointFrom } from "@excalidraw/math";
-import React from "react";
import { Excalidraw } from "../index";
import { API } from "../tests/helpers/api";
diff --git a/packages/excalidraw/actions/actionFlip.ts b/packages/excalidraw/actions/actionFlip.ts
index bfc7fb721..62be891cb 100644
--- a/packages/excalidraw/actions/actionFlip.ts
+++ b/packages/excalidraw/actions/actionFlip.ts
@@ -1,25 +1,22 @@
-import { flipHorizontal, flipVertical } from "../components/icons";
-import { getNonDeletedElements } from "../element";
+import { getNonDeletedElements } from "@excalidraw/element";
import {
bindOrUnbindLinearElements,
isBindingEnabled,
-} from "../element/binding";
-import { getCommonBoundingBox } from "../element/bounds";
-import { mutateElement, newElementWith } from "../element/mutateElement";
-import { deepCopyElement } from "../element/newElement";
-import { resizeMultipleElements } from "../element/resizeElements";
+} from "@excalidraw/element/binding";
+import { getCommonBoundingBox } from "@excalidraw/element/bounds";
+import {
+ mutateElement,
+ newElementWith,
+} from "@excalidraw/element/mutateElement";
+import { deepCopyElement } from "@excalidraw/element/duplicate";
+import { resizeMultipleElements } from "@excalidraw/element/resizeElements";
import {
isArrowElement,
isElbowArrow,
isLinearElement,
-} from "../element/typeChecks";
-import { updateFrameMembershipOfSelectedElements } from "../frame";
-import { CODES, KEYS } from "../keys";
-import { getSelectedElements } from "../scene";
-import { CaptureUpdateAction } from "../store";
-import { arrayToMap } from "../utils";
-
-import { register } from "./register";
+} from "@excalidraw/element/typeChecks";
+import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/frame";
+import { CODES, KEYS, arrayToMap } from "@excalidraw/common";
import type {
ExcalidrawArrowElement,
@@ -27,7 +24,15 @@ import type {
ExcalidrawElement,
NonDeleted,
NonDeletedSceneElementsMap,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import { getSelectedElements } from "../scene";
+import { CaptureUpdateAction } from "../store";
+
+import { flipHorizontal, flipVertical } from "../components/icons";
+
+import { register } from "./register";
+
import type { AppClassProperties, AppState } from "../types";
export const actionFlipHorizontal = register({
diff --git a/packages/excalidraw/actions/actionFrame.ts b/packages/excalidraw/actions/actionFrame.ts
index 198994b29..13d57b2a1 100644
--- a/packages/excalidraw/actions/actionFrame.ts
+++ b/packages/excalidraw/actions/actionFrame.ts
@@ -1,20 +1,28 @@
-import { frameToolIcon } from "../components/icons";
+import { getNonDeletedElements } from "@excalidraw/element";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+import { newFrameElement } from "@excalidraw/element/newElement";
+import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
+import {
+ addElementsToFrame,
+ removeAllElementsFromFrame,
+} from "@excalidraw/element/frame";
+import { getFrameChildren } from "@excalidraw/element/frame";
+
+import { KEYS, updateActiveTool } from "@excalidraw/common";
+
+import { getElementsInGroup } from "@excalidraw/element/groups";
+
+import { getCommonBounds } from "@excalidraw/element/bounds";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { setCursorForShape } from "../cursor";
-import { getCommonBounds, getNonDeletedElements } from "../element";
-import { mutateElement } from "../element/mutateElement";
-import { newFrameElement } from "../element/newElement";
-import { isFrameLikeElement } from "../element/typeChecks";
-import { addElementsToFrame, removeAllElementsFromFrame } from "../frame";
-import { getFrameChildren } from "../frame";
-import { getElementsInGroup } from "../groups";
-import { KEYS } from "../keys";
+import { frameToolIcon } from "../components/icons";
import { getSelectedElements } from "../scene";
import { CaptureUpdateAction } from "../store";
-import { updateActiveTool } from "../utils";
import { register } from "./register";
-import type { ExcalidrawElement } from "../element/types";
import type { AppClassProperties, AppState, UIAppState } from "../types";
const isSingleFrameSelected = (
diff --git a/packages/excalidraw/actions/actionGroup.tsx b/packages/excalidraw/actions/actionGroup.tsx
index ded2f5b72..6b47ef969 100644
--- a/packages/excalidraw/actions/actionGroup.tsx
+++ b/packages/excalidraw/actions/actionGroup.tsx
@@ -1,9 +1,9 @@
-import { ToolButton } from "../components/ToolButton";
-import { UngroupIcon, GroupIcon } from "../components/icons";
-import { getNonDeletedElements } from "../element";
-import { newElementWith } from "../element/mutateElement";
-import { isBoundToContainer } from "../element/typeChecks";
-import { syncMovedIndices } from "../fractionalIndex";
+import { getNonDeletedElements } from "@excalidraw/element";
+
+import { newElementWith } from "@excalidraw/element/mutateElement";
+
+import { isBoundToContainer } from "@excalidraw/element/typeChecks";
+
import {
frameAndChildrenSelectedTogether,
getElementsInResizingFrame,
@@ -12,7 +12,10 @@ import {
groupByFrameLikes,
removeElementsFromFrame,
replaceAllElementsInFrame,
-} from "../frame";
+} from "@excalidraw/element/frame";
+
+import { KEYS, randomId, arrayToMap, getShortcutKey } from "@excalidraw/common";
+
import {
getSelectedGroupIds,
selectGroup,
@@ -21,21 +24,26 @@ import {
addToGroup,
removeFromSelectedGroups,
isElementInGroup,
-} from "../groups";
-import { t } from "../i18n";
-import { KEYS } from "../keys";
-import { randomId } from "../random";
-import { isSomeElementSelected } from "../scene";
-import { CaptureUpdateAction } from "../store";
-import { arrayToMap, getShortcutKey } from "../utils";
+} from "@excalidraw/element/groups";
-import { register } from "./register";
+import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
import type {
ExcalidrawElement,
ExcalidrawTextElement,
OrderedExcalidrawElement,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import { ToolButton } from "../components/ToolButton";
+import { UngroupIcon, GroupIcon } from "../components/icons";
+
+import { t } from "../i18n";
+
+import { isSomeElementSelected } from "../scene";
+import { CaptureUpdateAction } from "../store";
+
+import { register } from "./register";
+
import type { AppClassProperties, AppState } from "../types";
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
diff --git a/packages/excalidraw/actions/actionHistory.tsx b/packages/excalidraw/actions/actionHistory.tsx
index da4f80219..a0dfb85df 100644
--- a/packages/excalidraw/actions/actionHistory.tsx
+++ b/packages/excalidraw/actions/actionHistory.tsx
@@ -1,14 +1,14 @@
+import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common";
+
+import type { SceneElementsMap } from "@excalidraw/element/types";
+
import { ToolButton } from "../components/ToolButton";
import { UndoIcon, RedoIcon } from "../components/icons";
-import { isWindows } from "../constants";
import { HistoryChangedEvent } from "../history";
import { useEmitter } from "../hooks/useEmitter";
import { t } from "../i18n";
-import { KEYS, matchKey } from "../keys";
import { CaptureUpdateAction } from "../store";
-import { arrayToMap } from "../utils";
-import type { SceneElementsMap } from "../element/types";
import type { History } from "../history";
import type { Store } from "../store";
import type { AppClassProperties, AppState } from "../types";
diff --git a/packages/excalidraw/actions/actionLinearEditor.tsx b/packages/excalidraw/actions/actionLinearEditor.tsx
index f17f0e565..56e327bd2 100644
--- a/packages/excalidraw/actions/actionLinearEditor.tsx
+++ b/packages/excalidraw/actions/actionLinearEditor.tsx
@@ -1,15 +1,18 @@
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+
+import { isElbowArrow, isLinearElement } from "@excalidraw/element/typeChecks";
+
+import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
+
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
import { ToolButton } from "../components/ToolButton";
import { lineEditorIcon } from "../components/icons";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import { isElbowArrow, isLinearElement } from "../element/typeChecks";
+
import { t } from "../i18n";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
-import type { ExcalidrawLinearElement } from "../element/types";
-
export const actionToggleLinearEditor = register({
name: "toggleLinearEditor",
category: DEFAULT_CATEGORIES.elements,
diff --git a/packages/excalidraw/actions/actionLink.tsx b/packages/excalidraw/actions/actionLink.tsx
index 91d01b7a6..71426267d 100644
--- a/packages/excalidraw/actions/actionLink.tsx
+++ b/packages/excalidraw/actions/actionLink.tsx
@@ -1,12 +1,14 @@
+import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
+
+import { KEYS, getShortcutKey } from "@excalidraw/common";
+
import { ToolButton } from "../components/ToolButton";
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
import { LinkIcon } from "../components/icons";
-import { isEmbeddableElement } from "../element/typeChecks";
import { t } from "../i18n";
-import { KEYS } from "../keys";
+
import { getSelectedElements } from "../scene";
import { CaptureUpdateAction } from "../store";
-import { getShortcutKey } from "../utils";
import { register } from "./register";
diff --git a/packages/excalidraw/actions/actionMenu.tsx b/packages/excalidraw/actions/actionMenu.tsx
index 15b763cb9..67863e020 100644
--- a/packages/excalidraw/actions/actionMenu.tsx
+++ b/packages/excalidraw/actions/actionMenu.tsx
@@ -1,8 +1,13 @@
+import { KEYS } from "@excalidraw/common";
+
+import { getNonDeletedElements } from "@excalidraw/element";
+
+import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions";
+
import { ToolButton } from "../components/ToolButton";
import { HamburgerMenuIcon, HelpIconThin, palette } from "../components/icons";
-import { showSelectedShapeActions, getNonDeletedElements } from "../element";
import { t } from "../i18n";
-import { KEYS } from "../keys";
+
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
diff --git a/packages/excalidraw/actions/actionProperties.test.tsx b/packages/excalidraw/actions/actionProperties.test.tsx
index c5467bb94..38419ce82 100644
--- a/packages/excalidraw/actions/actionProperties.test.tsx
+++ b/packages/excalidraw/actions/actionProperties.test.tsx
@@ -1,8 +1,12 @@
import { queryByTestId } from "@testing-library/react";
-import React from "react";
-import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
-import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
+import {
+ COLOR_PALETTE,
+ DEFAULT_ELEMENT_BACKGROUND_PICKS,
+ FONT_FAMILY,
+ STROKE_WIDTH,
+} from "@excalidraw/common";
+
import { Excalidraw } from "../index";
import { API } from "../tests/helpers/api";
import { UI } from "../tests/helpers/ui";
diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx
index 5a7590667..5a309b677 100644
--- a/packages/excalidraw/actions/actionProperties.tsx
+++ b/packages/excalidraw/actions/actionProperties.tsx
@@ -1,15 +1,77 @@
import { pointFrom } from "@excalidraw/math";
import { useEffect, useMemo, useRef, useState } from "react";
-import type { LocalPoint } from "@excalidraw/math";
-
-import { trackEvent } from "../analytics";
import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
DEFAULT_ELEMENT_STROKE_COLOR_PALETTE,
DEFAULT_ELEMENT_STROKE_PICKS,
-} from "../colors";
+ ARROW_TYPE,
+ DEFAULT_FONT_FAMILY,
+ DEFAULT_FONT_SIZE,
+ FONT_FAMILY,
+ ROUNDNESS,
+ STROKE_WIDTH,
+ VERTICAL_ALIGN,
+ KEYS,
+ randomInteger,
+ arrayToMap,
+ getFontFamilyString,
+ getShortcutKey,
+ tupleToCoors,
+ getLineHeight,
+} from "@excalidraw/common";
+
+import { getNonDeletedElements } from "@excalidraw/element";
+
+import {
+ bindLinearElement,
+ bindPointToSnapToElementOutline,
+ calculateFixedPointForElbowArrowBinding,
+ getHoveredElementForBinding,
+ updateBoundElements,
+} from "@excalidraw/element/binding";
+
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+
+import {
+ mutateElement,
+ newElementWith,
+} from "@excalidraw/element/mutateElement";
+
+import {
+ getBoundTextElement,
+ redrawTextBoundingBox,
+} from "@excalidraw/element/textElement";
+
+import {
+ isArrowElement,
+ isBoundToContainer,
+ isElbowArrow,
+ isLinearElement,
+ isTextElement,
+ isUsingAdaptiveRadius,
+} from "@excalidraw/element/typeChecks";
+
+import { hasStrokeColor } from "@excalidraw/element/comparisons";
+
+import { updateElbowArrowPoints } from "@excalidraw/element/elbowArrow";
+
+import type { LocalPoint } from "@excalidraw/math";
+
+import type {
+ Arrowhead,
+ ExcalidrawBindableElement,
+ ExcalidrawElement,
+ ExcalidrawLinearElement,
+ ExcalidrawTextElement,
+ FontFamilyValues,
+ TextAlign,
+ VerticalAlign,
+ NonDeletedSceneElementsMap,
+} from "@excalidraw/element/types";
+
+import { trackEvent } from "../analytics";
import { ButtonIconSelect } from "../components/ButtonIconSelect";
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
import { FontPicker } from "../components/FontPicker/FontPicker";
@@ -60,41 +122,9 @@ import {
ArrowheadCrowfootOneIcon,
ArrowheadCrowfootOneOrManyIcon,
} from "../components/icons";
-import {
- ARROW_TYPE,
- DEFAULT_FONT_FAMILY,
- DEFAULT_FONT_SIZE,
- FONT_FAMILY,
- ROUNDNESS,
- STROKE_WIDTH,
- VERTICAL_ALIGN,
-} from "../constants";
-import {
- getNonDeletedElements,
- isTextElement,
- redrawTextBoundingBox,
-} from "../element";
-import {
- bindLinearElement,
- bindPointToSnapToElementOutline,
- calculateFixedPointForElbowArrowBinding,
- getHoveredElementForBinding,
- updateBoundElements,
-} from "../element/binding";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import { mutateElement, newElementWith } from "../element/mutateElement";
-import { getBoundTextElement } from "../element/textElement";
-import {
- isArrowElement,
- isBoundToContainer,
- isElbowArrow,
- isLinearElement,
- isUsingAdaptiveRadius,
-} from "../element/typeChecks";
-import { Fonts, getLineHeight } from "../fonts";
+
+import { Fonts } from "../fonts";
import { getLanguage, t } from "../i18n";
-import { KEYS } from "../keys";
-import { randomInteger } from "../random";
import {
canHaveArrowheads,
getCommonAttributeOfSelectedElements,
@@ -102,30 +132,10 @@ import {
getTargetElements,
isSomeElementSelected,
} from "../scene";
-import { hasStrokeColor } from "../scene/comparisons";
import { CaptureUpdateAction } from "../store";
-import {
- arrayToMap,
- getFontFamilyString,
- getShortcutKey,
- tupleToCoors,
-} from "../utils";
-
-import { updateElbowArrowPoints } from "../element/elbowArrow";
import { register } from "./register";
-import type {
- Arrowhead,
- ExcalidrawBindableElement,
- ExcalidrawElement,
- ExcalidrawLinearElement,
- ExcalidrawTextElement,
- FontFamilyValues,
- TextAlign,
- VerticalAlign,
- NonDeletedSceneElementsMap,
-} from "../element/types";
import type { CaptureUpdateActionType } from "../store";
import type { AppClassProperties, AppState, Primitive } from "../types";
diff --git a/packages/excalidraw/actions/actionSelectAll.ts b/packages/excalidraw/actions/actionSelectAll.ts
index 5f6d7eabc..d7775774a 100644
--- a/packages/excalidraw/actions/actionSelectAll.ts
+++ b/packages/excalidraw/actions/actionSelectAll.ts
@@ -1,14 +1,18 @@
-import { selectAllIcon } from "../components/icons";
-import { getNonDeletedElements, isTextElement } from "../element";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import { isLinearElement } from "../element/typeChecks";
-import { selectGroupsForSelectedElements } from "../groups";
-import { KEYS } from "../keys";
+import { getNonDeletedElements } from "@excalidraw/element";
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+import { isLinearElement, isTextElement } from "@excalidraw/element/typeChecks";
+
+import { KEYS } from "@excalidraw/common";
+
+import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { CaptureUpdateAction } from "../store";
-import { register } from "./register";
+import { selectAllIcon } from "../components/icons";
-import type { ExcalidrawElement } from "../element/types";
+import { register } from "./register";
export const actionSelectAll = register({
name: "selectAll",
diff --git a/packages/excalidraw/actions/actionStyles.ts b/packages/excalidraw/actions/actionStyles.ts
index ea517c442..ed3c91e30 100644
--- a/packages/excalidraw/actions/actionStyles.ts
+++ b/packages/excalidraw/actions/actionStyles.ts
@@ -1,33 +1,39 @@
-import { paintIcon } from "../components/icons";
import {
DEFAULT_FONT_SIZE,
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
-} from "../constants";
-import {
- isTextElement,
- isExcalidrawElement,
- redrawTextBoundingBox,
-} from "../element";
-import { newElementWith } from "../element/mutateElement";
-import { getBoundTextElement } from "../element/textElement";
+ CODES,
+ KEYS,
+ getLineHeight,
+} from "@excalidraw/common";
+
+import { newElementWith } from "@excalidraw/element/mutateElement";
+
import {
hasBoundTextElement,
canApplyRoundnessTypeToElement,
getDefaultRoundnessTypeForElement,
isFrameLikeElement,
isArrowElement,
-} from "../element/typeChecks";
-import { getLineHeight } from "../fonts";
+ isExcalidrawElement,
+ isTextElement,
+} from "@excalidraw/element/typeChecks";
+
+import {
+ getBoundTextElement,
+ redrawTextBoundingBox,
+} from "@excalidraw/element/textElement";
+
+import type { ExcalidrawTextElement } from "@excalidraw/element/types";
+
+import { paintIcon } from "../components/icons";
+
import { t } from "../i18n";
-import { CODES, KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
-import type { ExcalidrawTextElement } from "../element/types";
-
// `copiedStyles` is exported only for tests.
export let copiedStyles: string = "{}";
diff --git a/packages/excalidraw/actions/actionTextAutoResize.ts b/packages/excalidraw/actions/actionTextAutoResize.ts
index b9ad7783f..4a36cab40 100644
--- a/packages/excalidraw/actions/actionTextAutoResize.ts
+++ b/packages/excalidraw/actions/actionTextAutoResize.ts
@@ -1,9 +1,12 @@
-import { isTextElement } from "../element";
-import { newElementWith } from "../element/mutateElement";
-import { measureText } from "../element/textMeasurements";
+import { getFontString } from "@excalidraw/common";
+
+import { newElementWith } from "@excalidraw/element/mutateElement";
+import { measureText } from "@excalidraw/element/textMeasurements";
+
+import { isTextElement } from "@excalidraw/element/typeChecks";
+
import { getSelectedElements } from "../scene";
import { CaptureUpdateAction } from "../store";
-import { getFontString } from "../utils";
import { register } from "./register";
diff --git a/packages/excalidraw/actions/actionToggleGridMode.tsx b/packages/excalidraw/actions/actionToggleGridMode.tsx
index 98360e843..9415051f3 100644
--- a/packages/excalidraw/actions/actionToggleGridMode.tsx
+++ b/packages/excalidraw/actions/actionToggleGridMode.tsx
@@ -1,5 +1,6 @@
+import { CODES, KEYS } from "@excalidraw/common";
+
import { gridIcon } from "../components/icons";
-import { CODES, KEYS } from "../keys";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
diff --git a/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx b/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx
index d51124132..ba092bff8 100644
--- a/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx
+++ b/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx
@@ -1,5 +1,6 @@
+import { CODES, KEYS } from "@excalidraw/common";
+
import { magnetIcon } from "../components/icons";
-import { CODES, KEYS } from "../keys";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
diff --git a/packages/excalidraw/actions/actionToggleSearchMenu.ts b/packages/excalidraw/actions/actionToggleSearchMenu.ts
index 75d9074c8..ce384fc66 100644
--- a/packages/excalidraw/actions/actionToggleSearchMenu.ts
+++ b/packages/excalidraw/actions/actionToggleSearchMenu.ts
@@ -1,6 +1,11 @@
+import {
+ KEYS,
+ CANVAS_SEARCH_TAB,
+ CLASSES,
+ DEFAULT_SIDEBAR,
+} from "@excalidraw/common";
+
import { searchIcon } from "../components/icons";
-import { CANVAS_SEARCH_TAB, CLASSES, DEFAULT_SIDEBAR } from "../constants";
-import { KEYS } from "../keys";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
diff --git a/packages/excalidraw/actions/actionToggleStats.tsx b/packages/excalidraw/actions/actionToggleStats.tsx
index d0cdc64a3..ffa812e96 100644
--- a/packages/excalidraw/actions/actionToggleStats.tsx
+++ b/packages/excalidraw/actions/actionToggleStats.tsx
@@ -1,5 +1,6 @@
+import { CODES, KEYS } from "@excalidraw/common";
+
import { abacusIcon } from "../components/icons";
-import { CODES, KEYS } from "../keys";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
@@ -8,7 +9,6 @@ export const actionToggleStats = register({
name: "stats",
label: "stats.fullTitle",
icon: abacusIcon,
- paletteName: "Toggle stats",
viewMode: true,
trackEvent: { category: "menu" },
keywords: ["edit", "attributes", "customize"],
diff --git a/packages/excalidraw/actions/actionToggleViewMode.tsx b/packages/excalidraw/actions/actionToggleViewMode.tsx
index 055135263..e42a7a102 100644
--- a/packages/excalidraw/actions/actionToggleViewMode.tsx
+++ b/packages/excalidraw/actions/actionToggleViewMode.tsx
@@ -1,5 +1,6 @@
+import { CODES, KEYS } from "@excalidraw/common";
+
import { eyeIcon } from "../components/icons";
-import { CODES, KEYS } from "../keys";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
@@ -7,7 +8,6 @@ import { register } from "./register";
export const actionToggleViewMode = register({
name: "viewMode",
label: "labels.viewMode",
- paletteName: "Toggle view mode",
icon: eyeIcon,
viewMode: true,
trackEvent: {
diff --git a/packages/excalidraw/actions/actionToggleZenMode.tsx b/packages/excalidraw/actions/actionToggleZenMode.tsx
index 31c72bf95..e56e02ca7 100644
--- a/packages/excalidraw/actions/actionToggleZenMode.tsx
+++ b/packages/excalidraw/actions/actionToggleZenMode.tsx
@@ -1,5 +1,6 @@
+import { CODES, KEYS } from "@excalidraw/common";
+
import { coffeeIcon } from "../components/icons";
-import { CODES, KEYS } from "../keys";
import { CaptureUpdateAction } from "../store";
import { register } from "./register";
@@ -8,7 +9,6 @@ export const actionToggleZenMode = register({
name: "zenMode",
label: "buttons.zenMode",
icon: coffeeIcon,
- paletteName: "Toggle zen mode",
viewMode: true,
trackEvent: {
category: "canvas",
diff --git a/packages/excalidraw/actions/actionZindex.tsx b/packages/excalidraw/actions/actionZindex.tsx
index 82d3d77da..8eb5a50f2 100644
--- a/packages/excalidraw/actions/actionZindex.tsx
+++ b/packages/excalidraw/actions/actionZindex.tsx
@@ -1,20 +1,20 @@
+import { KEYS, CODES, getShortcutKey, isDarwin } from "@excalidraw/common";
+
+import {
+ moveOneLeft,
+ moveOneRight,
+ moveAllLeft,
+ moveAllRight,
+} from "@excalidraw/element/zindex";
+
import {
BringForwardIcon,
BringToFrontIcon,
SendBackwardIcon,
SendToBackIcon,
} from "../components/icons";
-import { isDarwin } from "../constants";
import { t } from "../i18n";
-import { KEYS, CODES } from "../keys";
import { CaptureUpdateAction } from "../store";
-import { getShortcutKey } from "../utils";
-import {
- moveOneLeft,
- moveOneRight,
- moveAllLeft,
- moveAllRight,
-} from "../zindex";
import { register } from "./register";
@@ -24,9 +24,9 @@ export const actionSendBackward = register({
keywords: ["move down", "zindex", "layer"],
icon: SendBackwardIcon,
trackEvent: { category: "element" },
- perform: (elements, appState) => {
+ perform: (elements, appState, value, app) => {
return {
- elements: moveOneLeft(elements, appState),
+ elements: moveOneLeft(elements, appState, app.scene),
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
@@ -54,9 +54,9 @@ export const actionBringForward = register({
keywords: ["move up", "zindex", "layer"],
icon: BringForwardIcon,
trackEvent: { category: "element" },
- perform: (elements, appState) => {
+ perform: (elements, appState, value, app) => {
return {
- elements: moveOneRight(elements, appState),
+ elements: moveOneRight(elements, appState, app.scene),
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
diff --git a/packages/excalidraw/actions/index.ts b/packages/excalidraw/actions/index.ts
index a556bfbea..f37747aeb 100644
--- a/packages/excalidraw/actions/index.ts
+++ b/packages/excalidraw/actions/index.ts
@@ -30,6 +30,8 @@ export {
actionToggleTheme,
} from "./actionCanvas";
+export { actionSetEmbeddableAsActiveTool } from "./actionEmbeddable";
+
export { actionFinalize } from "./actionFinalize";
export {
diff --git a/packages/excalidraw/actions/manager.tsx b/packages/excalidraw/actions/manager.tsx
index d3609640e..171bb5df7 100644
--- a/packages/excalidraw/actions/manager.tsx
+++ b/packages/excalidraw/actions/manager.tsx
@@ -1,12 +1,14 @@
import React from "react";
-import { trackEvent } from "../analytics";
-import { isPromiseLike } from "../utils";
+import { isPromiseLike } from "@excalidraw/common";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import { trackEvent } from "../analytics";
+
import type { AppClassProperties, AppState } from "../types";
import type {
Action,
diff --git a/packages/excalidraw/actions/shortcuts.ts b/packages/excalidraw/actions/shortcuts.ts
index 89a7c5ae2..1a13f1703 100644
--- a/packages/excalidraw/actions/shortcuts.ts
+++ b/packages/excalidraw/actions/shortcuts.ts
@@ -1,8 +1,9 @@
-import { isDarwin } from "../constants";
-import { t } from "../i18n";
-import { getShortcutKey } from "../utils";
+import { isDarwin, getShortcutKey } from "@excalidraw/common";
+
+import type { SubtypeOf } from "@excalidraw/common/utility-types";
+
+import { t } from "../i18n";
-import type { SubtypeOf } from "../utility-types";
import type { ActionName } from "./types";
export type ShortcutName =
diff --git a/packages/excalidraw/actions/types.ts b/packages/excalidraw/actions/types.ts
index c30f53f5e..c63a122e0 100644
--- a/packages/excalidraw/actions/types.ts
+++ b/packages/excalidraw/actions/types.ts
@@ -1,7 +1,8 @@
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
import type { CaptureUpdateActionType } from "../store";
import type {
AppClassProperties,
@@ -138,7 +139,8 @@ export type ActionName =
| "copyElementLink"
| "linkToElement"
| "cropEditor"
- | "wrapSelectionInFrame";
+ | "wrapSelectionInFrame"
+ | "toggleLassoTool";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];
diff --git a/packages/excalidraw/analytics.ts b/packages/excalidraw/analytics.ts
index c8ab15b62..a1d31e4db 100644
--- a/packages/excalidraw/analytics.ts
+++ b/packages/excalidraw/analytics.ts
@@ -1,4 +1,7 @@
// place here categories that you want to track. We want to track just a
+
+import { isDevEnv } from "@excalidraw/common";
+
// small subset of categories at a given time.
const ALLOWED_CATEGORIES_TO_TRACK = new Set(["command_palette", "export"]);
@@ -21,7 +24,7 @@ export const trackEvent = (
return;
}
- if (import.meta.env.DEV) {
+ if (isDevEnv()) {
// comment out to debug in dev
return;
}
diff --git a/packages/excalidraw/animated-trail.ts b/packages/excalidraw/animated-trail.ts
index a20460771..af6162e99 100644
--- a/packages/excalidraw/animated-trail.ts
+++ b/packages/excalidraw/animated-trail.ts
@@ -1,9 +1,12 @@
import { LaserPointer } from "@excalidraw/laser-pointer";
-import type { LaserPointerOptions } from "@excalidraw/laser-pointer";
+import {
+ SVG_NS,
+ getSvgPathFromStroke,
+ sceneCoordsToViewportCoords,
+} from "@excalidraw/common";
-import { SVG_NS } from "./constants";
-import { getSvgPathFromStroke, sceneCoordsToViewportCoords } from "./utils";
+import type { LaserPointerOptions } from "@excalidraw/laser-pointer";
import type { AnimationFrameHandler } from "./animation-frame-handler";
import type App from "./components/App";
@@ -20,6 +23,8 @@ export interface Trail {
export interface AnimatedTrailOptions {
fill: (trail: AnimatedTrail) => string;
+ stroke?: (trail: AnimatedTrail) => string;
+ animateTrail?: boolean;
}
export class AnimatedTrail implements Trail {
@@ -28,16 +33,28 @@ export class AnimatedTrail implements Trail {
private container?: SVGSVGElement;
private trailElement: SVGPathElement;
+ private trailAnimation?: SVGAnimateElement;
constructor(
private animationFrameHandler: AnimationFrameHandler,
- private app: App,
+ protected app: App,
private options: Partial &
Partial,
) {
this.animationFrameHandler.register(this, this.onFrame.bind(this));
this.trailElement = document.createElementNS(SVG_NS, "path");
+ if (this.options.animateTrail) {
+ this.trailAnimation = document.createElementNS(SVG_NS, "animate");
+ // TODO: make this configurable
+ this.trailAnimation.setAttribute("attributeName", "stroke-dashoffset");
+ this.trailElement.setAttribute("stroke-dasharray", "7 7");
+ this.trailElement.setAttribute("stroke-dashoffset", "10");
+ this.trailAnimation.setAttribute("from", "0");
+ this.trailAnimation.setAttribute("to", `-14`);
+ this.trailAnimation.setAttribute("dur", "0.3s");
+ this.trailElement.appendChild(this.trailAnimation);
+ }
}
get hasCurrentTrail() {
@@ -101,8 +118,23 @@ export class AnimatedTrail implements Trail {
}
}
+ getCurrentTrail() {
+ return this.currentTrail;
+ }
+
+ clearTrails() {
+ this.pastTrails = [];
+ this.currentTrail = undefined;
+ this.update();
+ }
+
private update() {
+ this.pastTrails = [];
this.start();
+ if (this.trailAnimation) {
+ this.trailAnimation.setAttribute("begin", "indefinite");
+ this.trailAnimation.setAttribute("repeatCount", "indefinite");
+ }
}
private onFrame() {
@@ -129,14 +161,25 @@ export class AnimatedTrail implements Trail {
const svgPaths = paths.join(" ").trim();
this.trailElement.setAttribute("d", svgPaths);
- this.trailElement.setAttribute(
- "fill",
- (this.options.fill ?? (() => "black"))(this),
- );
+ if (this.trailAnimation) {
+ this.trailElement.setAttribute(
+ "fill",
+ (this.options.fill ?? (() => "black"))(this),
+ );
+ this.trailElement.setAttribute(
+ "stroke",
+ (this.options.stroke ?? (() => "black"))(this),
+ );
+ } else {
+ this.trailElement.setAttribute(
+ "fill",
+ (this.options.fill ?? (() => "black"))(this),
+ );
+ }
}
private drawTrail(trail: LaserPointer, state: AppState): string {
- const stroke = trail
+ const _stroke = trail
.getStrokeOutline(trail.options.size / state.zoom.value)
.map(([x, y]) => {
const result = sceneCoordsToViewportCoords(
@@ -147,6 +190,10 @@ export class AnimatedTrail implements Trail {
return [result.x, result.y];
});
+ const stroke = this.trailAnimation
+ ? _stroke.slice(0, _stroke.length / 2)
+ : _stroke;
+
return getSvgPathFromStroke(stroke, true);
}
}
diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts
index acda8468b..a75745f2a 100644
--- a/packages/excalidraw/appState.ts
+++ b/packages/excalidraw/appState.ts
@@ -1,5 +1,5 @@
-import { COLOR_PALETTE } from "./colors";
import {
+ COLOR_PALETTE,
ARROW_TYPE,
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
@@ -10,7 +10,7 @@ import {
STATS_PANELS,
THEME,
DEFAULT_GRID_STEP,
-} from "./constants";
+} from "@excalidraw/common";
import type { AppState, NormalizedZoomValue } from "./types";
@@ -52,6 +52,7 @@ export const getDefaultAppState = (): Omit<
type: "selection",
customType: null,
locked: DEFAULT_ELEMENT_PROPS.locked,
+ fromSelection: false,
lastActiveTool: null,
},
penMode: false,
diff --git a/packages/excalidraw/change.ts b/packages/excalidraw/change.ts
index 206f3dcc7..28eaf994f 100644
--- a/packages/excalidraw/change.ts
+++ b/packages/excalidraw/change.ts
@@ -1,36 +1,46 @@
-import { ENV } from "./constants";
+import {
+ arrayToMap,
+ arrayToObject,
+ assertNever,
+ isDevEnv,
+ isShallowEqual,
+ isTestEnv,
+ toBrandedType,
+} from "@excalidraw/common";
import {
BoundElement,
BindableElement,
bindingProperties,
updateBoundElements,
-} from "./element/binding";
-import { LinearElementEditor } from "./element/linearElementEditor";
-import { mutateElement, newElementWith } from "./element/mutateElement";
+} from "@excalidraw/element/binding";
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+import {
+ mutateElement,
+ newElementWith,
+} from "@excalidraw/element/mutateElement";
import {
getBoundTextElementId,
redrawTextBoundingBox,
-} from "./element/textElement";
+} from "@excalidraw/element/textElement";
import {
hasBoundTextElement,
isBindableElement,
isBoundToContainer,
isImageElement,
isTextElement,
-} from "./element/typeChecks";
-import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
-import { getNonDeletedGroupIds } from "./groups";
-import { getObservedAppState } from "./store";
-import {
- arrayToMap,
- arrayToObject,
- assertNever,
- isShallowEqual,
- toBrandedType,
-} from "./utils";
+} from "@excalidraw/element/typeChecks";
+
+import { getNonDeletedGroupIds } from "@excalidraw/element/groups";
+
+import {
+ orderByFractionalIndex,
+ syncMovedIndices,
+} from "@excalidraw/element/fractionalIndex";
+
+import type { BindableProp, BindingProp } from "@excalidraw/element/binding";
+
+import type { ElementUpdate } from "@excalidraw/element/mutateElement";
-import type { BindableProp, BindingProp } from "./element/binding";
-import type { ElementUpdate } from "./element/mutateElement";
import type {
ExcalidrawElement,
ExcalidrawImageElement,
@@ -40,14 +50,18 @@ import type {
Ordered,
OrderedExcalidrawElement,
SceneElementsMap,
-} from "./element/types";
+} from "@excalidraw/element/types";
+
+import type { SubtypeOf, ValueOf } from "@excalidraw/common/utility-types";
+
+import { getObservedAppState } from "./store";
+
import type {
AppState,
ObservedAppState,
ObservedElementsAppState,
ObservedStandaloneAppState,
} from "./types";
-import type { SubtypeOf, ValueOf } from "./utility-types";
/**
* Represents the difference between two objects of the same type.
@@ -514,7 +528,7 @@ export class AppStateChange implements Change {
// shouldn't really happen, but just in case
console.error(`Couldn't apply appstate change`, e);
- if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
+ if (isTestEnv() || isDevEnv()) {
throw e;
}
@@ -552,7 +566,7 @@ export class AppStateChange implements Change {
// if postprocessing fails it does not make sense to bubble up, but let's make sure we know about it
console.error(`Couldn't postprocess appstate change deltas.`);
- if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
+ if (isTestEnv() || isDevEnv()) {
throw e;
}
} finally {
@@ -842,7 +856,7 @@ export class ElementsChange implements Change {
change = new ElementsChange(added, removed, updated);
}
- if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
+ if (isTestEnv() || isDevEnv()) {
ElementsChange.validate(change, "added", this.satisfiesAddition);
ElementsChange.validate(change, "removed", this.satisfiesRemoval);
ElementsChange.validate(change, "updated", this.satisfiesUpdate);
@@ -1106,7 +1120,7 @@ export class ElementsChange implements Change {
} catch (e) {
console.error(`Couldn't apply elements change`, e);
- if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
+ if (isTestEnv() || isDevEnv()) {
throw e;
}
@@ -1137,7 +1151,7 @@ export class ElementsChange implements Change {
e,
);
- if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
+ if (isTestEnv() || isDevEnv()) {
throw e;
}
} finally {
@@ -1551,7 +1565,7 @@ export class ElementsChange implements Change {
// if postprocessing fails, it does not make sense to bubble up, but let's make sure we know about it
console.error(`Couldn't postprocess elements change deltas.`);
- if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
+ if (isTestEnv() || isDevEnv()) {
throw e;
}
} finally {
diff --git a/packages/excalidraw/charts.ts b/packages/excalidraw/charts.ts
index e54c55f28..7a221d547 100644
--- a/packages/excalidraw/charts.ts
+++ b/packages/excalidraw/charts.ts
@@ -1,21 +1,25 @@
import { pointFrom } from "@excalidraw/math";
-import type { Radians } from "@excalidraw/math";
-
import {
COLOR_PALETTE,
DEFAULT_CHART_COLOR_INDEX,
getAllColorsSpecificShade,
-} from "./colors";
-import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
VERTICAL_ALIGN,
-} from "./constants";
-import { newElement, newLinearElement, newTextElement } from "./element";
-import { randomId } from "./random";
+ randomId,
+ isDevEnv,
+} from "@excalidraw/common";
-import type { NonDeletedExcalidrawElement } from "./element/types";
+import {
+ newTextElement,
+ newLinearElement,
+ newElement,
+} from "@excalidraw/element/newElement";
+
+import type { Radians } from "@excalidraw/math";
+
+import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
export type ChartElements = readonly NonDeletedExcalidrawElement[];
@@ -373,7 +377,7 @@ const chartTypeBar = (
y,
groupId,
backgroundColor,
- import.meta.env.DEV,
+ isDevEnv(),
),
];
};
@@ -455,7 +459,7 @@ const chartTypeLine = (
y,
groupId,
backgroundColor,
- import.meta.env.DEV,
+ isDevEnv(),
),
line,
...lines,
diff --git a/packages/excalidraw/clients.ts b/packages/excalidraw/clients.ts
index 6cf4613a8..9467b1362 100644
--- a/packages/excalidraw/clients.ts
+++ b/packages/excalidraw/clients.ts
@@ -4,7 +4,8 @@ import {
COLOR_WHITE,
THEME,
UserIdleState,
-} from "./constants";
+} from "@excalidraw/common";
+
import { roundRect } from "./renderer/roundRect";
import type { InteractiveCanvasRenderConfig } from "./scene/types";
diff --git a/packages/excalidraw/clipboard.ts b/packages/excalidraw/clipboard.ts
index 1b0e9942a..40e4f8b96 100644
--- a/packages/excalidraw/clipboard.ts
+++ b/packages/excalidraw/clipboard.ts
@@ -1,25 +1,32 @@
-import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
import {
ALLOWED_PASTE_MIME_TYPES,
EXPORT_DATA_TYPES,
MIME_TYPES,
-} from "./constants";
-import { createFile, isSupportedImageFileType } from "./data/blob";
-import { mutateElement } from "./element/mutateElement";
-import { deepCopyElement } from "./element/newElement";
+ arrayToMap,
+ isMemberOf,
+ isPromiseLike,
+} from "@excalidraw/common";
+
+import { mutateElement } from "@excalidraw/element/mutateElement";
+import { deepCopyElement } from "@excalidraw/element/duplicate";
import {
isFrameLikeElement,
isInitializedImageElement,
-} from "./element/typeChecks";
-import { ExcalidrawError } from "./errors";
-import { getContainingFrame } from "./frame";
-import { arrayToMap, isMemberOf, isPromiseLike } from "./utils";
+} from "@excalidraw/element/typeChecks";
+
+import { getContainingFrame } from "@excalidraw/element/frame";
-import type { Spreadsheet } from "./charts";
import type {
ExcalidrawElement,
NonDeletedExcalidrawElement,
-} from "./element/types";
+} from "@excalidraw/element/types";
+
+import { ExcalidrawError } from "./errors";
+import { createFile, isSupportedImageFileType } from "./data/blob";
+import { tryParseSpreadsheet, VALID_SPREADSHEET } from "./charts";
+
+import type { Spreadsheet } from "./charts";
+
import type { BinaryFiles } from "./types";
type ElementsClipboard = {
diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx
index c74c1ad85..3a7df37a8 100644
--- a/packages/excalidraw/components/Actions.tsx
+++ b/packages/excalidraw/components/Actions.tsx
@@ -1,24 +1,41 @@
import clsx from "clsx";
import { useState } from "react";
-import { actionToggleZenMode } from "../actions";
+import {
+ CLASSES,
+ KEYS,
+ capitalizeString,
+ isTransparent,
+} from "@excalidraw/common";
-import { KEYS } from "../keys";
-import { CLASSES } from "../constants";
-import { alignActionsPredicate } from "../actions/actionAlign";
-import { trackEvent } from "../analytics";
-import { useTunnels } from "../context/tunnels";
import {
shouldAllowVerticalAlign,
suppportsHorizontalAlign,
-} from "../element/textElement";
+} from "@excalidraw/element/textElement";
+
import {
hasBoundTextElement,
isElbowArrow,
isImageElement,
isLinearElement,
isTextElement,
-} from "../element/typeChecks";
+} from "@excalidraw/element/typeChecks";
+
+import { hasStrokeColor, toolIsArrow } from "@excalidraw/element/comparisons";
+
+import type {
+ ExcalidrawElement,
+ ExcalidrawElementType,
+ NonDeletedElementsMap,
+ NonDeletedSceneElementsMap,
+} from "@excalidraw/element/types";
+
+import { actionToggleZenMode } from "../actions";
+
+import { alignActionsPredicate } from "../actions/actionAlign";
+import { trackEvent } from "../analytics";
+import { useTunnels } from "../context/tunnels";
+
import { t } from "../i18n";
import {
canChangeRoundness,
@@ -28,9 +45,8 @@ import {
hasStrokeStyle,
hasStrokeWidth,
} from "../scene";
-import { hasStrokeColor, toolIsArrow } from "../scene/comparisons";
-import { SHAPES } from "../shapes";
-import { capitalizeString, isTransparent } from "../utils";
+
+import { SHAPES } from "./shapes";
import "./Actions.scss";
@@ -46,14 +62,9 @@ import {
mermaidLogoIcon,
laserPointerToolIcon,
MagicIcon,
+ LassoIcon,
} from "./icons";
-import type {
- ExcalidrawElement,
- ExcalidrawElementType,
- NonDeletedElementsMap,
- NonDeletedSceneElementsMap,
-} from "../element/types";
import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
import type { ActionManager } from "../actions/manager";
@@ -73,7 +84,6 @@ export const canChangeStrokeColor = (
return (
(hasStrokeColor(appState.activeTool.type) &&
- appState.activeTool.type !== "image" &&
commonSelectedType !== "image" &&
commonSelectedType !== "frame" &&
commonSelectedType !== "magicframe") ||
@@ -285,6 +295,8 @@ export const ShapesSwitcher = ({
const frameToolSelected = activeTool.type === "frame";
const laserToolSelected = activeTool.type === "laser";
+ const lassoToolSelected = activeTool.type === "lasso";
+
const embeddableToolSelected = activeTool.type === "embeddable";
const { TTDDialogTriggerTunnel } = useTunnels();
@@ -306,6 +318,7 @@ export const ShapesSwitcher = ({
const shortcut = letter
? `${letter} ${t("helpDialog.or")} ${numericKey}`
: `${numericKey}`;
+
return (
{
if (appState.activeTool.type !== value) {
@@ -348,6 +369,7 @@ export const ShapesSwitcher = ({
"App-toolbar__extra-tools-trigger--selected":
frameToolSelected ||
embeddableToolSelected ||
+ lassoToolSelected ||
// in collab we're already highlighting the laser button
// outside toolbar, so let's not highlight extra-tools button
// on top of it
@@ -356,7 +378,15 @@ export const ShapesSwitcher = ({
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
title={t("toolBar.extraTools")}
>
- {extraToolsIcon}
+ {frameToolSelected
+ ? frameToolIcon
+ : embeddableToolSelected
+ ? EmbedIcon
+ : laserToolSelected && !app.props.isCollaborating
+ ? laserPointerToolIcon
+ : lassoToolSelected
+ ? LassoIcon
+ : extraToolsIcon}
setIsExtraToolsMenuOpen(false)}
@@ -389,6 +419,14 @@ export const ShapesSwitcher = ({
>
{t("toolBar.laser")}
+ app.setActiveTool({ type: "lasso" })}
+ icon={LassoIcon}
+ data-testid="toolbar-lasso"
+ selected={lassoToolSelected}
+ >
+ {t("toolBar.lasso")}
+
Generate
diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx
index 6017b73b7..276cde027 100644
--- a/packages/excalidraw/components/App.tsx
+++ b/packages/excalidraw/components/App.tsx
@@ -1,3 +1,10 @@
+import clsx from "clsx";
+import throttle from "lodash.throttle";
+import React, { useContext } from "react";
+import { flushSync } from "react-dom";
+import rough from "roughjs/bin/rough";
+import { nanoid } from "nanoid";
+
import {
clamp,
pointFrom,
@@ -11,16 +18,319 @@ import {
vectorNormalize,
} from "@excalidraw/math";
import { isPointInShape } from "@excalidraw/utils/collision";
-import { getSelectionBoxShape } from "@excalidraw/utils/geometry/shape";
-import clsx from "clsx";
-import throttle from "lodash.throttle";
-import { nanoid } from "nanoid";
-import React, { useContext } from "react";
-import { flushSync } from "react-dom";
-import rough from "roughjs/bin/rough";
+import { getSelectionBoxShape } from "@excalidraw/utils/shape";
+
+import {
+ COLOR_PALETTE,
+ CODES,
+ shouldResizeFromCenter,
+ shouldMaintainAspectRatio,
+ shouldRotateWithDiscreteAngle,
+ isArrowKey,
+ KEYS,
+ APP_NAME,
+ CURSOR_TYPE,
+ DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
+ DEFAULT_VERTICAL_ALIGN,
+ DRAGGING_THRESHOLD,
+ ELEMENT_SHIFT_TRANSLATE_AMOUNT,
+ ELEMENT_TRANSLATE_AMOUNT,
+ EVENT,
+ FRAME_STYLE,
+ IMAGE_MIME_TYPES,
+ IMAGE_RENDER_TIMEOUT,
+ isBrave,
+ LINE_CONFIRM_THRESHOLD,
+ MAX_ALLOWED_FILE_BYTES,
+ MIME_TYPES,
+ MQ_MAX_HEIGHT_LANDSCAPE,
+ MQ_MAX_WIDTH_LANDSCAPE,
+ MQ_MAX_WIDTH_PORTRAIT,
+ MQ_RIGHT_SIDEBAR_MIN_WIDTH,
+ POINTER_BUTTON,
+ ROUNDNESS,
+ SCROLL_TIMEOUT,
+ TAP_TWICE_TIMEOUT,
+ TEXT_TO_CENTER_SNAP_THRESHOLD,
+ THEME,
+ THEME_FILTER,
+ TOUCH_CTX_MENU_TIMEOUT,
+ VERTICAL_ALIGN,
+ YOUTUBE_STATES,
+ ZOOM_STEP,
+ POINTER_EVENTS,
+ TOOL_TYPE,
+ isIOS,
+ supportsResizeObserver,
+ DEFAULT_COLLISION_THRESHOLD,
+ DEFAULT_TEXT_ALIGN,
+ ARROW_TYPE,
+ DEFAULT_REDUCED_GLOBAL_ALPHA,
+ isSafari,
+ isLocalLink,
+ normalizeLink,
+ toValidURL,
+ getGridPoint,
+ getLineHeight,
+ debounce,
+ distance,
+ getFontString,
+ getNearestScrollableContainer,
+ isInputLike,
+ isToolIcon,
+ isWritableElement,
+ sceneCoordsToViewportCoords,
+ tupleToCoors,
+ viewportCoordsToSceneCoords,
+ wrapEvent,
+ updateObject,
+ updateActiveTool,
+ getShortcutKey,
+ isTransparent,
+ easeToValuesRAF,
+ muteFSAbortError,
+ isTestEnv,
+ isDevEnv,
+ easeOut,
+ updateStable,
+ addEventListener,
+ normalizeEOL,
+ getDateTime,
+ isShallowEqual,
+ arrayToMap,
+ type EXPORT_IMAGE_TYPES,
+ randomInteger,
+} from "@excalidraw/common";
+
+import {
+ getCommonBounds,
+ getElementAbsoluteCoords,
+} from "@excalidraw/element/bounds";
+
+import {
+ bindOrUnbindLinearElement,
+ bindOrUnbindLinearElements,
+ fixBindingsAfterDeletion,
+ getHoveredElementForBinding,
+ isBindingEnabled,
+ isLinearElementSimpleAndAlreadyBound,
+ maybeBindLinearElement,
+ shouldEnableBindingForPointerEvent,
+ updateBoundElements,
+ getSuggestedBindingsForArrows,
+} from "@excalidraw/element/binding";
+
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+
+import {
+ mutateElement,
+ newElementWith,
+} from "@excalidraw/element/mutateElement";
+
+import {
+ newFrameElement,
+ newFreeDrawElement,
+ newEmbeddableElement,
+ newMagicFrameElement,
+ newIframeElement,
+ newArrowElement,
+ newElement,
+ newImageElement,
+ newLinearElement,
+ newTextElement,
+ refreshTextDimensions,
+} from "@excalidraw/element/newElement";
+
+import {
+ deepCopyElement,
+ duplicateElements,
+} from "@excalidraw/element/duplicate";
+
+import {
+ hasBoundTextElement,
+ isArrowElement,
+ isBindingElement,
+ isBindingElementType,
+ isBoundToContainer,
+ isFrameLikeElement,
+ isImageElement,
+ isEmbeddableElement,
+ isInitializedImageElement,
+ isLinearElement,
+ isLinearElementType,
+ isUsingAdaptiveRadius,
+ isIframeElement,
+ isIframeLikeElement,
+ isMagicFrameElement,
+ isTextBindableContainer,
+ isElbowArrow,
+ isFlowchartNodeElement,
+ isBindableElement,
+ isTextElement,
+} from "@excalidraw/element/typeChecks";
+
+import {
+ getLockedLinearCursorAlignSize,
+ getNormalizedDimensions,
+ isElementCompletelyInViewport,
+ isElementInViewport,
+ isInvisiblySmallElement,
+} from "@excalidraw/element/sizeHelpers";
+
+import {
+ getBoundTextShape,
+ getCornerRadius,
+ getElementShape,
+ isPathALoop,
+} from "@excalidraw/element/shapes";
+
+import {
+ createSrcDoc,
+ embeddableURLValidator,
+ maybeParseEmbedSrc,
+ getEmbedLink,
+} from "@excalidraw/element/embeddable";
+
+import {
+ getInitializedImageElements,
+ loadHTMLImageElement,
+ normalizeSVG,
+ updateImageCache as _updateImageCache,
+} from "@excalidraw/element/image";
+
+import {
+ getBoundTextElement,
+ getContainerCenter,
+ getContainerElement,
+ isValidTextContainer,
+ redrawTextBoundingBox,
+} from "@excalidraw/element/textElement";
+
+import { shouldShowBoundingBox } from "@excalidraw/element/transformHandles";
+
+import {
+ getFrameChildren,
+ isCursorInFrame,
+ addElementsToFrame,
+ replaceAllElementsInFrame,
+ removeElementsFromFrame,
+ getElementsInResizingFrame,
+ getElementsInNewFrame,
+ getContainingFrame,
+ elementOverlapsWithFrame,
+ updateFrameMembershipOfSelectedElements,
+ isElementInFrame,
+ getFrameLikeTitle,
+ getElementsOverlappingFrame,
+ filterElementsEligibleAsFrameChildren,
+} from "@excalidraw/element/frame";
+
+import {
+ hitElementBoundText,
+ hitElementBoundingBoxOnly,
+ hitElementItself,
+} from "@excalidraw/element/collision";
+
+import { getVisibleSceneBounds } from "@excalidraw/element/bounds";
+
+import {
+ FlowChartCreator,
+ FlowChartNavigator,
+ getLinkDirectionFromKey,
+} from "@excalidraw/element/flowchart";
+
+import { cropElement } from "@excalidraw/element/cropElement";
+
+import { wrapText } from "@excalidraw/element/textWrapping";
+
+import {
+ isElementLink,
+ parseElementLinkFromURL,
+} from "@excalidraw/element/elementLink";
+
+import {
+ isMeasureTextSupported,
+ normalizeText,
+ measureText,
+ getLineHeightInPx,
+ getApproxMinLineWidth,
+ getApproxMinLineHeight,
+ getMinTextElementWidth,
+} from "@excalidraw/element/textMeasurements";
+
+import { ShapeCache } from "@excalidraw/element/ShapeCache";
+
+import { getRenderOpacity } from "@excalidraw/element/renderElement";
+
+import {
+ editGroupForSelectedElement,
+ getElementsInGroup,
+ getSelectedGroupIdForElement,
+ getSelectedGroupIds,
+ isElementInGroup,
+ isSelectedViaGroup,
+ selectGroupsForSelectedElements,
+} from "@excalidraw/element/groups";
+
+import {
+ syncInvalidIndices,
+ syncMovedIndices,
+} from "@excalidraw/element/fractionalIndex";
+
+import {
+ excludeElementsInFramesFromSelection,
+ makeNextSelectedElementIds,
+} from "@excalidraw/element/selection";
+
+import {
+ getResizeOffsetXY,
+ getResizeArrowDirection,
+ transformElements,
+} from "@excalidraw/element/resizeElements";
+
+import {
+ getCursorForResizingElement,
+ getElementWithTransformHandleType,
+ getTransformHandleTypeFromCoords,
+} from "@excalidraw/element/resizeTest";
+
+import {
+ dragNewElement,
+ dragSelectedElements,
+ getDragOffsetXY,
+} from "@excalidraw/element/dragElements";
+
+import { isNonDeletedElement } from "@excalidraw/element";
import type { LocalPoint, Radians } from "@excalidraw/math";
+import type {
+ ExcalidrawBindableElement,
+ ExcalidrawElement,
+ ExcalidrawFreeDrawElement,
+ ExcalidrawGenericElement,
+ ExcalidrawLinearElement,
+ ExcalidrawTextElement,
+ NonDeleted,
+ InitializedExcalidrawImageElement,
+ ExcalidrawImageElement,
+ FileId,
+ NonDeletedExcalidrawElement,
+ ExcalidrawTextContainer,
+ ExcalidrawFrameLikeElement,
+ ExcalidrawMagicFrameElement,
+ ExcalidrawIframeLikeElement,
+ IframeData,
+ ExcalidrawIframeElement,
+ ExcalidrawEmbeddableElement,
+ Ordered,
+ MagicGenerationData,
+ ExcalidrawNonSelectionElement,
+ ExcalidrawArrowElement,
+} from "@excalidraw/element/types";
+
+import type { ValueOf } from "@excalidraw/common/utility-types";
+
import {
actionAddToLibrary,
actionBringForward,
@@ -77,147 +387,13 @@ import {
isHandToolActive,
} from "../appState";
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
-import {
- APP_NAME,
- CURSOR_TYPE,
- DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
- DEFAULT_VERTICAL_ALIGN,
- DRAGGING_THRESHOLD,
- ELEMENT_SHIFT_TRANSLATE_AMOUNT,
- ELEMENT_TRANSLATE_AMOUNT,
- ENV,
- EVENT,
- FRAME_STYLE,
- IMAGE_MIME_TYPES,
- IMAGE_RENDER_TIMEOUT,
- isBrave,
- LINE_CONFIRM_THRESHOLD,
- MAX_ALLOWED_FILE_BYTES,
- MIME_TYPES,
- MQ_MAX_HEIGHT_LANDSCAPE,
- MQ_MAX_WIDTH_LANDSCAPE,
- MQ_MAX_WIDTH_PORTRAIT,
- MQ_RIGHT_SIDEBAR_MIN_WIDTH,
- POINTER_BUTTON,
- ROUNDNESS,
- SCROLL_TIMEOUT,
- TAP_TWICE_TIMEOUT,
- TEXT_TO_CENTER_SNAP_THRESHOLD,
- THEME,
- THEME_FILTER,
- TOUCH_CTX_MENU_TIMEOUT,
- VERTICAL_ALIGN,
- YOUTUBE_STATES,
- ZOOM_STEP,
- POINTER_EVENTS,
- TOOL_TYPE,
- isIOS,
- supportsResizeObserver,
- DEFAULT_COLLISION_THRESHOLD,
- DEFAULT_TEXT_ALIGN,
- ARROW_TYPE,
- DEFAULT_REDUCED_GLOBAL_ALPHA,
- isSafari,
- type EXPORT_IMAGE_TYPES,
-} from "../constants";
import { exportCanvas, loadFromBlob } from "../data";
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { restore, restoreElements } from "../data/restore";
-import {
- dragNewElement,
- dragSelectedElements,
- duplicateElement,
- getCommonBounds,
- getCursorForResizingElement,
- getDragOffsetXY,
- getElementWithTransformHandleType,
- getNormalizedDimensions,
- getResizeArrowDirection,
- getResizeOffsetXY,
- getLockedLinearCursorAlignSize,
- getTransformHandleTypeFromCoords,
- isInvisiblySmallElement,
- isNonDeletedElement,
- isTextElement,
- newElement,
- newLinearElement,
- newTextElement,
- newImageElement,
- transformElements,
- refreshTextDimensions,
- redrawTextBoundingBox,
- getElementAbsoluteCoords,
-} from "../element";
-import {
- bindOrUnbindLinearElement,
- bindOrUnbindLinearElements,
- fixBindingsAfterDeletion,
- fixBindingsAfterDuplication,
- getHoveredElementForBinding,
- isBindingEnabled,
- isLinearElementSimpleAndAlreadyBound,
- maybeBindLinearElement,
- shouldEnableBindingForPointerEvent,
- updateBoundElements,
- getSuggestedBindingsForArrows,
-} from "../element/binding";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import { mutateElement, newElementWith } from "../element/mutateElement";
-import {
- deepCopyElement,
- duplicateElements,
- newFrameElement,
- newFreeDrawElement,
- newEmbeddableElement,
- newMagicFrameElement,
- newIframeElement,
- newArrowElement,
-} from "../element/newElement";
-import {
- hasBoundTextElement,
- isArrowElement,
- isBindingElement,
- isBindingElementType,
- isBoundToContainer,
- isFrameLikeElement,
- isImageElement,
- isEmbeddableElement,
- isInitializedImageElement,
- isLinearElement,
- isLinearElementType,
- isUsingAdaptiveRadius,
- isIframeElement,
- isIframeLikeElement,
- isMagicFrameElement,
- isTextBindableContainer,
- isElbowArrow,
- isFlowchartNodeElement,
- isBindableElement,
-} from "../element/typeChecks";
import { getCenter, getDistance } from "../gesture";
-import {
- editGroupForSelectedElement,
- getElementsInGroup,
- getSelectedGroupIdForElement,
- getSelectedGroupIds,
- isElementInGroup,
- isSelectedViaGroup,
- selectGroupsForSelectedElements,
-} from "../groups";
import { History } from "../history";
import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n";
-import {
- CODES,
- shouldResizeFromCenter,
- shouldMaintainAspectRatio,
- shouldRotateWithDiscreteAngle,
- isArrowKey,
- KEYS,
-} from "../keys";
-import {
- isElementCompletelyInViewport,
- isElementInViewport,
-} from "../element/sizeHelpers";
+
import {
calculateScrollCenter,
getElementsWithinSelection,
@@ -228,46 +404,6 @@ import {
} from "../scene";
import Scene from "../scene/Scene";
import { getStateForZoom } from "../scene/zoom";
-import {
- findShapeByKey,
- getBoundTextShape,
- getCornerRadius,
- getElementShape,
- isPathALoop,
-} from "../shapes";
-import {
- debounce,
- distance,
- getFontString,
- getNearestScrollableContainer,
- isInputLike,
- isToolIcon,
- isWritableElement,
- sceneCoordsToViewportCoords,
- tupleToCoors,
- viewportCoordsToSceneCoords,
- wrapEvent,
- updateObject,
- updateActiveTool,
- getShortcutKey,
- isTransparent,
- easeToValuesRAF,
- muteFSAbortError,
- isTestEnv,
- easeOut,
- updateStable,
- addEventListener,
- normalizeEOL,
- getDateTime,
- isShallowEqual,
- arrayToMap,
-} from "../utils";
-import {
- createSrcDoc,
- embeddableURLValidator,
- maybeParseEmbedSrc,
- getEmbedLink,
-} from "../element/embeddable";
import {
dataURLToFile,
dataURLToString,
@@ -284,49 +420,15 @@ import {
resizeImageFile,
SVGStringToFile,
} from "../data/blob";
-import {
- getInitializedImageElements,
- loadHTMLImageElement,
- normalizeSVG,
- updateImageCache as _updateImageCache,
-} from "../element/image";
+
import { fileOpen } from "../data/filesystem";
-import {
- bindTextToShapeAfterDuplication,
- getBoundTextElement,
- getContainerCenter,
- getContainerElement,
- isValidTextContainer,
-} from "../element/textElement";
import {
showHyperlinkTooltip,
hideHyperlinkToolip,
Hyperlink,
} from "../components/hyperlink/Hyperlink";
-import { isLocalLink, normalizeLink, toValidURL } from "../data/url";
-import { shouldShowBoundingBox } from "../element/transformHandles";
-import { Fonts, getLineHeight } from "../fonts";
-import {
- getFrameChildren,
- isCursorInFrame,
- bindElementsToFramesAfterDuplication,
- addElementsToFrame,
- replaceAllElementsInFrame,
- removeElementsFromFrame,
- getElementsInResizingFrame,
- getElementsInNewFrame,
- getContainingFrame,
- elementOverlapsWithFrame,
- updateFrameMembershipOfSelectedElements,
- isElementInFrame,
- getFrameLikeTitle,
- getElementsOverlappingFrame,
- filterElementsEligibleAsFrameChildren,
-} from "../frame";
-import {
- excludeElementsInFramesFromSelection,
- makeNextSelectedElementIds,
-} from "../scene/selection";
+
+import { Fonts } from "../fonts";
import { editorJotaiStore } from "../editor-jotai";
import { ImageSceneDataError } from "../errors";
import {
@@ -340,11 +442,9 @@ import {
getReferenceSnapPoints,
SnapCache,
isGridModeEnabled,
- getGridPoint,
} from "../snapping";
import { convertToExcalidrawElements } from "../data/transform";
import { Renderer } from "../scene/Renderer";
-import { ShapeCache } from "../scene/ShapeCache";
import {
setEraserCursor,
setCursor,
@@ -352,40 +452,17 @@ import {
setCursorForShape,
} from "../cursor";
import { Emitter } from "../emitter";
-import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
-import { COLOR_PALETTE } from "../colors";
+import { ElementCanvasButtons } from "../components/ElementCanvasButtons";
import { Store, CaptureUpdateAction } from "../store";
import { AnimatedTrail } from "../animated-trail";
import { LaserTrails } from "../laser-trails";
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
-import { getRenderOpacity } from "../renderer/renderElement";
-import {
- hitElementBoundText,
- hitElementBoundingBoxOnly,
- hitElementItself,
-} from "../element/collision";
-import { textWysiwyg } from "../element/textWysiwyg";
+import { textWysiwyg } from "../wysiwyg/textWysiwyg";
import { isOverScrollBars } from "../scene/scrollbars";
-import { syncInvalidIndices, syncMovedIndices } from "../fractionalIndex";
-import { getVisibleSceneBounds } from "../element/bounds";
+
import { isMaybeMermaidDefinition } from "../mermaid";
-import {
- FlowChartCreator,
- FlowChartNavigator,
- getLinkDirectionFromKey,
-} from "../element/flowchart";
-import { cropElement } from "../element/cropElement";
-import { wrapText } from "../element/textWrapping";
-import { isElementLink, parseElementLinkFromURL } from "../element/elementLink";
-import {
- isMeasureTextSupported,
- normalizeText,
- measureText,
- getLineHeightInPx,
- getApproxMinLineWidth,
- getApproxMinLineHeight,
- getMinTextElementWidth,
-} from "../element/textMeasurements";
+
+import { LassoTrail } from "../lasso";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import BraveMeasureTextError from "./BraveMeasureTextError";
@@ -406,40 +483,19 @@ import {
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
import { Toast } from "./Toast";
-import type { Action, ActionResult } from "../actions/types";
+import { findShapeByKey } from "./shapes";
+
+import type {
+ RenderInteractiveSceneCallback,
+ ScrollBars,
+} from "../scene/types";
+
import type { PastedMixedContent } from "../clipboard";
import type { ExportedElements } from "../data";
import type { ContextMenuItems } from "./ContextMenu";
import type { FileSystemHandle } from "../data/filesystem";
import type { ExcalidrawElementSkeleton } from "../data/transform";
-import type {
- ExcalidrawBindableElement,
- ExcalidrawElement,
- ExcalidrawFreeDrawElement,
- ExcalidrawGenericElement,
- ExcalidrawLinearElement,
- ExcalidrawTextElement,
- NonDeleted,
- InitializedExcalidrawImageElement,
- ExcalidrawImageElement,
- FileId,
- NonDeletedExcalidrawElement,
- ExcalidrawTextContainer,
- ExcalidrawFrameLikeElement,
- ExcalidrawMagicFrameElement,
- ExcalidrawIframeLikeElement,
- IframeData,
- ExcalidrawIframeElement,
- ExcalidrawEmbeddableElement,
- Ordered,
- MagicGenerationData,
- ExcalidrawNonSelectionElement,
- ExcalidrawArrowElement,
-} from "../element/types";
-import type {
- RenderInteractiveSceneCallback,
- ScrollBars,
-} from "../scene/types";
+
import type {
AppClassProperties,
AppProps,
@@ -468,8 +524,8 @@ import type {
NullableGridSize,
Offsets,
} from "../types";
-import type { ValueOf } from "../utility-types";
import type { RoughCanvas } from "roughjs/bin/canvas";
+import type { Action, ActionResult } from "../actions/types";
const AppContext = React.createContext(null!);
const AppPropsContext = React.createContext(null!);
@@ -639,6 +695,7 @@ class App extends React.Component {
? "rgba(0, 0, 0, 0.2)"
: "rgba(255, 255, 255, 0.2)",
});
+ lassoTrail = new LassoTrail(this.animationFrameHandler, this);
onChangeEmitter = new Emitter<
[
@@ -1617,7 +1674,11 @@ class App extends React.Component {
{selectedElements.length === 1 &&
this.state.openDialog?.name !==
@@ -2439,7 +2500,7 @@ class App extends React.Component {
this.excalidrawContainerValue.container =
this.excalidrawContainerRef.current;
- if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
+ if (isTestEnv() || isDevEnv()) {
const setState = this.setState.bind(this);
Object.defineProperties(window.h, {
state: {
@@ -3224,17 +3285,16 @@ class App extends React.Component {
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
- const newElements = duplicateElements(
- elements.map((element) => {
+ const { newElements } = duplicateElements({
+ type: "everything",
+ elements: elements.map((element) => {
return newElementWith(element, {
x: element.x + gridX - minX,
y: element.y + gridY - minY,
});
}),
- {
- randomizeSeed: !opts.retainSeed,
- },
- );
+ randomizeSeed: !opts.retainSeed,
+ });
const prevElements = this.scene.getElementsIncludingDeleted();
let nextElements = [...prevElements, ...newElements];
@@ -4578,7 +4638,10 @@ class App extends React.Component {
this.state.openDialog?.name === "elementLinkSelector"
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
- } else if (this.state.activeTool.type === "selection") {
+ } else if (
+ this.state.activeTool.type === "selection" ||
+ this.state.activeTool.type === "lasso"
+ ) {
resetCursor(this.interactiveCanvas);
} else {
setCursorForShape(this.interactiveCanvas, this.state);
@@ -4686,7 +4749,8 @@ class App extends React.Component {
}
)
| { type: "custom"; customType: string }
- ) & { locked?: boolean },
+ ) & { locked?: boolean; fromSelection?: boolean },
+ keepSelection = false,
) => {
if (!this.isToolSupported(tool.type)) {
console.warn(
@@ -4728,7 +4792,21 @@ class App extends React.Component {
this.store.shouldCaptureIncrement();
}
- if (nextActiveTool.type !== "selection") {
+ if (nextActiveTool.type === "lasso") {
+ return {
+ ...prevState,
+ activeTool: nextActiveTool,
+ ...(keepSelection
+ ? {}
+ : {
+ selectedElementIds: makeNextSelectedElementIds({}, prevState),
+ selectedGroupIds: makeNextSelectedElementIds({}, prevState),
+ editingGroupId: null,
+ multiElement: null,
+ }),
+ ...commonResets,
+ };
+ } else if (nextActiveTool.type !== "selection") {
return {
...prevState,
activeTool: nextActiveTool,
@@ -6095,7 +6173,12 @@ class App extends React.Component {
this.setState({
activeEmbeddable: { element: hitElement, state: "hover" },
});
- } else if (!hitElement || !isElbowArrow(hitElement)) {
+ } else if (
+ !hitElement ||
+ // Ebow arrows can only be moved when unconnected
+ !isElbowArrow(hitElement) ||
+ !(hitElement.startBinding || hitElement.endBinding)
+ ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
if (this.state.activeEmbeddable?.state === "hover") {
this.setState({ activeEmbeddable: null });
@@ -6288,7 +6371,13 @@ class App extends React.Component {
if (isHoveringAPointHandle || segmentMidPointHoveredCoords) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
- setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
+ if (
+ // Ebow arrows can only be moved when unconnected
+ !isElbowArrow(element) ||
+ !(element.startBinding || element.endBinding)
+ ) {
+ setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
+ }
}
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
if (
@@ -6540,6 +6629,7 @@ class App extends React.Component {
!this.state.penMode ||
event.pointerType !== "touch" ||
this.state.activeTool.type === "selection" ||
+ this.state.activeTool.type === "lasso" ||
this.state.activeTool.type === "text" ||
this.state.activeTool.type === "image";
@@ -6547,7 +6637,13 @@ class App extends React.Component {
return;
}
- if (this.state.activeTool.type === "text") {
+ if (this.state.activeTool.type === "lasso") {
+ this.lassoTrail.startPath(
+ pointerDownState.origin.x,
+ pointerDownState.origin.y,
+ event.shiftKey,
+ );
+ } else if (this.state.activeTool.type === "text") {
this.handleTextOnPointerDown(event, pointerDownState);
} else if (
this.state.activeTool.type === "arrow" ||
@@ -7004,7 +7100,10 @@ class App extends React.Component {
}
private clearSelectionIfNotUsingSelection = (): void => {
- if (this.state.activeTool.type !== "selection") {
+ if (
+ this.state.activeTool.type !== "selection" &&
+ this.state.activeTool.type !== "lasso"
+ ) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedGroupIds: {},
@@ -8138,6 +8237,7 @@ class App extends React.Component {
...this.state.selectedLinearElement,
pointerDownState: ret.pointerDownState,
selectedPointsIndices: ret.selectedPointsIndices,
+ segmentMidPointHoveredCoords: null,
},
});
}
@@ -8147,6 +8247,7 @@ class App extends React.Component {
...this.state.editingLinearElement,
pointerDownState: ret.pointerDownState,
selectedPointsIndices: ret.selectedPointsIndices,
+ segmentMidPointHoveredCoords: null,
},
});
}
@@ -8160,7 +8261,7 @@ class App extends React.Component {
return;
}
- const didDrag = LinearElementEditor.handlePointDragging(
+ const newLinearElementEditor = LinearElementEditor.handlePointDragging(
event,
this,
pointerCoords.x,
@@ -8174,29 +8275,18 @@ class App extends React.Component {
linearElementEditor,
this.scene,
);
- if (didDrag) {
+ if (newLinearElementEditor) {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
pointerDownState.drag.hasOccurred = true;
- if (
- this.state.editingLinearElement &&
- !this.state.editingLinearElement.isDragging
- ) {
- this.setState({
- editingLinearElement: {
- ...this.state.editingLinearElement,
- isDragging: true,
- },
- });
- }
- if (!this.state.selectedLinearElement.isDragging) {
- this.setState({
- selectedLinearElement: {
- ...this.state.selectedLinearElement,
- isDragging: true,
- },
- });
- }
+
+ this.setState({
+ editingLinearElement: this.state.editingLinearElement
+ ? newLinearElementEditor
+ : null,
+ selectedLinearElement: newLinearElementEditor,
+ });
+
return;
}
}
@@ -8213,7 +8303,8 @@ class App extends React.Component {
if (
(hasHitASelectedElement ||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
- !isSelectingPointsInLineEditor
+ !isSelectingPointsInLineEditor &&
+ this.state.activeTool.type !== "lasso"
) {
const selectedElements = this.scene.getSelectedElements(this.state);
@@ -8422,146 +8513,70 @@ class App extends React.Component {
pointerDownState.hit.hasBeenDuplicated = true;
- const nextElements = [];
- const elementsToAppend = [];
- const groupIdMap = new Map();
- const oldIdToDuplicatedId = new Map();
- const hitElement = pointerDownState.hit.element;
- const selectedElementIds = new Set(
- this.scene
- .getSelectedElements({
- selectedElementIds: this.state.selectedElementIds,
- includeBoundTextElement: true,
- includeElementsInFrames: true,
- })
- .map((element) => element.id),
- );
-
const elements = this.scene.getElementsIncludingDeleted();
-
- for (const element of elements) {
- const isInSelection =
- selectedElementIds.has(element.id) ||
- // case: the state.selectedElementIds might not have been
- // updated yet by the time this mousemove event is fired
- (element.id === hitElement?.id &&
- pointerDownState.hit.wasAddedToSelection);
- // NOTE (mtolmacs): This is a temporary fix for very large scenes
- if (
- Math.abs(element.x) > 1e7 ||
- Math.abs(element.x) > 1e7 ||
- Math.abs(element.width) > 1e7 ||
- Math.abs(element.height) > 1e7
- ) {
- console.error(
- `Alt+dragging element in scene with invalid dimensions`,
- element.x,
- element.y,
- element.width,
- element.height,
- isInSelection,
- );
-
- return;
- }
-
- if (isInSelection) {
- const duplicatedElement = duplicateElement(
- this.state.editingGroupId,
- groupIdMap,
- element,
- );
-
- // NOTE (mtolmacs): This is a temporary fix for very large scenes
- if (
- Math.abs(duplicatedElement.x) > 1e7 ||
- Math.abs(duplicatedElement.x) > 1e7 ||
- Math.abs(duplicatedElement.width) > 1e7 ||
- Math.abs(duplicatedElement.height) > 1e7
- ) {
- console.error(
- `Alt+dragging duplicated element with invalid dimensions`,
- duplicatedElement.x,
- duplicatedElement.y,
- duplicatedElement.width,
- duplicatedElement.height,
- );
-
- return;
- }
-
- const origElement = pointerDownState.originalElements.get(
- element.id,
- )!;
-
- // NOTE (mtolmacs): This is a temporary fix for very large scenes
- if (
- Math.abs(origElement.x) > 1e7 ||
- Math.abs(origElement.x) > 1e7 ||
- Math.abs(origElement.width) > 1e7 ||
- Math.abs(origElement.height) > 1e7
- ) {
- console.error(
- `Alt+dragging duplicated element with invalid dimensions`,
- origElement.x,
- origElement.y,
- origElement.width,
- origElement.height,
- );
-
- return;
- }
-
- mutateElement(duplicatedElement, {
- x: origElement.x,
- y: origElement.y,
- });
-
- // put duplicated element to pointerDownState.originalElements
- // so that we can snap to the duplicated element without releasing
- pointerDownState.originalElements.set(
- duplicatedElement.id,
- duplicatedElement,
- );
-
- nextElements.push(duplicatedElement);
- elementsToAppend.push(element);
- oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
- } else {
- nextElements.push(element);
- }
+ const hitElement = pointerDownState.hit.element;
+ const selectedElements = this.scene.getSelectedElements({
+ selectedElementIds: this.state.selectedElementIds,
+ includeBoundTextElement: true,
+ includeElementsInFrames: true,
+ });
+ if (
+ hitElement &&
+ // hit element may not end up being selected
+ // if we're alt-dragging a common bounding box
+ // over the hit element
+ pointerDownState.hit.wasAddedToSelection &&
+ !selectedElements.find((el) => el.id === hitElement.id)
+ ) {
+ selectedElements.push(hitElement);
}
- let nextSceneElements: ExcalidrawElement[] = [
- ...nextElements,
- ...elementsToAppend,
- ];
+ const idsOfElementsToDuplicate = new Map(
+ selectedElements.map((el) => [el.id, el]),
+ );
+
+ const { newElements: clonedElements, elementsWithClones } =
+ duplicateElements({
+ type: "in-place",
+ elements,
+ appState: this.state,
+ randomizeSeed: true,
+ idsOfElementsToDuplicate,
+ overrides: (el) => {
+ const origEl = pointerDownState.originalElements.get(el.id);
+
+ if (origEl) {
+ return {
+ x: origEl.x,
+ y: origEl.y,
+ seed: origEl.seed,
+ };
+ }
+
+ return {};
+ },
+ reverseOrder: true,
+ });
+ clonedElements.forEach((element) => {
+ pointerDownState.originalElements.set(element.id, element);
+ });
const mappedNewSceneElements = this.props.onDuplicate?.(
- nextSceneElements,
+ elementsWithClones,
elements,
);
- nextSceneElements = mappedNewSceneElements || nextSceneElements;
-
- syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend));
-
- bindTextToShapeAfterDuplication(
- nextElements,
- elementsToAppend,
- oldIdToDuplicatedId,
- );
- fixBindingsAfterDuplication(
- nextSceneElements,
- elementsToAppend,
- oldIdToDuplicatedId,
- "duplicatesServeAsOld",
- );
- bindElementsToFramesAfterDuplication(
- nextSceneElements,
- elementsToAppend,
- oldIdToDuplicatedId,
- );
+ const nextSceneElements = syncMovedIndices(
+ mappedNewSceneElements || elementsWithClones,
+ arrayToMap(clonedElements),
+ ).map((el) => {
+ if (idsOfElementsToDuplicate.has(el.id)) {
+ return newElementWith(el, {
+ seed: randomInteger(),
+ });
+ }
+ return el;
+ });
this.scene.replaceAllElements(nextSceneElements);
this.maybeCacheVisibleGaps(event, selectedElements, true);
@@ -8575,7 +8590,37 @@ class App extends React.Component {
if (this.state.selectionElement) {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
- this.maybeDragNewGenericElement(pointerDownState, event);
+ if (event.altKey) {
+ this.setActiveTool(
+ { type: "lasso", fromSelection: true },
+ event.shiftKey,
+ );
+ this.lassoTrail.startPath(
+ pointerDownState.origin.x,
+ pointerDownState.origin.y,
+ event.shiftKey,
+ );
+ this.setAppState({
+ selectionElement: null,
+ });
+ } else {
+ this.maybeDragNewGenericElement(pointerDownState, event);
+ }
+ } else if (this.state.activeTool.type === "lasso") {
+ if (!event.altKey && this.state.activeTool.fromSelection) {
+ this.setActiveTool({ type: "selection" });
+ this.createGenericElementOnPointerDown("selection", pointerDownState);
+ pointerDownState.lastCoords.x = pointerCoords.x;
+ pointerDownState.lastCoords.y = pointerCoords.y;
+ this.maybeDragNewGenericElement(pointerDownState, event);
+ this.lassoTrail.endPath();
+ } else {
+ this.lassoTrail.addPointToPath(
+ pointerCoords.x,
+ pointerCoords.y,
+ event.shiftKey,
+ );
+ }
} else {
// It is very important to read this.state within each move event,
// otherwise we would read a stale one!
@@ -8830,6 +8875,8 @@ class App extends React.Component {
originSnapOffset: null,
}));
+ // just in case, tool changes mid drag, always clean up
+ this.lassoTrail.endPath();
this.lastPointerMoveCoords = null;
SnapCache.setReferenceSnapPoints(null);
@@ -9546,6 +9593,8 @@ class App extends React.Component {
}
if (
+ // do not clear selection if lasso is active
+ this.state.activeTool.type !== "lasso" &&
// not elbow midpoint dragged
!(hitElement && isElbowArrow(hitElement)) &&
// not dragged
@@ -9644,7 +9693,13 @@ class App extends React.Component {
return;
}
- if (!activeTool.locked && activeTool.type !== "freedraw") {
+ if (
+ !activeTool.locked &&
+ activeTool.type !== "freedraw" &&
+ (activeTool.type !== "lasso" ||
+ // if lasso is turned on but from selection => reset to selection
+ (activeTool.type === "lasso" && activeTool.fromSelection))
+ ) {
resetCursor(this.interactiveCanvas);
this.setState({
newElement: null,
@@ -10499,7 +10554,7 @@ class App extends React.Component {
width: distance(pointerDownState.origin.x, pointerCoords.x),
height: distance(pointerDownState.origin.y, pointerCoords.y),
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
- shouldResizeFromCenter: shouldResizeFromCenter(event),
+ shouldResizeFromCenter: false,
zoom: this.state.zoom.value,
informMutation,
});
@@ -11154,7 +11209,7 @@ declare global {
}
export const createTestHook = () => {
- if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
+ if (isTestEnv() || isDevEnv()) {
window.h = window.h || ({} as Window["h"]);
Object.defineProperties(window.h, {
diff --git a/packages/excalidraw/components/Button.tsx b/packages/excalidraw/components/Button.tsx
index 9512d607f..acdd4afde 100644
--- a/packages/excalidraw/components/Button.tsx
+++ b/packages/excalidraw/components/Button.tsx
@@ -1,7 +1,7 @@
import clsx from "clsx";
import React from "react";
-import { composeEventHandlers } from "../utils";
+import { composeEventHandlers } from "@excalidraw/common";
import "./Button.scss";
diff --git a/packages/excalidraw/components/ColorPicker/ColorInput.tsx b/packages/excalidraw/components/ColorPicker/ColorInput.tsx
index dc7d572a3..a3f6722eb 100644
--- a/packages/excalidraw/components/ColorPicker/ColorInput.tsx
+++ b/packages/excalidraw/components/ColorPicker/ColorInput.tsx
@@ -1,10 +1,10 @@
import clsx from "clsx";
import { useCallback, useEffect, useRef, useState } from "react";
+import { KEYS, getShortcutKey } from "@excalidraw/common";
+
import { useAtom } from "../../editor-jotai";
import { t } from "../../i18n";
-import { KEYS } from "../../keys";
-import { getShortcutKey } from "../../utils";
import { useDevice } from "../App";
import { activeEyeDropperAtom } from "../EyeDropper";
import { eyeDropperIcon } from "../icons";
diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.scss b/packages/excalidraw/components/ColorPicker/ColorPicker.scss
index 39e1845c3..56b40869b 100644
--- a/packages/excalidraw/components/ColorPicker/ColorPicker.scss
+++ b/packages/excalidraw/components/ColorPicker/ColorPicker.scss
@@ -27,16 +27,22 @@
.color-picker__top-picks {
display: flex;
justify-content: space-between;
+ align-items: center;
}
.color-picker__button {
- --radius: 0.25rem;
+ --radius: 4px;
+ --size: 1.375rem;
+
+ &.has-outline {
+ box-shadow: inset 0 0 0 1px #d9d9d9;
+ }
padding: 0;
margin: 0;
- width: 1.35rem;
- height: 1.35rem;
- border: 1px solid var(--color-gray-30);
+ width: var(--size);
+ height: var(--size);
+ border: 0;
border-radius: var(--radius);
filter: var(--theme-filter);
background-color: var(--swatch-color);
@@ -45,16 +51,20 @@
font-family: inherit;
box-sizing: border-box;
- &:hover {
+ &:hover:not(.active):not(.color-picker__button--large) {
+ transform: scale(1.075);
+ }
+
+ &:hover:not(.active).color-picker__button--large {
&::after {
content: "";
position: absolute;
- top: -2px;
- left: -2px;
- right: -2px;
- bottom: -2px;
+ top: -1px;
+ left: -1px;
+ right: -1px;
+ bottom: -1px;
box-shadow: 0 0 0 1px var(--color-gray-30);
- border-radius: calc(var(--radius) + 1px);
+ border-radius: var(--radius);
filter: var(--theme-filter);
}
}
@@ -62,13 +72,14 @@
&.active {
.color-picker__button-outline {
position: absolute;
- top: -2px;
- left: -2px;
- right: -2px;
- bottom: -2px;
+ --offset: -1px;
+ top: var(--offset);
+ left: var(--offset);
+ right: var(--offset);
+ bottom: var(--offset);
box-shadow: 0 0 0 1px var(--color-primary-darkest);
z-index: 1; // due hover state so this has preference
- border-radius: calc(var(--radius) + 1px);
+ border-radius: var(--radius);
filter: var(--theme-filter);
}
}
@@ -123,10 +134,11 @@
.color-picker__button__hotkey-label {
position: absolute;
- right: 4px;
- bottom: 4px;
+ right: 5px;
+ bottom: 3px;
filter: none;
font-size: 11px;
+ font-weight: 500;
}
.color-picker {
diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx
index 7f6ee8a85..eb6d82d9e 100644
--- a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx
+++ b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx
@@ -2,10 +2,18 @@ import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx";
import { useRef } from "react";
-import { COLOR_PALETTE } from "../../colors";
+import {
+ COLOR_OUTLINE_CONTRAST_THRESHOLD,
+ COLOR_PALETTE,
+ isTransparent,
+} from "@excalidraw/common";
+
+import type { ColorTuple, ColorPaletteCustom } from "@excalidraw/common";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { useAtom } from "../../editor-jotai";
import { t } from "../../i18n";
-import { isTransparent } from "../../utils";
import { useExcalidrawContainer } from "../App";
import { ButtonSeparator } from "../ButtonSeparator";
import { activeEyeDropperAtom } from "../EyeDropper";
@@ -15,13 +23,12 @@ import { ColorInput } from "./ColorInput";
import { Picker } from "./Picker";
import PickerHeading from "./PickerHeading";
import { TopPicks } from "./TopPicks";
-import { activeColorPickerSectionAtom } from "./colorPickerUtils";
+import { activeColorPickerSectionAtom, isColorDark } from "./colorPickerUtils";
import "./ColorPicker.scss";
import type { ColorPickerType } from "./colorPickerUtils";
-import type { ColorTuple, ColorPaletteCustom } from "../../colors";
-import type { ExcalidrawElement } from "../../element/types";
+
import type { AppState } from "../../types";
const isValidColor = (color: string) => {
@@ -187,6 +194,7 @@ const ColorPickerTrigger = ({
type="button"
className={clsx("color-picker__button active-color properties-trigger", {
"is-transparent": color === "transparent" || !color,
+ "has-outline": !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
})}
aria-label={label}
style={color ? { "--swatch-color": color } : undefined}
diff --git a/packages/excalidraw/components/ColorPicker/CustomColorList.tsx b/packages/excalidraw/components/ColorPicker/CustomColorList.tsx
index 2c735102a..45d5db84c 100644
--- a/packages/excalidraw/components/ColorPicker/CustomColorList.tsx
+++ b/packages/excalidraw/components/ColorPicker/CustomColorList.tsx
@@ -40,7 +40,7 @@ export const CustomColorList = ({
tabIndex={-1}
type="button"
className={clsx(
- "color-picker__button color-picker__button--large",
+ "color-picker__button color-picker__button--large has-outline",
{
active: color === c,
"is-transparent": c === "transparent" || !c,
@@ -56,7 +56,7 @@ export const CustomColorList = ({
key={i}
>
-
+
);
})}
diff --git a/packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx b/packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx
index 6e4d5e39c..898a28970 100644
--- a/packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx
+++ b/packages/excalidraw/components/ColorPicker/HotkeyLabel.tsx
@@ -1,24 +1,22 @@
import React from "react";
-import { getContrastYIQ } from "./colorPickerUtils";
+import { isColorDark } from "./colorPickerUtils";
interface HotkeyLabelProps {
color: string;
keyLabel: string | number;
- isCustomColor?: boolean;
isShade?: boolean;
}
const HotkeyLabel = ({
color,
keyLabel,
- isCustomColor = false,
isShade = false,
}: HotkeyLabelProps) => {
return (
{isShade && "⇧"}
diff --git a/packages/excalidraw/components/ColorPicker/Picker.tsx b/packages/excalidraw/components/ColorPicker/Picker.tsx
index 9f311e9c1..3c54c6769 100644
--- a/packages/excalidraw/components/ColorPicker/Picker.tsx
+++ b/packages/excalidraw/components/ColorPicker/Picker.tsx
@@ -1,13 +1,19 @@
import React, { useEffect, useState } from "react";
+import { EVENT } from "@excalidraw/common";
+
import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
-} from "../../colors";
-import { EVENT } from "../../constants";
+ KEYS,
+} from "@excalidraw/common";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import type { ColorPaletteCustom } from "@excalidraw/common";
+
import { useAtom } from "../../editor-jotai";
import { t } from "../../i18n";
-import { KEYS } from "../../keys";
import { CustomColorList } from "./CustomColorList";
import PickerColorList from "./PickerColorList";
@@ -22,8 +28,6 @@ import {
import { colorPickerKeyNavHandler } from "./keyboardNavHandlers";
import type { ColorPickerType } from "./colorPickerUtils";
-import type { ColorPaletteCustom } from "../../colors";
-import type { ExcalidrawElement } from "../../element/types";
interface PickerProps {
color: string;
diff --git a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
index b355228f1..38e5cf8c5 100644
--- a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
+++ b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
@@ -1,6 +1,8 @@
import clsx from "clsx";
import { useEffect, useRef } from "react";
+import type { ColorPaletteCustom } from "@excalidraw/common";
+
import { useAtom } from "../../editor-jotai";
import { t } from "../../i18n";
@@ -11,7 +13,6 @@ import {
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
-import type { ColorPaletteCustom } from "../../colors";
import type { TranslationKeys } from "../../i18n";
interface PickerColorListProps {
@@ -64,7 +65,7 @@ const PickerColorList = ({
tabIndex={-1}
type="button"
className={clsx(
- "color-picker__button color-picker__button--large",
+ "color-picker__button color-picker__button--large has-outline",
{
active: colorObj?.colorName === key,
"is-transparent": color === "transparent" || !color,
diff --git a/packages/excalidraw/components/ColorPicker/ShadeList.tsx b/packages/excalidraw/components/ColorPicker/ShadeList.tsx
index 35d89ea80..1c8e4c4eb 100644
--- a/packages/excalidraw/components/ColorPicker/ShadeList.tsx
+++ b/packages/excalidraw/components/ColorPicker/ShadeList.tsx
@@ -1,6 +1,8 @@
import clsx from "clsx";
import { useEffect, useRef } from "react";
+import type { ColorPaletteCustom } from "@excalidraw/common";
+
import { useAtom } from "../../editor-jotai";
import { t } from "../../i18n";
@@ -10,8 +12,6 @@ import {
getColorNameAndShadeFromColor,
} from "./colorPickerUtils";
-import type { ColorPaletteCustom } from "../../colors";
-
interface ShadeListProps {
hex: string;
onChange: (color: string) => void;
@@ -55,7 +55,7 @@ export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
key={i}
type="button"
className={clsx(
- "color-picker__button color-picker__button--large",
+ "color-picker__button color-picker__button--large has-outline",
{ active: i === shade },
)}
aria-label="Shade"
diff --git a/packages/excalidraw/components/ColorPicker/TopPicks.tsx b/packages/excalidraw/components/ColorPicker/TopPicks.tsx
index 9bd8fdb9a..8531172fb 100644
--- a/packages/excalidraw/components/ColorPicker/TopPicks.tsx
+++ b/packages/excalidraw/components/ColorPicker/TopPicks.tsx
@@ -1,10 +1,13 @@
import clsx from "clsx";
import {
+ COLOR_OUTLINE_CONTRAST_THRESHOLD,
DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
DEFAULT_ELEMENT_STROKE_PICKS,
-} from "../../colors";
+} from "@excalidraw/common";
+
+import { isColorDark } from "./colorPickerUtils";
import type { ColorPickerType } from "./colorPickerUtils";
@@ -51,6 +54,10 @@ export const TopPicks = ({
className={clsx("color-picker__button", {
active: color === activeColor,
"is-transparent": color === "transparent" || !color,
+ "has-outline": !isColorDark(
+ color,
+ COLOR_OUTLINE_CONTRAST_THRESHOLD,
+ ),
})}
style={{ "--swatch-color": color }}
key={color}
diff --git a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts b/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
index bbb4e587d..f572bd49f 100644
--- a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
+++ b/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
@@ -1,8 +1,10 @@
-import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "../../colors";
-import { atom } from "../../editor-jotai";
+import { MAX_CUSTOM_COLORS_USED_IN_CANVAS } from "@excalidraw/common";
-import type { ColorPickerColor, ColorPaletteCustom } from "../../colors";
-import type { ExcalidrawElement } from "../../element/types";
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import type { ColorPickerColor, ColorPaletteCustom } from "@excalidraw/common";
+
+import { atom } from "../../editor-jotai";
export const getColorNameAndShadeFromColor = ({
palette,
@@ -91,19 +93,42 @@ export type ActiveColorPickerSectionAtomType =
export const activeColorPickerSectionAtom =
atom
(null);
-const calculateContrast = (r: number, g: number, b: number) => {
+const calculateContrast = (r: number, g: number, b: number): number => {
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
- return yiq >= 160 ? "black" : "white";
+ return yiq;
};
-// inspiration from https://stackoverflow.com/a/11868398
-export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
- if (isCustomColor) {
- const style = new Option().style;
- style.color = bgHex;
+// YIQ algo, inspiration from https://stackoverflow.com/a/11868398
+export const isColorDark = (color: string, threshold = 160): boolean => {
+ // no color ("") -> assume it default to black
+ if (!color) {
+ return true;
+ }
- if (style.color) {
- const rgb = style.color
+ if (color === "transparent") {
+ return false;
+ }
+
+ // a string color (white etc) or any other format -> convert to rgb by way
+ // of creating a DOM node and retrieving the computeStyle
+ if (!color.startsWith("#")) {
+ const node = document.createElement("div");
+ node.style.color = color;
+
+ if (node.style.color) {
+ // making invisible so document doesn't reflow (hopefully).
+ // display=none works too, but supposedly not in all browsers
+ node.style.position = "absolute";
+ node.style.visibility = "hidden";
+ node.style.width = "0";
+ node.style.height = "0";
+
+ // needs to be in DOM else browser won't compute the style
+ document.body.appendChild(node);
+ const computedColor = getComputedStyle(node).color;
+ document.body.removeChild(node);
+ // computed style is in rgb() format
+ const rgb = computedColor
.replace(/^(rgb|rgba)\(/, "")
.replace(/\)$/, "")
.replace(/\s/g, "")
@@ -112,20 +137,17 @@ export const getContrastYIQ = (bgHex: string, isCustomColor: boolean) => {
const g = parseInt(rgb[1]);
const b = parseInt(rgb[2]);
- return calculateContrast(r, g, b);
+ return calculateContrast(r, g, b) < threshold;
}
+ // invalid color -> assume it default to black
+ return true;
}
- // TODO: ? is this wanted?
- if (bgHex === "transparent") {
- return "black";
- }
+ const r = parseInt(color.slice(1, 3), 16);
+ const g = parseInt(color.slice(3, 5), 16);
+ const b = parseInt(color.slice(5, 7), 16);
- const r = parseInt(bgHex.substring(1, 3), 16);
- const g = parseInt(bgHex.substring(3, 5), 16);
- const b = parseInt(bgHex.substring(5, 7), 16);
-
- return calculateContrast(r, g, b);
+ return calculateContrast(r, g, b) < threshold;
};
export type ColorPickerType =
diff --git a/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts b/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
index c4e321700..3e27229bc 100644
--- a/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
+++ b/packages/excalidraw/components/ColorPicker/keyboardNavHandlers.ts
@@ -1,5 +1,12 @@
-import { COLORS_PER_ROW, COLOR_PALETTE } from "../../colors";
-import { KEYS } from "../../keys";
+import { COLORS_PER_ROW, COLOR_PALETTE, KEYS } from "@excalidraw/common";
+
+import type {
+ ColorPickerColor,
+ ColorPalette,
+ ColorPaletteCustom,
+} from "@excalidraw/common";
+
+import type { ValueOf } from "@excalidraw/common/utility-types";
import {
colorPickerHotkeyBindings,
@@ -7,12 +14,6 @@ import {
} from "./colorPickerUtils";
import type { ActiveColorPickerSectionAtomType } from "./colorPickerUtils";
-import type {
- ColorPickerColor,
- ColorPalette,
- ColorPaletteCustom,
-} from "../../colors";
-import type { ValueOf } from "../../utility-types";
const arrowHandler = (
eventKey: string,
diff --git a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
index 7febb61c9..8b45e3377 100644
--- a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
+++ b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
@@ -2,6 +2,17 @@ import clsx from "clsx";
import fuzzy from "fuzzy";
import { useEffect, useRef, useState } from "react";
+import {
+ DEFAULT_SIDEBAR,
+ EVENT,
+ KEYS,
+ capitalizeString,
+ getShortcutKey,
+ isWritableElement,
+} from "@excalidraw/common";
+
+import type { MarkRequired } from "@excalidraw/common/utility-types";
+
import {
actionClearCanvas,
actionLink,
@@ -13,12 +24,10 @@ import {
} from "../../actions/actionElementLink";
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { trackEvent } from "../../analytics";
-import { DEFAULT_SIDEBAR, EVENT } from "../../constants";
import { useUIAppState } from "../../context/ui-appState";
import { deburr } from "../../deburr";
import { atom, useAtom, editorJotaiStore } from "../../editor-jotai";
import { t } from "../../i18n";
-import { KEYS } from "../../keys";
import {
useApp,
useAppProps,
@@ -42,13 +51,7 @@ import {
LibraryIcon,
} from "../icons";
-import {
- capitalizeString,
- getShortcutKey,
- isWritableElement,
-} from "../../utils";
-
-import { SHAPES } from "../../shapes";
+import { SHAPES } from "../shapes";
import { canChangeBackgroundColor, canChangeStrokeColor } from "../Actions";
import { useStableCallback } from "../../hooks/useStableCallback";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
@@ -60,7 +63,6 @@ import "./CommandPalette.scss";
import type { CommandPaletteItem } from "./types";
import type { AppProps, AppState, UIAppState } from "../../types";
-import type { MarkRequired } from "../../utility-types";
import type { ShortcutName } from "../../actions/shortcuts";
import type { TranslationKeys } from "../../i18n";
import type { Action } from "../../actions/types";
@@ -313,6 +315,7 @@ function CommandPaletteInner({
const toolCommands: CommandPaletteItem[] = [
actionManager.actions.toggleHandTool,
actionManager.actions.setFrameAsActiveTool,
+ actionManager.actions.toggleLassoTool,
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools));
const editorCommands: CommandPaletteItem[] = [
diff --git a/packages/excalidraw/components/DarkModeToggle.tsx b/packages/excalidraw/components/DarkModeToggle.tsx
index f04712944..474181ba4 100644
--- a/packages/excalidraw/components/DarkModeToggle.tsx
+++ b/packages/excalidraw/components/DarkModeToggle.tsx
@@ -1,12 +1,13 @@
-import { THEME } from "../constants";
+import { THEME } from "@excalidraw/common";
+
+import type { Theme } from "@excalidraw/element/types";
+
import { t } from "../i18n";
import { ToolButton } from "./ToolButton";
import "./ToolIcon.scss";
-import type { Theme } from "../element/types";
-
// We chose to use only explicit toggle and not a third option for system value,
// but this could be added in the future.
export const DarkModeToggle = (props: {
diff --git a/packages/excalidraw/components/DefaultSidebar.test.tsx b/packages/excalidraw/components/DefaultSidebar.test.tsx
index f8bc0dbbb..1b3f1cac9 100644
--- a/packages/excalidraw/components/DefaultSidebar.test.tsx
+++ b/packages/excalidraw/components/DefaultSidebar.test.tsx
@@ -1,6 +1,7 @@
import React from "react";
-import { DEFAULT_SIDEBAR } from "../constants";
+import { DEFAULT_SIDEBAR } from "@excalidraw/common";
+
import { DefaultSidebar } from "../index";
import {
fireEvent,
diff --git a/packages/excalidraw/components/DefaultSidebar.tsx b/packages/excalidraw/components/DefaultSidebar.tsx
index cd9683c60..4f1aa91e8 100644
--- a/packages/excalidraw/components/DefaultSidebar.tsx
+++ b/packages/excalidraw/components/DefaultSidebar.tsx
@@ -4,10 +4,13 @@ import {
CANVAS_SEARCH_TAB,
DEFAULT_SIDEBAR,
LIBRARY_SIDEBAR_TAB,
-} from "../constants";
+ composeEventHandlers,
+} from "@excalidraw/common";
+
+import type { MarkOptional, Merge } from "@excalidraw/common/utility-types";
+
import { useTunnels } from "../context/tunnels";
import { useUIAppState } from "../context/ui-appState";
-import { composeEventHandlers } from "../utils";
import "../components/dropdownMenu/DropdownMenu.scss";
@@ -18,7 +21,6 @@ import { Sidebar } from "./Sidebar/Sidebar";
import { withInternalFallback } from "./hoc/withInternalFallback";
import { LibraryIcon, searchIcon } from "./icons";
-import type { MarkOptional, Merge } from "../utility-types";
import type { SidebarProps, SidebarTriggerProps } from "./Sidebar/common";
const DefaultSidebarTrigger = withInternalFallback(
diff --git a/packages/excalidraw/components/Dialog.tsx b/packages/excalidraw/components/Dialog.tsx
index cf6b1254a..00ae2be0c 100644
--- a/packages/excalidraw/components/Dialog.tsx
+++ b/packages/excalidraw/components/Dialog.tsx
@@ -1,11 +1,11 @@
import clsx from "clsx";
import React, { useEffect, useState } from "react";
+import { KEYS, queryFocusableElements } from "@excalidraw/common";
+
import { useSetAtom } from "../editor-jotai";
import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { t } from "../i18n";
-import { KEYS } from "../keys";
-import { queryFocusableElements } from "../utils";
import {
useExcalidrawContainer,
diff --git a/packages/excalidraw/element/ElementCanvasButtons.scss b/packages/excalidraw/components/ElementCanvasButtons.scss
similarity index 100%
rename from packages/excalidraw/element/ElementCanvasButtons.scss
rename to packages/excalidraw/components/ElementCanvasButtons.scss
diff --git a/packages/excalidraw/element/ElementCanvasButtons.tsx b/packages/excalidraw/components/ElementCanvasButtons.tsx
similarity index 85%
rename from packages/excalidraw/element/ElementCanvasButtons.tsx
rename to packages/excalidraw/components/ElementCanvasButtons.tsx
index e67e8d5bc..424c4f3b4 100644
--- a/packages/excalidraw/element/ElementCanvasButtons.tsx
+++ b/packages/excalidraw/components/ElementCanvasButtons.tsx
@@ -1,14 +1,17 @@
+import { sceneCoordsToViewportCoords } from "@excalidraw/common";
+import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
+
+import type {
+ ElementsMap,
+ NonDeletedExcalidrawElement,
+} from "@excalidraw/element/types";
+
import { useExcalidrawAppState } from "../components/App";
-import { sceneCoordsToViewportCoords } from "../utils";
import "./ElementCanvasButtons.scss";
-import { getElementAbsoluteCoords } from ".";
-
import type { AppState } from "../types";
-import type { ElementsMap, NonDeletedExcalidrawElement } from "./types";
-
const CONTAINER_PADDING = 5;
const getContainerCoords = (
diff --git a/packages/excalidraw/components/ElementLinkDialog.tsx b/packages/excalidraw/components/ElementLinkDialog.tsx
index 7cf329633..5a0b9107b 100644
--- a/packages/excalidraw/components/ElementLinkDialog.tsx
+++ b/packages/excalidraw/components/ElementLinkDialog.tsx
@@ -1,13 +1,16 @@
import { useCallback, useEffect, useState } from "react";
-import { normalizeLink } from "../data/url";
+import { normalizeLink, KEYS } from "@excalidraw/common";
+
import {
defaultGetElementLinkFromSelection,
getLinkIdAndTypeFromSelection,
-} from "../element/elementLink";
-import { mutateElement } from "../element/mutateElement";
+} from "@excalidraw/element/elementLink";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+
+import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
+
import { t } from "../i18n";
-import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import DialogActionButton from "./DialogActionButton";
@@ -17,7 +20,6 @@ import { TrashIcon } from "./icons";
import "./ElementLinkDialog.scss";
-import type { ElementsMap, ExcalidrawElement } from "../element/types";
import type { AppProps, AppState, UIAppState } from "../types";
const ElementLinkDialog = ({
diff --git a/packages/excalidraw/components/EyeDropper.tsx b/packages/excalidraw/components/EyeDropper.tsx
index 8c4a73e32..f7f98123d 100644
--- a/packages/excalidraw/components/EyeDropper.tsx
+++ b/packages/excalidraw/components/EyeDropper.tsx
@@ -1,22 +1,21 @@
import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
-import { rgbToHex } from "../colors";
-import { EVENT } from "../constants";
+import { EVENT, KEYS, rgbToHex } from "@excalidraw/common";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { useUIAppState } from "../context/ui-appState";
import { atom } from "../editor-jotai";
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
import { useOutsideClick } from "../hooks/useOutsideClick";
import { useStable } from "../hooks/useStable";
-import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
import "./EyeDropper.scss";
-import type { ExcalidrawElement } from "../element/types";
-
import type { ColorPickerType } from "./ColorPicker/colorPickerUtils";
export type EyeDropperProperties = {
diff --git a/packages/excalidraw/components/FilledButton.tsx b/packages/excalidraw/components/FilledButton.tsx
index 4e5b3e3ba..53b30a046 100644
--- a/packages/excalidraw/components/FilledButton.tsx
+++ b/packages/excalidraw/components/FilledButton.tsx
@@ -1,8 +1,9 @@
import clsx from "clsx";
import React, { forwardRef, useState } from "react";
+import { isPromiseLike } from "@excalidraw/common";
+
import { AbortError } from "../errors";
-import { isPromiseLike } from "../utils";
import Spinner from "./Spinner";
import { tablerCheckIcon } from "./icons";
diff --git a/packages/excalidraw/components/FontPicker/FontPicker.tsx b/packages/excalidraw/components/FontPicker/FontPicker.tsx
index 4018ad243..546e1fa34 100644
--- a/packages/excalidraw/components/FontPicker/FontPicker.tsx
+++ b/packages/excalidraw/components/FontPicker/FontPicker.tsx
@@ -1,7 +1,10 @@
import * as Popover from "@radix-ui/react-popover";
import React, { useCallback, useMemo } from "react";
-import { FONT_FAMILY } from "../../constants";
+import { FONT_FAMILY } from "@excalidraw/common";
+
+import type { FontFamilyValues } from "@excalidraw/element/types";
+
import { t } from "../../i18n";
import { ButtonIconSelect } from "../ButtonIconSelect";
import { ButtonSeparator } from "../ButtonSeparator";
@@ -16,8 +19,6 @@ import { FontPickerTrigger } from "./FontPickerTrigger";
import "./FontPicker.scss";
-import type { FontFamilyValues } from "../../element/types";
-
export const DEFAULT_FONTS = [
{
value: FONT_FAMILY.Excalifont,
diff --git a/packages/excalidraw/components/FontPicker/FontPickerList.tsx b/packages/excalidraw/components/FontPicker/FontPickerList.tsx
index d93b08695..2ec9e7d6d 100644
--- a/packages/excalidraw/components/FontPicker/FontPickerList.tsx
+++ b/packages/excalidraw/components/FontPicker/FontPickerList.tsx
@@ -7,10 +7,19 @@ import React, {
type KeyboardEventHandler,
} from "react";
-import { type FontFamilyValues } from "../../element/types";
+import { type FontFamilyValues } from "@excalidraw/element/types";
+
+import {
+ arrayToList,
+ debounce,
+ FONT_FAMILY,
+ getFontFamilyString,
+} from "@excalidraw/common";
+
+import type { ValueOf } from "@excalidraw/common/utility-types";
+
import { Fonts } from "../../fonts";
import { t } from "../../i18n";
-import { arrayToList, debounce, getFontFamilyString } from "../../utils";
import { useApp, useAppProps, useExcalidrawContainer } from "../App";
import { PropertiesPopover } from "../PropertiesPopover";
import { QuickSearch } from "../QuickSearch";
@@ -20,11 +29,15 @@ import DropdownMenuItem, {
DropDownMenuItemBadgeType,
DropDownMenuItemBadge,
} from "../dropdownMenu/DropdownMenuItem";
-import { FontFamilyNormalIcon } from "../icons";
+import {
+ FontFamilyCodeIcon,
+ FontFamilyHeadingIcon,
+ FontFamilyNormalIcon,
+ FreedrawIcon,
+} from "../icons";
import { fontPickerKeyHandler } from "./keyboardNavHandlers";
-import type { ValueOf } from "../../utility-types";
import type { JSX } from "react";
export interface FontDescriptor {
@@ -48,6 +61,24 @@ interface FontPickerListProps {
onClose: () => void;
}
+const getFontFamilyIcon = (fontFamily: FontFamilyValues): JSX.Element => {
+ switch (fontFamily) {
+ case FONT_FAMILY.Excalifont:
+ case FONT_FAMILY.Virgil:
+ return FreedrawIcon;
+ case FONT_FAMILY.Nunito:
+ case FONT_FAMILY.Helvetica:
+ return FontFamilyNormalIcon;
+ case FONT_FAMILY["Lilita One"]:
+ return FontFamilyHeadingIcon;
+ case FONT_FAMILY["Comic Shanns"]:
+ case FONT_FAMILY.Cascadia:
+ return FontFamilyCodeIcon;
+ default:
+ return FontFamilyNormalIcon;
+ }
+};
+
export const FontPickerList = React.memo(
({
selectedFontFamily,
@@ -73,7 +104,7 @@ export const FontPickerList = React.memo(
.map(([familyId, { metadata, fontFaces }]) => {
const fontDescriptor = {
value: familyId,
- icon: metadata.icon ?? FontFamilyNormalIcon,
+ icon: getFontFamilyIcon(familyId),
text: fontFaces[0]?.fontFace?.family ?? "Unknown",
};
diff --git a/packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx b/packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx
index d83dda0fd..6f3cc638a 100644
--- a/packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx
+++ b/packages/excalidraw/components/FontPicker/FontPickerTrigger.tsx
@@ -1,14 +1,14 @@
import * as Popover from "@radix-ui/react-popover";
import { useMemo } from "react";
+import type { FontFamilyValues } from "@excalidraw/element/types";
+
import { t } from "../../i18n";
import { ButtonIcon } from "../ButtonIcon";
import { TextIcon } from "../icons";
import { isDefaultFont } from "./FontPicker";
-import type { FontFamilyValues } from "../../element/types";
-
interface FontPickerTriggerProps {
selectedFontFamily: FontFamilyValues | null;
}
diff --git a/packages/excalidraw/components/FontPicker/keyboardNavHandlers.ts b/packages/excalidraw/components/FontPicker/keyboardNavHandlers.ts
index b0ecdf371..16ffa7c45 100644
--- a/packages/excalidraw/components/FontPicker/keyboardNavHandlers.ts
+++ b/packages/excalidraw/components/FontPicker/keyboardNavHandlers.ts
@@ -1,9 +1,9 @@
-import { KEYS } from "../../keys";
+import { KEYS } from "@excalidraw/common";
+
+import type { Node } from "@excalidraw/common";
import { type FontDescriptor } from "./FontPickerList";
-import type { Node } from "../../utils";
-
interface FontPickerKeyNavHandlerProps {
event: React.KeyboardEvent;
inputRef: React.RefObject;
diff --git a/packages/excalidraw/components/HandButton.tsx b/packages/excalidraw/components/HandButton.tsx
index 4bffb1000..5ebfdf9d3 100644
--- a/packages/excalidraw/components/HandButton.tsx
+++ b/packages/excalidraw/components/HandButton.tsx
@@ -1,6 +1,6 @@
import clsx from "clsx";
-import { KEYS } from "../keys";
+import { KEYS } from "@excalidraw/common";
import { ToolButton } from "./ToolButton";
import { handIcon } from "./icons";
diff --git a/packages/excalidraw/components/HelpDialog.tsx b/packages/excalidraw/components/HelpDialog.tsx
index 19ecaa57e..60fc40372 100644
--- a/packages/excalidraw/components/HelpDialog.tsx
+++ b/packages/excalidraw/components/HelpDialog.tsx
@@ -1,11 +1,12 @@
import React from "react";
+import { isDarwin, isFirefox, isWindows } from "@excalidraw/common";
+
+import { KEYS, getShortcutKey } from "@excalidraw/common";
+
import { getShortcutFromShortcutName } from "../actions/shortcuts";
import { probablySupportsClipboardBlob } from "../clipboard";
-import { isDarwin, isFirefox, isWindows } from "../constants";
import { t } from "../i18n";
-import { KEYS } from "../keys";
-import { getShortcutKey } from "../utils";
import { Dialog } from "./Dialog";
import { ExternalLinkIcon, GithubIcon, youtubeIcon } from "./icons";
diff --git a/packages/excalidraw/components/HintViewer.tsx b/packages/excalidraw/components/HintViewer.tsx
index c285e361b..5072e4471 100644
--- a/packages/excalidraw/components/HintViewer.tsx
+++ b/packages/excalidraw/components/HintViewer.tsx
@@ -1,18 +1,20 @@
-import { isEraserActive } from "../appState";
+import { CANVAS_SEARCH_TAB, DEFAULT_SIDEBAR } from "@excalidraw/common";
+
import {
isFlowchartNodeElement,
isImageElement,
isLinearElement,
isTextBindableContainer,
isTextElement,
-} from "../element/typeChecks";
+} from "@excalidraw/element/typeChecks";
+
+import { getShortcutKey } from "@excalidraw/common";
+
+import { isNodeInFlowchart } from "@excalidraw/element/flowchart";
+
import { t } from "../i18n";
-
-import { getShortcutKey } from "../utils";
-
-import { isNodeInFlowchart } from "../element/flowchart";
+import { isEraserActive } from "../appState";
import { isGridModeEnabled } from "../snapping";
-import { CANVAS_SEARCH_TAB, DEFAULT_SIDEBAR } from "../constants";
import "./HintViewer.scss";
@@ -118,7 +120,7 @@ const getHints = ({
!appState.editingTextElement &&
!appState.editingLinearElement
) {
- return t("hints.deepBoxSelect");
+ return [t("hints.deepBoxSelect")];
}
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
@@ -126,7 +128,7 @@ const getHints = ({
}
if (!selectedElements.length && !isMobile) {
- return t("hints.canvasPanning");
+ return [t("hints.canvasPanning")];
}
if (selectedElements.length === 1) {
diff --git a/packages/excalidraw/components/IconPicker.tsx b/packages/excalidraw/components/IconPicker.tsx
index b91d37c23..5630ae8d7 100644
--- a/packages/excalidraw/components/IconPicker.tsx
+++ b/packages/excalidraw/components/IconPicker.tsx
@@ -2,9 +2,10 @@ import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx";
import React, { useEffect } from "react";
+import { isArrowKey, KEYS } from "@excalidraw/common";
+
import { atom, useAtom } from "../editor-jotai";
import { getLanguage, t } from "../i18n";
-import { isArrowKey, KEYS } from "../keys";
import Collapsible from "./Stats/Collapsible";
import { useDevice } from "./App";
diff --git a/packages/excalidraw/components/ImageExportDialog.tsx b/packages/excalidraw/components/ImageExportDialog.tsx
index 0d19f5b5d..e8e0b70f4 100644
--- a/packages/excalidraw/components/ImageExportDialog.tsx
+++ b/packages/excalidraw/components/ImageExportDialog.tsx
@@ -1,6 +1,16 @@
import { exportToCanvas } from "@excalidraw/utils/export";
import React, { useEffect, useRef, useState } from "react";
+import {
+ DEFAULT_EXPORT_PADDING,
+ EXPORT_IMAGE_TYPES,
+ isFirefox,
+ EXPORT_SCALES,
+ cloneJSON,
+} from "@excalidraw/common";
+
+import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
+
import {
actionExportWithDarkMode,
actionChangeExportBackground,
@@ -9,12 +19,6 @@ import {
actionChangeProjectName,
} from "../actions/actionExport";
import { probablySupportsClipboardBlob } from "../clipboard";
-import {
- DEFAULT_EXPORT_PADDING,
- EXPORT_IMAGE_TYPES,
- isFirefox,
- EXPORT_SCALES,
-} from "../constants";
import { prepareElementsForExport } from "../data";
import { canvasToBlob } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
@@ -22,7 +26,6 @@ import { useCopyStatus } from "../hooks/useCopiedIndicator";
import { t } from "../i18n";
import { isSomeElementSelected } from "../scene";
-import { cloneJSON } from "../utils";
import { copyIcon, downloadIcon, helpIcon } from "./icons";
import { Dialog } from "./Dialog";
@@ -34,7 +37,7 @@ import { FilledButton } from "./FilledButton";
import "./ImageExportDialog.scss";
import type { ActionManager } from "../actions/manager";
-import type { NonDeletedExcalidrawElement } from "../element/types";
+
import type { AppClassProperties, BinaryFiles, UIAppState } from "../types";
const supportsContextFilters =
diff --git a/packages/excalidraw/components/InitializeApp.tsx b/packages/excalidraw/components/InitializeApp.tsx
index efc83a55b..1e5dda6dc 100644
--- a/packages/excalidraw/components/InitializeApp.tsx
+++ b/packages/excalidraw/components/InitializeApp.tsx
@@ -1,10 +1,11 @@
import React, { useEffect, useState } from "react";
+import type { Theme } from "@excalidraw/element/types";
+
import { defaultLang, languages, setLanguage } from "../i18n";
import { LoadingMessage } from "./LoadingMessage";
-import type { Theme } from "../element/types";
import type { Language } from "../i18n";
interface Props {
diff --git a/packages/excalidraw/components/JSONExportDialog.tsx b/packages/excalidraw/components/JSONExportDialog.tsx
index ae203d3e4..4dfedc692 100644
--- a/packages/excalidraw/components/JSONExportDialog.tsx
+++ b/packages/excalidraw/components/JSONExportDialog.tsx
@@ -1,11 +1,14 @@
import React from "react";
+import { getFrame } from "@excalidraw/common";
+
+import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
+
import { actionSaveFileToDisk } from "../actions/actionExport";
import { trackEvent } from "../analytics";
import { nativeFileSystemSupported } from "../data/filesystem";
import { t } from "../i18n";
-import { getFrame } from "../utils";
import { Card } from "./Card";
import { Dialog } from "./Dialog";
@@ -15,7 +18,7 @@ import { exportToFileIcon, LinkIcon } from "./icons";
import "./ExportDialog.scss";
import type { ActionManager } from "../actions/manager";
-import type { NonDeletedExcalidrawElement } from "../element/types";
+
import type { ExportOpts, BinaryFiles, UIAppState } from "../types";
export type ExportCB = (
diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx
index 83ac7ae9c..b5491dedd 100644
--- a/packages/excalidraw/components/LayerUI.tsx
+++ b/packages/excalidraw/components/LayerUI.tsx
@@ -1,20 +1,32 @@
import clsx from "clsx";
import React from "react";
-import { mutateElement } from "../element/mutateElement";
-import { ShapeCache } from "../scene/ShapeCache";
+import {
+ CLASSES,
+ DEFAULT_SIDEBAR,
+ TOOL_TYPE,
+ capitalizeString,
+ isShallowEqual,
+} from "@excalidraw/common";
+
+import { mutateElement } from "@excalidraw/element/mutateElement";
+
+import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions";
+
+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";
-import { CLASSES, DEFAULT_SIDEBAR, TOOL_TYPE } from "../constants";
import { TunnelsContext, useInitializeTunnels } from "../context/tunnels";
import { UIAppStateContext } from "../context/ui-appState";
import { useAtom, useAtomValue } from "../editor-jotai";
-import { showSelectedShapeActions } from "../element";
+
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
-import { capitalizeString, isShallowEqual } from "../utils";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { LoadingMessage } from "./LoadingMessage";
@@ -51,7 +63,7 @@ import "./LayerUI.scss";
import "./Toolbar.scss";
import type { ActionManager } from "../actions/manager";
-import type { NonDeletedExcalidrawElement } from "../element/types";
+
import type { Language } from "../i18n";
import type {
AppProps,
diff --git a/packages/excalidraw/components/LibraryMenu.tsx b/packages/excalidraw/components/LibraryMenu.tsx
index 03c476e47..62bd235c2 100644
--- a/packages/excalidraw/components/LibraryMenu.tsx
+++ b/packages/excalidraw/components/LibraryMenu.tsx
@@ -7,8 +7,18 @@ import React, {
useRef,
} from "react";
+import {
+ LIBRARY_DISABLED_TYPES,
+ randomId,
+ isShallowEqual,
+} from "@excalidraw/common";
+
+import type {
+ ExcalidrawElement,
+ NonDeletedExcalidrawElement,
+} from "@excalidraw/element/types";
+
import { trackEvent } from "../analytics";
-import { LIBRARY_DISABLED_TYPES } from "../constants";
import { useUIAppState } from "../context/ui-appState";
import {
distributeLibraryItemsOnSquareGrid,
@@ -16,9 +26,8 @@ import {
} from "../data/library";
import { atom, useAtom } from "../editor-jotai";
import { t } from "../i18n";
-import { randomId } from "../random";
+
import { getSelectedElements } from "../scene";
-import { isShallowEqual } from "../utils";
import {
useApp,
@@ -32,10 +41,6 @@ import Spinner from "./Spinner";
import "./LibraryMenu.scss";
-import type {
- ExcalidrawElement,
- NonDeletedExcalidrawElement,
-} from "../element/types";
import type {
LibraryItems,
LibraryItem,
diff --git a/packages/excalidraw/components/LibraryMenuBrowseButton.tsx b/packages/excalidraw/components/LibraryMenuBrowseButton.tsx
index 2b2623ed1..86bd4e2ad 100644
--- a/packages/excalidraw/components/LibraryMenuBrowseButton.tsx
+++ b/packages/excalidraw/components/LibraryMenuBrowseButton.tsx
@@ -1,4 +1,5 @@
-import { VERSIONS } from "../constants";
+import { VERSIONS } from "@excalidraw/common";
+
import { t } from "../i18n";
import type { ExcalidrawProps, UIAppState } from "../types";
diff --git a/packages/excalidraw/components/LibraryMenuHeaderContent.tsx b/packages/excalidraw/components/LibraryMenuHeaderContent.tsx
index cc6c942ae..5b003effa 100644
--- a/packages/excalidraw/components/LibraryMenuHeaderContent.tsx
+++ b/packages/excalidraw/components/LibraryMenuHeaderContent.tsx
@@ -1,6 +1,8 @@
import clsx from "clsx";
import { useCallback, useState } from "react";
+import { muteFSAbortError } from "@excalidraw/common";
+
import { useUIAppState } from "../context/ui-appState";
import { fileOpen } from "../data/filesystem";
import { saveLibraryAsJSON } from "../data/json";
@@ -8,7 +10,6 @@ import { libraryItemsAtom } from "../data/library";
import { useAtom } from "../editor-jotai";
import { useLibraryCache } from "../hooks/useLibraryItemSvg";
import { t } from "../i18n";
-import { muteFSAbortError } from "../utils";
import { useApp, useExcalidrawSetAppState } from "./App";
import ConfirmDialog from "./ConfirmDialog";
diff --git a/packages/excalidraw/components/LibraryMenuItems.tsx b/packages/excalidraw/components/LibraryMenuItems.tsx
index af5b9d3e6..f70315953 100644
--- a/packages/excalidraw/components/LibraryMenuItems.tsx
+++ b/packages/excalidraw/components/LibraryMenuItems.tsx
@@ -6,13 +6,14 @@ import React, {
useState,
} from "react";
-import { MIME_TYPES } from "../constants";
+import { MIME_TYPES, arrayToMap } from "@excalidraw/common";
+
+import { duplicateElements } from "@excalidraw/element/duplicate";
+
import { serializeLibraryAsJSON } from "../data/json";
-import { duplicateElements } from "../element/newElement";
import { useLibraryCache } from "../hooks/useLibraryItemSvg";
import { useScrollPosition } from "../hooks/useScrollPosition";
import { t } from "../i18n";
-import { arrayToMap } from "../utils";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
@@ -20,6 +21,7 @@ import {
LibraryMenuSection,
LibraryMenuSectionGrid,
} from "./LibraryMenuSection";
+
import Spinner from "./Spinner";
import Stack from "./Stack";
@@ -160,7 +162,11 @@ export default function LibraryMenuItems({
...item,
// duplicate each library item before inserting on canvas to confine
// ids and bindings to each library item. See #6465
- elements: duplicateElements(item.elements, { randomizeSeed: true }),
+ elements: duplicateElements({
+ type: "everything",
+ elements: item.elements,
+ randomizeSeed: true,
+ }).newElements,
};
});
},
diff --git a/packages/excalidraw/components/LibraryMenuSection.tsx b/packages/excalidraw/components/LibraryMenuSection.tsx
index 5f7de2b09..d98b413fb 100644
--- a/packages/excalidraw/components/LibraryMenuSection.tsx
+++ b/packages/excalidraw/components/LibraryMenuSection.tsx
@@ -1,10 +1,11 @@
import React, { memo, useEffect, useState } from "react";
+import type { ExcalidrawElement, NonDeleted } from "@excalidraw/element/types";
+
import { useTransition } from "../hooks/useTransition";
import { EmptyLibraryUnit, LibraryUnit } from "./LibraryUnit";
-import type { ExcalidrawElement, NonDeleted } from "../element/types";
import type { SvgCache } from "../hooks/useLibraryItemSvg";
import type { LibraryItem } from "../types";
import type { ReactNode } from "react";
diff --git a/packages/excalidraw/components/LoadingMessage.tsx b/packages/excalidraw/components/LoadingMessage.tsx
index bdcc5a341..c971b37ee 100644
--- a/packages/excalidraw/components/LoadingMessage.tsx
+++ b/packages/excalidraw/components/LoadingMessage.tsx
@@ -1,13 +1,14 @@
import clsx from "clsx";
import { useState, useEffect } from "react";
-import { THEME } from "../constants";
+import { THEME } from "@excalidraw/common";
+
+import type { Theme } from "@excalidraw/element/types";
+
import { t } from "../i18n";
import Spinner from "./Spinner";
-import type { Theme } from "../element/types";
-
export const LoadingMessage: React.FC<{ delay?: number; theme?: Theme }> = ({
delay,
theme,
diff --git a/packages/excalidraw/components/MobileMenu.tsx b/packages/excalidraw/components/MobileMenu.tsx
index 08a20174c..6e75b5992 100644
--- a/packages/excalidraw/components/MobileMenu.tsx
+++ b/packages/excalidraw/components/MobileMenu.tsx
@@ -1,8 +1,11 @@
import React from "react";
+import { showSelectedShapeActions } from "@excalidraw/element/showSelectedShapeActions";
+
+import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
+
import { isHandToolActive } from "../appState";
import { useTunnels } from "../context/tunnels";
-import { showSelectedShapeActions } from "../element";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
@@ -18,7 +21,6 @@ import { Section } from "./Section";
import Stack from "./Stack";
import type { ActionManager } from "../actions/manager";
-import type { NonDeletedExcalidrawElement } from "../element/types";
import type {
AppClassProperties,
AppProps,
diff --git a/packages/excalidraw/components/Modal.tsx b/packages/excalidraw/components/Modal.tsx
index a5ade9efa..32f42986f 100644
--- a/packages/excalidraw/components/Modal.tsx
+++ b/packages/excalidraw/components/Modal.tsx
@@ -2,8 +2,9 @@ import clsx from "clsx";
import { useRef } from "react";
import { createPortal } from "react-dom";
+import { KEYS } from "@excalidraw/common";
+
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
-import { KEYS } from "../keys";
import "./Modal.scss";
diff --git a/packages/excalidraw/components/PasteChartDialog.tsx b/packages/excalidraw/components/PasteChartDialog.tsx
index 58bd5a385..2566017ac 100644
--- a/packages/excalidraw/components/PasteChartDialog.tsx
+++ b/packages/excalidraw/components/PasteChartDialog.tsx
@@ -1,6 +1,8 @@
import oc from "open-color";
import React, { useLayoutEffect, useRef, useState } from "react";
+import type { ChartType } from "@excalidraw/element/types";
+
import { trackEvent } from "../analytics";
import { renderSpreadsheet } from "../charts";
import { t } from "../i18n";
@@ -12,7 +14,6 @@ import { Dialog } from "./Dialog";
import "./PasteChartDialog.scss";
import type { ChartElements, Spreadsheet } from "../charts";
-import type { ChartType } from "../element/types";
import type { UIAppState } from "../types";
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
diff --git a/packages/excalidraw/components/Popover.tsx b/packages/excalidraw/components/Popover.tsx
index 2bd72b20b..4864b37d1 100644
--- a/packages/excalidraw/components/Popover.tsx
+++ b/packages/excalidraw/components/Popover.tsx
@@ -1,8 +1,7 @@
import React, { useLayoutEffect, useRef, useEffect } from "react";
import { unstable_batchedUpdates } from "react-dom";
-import { KEYS } from "../keys";
-import { queryFocusableElements } from "../utils";
+import { KEYS, queryFocusableElements } from "@excalidraw/common";
import "./Popover.scss";
diff --git a/packages/excalidraw/components/ProjectName.tsx b/packages/excalidraw/components/ProjectName.tsx
index 0d60ddd66..ad50c7fd5 100644
--- a/packages/excalidraw/components/ProjectName.tsx
+++ b/packages/excalidraw/components/ProjectName.tsx
@@ -1,7 +1,6 @@
import React, { useState } from "react";
-import { focusNearestParent } from "../utils";
-import { KEYS } from "../keys";
+import { focusNearestParent, KEYS } from "@excalidraw/common";
import { useExcalidrawContainer } from "./App";
diff --git a/packages/excalidraw/components/PropertiesPopover.tsx b/packages/excalidraw/components/PropertiesPopover.tsx
index a30f1374d..d8372ea27 100644
--- a/packages/excalidraw/components/PropertiesPopover.tsx
+++ b/packages/excalidraw/components/PropertiesPopover.tsx
@@ -2,7 +2,7 @@ import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx";
import React, { type ReactNode } from "react";
-import { isInteractive } from "../utils";
+import { isInteractive } from "@excalidraw/common";
import { useDevice } from "./App";
import { Island } from "./Island";
diff --git a/packages/excalidraw/components/PublishLibrary.tsx b/packages/excalidraw/components/PublishLibrary.tsx
index 8a7188b63..c1db91296 100644
--- a/packages/excalidraw/components/PublishLibrary.tsx
+++ b/packages/excalidraw/components/PublishLibrary.tsx
@@ -8,11 +8,12 @@ import {
EXPORT_SOURCE,
MIME_TYPES,
VERSIONS,
-} from "../constants";
+ chunk,
+} from "@excalidraw/common";
+
import { EditorLocalStorage } from "../data/EditorLocalStorage";
import { canvasToBlob, resizeImageFile } from "../data/blob";
import { t } from "../i18n";
-import { chunk } from "../utils";
import { Dialog } from "./Dialog";
import DialogActionButton from "./DialogActionButton";
diff --git a/packages/excalidraw/components/Range.scss b/packages/excalidraw/components/Range.scss
index 01cb91689..8dcc705fe 100644
--- a/packages/excalidraw/components/Range.scss
+++ b/packages/excalidraw/components/Range.scss
@@ -6,7 +6,7 @@
.range-wrapper {
position: relative;
padding-top: 10px;
- padding-bottom: 30px;
+ padding-bottom: 25px;
}
.range-input {
diff --git a/packages/excalidraw/components/SearchMenu.tsx b/packages/excalidraw/components/SearchMenu.tsx
index 70d3f4f27..3e0b31a69 100644
--- a/packages/excalidraw/components/SearchMenu.tsx
+++ b/packages/excalidraw/components/SearchMenu.tsx
@@ -3,17 +3,28 @@ import clsx from "clsx";
import debounce from "lodash.debounce";
import { Fragment, memo, useEffect, useRef, useState } from "react";
-import { CLASSES, EVENT } from "../constants";
-import { atom, useAtom } from "../editor-jotai";
-import { isTextElement, newTextElement } from "../element";
-import { isElementCompletelyInViewport } from "../element/sizeHelpers";
+import { CLASSES, EVENT } from "@excalidraw/common";
+
+import { isElementCompletelyInViewport } from "@excalidraw/element/sizeHelpers";
+
+import { measureText } from "@excalidraw/element/textMeasurements";
+
+import {
+ KEYS,
+ randomInteger,
+ addEventListener,
+ getFontString,
+} from "@excalidraw/common";
+
+import { newTextElement } from "@excalidraw/element/newElement";
+import { isTextElement } from "@excalidraw/element/typeChecks";
+
+import type { ExcalidrawTextElement } from "@excalidraw/element/types";
+
+import { atom, useAtom } from "../editor-jotai";
-import { measureText } from "../element/textMeasurements";
import { useStable } from "../hooks/useStable";
import { t } from "../i18n";
-import { KEYS } from "../keys";
-import { randomInteger } from "../random";
-import { addEventListener, getFontString } from "../utils";
import { useApp, useExcalidrawSetAppState } from "./App";
import { Button } from "./Button";
@@ -22,7 +33,6 @@ import { collapseDownIcon, upIcon, searchIcon } from "./icons";
import "./SearchMenu.scss";
-import type { ExcalidrawTextElement } from "../element/types";
import type { AppClassProperties } from "../types";
const searchQueryAtom = atom("");
diff --git a/packages/excalidraw/components/Sidebar/Sidebar.test.tsx b/packages/excalidraw/components/Sidebar/Sidebar.test.tsx
index 39a1cbc63..dd52fdb11 100644
--- a/packages/excalidraw/components/Sidebar/Sidebar.test.tsx
+++ b/packages/excalidraw/components/Sidebar/Sidebar.test.tsx
@@ -1,7 +1,8 @@
import React from "react";
import { vi } from "vitest";
-import { DEFAULT_SIDEBAR } from "../../constants";
+import { DEFAULT_SIDEBAR } from "@excalidraw/common";
+
import { Excalidraw, Sidebar } from "../../index";
import {
act,
diff --git a/packages/excalidraw/components/Sidebar/Sidebar.tsx b/packages/excalidraw/components/Sidebar/Sidebar.tsx
index 8a008fd9f..d08ba5f59 100644
--- a/packages/excalidraw/components/Sidebar/Sidebar.tsx
+++ b/packages/excalidraw/components/Sidebar/Sidebar.tsx
@@ -9,12 +9,11 @@ import React, {
useCallback,
} from "react";
-import { EVENT } from "../../constants";
+import { EVENT, isDevEnv, KEYS, updateObject } from "@excalidraw/common";
+
import { useUIAppState } from "../../context/ui-appState";
import { atom, useSetAtom } from "../../editor-jotai";
import { useOutsideClick } from "../../hooks/useOutsideClick";
-import { KEYS } from "../../keys";
-import { updateObject } from "../../utils";
import { useDevice, useExcalidrawSetAppState } from "../App";
import { Island } from "../Island";
@@ -52,7 +51,7 @@ export const SidebarInner = forwardRef(
}: SidebarProps & Omit, "onSelect">,
ref: React.ForwardedRef,
) => {
- if (import.meta.env.DEV && onDock && docked == null) {
+ if (isDevEnv() && onDock && docked == null) {
console.warn(
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`",
);
diff --git a/packages/excalidraw/components/Stats/Angle.tsx b/packages/excalidraw/components/Stats/Angle.tsx
index 10c76d519..67693551f 100644
--- a/packages/excalidraw/components/Stats/Angle.tsx
+++ b/packages/excalidraw/components/Stats/Angle.tsx
@@ -1,17 +1,20 @@
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";
+
import type { Degrees } from "@excalidraw/math";
-import { mutateElement } from "../../element/mutateElement";
-import { getBoundTextElement } from "../../element/textElement";
-import { isArrowElement, isElbowArrow } from "../../element/typeChecks";
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { angleIcon } from "../icons";
import DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
-import type { ExcalidrawElement } from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx
index f58c06acc..142abc407 100644
--- a/packages/excalidraw/components/Stats/Dimension.tsx
+++ b/packages/excalidraw/components/Stats/Dimension.tsx
@@ -1,19 +1,20 @@
import { clamp, round } from "@excalidraw/math";
-import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
+import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
import {
MINIMAL_CROP_SIZE,
getUncroppedWidthAndHeight,
-} from "../../element/cropElement";
-import { mutateElement } from "../../element/mutateElement";
-import { resizeSingleElement } from "../../element/resizeElements";
-import { isImageElement } from "../../element/typeChecks";
+} 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 DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
-import type { ExcalidrawElement } from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/DragInput.scss b/packages/excalidraw/components/Stats/DragInput.scss
index 76b9d147b..f31616d94 100644
--- a/packages/excalidraw/components/Stats/DragInput.scss
+++ b/packages/excalidraw/components/Stats/DragInput.scss
@@ -2,10 +2,12 @@
.drag-input-container {
display: flex;
width: 100%;
+ border-radius: var(--border-radius-lg);
&:focus-within {
box-shadow: 0 0 0 1px var(--color-primary-darkest);
border-radius: var(--border-radius-md);
+ background: transparent;
}
}
@@ -16,24 +18,14 @@
.drag-input-label {
flex-shrink: 0;
- border: 1px solid var(--default-border-color);
- border-right: 0;
- padding: 0 0.5rem 0 0.75rem;
+ border: 0;
+ padding: 0 0.5rem 0 0.25rem;
min-width: 1rem;
+ width: 1.5rem;
height: 2rem;
- box-sizing: border-box;
+ box-sizing: content-box;
color: var(--popup-text-color);
- :root[dir="ltr"] & {
- border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
- }
-
- :root[dir="rtl"] & {
- border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
- border-right: 1px solid var(--default-border-color);
- border-left: 0;
- }
-
display: flex;
align-items: center;
justify-content: center;
@@ -51,20 +43,8 @@
border: 0;
outline: none;
height: 2rem;
- border: 1px solid var(--default-border-color);
- border-left: 0;
letter-spacing: 0.4px;
- :root[dir="ltr"] & {
- border-radius: 0 var(--border-radius-md) var(--border-radius-md) 0;
- }
-
- :root[dir="rtl"] & {
- border-radius: var(--border-radius-md) 0 0 var(--border-radius-md);
- border-left: 1px solid var(--default-border-color);
- border-right: 0;
- }
-
padding: 0.5rem;
padding-left: 0.25rem;
appearance: none;
diff --git a/packages/excalidraw/components/Stats/DragInput.tsx b/packages/excalidraw/components/Stats/DragInput.tsx
index fbea33e55..b4795308d 100644
--- a/packages/excalidraw/components/Stats/DragInput.tsx
+++ b/packages/excalidraw/components/Stats/DragInput.tsx
@@ -1,11 +1,13 @@
import clsx from "clsx";
import { useEffect, useRef, useState } from "react";
-import { EVENT } from "../../constants";
-import { deepCopyElement } from "../../element/newElement";
-import { KEYS } from "../../keys";
+import { EVENT, KEYS, cloneJSON } from "@excalidraw/common";
+
+import { deepCopyElement } from "@excalidraw/element/duplicate";
+
+import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
+
import { CaptureUpdateAction } from "../../store";
-import { cloneJSON } from "../../utils";
import { useApp } from "../App";
import { InlineIcon } from "../InlineIcon";
@@ -14,7 +16,6 @@ import { SMALLEST_DELTA } from "./utils";
import "./DragInput.scss";
import type { StatsInputProperty } from "./utils";
-import type { ElementsMap, ExcalidrawElement } from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/FontSize.tsx b/packages/excalidraw/components/Stats/FontSize.tsx
index 4fbdab833..90bdee564 100644
--- a/packages/excalidraw/components/Stats/FontSize.tsx
+++ b/packages/excalidraw/components/Stats/FontSize.tsx
@@ -1,17 +1,24 @@
-import { isTextElement, redrawTextBoundingBox } from "../../element";
-import { mutateElement } from "../../element/mutateElement";
-import { getBoundTextElement } from "../../element/textElement";
-import { hasBoundTextElement } from "../../element/typeChecks";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+import {
+ getBoundTextElement,
+ redrawTextBoundingBox,
+} from "@excalidraw/element/textElement";
+import {
+ hasBoundTextElement,
+ isTextElement,
+} from "@excalidraw/element/typeChecks";
+
+import type {
+ ExcalidrawElement,
+ ExcalidrawTextElement,
+} from "@excalidraw/element/types";
+
import { fontSizeIcon } from "../icons";
import StatsDragInput from "./DragInput";
import { getStepSizedValue } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
-import type {
- ExcalidrawElement,
- ExcalidrawTextElement,
-} from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/MultiAngle.tsx b/packages/excalidraw/components/Stats/MultiAngle.tsx
index ec314c183..3cabd19c0 100644
--- a/packages/excalidraw/components/Stats/MultiAngle.tsx
+++ b/packages/excalidraw/components/Stats/MultiAngle.tsx
@@ -1,18 +1,22 @@
import { degreesToRadians, radiansToDegrees } from "@excalidraw/math";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+
+import { getBoundTextElement } from "@excalidraw/element/textElement";
+import { isArrowElement } from "@excalidraw/element/typeChecks";
+
+import { isInGroup } from "@excalidraw/element/groups";
+
import type { Degrees } from "@excalidraw/math";
-import { mutateElement } from "../../element/mutateElement";
-import { getBoundTextElement } from "../../element/textElement";
-import { isArrowElement } from "../../element/typeChecks";
-import { isInGroup } from "../../groups";
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { angleIcon } from "../icons";
import DragInput from "./DragInput";
import { getStepSizedValue, isPropertyEditable } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
-import type { ExcalidrawElement } from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/MultiDimension.tsx b/packages/excalidraw/components/Stats/MultiDimension.tsx
index fb9cea942..b482611af 100644
--- a/packages/excalidraw/components/Stats/MultiDimension.tsx
+++ b/packages/excalidraw/components/Stats/MultiDimension.tsx
@@ -1,18 +1,27 @@
import { pointFrom, type GlobalPoint } from "@excalidraw/math";
import { useMemo } from "react";
-import { MIN_WIDTH_OR_HEIGHT } from "../../constants";
-import { getCommonBounds, isTextElement } from "../../element";
-import { updateBoundElements } from "../../element/binding";
-import { mutateElement } from "../../element/mutateElement";
+import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common";
+import { updateBoundElements } from "@excalidraw/element/binding";
+import { mutateElement } from "@excalidraw/element/mutateElement";
import {
rescalePointsInElement,
resizeSingleElement,
-} from "../../element/resizeElements";
+} from "@excalidraw/element/resizeElements";
import {
getBoundTextElement,
handleBindTextResize,
-} from "../../element/textElement";
+} from "@excalidraw/element/textElement";
+
+import { isTextElement } from "@excalidraw/element/typeChecks";
+
+import { getCommonBounds } from "@excalidraw/utils";
+
+import type {
+ ElementsMap,
+ ExcalidrawElement,
+ NonDeletedSceneElementsMap,
+} from "@excalidraw/element/types";
import DragInput from "./DragInput";
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
@@ -20,11 +29,6 @@ import { getElementsInAtomicUnit } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type { AtomicUnit } from "./utils";
-import type {
- ElementsMap,
- ExcalidrawElement,
- NonDeletedSceneElementsMap,
-} from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/MultiFontSize.tsx b/packages/excalidraw/components/Stats/MultiFontSize.tsx
index 8335399ef..6bac4bd3c 100644
--- a/packages/excalidraw/components/Stats/MultiFontSize.tsx
+++ b/packages/excalidraw/components/Stats/MultiFontSize.tsx
@@ -1,19 +1,27 @@
-import { isTextElement, redrawTextBoundingBox } from "../../element";
-import { mutateElement } from "../../element/mutateElement";
-import { getBoundTextElement } from "../../element/textElement";
-import { hasBoundTextElement } from "../../element/typeChecks";
-import { isInGroup } from "../../groups";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+import {
+ getBoundTextElement,
+ redrawTextBoundingBox,
+} from "@excalidraw/element/textElement";
+import {
+ hasBoundTextElement,
+ isTextElement,
+} from "@excalidraw/element/typeChecks";
+
+import { isInGroup } from "@excalidraw/element/groups";
+
+import type {
+ ExcalidrawElement,
+ ExcalidrawTextElement,
+ NonDeletedSceneElementsMap,
+} from "@excalidraw/element/types";
+
import { fontSizeIcon } from "../icons";
import StatsDragInput from "./DragInput";
import { getStepSizedValue } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
-import type {
- ExcalidrawElement,
- ExcalidrawTextElement,
- NonDeletedSceneElementsMap,
-} from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/MultiPosition.tsx b/packages/excalidraw/components/Stats/MultiPosition.tsx
index a8fdacf29..98058efec 100644
--- a/packages/excalidraw/components/Stats/MultiPosition.tsx
+++ b/packages/excalidraw/components/Stats/MultiPosition.tsx
@@ -1,7 +1,16 @@
import { pointFrom, pointRotateRads } from "@excalidraw/math";
import { useMemo } from "react";
-import { getCommonBounds, isTextElement } from "../../element";
+import { isTextElement } from "@excalidraw/element/typeChecks";
+
+import { getCommonBounds } from "@excalidraw/element/bounds";
+
+import type {
+ ElementsMap,
+ ExcalidrawElement,
+ NonDeletedExcalidrawElement,
+ NonDeletedSceneElementsMap,
+} from "@excalidraw/element/types";
import StatsDragInput from "./DragInput";
import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils";
@@ -9,12 +18,6 @@ import { getElementsInAtomicUnit, moveElement } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
import type { AtomicUnit } from "./utils";
-import type {
- ElementsMap,
- ExcalidrawElement,
- NonDeletedExcalidrawElement,
- NonDeletedSceneElementsMap,
-} from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/Position.tsx b/packages/excalidraw/components/Stats/Position.tsx
index 08d0a8830..bf6dfd161 100644
--- a/packages/excalidraw/components/Stats/Position.tsx
+++ b/packages/excalidraw/components/Stats/Position.tsx
@@ -3,15 +3,16 @@ import { clamp, pointFrom, pointRotateRads, round } from "@excalidraw/math";
import {
getFlipAdjustedCropPosition,
getUncroppedWidthAndHeight,
-} from "../../element/cropElement";
-import { mutateElement } from "../../element/mutateElement";
-import { isImageElement } from "../../element/typeChecks";
+} 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 StatsDragInput from "./DragInput";
import { getStepSizedValue, moveElement } from "./utils";
import type { DragInputCallbackType } from "./DragInput";
-import type { ElementsMap, ExcalidrawElement } from "../../element/types";
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/Stats/Stats.scss b/packages/excalidraw/components/Stats/Stats.scss
index 106ecf303..384e0fd3c 100644
--- a/packages/excalidraw/components/Stats/Stats.scss
+++ b/packages/excalidraw/components/Stats/Stats.scss
@@ -41,6 +41,10 @@
div + div {
text-align: right;
}
+
+ &:empty {
+ display: none;
+ }
}
&__row--heading {
diff --git a/packages/excalidraw/components/Stats/index.tsx b/packages/excalidraw/components/Stats/index.tsx
index 56764fefb..11a5d6b5d 100644
--- a/packages/excalidraw/components/Stats/index.tsx
+++ b/packages/excalidraw/components/Stats/index.tsx
@@ -3,12 +3,17 @@ import clsx from "clsx";
import throttle from "lodash.throttle";
import { useEffect, useMemo, useState, memo } from "react";
-import { STATS_PANELS } from "../../constants";
-import { getCommonBounds } from "../../element/bounds";
-import { getUncroppedWidthAndHeight } from "../../element/cropElement";
-import { isElbowArrow, isImageElement } from "../../element/typeChecks";
-import { frameAndChildrenSelectedTogether } from "../../frame";
-import { elementsAreInSameGroup } from "../../groups";
+import { STATS_PANELS } from "@excalidraw/common";
+import { getCommonBounds } from "@excalidraw/element/bounds";
+import { getUncroppedWidthAndHeight } from "@excalidraw/element/cropElement";
+import { isElbowArrow, isImageElement } from "@excalidraw/element/typeChecks";
+
+import { frameAndChildrenSelectedTogether } from "@excalidraw/element/frame";
+
+import { elementsAreInSameGroup } from "@excalidraw/element/groups";
+
+import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
+
import { t } from "../../i18n";
import { isGridModeEnabled } from "../../snapping";
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
@@ -29,7 +34,6 @@ import { getAtomicUnits } from "./utils";
import "./Stats.scss";
-import type { NonDeletedExcalidrawElement } from "../../element/types";
import type {
AppClassProperties,
AppState,
@@ -285,7 +289,11 @@ export const StatsInner = memo(
)}
-
+
{appState.croppingElementId
? t("labels.imageCropping")
: t(`element.${singleElement.type}`)}
diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx
index b1fe0d685..fc94da056 100644
--- a/packages/excalidraw/components/Stats/stats.test.tsx
+++ b/packages/excalidraw/components/Stats/stats.test.tsx
@@ -3,14 +3,23 @@ import { act, fireEvent, queryByTestId } from "@testing-library/react";
import React from "react";
import { vi } from "vitest";
+import { setDateTimeForTests, reseed } from "@excalidraw/common";
+
+import { isInGroup } from "@excalidraw/element/groups";
+
+import { isTextElement } from "@excalidraw/element/typeChecks";
+
import type { Degrees } from "@excalidraw/math";
-import { Excalidraw, mutateElement } from "../..";
+import type {
+ ExcalidrawElement,
+ ExcalidrawLinearElement,
+ ExcalidrawTextElement,
+} from "@excalidraw/element/types";
+
+import { Excalidraw, getCommonBounds, mutateElement } from "../..";
import { actionGroup } from "../../actions";
-import { getCommonBounds, isTextElement } from "../../element";
-import { isInGroup } from "../../groups";
import { t } from "../../i18n";
-import { reseed } from "../../random";
import * as StaticScene from "../../renderer/staticScene";
import { API } from "../../tests/helpers/api";
import { Keyboard, Pointer, UI } from "../../tests/helpers/ui";
@@ -21,16 +30,9 @@ import {
render,
restoreOriginalGetBoundingClientRect,
} from "../../tests/test-utils";
-import { setDateTimeForTests } from "../../utils";
import { getStepSizedValue } from "./utils";
-import type {
- ExcalidrawElement,
- ExcalidrawLinearElement,
- ExcalidrawTextElement,
-} from "../../element/types";
-
const { h } = window;
const mouse = new Pointer("mouse");
const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
diff --git a/packages/excalidraw/components/Stats/utils.ts b/packages/excalidraw/components/Stats/utils.ts
index 219e02fa5..dbb47a234 100644
--- a/packages/excalidraw/components/Stats/utils.ts
+++ b/packages/excalidraw/components/Stats/utils.ts
@@ -1,30 +1,32 @@
import { pointFrom, pointRotateRads } from "@excalidraw/math";
-import type { Radians } from "@excalidraw/math";
-
import {
bindOrUnbindLinearElements,
updateBoundElements,
-} from "../../element/binding";
-import { mutateElement } from "../../element/mutateElement";
-import { getBoundTextElement } from "../../element/textElement";
+} from "@excalidraw/element/binding";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+import { getBoundTextElement } from "@excalidraw/element/textElement";
import {
isFrameLikeElement,
isLinearElement,
isTextElement,
-} from "../../element/typeChecks";
+} from "@excalidraw/element/typeChecks";
+
import {
getSelectedGroupIds,
getElementsInGroup,
isInGroup,
-} from "../../groups";
+} from "@excalidraw/element/groups";
+
+import type { Radians } from "@excalidraw/math";
import type {
ElementsMap,
ExcalidrawElement,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
-} from "../../element/types";
+} from "@excalidraw/element/types";
+
import type Scene from "../../scene/Scene";
import type { AppState } from "../../types";
diff --git a/packages/excalidraw/components/TTDDialog/MermaidToExcalidraw.tsx b/packages/excalidraw/components/TTDDialog/MermaidToExcalidraw.tsx
index b8cd48dfa..8a4f92840 100644
--- a/packages/excalidraw/components/TTDDialog/MermaidToExcalidraw.tsx
+++ b/packages/excalidraw/components/TTDDialog/MermaidToExcalidraw.tsx
@@ -1,11 +1,13 @@
import { useState, useRef, useEffect, useDeferredValue } from "react";
+import { EDITOR_LS_KEYS, debounce, isDevEnv } from "@excalidraw/common";
+
+import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
+
import { useApp } from "../App";
import { ArrowRightIcon } from "../icons";
-import { EDITOR_LS_KEYS } from "../../constants";
import { EditorLocalStorage } from "../../data/EditorLocalStorage";
import { t } from "../../i18n";
-import { debounce, isDevEnv } from "../../utils";
import Trans from "../Trans";
import { TTDDialogInput } from "./TTDDialogInput";
@@ -23,7 +25,6 @@ import "./MermaidToExcalidraw.scss";
import type { BinaryFiles } from "../../types";
import type { MermaidToExcalidrawLibProps } from "./common";
-import type { NonDeletedExcalidrawElement } from "../../element/types";
const MERMAID_EXAMPLE =
"flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
diff --git a/packages/excalidraw/components/TTDDialog/TTDDialog.tsx b/packages/excalidraw/components/TTDDialog/TTDDialog.tsx
index 3b9b868f7..68d230d24 100644
--- a/packages/excalidraw/components/TTDDialog/TTDDialog.tsx
+++ b/packages/excalidraw/components/TTDDialog/TTDDialog.tsx
@@ -1,6 +1,9 @@
-import { isFiniteNumber } from "@excalidraw/math";
import { useEffect, useRef, useState } from "react";
+import { isFiniteNumber } from "@excalidraw/math";
+
+import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
+
import { trackEvent } from "../../analytics";
import { useUIAppState } from "../../context/ui-appState";
import { atom, useAtom } from "../../editor-jotai";
@@ -32,7 +35,7 @@ import "./TTDDialog.scss";
import type { ChangeEventHandler } from "react";
import type { MermaidToExcalidrawLibProps } from "./common";
-import type { NonDeletedExcalidrawElement } from "../../element/types";
+
import type { BinaryFiles } from "../../types";
const MIN_PROMPT_LENGTH = 3;
diff --git a/packages/excalidraw/components/TTDDialog/TTDDialogInput.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogInput.tsx
index 8e349643e..9bd80b681 100644
--- a/packages/excalidraw/components/TTDDialog/TTDDialogInput.tsx
+++ b/packages/excalidraw/components/TTDDialog/TTDDialogInput.tsx
@@ -1,7 +1,6 @@
import { useEffect, useRef } from "react";
-import { EVENT } from "../../constants";
-import { KEYS } from "../../keys";
+import { EVENT, KEYS } from "@excalidraw/common";
import type { ChangeEventHandler } from "react";
diff --git a/packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx
index a8831e3a0..05cad640b 100644
--- a/packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx
+++ b/packages/excalidraw/components/TTDDialog/TTDDialogSubmitShortcut.tsx
@@ -1,4 +1,4 @@
-import { getShortcutKey } from "../../utils";
+import { getShortcutKey } from "@excalidraw/common";
export const TTDDialogSubmitShortcut = () => {
return (
diff --git a/packages/excalidraw/components/TTDDialog/TTDDialogTabs.tsx b/packages/excalidraw/components/TTDDialog/TTDDialogTabs.tsx
index 1c9075e7d..fe4128106 100644
--- a/packages/excalidraw/components/TTDDialog/TTDDialogTabs.tsx
+++ b/packages/excalidraw/components/TTDDialog/TTDDialogTabs.tsx
@@ -1,7 +1,8 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import { useRef } from "react";
-import { isMemberOf } from "../../utils";
+import { isMemberOf } from "@excalidraw/common";
+
import { useExcalidrawSetAppState } from "../App";
import type { ReactNode } from "react";
diff --git a/packages/excalidraw/components/TTDDialog/common.ts b/packages/excalidraw/components/TTDDialog/common.ts
index 89c342c65..2e59565cf 100644
--- a/packages/excalidraw/components/TTDDialog/common.ts
+++ b/packages/excalidraw/components/TTDDialog/common.ts
@@ -1,13 +1,15 @@
+import { DEFAULT_EXPORT_PADDING, EDITOR_LS_KEYS } from "@excalidraw/common";
+
import type { MermaidConfig } from "@excalidraw/mermaid-to-excalidraw";
import type { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
-import { DEFAULT_EXPORT_PADDING, EDITOR_LS_KEYS } from "../../constants";
+import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types";
+
import { EditorLocalStorage } from "../../data/EditorLocalStorage";
import { canvasToBlob } from "../../data/blob";
import { t } from "../../i18n";
import { convertToExcalidrawElements, exportToCanvas } from "../../index";
-import type { NonDeletedExcalidrawElement } from "../../element/types";
import type { AppClassProperties, BinaryFiles } from "../../types";
const resetPreview = ({
diff --git a/packages/excalidraw/components/ToolButton.tsx b/packages/excalidraw/components/ToolButton.tsx
index fb76731b0..f833a6d1f 100644
--- a/packages/excalidraw/components/ToolButton.tsx
+++ b/packages/excalidraw/components/ToolButton.tsx
@@ -1,15 +1,17 @@
import clsx from "clsx";
import React, { useEffect, useRef, useState } from "react";
+import { isPromiseLike } from "@excalidraw/common";
+
+import type { PointerType } from "@excalidraw/element/types";
+
import { AbortError } from "../errors";
-import { isPromiseLike } from "../utils";
import "./ToolIcon.scss";
import Spinner from "./Spinner";
import { useExcalidrawContainer } from "./App";
-import type { PointerType } from "../element/types";
import type { CSSProperties } from "react";
export type ToolButtonSize = "small" | "medium";
diff --git a/packages/excalidraw/components/UserList.scss b/packages/excalidraw/components/UserList.scss
index fdcadef7e..025f4d16e 100644
--- a/packages/excalidraw/components/UserList.scss
+++ b/packages/excalidraw/components/UserList.scss
@@ -1,4 +1,4 @@
-@import "../css/variables.module";
+@import "../css/variables.module.scss";
.excalidraw {
--avatar-size: 1.75rem;
diff --git a/packages/excalidraw/components/UserList.tsx b/packages/excalidraw/components/UserList.tsx
index 6c74411a1..811289329 100644
--- a/packages/excalidraw/components/UserList.tsx
+++ b/packages/excalidraw/components/UserList.tsx
@@ -2,9 +2,11 @@ import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx";
import React, { useLayoutEffect } from "react";
-import { supportsResizeObserver } from "../constants";
+import { supportsResizeObserver, isShallowEqual } from "@excalidraw/common";
+
+import type { MarkRequired } from "@excalidraw/common/utility-types";
+
import { t } from "../i18n";
-import { isShallowEqual } from "../utils";
import { useExcalidrawActionManager } from "./App";
import { Island } from "./Island";
@@ -16,7 +18,6 @@ import "./UserList.scss";
import type { ActionManager } from "../actions/manager";
import type { Collaborator, SocketId } from "../types";
-import type { MarkRequired } from "../utility-types";
export type GoToCollaboratorComponentProps = {
socketId: SocketId;
diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
index 8388e5e6c..1f4f57433 100644
--- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
+++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
@@ -1,15 +1,20 @@
import React, { useEffect, useRef } from "react";
-import { CURSOR_TYPE } from "../../constants";
-import { t } from "../../i18n";
-import { isRenderThrottlingEnabled } from "../../reactUtils";
-import { renderInteractiveScene } from "../../renderer/interactiveScene";
-import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils";
+import {
+ CURSOR_TYPE,
+ isShallowEqual,
+ sceneCoordsToViewportCoords,
+} from "@excalidraw/common";
import type {
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
-} from "../../element/types";
+} from "@excalidraw/element/types";
+
+import { t } from "../../i18n";
+import { isRenderThrottlingEnabled } from "../../reactUtils";
+import { renderInteractiveScene } from "../../renderer/interactiveScene";
+
import type {
InteractiveCanvasRenderConfig,
RenderableElementsMap,
diff --git a/packages/excalidraw/components/canvases/NewElementCanvas.tsx b/packages/excalidraw/components/canvases/NewElementCanvas.tsx
index 524d6af34..4310f1bd1 100644
--- a/packages/excalidraw/components/canvases/NewElementCanvas.tsx
+++ b/packages/excalidraw/components/canvases/NewElementCanvas.tsx
@@ -1,9 +1,10 @@
import { useEffect, useRef } from "react";
+import type { NonDeletedSceneElementsMap } from "@excalidraw/element/types";
+
import { isRenderThrottlingEnabled } from "../../reactUtils";
import { renderNewElementScene } from "../../renderer/renderNewElementScene";
-import type { NonDeletedSceneElementsMap } from "../../element/types";
import type {
RenderableElementsMap,
StaticCanvasRenderConfig,
diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx
index 6e2df9537..5a498ebac 100644
--- a/packages/excalidraw/components/canvases/StaticCanvas.tsx
+++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx
@@ -1,13 +1,15 @@
import React, { useEffect, useRef } from "react";
-import { isRenderThrottlingEnabled } from "../../reactUtils";
-import { renderStaticScene } from "../../renderer/staticScene";
-import { isShallowEqual } from "../../utils";
+import { isShallowEqual } from "@excalidraw/common";
import type {
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
-} from "../../element/types";
+} from "@excalidraw/element/types";
+
+import { isRenderThrottlingEnabled } from "../../reactUtils";
+import { renderStaticScene } from "../../renderer/staticScene";
+
import type {
RenderableElementsMap,
StaticCanvasRenderConfig,
@@ -85,34 +87,36 @@ const StaticCanvas = (props: StaticCanvasProps) => {
return
;
};
-const getRelevantAppStateProps = (
- appState: AppState,
-): StaticCanvasAppState => ({
- zoom: appState.zoom,
- scrollX: appState.scrollX,
- scrollY: appState.scrollY,
- width: appState.width,
- height: appState.height,
- viewModeEnabled: appState.viewModeEnabled,
- openDialog: appState.openDialog,
- hoveredElementIds: appState.hoveredElementIds,
- offsetLeft: appState.offsetLeft,
- offsetTop: appState.offsetTop,
- theme: appState.theme,
- pendingImageElementId: appState.pendingImageElementId,
- shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
- viewBackgroundColor: appState.viewBackgroundColor,
- exportScale: appState.exportScale,
- selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged,
- gridSize: appState.gridSize,
- gridStep: appState.gridStep,
- frameRendering: appState.frameRendering,
- selectedElementIds: appState.selectedElementIds,
- frameToHighlight: appState.frameToHighlight,
- editingGroupId: appState.editingGroupId,
- currentHoveredFontFamily: appState.currentHoveredFontFamily,
- croppingElementId: appState.croppingElementId,
-});
+const getRelevantAppStateProps = (appState: AppState): StaticCanvasAppState => {
+ const relevantAppStateProps = {
+ zoom: appState.zoom,
+ scrollX: appState.scrollX,
+ scrollY: appState.scrollY,
+ width: appState.width,
+ height: appState.height,
+ viewModeEnabled: appState.viewModeEnabled,
+ openDialog: appState.openDialog,
+ hoveredElementIds: appState.hoveredElementIds,
+ offsetLeft: appState.offsetLeft,
+ offsetTop: appState.offsetTop,
+ theme: appState.theme,
+ pendingImageElementId: appState.pendingImageElementId,
+ shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
+ viewBackgroundColor: appState.viewBackgroundColor,
+ exportScale: appState.exportScale,
+ selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged,
+ gridSize: appState.gridSize,
+ gridStep: appState.gridStep,
+ frameRendering: appState.frameRendering,
+ selectedElementIds: appState.selectedElementIds,
+ frameToHighlight: appState.frameToHighlight,
+ editingGroupId: appState.editingGroupId,
+ currentHoveredFontFamily: appState.currentHoveredFontFamily,
+ croppingElementId: appState.croppingElementId,
+ };
+
+ return relevantAppStateProps;
+};
const areEqual = (
prevProps: StaticCanvasProps,
diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx
index d4c0b7a8d..5be65b100 100644
--- a/packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx
+++ b/packages/excalidraw/components/dropdownMenu/DropdownMenu.test.tsx
@@ -1,7 +1,8 @@
import React from "react";
+import { KEYS } from "@excalidraw/common";
+
import { Excalidraw } from "../../index";
-import { KEYS } from "../../keys";
import { Keyboard } from "../../tests/helpers/ui";
import {
render,
diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx
index 24a0a4d27..de6fc31c1 100644
--- a/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx
+++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuContent.tsx
@@ -1,10 +1,10 @@
import clsx from "clsx";
import React, { useEffect, useRef } from "react";
-import { EVENT } from "../../constants";
+import { EVENT, KEYS } from "@excalidraw/common";
+
import { useOutsideClick } from "../../hooks/useOutsideClick";
import { useStable } from "../../hooks/useStable";
-import { KEYS } from "../../keys";
import { useDevice } from "../App";
import { Island } from "../Island";
import Stack from "../Stack";
diff --git a/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx b/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx
index fc19bd1bf..59c5dd2f8 100644
--- a/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx
+++ b/packages/excalidraw/components/dropdownMenu/DropdownMenuItem.tsx
@@ -1,6 +1,9 @@
import React, { useEffect, useRef } from "react";
-import { THEME } from "../../constants";
+import { THEME } from "@excalidraw/common";
+
+import type { ValueOf } from "@excalidraw/common/utility-types";
+
import { useExcalidrawAppState } from "../App";
import MenuItemContent from "./DropdownMenuItemContent";
@@ -9,7 +12,6 @@ import {
useHandleDropdownMenuItemClick,
} from "./common";
-import type { ValueOf } from "../../utility-types";
import type { JSX } from "react";
const DropdownMenuItem = ({
diff --git a/packages/excalidraw/components/dropdownMenu/common.ts b/packages/excalidraw/components/dropdownMenu/common.ts
index 312b78aae..feca5c406 100644
--- a/packages/excalidraw/components/dropdownMenu/common.ts
+++ b/packages/excalidraw/components/dropdownMenu/common.ts
@@ -1,7 +1,6 @@
import React, { useContext } from "react";
-import { EVENT } from "../../constants";
-import { composeEventHandlers } from "../../utils";
+import { EVENT, composeEventHandlers } from "@excalidraw/common";
export const DropdownMenuContentPropsContext = React.createContext<{
onSelect?: (event: Event) => void;
diff --git a/packages/excalidraw/components/hyperlink/Hyperlink.tsx b/packages/excalidraw/components/hyperlink/Hyperlink.tsx
index 88983aeab..9a386a163 100644
--- a/packages/excalidraw/components/hyperlink/Hyperlink.tsx
+++ b/packages/excalidraw/components/hyperlink/Hyperlink.tsx
@@ -8,37 +8,51 @@ import {
useState,
} from "react";
-import { trackEvent } from "../../analytics";
-import { getTooltipDiv, updateTooltipPosition } from "../../components/Tooltip";
-import { EVENT, HYPERLINK_TOOLTIP_DELAY } from "../../constants";
-import { isLocalLink, normalizeLink } from "../../data/url";
-import { getElementAbsoluteCoords } from "../../element/bounds";
-import { hitElementBoundingBox } from "../../element/collision";
-import { isElementLink } from "../../element/elementLink";
-import { getEmbedLink, embeddableURLValidator } from "../../element/embeddable";
-import { mutateElement } from "../../element/mutateElement";
-import { t } from "../../i18n";
+import { EVENT, HYPERLINK_TOOLTIP_DELAY, KEYS } from "@excalidraw/common";
+
+import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
+
+import { hitElementBoundingBox } from "@excalidraw/element/collision";
+
+import { isElementLink } from "@excalidraw/element/elementLink";
+
+import {
+ getEmbedLink,
+ embeddableURLValidator,
+} from "@excalidraw/element/embeddable";
+
+import { mutateElement } from "@excalidraw/element/mutateElement";
+
import {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
wrapEvent,
-} from "../../utils";
-import { useAppProps, useDevice, useExcalidrawAppState } from "../App";
-import { ToolButton } from "../ToolButton";
-import { FreedrawIcon, TrashIcon, elementLinkIcon } from "../icons";
-import { KEYS } from "../../keys";
-import { getSelectedElements } from "../../scene";
-import { isEmbeddableElement } from "../../element/typeChecks";
+ isLocalLink,
+ normalizeLink,
+} from "@excalidraw/common";
-import { getLinkHandleFromCoords } from "./helpers";
-
-import "./Hyperlink.scss";
+import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
import type {
ElementsMap,
ExcalidrawEmbeddableElement,
NonDeletedExcalidrawElement,
-} from "../../element/types";
+} from "@excalidraw/element/types";
+
+import { trackEvent } from "../../analytics";
+import { getTooltipDiv, updateTooltipPosition } from "../../components/Tooltip";
+
+import { t } from "../../i18n";
+
+import { useAppProps, useDevice, useExcalidrawAppState } from "../App";
+import { ToolButton } from "../ToolButton";
+import { FreedrawIcon, TrashIcon, elementLinkIcon } from "../icons";
+import { getSelectedElements } from "../../scene";
+
+import { getLinkHandleFromCoords } from "./helpers";
+
+import "./Hyperlink.scss";
+
import type { AppState, ExcalidrawProps, UIAppState } from "../../types";
const POPUP_WIDTH = 380;
diff --git a/packages/excalidraw/components/hyperlink/helpers.ts b/packages/excalidraw/components/hyperlink/helpers.ts
index 3d39e2ebe..d1345db98 100644
--- a/packages/excalidraw/components/hyperlink/helpers.ts
+++ b/packages/excalidraw/components/hyperlink/helpers.ts
@@ -1,17 +1,19 @@
import { pointFrom, pointRotateRads } from "@excalidraw/math";
+import { MIME_TYPES } from "@excalidraw/common";
+import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
+import { hitElementBoundingBox } from "@excalidraw/element/collision";
+
+import { DEFAULT_LINK_SIZE } from "@excalidraw/element/renderElement";
+
import type { GlobalPoint, Radians } from "@excalidraw/math";
-import { MIME_TYPES } from "../../constants";
-import { getElementAbsoluteCoords } from "../../element/bounds";
-import { hitElementBoundingBox } from "../../element/collision";
-import { DEFAULT_LINK_SIZE } from "../../renderer/renderElement";
-
-import type { Bounds } from "../../element/bounds";
+import type { Bounds } from "@excalidraw/element/bounds";
import type {
ElementsMap,
NonDeletedExcalidrawElement,
-} from "../../element/types";
+} from "@excalidraw/element/types";
+
import type { AppState, UIAppState } from "../../types";
export const EXTERNAL_LINK_IMG = document.createElement("img");
diff --git a/packages/excalidraw/components/icons.tsx b/packages/excalidraw/components/icons.tsx
index 03e6ca2bb..f3808a69d 100644
--- a/packages/excalidraw/components/icons.tsx
+++ b/packages/excalidraw/components/icons.tsx
@@ -10,9 +10,9 @@ import clsx from "clsx";
import oc from "open-color";
import React from "react";
-import { THEME } from "../constants";
+import { THEME } from "@excalidraw/common";
-import type { Theme } from "../element/types";
+import type { Theme } from "@excalidraw/element/types";
export const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
@@ -274,6 +274,21 @@ export const SelectionIcon = createIcon(
{ fill: "none", width: 22, height: 22, strokeWidth: 1.25 },
);
+export const LassoIcon = createIcon(
+
+
+
+
+ ,
+
+ { fill: "none", width: 22, height: 22, strokeWidth: 1.25 },
+);
+
// tabler-icons: square
export const RectangleIcon = createIcon(
@@ -406,7 +421,7 @@ export const TrashIcon = createIcon(
);
export const EmbedIcon = createIcon(
-
+
,
diff --git a/packages/excalidraw/components/main-menu/DefaultItems.tsx b/packages/excalidraw/components/main-menu/DefaultItems.tsx
index c2307d521..29a2761a1 100644
--- a/packages/excalidraw/components/main-menu/DefaultItems.tsx
+++ b/packages/excalidraw/components/main-menu/DefaultItems.tsx
@@ -1,5 +1,9 @@
import clsx from "clsx";
+import { THEME } from "@excalidraw/common";
+
+import type { Theme } from "@excalidraw/element/types";
+
import {
actionClearCanvas,
actionLoadScene,
@@ -10,7 +14,6 @@ import {
} from "../../actions";
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { trackEvent } from "../../analytics";
-import { THEME } from "../../constants";
import { useUIAppState } from "../../context/ui-appState";
import { useSetAtom } from "../../editor-jotai";
import { useI18n } from "../../i18n";
@@ -44,8 +47,6 @@ import {
import "./DefaultItems.scss";
-import type { Theme } from "../../element/types";
-
export const LoadScene = () => {
const { t } = useI18n();
const actionManager = useExcalidrawActionManager();
diff --git a/packages/excalidraw/components/main-menu/MainMenu.tsx b/packages/excalidraw/components/main-menu/MainMenu.tsx
index 54f60a364..7c2b5fb4a 100644
--- a/packages/excalidraw/components/main-menu/MainMenu.tsx
+++ b/packages/excalidraw/components/main-menu/MainMenu.tsx
@@ -1,9 +1,10 @@
import React from "react";
+import { composeEventHandlers } from "@excalidraw/common";
+
import { useTunnels } from "../../context/tunnels";
import { useUIAppState } from "../../context/ui-appState";
import { t } from "../../i18n";
-import { composeEventHandlers } from "../../utils";
import { useDevice, useExcalidrawSetAppState } from "../App";
import { UserList } from "../UserList";
import DropdownMenu from "../dropdownMenu/DropdownMenu";
diff --git a/packages/excalidraw/components/shapes.tsx b/packages/excalidraw/components/shapes.tsx
new file mode 100644
index 000000000..7411a9e25
--- /dev/null
+++ b/packages/excalidraw/components/shapes.tsx
@@ -0,0 +1,100 @@
+import { KEYS } from "@excalidraw/common";
+
+import {
+ SelectionIcon,
+ RectangleIcon,
+ DiamondIcon,
+ EllipseIcon,
+ ArrowIcon,
+ LineIcon,
+ FreedrawIcon,
+ TextIcon,
+ ImageIcon,
+ EraserIcon,
+} from "./icons";
+
+export const SHAPES = [
+ {
+ icon: SelectionIcon,
+ value: "selection",
+ key: KEYS.V,
+ numericKey: KEYS["1"],
+ fillable: true,
+ },
+ {
+ icon: RectangleIcon,
+ value: "rectangle",
+ key: KEYS.R,
+ numericKey: KEYS["2"],
+ fillable: true,
+ },
+ {
+ icon: DiamondIcon,
+ value: "diamond",
+ key: KEYS.D,
+ numericKey: KEYS["3"],
+ fillable: true,
+ },
+ {
+ icon: EllipseIcon,
+ value: "ellipse",
+ key: KEYS.O,
+ numericKey: KEYS["4"],
+ fillable: true,
+ },
+ {
+ icon: ArrowIcon,
+ value: "arrow",
+ key: KEYS.A,
+ numericKey: KEYS["5"],
+ fillable: true,
+ },
+ {
+ icon: LineIcon,
+ value: "line",
+ key: KEYS.L,
+ numericKey: KEYS["6"],
+ fillable: true,
+ },
+ {
+ icon: FreedrawIcon,
+ value: "freedraw",
+ key: [KEYS.P, KEYS.X],
+ numericKey: KEYS["7"],
+ fillable: false,
+ },
+ {
+ icon: TextIcon,
+ value: "text",
+ key: KEYS.T,
+ numericKey: KEYS["8"],
+ fillable: false,
+ },
+ {
+ icon: ImageIcon,
+ value: "image",
+ key: null,
+ numericKey: KEYS["9"],
+ fillable: false,
+ },
+ {
+ icon: EraserIcon,
+ value: "eraser",
+ key: KEYS.E,
+ numericKey: KEYS["0"],
+ fillable: false,
+ },
+] as const;
+
+export const findShapeByKey = (key: string) => {
+ const shape = SHAPES.find((shape, index) => {
+ return (
+ (shape.numericKey != null && key === shape.numericKey.toString()) ||
+ (shape.key &&
+ (typeof shape.key === "string"
+ ? shape.key === key
+ : (shape.key as readonly string[]).includes(key)))
+ );
+ });
+ return shape?.value || null;
+};
diff --git a/packages/excalidraw/css/theme.scss b/packages/excalidraw/css/theme.scss
index fd6a8dacb..1d6a56966 100644
--- a/packages/excalidraw/css/theme.scss
+++ b/packages/excalidraw/css/theme.scss
@@ -148,7 +148,7 @@
--border-radius-lg: 0.5rem;
--color-surface-high: #f1f0ff;
- --color-surface-mid: #f2f2f7;
+ --color-surface-mid: #f6f6f9;
--color-surface-low: #ececf4;
--color-surface-lowest: #ffffff;
--color-on-surface: #1b1b1f;
@@ -252,7 +252,7 @@
--color-logo-text: #e2dfff;
- --color-surface-high: hsl(245, 10%, 21%);
+ --color-surface-high: #2e2d39;
--color-surface-low: hsl(240, 8%, 15%);
--color-surface-mid: hsl(240 6% 10%);
--color-surface-lowest: hsl(0, 0%, 7%);
diff --git a/packages/excalidraw/cursor.ts b/packages/excalidraw/cursor.ts
index 1aa6e52d2..7125ccedf 100644
--- a/packages/excalidraw/cursor.ts
+++ b/packages/excalidraw/cursor.ts
@@ -1,7 +1,8 @@
import OpenColor from "open-color";
+import { CURSOR_TYPE, MIME_TYPES, THEME } from "@excalidraw/common";
+
import { isHandToolActive, isEraserActive } from "./appState";
-import { CURSOR_TYPE, MIME_TYPES, THEME } from "./constants";
import type { AppState, DataURL } from "./types";
diff --git a/packages/excalidraw/data/EditorLocalStorage.ts b/packages/excalidraw/data/EditorLocalStorage.ts
index bb6eeb478..18d305315 100644
--- a/packages/excalidraw/data/EditorLocalStorage.ts
+++ b/packages/excalidraw/data/EditorLocalStorage.ts
@@ -1,4 +1,5 @@
-import type { EDITOR_LS_KEYS } from "../constants";
+import type { EDITOR_LS_KEYS } from "@excalidraw/common";
+
import type { JSONValue } from "../types";
export class EditorLocalStorage {
diff --git a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap
index 917f3d95e..70f8daa31 100644
--- a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap
+++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap
@@ -104,12 +104,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100,
"points": [
[
- 0.5,
- 0.5,
+ 0,
+ 0,
],
[
- 394.5,
- 34.5,
+ 394,
+ 34,
],
],
"roughness": 1,
@@ -129,8 +129,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"version": 4,
"versionNonce": Any,
"width": 395,
- "x": 247,
- "y": 420,
+ "x": 247.5,
+ "y": 420.5,
}
`;
@@ -160,11 +160,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 399.5,
+ 399,
0,
],
],
@@ -185,7 +185,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"version": 4,
"versionNonce": Any,
"width": 400,
- "x": 227,
+ "x": 227.5,
"y": 450,
}
`;
@@ -350,11 +350,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -375,7 +375,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"version": 4,
"versionNonce": Any,
"width": 100,
- "x": 255,
+ "x": 255.5,
"y": 239,
}
`;
@@ -452,11 +452,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -477,7 +477,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"version": 4,
"versionNonce": Any,
"width": 100,
- "x": 255,
+ "x": 255.5,
"y": 239,
}
`;
@@ -628,11 +628,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -653,7 +653,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"version": 4,
"versionNonce": Any,
"width": 100,
- "x": 255,
+ "x": 255.5,
"y": 239,
}
`;
@@ -845,11 +845,11 @@ exports[`Test Transform > should transform linear elements 1`] = `
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -866,7 +866,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
"version": 2,
"versionNonce": Any,
"width": 100,
- "x": 100,
+ "x": 100.5,
"y": 20,
}
`;
@@ -893,11 +893,11 @@ exports[`Test Transform > should transform linear elements 2`] = `
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -914,7 +914,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
"version": 2,
"versionNonce": Any,
"width": 100,
- "x": 450,
+ "x": 450.5,
"y": 20,
}
`;
@@ -1490,11 +1490,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 272.485,
+ 271.985,
0,
],
],
@@ -1517,7 +1517,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"version": 4,
"versionNonce": Any,
"width": 272.985,
- "x": 111.262,
+ "x": 111.762,
"y": 57,
}
`;
@@ -1862,11 +1862,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -1883,7 +1883,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"version": 2,
"versionNonce": Any,
"width": 100,
- "x": 100,
+ "x": 100.5,
"y": 100,
}
`;
@@ -1915,11 +1915,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -1936,7 +1936,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"version": 2,
"versionNonce": Any,
"width": 100,
- "x": 100,
+ "x": 100.5,
"y": 200,
}
`;
@@ -1968,11 +1968,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -1989,7 +1989,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"version": 2,
"versionNonce": Any,
"width": 100,
- "x": 100,
+ "x": 100.5,
"y": 300,
}
`;
@@ -2021,11 +2021,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"opacity": 100,
"points": [
[
- 0.5,
+ 0,
0,
],
[
- 99.5,
+ 99,
0,
],
],
@@ -2042,7 +2042,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"version": 2,
"versionNonce": Any,
"width": 100,
- "x": 100,
+ "x": 100.5,
"y": 400,
}
`;
diff --git a/packages/excalidraw/data/blob.ts b/packages/excalidraw/data/blob.ts
index 54505068f..3e5db7c29 100644
--- a/packages/excalidraw/data/blob.ts
+++ b/packages/excalidraw/data/blob.ts
@@ -1,22 +1,31 @@
import { nanoid } from "nanoid";
+import {
+ IMAGE_MIME_TYPES,
+ MIME_TYPES,
+ bytesToHexString,
+ isPromiseLike,
+} from "@excalidraw/common";
+
+import { clearElementsForExport } from "@excalidraw/element";
+
+import type { ValueOf } from "@excalidraw/common/utility-types";
+import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
+
import { cleanAppStateForExport } from "../appState";
-import { IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
-import { clearElementsForExport } from "../element";
+
import { CanvasError, ImageSceneDataError } from "../errors";
import { calculateScrollCenter } from "../scene";
import { decodeSvgBase64Payload } from "../scene/export";
-import { bytesToHexString, isPromiseLike } from "../utils";
import { base64ToString, stringToBase64, toByteString } from "./encode";
import { nativeFileSystemSupported } from "./filesystem";
import { isValidExcalidrawData, isValidLibrary } from "./json";
import { restore, restoreLibraryItems } from "./restore";
-import type { FileSystemHandle } from "./filesystem";
-import type { ExcalidrawElement, FileId } from "../element/types";
import type { AppState, DataURL, LibraryItem } from "../types";
-import type { ValueOf } from "../utility-types";
+
+import type { FileSystemHandle } from "./filesystem";
import type { ImportedLibraryData } from "./types";
const parseFileContents = async (blob: Blob | File): Promise => {
diff --git a/packages/excalidraw/data/encryption.ts b/packages/excalidraw/data/encryption.ts
index a796d05b4..ee1a88f35 100644
--- a/packages/excalidraw/data/encryption.ts
+++ b/packages/excalidraw/data/encryption.ts
@@ -1,4 +1,4 @@
-import { ENCRYPTION_KEY_BITS } from "../constants";
+import { ENCRYPTION_KEY_BITS } from "@excalidraw/common";
import { blobToArrayBuffer } from "./blob";
diff --git a/packages/excalidraw/data/filesystem.ts b/packages/excalidraw/data/filesystem.ts
index 0bdab68fd..0f4ae745f 100644
--- a/packages/excalidraw/data/filesystem.ts
+++ b/packages/excalidraw/data/filesystem.ts
@@ -4,9 +4,9 @@ import {
supported as nativeFileSystemSupported,
} from "browser-fs-access";
-import { EVENT, MIME_TYPES } from "../constants";
+import { EVENT, MIME_TYPES, debounce } from "@excalidraw/common";
+
import { AbortError } from "../errors";
-import { debounce } from "../utils";
import type { FileSystemHandle } from "browser-fs-access";
diff --git a/packages/excalidraw/data/image.ts b/packages/excalidraw/data/image.ts
index e54f7bab5..c9c84c95b 100644
--- a/packages/excalidraw/data/image.ts
+++ b/packages/excalidraw/data/image.ts
@@ -2,7 +2,7 @@ import tEXt from "png-chunk-text";
import encodePng from "png-chunks-encode";
import decodePng from "png-chunks-extract";
-import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
+import { EXPORT_DATA_TYPES, MIME_TYPES } from "@excalidraw/common";
import { blobToArrayBuffer } from "./blob";
import { encode, decode } from "./encode";
diff --git a/packages/excalidraw/data/index.ts b/packages/excalidraw/data/index.ts
index 013ac5b75..ac8147e85 100644
--- a/packages/excalidraw/data/index.ts
+++ b/packages/excalidraw/data/index.ts
@@ -1,32 +1,39 @@
-import {
- copyBlobToClipboardAsPng,
- copyTextToSystemClipboard,
-} from "../clipboard";
import {
DEFAULT_EXPORT_PADDING,
DEFAULT_FILENAME,
IMAGE_MIME_TYPES,
isFirefox,
MIME_TYPES,
-} from "../constants";
-import { getNonDeletedElements } from "../element";
-import { isFrameLikeElement } from "../element/typeChecks";
-import { getElementsOverlappingFrame } from "../frame";
+ cloneJSON,
+} from "@excalidraw/common";
+
+import { getNonDeletedElements } from "@excalidraw/element";
+
+import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
+
+import { getElementsOverlappingFrame } from "@excalidraw/element/frame";
+
+import type {
+ ExcalidrawElement,
+ ExcalidrawFrameLikeElement,
+ NonDeletedExcalidrawElement,
+} from "@excalidraw/element/types";
+
+import {
+ copyBlobToClipboardAsPng,
+ copyTextToSystemClipboard,
+} from "../clipboard";
+
import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas, exportToSvg } from "../scene/export";
-import { cloneJSON } from "../utils";
import { canvasToBlob } from "./blob";
import { fileSave } from "./filesystem";
import { serializeAsJSON } from "./json";
import type { FileSystemHandle } from "./filesystem";
-import type {
- ExcalidrawElement,
- ExcalidrawFrameLikeElement,
- NonDeletedExcalidrawElement,
-} from "../element/types";
+
import type { ExportType } from "../scene/types";
import type { AppState, BinaryFiles } from "../types";
diff --git a/packages/excalidraw/data/json.ts b/packages/excalidraw/data/json.ts
index c1cdd4f92..527c9e56e 100644
--- a/packages/excalidraw/data/json.ts
+++ b/packages/excalidraw/data/json.ts
@@ -1,17 +1,23 @@
-import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import {
DEFAULT_FILENAME,
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
MIME_TYPES,
VERSIONS,
-} from "../constants";
-import { clearElementsForDatabase, clearElementsForExport } from "../element";
+} from "@excalidraw/common";
+
+import {
+ clearElementsForDatabase,
+ clearElementsForExport,
+} from "@excalidraw/element";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import { isImageFileHandle, loadFromBlob, normalizeFile } from "./blob";
import { fileOpen, fileSave } from "./filesystem";
-import type { ExcalidrawElement } from "../element/types";
import type { AppState, BinaryFiles, LibraryItems } from "../types";
import type {
ExportedDataState,
diff --git a/packages/excalidraw/data/library.ts b/packages/excalidraw/data/library.ts
index 76424f54a..74252657e 100644
--- a/packages/excalidraw/data/library.ts
+++ b/packages/excalidraw/data/library.ts
@@ -7,29 +7,35 @@ import {
EVENT,
DEFAULT_SIDEBAR,
LIBRARY_SIDEBAR_TAB,
-} from "../constants";
-import { atom, editorJotaiStore } from "../editor-jotai";
-import { hashElementsVersion, hashString } from "../element";
-import { getCommonBoundingBox } from "../element/bounds";
-import { Emitter } from "../emitter";
-import { AbortError } from "../errors";
-import { libraryItemSvgsCache } from "../hooks/useLibraryItemSvg";
-import { t } from "../i18n";
-import { Queue } from "../queue";
-import {
arrayToMap,
cloneJSON,
preventUnload,
promiseTry,
resolvablePromise,
-} from "../utils";
+ toValidURL,
+ Queue,
+} from "@excalidraw/common";
+
+import { hashElementsVersion, hashString } from "@excalidraw/element";
+
+import { getCommonBoundingBox } from "@excalidraw/element/bounds";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import type { MaybePromise } from "@excalidraw/common/utility-types";
+
+import { atom, editorJotaiStore } from "../editor-jotai";
+
+import { Emitter } from "../emitter";
+import { AbortError } from "../errors";
+import { libraryItemSvgsCache } from "../hooks/useLibraryItemSvg";
+import { t } from "../i18n";
import { loadLibraryFromBlob } from "./blob";
import { restoreLibraryItems } from "./restore";
-import { toValidURL } from "./url";
import type App from "../components/App";
-import type { ExcalidrawElement } from "../element/types";
+
import type {
LibraryItems,
LibraryItem,
@@ -37,7 +43,6 @@ import type {
LibraryItemsSource,
LibraryItems_anyVersion,
} from "../types";
-import type { MaybePromise } from "../utility-types";
/**
* format: hostname or hostname/pathname
diff --git a/packages/excalidraw/data/reconcile.ts b/packages/excalidraw/data/reconcile.ts
index ef644c3aa..a69ee2dee 100644
--- a/packages/excalidraw/data/reconcile.ts
+++ b/packages/excalidraw/data/reconcile.ts
@@ -1,16 +1,18 @@
import throttle from "lodash.throttle";
-import { ENV } from "../constants";
+import { arrayToMap, isDevEnv, isTestEnv } from "@excalidraw/common";
+
import {
orderByFractionalIndex,
syncInvalidIndices,
validateFractionalIndices,
-} from "../fractionalIndex";
-import { arrayToMap } from "../utils";
+} from "@excalidraw/element/fractionalIndex";
+
+import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
+
+import type { MakeBrand } from "@excalidraw/common/utility-types";
-import type { OrderedExcalidrawElement } from "../element/types";
import type { AppState } from "../types";
-import type { MakeBrand } from "../utility-types";
export type ReconciledExcalidrawElement = OrderedExcalidrawElement &
MakeBrand<"ReconciledElement">;
@@ -47,11 +49,7 @@ const validateIndicesThrottled = throttle(
localElements: readonly OrderedExcalidrawElement[],
remoteElements: readonly RemoteExcalidrawElement[],
) => {
- if (
- import.meta.env.DEV ||
- import.meta.env.MODE === ENV.TEST ||
- window?.DEBUG_FRACTIONAL_INDICES
- ) {
+ if (isDevEnv() || isTestEnv() || window?.DEBUG_FRACTIONAL_INDICES) {
// create new instances due to the mutation
const elements = syncInvalidIndices(
orderedElements.map((x) => ({ ...x })),
@@ -59,7 +57,7 @@ const validateIndicesThrottled = throttle(
validateFractionalIndices(elements, {
// throw in dev & test only, to remain functional on `DEBUG_FRACTIONAL_INDICES`
- shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
+ shouldThrow: isTestEnv() || isDevEnv(),
includeBoundTextValidation: true,
reconciliationContext: {
localElements,
diff --git a/packages/excalidraw/data/resave.ts b/packages/excalidraw/data/resave.ts
index 2c448429a..188041d69 100644
--- a/packages/excalidraw/data/resave.ts
+++ b/packages/excalidraw/data/resave.ts
@@ -1,8 +1,9 @@
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import { getFileHandleType, isImageFileHandleType } from "./blob";
import { exportCanvas, prepareElementsForExport } from ".";
-import type { ExcalidrawElement } from "../element/types";
import type { AppState, BinaryFiles } from "../types";
export const resaveAsImageWithScene = async (
diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts
index df1a64621..4f050c922 100644
--- a/packages/excalidraw/data/restore.ts
+++ b/packages/excalidraw/data/restore.ts
@@ -1,8 +1,5 @@
import { isFiniteNumber, pointFrom } from "@excalidraw/math";
-import type { LocalPoint, Radians } from "@excalidraw/math";
-
-import { getDefaultAppState } from "../appState";
import {
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
@@ -13,22 +10,24 @@ import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_GRID_SIZE,
DEFAULT_GRID_STEP,
-} from "../constants";
-import {
- getNonDeletedElements,
- getNormalizedDimensions,
- isInvisiblySmallElement,
- refreshTextDimensions,
-} from "../element";
-import { normalizeFixedPoint } from "../element/binding";
+ randomId,
+ getUpdatedTimestamp,
+ updateActiveTool,
+ arrayToMap,
+ getSizeFromPoints,
+ normalizeLink,
+ getLineHeight,
+} from "@excalidraw/common";
+import { getNonDeletedElements } from "@excalidraw/element";
+import { normalizeFixedPoint } from "@excalidraw/element/binding";
import {
updateElbowArrowPoints,
validateElbowPoints,
-} from "../element/elbowArrow";
-import { LinearElementEditor } from "../element/linearElementEditor";
-import { bumpVersion } from "../element/mutateElement";
-import { getContainerElement } from "../element/textElement";
-import { detectLineHeight } from "../element/textMeasurements";
+} from "@excalidraw/element/elbowArrow";
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+import { bumpVersion } from "@excalidraw/element/mutateElement";
+import { getContainerElement } from "@excalidraw/element/textElement";
+import { detectLineHeight } from "@excalidraw/element/textMeasurements";
import {
isArrowElement,
isElbowArrow,
@@ -36,20 +35,17 @@ import {
isLinearElement,
isTextElement,
isUsingAdaptiveRadius,
-} from "../element/typeChecks";
-import { getLineHeight } from "../fonts";
-import { syncInvalidIndices } from "../fractionalIndex";
-import { randomId } from "../random";
-import {
- getNormalizedGridSize,
- getNormalizedGridStep,
- getNormalizedZoom,
-} from "../scene";
-import { getUpdatedTimestamp, updateActiveTool } from "../utils";
-import { arrayToMap } from "../utils";
-import { getSizeFromPoints } from "../points";
+} from "@excalidraw/element/typeChecks";
-import { normalizeLink } from "./url";
+import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
+
+import { refreshTextDimensions } from "@excalidraw/element/newElement";
+
+import { getNormalizedDimensions } from "@excalidraw/element/sizeHelpers";
+
+import { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
+
+import type { LocalPoint, Radians } from "@excalidraw/math";
import type {
ExcalidrawArrowElement,
@@ -65,9 +61,19 @@ import type {
OrderedExcalidrawElement,
PointBinding,
StrokeRoundness,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import type { MarkOptional, Mutable } from "@excalidraw/common/utility-types";
+
+import { getDefaultAppState } from "../appState";
+
+import {
+ getNormalizedGridSize,
+ getNormalizedGridStep,
+ getNormalizedZoom,
+} from "../scene";
+
import type { AppState, BinaryFiles, LibraryItem } from "../types";
-import type { MarkOptional, Mutable } from "../utility-types";
import type { ImportedDataState, LegacyAppState } from "./types";
type RestoredAppState = Omit<
@@ -80,6 +86,7 @@ export const AllowedExcalidrawActiveTools: Record<
boolean
> = {
selection: true,
+ lasso: true,
text: true,
rectangle: true,
diamond: true,
@@ -215,7 +222,7 @@ const restoreElementWithProperties = <
"customData" in extra ? extra.customData : element.customData;
}
- return {
+ const ret = {
// spread the original element properties to not lose unknown ones
// for forward-compatibility
...element,
@@ -224,6 +231,12 @@ const restoreElementWithProperties = <
...getNormalizedDimensions(base),
...extra,
} as unknown as T;
+
+ // strip legacy props (migrated in previous steps)
+ delete ret.strokeSharpness;
+ delete ret.boundElementIds;
+
+ return ret;
};
const restoreElement = (
diff --git a/packages/excalidraw/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts
index 94f7b6c82..0b0718e8e 100644
--- a/packages/excalidraw/data/transform.test.ts
+++ b/packages/excalidraw/data/transform.test.ts
@@ -1,10 +1,11 @@
import { pointFrom } from "@excalidraw/math";
import { vi } from "vitest";
+import type { ExcalidrawArrowElement } from "@excalidraw/element/types";
+
import { convertToExcalidrawElements } from "./transform";
import type { ExcalidrawElementSkeleton } from "./transform";
-import type { ExcalidrawArrowElement } from "../element/types";
const opts = { regenerateIds: false };
@@ -426,7 +427,7 @@ describe("Test Transform", () => {
const [arrow, text, rectangle, ellipse] = excalidrawElements;
expect(arrow).toMatchObject({
type: "arrow",
- x: 255,
+ x: 255.5,
y: 239,
boundElements: [{ id: text.id, type: "text" }],
startBinding: {
@@ -511,7 +512,7 @@ describe("Test Transform", () => {
expect(arrow).toMatchObject({
type: "arrow",
- x: 255,
+ x: 255.5,
y: 239,
boundElements: [{ id: text1.id, type: "text" }],
startBinding: {
@@ -729,7 +730,7 @@ describe("Test Transform", () => {
const [, , arrow, text] = excalidrawElements;
expect(arrow).toMatchObject({
type: "arrow",
- x: 255,
+ x: 255.5,
y: 239,
boundElements: [
{
diff --git a/packages/excalidraw/data/transform.ts b/packages/excalidraw/data/transform.ts
index 704b63cb5..15ad1ffde 100644
--- a/packages/excalidraw/data/transform.ts
+++ b/packages/excalidraw/data/transform.ts
@@ -5,37 +5,41 @@ import {
DEFAULT_FONT_SIZE,
TEXT_ALIGN,
VERTICAL_ALIGN,
-} from "../constants";
-import {
- getCommonBounds,
- newElement,
- newLinearElement,
- redrawTextBoundingBox,
-} from "../element";
-import { bindLinearElement } from "../element/binding";
-import {
- newArrowElement,
- newFrameElement,
- newImageElement,
- newMagicFrameElement,
- newTextElement,
-} from "../element/newElement";
-import { measureText, normalizeText } from "../element/textMeasurements";
-import { isArrowElement } from "../element/typeChecks";
-import { getLineHeight } from "../fonts";
-import { syncInvalidIndices } from "../fractionalIndex";
-import { getSizeFromPoints } from "../points";
-import { randomId } from "../random";
-import {
+ getSizeFromPoints,
+ randomId,
arrayToMap,
assertNever,
cloneJSON,
getFontString,
isDevEnv,
toBrandedType,
-} from "../utils";
+ getLineHeight,
+} from "@excalidraw/common";
+
+import { bindLinearElement } from "@excalidraw/element/binding";
+import {
+ newArrowElement,
+ newElement,
+ newFrameElement,
+ newImageElement,
+ newLinearElement,
+ newMagicFrameElement,
+ newTextElement,
+} from "@excalidraw/element/newElement";
+import {
+ measureText,
+ normalizeText,
+} from "@excalidraw/element/textMeasurements";
+import { isArrowElement } from "@excalidraw/element/typeChecks";
+
+import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
+
+import { redrawTextBoundingBox } from "@excalidraw/element/textElement";
+
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+
+import type { ElementConstructorOpts } from "@excalidraw/element/newElement";
-import type { ElementConstructorOpts } from "../element/newElement";
import type {
ElementsMap,
ExcalidrawArrowElement,
@@ -55,8 +59,11 @@ import type {
NonDeletedSceneElementsMap,
TextAlign,
VerticalAlign,
-} from "../element/types";
-import type { MarkOptional } from "../utility-types";
+} from "@excalidraw/element/types";
+
+import type { MarkOptional } from "@excalidraw/common/utility-types";
+
+import { getCommonBounds } from "..";
export type ValidLinearElement = {
type: "arrow" | "line";
@@ -458,7 +465,13 @@ const bindLinearElementToElement = (
newPoints[endPointIndex][1] += delta;
}
- Object.assign(linearElement, { points: newPoints });
+ Object.assign(
+ linearElement,
+ LinearElementEditor.getNormalizedPoints({
+ ...linearElement,
+ points: newPoints,
+ }),
+ );
return {
linearElement,
diff --git a/packages/excalidraw/data/types.ts b/packages/excalidraw/data/types.ts
index de3436137..6878b81b1 100644
--- a/packages/excalidraw/data/types.ts
+++ b/packages/excalidraw/data/types.ts
@@ -1,6 +1,8 @@
+import type { VERSIONS } from "@excalidraw/common";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
import type { cleanAppStateForExport } from "../appState";
-import type { VERSIONS } from "../constants";
-import type { ExcalidrawElement } from "../element/types";
import type {
AppState,
BinaryFiles,
diff --git a/packages/excalidraw/element/heading.ts b/packages/excalidraw/element/heading.ts
deleted file mode 100644
index ddebeca53..000000000
--- a/packages/excalidraw/element/heading.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-import {
- pointFrom,
- pointRotateRads,
- pointScaleFromOrigin,
- radiansToDegrees,
- triangleIncludesPoint,
- vectorFromPoint,
-} from "@excalidraw/math";
-
-import type {
- LocalPoint,
- GlobalPoint,
- Triangle,
- Vector,
- Radians,
-} from "@excalidraw/math";
-
-import { getCenterForBounds, type Bounds } from "./bounds";
-
-import type { ExcalidrawBindableElement } from "./types";
-
-export const HEADING_RIGHT = [1, 0] as Heading;
-export const HEADING_DOWN = [0, 1] as Heading;
-export const HEADING_LEFT = [-1, 0] as Heading;
-export const HEADING_UP = [0, -1] as Heading;
-export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
-
-export const headingForDiamond = (
- a: Point,
- b: Point,
-) => {
- const angle = radiansToDegrees(
- Math.atan2(b[1] - a[1], b[0] - a[0]) as Radians,
- );
- if (angle >= 315 || angle < 45) {
- return HEADING_UP;
- } else if (angle >= 45 && angle < 135) {
- return HEADING_RIGHT;
- } else if (angle >= 135 && angle < 225) {
- return HEADING_DOWN;
- }
- return HEADING_LEFT;
-};
-
-export const vectorToHeading = (vec: Vector): Heading => {
- const [x, y] = vec;
- const absX = Math.abs(x);
- const absY = Math.abs(y);
- if (x > absY) {
- return HEADING_RIGHT;
- } else if (x <= -absY) {
- return HEADING_LEFT;
- } else if (y > absX) {
- return HEADING_DOWN;
- }
- return HEADING_UP;
-};
-
-export const headingForPoint = (
- p: P,
- o: P,
-) => vectorToHeading(vectorFromPoint
(p, o));
-
-export const headingForPointIsHorizontal =
(
- p: P,
- o: P,
-) => headingIsHorizontal(headingForPoint
(p, o));
-
-export const compareHeading = (a: Heading, b: Heading) =>
- a[0] === b[0] && a[1] === b[1];
-
-export const headingIsHorizontal = (a: Heading) =>
- compareHeading(a, HEADING_RIGHT) || compareHeading(a, HEADING_LEFT);
-
-export const headingIsVertical = (a: Heading) => !headingIsHorizontal(a);
-
-// Gets the heading for the point by creating a bounding box around the rotated
-// close fitting bounding box, then creating 4 search cones around the center of
-// the external bbox.
-export const headingForPointFromElement = <
- Point extends GlobalPoint | LocalPoint,
->(
- element: Readonly,
- aabb: Readonly,
- p: Readonly,
-): Heading => {
- const SEARCH_CONE_MULTIPLIER = 2;
-
- const midPoint = getCenterForBounds(aabb);
-
- if (element.type === "diamond") {
- if (p[0] < element.x) {
- return HEADING_LEFT;
- } else if (p[1] < element.y) {
- return HEADING_UP;
- } else if (p[0] > element.x + element.width) {
- return HEADING_RIGHT;
- } else if (p[1] > element.y + element.height) {
- return HEADING_DOWN;
- }
-
- const top = pointRotateRads(
- pointScaleFromOrigin(
- pointFrom(element.x + element.width / 2, element.y),
- midPoint,
- SEARCH_CONE_MULTIPLIER,
- ),
- midPoint,
- element.angle,
- );
- const right = pointRotateRads(
- pointScaleFromOrigin(
- pointFrom(element.x + element.width, element.y + element.height / 2),
- midPoint,
- SEARCH_CONE_MULTIPLIER,
- ),
- midPoint,
- element.angle,
- );
- const bottom = pointRotateRads(
- pointScaleFromOrigin(
- pointFrom(element.x + element.width / 2, element.y + element.height),
- midPoint,
- SEARCH_CONE_MULTIPLIER,
- ),
- midPoint,
- element.angle,
- );
- const left = pointRotateRads(
- pointScaleFromOrigin(
- pointFrom(element.x, element.y + element.height / 2),
- midPoint,
- SEARCH_CONE_MULTIPLIER,
- ),
- midPoint,
- element.angle,
- );
-
- if (
- triangleIncludesPoint([top, right, midPoint] as Triangle, p)
- ) {
- return headingForDiamond(top, right);
- } else if (
- triangleIncludesPoint(
- [right, bottom, midPoint] as Triangle,
- p,
- )
- ) {
- return headingForDiamond(right, bottom);
- } else if (
- triangleIncludesPoint(
- [bottom, left, midPoint] as Triangle,
- p,
- )
- ) {
- return headingForDiamond(bottom, left);
- }
-
- return headingForDiamond(left, top);
- }
-
- const topLeft = pointScaleFromOrigin(
- pointFrom(aabb[0], aabb[1]),
- midPoint,
- SEARCH_CONE_MULTIPLIER,
- ) as Point;
- const topRight = pointScaleFromOrigin(
- pointFrom(aabb[2], aabb[1]),
- midPoint,
- SEARCH_CONE_MULTIPLIER,
- ) as Point;
- const bottomLeft = pointScaleFromOrigin(
- pointFrom(aabb[0], aabb[3]),
- midPoint,
- SEARCH_CONE_MULTIPLIER,
- ) as Point;
- const bottomRight = pointScaleFromOrigin(
- pointFrom(aabb[2], aabb[3]),
- midPoint,
- SEARCH_CONE_MULTIPLIER,
- ) as Point;
-
- return triangleIncludesPoint(
- [topLeft, topRight, midPoint] as Triangle,
- p,
- )
- ? HEADING_UP
- : triangleIncludesPoint(
- [topRight, bottomRight, midPoint] as Triangle,
- p,
- )
- ? HEADING_RIGHT
- : triangleIncludesPoint(
- [bottomRight, bottomLeft, midPoint] as Triangle,
- p,
- )
- ? HEADING_DOWN
- : HEADING_LEFT;
-};
-
-export const flipHeading = (h: Heading): Heading =>
- [
- h[0] === 0 ? 0 : h[0] > 0 ? -1 : 1,
- h[1] === 0 ? 0 : h[1] > 0 ? -1 : 1,
- ] as Heading;
diff --git a/packages/excalidraw/element/newElement.test.ts b/packages/excalidraw/element/newElement.test.ts
deleted file mode 100644
index 418ede1be..000000000
--- a/packages/excalidraw/element/newElement.test.ts
+++ /dev/null
@@ -1,377 +0,0 @@
-import { pointFrom } from "@excalidraw/math";
-
-import type { LocalPoint } from "@excalidraw/math";
-
-import { FONT_FAMILY, ROUNDNESS } from "../constants";
-import { API } from "../tests/helpers/api";
-import { isPrimitive } from "../utils";
-
-import { mutateElement } from "./mutateElement";
-import { duplicateElement, duplicateElements } from "./newElement";
-
-import type { ExcalidrawLinearElement } from "./types";
-
-const assertCloneObjects = (source: any, clone: any) => {
- for (const key in clone) {
- if (clone.hasOwnProperty(key) && !isPrimitive(clone[key])) {
- expect(clone[key]).not.toBe(source[key]);
- if (source[key]) {
- assertCloneObjects(source[key], clone[key]);
- }
- }
- }
-};
-
-describe("duplicating single elements", () => {
- it("clones arrow element", () => {
- const element = API.createElement({
- type: "arrow",
- x: 0,
- y: 0,
- strokeColor: "#000000",
- backgroundColor: "transparent",
- fillStyle: "hachure",
- strokeWidth: 1,
- strokeStyle: "solid",
- roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
- roughness: 1,
- opacity: 100,
- });
-
- // @ts-ignore
- element.__proto__ = { hello: "world" };
-
- mutateElement(element, {
- points: [pointFrom(1, 2), pointFrom(3, 4)],
- });
-
- const copy = duplicateElement(null, new Map(), element);
-
- assertCloneObjects(element, copy);
-
- // assert we clone the object's prototype
- // @ts-ignore
- expect(copy.__proto__).toEqual({ hello: "world" });
- expect(copy.hasOwnProperty("hello")).toBe(false);
-
- expect(copy.points).not.toBe(element.points);
- expect(copy).not.toHaveProperty("shape");
- expect(copy.id).not.toBe(element.id);
- expect(typeof copy.id).toBe("string");
- expect(copy.seed).not.toBe(element.seed);
- expect(typeof copy.seed).toBe("number");
- expect(copy).toEqual({
- ...element,
- id: copy.id,
- seed: copy.seed,
- });
- });
-
- it("clones text element", () => {
- const element = API.createElement({
- type: "text",
- x: 0,
- y: 0,
- strokeColor: "#000000",
- backgroundColor: "transparent",
- fillStyle: "hachure",
- strokeWidth: 1,
- strokeStyle: "solid",
- roundness: null,
- roughness: 1,
- opacity: 100,
- text: "hello",
- fontSize: 20,
- fontFamily: FONT_FAMILY.Virgil,
- textAlign: "left",
- verticalAlign: "top",
- });
-
- const copy = duplicateElement(null, new Map(), element);
-
- assertCloneObjects(element, copy);
-
- expect(copy).not.toHaveProperty("points");
- expect(copy).not.toHaveProperty("shape");
- expect(copy.id).not.toBe(element.id);
- expect(typeof copy.id).toBe("string");
- expect(typeof copy.seed).toBe("number");
- });
-});
-
-describe("duplicating multiple elements", () => {
- it("duplicateElements should clone bindings", () => {
- const rectangle1 = API.createElement({
- type: "rectangle",
- id: "rectangle1",
- boundElements: [
- { id: "arrow1", type: "arrow" },
- { id: "arrow2", type: "arrow" },
- { id: "text1", type: "text" },
- ],
- });
-
- const text1 = API.createElement({
- type: "text",
- id: "text1",
- containerId: "rectangle1",
- });
-
- const arrow1 = API.createElement({
- type: "arrow",
- id: "arrow1",
- startBinding: {
- elementId: "rectangle1",
- focus: 0.2,
- gap: 7,
- fixedPoint: [0.5, 1],
- },
- });
-
- const arrow2 = API.createElement({
- type: "arrow",
- id: "arrow2",
- endBinding: {
- elementId: "rectangle1",
- focus: 0.2,
- gap: 7,
- fixedPoint: [0.5, 1],
- },
- boundElements: [{ id: "text2", type: "text" }],
- });
-
- const text2 = API.createElement({
- type: "text",
- id: "text2",
- containerId: "arrow2",
- });
-
- // -------------------------------------------------------------------------
-
- const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
- const clonedElements = duplicateElements(origElements);
-
- // generic id in-equality checks
- // --------------------------------------------------------------------------
- expect(origElements.map((e) => e.type)).toEqual(
- clonedElements.map((e) => e.type),
- );
- origElements.forEach((origElement, idx) => {
- const clonedElement = clonedElements[idx];
- expect(origElement).toEqual(
- expect.objectContaining({
- id: expect.not.stringMatching(clonedElement.id),
- type: clonedElement.type,
- }),
- );
- if ("containerId" in origElement) {
- expect(origElement.containerId).not.toBe(
- (clonedElement as any).containerId,
- );
- }
- if ("endBinding" in origElement) {
- if (origElement.endBinding) {
- expect(origElement.endBinding.elementId).not.toBe(
- (clonedElement as any).endBinding?.elementId,
- );
- } else {
- expect((clonedElement as any).endBinding).toBeNull();
- }
- }
- if ("startBinding" in origElement) {
- if (origElement.startBinding) {
- expect(origElement.startBinding.elementId).not.toBe(
- (clonedElement as any).startBinding?.elementId,
- );
- } else {
- expect((clonedElement as any).startBinding).toBeNull();
- }
- }
- });
- // --------------------------------------------------------------------------
-
- const clonedArrows = clonedElements.filter(
- (e) => e.type === "arrow",
- ) as ExcalidrawLinearElement[];
-
- const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
- clonedElements as any as typeof origElements;
-
- expect(clonedText1.containerId).toBe(clonedRectangle.id);
- expect(
- clonedRectangle.boundElements!.find((e) => e.id === clonedText1.id),
- ).toEqual(
- expect.objectContaining({
- id: clonedText1.id,
- type: clonedText1.type,
- }),
- );
-
- clonedArrows.forEach((arrow) => {
- expect(
- clonedRectangle.boundElements!.find((e) => e.id === arrow.id),
- ).toEqual(
- expect.objectContaining({
- id: arrow.id,
- type: arrow.type,
- }),
- );
-
- if (arrow.endBinding) {
- expect(arrow.endBinding.elementId).toBe(clonedRectangle.id);
- }
- if (arrow.startBinding) {
- expect(arrow.startBinding.elementId).toBe(clonedRectangle.id);
- }
- });
-
- expect(clonedArrow2.boundElements).toEqual([
- { type: "text", id: clonedArrowLabel.id },
- ]);
- expect(clonedArrowLabel.containerId).toBe(clonedArrow2.id);
- });
-
- it("should remove id references of elements that aren't found", () => {
- const rectangle1 = API.createElement({
- type: "rectangle",
- id: "rectangle1",
- boundElements: [
- // should keep
- { id: "arrow1", type: "arrow" },
- // should drop
- { id: "arrow-not-exists", type: "arrow" },
- // should drop
- { id: "text-not-exists", type: "text" },
- ],
- });
-
- const arrow1 = API.createElement({
- type: "arrow",
- id: "arrow1",
- startBinding: {
- elementId: "rectangle1",
- focus: 0.2,
- gap: 7,
- fixedPoint: [0.5, 1],
- },
- });
-
- const text1 = API.createElement({
- type: "text",
- id: "text1",
- containerId: "rectangle-not-exists",
- });
-
- const arrow2 = API.createElement({
- type: "arrow",
- id: "arrow2",
- startBinding: {
- elementId: "rectangle1",
- focus: 0.2,
- gap: 7,
- fixedPoint: [0.5, 1],
- },
- endBinding: {
- elementId: "rectangle-not-exists",
- focus: 0.2,
- gap: 7,
- fixedPoint: [0.5, 1],
- },
- });
-
- const arrow3 = API.createElement({
- type: "arrow",
- id: "arrow2",
- startBinding: {
- elementId: "rectangle-not-exists",
- focus: 0.2,
- gap: 7,
- fixedPoint: [0.5, 1],
- },
- endBinding: {
- elementId: "rectangle1",
- focus: 0.2,
- gap: 7,
- fixedPoint: [0.5, 1],
- },
- });
-
- // -------------------------------------------------------------------------
-
- const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
- const clonedElements = duplicateElements(
- origElements,
- ) as any as typeof origElements;
- const [
- clonedRectangle,
- clonedText1,
- clonedArrow1,
- clonedArrow2,
- clonedArrow3,
- ] = clonedElements;
-
- expect(clonedRectangle.boundElements).toEqual([
- { id: clonedArrow1.id, type: "arrow" },
- ]);
-
- expect(clonedText1.containerId).toBe(null);
-
- expect(clonedArrow2.startBinding).toEqual({
- ...arrow2.startBinding,
- elementId: clonedRectangle.id,
- });
- expect(clonedArrow2.endBinding).toBe(null);
-
- expect(clonedArrow3.startBinding).toBe(null);
- expect(clonedArrow3.endBinding).toEqual({
- ...arrow3.endBinding,
- elementId: clonedRectangle.id,
- });
- });
-
- describe("should duplicate all group ids", () => {
- it("should regenerate all group ids and keep them consistent across elements", () => {
- const rectangle1 = API.createElement({
- type: "rectangle",
- groupIds: ["g1"],
- });
- const rectangle2 = API.createElement({
- type: "rectangle",
- groupIds: ["g2", "g1"],
- });
- const rectangle3 = API.createElement({
- type: "rectangle",
- groupIds: ["g2", "g1"],
- });
-
- const origElements = [rectangle1, rectangle2, rectangle3] as const;
- const clonedElements = duplicateElements(
- origElements,
- ) as any as typeof origElements;
- const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
- clonedElements;
-
- expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
- expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
- expect(rectangle2.groupIds[1]).not.toBe(clonedRectangle2.groupIds[1]);
-
- expect(clonedRectangle1.groupIds[0]).toBe(clonedRectangle2.groupIds[1]);
- expect(clonedRectangle2.groupIds[0]).toBe(clonedRectangle3.groupIds[0]);
- expect(clonedRectangle2.groupIds[1]).toBe(clonedRectangle3.groupIds[1]);
- });
-
- it("should keep and regenerate ids of groups even if invalid", () => {
- // lone element shouldn't be able to be grouped with itself,
- // but hard to check against in a performant way so we ignore it
- const rectangle1 = API.createElement({
- type: "rectangle",
- groupIds: ["g1"],
- });
-
- const [clonedRectangle1] = duplicateElements([rectangle1]);
-
- expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
- expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
- });
- });
-});
diff --git a/packages/excalidraw/errors.ts b/packages/excalidraw/errors.ts
index d6091b0e9..20fe694cd 100644
--- a/packages/excalidraw/errors.ts
+++ b/packages/excalidraw/errors.ts
@@ -33,10 +33,6 @@ export class ImageSceneDataError extends Error {
}
}
-export class InvalidFractionalIndexError extends Error {
- public code = "ELEMENT_HAS_INVALID_INDEX" as const;
-}
-
type WorkerErrorCodes = "WORKER_URL_NOT_DEFINED" | "WORKER_IN_THE_MAIN_CHUNK";
export class WorkerUrlNotDefinedError extends Error {
diff --git a/packages/excalidraw/fonts/Emoji/index.ts b/packages/excalidraw/fonts/Emoji/index.ts
index 323d075cb..09aea663d 100644
--- a/packages/excalidraw/fonts/Emoji/index.ts
+++ b/packages/excalidraw/fonts/Emoji/index.ts
@@ -1,4 +1,5 @@
-import { LOCAL_FONT_PROTOCOL } from "../FontMetadata";
+import { LOCAL_FONT_PROTOCOL } from "@excalidraw/common";
+
import { type ExcalidrawFontFaceDescriptor } from "../Fonts";
export const EmojiFontFaces: ExcalidrawFontFaceDescriptor[] = [
diff --git a/packages/excalidraw/fonts/ExcalidrawFontFace.ts b/packages/excalidraw/fonts/ExcalidrawFontFace.ts
index 615fef20f..46ea3d018 100644
--- a/packages/excalidraw/fonts/ExcalidrawFontFace.ts
+++ b/packages/excalidraw/fonts/ExcalidrawFontFace.ts
@@ -1,7 +1,6 @@
-import { subsetWoff2GlyphsByCodepoints } from "../subset/subset-main";
-import { promiseTry } from "../utils";
+import { promiseTry, LOCAL_FONT_PROTOCOL } from "@excalidraw/common";
-import { LOCAL_FONT_PROTOCOL } from "./FontMetadata";
+import { subsetWoff2GlyphsByCodepoints } from "../subset/subset-main";
type DataURL = string;
diff --git a/packages/excalidraw/fonts/Fonts.ts b/packages/excalidraw/fonts/Fonts.ts
index c1b94bdef..79b5ea1af 100644
--- a/packages/excalidraw/fonts/Fonts.ts
+++ b/packages/excalidraw/fonts/Fonts.ts
@@ -4,20 +4,35 @@ import {
CJK_HAND_DRAWN_FALLBACK_FONT,
WINDOWS_EMOJI_FALLBACK_FONT,
getFontFamilyFallbacks,
-} from "../constants";
-import { isTextElement } from "../element";
-import { getContainerElement } from "../element/textElement";
-import { charWidth } from "../element/textMeasurements";
-import { containsCJK } from "../element/textWrapping";
-import { ShapeCache } from "../scene/ShapeCache";
-import { getFontString, PromisePool, promiseTry } from "../utils";
+} from "@excalidraw/common";
+import { getContainerElement } from "@excalidraw/element/textElement";
+import { charWidth } from "@excalidraw/element/textMeasurements";
+import { containsCJK } from "@excalidraw/element/textWrapping";
+
+import {
+ FONT_METADATA,
+ type FontMetadata,
+ getFontString,
+ PromisePool,
+ promiseTry,
+} from "@excalidraw/common";
+
+import { ShapeCache } from "@excalidraw/element/ShapeCache";
+
+import { isTextElement } from "@excalidraw/element/typeChecks";
+
+import type {
+ ExcalidrawElement,
+ ExcalidrawTextElement,
+} from "@excalidraw/element/types";
+
+import type { ValueOf } from "@excalidraw/common/utility-types";
import { CascadiaFontFaces } from "./Cascadia";
import { ComicShannsFontFaces } from "./ComicShanns";
import { EmojiFontFaces } from "./Emoji";
import { ExcalidrawFontFace } from "./ExcalidrawFontFace";
import { ExcalifontFontFaces } from "./Excalifont";
-import { FONT_METADATA, type FontMetadata } from "./FontMetadata";
import { HelveticaFontFaces } from "./Helvetica";
import { LiberationFontFaces } from "./Liberation";
import { LilitaFontFaces } from "./Lilita";
@@ -25,13 +40,7 @@ import { NunitoFontFaces } from "./Nunito";
import { VirgilFontFaces } from "./Virgil";
import { XiaolaiFontFaces } from "./Xiaolai";
-import type {
- ExcalidrawElement,
- ExcalidrawTextElement,
- FontFamilyValues,
-} from "../element/types";
import type Scene from "../scene/Scene";
-import type { ValueOf } from "../utility-types";
export class Fonts {
// it's ok to track fonts across multiple instances only once, so let's use
@@ -454,37 +463,6 @@ export class Fonts {
}
}
-/**
- * Calculates vertical offset for a text with alphabetic baseline.
- */
-export const getVerticalOffset = (
- fontFamily: ExcalidrawTextElement["fontFamily"],
- fontSize: ExcalidrawTextElement["fontSize"],
- lineHeightPx: number,
-) => {
- const { unitsPerEm, ascender, descender } =
- Fonts.registered.get(fontFamily)?.metadata.metrics ||
- FONT_METADATA[FONT_FAMILY.Virgil].metrics;
-
- const fontSizeEm = fontSize / unitsPerEm;
- const lineGap =
- (lineHeightPx - fontSizeEm * ascender + fontSizeEm * descender) / 2;
-
- const verticalOffset = fontSizeEm * ascender + lineGap;
- return verticalOffset;
-};
-
-/**
- * Gets line height forr a selected family.
- */
-export const getLineHeight = (fontFamily: FontFamilyValues) => {
- const { lineHeight } =
- Fonts.registered.get(fontFamily)?.metadata.metrics ||
- FONT_METADATA[FONT_FAMILY.Excalifont].metrics;
-
- return lineHeight as ExcalidrawTextElement["lineHeight"];
-};
-
export interface ExcalidrawFontFaceDescriptor {
uri: string;
descriptors?: FontFaceDescriptors;
diff --git a/packages/excalidraw/fonts/Helvetica/index.ts b/packages/excalidraw/fonts/Helvetica/index.ts
index f13d15118..20a08e464 100644
--- a/packages/excalidraw/fonts/Helvetica/index.ts
+++ b/packages/excalidraw/fonts/Helvetica/index.ts
@@ -1,4 +1,5 @@
-import { LOCAL_FONT_PROTOCOL } from "../FontMetadata";
+import { LOCAL_FONT_PROTOCOL } from "@excalidraw/common";
+
import { type ExcalidrawFontFaceDescriptor } from "../Fonts";
export const HelveticaFontFaces: ExcalidrawFontFaceDescriptor[] = [
diff --git a/packages/excalidraw/fonts/Lilita/index.ts b/packages/excalidraw/fonts/Lilita/index.ts
index 37a5d6a5e..4641dcb0f 100644
--- a/packages/excalidraw/fonts/Lilita/index.ts
+++ b/packages/excalidraw/fonts/Lilita/index.ts
@@ -1,4 +1,5 @@
-import { GOOGLE_FONTS_RANGES } from "../FontMetadata";
+import { GOOGLE_FONTS_RANGES } from "@excalidraw/common";
+
import { type ExcalidrawFontFaceDescriptor } from "../Fonts";
import LilitaLatinExt from "./Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
diff --git a/packages/excalidraw/fonts/Nunito/index.ts b/packages/excalidraw/fonts/Nunito/index.ts
index 3b092b0d6..07fb757c8 100644
--- a/packages/excalidraw/fonts/Nunito/index.ts
+++ b/packages/excalidraw/fonts/Nunito/index.ts
@@ -1,4 +1,5 @@
-import { GOOGLE_FONTS_RANGES } from "../FontMetadata";
+import { GOOGLE_FONTS_RANGES } from "@excalidraw/common";
+
import { type ExcalidrawFontFaceDescriptor } from "../Fonts";
import Cyrilic from "./Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
diff --git a/packages/excalidraw/global.d.ts b/packages/excalidraw/global.d.ts
index 207265716..e9b6c3f96 100644
--- a/packages/excalidraw/global.d.ts
+++ b/packages/excalidraw/global.d.ts
@@ -2,9 +2,9 @@ interface Window {
ClipboardItem: any;
__EXCALIDRAW_SHA__: string | undefined;
EXCALIDRAW_ASSET_PATH: string | string[] | undefined;
- EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
DEBUG_FRACTIONAL_INDICES: boolean | undefined;
+ EXCALIDRAW_EXPORT_SOURCE: string;
gtag: Function;
sa_event: Function;
fathom: { trackEvent: Function };
diff --git a/packages/excalidraw/history.ts b/packages/excalidraw/history.ts
index 48ea012bd..0481c8411 100644
--- a/packages/excalidraw/history.ts
+++ b/packages/excalidraw/history.ts
@@ -1,7 +1,8 @@
+import type { SceneElementsMap } from "@excalidraw/element/types";
+
import { Emitter } from "./emitter";
import type { AppStateChange, ElementsChange } from "./change";
-import type { SceneElementsMap } from "./element/types";
import type { Snapshot } from "./store";
import type { AppState } from "./types";
diff --git a/packages/excalidraw/hooks/useCreatePortalContainer.ts b/packages/excalidraw/hooks/useCreatePortalContainer.ts
index b557d7e2f..fb0d24bc3 100644
--- a/packages/excalidraw/hooks/useCreatePortalContainer.ts
+++ b/packages/excalidraw/hooks/useCreatePortalContainer.ts
@@ -1,7 +1,8 @@
import { useState, useLayoutEffect } from "react";
+import { THEME } from "@excalidraw/common";
+
import { useDevice, useExcalidrawContainer } from "../components/App";
-import { THEME } from "../constants";
import { useUIAppState } from "../context/ui-appState";
export const useCreatePortalContainer = (opts?: {
diff --git a/packages/excalidraw/hooks/useLibraryItemSvg.ts b/packages/excalidraw/hooks/useLibraryItemSvg.ts
index a79aab5c5..ad423ab95 100644
--- a/packages/excalidraw/hooks/useLibraryItemSvg.ts
+++ b/packages/excalidraw/hooks/useLibraryItemSvg.ts
@@ -1,7 +1,8 @@
import { exportToSvg } from "@excalidraw/utils/export";
import { useEffect, useState } from "react";
-import { COLOR_PALETTE } from "../colors";
+import { COLOR_PALETTE } from "@excalidraw/common";
+
import { atom, useAtom } from "../editor-jotai";
import type { LibraryItem } from "../types";
diff --git a/packages/excalidraw/hooks/useOutsideClick.ts b/packages/excalidraw/hooks/useOutsideClick.ts
index 7ec2113c2..75d68f6e8 100644
--- a/packages/excalidraw/hooks/useOutsideClick.ts
+++ b/packages/excalidraw/hooks/useOutsideClick.ts
@@ -1,6 +1,6 @@
import { useEffect } from "react";
-import { EVENT } from "../constants";
+import { EVENT } from "@excalidraw/common";
export function useOutsideClick(
ref: React.RefObject,
diff --git a/packages/excalidraw/i18n.ts b/packages/excalidraw/i18n.ts
index 47bae5979..3d39893f4 100644
--- a/packages/excalidraw/i18n.ts
+++ b/packages/excalidraw/i18n.ts
@@ -1,9 +1,11 @@
+import { isDevEnv } from "@excalidraw/common";
+
+import type { NestedKeyOf } from "@excalidraw/common/utility-types";
+
import { useAtomValue, editorJotaiStore, atom } from "./editor-jotai";
import fallbackLangData from "./locales/en.json";
import percentages from "./locales/percentages.json";
-import type { NestedKeyOf } from "./utility-types";
-
const COMPLETION_THRESHOLD = 85;
export interface Language {
@@ -73,7 +75,7 @@ export const languages: Language[] = [
];
const TEST_LANG_CODE = "__test__";
-if (import.meta.env.DEV) {
+if (isDevEnv()) {
languages.unshift(
{ code: TEST_LANG_CODE, label: "test language" },
{
diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx
index f9f25b530..5ea746754 100644
--- a/packages/excalidraw/index.tsx
+++ b/packages/excalidraw/index.tsx
@@ -1,16 +1,16 @@
import React, { useEffect } from "react";
+import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common";
+
import App from "./components/App";
import { InitializeApp } from "./components/InitializeApp";
import Footer from "./components/footer/FooterCenter";
import LiveCollaborationTrigger from "./components/live-collaboration/LiveCollaborationTrigger";
import MainMenu from "./components/main-menu/MainMenu";
import WelcomeScreen from "./components/welcome-screen/WelcomeScreen";
-import { DEFAULT_UI_OPTIONS } from "./constants";
import { defaultLang } from "./i18n";
import { EditorJotaiProvider, editorJotaiStore } from "./editor-jotai";
import polyfill from "./polyfill";
-import { isShallowEqual } from "./utils";
import "./css/app.scss";
import "./css/styles.scss";
@@ -215,10 +215,12 @@ export {
getSceneVersion,
hashElementsVersion,
hashString,
- isInvisiblySmallElement,
getNonDeletedElements,
- getTextFromElements,
-} from "./element";
+} from "@excalidraw/element";
+
+export { getTextFromElements } from "@excalidraw/element/textElement";
+export { isInvisiblySmallElement } from "@excalidraw/element/sizeHelpers";
+
export { defaultLang, useI18n, languages } from "./i18n";
export {
restore,
@@ -242,9 +244,9 @@ export {
loadSceneOrLibraryFromBlob,
loadLibraryFromBlob,
} from "./data/blob";
-export { getFreeDrawSvgPath } from "./renderer/renderElement";
+export { getFreeDrawSvgPath } from "@excalidraw/element/renderElement";
export { mergeLibraryItems, getLibraryItemsHash } from "./data/library";
-export { isLinearElement } from "./element/typeChecks";
+export { isLinearElement } from "@excalidraw/element/typeChecks";
export {
FONT_FAMILY,
@@ -253,13 +255,14 @@ export {
ROUNDNESS,
DEFAULT_LASER_COLOR,
UserIdleState,
-} from "./constants";
+ normalizeLink,
+} from "@excalidraw/common";
export {
mutateElement,
newElementWith,
bumpVersion,
-} from "./element/mutateElement";
+} from "@excalidraw/element/mutateElement";
export { CaptureUpdateAction } from "./store";
@@ -268,7 +271,7 @@ export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library";
export {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
-} from "./utils";
+} from "@excalidraw/common";
export { Sidebar } from "./components/Sidebar/Sidebar";
export { Button } from "./components/Button";
@@ -283,10 +286,12 @@ export { DefaultSidebar } from "./components/DefaultSidebar";
export { TTDDialog } from "./components/TTDDialog/TTDDialog";
export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
-export { normalizeLink } from "./data/url";
export { zoomToFitBounds } from "./actions/actionCanvas";
export { convertToExcalidrawElements } from "./data/transform";
-export { getCommonBounds, getVisibleSceneBounds } from "./element/bounds";
+export {
+ getCommonBounds,
+ getVisibleSceneBounds,
+} from "@excalidraw/element/bounds";
export {
elementsOverlappingBBox,
@@ -296,6 +301,6 @@ export {
export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToCodePlugin";
export { getDataURL } from "./data/blob";
-export { isElementLink } from "./element/elementLink";
+export { isElementLink } from "@excalidraw/element/elementLink";
-export { setCustomTextMetricsProvider } from "./element/textMeasurements";
+export { setCustomTextMetricsProvider } from "@excalidraw/element/textMeasurements";
diff --git a/packages/excalidraw/laser-trails.ts b/packages/excalidraw/laser-trails.ts
index 06e6b573a..7956ae5d2 100644
--- a/packages/excalidraw/laser-trails.ts
+++ b/packages/excalidraw/laser-trails.ts
@@ -1,9 +1,9 @@
+import { DEFAULT_LASER_COLOR, easeOut } from "@excalidraw/common";
+
import type { LaserPointerOptions } from "@excalidraw/laser-pointer";
import { AnimatedTrail } from "./animated-trail";
import { getClientColor } from "./clients";
-import { DEFAULT_LASER_COLOR } from "./constants";
-import { easeOut } from "./utils";
import type { Trail } from "./animated-trail";
import type { AnimationFrameHandler } from "./animation-frame-handler";
diff --git a/packages/excalidraw/lasso/index.ts b/packages/excalidraw/lasso/index.ts
new file mode 100644
index 000000000..d59b2d743
--- /dev/null
+++ b/packages/excalidraw/lasso/index.ts
@@ -0,0 +1,201 @@
+import {
+ type GlobalPoint,
+ type LineSegment,
+ pointFrom,
+} from "@excalidraw/math";
+
+import { getElementLineSegments } from "@excalidraw/element/bounds";
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
+import {
+ isFrameLikeElement,
+ isLinearElement,
+ isTextElement,
+} from "@excalidraw/element/typeChecks";
+
+import { getFrameChildren } from "@excalidraw/element/frame";
+import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
+
+import { getContainerElement } from "@excalidraw/element/textElement";
+
+import { arrayToMap, easeOut } from "@excalidraw/common";
+
+import type {
+ ExcalidrawElement,
+ ExcalidrawLinearElement,
+ NonDeleted,
+} from "@excalidraw/element/types";
+
+import { type AnimationFrameHandler } from "../animation-frame-handler";
+
+import { AnimatedTrail } from "../animated-trail";
+
+import { getLassoSelectedElementIds } from "./utils";
+
+import type App from "../components/App";
+
+export class LassoTrail extends AnimatedTrail {
+ private intersectedElements: Set = new Set();
+ private enclosedElements: Set = new Set();
+ private elementsSegments: Map[]> | null =
+ null;
+ private keepPreviousSelection: boolean = false;
+
+ constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
+ super(animationFrameHandler, app, {
+ animateTrail: true,
+ streamline: 0.4,
+ sizeMapping: (c) => {
+ const DECAY_TIME = Infinity;
+ const DECAY_LENGTH = 5000;
+ const t = Math.max(
+ 0,
+ 1 - (performance.now() - c.pressure) / DECAY_TIME,
+ );
+ const l =
+ (DECAY_LENGTH -
+ Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
+ DECAY_LENGTH;
+
+ return Math.min(easeOut(l), easeOut(t));
+ },
+ fill: () => "rgba(105,101,219,0.05)",
+ stroke: () => "rgba(105,101,219)",
+ });
+ }
+
+ startPath(x: number, y: number, keepPreviousSelection = false) {
+ // clear any existing trails just in case
+ this.endPath();
+
+ super.startPath(x, y);
+ this.intersectedElements.clear();
+ this.enclosedElements.clear();
+
+ this.keepPreviousSelection = keepPreviousSelection;
+
+ if (!this.keepPreviousSelection) {
+ this.app.setState({
+ selectedElementIds: {},
+ selectedGroupIds: {},
+ selectedLinearElement: null,
+ });
+ }
+ }
+
+ selectElementsFromIds = (ids: string[]) => {
+ this.app.setState((prevState) => {
+ const nextSelectedElementIds = ids.reduce((acc, id) => {
+ acc[id] = true;
+ return acc;
+ }, {} as Record);
+
+ if (this.keepPreviousSelection) {
+ for (const id of Object.keys(prevState.selectedElementIds)) {
+ nextSelectedElementIds[id] = true;
+ }
+ }
+
+ for (const [id] of Object.entries(nextSelectedElementIds)) {
+ const element = this.app.scene.getNonDeletedElement(id);
+
+ if (element && isTextElement(element)) {
+ const container = getContainerElement(
+ element,
+ this.app.scene.getNonDeletedElementsMap(),
+ );
+ if (container) {
+ nextSelectedElementIds[container.id] = true;
+ delete nextSelectedElementIds[element.id];
+ }
+ }
+ }
+
+ // remove all children of selected frames
+ for (const [id] of Object.entries(nextSelectedElementIds)) {
+ const element = this.app.scene.getNonDeletedElement(id);
+
+ if (element && isFrameLikeElement(element)) {
+ const elementsInFrame = getFrameChildren(
+ this.app.scene.getNonDeletedElementsMap(),
+ element.id,
+ );
+ for (const child of elementsInFrame) {
+ delete nextSelectedElementIds[child.id];
+ }
+ }
+ }
+
+ const nextSelection = selectGroupsForSelectedElements(
+ {
+ editingGroupId: prevState.editingGroupId,
+ selectedElementIds: nextSelectedElementIds,
+ },
+ this.app.scene.getNonDeletedElements(),
+ prevState,
+ this.app,
+ );
+
+ const selectedIds = [...Object.keys(nextSelection.selectedElementIds)];
+ const selectedGroupIds = [...Object.keys(nextSelection.selectedGroupIds)];
+
+ return {
+ selectedElementIds: nextSelection.selectedElementIds,
+ selectedGroupIds: nextSelection.selectedGroupIds,
+ selectedLinearElement:
+ selectedIds.length === 1 &&
+ !selectedGroupIds.length &&
+ isLinearElement(this.app.scene.getNonDeletedElement(selectedIds[0]))
+ ? new LinearElementEditor(
+ this.app.scene.getNonDeletedElement(
+ selectedIds[0],
+ ) as NonDeleted,
+ )
+ : null,
+ };
+ });
+ };
+
+ addPointToPath = (x: number, y: number, keepPreviousSelection = false) => {
+ super.addPointToPath(x, y);
+
+ this.keepPreviousSelection = keepPreviousSelection;
+
+ this.updateSelection();
+ };
+
+ private updateSelection = () => {
+ const lassoPath = super
+ .getCurrentTrail()
+ ?.originalPoints?.map((p) => pointFrom(p[0], p[1]));
+
+ if (!this.elementsSegments) {
+ this.elementsSegments = new Map();
+ const visibleElementsMap = arrayToMap(this.app.visibleElements);
+ for (const element of this.app.visibleElements) {
+ const segments = getElementLineSegments(element, visibleElementsMap);
+ this.elementsSegments.set(element.id, segments);
+ }
+ }
+
+ if (lassoPath) {
+ const { selectedElementIds } = getLassoSelectedElementIds({
+ lassoPath,
+ elements: this.app.visibleElements,
+ elementsSegments: this.elementsSegments,
+ intersectedElements: this.intersectedElements,
+ enclosedElements: this.enclosedElements,
+ simplifyDistance: 5 / this.app.state.zoom.value,
+ });
+
+ this.selectElementsFromIds(selectedElementIds);
+ }
+ };
+
+ endPath(): void {
+ super.endPath();
+ super.clearTrails();
+ this.intersectedElements.clear();
+ this.enclosedElements.clear();
+ this.elementsSegments = null;
+ }
+}
diff --git a/packages/excalidraw/lasso/utils.ts b/packages/excalidraw/lasso/utils.ts
new file mode 100644
index 000000000..f5a7eefdc
--- /dev/null
+++ b/packages/excalidraw/lasso/utils.ts
@@ -0,0 +1,107 @@
+import { simplify } from "points-on-curve";
+
+import {
+ polygonFromPoints,
+ lineSegment,
+ lineSegmentIntersectionPoints,
+ polygonIncludesPointNonZero,
+} from "@excalidraw/math";
+
+import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+export type ElementsSegmentsMap = Map[]>;
+
+export const getLassoSelectedElementIds = (input: {
+ lassoPath: GlobalPoint[];
+ elements: readonly ExcalidrawElement[];
+ elementsSegments: ElementsSegmentsMap;
+ intersectedElements: Set;
+ enclosedElements: Set;
+ simplifyDistance?: number;
+}): {
+ selectedElementIds: string[];
+} => {
+ const {
+ lassoPath,
+ elements,
+ elementsSegments,
+ intersectedElements,
+ enclosedElements,
+ simplifyDistance,
+ } = input;
+ // simplify the path to reduce the number of points
+ let path: GlobalPoint[] = lassoPath;
+ if (simplifyDistance) {
+ path = simplify(lassoPath, simplifyDistance) as GlobalPoint[];
+ }
+ // as the path might not enclose a shape anymore, clear before checking
+ enclosedElements.clear();
+ for (const element of elements) {
+ if (
+ !intersectedElements.has(element.id) &&
+ !enclosedElements.has(element.id)
+ ) {
+ const enclosed = enclosureTest(path, element, elementsSegments);
+ if (enclosed) {
+ enclosedElements.add(element.id);
+ } else {
+ const intersects = intersectionTest(path, element, elementsSegments);
+ if (intersects) {
+ intersectedElements.add(element.id);
+ }
+ }
+ }
+ }
+
+ const results = [...intersectedElements, ...enclosedElements];
+
+ return {
+ selectedElementIds: results,
+ };
+};
+
+const enclosureTest = (
+ lassoPath: GlobalPoint[],
+ element: ExcalidrawElement,
+ elementsSegments: ElementsSegmentsMap,
+): boolean => {
+ const lassoPolygon = polygonFromPoints(lassoPath);
+ const segments = elementsSegments.get(element.id);
+ if (!segments) {
+ return false;
+ }
+
+ return segments.some((segment) => {
+ return segment.some((point) =>
+ polygonIncludesPointNonZero(point, lassoPolygon),
+ );
+ });
+};
+
+const intersectionTest = (
+ lassoPath: GlobalPoint[],
+ element: ExcalidrawElement,
+ elementsSegments: ElementsSegmentsMap,
+): boolean => {
+ const elementSegments = elementsSegments.get(element.id);
+ if (!elementSegments) {
+ return false;
+ }
+
+ const lassoSegments = lassoPath.reduce((acc, point, index) => {
+ if (index === 0) {
+ return acc;
+ }
+ acc.push(lineSegment(lassoPath[index - 1], point));
+ return acc;
+ }, [] as LineSegment[]);
+
+ return lassoSegments.some((lassoSegment) =>
+ elementSegments.some(
+ (elementSegment) =>
+ // introduce a bit of tolerance to account for roughness and simplification of paths
+ lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
+ ),
+ );
+};
diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json
index f14b79705..381f2b67f 100644
--- a/packages/excalidraw/locales/en.json
+++ b/packages/excalidraw/locales/en.json
@@ -276,6 +276,7 @@
},
"toolBar": {
"selection": "Selection",
+ "lasso": "Lasso selection",
"image": "Insert image",
"rectangle": "Rectangle",
"diamond": "Diamond",
diff --git a/packages/excalidraw/package.json b/packages/excalidraw/package.json
index 578f5e575..9ea877e57 100644
--- a/packages/excalidraw/package.json
+++ b/packages/excalidraw/package.json
@@ -6,6 +6,18 @@
"main": "./dist/prod/index.js",
"module": "./dist/prod/index.js",
"exports": {
+ "./common/*": {
+ "types": "./dist/types/common/src/*.d.ts"
+ },
+ "./element/*": {
+ "types": "./dist/types/element/src/*.d.ts"
+ },
+ "./math/*": {
+ "types": "./dist/types/math/src/*.d.ts"
+ },
+ "./utils/*": {
+ "types": "./dist/types/utils/src/*.d.ts"
+ },
"./*": {
"types": "./dist/types/excalidraw/*.d.ts"
},
@@ -64,7 +76,7 @@
"@excalidraw/mermaid-to-excalidraw": "1.1.2",
"@excalidraw/random-username": "1.1.0",
"@radix-ui/react-popover": "1.1.6",
- "@radix-ui/react-tabs": "1.0.2",
+ "@radix-ui/react-tabs": "1.1.3",
"browser-fs-access": "0.29.1",
"canvas-roundrect-polyfill": "0.0.1",
"clsx": "1.1.1",
diff --git a/packages/excalidraw/reactUtils.ts b/packages/excalidraw/reactUtils.ts
index 5bc466395..a779fcfcc 100644
--- a/packages/excalidraw/reactUtils.ts
+++ b/packages/excalidraw/reactUtils.ts
@@ -5,7 +5,7 @@
import { version as ReactVersion } from "react";
import { unstable_batchedUpdates } from "react-dom";
-import { throttleRAF } from "./utils";
+import { throttleRAF } from "@excalidraw/common";
export const withBatchedUpdates = <
TFunction extends ((event: any) => void) | (() => void),
diff --git a/packages/excalidraw/renderer/helpers.ts b/packages/excalidraw/renderer/helpers.ts
index 05097f95b..765ef4294 100644
--- a/packages/excalidraw/renderer/helpers.ts
+++ b/packages/excalidraw/renderer/helpers.ts
@@ -1,4 +1,4 @@
-import { THEME, THEME_FILTER } from "../constants";
+import { THEME, THEME_FILTER } from "@excalidraw/common";
import type { StaticCanvasRenderConfig } from "../scene/types";
import type { StaticCanvasAppState, AppState } from "../types";
diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts
index 257decd62..3000c206c 100644
--- a/packages/excalidraw/renderer/interactiveScene.ts
+++ b/packages/excalidraw/renderer/interactiveScene.ts
@@ -1,72 +1,66 @@
+import oc from "open-color";
import {
pointFrom,
type GlobalPoint,
type LocalPoint,
type Radians,
} from "@excalidraw/math";
-import oc from "open-color";
-import { getClientColor, renderRemoteCursors } from "../clients";
import {
DEFAULT_TRANSFORM_HANDLE_SPACING,
FRAME_STYLE,
THEME,
-} from "../constants";
-import {
- getElementAbsoluteCoords,
- getTransformHandlesFromCoords,
- getTransformHandles,
- getCommonBounds,
-} from "../element";
+ arrayToMap,
+ invariant,
+ throttleRAF,
+} from "@excalidraw/common";
+
import {
BINDING_HIGHLIGHT_OFFSET,
BINDING_HIGHLIGHT_THICKNESS,
maxBindingGap,
-} from "../element/binding";
-import { LinearElementEditor } from "../element/linearElementEditor";
+} from "@excalidraw/element/binding";
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import {
getOmitSidesForDevice,
+ getTransformHandles,
+ getTransformHandlesFromCoords,
shouldShowBoundingBox,
-} from "../element/transformHandles";
+} from "@excalidraw/element/transformHandles";
import {
isElbowArrow,
isFrameLikeElement,
isImageElement,
isLinearElement,
isTextElement,
-} from "../element/typeChecks";
+} from "@excalidraw/element/typeChecks";
+
+import { getCornerRadius } from "@excalidraw/element/shapes";
+
+import { renderSelectionElement } from "@excalidraw/element/renderElement";
+
import {
isSelectedViaGroup,
getSelectedGroupIds,
getElementsInGroup,
selectGroupsFromGivenElements,
-} from "../groups";
-import { renderSelectionElement } from "../renderer/renderElement";
-import { renderSnaps } from "../renderer/renderSnaps";
-import { roundRect } from "../renderer/roundRect";
-import {
- getScrollBars,
- SCROLLBAR_COLOR,
- SCROLLBAR_WIDTH,
-} from "../scene/scrollbars";
-import { getCornerRadius } from "../shapes";
-import { type InteractiveCanvasAppState } from "../types";
-import { arrayToMap, invariant, throttleRAF } from "../utils";
+} from "@excalidraw/element/groups";
import {
- bootstrapCanvas,
- fillCircle,
- getNormalizedCanvasDimensions,
-} from "./helpers";
+ getCommonBounds,
+ getElementAbsoluteCoords,
+} from "@excalidraw/element/bounds";
import type {
SuggestedBinding,
SuggestedPointBinding,
-} from "../element/binding";
+} from "@excalidraw/element/binding";
+
import type {
TransformHandles,
TransformHandleType,
-} from "../element/transformHandles";
+} from "@excalidraw/element/transformHandles";
+
import type {
ElementsMap,
ExcalidrawBindableElement,
@@ -77,7 +71,25 @@ import type {
ExcalidrawTextElement,
GroupId,
NonDeleted,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import { renderSnaps } from "../renderer/renderSnaps";
+import { roundRect } from "../renderer/roundRect";
+import {
+ getScrollBars,
+ SCROLLBAR_COLOR,
+ SCROLLBAR_WIDTH,
+} from "../scene/scrollbars";
+import { type InteractiveCanvasAppState } from "../types";
+
+import { getClientColor, renderRemoteCursors } from "../clients";
+
+import {
+ bootstrapCanvas,
+ fillCircle,
+ getNormalizedCanvasDimensions,
+} from "./helpers";
+
import type {
InteractiveCanvasRenderConfig,
InteractiveSceneRenderConfig,
@@ -886,23 +898,24 @@ const _renderInteractiveScene = ({
);
}
- if (
- isElbowArrow(selectedElements[0]) &&
- appState.selectedLinearElement &&
- appState.selectedLinearElement.segmentMidPointHoveredCoords
- ) {
- renderElbowArrowMidPointHighlight(context, appState);
- } else if (
- appState.selectedLinearElement &&
- appState.selectedLinearElement.hoverPointIndex >= 0 &&
- !(
- isElbowArrow(selectedElements[0]) &&
- appState.selectedLinearElement.hoverPointIndex > 0 &&
- appState.selectedLinearElement.hoverPointIndex <
- selectedElements[0].points.length - 1
- )
- ) {
- renderLinearElementPointHighlight(context, appState, elementsMap);
+ // Arrows have a different highlight behavior when
+ // they are the only selected element
+ if (appState.selectedLinearElement) {
+ const editor = appState.selectedLinearElement;
+ const firstSelectedLinear = selectedElements.find(
+ (el) => el.id === editor.elementId, // Don't forget bound text elements!
+ );
+
+ if (editor.segmentMidPointHoveredCoords) {
+ renderElbowArrowMidPointHighlight(context, appState);
+ } else if (
+ isElbowArrow(firstSelectedLinear)
+ ? editor.hoverPointIndex === 0 ||
+ editor.hoverPointIndex === firstSelectedLinear.points.length - 1
+ : editor.hoverPointIndex >= 0
+ ) {
+ renderLinearElementPointHighlight(context, appState, elementsMap);
+ }
}
// Paint selected elements
@@ -1073,7 +1086,7 @@ const _renderInteractiveScene = ({
const dashedLinePadding =
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
context.fillStyle = oc.white;
- const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
+ const [x1, y1, x2, y2] = getCommonBounds(selectedElements, elementsMap);
const initialLineDash = context.getLineDash();
context.setLineDash([2 / appState.zoom.value]);
const lineWidth = context.lineWidth;
diff --git a/packages/excalidraw/renderer/renderNewElementScene.ts b/packages/excalidraw/renderer/renderNewElementScene.ts
index f80408366..bbc14654a 100644
--- a/packages/excalidraw/renderer/renderNewElementScene.ts
+++ b/packages/excalidraw/renderer/renderNewElementScene.ts
@@ -1,7 +1,8 @@
-import { throttleRAF } from "../utils";
+import { throttleRAF } from "@excalidraw/common";
+
+import { renderElement } from "@excalidraw/element/renderElement";
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
-import { renderElement } from "./renderElement";
import type { NewElementSceneRenderConfig } from "../scene/types";
diff --git a/packages/excalidraw/renderer/renderSnaps.ts b/packages/excalidraw/renderer/renderSnaps.ts
index 8c26675ac..dd131f779 100644
--- a/packages/excalidraw/renderer/renderSnaps.ts
+++ b/packages/excalidraw/renderer/renderSnaps.ts
@@ -1,6 +1,6 @@
import { pointFrom, type GlobalPoint, type LocalPoint } from "@excalidraw/math";
-import { THEME } from "../constants";
+import { THEME } from "@excalidraw/common";
import type { PointSnapLine, PointerSnapLine } from "../snapping";
import type { InteractiveCanvasAppState } from "../types";
diff --git a/packages/excalidraw/renderer/staticScene.ts b/packages/excalidraw/renderer/staticScene.ts
index 62aeed5b9..16743ff91 100644
--- a/packages/excalidraw/renderer/staticScene.ts
+++ b/packages/excalidraw/renderer/staticScene.ts
@@ -1,33 +1,36 @@
-import {
- EXTERNAL_LINK_IMG,
- ELEMENT_LINK_IMG,
- getLinkHandleFromCoords,
-} from "../components/hyperlink/helpers";
-import { FRAME_STYLE } from "../constants";
-import { getElementAbsoluteCoords } from "../element";
-import { isElementLink } from "../element/elementLink";
-import { createPlaceholderEmbeddableLabel } from "../element/embeddable";
-import { getBoundTextElement } from "../element/textElement";
+import { FRAME_STYLE, throttleRAF } from "@excalidraw/common";
+import { isElementLink } from "@excalidraw/element/elementLink";
+import { createPlaceholderEmbeddableLabel } from "@excalidraw/element/embeddable";
+import { getBoundTextElement } from "@excalidraw/element/textElement";
import {
isEmbeddableElement,
isIframeLikeElement,
isTextElement,
-} from "../element/typeChecks";
+} from "@excalidraw/element/typeChecks";
import {
elementOverlapsWithFrame,
getTargetFrame,
shouldApplyFrameClip,
-} from "../frame";
-import { renderElement } from "../renderer/renderElement";
-import { throttleRAF } from "../utils";
+} from "@excalidraw/element/frame";
-import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
+import { renderElement } from "@excalidraw/element/renderElement";
+
+import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
import type {
ElementsMap,
ExcalidrawFrameLikeElement,
NonDeletedExcalidrawElement,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import {
+ EXTERNAL_LINK_IMG,
+ ELEMENT_LINK_IMG,
+ getLinkHandleFromCoords,
+} from "../components/hyperlink/helpers";
+
+import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
+
import type {
StaticCanvasRenderConfig,
StaticSceneRenderConfig,
diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts
index 23a6890df..0d3f5bad9 100644
--- a/packages/excalidraw/renderer/staticSvgScene.ts
+++ b/packages/excalidraw/renderer/staticSvgScene.ts
@@ -3,39 +3,50 @@ import {
MAX_DECIMALS_FOR_SVG_EXPORT,
MIME_TYPES,
SVG_NS,
-} from "../constants";
-import { normalizeLink, toValidURL } from "../data/url";
-import { getElementAbsoluteCoords, hashString } from "../element";
-import { getUncroppedWidthAndHeight } from "../element/cropElement";
+ getFontFamilyString,
+ isRTL,
+ isTestEnv,
+ getVerticalOffset,
+} from "@excalidraw/common";
+import { normalizeLink, toValidURL } from "@excalidraw/common";
+import { hashString } from "@excalidraw/element";
+import { getUncroppedWidthAndHeight } from "@excalidraw/element/cropElement";
import {
createPlaceholderEmbeddableLabel,
getEmbedLink,
-} from "../element/embeddable";
-import { LinearElementEditor } from "../element/linearElementEditor";
+} from "@excalidraw/element/embeddable";
+import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import {
getBoundTextElement,
getContainerElement,
-} from "../element/textElement";
-import { getLineHeightInPx } from "../element/textMeasurements";
+} from "@excalidraw/element/textElement";
+import { getLineHeightInPx } from "@excalidraw/element/textMeasurements";
import {
isArrowElement,
isIframeLikeElement,
isInitializedImageElement,
isTextElement,
-} from "../element/typeChecks";
-import { getVerticalOffset } from "../fonts";
-import { getContainingFrame } from "../frame";
-import { ShapeCache } from "../scene/ShapeCache";
-import { getCornerRadius, isPathALoop } from "../shapes";
-import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
+} from "@excalidraw/element/typeChecks";
-import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
+import { getContainingFrame } from "@excalidraw/element/frame";
+
+import { getCornerRadius, isPathALoop } from "@excalidraw/element/shapes";
+
+import { ShapeCache } from "@excalidraw/element/ShapeCache";
+
+import {
+ getFreeDrawSvgPath,
+ IMAGE_INVERT_FILTER,
+} from "@excalidraw/element/renderElement";
+
+import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
import type {
ExcalidrawElement,
ExcalidrawTextElementWithContainer,
NonDeletedExcalidrawElement,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
import type { RenderableElementsMap, SVGRenderConfig } from "../scene/types";
import type { AppState, BinaryFiles } from "../types";
import type { Drawable } from "roughjs/bin/core";
diff --git a/packages/excalidraw/scene/Renderer.ts b/packages/excalidraw/scene/Renderer.ts
index 19a6c0b10..e22c997ed 100644
--- a/packages/excalidraw/scene/Renderer.ts
+++ b/packages/excalidraw/scene/Renderer.ts
@@ -1,16 +1,20 @@
-import { isElementInViewport } from "../element/sizeHelpers";
-import { isImageElement } from "../element/typeChecks";
-import { renderInteractiveSceneThrottled } from "../renderer/interactiveScene";
-import { renderStaticSceneThrottled } from "../renderer/staticScene";
-import { memoize, toBrandedType } from "../utils";
+import { isElementInViewport } from "@excalidraw/element/sizeHelpers";
+import { isImageElement } from "@excalidraw/element/typeChecks";
+
+import { memoize, toBrandedType } from "@excalidraw/common";
-import type Scene from "./Scene";
-import type { RenderableElementsMap } from "./types";
import type {
ExcalidrawElement,
NonDeletedElementsMap,
NonDeletedExcalidrawElement,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+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";
export class Renderer {
diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts
index 0eab8b80e..dc66837fb 100644
--- a/packages/excalidraw/scene/Scene.ts
+++ b/packages/excalidraw/scene/Scene.ts
@@ -1,21 +1,25 @@
import throttle from "lodash.throttle";
-import { ENV } from "../constants";
-import { isNonDeletedElement } from "../element";
-import { isFrameLikeElement } from "../element/typeChecks";
+import {
+ randomInteger,
+ arrayToMap,
+ toBrandedType,
+ isDevEnv,
+ isTestEnv,
+} from "@excalidraw/common";
+import { isNonDeletedElement } from "@excalidraw/element";
+import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
+import { getElementsInGroup } from "@excalidraw/element/groups";
+
import {
syncInvalidIndices,
syncMovedIndices,
validateFractionalIndices,
-} from "../fractionalIndex";
-import { getElementsInGroup } from "../groups";
-import { randomInteger } from "../random";
-import { arrayToMap } from "../utils";
-import { toBrandedType } from "../utils";
+} from "@excalidraw/element/fractionalIndex";
-import { getSelectedElements } from "./selection";
+import { getSelectedElements } from "@excalidraw/element/selection";
-import type { LinearElementEditor } from "../element/linearElementEditor";
+import type { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import type {
ExcalidrawElement,
NonDeletedExcalidrawElement,
@@ -26,9 +30,11 @@ import type {
NonDeletedSceneElementsMap,
OrderedExcalidrawElement,
Ordered,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import type { Assert, SameType } from "@excalidraw/common/utility-types";
+
import type { AppState } from "../types";
-import type { Assert, SameType } from "../utility-types";
type ElementIdKey = InstanceType["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey;
@@ -57,14 +63,10 @@ const getNonDeletedElements = (
const validateIndicesThrottled = throttle(
(elements: readonly ExcalidrawElement[]) => {
- if (
- import.meta.env.DEV ||
- import.meta.env.MODE === ENV.TEST ||
- window?.DEBUG_FRACTIONAL_INDICES
- ) {
+ if (isDevEnv() || isTestEnv() || window?.DEBUG_FRACTIONAL_INDICES) {
validateFractionalIndices(elements, {
// throw only in dev & test, to remain functional on `DEBUG_FRACTIONAL_INDICES`
- shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
+ shouldThrow: isDevEnv() || isTestEnv(),
includeBoundTextValidation: true,
});
}
diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts
index 8ca35de2c..229992354 100644
--- a/packages/excalidraw/scene/export.ts
+++ b/packages/excalidraw/scene/export.ts
@@ -1,6 +1,5 @@
import rough from "roughjs/bin/rough";
-import { getDefaultAppState } from "../appState";
import {
DEFAULT_EXPORT_PADDING,
FRAME_STYLE,
@@ -10,39 +9,60 @@ import {
THEME_FILTER,
MIME_TYPES,
EXPORT_DATA_TYPES,
-} from "../constants";
-import { base64ToString, decode, encode, stringToBase64 } from "../data/encode";
-import { serializeAsJSON } from "../data/json";
-import { newTextElement } from "../element";
-import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
+ arrayToMap,
+ distance,
+ getFontString,
+ toBrandedType,
+} from "@excalidraw/common";
+
+import {
+ getCommonBounds,
+ getElementAbsoluteCoords,
+} from "@excalidraw/element/bounds";
+
import {
getInitializedImageElements,
updateImageCache,
-} from "../element/image";
-import { newElementWith } from "../element/mutateElement";
-import { isFrameLikeElement } from "../element/typeChecks";
-import { Fonts } from "../fonts";
-import { syncInvalidIndices } from "../fractionalIndex";
+} from "@excalidraw/element/image";
+
+import { newElementWith } from "@excalidraw/element/mutateElement";
+
+import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
+
import {
getElementsOverlappingFrame,
getFrameLikeElements,
getFrameLikeTitle,
getRootElements,
-} from "../frame";
-import { renderStaticScene } from "../renderer/staticScene";
-import { renderSceneToSvg } from "../renderer/staticSvgScene";
-import { type Mutable } from "../utility-types";
-import { arrayToMap, distance, getFontString, toBrandedType } from "../utils";
+} from "@excalidraw/element/frame";
+
+import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
+
+import { type Mutable } from "@excalidraw/common/utility-types";
+
+import { newTextElement } from "@excalidraw/element/newElement";
+
+import type { Bounds } from "@excalidraw/element/bounds";
-import type { RenderableElementsMap } from "./types";
-import type { Bounds } from "../element/bounds";
import type {
ExcalidrawElement,
ExcalidrawFrameLikeElement,
ExcalidrawTextElement,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import { getDefaultAppState } from "../appState";
+import { base64ToString, decode, encode, stringToBase64 } from "../data/encode";
+import { serializeAsJSON } from "../data/json";
+
+import { Fonts } from "../fonts";
+
+import { renderStaticScene } from "../renderer/staticScene";
+import { renderSceneToSvg } from "../renderer/staticSvgScene";
+
+import type { RenderableElementsMap } from "./types";
+
import type { AppState, BinaryFiles } from "../types";
const truncateText = (element: ExcalidrawTextElement, maxWidth: number) => {
diff --git a/packages/excalidraw/scene/index.ts b/packages/excalidraw/scene/index.ts
index 1c0b795f1..6f39a7fe2 100644
--- a/packages/excalidraw/scene/index.ts
+++ b/packages/excalidraw/scene/index.ts
@@ -4,7 +4,7 @@ export {
getCommonAttributeOfSelectedElements,
getSelectedElements,
getTargetElements,
-} from "./selection";
+} from "@excalidraw/element/selection";
export { calculateScrollCenter } from "./scroll";
export {
hasBackground,
@@ -12,7 +12,7 @@ export {
hasStrokeStyle,
canHaveArrowheads,
canChangeRoundness,
-} from "./comparisons";
+} from "@excalidraw/element/comparisons";
export {
getNormalizedZoom,
getNormalizedGridSize,
diff --git a/packages/excalidraw/scene/normalize.ts b/packages/excalidraw/scene/normalize.ts
index 0c73c5a8a..605ae2fa6 100644
--- a/packages/excalidraw/scene/normalize.ts
+++ b/packages/excalidraw/scene/normalize.ts
@@ -1,6 +1,6 @@
-import { clamp, round } from "@excalidraw/math";
+import { MAX_ZOOM, MIN_ZOOM } from "@excalidraw/common";
-import { MAX_ZOOM, MIN_ZOOM } from "../constants";
+import { clamp, round } from "@excalidraw/math";
import type { NormalizedZoomValue } from "../types";
diff --git a/packages/excalidraw/scene/scroll.ts b/packages/excalidraw/scene/scroll.ts
index 989564eb0..a99ad075f 100644
--- a/packages/excalidraw/scene/scroll.ts
+++ b/packages/excalidraw/scene/scroll.ts
@@ -1,14 +1,15 @@
-import {
- getCommonBounds,
- getClosestElementBounds,
- getVisibleElements,
-} from "../element";
+import { getVisibleElements } from "@excalidraw/element";
import {
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
-} from "../utils";
+} from "@excalidraw/common";
+
+import { getClosestElementBounds } from "@excalidraw/element/bounds";
+
+import { getCommonBounds } from "@excalidraw/element/bounds";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
-import type { ExcalidrawElement } from "../element/types";
import type { AppState, Offsets, PointerCoords, Zoom } from "../types";
const isOutsideViewPort = (appState: AppState, cords: Array) => {
diff --git a/packages/excalidraw/scene/scrollbars.ts b/packages/excalidraw/scene/scrollbars.ts
index b44d79f2b..4fa4349f2 100644
--- a/packages/excalidraw/scene/scrollbars.ts
+++ b/packages/excalidraw/scene/scrollbars.ts
@@ -1,8 +1,11 @@
-import { getCommonBounds } from "../element";
-import { getLanguage } from "../i18n";
-import { getGlobalCSSVariable } from "../utils";
+import { getGlobalCSSVariable } from "@excalidraw/common";
+
+import { getCommonBounds } from "@excalidraw/element/bounds";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import { getLanguage } from "../i18n";
-import type { ExcalidrawElement } from "../element/types";
import type { InteractiveCanvasAppState } from "../types";
import type { ScrollBars } from "./types";
diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts
index 3c198d153..08b05a57d 100644
--- a/packages/excalidraw/scene/types.ts
+++ b/packages/excalidraw/scene/types.ts
@@ -1,10 +1,13 @@
-import type { UserIdleState } from "../constants";
+import type { UserIdleState } from "@excalidraw/common";
import type {
ExcalidrawElement,
NonDeletedElementsMap,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
-} from "../element/types";
+} from "@excalidraw/element/types";
+
+import type { MakeBrand } from "@excalidraw/common/utility-types";
+
import type {
AppClassProperties,
AppState,
@@ -16,7 +19,6 @@ import type {
Device,
PendingExcalidrawElements,
} from "../types";
-import type { MakeBrand } from "../utility-types";
import type { RoughCanvas } from "roughjs/bin/canvas";
import type { Drawable } from "roughjs/bin/core";
diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts
index 7c85f4112..6ea23bd87 100644
--- a/packages/excalidraw/snapping.ts
+++ b/packages/excalidraw/snapping.ts
@@ -7,34 +7,38 @@ import {
type GlobalPoint,
} from "@excalidraw/math";
-import type { InclusiveRange } from "@excalidraw/math";
-
-import { TOOL_TYPE } from "./constants";
+import { TOOL_TYPE, KEYS } from "@excalidraw/common";
import {
getCommonBounds,
getDraggedElementsBounds,
getElementAbsoluteCoords,
-} from "./element/bounds";
-import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks";
-import { getMaximumGroups } from "./groups";
-import { KEYS } from "./keys";
+} from "@excalidraw/element/bounds";
+import {
+ isBoundToContainer,
+ isFrameLikeElement,
+} from "@excalidraw/element/typeChecks";
+
+import { getMaximumGroups } from "@excalidraw/element/groups";
+
import {
getSelectedElements,
getVisibleAndNonSelectedElements,
-} from "./scene/selection";
+} from "@excalidraw/element/selection";
-import type { Bounds } from "./element/bounds";
-import type { MaybeTransformHandleType } from "./element/transformHandles";
+import type { InclusiveRange } from "@excalidraw/math";
+
+import type { Bounds } from "@excalidraw/element/bounds";
+import type { MaybeTransformHandleType } from "@excalidraw/element/transformHandles";
import type {
ElementsMap,
ExcalidrawElement,
NonDeletedExcalidrawElement,
-} from "./element/types";
+} from "@excalidraw/element/types";
+
import type {
AppClassProperties,
AppState,
KeyboardModifiersObject,
- NullableGridSize,
} from "./types";
const SNAP_DISTANCE = 8;
@@ -1411,18 +1415,3 @@ export const isActiveToolNonLinearSnappable = (
activeToolType === TOOL_TYPE.text
);
};
-
-// TODO: Rounding this point causes some shake when free drawing
-export const getGridPoint = (
- x: number,
- y: number,
- gridSize: NullableGridSize,
-): [number, number] => {
- if (gridSize) {
- return [
- Math.round(x / gridSize) * gridSize,
- Math.round(y / gridSize) * gridSize,
- ];
- }
- return [x, y];
-};
diff --git a/packages/excalidraw/store.ts b/packages/excalidraw/store.ts
index 8b0065884..7a5590e54 100644
--- a/packages/excalidraw/store.ts
+++ b/packages/excalidraw/store.ts
@@ -1,14 +1,19 @@
+import { isDevEnv, isShallowEqual, isTestEnv } from "@excalidraw/common";
+
+import { deepCopyElement } from "@excalidraw/element/duplicate";
+
+import { newElementWith } from "@excalidraw/element/mutateElement";
+
+import type { OrderedExcalidrawElement } from "@excalidraw/element/types";
+
+import type { ValueOf } from "@excalidraw/common/utility-types";
+
import { getDefaultAppState } from "./appState";
import { AppStateChange, ElementsChange } from "./change";
-import { ENV } from "./constants";
-import { newElementWith } from "./element/mutateElement";
-import { deepCopyElement } from "./element/newElement";
-import { Emitter } from "./emitter";
-import { isShallowEqual } from "./utils";
-import type { OrderedExcalidrawElement } from "./element/types";
+import { Emitter } from "./emitter";
+
import type { AppState, ObservedAppState } from "./types";
-import type { ValueOf } from "./utility-types";
// hidden non-enumerable property for runtime checks
const hiddenObservedAppStateProp = "__observedAppState";
@@ -256,7 +261,7 @@ export class Store implements IStore {
const message = `There can be at most three store actions scheduled at the same time, but there are "${this.scheduledActions.size}".`;
console.error(message, this.scheduledActions.values());
- if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
+ if (isTestEnv() || isDevEnv()) {
throw new Error(message);
}
}
diff --git a/packages/excalidraw/subset/subset-main.ts b/packages/excalidraw/subset/subset-main.ts
index 5f39af99c..d5e4ba7be 100644
--- a/packages/excalidraw/subset/subset-main.ts
+++ b/packages/excalidraw/subset/subset-main.ts
@@ -1,5 +1,6 @@
+import { isServerEnv, promiseTry } from "@excalidraw/common";
+
import { WorkerInTheMainChunkError, WorkerUrlNotDefinedError } from "../errors";
-import { isServerEnv, promiseTry } from "../utils";
import { WorkerPool } from "../workers";
import type { Commands } from "./subset-shared.chunk";
diff --git a/packages/excalidraw/tests/App.test.tsx b/packages/excalidraw/tests/App.test.tsx
index 8b86477f9..7a1790cee 100644
--- a/packages/excalidraw/tests/App.test.tsx
+++ b/packages/excalidraw/tests/App.test.tsx
@@ -1,8 +1,9 @@
import React from "react";
import { vi } from "vitest";
+import { reseed } from "@excalidraw/common";
+
import { Excalidraw } from "../index";
-import { reseed } from "../random";
import * as StaticScene from "../renderer/staticScene";
import { render, queryByTestId, unmountComponent } from "../tests/test-utils";
diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
index f77eb8ddb..349dd9e64 100644
--- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1088,6 +1089,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1307,6 +1309,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1641,6 +1644,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1975,6 +1979,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2194,6 +2199,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2437,6 +2443,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2606,8 +2613,8 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
- "version": 4,
- "versionNonce": 238820263,
+ "version": 5,
+ "versionNonce": 400692809,
"width": 20,
"x": 0,
"y": 10,
@@ -2741,6 +2748,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3113,6 +3121,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3591,6 +3600,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3917,6 +3927,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4243,6 +4254,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4649,6 +4661,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -5870,6 +5883,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7137,6 +7151,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7408,7 +7423,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
,
"label": "labels.elementLock.unlockAll",
"name": "unlockAllElements",
- "paletteName": "Unlock all elements",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
@@ -7559,7 +7573,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"keyTest": [Function],
"label": "buttons.zenMode",
"name": "zenMode",
- "paletteName": "Toggle zen mode",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
@@ -7603,7 +7616,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"keyTest": [Function],
"label": "labels.viewMode",
"name": "viewMode",
- "paletteName": "Toggle view mode",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
@@ -7677,7 +7689,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
],
"label": "stats.fullTitle",
"name": "stats",
- "paletteName": "Toggle stats",
"perform": [Function],
"trackEvent": {
"category": "menu",
@@ -7814,6 +7825,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8802,6 +8814,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
diff --git a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
index e5e431dfc..bbcc8d7e0 100644
--- a/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/excalidraw.test.tsx.snap
@@ -572,7 +572,7 @@ exports[` > Test UIOptions prop > Test canvasActions > should rende
class="color-picker__top-picks"
>
> Test UIOptions prop > Test canvasActions > should rende
/>
> Test UIOptions prop > Test canvasActions > should rende
/>
> Test UIOptions prop > Test canvasActions > should rende
/>
> Test UIOptions prop > Test canvasActions > should rende
/>
> Test UIOptions prop > Test canvasActions > should rende
aria-expanded="false"
aria-haspopup="dialog"
aria-label="Canvas background"
- class="color-picker__button active-color properties-trigger"
+ class="color-picker__button active-color properties-trigger has-outline"
data-state="closed"
style="--swatch-color: #ffffff;"
title="Show background color picker"
diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
index d740e975c..9ffb97128 100644
--- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -604,6 +605,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1111,6 +1113,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1482,6 +1485,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1854,6 +1858,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2124,6 +2129,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2563,6 +2569,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2865,6 +2872,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3152,6 +3160,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3449,6 +3458,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3738,6 +3748,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3976,6 +3987,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4238,6 +4250,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4514,6 +4527,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4748,6 +4762,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4982,6 +4997,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -5214,6 +5230,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -5446,6 +5463,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -5708,6 +5726,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -6042,6 +6061,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -6470,6 +6490,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -6851,6 +6872,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7173,6 +7195,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7474,6 +7497,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7706,6 +7730,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8064,6 +8089,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8422,6 +8448,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8829,6 +8856,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "freedraw",
@@ -9119,6 +9147,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -9387,6 +9416,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -9654,6 +9684,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -9888,6 +9919,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -10192,6 +10224,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -10535,6 +10568,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -10773,6 +10807,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -11225,6 +11260,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -11482,6 +11518,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -11724,6 +11761,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -11968,6 +12006,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "freedraw",
@@ -12372,6 +12411,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -12622,6 +12662,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -12866,6 +12907,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -13110,6 +13152,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -13360,6 +13403,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -13695,6 +13739,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -13870,6 +13915,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -14161,6 +14207,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -14431,6 +14478,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -14709,6 +14757,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -14873,6 +14922,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -15172,9 +15222,11 @@ History {
"selectedElementIds": {
"id61": true,
},
+ "selectedLinearElementId": "id61",
},
"inserted": {
"selectedElementIds": {},
+ "selectedLinearElementId": null,
},
},
},
@@ -15568,6 +15620,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -16187,6 +16240,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -16806,6 +16860,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -17516,6 +17571,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -18263,6 +18319,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -18740,6 +18797,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -18946,7 +19004,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
- "version": 3,
+ "version": 4,
"width": 100,
"x": 10,
"y": 10,
@@ -18980,7 +19038,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
- "version": 3,
+ "version": 4,
"width": 100,
"x": 110,
"y": 110,
@@ -19014,7 +19072,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
- "version": 6,
+ "version": 7,
"width": 100,
"x": 10,
"y": 10,
@@ -19048,7 +19106,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
- "version": 6,
+ "version": 7,
"width": 100,
"x": 110,
"y": 110,
@@ -19265,6 +19323,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -19724,6 +19783,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap
index 90236a4dd..4b863d4e7 100644
--- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap
@@ -11,7 +11,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
"groupIds": [],
"height": 50,
"id": "id2",
- "index": "a0",
+ "index": "Zz",
"isDeleted": false,
"link": null,
"locked": false,
@@ -20,14 +20,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
"roundness": {
"type": 3,
},
- "seed": 238820263,
+ "seed": 1278240551,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
- "version": 5,
- "versionNonce": 400692809,
+ "version": 6,
+ "versionNonce": 1604849351,
"width": 30,
"x": 30,
"y": 20,
@@ -45,7 +45,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
"groupIds": [],
"height": 50,
"id": "id0",
- "index": "a1",
+ "index": "a0",
"isDeleted": false,
"link": null,
"locked": false,
@@ -54,14 +54,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
"roundness": {
"type": 3,
},
- "seed": 1278240551,
+ "seed": 1505387817,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 6,
- "versionNonce": 23633383,
+ "versionNonce": 915032327,
"width": 30,
"x": -10,
"y": 60,
diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
index 5d48ead6c..319287792 100644
--- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`given element A and group of elements B and given both are selected whe
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -420,6 +421,7 @@ exports[`given element A and group of elements B and given both are selected whe
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -826,6 +828,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1371,6 +1374,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1575,6 +1579,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -1950,6 +1955,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2139,7 +2145,7 @@ History {
"frameId": null,
"groupIds": [],
"height": 10,
- "index": "a0",
+ "index": "Zz",
"isDeleted": false,
"link": null,
"locked": false,
@@ -2164,12 +2170,10 @@ History {
"updated": Map {
"id0" => Delta {
"deleted": {
- "index": "a1",
"x": 20,
"y": 20,
},
"inserted": {
- "index": "a0",
"x": 10,
"y": 10,
},
@@ -2190,6 +2194,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2370,6 +2375,7 @@ exports[`regression tests > can drag element that covers another element, while
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2690,6 +2696,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2936,6 +2943,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3179,6 +3187,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3409,6 +3418,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3665,6 +3675,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -3976,6 +3987,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4398,6 +4410,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4681,6 +4694,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -4934,6 +4948,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -5144,6 +5159,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -5343,6 +5359,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -5725,6 +5742,7 @@ exports[`regression tests > drags selected elements from point inside common bou
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -6015,6 +6033,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "freedraw",
@@ -6823,6 +6842,7 @@ exports[`regression tests > given a group of selected elements with an element t
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7153,6 +7173,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7429,6 +7450,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7663,6 +7685,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -7900,6 +7923,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8080,6 +8104,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8260,6 +8285,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8440,6 +8466,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8663,6 +8690,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -8885,6 +8913,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "freedraw",
@@ -9079,6 +9108,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -9302,6 +9332,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -9482,6 +9513,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -9704,6 +9736,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -9884,6 +9917,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "freedraw",
@@ -10078,6 +10112,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -10258,6 +10293,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -10631,7 +10667,7 @@ History {
"id7",
],
"height": 10,
- "index": "a0",
+ "index": "Zx",
"isDeleted": false,
"link": null,
"locked": false,
@@ -10664,7 +10700,7 @@ History {
"id7",
],
"height": 10,
- "index": "a1",
+ "index": "Zy",
"isDeleted": false,
"link": null,
"locked": false,
@@ -10697,7 +10733,7 @@ History {
"id7",
],
"height": 10,
- "index": "a2",
+ "index": "Zz",
"isDeleted": false,
"link": null,
"locked": false,
@@ -10722,36 +10758,30 @@ History {
"updated": Map {
"id0" => Delta {
"deleted": {
- "index": "a3",
"x": 20,
"y": 20,
},
"inserted": {
- "index": "a0",
"x": 10,
"y": 10,
},
},
"id1" => Delta {
"deleted": {
- "index": "a4",
"x": 40,
"y": 20,
},
"inserted": {
- "index": "a1",
"x": 30,
"y": 10,
},
},
"id2" => Delta {
"deleted": {
- "index": "a5",
"x": 60,
"y": 20,
},
"inserted": {
- "index": "a2",
"x": 50,
"y": 10,
},
@@ -10772,6 +10802,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -11049,6 +11080,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -11175,6 +11207,7 @@ exports[`regression tests > shift click on selected element should deselect it o
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -11374,6 +11407,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -11685,6 +11719,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -12097,6 +12132,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -12710,6 +12746,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -12839,6 +12876,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -13423,6 +13461,7 @@ exports[`regression tests > switches from group of selected elements to another
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -13761,6 +13800,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -14026,6 +14066,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -14152,6 +14193,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -14531,6 +14573,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "text",
@@ -14657,6 +14700,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
diff --git a/packages/excalidraw/tests/actionStyles.test.tsx b/packages/excalidraw/tests/actionStyles.test.tsx
index 2e60000ad..e81e9e4e4 100644
--- a/packages/excalidraw/tests/actionStyles.test.tsx
+++ b/packages/excalidraw/tests/actionStyles.test.tsx
@@ -1,8 +1,9 @@
import React from "react";
+import { CODES } from "@excalidraw/common";
+
import { copiedStyles } from "../actions/actionStyles";
import { Excalidraw } from "../index";
-import { CODES } from "../keys";
import { API } from "../tests/helpers/api";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import {
diff --git a/packages/excalidraw/tests/appState.test.tsx b/packages/excalidraw/tests/appState.test.tsx
index e97c5f3be..abb7ac176 100644
--- a/packages/excalidraw/tests/appState.test.tsx
+++ b/packages/excalidraw/tests/appState.test.tsx
@@ -1,15 +1,16 @@
import React from "react";
+import { EXPORT_DATA_TYPES, MIME_TYPES } from "@excalidraw/common";
+
+import type { ExcalidrawTextElement } from "@excalidraw/element/types";
+
import { getDefaultAppState } from "../appState";
-import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { Excalidraw } from "../index";
import { API } from "./helpers/api";
import { Pointer, UI } from "./helpers/ui";
import { fireEvent, queryByTestId, render, waitFor } from "./test-utils";
-import type { ExcalidrawTextElement } from "../element/types";
-
const { h } = window;
describe("appState", () => {
diff --git a/packages/excalidraw/tests/clipboard.test.tsx b/packages/excalidraw/tests/clipboard.test.tsx
index c7e17f49d..0759afd94 100644
--- a/packages/excalidraw/tests/clipboard.test.tsx
+++ b/packages/excalidraw/tests/clipboard.test.tsx
@@ -1,13 +1,15 @@
import React from "react";
import { vi } from "vitest";
+import { getLineHeightInPx } from "@excalidraw/element/textMeasurements";
+
+import { KEYS, arrayToMap, getLineHeight } from "@excalidraw/common";
+
+import { getElementBounds } from "@excalidraw/element/bounds";
+
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
-import { getElementBounds } from "../element";
-import { getLineHeightInPx } from "../element/textMeasurements";
-import { getLineHeight } from "../fonts";
+
import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
-import { arrayToMap } from "../utils";
import { API } from "./helpers/api";
import { mockMermaidToExcalidraw } from "./helpers/mocks";
@@ -25,7 +27,7 @@ const { h } = window;
const mouse = new Pointer("mouse");
-vi.mock("../keys.ts", async (importOriginal) => {
+vi.mock("@excalidraw/common", async (importOriginal) => {
const module: any = await importOriginal();
return {
__esmodule: true,
diff --git a/packages/excalidraw/tests/contextmenu.test.tsx b/packages/excalidraw/tests/contextmenu.test.tsx
index 7de349516..ef36e3d52 100644
--- a/packages/excalidraw/tests/contextmenu.test.tsx
+++ b/packages/excalidraw/tests/contextmenu.test.tsx
@@ -1,12 +1,13 @@
import React from "react";
import { vi } from "vitest";
+import { KEYS, reseed } from "@excalidraw/common";
+
+import { setDateTimeForTests } from "@excalidraw/common";
+
import { copiedStyles } from "../actions/actionStyles";
import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
-import { reseed } from "../random";
import * as StaticScene from "../renderer/staticScene";
-import { setDateTimeForTests } from "../utils";
import { API } from "./helpers/api";
import { UI, Pointer, Keyboard } from "./helpers/ui";
diff --git a/packages/excalidraw/tests/cropElement.test.tsx b/packages/excalidraw/tests/cropElement.test.tsx
index ddc93e3fe..8011483fa 100644
--- a/packages/excalidraw/tests/cropElement.test.tsx
+++ b/packages/excalidraw/tests/cropElement.test.tsx
@@ -1,17 +1,22 @@
import React from "react";
import { vi } from "vitest";
+import { KEYS, cloneJSON } from "@excalidraw/common";
+
+import { duplicateElement } from "@excalidraw/element/duplicate";
+
+import type {
+ ExcalidrawImageElement,
+ ImageCrop,
+} from "@excalidraw/element/types";
+
import { Excalidraw, exportToCanvas, exportToSvg } from "..";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
-import { duplicateElement } from "../element";
-import { KEYS } from "../keys";
-import { cloneJSON } from "../utils";
import { API } from "./helpers/api";
import { Keyboard, Pointer, UI } from "./helpers/ui";
import { act, GlobalTestState, render, unmountComponent } from "./test-utils";
-import type { ExcalidrawImageElement, ImageCrop } from "../element/types";
import type { NormalizedZoomValue } from "../types";
const { h } = window;
diff --git a/packages/excalidraw/tests/data/reconcile.test.ts b/packages/excalidraw/tests/data/reconcile.test.ts
index 35a33956a..1c0bf13db 100644
--- a/packages/excalidraw/tests/data/reconcile.test.ts
+++ b/packages/excalidraw/tests/data/reconcile.test.ts
@@ -1,13 +1,16 @@
-import { reconcileElements } from "../../data/reconcile";
-import { syncInvalidIndices } from "../../fractionalIndex";
-import { randomInteger } from "../../random";
-import { cloneJSON } from "../../utils";
+import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
+
+import { randomInteger, cloneJSON } from "@excalidraw/common";
-import type { RemoteExcalidrawElement } from "../../data/reconcile";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
-} from "../../element/types";
+} from "@excalidraw/element/types";
+
+import { reconcileElements } from "../../data/reconcile";
+
+import type { RemoteExcalidrawElement } from "../../data/reconcile";
+
import type { AppState } from "../../types";
type Id = string;
diff --git a/packages/excalidraw/tests/data/restore.test.ts b/packages/excalidraw/tests/data/restore.test.ts
index 529284358..4b414bbf1 100644
--- a/packages/excalidraw/tests/data/restore.test.ts
+++ b/packages/excalidraw/tests/data/restore.test.ts
@@ -1,21 +1,24 @@
import { pointFrom } from "@excalidraw/math";
import { vi } from "vitest";
-import { getDefaultAppState } from "../../appState";
-import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants";
-import * as restore from "../../data/restore";
-import { newElementWith } from "../../element/mutateElement";
-import * as sizeHelpers from "../../element/sizeHelpers";
-import { API } from "../helpers/api";
+import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "@excalidraw/common";
+
+import { newElementWith } from "@excalidraw/element/mutateElement";
+import * as sizeHelpers from "@excalidraw/element/sizeHelpers";
-import type { ImportedDataState } from "../../data/types";
import type {
ExcalidrawElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
-} from "../../element/types";
-import type { NormalizedZoomValue } from "../../types";
+} from "@excalidraw/element/types";
+import type { NormalizedZoomValue } from "@excalidraw/excalidraw/types";
+
+import { API } from "../helpers/api";
+import * as restore from "../../data/restore";
+import { getDefaultAppState } from "../../appState";
+
+import type { ImportedDataState } from "../../data/types";
describe("restoreElements", () => {
const mockSizeHelper = vi.spyOn(sizeHelpers, "isInvisiblySmallElement");
diff --git a/packages/excalidraw/tests/dragCreate.test.tsx b/packages/excalidraw/tests/dragCreate.test.tsx
index ecdfbcb64..c33da5e7e 100644
--- a/packages/excalidraw/tests/dragCreate.test.tsx
+++ b/packages/excalidraw/tests/dragCreate.test.tsx
@@ -1,9 +1,11 @@
import React from "react";
import { vi } from "vitest";
+import { KEYS, reseed } from "@excalidraw/common";
+
+import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
+
import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
-import { reseed } from "../random";
import * as InteractiveScene from "../renderer/interactiveScene";
import * as StaticScene from "../renderer/staticScene";
@@ -15,8 +17,6 @@ import {
unmountComponent,
} from "./test-utils";
-import type { ExcalidrawLinearElement } from "../element/types";
-
unmountComponent();
const renderInteractiveScene = vi.spyOn(
diff --git a/packages/excalidraw/tests/elementLocking.test.tsx b/packages/excalidraw/tests/elementLocking.test.tsx
index a1ca14683..45e370ed8 100644
--- a/packages/excalidraw/tests/elementLocking.test.tsx
+++ b/packages/excalidraw/tests/elementLocking.test.tsx
@@ -1,10 +1,13 @@
import React from "react";
+import { mutateElement } from "@excalidraw/element/mutateElement";
+
+import { KEYS } from "@excalidraw/common";
+
import { actionSelectAll } from "../actions";
-import { mutateElement } from "../element/mutateElement";
import { t } from "../i18n";
import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
+
import { API } from "../tests/helpers/api";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { render, unmountComponent } from "../tests/test-utils";
diff --git a/packages/excalidraw/tests/excalidraw.test.tsx b/packages/excalidraw/tests/excalidraw.test.tsx
index 0e33f8167..34135c618 100644
--- a/packages/excalidraw/tests/excalidraw.test.tsx
+++ b/packages/excalidraw/tests/excalidraw.test.tsx
@@ -2,7 +2,8 @@ import { queryByText, queryByTestId } from "@testing-library/react";
import React from "react";
import { useMemo } from "react";
-import { THEME } from "../constants";
+import { THEME } from "@excalidraw/common";
+
import { t } from "../i18n";
import { Excalidraw, Footer, MainMenu } from "../index";
diff --git a/packages/excalidraw/tests/export.test.tsx b/packages/excalidraw/tests/export.test.tsx
index c10336271..a42e56b90 100644
--- a/packages/excalidraw/tests/export.test.tsx
+++ b/packages/excalidraw/tests/export.test.tsx
@@ -1,7 +1,10 @@
import React from "react";
+import { SVG_NS } from "@excalidraw/common";
+
+import type { FileId } from "@excalidraw/element/types";
+
import { getDefaultAppState } from "../appState";
-import { SVG_NS } from "../constants";
import { getDataURL } from "../data/blob";
import { encodePngMetadata } from "../data/image";
import { serializeAsJSON } from "../data/json";
@@ -15,8 +18,6 @@ import {
import { API } from "./helpers/api";
import { render, waitFor } from "./test-utils";
-import type { FileId } from "../element/types";
-
const { h } = window;
const testElements = [
diff --git a/packages/excalidraw/tests/fixtures/diagramFixture.ts b/packages/excalidraw/tests/fixtures/diagramFixture.ts
index a4fdc1560..8512fed36 100644
--- a/packages/excalidraw/tests/fixtures/diagramFixture.ts
+++ b/packages/excalidraw/tests/fixtures/diagramFixture.ts
@@ -1,4 +1,4 @@
-import { VERSIONS } from "../../constants";
+import { VERSIONS } from "@excalidraw/common";
import {
diamondFixture,
diff --git a/packages/excalidraw/tests/fixtures/elementFixture.ts b/packages/excalidraw/tests/fixtures/elementFixture.ts
index a7d8c5080..35aabd55f 100644
--- a/packages/excalidraw/tests/fixtures/elementFixture.ts
+++ b/packages/excalidraw/tests/fixtures/elementFixture.ts
@@ -1,8 +1,8 @@
+import { DEFAULT_FONT_FAMILY } from "@excalidraw/common";
+
import type { Radians } from "@excalidraw/math";
-import { DEFAULT_FONT_FAMILY } from "../../constants";
-
-import type { ExcalidrawElement } from "../../element/types";
+import type { ExcalidrawElement } from "@excalidraw/element/types";
const elementBase: Omit = {
id: "vWrqOAfkind2qcm7LDAGZ",
diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx
index 9d9f68611..22a6c67f8 100644
--- a/packages/excalidraw/tests/flip.test.tsx
+++ b/packages/excalidraw/tests/flip.test.tsx
@@ -1,18 +1,30 @@
-import { pointFrom, type Radians } from "@excalidraw/math";
import React from "react";
import { vi } from "vitest";
+import { ROUNDNESS, KEYS, arrayToMap, cloneJSON } from "@excalidraw/common";
+
+import { pointFrom, type Radians } from "@excalidraw/math";
+
+import { getBoundTextElementPosition } from "@excalidraw/element/textElement";
+import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
+import { newLinearElement } from "@excalidraw/element/newElement";
+
import type { LocalPoint } from "@excalidraw/math";
+import type {
+ ExcalidrawElement,
+ ExcalidrawImageElement,
+ ExcalidrawLinearElement,
+ ExcalidrawTextElementWithContainer,
+ FileId,
+} from "@excalidraw/element/types";
+
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
import { createPasteEvent } from "../clipboard";
-import { ROUNDNESS } from "../constants";
-import { getElementAbsoluteCoords } from "../element";
-import { newLinearElement } from "../element";
-import { getBoundTextElementPosition } from "../element/textElement";
import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
-import { arrayToMap, cloneJSON } from "../utils";
+
+// Importing to spy on it and mock the implementation (mocking does not work with simple vi.mock for some reason)
+import * as blobModule from "../data/blob";
import { API } from "./helpers/api";
import { UI, Pointer, Keyboard } from "./helpers/ui";
@@ -25,25 +37,17 @@ import {
waitFor,
} from "./test-utils";
-import type {
- ExcalidrawElement,
- ExcalidrawImageElement,
- ExcalidrawLinearElement,
- ExcalidrawTextElementWithContainer,
- FileId,
-} from "../element/types";
import type { NormalizedZoomValue } from "../types";
const { h } = window;
const mouse = new Pointer("mouse");
-vi.mock("../data/blob", async (actual) => {
- const orig: Object = await actual();
- return {
- ...orig,
- resizeImageFile: (imageFile: File) => imageFile,
- generateIdFromFile: () => "fileId" as FileId,
- };
+beforeEach(() => {
+ const generateIdSpy = vi.spyOn(blobModule, "generateIdFromFile");
+ const resizeFileSpy = vi.spyOn(blobModule, "resizeImageFile");
+
+ generateIdSpy.mockImplementation(() => Promise.resolve("fileId" as FileId));
+ resizeFileSpy.mockImplementation((file: File) => Promise.resolve(file));
});
beforeEach(async () => {
diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts
index 2aa9ee999..09aa308a5 100644
--- a/packages/excalidraw/tests/helpers/api.ts
+++ b/packages/excalidraw/tests/helpers/api.ts
@@ -4,29 +4,26 @@ import util from "util";
import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math";
-import { getDefaultAppState } from "../../appState";
-import { createTestHook } from "../../components/App";
-import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
-import { getMimeType } from "../../data/blob";
-import { newElement, newTextElement, newLinearElement } from "../../element";
-import { mutateElement } from "../../element/mutateElement";
+import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
+
+import { mutateElement } from "@excalidraw/element/mutateElement";
import {
newArrowElement,
+ newElement,
newEmbeddableElement,
newFrameElement,
newFreeDrawElement,
newIframeElement,
newImageElement,
+ newLinearElement,
newMagicFrameElement,
-} from "../../element/newElement";
-import { isLinearElementType } from "../../element/typeChecks";
-import { selectGroupsForSelectedElements } from "../../groups";
-import { getSelectedElements } from "../../scene/selection";
-import { assertNever } from "../../utils";
-import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils";
+ newTextElement,
+} from "@excalidraw/element/newElement";
+
+import { isLinearElementType } from "@excalidraw/element/typeChecks";
+import { getSelectedElements } from "@excalidraw/element/selection";
+import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
-import type { Action } from "../../actions/types";
-import type App from "../../components/App";
import type {
ExcalidrawElement,
ExcalidrawGenericElement,
@@ -41,9 +38,19 @@ import type {
ExcalidrawElbowArrowElement,
ExcalidrawArrowElement,
FixedSegment,
-} from "../../element/types";
+} from "@excalidraw/element/types";
+
+import type { Mutable } from "@excalidraw/common/utility-types";
+
+import { getMimeType } from "../../data/blob";
+import { createTestHook } from "../../components/App";
+import { getDefaultAppState } from "../../appState";
+import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils";
+
+import type { Action } from "../../actions/types";
+import type App from "../../components/App";
import type { AppState } from "../../types";
-import type { Mutable } from "../../utility-types";
+
const readFile = util.promisify(fs.readFile);
// so that window.h is available when App.tsx is not imported as well.
diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts
index a72e3fa74..32de489f1 100644
--- a/packages/excalidraw/tests/helpers/ui.ts
+++ b/packages/excalidraw/tests/helpers/ui.ts
@@ -1,11 +1,11 @@
import { pointFrom, pointRotateRads } from "@excalidraw/math";
-import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
-
-import { createTestHook } from "../../components/App";
-import { getCommonBounds, getElementPointsCoords } from "../../element/bounds";
-import { cropElement } from "../../element/cropElement";
-import { mutateElement } from "../../element/mutateElement";
+import {
+ getCommonBounds,
+ getElementPointsCoords,
+} from "@excalidraw/element/bounds";
+import { cropElement } from "@excalidraw/element/cropElement";
+import { mutateElement } from "@excalidraw/element/mutateElement";
import {
getTransformHandles,
getTransformHandlesFromCoords,
@@ -13,21 +13,18 @@ import {
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
type TransformHandle,
type TransformHandleDirection,
-} from "../../element/transformHandles";
+} from "@excalidraw/element/transformHandles";
import {
isLinearElement,
isFreeDrawElement,
isTextElement,
isFrameLikeElement,
-} from "../../element/typeChecks";
-import { KEYS } from "../../keys";
-import { arrayToMap } from "../../utils";
-import { getTextEditor } from "../queries/dom";
-import { act, fireEvent, GlobalTestState, screen } from "../test-utils";
+} from "@excalidraw/element/typeChecks";
+import { KEYS, arrayToMap, elementCenterPoint } from "@excalidraw/common";
-import { API } from "./api";
+import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
-import type { TransformHandleType } from "../../element/transformHandles";
+import type { TransformHandleType } from "@excalidraw/element/transformHandles";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -39,7 +36,14 @@ import type {
ExcalidrawTextContainer,
ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
-} from "../../element/types";
+} from "@excalidraw/element/types";
+
+import { createTestHook } from "../../components/App";
+import { getTextEditor } from "../queries/dom";
+import { act, fireEvent, GlobalTestState, screen } from "../test-utils";
+
+import { API } from "./api";
+
import type { ToolType } from "../../types";
// so that window.h is available when App.tsx is not imported as well.
@@ -147,7 +151,7 @@ export class Keyboard {
const getElementPointForSelection = (
element: ExcalidrawElement,
): GlobalPoint => {
- const { x, y, width, height, angle } = element;
+ const { x, y, width, angle } = element;
const target = pointFrom(
x +
(isLinearElement(element) || isFreeDrawElement(element) ? 0 : width / 2),
@@ -162,7 +166,7 @@ const getElementPointForSelection = (
(bounds[1] + bounds[3]) / 2,
);
} else {
- center = pointFrom(x + width / 2, y + height / 2);
+ center = elementCenterPoint(element);
}
if (isTextElement(element)) {
@@ -398,7 +402,10 @@ const proxy = (
};
/** Tools that can be used to draw shapes */
-type DrawingToolName = Exclude;
+type DrawingToolName = Exclude<
+ ToolType,
+ "lock" | "selection" | "eraser" | "lasso"
+>;
type Element = T extends "line" | "freedraw"
? ExcalidrawLinearElement
diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx
index 6e7066b90..8dd65c7a5 100644
--- a/packages/excalidraw/tests/history.test.tsx
+++ b/packages/excalidraw/tests/history.test.tsx
@@ -8,10 +8,35 @@ import {
import { vi } from "vitest";
import { pointFrom } from "@excalidraw/math";
+import { newElementWith } from "@excalidraw/element/mutateElement";
+
+import {
+ EXPORT_DATA_TYPES,
+ MIME_TYPES,
+ ORIG_ID,
+ KEYS,
+ arrayToMap,
+ COLOR_PALETTE,
+ DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
+ DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
+} from "@excalidraw/common";
+
+import "@excalidraw/utils/test-utils";
+
import type { LocalPoint, Radians } from "@excalidraw/math";
+import type {
+ ExcalidrawElbowArrowElement,
+ ExcalidrawFrameElement,
+ ExcalidrawGenericElement,
+ ExcalidrawLinearElement,
+ ExcalidrawTextElement,
+ FixedPointBinding,
+ FractionalIndex,
+ SceneElementsMap,
+} from "@excalidraw/element/types";
+
import "../global.d.ts";
-import "../../utils/test-utils";
import {
actionSendBackward,
@@ -23,17 +48,8 @@ import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import { getDefaultAppState } from "../appState";
import { HistoryEntry } from "../history";
import { Excalidraw } from "../index";
-import { KEYS } from "../keys";
import * as StaticScene from "../renderer/staticScene";
-import { EXPORT_DATA_TYPES, MIME_TYPES, ORIG_ID } from "../constants";
import { Snapshot, CaptureUpdateAction } from "../store";
-import { arrayToMap } from "../utils";
-import {
- COLOR_PALETTE,
- DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
- DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
-} from "../colors";
-import { newElementWith } from "../element/mutateElement";
import { AppStateChange, ElementsChange } from "../change";
import { API } from "./helpers/api";
@@ -47,16 +63,6 @@ import {
getCloneByOrigId,
} from "./test-utils";
-import type {
- ExcalidrawElbowArrowElement,
- ExcalidrawFrameElement,
- ExcalidrawGenericElement,
- ExcalidrawLinearElement,
- ExcalidrawTextElement,
- FixedPointBinding,
- FractionalIndex,
- SceneElementsMap,
-} from "../element/types";
import type { AppState } from "../types";
const { h } = window;
diff --git a/packages/excalidraw/tests/lasso.test.tsx b/packages/excalidraw/tests/lasso.test.tsx
new file mode 100644
index 000000000..00e0ec2b4
--- /dev/null
+++ b/packages/excalidraw/tests/lasso.test.tsx
@@ -0,0 +1,1813 @@
+/**
+ * Test case:
+ *
+ * create a few random elements on canvas
+ * creates a lasso path for each of these cases
+ * - do not intersect / enclose at all
+ * - intersects some, does not enclose/intersect the rest
+ * - intersects and encloses some
+ * - single linear element should be selected if lasso intersects/encloses it
+ *
+ *
+ * special cases:
+ * - selects only frame if frame and children both selected by lasso
+ * - selects group if any group from group is selected
+ */
+
+import {
+ type GlobalPoint,
+ type LocalPoint,
+ pointFrom,
+ type Radians,
+} from "@excalidraw/math";
+
+import { getElementLineSegments } from "@excalidraw/element/bounds";
+
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+import { Excalidraw } from "../index";
+
+import { getSelectedElements } from "../scene";
+
+import { getLassoSelectedElementIds } from "../lasso/utils";
+
+import { act, render } from "./test-utils";
+
+import type { ElementsSegmentsMap } from "../lasso/utils";
+
+const { h } = window;
+
+beforeEach(async () => {
+ localStorage.clear();
+ await render( );
+ h.state.width = 1000;
+ h.state.height = 1000;
+});
+
+const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => {
+ act(() => {
+ h.app.lassoTrail.startPath(startPoint[0], startPoint[1]);
+
+ points.forEach((point) => {
+ h.app.lassoTrail.addPointToPath(
+ startPoint[0] + point[0],
+ startPoint[1] + point[1],
+ );
+ });
+
+ const elementsSegments: ElementsSegmentsMap = new Map();
+ for (const element of h.elements) {
+ const segments = getElementLineSegments(
+ element,
+ h.app.scene.getElementsMapIncludingDeleted(),
+ );
+ elementsSegments.set(element.id, segments);
+ }
+
+ const result = getLassoSelectedElementIds({
+ lassoPath:
+ h.app.lassoTrail
+ .getCurrentTrail()
+ ?.originalPoints?.map((p) => pointFrom(p[0], p[1])) ??
+ [],
+ elements: h.elements,
+ elementsSegments,
+ intersectedElements: new Set(),
+ enclosedElements: new Set(),
+ });
+
+ act(() =>
+ h.app.lassoTrail.selectElementsFromIds(result.selectedElementIds),
+ );
+
+ h.app.lassoTrail.endPath();
+ });
+};
+
+describe("Basic lasso selection tests", () => {
+ beforeEach(() => {
+ const elements: ExcalidrawElement[] = [
+ {
+ id: "FLZN67ISZbMV-RH8SzS9W",
+ type: "rectangle",
+ x: 0,
+ y: 0,
+ width: 107.11328125,
+ height: 90.16015625,
+ angle: 5.40271241072378,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: null,
+ index: "a8",
+ roundness: {
+ type: 3,
+ },
+ seed: 1558764732,
+ version: 43,
+ versionNonce: 575357188,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740723127946,
+ link: null,
+ locked: false,
+ },
+ {
+ id: "T3TSAFUwp--pT2b_q7Y5U",
+ type: "diamond",
+ x: 349.822265625,
+ y: -201.244140625,
+ width: 123.3828125,
+ height: 74.66796875,
+ angle: 0.6498998717212414,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: null,
+ index: "a9",
+ roundness: {
+ type: 2,
+ },
+ seed: 1720937276,
+ version: 69,
+ versionNonce: 1991578556,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740723132096,
+ link: null,
+ locked: false,
+ },
+ {
+ id: "a9RZwSeqlZHyhses2iYZ0",
+ type: "ellipse",
+ x: 188.259765625,
+ y: -48.193359375,
+ width: 146.8984375,
+ height: 91.01171875,
+ angle: 0.6070652964532064,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: null,
+ index: "aA",
+ roundness: {
+ type: 2,
+ },
+ seed: 476696636,
+ version: 38,
+ versionNonce: 1903760444,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740723125079,
+ link: null,
+ locked: false,
+ },
+ {
+ id: "vCw17KEn9h4sY2KMdnq0G",
+ type: "arrow",
+ x: -257.388671875,
+ y: 78.583984375,
+ width: 168.4765625,
+ height: 153.38671875,
+ angle: 0,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: null,
+ index: "aB",
+ roundness: {
+ type: 2,
+ },
+ seed: 1302309508,
+ version: 19,
+ versionNonce: 1230691388,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740723110578,
+ link: null,
+ locked: false,
+ points: [
+ [0, 0],
+ [168.4765625, -153.38671875],
+ ],
+ lastCommittedPoint: null,
+ startBinding: null,
+ endBinding: null,
+ startArrowhead: null,
+ endArrowhead: "arrow",
+ elbowed: false,
+ },
+ {
+ id: "dMsLoKhGsWQXpiKGWZ6Cn",
+ type: "line",
+ x: -113.748046875,
+ y: -165.224609375,
+ width: 206.12890625,
+ height: 35.4140625,
+ angle: 0,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: null,
+ index: "aC",
+ roundness: {
+ type: 2,
+ },
+ seed: 514585788,
+ version: 18,
+ versionNonce: 1338507580,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740723112995,
+ link: null,
+ locked: false,
+ points: [
+ [0, 0],
+ [206.12890625, 35.4140625],
+ ],
+ lastCommittedPoint: null,
+ startBinding: null,
+ endBinding: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ },
+ {
+ id: "1GUDjUg8ibE_4qMFtdQiK",
+ type: "freedraw",
+ x: 384.404296875,
+ y: 91.580078125,
+ width: 537.55078125,
+ height: 288.48046875,
+ angle: 5.5342222396022285,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: null,
+ index: "aD",
+ roundness: null,
+ seed: 103578044,
+ version: 167,
+ versionNonce: 1117299588,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740723137180,
+ link: null,
+ locked: false,
+ points: [
+ [0, 0],
+ [-0.10546875, 0],
+ [-3.23046875, -0.859375],
+ [-18.09765625, -4.6953125],
+ [-54.40625, -13.765625],
+ [-103.48046875, -23.05859375],
+ [-155.6640625, -27.5390625],
+ [-205.5703125, -27.96484375],
+ [-239, -24.4765625],
+ [-257.27734375, -17.0390625],
+ [-270.1015625, -5.43359375],
+ [-279.94140625, 12.12109375],
+ [-286.828125, 36.6875],
+ [-291.03515625, 65.63671875],
+ [-292.5546875, 94.96875],
+ [-291.8203125, 122.1875],
+ [-286.140625, 144.703125],
+ [-274.60546875, 160.01953125],
+ [-257.1171875, 170.375],
+ [-237.7890625, 176.1953125],
+ [-218.85546875, 178.69921875],
+ [-199.33984375, 181.56640625],
+ [-182.4609375, 188.4765625],
+ [-168.97265625, 200.14453125],
+ [-160.83984375, 211.1875],
+ [-156.40234375, 220.0703125],
+ [-153.60546875, 226.12890625],
+ [-151.3203125, 229.30078125],
+ [-146.28125, 231.7421875],
+ [-136.140625, 233.30859375],
+ [-122.1953125, 233.80078125],
+ [-108.66015625, 234.23828125],
+ [-97.0234375, 235.0546875],
+ [-89.6171875, 235.7421875],
+ [-85.84375, 237.52734375],
+ [-82.546875, 240.41796875],
+ [-79.64453125, 243.2734375],
+ [-75.71875, 245.99609375],
+ [-69.734375, 248.4453125],
+ [-59.6640625, 250.87890625],
+ [-45.1171875, 252.4453125],
+ [-23.9453125, 251.7265625],
+ [7.41796875, 244.0546875],
+ [48.58203125, 223.734375],
+ [93.5078125, 192.859375],
+ [135.8359375, 153.9453125],
+ [168.875, 114.015625],
+ [186.5625, 86.640625],
+ [194.9765625, 71.19140625],
+ [199.0234375, 62.671875],
+ [199.875, 59.6171875],
+ [200.1796875, 58.72265625],
+ [200.4140625, 58.62109375],
+ [200.87109375, 58.57421875],
+ [203.1796875, 58.2734375],
+ [208.72265625, 55.671875],
+ [216.421875, 50.89453125],
+ [224.546875, 45.265625],
+ [234.40625, 36.30859375],
+ [241.71484375, 28.14453125],
+ [243.6875, 24.1171875],
+ [244.6171875, 21.34375],
+ [244.99609375, 18.5625],
+ [243.78515625, 12.41015625],
+ [237.6328125, -4.8125],
+ [222.91796875, -36.03515625],
+ [222.91796875, -36.03515625],
+ ],
+ pressures: [],
+ simulatePressure: true,
+ lastCommittedPoint: null,
+ },
+ ].map(
+ (e) =>
+ ({
+ ...e,
+ angle: e.angle as Radians,
+ index: null,
+ } as ExcalidrawElement),
+ );
+
+ act(() => {
+ h.elements = elements;
+ h.app.setActiveTool({ type: "lasso" });
+ });
+ });
+
+ it("None should be selected", () => {
+ const startPoint = pointFrom(-533, 611);
+
+ const points = [
+ [0, 0],
+ [0.1015625, -0.09765625],
+ [10.16796875, -8.15625],
+ [25.71484375, -18.5078125],
+ [46.078125, -28.63671875],
+ [90.578125, -41.9140625],
+ [113.04296875, -45.0859375],
+ [133.95703125, -46.2890625],
+ [152.92578125, -46.2890625],
+ [170.921875, -44.98828125],
+ [190.1640625, -39.61328125],
+ [213.73046875, -29],
+ [238.859375, -16.59375],
+ [261.87890625, -5.80078125],
+ [281.63671875, 2.4453125],
+ [300.125, 9.01953125],
+ [320.09375, 14.046875],
+ [339.140625, 16.95703125],
+ [358.3203125, 18.41796875],
+ [377.5234375, 17.890625],
+ [396.45703125, 14.53515625],
+ [416.4921875, 8.015625],
+ [438.796875, -1.54296875],
+ [461.6328125, -11.5703125],
+ [483.36328125, -21.48828125],
+ [503.37109375, -30.87109375],
+ [517.0546875, -36.49609375],
+ [525.62109375, -39.6640625],
+ [531.45703125, -41.46875],
+ [534.1328125, -41.9375],
+ [535.32421875, -42.09375],
+ [544.4140625, -42.09375],
+ [567.2265625, -42.09375],
+ [608.1875, -38.5625],
+ [665.203125, -27.66796875],
+ [725.8984375, -11.30078125],
+ [785.05078125, 8.17578125],
+ [832.12109375, 25.55078125],
+ [861.62109375, 36.32421875],
+ [881.91796875, 42.203125],
+ [896.75, 45.125],
+ [907.04296875, 46.46484375],
+ [917.44921875, 46.42578125],
+ [930.671875, 42.59765625],
+ [945.953125, 34.66796875],
+ [964.08984375, 22.43359375],
+ [989.8125, 2.328125],
+ [1014.6640625, -17.79296875],
+ [1032.7734375, -32.70703125],
+ [1045.984375, -43.9921875],
+ [1052.48828125, -50.1875],
+ [1054.97265625, -53.3046875],
+ [1055.65234375, -54.38671875],
+ [1060.48046875, -54.83984375],
+ [1073.03125, -55.2734375],
+ [1095.6484375, -54],
+ [1125.41796875, -49.05859375],
+ [1155.33984375, -41.21484375],
+ [1182.33203125, -33.6875],
+ [1204.1171875, -27.75390625],
+ [1220.95703125, -23.58203125],
+ [1235.390625, -21.06640625],
+ [1248.078125, -19.3515625],
+ [1257.78125, -18.6484375],
+ [1265.6640625, -19.22265625],
+ [1271.5703125, -20.42578125],
+ [1276.046875, -21.984375],
+ [1280.328125, -25.23828125],
+ [1284.19140625, -29.953125],
+ [1288.22265625, -35.8125],
+ [1292.87109375, -43.21484375],
+ [1296.6796875, -50.44921875],
+ [1299.3828125, -56.40234375],
+ [1301.48828125, -61.08203125],
+ [1302.89453125, -64.75],
+ [1303.890625, -67.37890625],
+ [1304.41796875, -68.953125],
+ [1304.65234375, -69.8046875],
+ [1304.80078125, -70.2578125],
+ [1304.80078125, -70.2578125],
+ ] as LocalPoint[];
+
+ updatePath(startPoint, points);
+
+ const selectedElements = getSelectedElements(h.elements, h.app.state);
+
+ expect(selectedElements.length).toBe(0);
+ });
+
+ it("Intersects some, does not enclose/intersect the rest", () => {
+ const startPoint = pointFrom(-311, 50);
+ const points = [
+ [0, 0],
+ [0.1015625, 0],
+ [3.40234375, -2.25390625],
+ [12.25390625, -7.84375],
+ [22.71484375, -13.89453125],
+ [39.09765625, -22.3359375],
+ [58.5546875, -31.9609375],
+ [79.91796875, -41.21875],
+ [90.53125, -44.76953125],
+ [99.921875, -47.16796875],
+ [107.46484375, -48.640625],
+ [113.92578125, -49.65625],
+ [119.57421875, -50.1953125],
+ [124.640625, -50.1953125],
+ [129.49609375, -50.1953125],
+ [134.53125, -50.1953125],
+ [140.59375, -50.1953125],
+ [147.27734375, -49.87109375],
+ [154.32421875, -48.453125],
+ [160.93359375, -46.0390625],
+ [166.58203125, -42.8828125],
+ [172.0078125, -38.8671875],
+ [176.75390625, -34.1015625],
+ [180.41796875, -29.609375],
+ [183.09375, -25.0390625],
+ [185.11328125, -19.70703125],
+ [186.8828125, -13.04296875],
+ [188.515625, -6.39453125],
+ [189.8515625, -1.04296875],
+ [190.9609375, 4.34375],
+ [191.9296875, 9.3125],
+ [193.06640625, 13.73046875],
+ [194.21875, 17.51953125],
+ [195.32421875, 20.83984375],
+ [196.5625, 23.4296875],
+ [198.2109375, 25.5234375],
+ [200.04296875, 27.38671875],
+ [202.1640625, 28.80078125],
+ [204.43359375, 30.33984375],
+ [207.10546875, 31.7109375],
+ [210.69921875, 33.1640625],
+ [214.6015625, 34.48828125],
+ [218.5390625, 35.18359375],
+ [222.703125, 35.71875],
+ [227.16015625, 35.98828125],
+ [232.01171875, 35.98828125],
+ [237.265625, 35.98828125],
+ [242.59765625, 35.015625],
+ [247.421875, 33.4140625],
+ [251.61328125, 31.90625],
+ [255.84375, 30.1328125],
+ [260.25390625, 28.62109375],
+ [264.44140625, 27.41796875],
+ [268.5546875, 26.34765625],
+ [272.6171875, 25.42578125],
+ [276.72265625, 24.37890625],
+ [281.234375, 23.140625],
+ [286.69921875, 22.046875],
+ [293.5859375, 20.82421875],
+ [300.6328125, 19.4140625],
+ [309.83984375, 18.1640625],
+ [320.28125, 16.7578125],
+ [329.46875, 15.91015625],
+ [337.453125, 15.53515625],
+ [344.515625, 14.8203125],
+ [350.45703125, 14.4453125],
+ [354.64453125, 14.5546875],
+ [358.10546875, 14.921875],
+ [360.83203125, 15.5234375],
+ [362.796875, 16.3671875],
+ [364.1328125, 17.43359375],
+ [365.13671875, 18.6015625],
+ [365.8984375, 19.8203125],
+ [366.71484375, 21.30078125],
+ [368.34375, 23.59765625],
+ [370.37890625, 26.70703125],
+ [372.15625, 30.5],
+ [374.16015625, 34.390625],
+ [376.21875, 38.4921875],
+ [378.19140625, 43.921875],
+ [380.4140625, 50.31640625],
+ [382.671875, 56.2890625],
+ [384.48046875, 61.34765625],
+ [385.7890625, 65.14453125],
+ [386.5390625, 66.98828125],
+ [386.921875, 67.60546875],
+ [387.171875, 67.80859375],
+ [388.0390625, 68.32421875],
+ [392.23828125, 70.3671875],
+ [403.59765625, 76.4296875],
+ [419.5390625, 85.5],
+ [435.5078125, 93.82421875],
+ [451.3046875, 101.015625],
+ [465.05078125, 107.02734375],
+ [476.828125, 111.97265625],
+ [487.38671875, 115.578125],
+ [495.98046875, 118.03125],
+ [503.203125, 120.3515625],
+ [510.375, 122.3828125],
+ [517.8203125, 124.32421875],
+ [525.38671875, 126.9375],
+ [532.9765625, 130.12890625],
+ [539.046875, 133.22265625],
+ [543.85546875, 136.421875],
+ [549.28125, 140.84375],
+ [554.41015625, 146.04296875],
+ [558.34375, 151.4921875],
+ [561.859375, 157.09375],
+ [564.734375, 162.71875],
+ [566.95703125, 168.375],
+ [568.87109375, 174.33984375],
+ [570.41796875, 181.26953125],
+ [571.74609375, 189.37890625],
+ [572.55859375, 197.3515625],
+ [573.046875, 204.26171875],
+ [573.7421875, 210.9453125],
+ [574.38671875, 216.91796875],
+ [574.75, 222.8515625],
+ [575.0703125, 228.78515625],
+ [575.67578125, 234.0078125],
+ [576.26171875, 238.3515625],
+ [576.84765625, 242.64453125],
+ [577.328125, 247.53125],
+ [577.6484375, 252.56640625],
+ [577.80859375, 257.91015625],
+ [578.12890625, 263.2578125],
+ [578.44921875, 269.1875],
+ [578.16796875, 275.17578125],
+ [577.5234375, 281.078125],
+ [576.14453125, 287.59375],
+ [574.19921875, 296.390625],
+ [571.96484375, 306.03125],
+ [568.765625, 315.54296875],
+ [564.68359375, 325.640625],
+ [560.3671875, 335.03125],
+ [555.93359375, 343.68359375],
+ [551.56640625, 352.03515625],
+ [547.86328125, 359.2734375],
+ [543.82421875, 365.2421875],
+ [539.91015625, 370.0078125],
+ [537.37109375, 372.5546875],
+ [535.4765625, 374.23828125],
+ [533.37890625, 375.5859375],
+ [531.2578125, 376.75390625],
+ [528.46875, 378.96875],
+ [524.296875, 381.8359375],
+ [519.03515625, 385.31640625],
+ [513.50390625, 389.2890625],
+ [506.43359375, 394.55078125],
+ [497.18359375, 401.51953125],
+ [488.43359375, 408.40625],
+ [481.15234375, 414.0703125],
+ [475.64453125, 417.7578125],
+ [471.55078125, 420.32421875],
+ [468.73828125, 421.828125],
+ [467.1640625, 422.328125],
+ [465.9296875, 422.6953125],
+ [464.7109375, 422.91796875],
+ [463.2734375, 423.12890625],
+ [462.06640625, 423.33203125],
+ [460.88671875, 423.33203125],
+ [459.484375, 423.33203125],
+ [458.57421875, 423.33203125],
+ [457.9296875, 423.10546875],
+ [457.15234375, 422.796875],
+ [456.3984375, 422.5625],
+ [455.8828125, 422.41015625],
+ [455.55859375, 422.41015625],
+ [455.453125, 422.3203125],
+ [455.4453125, 422.06640625],
+ [455.4453125, 422.06640625],
+ ] as LocalPoint[];
+
+ updatePath(startPoint, points);
+ const selectedElements = getSelectedElements(h.elements, h.state);
+ expect(selectedElements.length).toBe(3);
+ expect(selectedElements.filter((e) => e.type === "arrow").length).toBe(1);
+ expect(selectedElements.filter((e) => e.type === "rectangle").length).toBe(
+ 1,
+ );
+ expect(selectedElements.filter((e) => e.type === "freedraw").length).toBe(
+ 1,
+ );
+ });
+
+ it("Intersects some and encloses some", () => {
+ const startPoint = pointFrom(112, -190);
+ const points = [
+ [0, 0],
+ [-0.1015625, 0],
+ [-6.265625, 3.09375],
+ [-18.3671875, 9.015625],
+ [-28.3125, 13.94921875],
+ [-38.03125, 19.0625],
+ [-52.578125, 28.72265625],
+ [-54.51953125, 33.00390625],
+ [-55.39453125, 36.07421875],
+ [-56.046875, 39.890625],
+ [-57.06640625, 45.2734375],
+ [-57.76171875, 51.2265625],
+ [-57.76171875, 56.16796875],
+ [-57.76171875, 60.96875],
+ [-57.76171875, 65.796875],
+ [-57.76171875, 70.54296875],
+ [-57.33203125, 75.21484375],
+ [-56.17578125, 79.5078125],
+ [-54.55078125, 83.5625],
+ [-51.88671875, 88.09375],
+ [-48.72265625, 92.46875],
+ [-45.32421875, 96.2421875],
+ [-41.62890625, 100.5859375],
+ [-37.9375, 104.92578125],
+ [-33.94921875, 108.91796875],
+ [-29.703125, 113.51953125],
+ [-24.45703125, 118.49609375],
+ [-18.66796875, 123.5390625],
+ [-12.7109375, 128.96484375],
+ [-6.2578125, 133.984375],
+ [0.203125, 138.5078125],
+ [7.1640625, 143.71875],
+ [16.08984375, 149.9765625],
+ [25.01953125, 156.1640625],
+ [33.8203125, 162.25],
+ [42.05078125, 167.79296875],
+ [48.75390625, 172.46484375],
+ [55.3984375, 177.90625],
+ [61.296875, 184.12890625],
+ [66.02734375, 191.21484375],
+ [69.765625, 198.109375],
+ [73.03515625, 204.79296875],
+ [76.09375, 212.26171875],
+ [78.984375, 219.52734375],
+ [81.58203125, 226.34765625],
+ [84.1640625, 232.3046875],
+ [86.7265625, 237.16796875],
+ [89.68359375, 241.34765625],
+ [93.83984375, 245.12890625],
+ [100.12109375, 249.328125],
+ [107.109375, 253.65625],
+ [114.08203125, 257.89453125],
+ [122.578125, 262.31640625],
+ [130.83984375, 266.359375],
+ [138.33203125, 269.8671875],
+ [144.984375, 272.3515625],
+ [150.265625, 274.1953125],
+ [155.42578125, 275.9296875],
+ [159.1328125, 276.73828125],
+ [161.2421875, 276.73828125],
+ [165.11328125, 276.7578125],
+ [172.546875, 276.76171875],
+ [183.14453125, 276.76171875],
+ [194.015625, 276.76171875],
+ [204.1796875, 276.76171875],
+ [213.484375, 276.76171875],
+ [221.40625, 276.76171875],
+ [228.47265625, 276.76171875],
+ [234.40234375, 276.67578125],
+ [240.28515625, 275.9765625],
+ [246.12109375, 274.59375],
+ [250.75390625, 272.8515625],
+ [255.046875, 270.18359375],
+ [259.6328125, 266.60546875],
+ [264.04296875, 262.4375],
+ [268.69140625, 256.69921875],
+ [273.25390625, 249.9375],
+ [277.85546875, 243.0546875],
+ [282.19140625, 236.5859375],
+ [285.24609375, 231.484375],
+ [287.39453125, 227.1875],
+ [289.078125, 223.78125],
+ [290.328125, 221.28125],
+ [291.0390625, 219.2109375],
+ [291.40625, 217.83984375],
+ [291.546875, 216.75390625],
+ [291.546875, 215.84375],
+ [291.75390625, 214.7734375],
+ [291.9609375, 213.15234375],
+ [291.9609375, 211.125],
+ [291.9609375, 208.6953125],
+ [291.9609375, 205.25],
+ [291.9609375, 201.4453125],
+ [291.62890625, 197.68359375],
+ [291.0625, 194.29296875],
+ [290.6484375, 192.21875],
+ [290.25390625, 190.8203125],
+ [289.88671875, 189.94140625],
+ [289.75, 189.53125],
+ [289.75, 189.2109375],
+ [289.7265625, 188.29296875],
+ [290.09375, 186.3125],
+ [293.04296875, 182.46875],
+ [298.671875, 177.46484375],
+ [305.45703125, 172.13671875],
+ [312.4921875, 167.35546875],
+ [318.640625, 163.6875],
+ [323.1484375, 161.0703125],
+ [326.484375, 159.37109375],
+ [329.8046875, 157.39453125],
+ [332.98046875, 155.2265625],
+ [336.09765625, 152.6875],
+ [339.14453125, 149.640625],
+ [342.37890625, 146.5078125],
+ [345.96875, 143.03125],
+ [349.4609375, 139.24609375],
+ [353.23046875, 134.83203125],
+ [356.68359375, 129.72265625],
+ [359.48828125, 123.9140625],
+ [362.76953125, 116.09765625],
+ [367.91796875, 93.69140625],
+ [368.23828125, 88.5546875],
+ [368.34375, 86.2890625],
+ [369.94921875, 80.15234375],
+ [372.7578125, 72.04296875],
+ [375.703125, 62.5],
+ [378.33203125, 52.72265625],
+ [380.109375, 44.4453125],
+ [381.40625, 37.59375],
+ [382.26953125, 31.95703125],
+ [382.71875, 26.60546875],
+ [382.81640625, 21.76171875],
+ [382.81640625, 17.84375],
+ [382.55859375, 13.9609375],
+ [382.27734375, 9.65625],
+ [381.67578125, 5.3515625],
+ [380.40625, 1.0703125],
+ [378.71484375, -3.2109375],
+ [376.48046875, -7.52734375],
+ [373.93359375, -11.71875],
+ [370.44140625, -16.32421875],
+ [365.86328125, -21.49609375],
+ [359.94921875, -26.8359375],
+ [353.33984375, -32.046875],
+ [345.84765625, -37.30859375],
+ [336.55859375, -43.21484375],
+ [326.34765625, -48.5859375],
+ [315.515625, -53.15234375],
+ [305.375, -56.67578125],
+ [296, -59.47265625],
+ [286.078125, -61.984375],
+ [276.078125, -63.78125],
+ [266.578125, -65.09765625],
+ [258.90625, -66.11328125],
+ [249.8984375, -67.34765625],
+ [238.84765625, -68.6796875],
+ [229.19921875, -70.01171875],
+ [219.66015625, -71.50390625],
+ [209.109375, -72.99609375],
+ [197.14453125, -74.625],
+ [186.52734375, -76.421875],
+ [176.66796875, -77.8203125],
+ [167.26953125, -79.1328125],
+ [159.57421875, -80.6328125],
+ [152.75, -81.4609375],
+ [146.4609375, -81.89453125],
+ [139.97265625, -82.23828125],
+ [133.546875, -82.23828125],
+ [127.84765625, -82.23828125],
+ [123.01953125, -82.23828125],
+ [117.9375, -81.9140625],
+ [112.59765625, -81.046875],
+ [107.3046875, -79.90234375],
+ [100.41796875, -78.45703125],
+ [92.74609375, -76.87890625],
+ [85.40625, -75.359375],
+ [77.546875, -73.80859375],
+ [69.71875, -72.6640625],
+ [62.4921875, -71.9609375],
+ [56.02734375, -71.23046875],
+ [50.37109375, -70.26171875],
+ [46.20703125, -69.32421875],
+ [43.45703125, -68.48046875],
+ [41.48046875, -67.5703125],
+ [39.99609375, -66.90234375],
+ [38.51171875, -66.23828125],
+ [36.7734375, -65.3671875],
+ [35.4609375, -64.359375],
+ [34.18359375, -63.328125],
+ [33.0078125, -62.54296875],
+ [31.8125, -61.76953125],
+ [30.5234375, -60.8984375],
+ [29.4921875, -60.09765625],
+ [28.5078125, -59.3828125],
+ [27.24609375, -58.61328125],
+ [25.49609375, -57.73828125],
+ [23.7421875, -56.859375],
+ [21.99609375, -55.984375],
+ [20.51953125, -55.16796875],
+ [19.4921875, -54.44140625],
+ [18.81640625, -53.84375],
+ [18.35546875, -53.52734375],
+ [18.0859375, -53.46484375],
+ [17.85546875, -53.44921875],
+ [17.85546875, -53.44921875],
+ ] as LocalPoint[];
+
+ updatePath(startPoint, points);
+
+ const selectedElements = getSelectedElements(h.elements, h.state);
+ expect(selectedElements.length).toBe(4);
+ expect(selectedElements.filter((e) => e.type === "line").length).toBe(1);
+ expect(selectedElements.filter((e) => e.type === "ellipse").length).toBe(1);
+ expect(selectedElements.filter((e) => e.type === "diamond").length).toBe(1);
+ expect(selectedElements.filter((e) => e.type === "freedraw").length).toBe(
+ 1,
+ );
+ });
+
+ it("Single linear element", () => {
+ const startPoint = pointFrom(62, -200);
+ const points = [
+ [0, 0],
+ [0, 0.1015625],
+ [-1.65625, 2.2734375],
+ [-8.43359375, 12.265625],
+ [-17.578125, 25.83203125],
+ [-25.484375, 37.38671875],
+ [-31.453125, 47.828125],
+ [-34.92578125, 55.21875],
+ [-37.1171875, 60.05859375],
+ [-38.4375, 63.49609375],
+ [-39.5, 66.6328125],
+ [-40.57421875, 69.84375],
+ [-41.390625, 73.53515625],
+ [-41.9296875, 77.078125],
+ [-42.40625, 79.71484375],
+ [-42.66796875, 81.83203125],
+ [-42.70703125, 83.32421875],
+ [-42.70703125, 84.265625],
+ [-42.70703125, 85.171875],
+ [-42.70703125, 86.078125],
+ [-42.70703125, 86.6484375],
+ [-42.70703125, 87],
+ [-42.70703125, 87.1796875],
+ [-42.70703125, 87.4296875],
+ [-42.70703125, 87.83203125],
+ [-42.70703125, 88.86328125],
+ [-42.70703125, 91.27734375],
+ [-42.70703125, 95.0703125],
+ [-42.44140625, 98.46875],
+ [-42.17578125, 100.265625],
+ [-42.17578125, 101.16015625],
+ [-42.16015625, 101.76171875],
+ [-42.0625, 102.12109375],
+ [-42.0625, 102.12109375],
+ ] as LocalPoint[];
+ updatePath(startPoint, points);
+
+ const selectedElements = getSelectedElements(h.elements, h.state);
+ expect(selectedElements.length).toBe(1);
+ expect(h.app.state.selectedLinearElement).toBeDefined();
+ });
+});
+
+describe("Special cases", () => {
+ it("Select only frame if its children are also selected", () => {
+ act(() => {
+ const elements = [
+ {
+ id: "CaUA2mmuudojzY98_oVXo",
+ type: "rectangle",
+ x: -96.64353835077907,
+ y: -270.1600585741129,
+ width: 146.8359375,
+ height: 104.921875,
+ angle: 0,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: "85VShCn1P9k81JqSeOg-c",
+ index: "aE",
+ roundness: {
+ type: 3,
+ },
+ seed: 227442978,
+ version: 15,
+ versionNonce: 204983970,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740959550684,
+ link: null,
+ locked: false,
+ },
+ {
+ id: "RZzDDA1DBJHw5OzHVNDvc",
+ type: "diamond",
+ x: 126.64943039922093,
+ y: -212.4920898241129,
+ width: 102.55859375,
+ height: 93.80078125,
+ angle: 0,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: "85VShCn1P9k81JqSeOg-c",
+ index: "aH",
+ roundness: {
+ type: 2,
+ },
+ seed: 955233890,
+ version: 14,
+ versionNonce: 2135303358,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740959550684,
+ link: null,
+ locked: false,
+ },
+ {
+ id: "CSVDDbC9vxqgO2uDahcE9",
+ type: "ellipse",
+ x: -20.999007100779068,
+ y: -87.0272460741129,
+ width: 116.13671875,
+ height: 70.7734375,
+ angle: 0,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ groupIds: [],
+ frameId: "85VShCn1P9k81JqSeOg-c",
+ index: "aI",
+ roundness: {
+ type: 2,
+ },
+ seed: 807647870,
+ version: 16,
+ versionNonce: 455740962,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740959550684,
+ link: null,
+ locked: false,
+ },
+ {
+ id: "85VShCn1P9k81JqSeOg-c",
+ type: "frame",
+ x: -164.95603835077907,
+ y: -353.5155273241129,
+ width: 451.04296875,
+ height: 397.09765625,
+ angle: 0,
+ strokeColor: "#bbb",
+ backgroundColor: "transparent",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 0,
+ opacity: 100,
+ groupIds: [],
+ frameId: null,
+ index: "aJ",
+ roundness: null,
+ seed: 1134892578,
+ version: 57,
+ versionNonce: 1699466238,
+ isDeleted: false,
+ boundElements: [],
+ updated: 1740959550367,
+ link: null,
+ locked: false,
+ name: null,
+ },
+ ].map((e) => ({
+ ...e,
+ index: null,
+ angle: e.angle as Radians,
+ })) as ExcalidrawElement[];
+
+ h.elements = elements;
+ });
+
+ const startPoint = pointFrom