{isShade && "⇧"}
diff --git a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
index 50594a59e..38e5cf8c5 100644
--- a/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
+++ b/packages/excalidraw/components/ColorPicker/PickerColorList.tsx
@@ -65,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 aa2c25ea0..1c8e4c4eb 100644
--- a/packages/excalidraw/components/ColorPicker/ShadeList.tsx
+++ b/packages/excalidraw/components/ColorPicker/ShadeList.tsx
@@ -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 6d18a9587..8531172fb 100644
--- a/packages/excalidraw/components/ColorPicker/TopPicks.tsx
+++ b/packages/excalidraw/components/ColorPicker/TopPicks.tsx
@@ -1,11 +1,14 @@
import clsx from "clsx";
import {
+ COLOR_OUTLINE_CONTRAST_THRESHOLD,
DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
DEFAULT_ELEMENT_STROKE_PICKS,
} from "@excalidraw/common";
+import { isColorDark } from "./colorPickerUtils";
+
import type { ColorPickerType } from "./colorPickerUtils";
interface TopPicksProps {
@@ -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 4925a3145..f572bd49f 100644
--- a/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
+++ b/packages/excalidraw/components/ColorPicker/colorPickerUtils.ts
@@ -93,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, "")
@@ -114,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/CommandPalette/CommandPalette.tsx b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
index 5508aa235..60289cfa1 100644
--- a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
+++ b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx
@@ -317,6 +317,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/HintViewer.tsx b/packages/excalidraw/components/HintViewer.tsx
index 6eb1a2186..5072e4471 100644
--- a/packages/excalidraw/components/HintViewer.tsx
+++ b/packages/excalidraw/components/HintViewer.tsx
@@ -120,7 +120,7 @@ const getHints = ({
!appState.editingTextElement &&
!appState.editingLinearElement
) {
- return t("hints.deepBoxSelect");
+ return [t("hints.deepBoxSelect")];
}
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
@@ -128,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/LibraryMenuItems.tsx b/packages/excalidraw/components/LibraryMenuItems.tsx
index f70315953..160cc2640 100644
--- a/packages/excalidraw/components/LibraryMenuItems.tsx
+++ b/packages/excalidraw/components/LibraryMenuItems.tsx
@@ -166,7 +166,7 @@ export default function LibraryMenuItems({
type: "everything",
elements: item.elements,
randomizeSeed: true,
- }).newElements,
+ }).duplicatedElements,
};
});
},
diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
index 1f4f57433..6505c788a 100644
--- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
+++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx
@@ -34,6 +34,7 @@ type InteractiveCanvasProps = {
selectionNonce: number | undefined;
scale: number;
appState: InteractiveCanvasAppState;
+ renderScrollbars: boolean;
device: Device;
renderInteractiveSceneCallback: (
data: RenderInteractiveSceneCallback,
@@ -143,7 +144,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
remotePointerUsernames,
remotePointerUserStates,
selectionColor,
- renderScrollbars: false,
+ renderScrollbars: props.renderScrollbars,
},
device: props.device,
callback: props.renderInteractiveSceneCallback,
@@ -230,7 +231,8 @@ const areEqual = (
// on appState)
prevProps.elementsMap !== nextProps.elementsMap ||
prevProps.visibleElements !== nextProps.visibleElements ||
- prevProps.selectedElements !== nextProps.selectedElements
+ prevProps.selectedElements !== nextProps.selectedElements ||
+ prevProps.renderScrollbars !== nextProps.renderScrollbars
) {
return false;
}
diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx
index b70c8ace6..5a498ebac 100644
--- a/packages/excalidraw/components/canvases/StaticCanvas.tsx
+++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx
@@ -87,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/icons.tsx b/packages/excalidraw/components/icons.tsx
index c6d7f9473..f3808a69d 100644
--- a/packages/excalidraw/components/icons.tsx
+++ b/packages/excalidraw/components/icons.tsx
@@ -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/css/styles.scss b/packages/excalidraw/css/styles.scss
index 33f9b4df0..6f1d9cd48 100644
--- a/packages/excalidraw/css/styles.scss
+++ b/packages/excalidraw/css/styles.scss
@@ -173,7 +173,7 @@ body.excalidraw-cursor-resize * {
.buttonList {
flex-wrap: wrap;
display: flex;
- column-gap: 0.375rem;
+ column-gap: 0.5rem;
row-gap: 0.5rem;
label {
@@ -386,16 +386,10 @@ body.excalidraw-cursor-resize * {
.App-menu__left {
overflow-y: auto;
- padding: 0.75rem 0.75rem 0.25rem 0.75rem;
- width: 11.875rem;
+ padding: 0.75rem;
+ width: 12.5rem;
box-sizing: border-box;
position: absolute;
-
- .buttonList label,
- .buttonList button,
- .buttonList .zIndexButton {
- --button-bg: transparent;
- }
}
.dropdown-select {
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/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/index.ts b/packages/excalidraw/data/index.ts
index ac8147e85..93d5f5677 100644
--- a/packages/excalidraw/data/index.ts
+++ b/packages/excalidraw/data/index.ts
@@ -5,6 +5,7 @@ import {
isFirefox,
MIME_TYPES,
cloneJSON,
+ SVG_DOCUMENT_PREAMBLE,
} from "@excalidraw/common";
import { getNonDeletedElements } from "@excalidraw/element";
@@ -134,7 +135,11 @@ export const exportCanvas = async (
if (type === "svg") {
return fileSave(
svgPromise.then((svg) => {
- return new Blob([svg.outerHTML], { type: MIME_TYPES.svg });
+ // adding SVG preamble so that older software parse the SVG file
+ // properly
+ return new Blob([SVG_DOCUMENT_PREAMBLE + svg.outerHTML], {
+ type: MIME_TYPES.svg,
+ });
}),
{
description: "Export to SVG",
diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts
index 30702f130..1811cbb57 100644
--- a/packages/excalidraw/data/restore.ts
+++ b/packages/excalidraw/data/restore.ts
@@ -86,6 +86,7 @@ export const AllowedExcalidrawActiveTools: Record<
boolean
> = {
selection: true,
+ lasso: true,
text: true,
rectangle: true,
diamond: true,
@@ -438,7 +439,7 @@ const repairContainerElement = (
// if defined, lest boundElements is stale
!boundElement.containerId
) {
- (boundElement as Mutable).containerId =
+ (boundElement as Mutable).containerId =
container.id;
}
}
@@ -463,6 +464,10 @@ const repairBoundElement = (
? elementsMap.get(boundElement.containerId)
: null;
+ (boundElement as Mutable).angle = (
+ isArrowElement(container) ? 0 : container?.angle ?? 0
+ ) as Radians;
+
if (!container) {
boundElement.containerId = null;
return;
diff --git a/packages/excalidraw/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts
index 27643e7e1..0b0718e8e 100644
--- a/packages/excalidraw/data/transform.test.ts
+++ b/packages/excalidraw/data/transform.test.ts
@@ -427,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: {
@@ -512,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: {
@@ -730,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 9def9f5fc..15ad1ffde 100644
--- a/packages/excalidraw/data/transform.ts
+++ b/packages/excalidraw/data/transform.ts
@@ -36,6 +36,8 @@ 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 {
@@ -463,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/eraser/index.ts b/packages/excalidraw/eraser/index.ts
new file mode 100644
index 000000000..2ea668aef
--- /dev/null
+++ b/packages/excalidraw/eraser/index.ts
@@ -0,0 +1,243 @@
+import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
+import { getElementLineSegments } from "@excalidraw/element/bounds";
+import {
+ lineSegment,
+ lineSegmentIntersectionPoints,
+ pointFrom,
+} from "@excalidraw/math";
+
+import { getElementsInGroup } from "@excalidraw/element/groups";
+
+import { getElementShape } from "@excalidraw/element/shapes";
+import { shouldTestInside } from "@excalidraw/element/collision";
+import { isPointInShape } from "@excalidraw/utils/collision";
+import {
+ hasBoundTextElement,
+ isBoundToContainer,
+} from "@excalidraw/element/typeChecks";
+import { getBoundTextElementId } from "@excalidraw/element/textElement";
+
+import type { GeometricShape } from "@excalidraw/utils/shape";
+import type {
+ ElementsSegmentsMap,
+ GlobalPoint,
+ LineSegment,
+} from "@excalidraw/math/types";
+import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
+
+import { AnimatedTrail } from "../animated-trail";
+
+import type { AnimationFrameHandler } from "../animation-frame-handler";
+
+import type App from "../components/App";
+
+// just enough to form a segment; this is sufficient for eraser
+const POINTS_ON_TRAIL = 2;
+
+export class EraserTrail extends AnimatedTrail {
+ private elementsToErase: Set = new Set();
+ private groupsToErase: Set = new Set();
+ private segmentsCache: Map[]> = new Map();
+ private geometricShapesCache: Map> =
+ new Map();
+
+ constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
+ super(animationFrameHandler, app, {
+ streamline: 0.2,
+ size: 5,
+ keepHead: true,
+ sizeMapping: (c) => {
+ const DECAY_TIME = 200;
+ const DECAY_LENGTH = 10;
+ const t = Math.max(
+ 0,
+ 1 - (performance.now() - c.pressure) / DECAY_TIME,
+ );
+ const l =
+ (DECAY_LENGTH -
+ Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
+ DECAY_LENGTH;
+
+ return Math.min(easeOut(l), easeOut(t));
+ },
+ fill: () =>
+ app.state.theme === THEME.LIGHT
+ ? "rgba(0, 0, 0, 0.2)"
+ : "rgba(255, 255, 255, 0.2)",
+ });
+ }
+
+ startPath(x: number, y: number): void {
+ this.endPath();
+ super.startPath(x, y);
+ this.elementsToErase.clear();
+ }
+
+ addPointToPath(x: number, y: number, restore = false) {
+ super.addPointToPath(x, y);
+
+ const elementsToEraser = this.updateElementsToBeErased(restore);
+
+ return elementsToEraser;
+ }
+
+ private updateElementsToBeErased(restoreToErase?: boolean) {
+ let eraserPath: GlobalPoint[] =
+ super
+ .getCurrentTrail()
+ ?.originalPoints?.map((p) => pointFrom(p[0], p[1])) || [];
+
+ // for efficiency and avoid unnecessary calculations,
+ // take only POINTS_ON_TRAIL points to form some number of segments
+ eraserPath = eraserPath?.slice(eraserPath.length - POINTS_ON_TRAIL);
+
+ const candidateElements = this.app.visibleElements.filter(
+ (el) => !el.locked,
+ );
+
+ const candidateElementsMap = arrayToMap(candidateElements);
+
+ const pathSegments = eraserPath.reduce((acc, point, index) => {
+ if (index === 0) {
+ return acc;
+ }
+ acc.push(lineSegment(eraserPath[index - 1], point));
+ return acc;
+ }, [] as LineSegment[]);
+
+ if (pathSegments.length === 0) {
+ return [];
+ }
+
+ for (const element of candidateElements) {
+ // restore only if already added to the to-be-erased set
+ if (restoreToErase && this.elementsToErase.has(element.id)) {
+ const intersects = eraserTest(
+ pathSegments,
+ element,
+ this.segmentsCache,
+ this.geometricShapesCache,
+ candidateElementsMap,
+ this.app,
+ );
+
+ if (intersects) {
+ const shallowestGroupId = element.groupIds.at(-1)!;
+
+ if (this.groupsToErase.has(shallowestGroupId)) {
+ const elementsInGroup = getElementsInGroup(
+ this.app.scene.getNonDeletedElementsMap(),
+ shallowestGroupId,
+ );
+ for (const elementInGroup of elementsInGroup) {
+ this.elementsToErase.delete(elementInGroup.id);
+ }
+ this.groupsToErase.delete(shallowestGroupId);
+ }
+
+ if (isBoundToContainer(element)) {
+ this.elementsToErase.delete(element.containerId);
+ }
+
+ if (hasBoundTextElement(element)) {
+ const boundText = getBoundTextElementId(element);
+
+ if (boundText) {
+ this.elementsToErase.delete(boundText);
+ }
+ }
+
+ this.elementsToErase.delete(element.id);
+ }
+ } else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
+ const intersects = eraserTest(
+ pathSegments,
+ element,
+ this.segmentsCache,
+ this.geometricShapesCache,
+ candidateElementsMap,
+ this.app,
+ );
+
+ if (intersects) {
+ const shallowestGroupId = element.groupIds.at(-1)!;
+
+ if (!this.groupsToErase.has(shallowestGroupId)) {
+ const elementsInGroup = getElementsInGroup(
+ this.app.scene.getNonDeletedElementsMap(),
+ shallowestGroupId,
+ );
+
+ for (const elementInGroup of elementsInGroup) {
+ this.elementsToErase.add(elementInGroup.id);
+ }
+ this.groupsToErase.add(shallowestGroupId);
+ }
+
+ if (hasBoundTextElement(element)) {
+ const boundText = getBoundTextElementId(element);
+
+ if (boundText) {
+ this.elementsToErase.add(boundText);
+ }
+ }
+
+ if (isBoundToContainer(element)) {
+ this.elementsToErase.add(element.containerId);
+ }
+
+ this.elementsToErase.add(element.id);
+ }
+ }
+ }
+
+ return Array.from(this.elementsToErase);
+ }
+
+ endPath(): void {
+ super.endPath();
+ super.clearTrails();
+ this.elementsToErase.clear();
+ this.groupsToErase.clear();
+ this.segmentsCache.clear();
+ }
+}
+
+const eraserTest = (
+ pathSegments: LineSegment[],
+ element: ExcalidrawElement,
+ elementsSegments: ElementsSegmentsMap,
+ shapesCache: Map>,
+ elementsMap: ElementsMap,
+ app: App,
+): boolean => {
+ let shape = shapesCache.get(element.id);
+
+ if (!shape) {
+ shape = getElementShape(element, elementsMap);
+ shapesCache.set(element.id, shape);
+ }
+
+ const lastPoint = pathSegments[pathSegments.length - 1][1];
+ if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
+ return true;
+ }
+
+ let elementSegments = elementsSegments.get(element.id);
+
+ if (!elementSegments) {
+ elementSegments = getElementLineSegments(element, elementsMap);
+ elementsSegments.set(element.id, elementSegments);
+ }
+
+ return pathSegments.some((pathSegment) =>
+ elementSegments?.some(
+ (elementSegment) =>
+ lineSegmentIntersectionPoints(
+ pathSegment,
+ elementSegment,
+ app.getElementHitThreshold(),
+ ) !== null,
+ ),
+ );
+};
diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx
index 5ea746754..17c59a1b5 100644
--- a/packages/excalidraw/index.tsx
+++ b/packages/excalidraw/index.tsx
@@ -53,6 +53,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
renderEmbeddable,
aiEnabled,
showDeprecatedFonts,
+ renderScrollbars,
} = props;
const canvasActions = props.UIOptions?.canvasActions;
@@ -143,6 +144,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
renderEmbeddable={renderEmbeddable}
aiEnabled={aiEnabled !== false}
showDeprecatedFonts={showDeprecatedFonts}
+ renderScrollbars={renderScrollbars}
>
{children}
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..d05f39998
--- /dev/null
+++ b/packages/excalidraw/lasso/utils.ts
@@ -0,0 +1,109 @@
+import { simplify } from "points-on-curve";
+
+import {
+ polygonFromPoints,
+ lineSegment,
+ lineSegmentIntersectionPoints,
+ polygonIncludesPointNonZero,
+} from "@excalidraw/math";
+
+import type {
+ ElementsSegmentsMap,
+ GlobalPoint,
+ LineSegment,
+} from "@excalidraw/math/types";
+import type { ExcalidrawElement } from "@excalidraw/element/types";
+
+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 92ab19b05..8f0e88c6b 100644
--- a/packages/excalidraw/locales/en.json
+++ b/packages/excalidraw/locales/en.json
@@ -278,6 +278,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 29239b486..27fae8455 100644
--- a/packages/excalidraw/package.json
+++ b/packages/excalidraw/package.json
@@ -76,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/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts
index 3000c206c..69c6a8196 100644
--- a/packages/excalidraw/renderer/interactiveScene.ts
+++ b/packages/excalidraw/renderer/interactiveScene.ts
@@ -1182,7 +1182,7 @@ const _renderInteractiveScene = ({
let scrollBars;
if (renderConfig.renderScrollbars) {
scrollBars = getScrollBars(
- visibleElements,
+ elementsMap,
normalizedWidth,
normalizedHeight,
appState,
diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts
index dc66837fb..5afe3a4c5 100644
--- a/packages/excalidraw/scene/Scene.ts
+++ b/packages/excalidraw/scene/Scene.ts
@@ -6,6 +6,7 @@ import {
toBrandedType,
isDevEnv,
isTestEnv,
+ isReadonlyArray,
} from "@excalidraw/common";
import { isNonDeletedElement } from "@excalidraw/element";
import { isFrameLikeElement } from "@excalidraw/element/typeChecks";
@@ -292,11 +293,9 @@ class Scene {
}
replaceAllElements(nextElements: ElementsMapOrArray) {
- const _nextElements =
- // ts doesn't like `Array.isArray` of `instanceof Map`
- nextElements instanceof Array
- ? nextElements
- : Array.from(nextElements.values());
+ const _nextElements = isReadonlyArray(nextElements)
+ ? nextElements
+ : Array.from(nextElements.values());
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
validateIndicesThrottled(_nextElements);
diff --git a/packages/excalidraw/scene/scrollbars.ts b/packages/excalidraw/scene/scrollbars.ts
index 4fa4349f2..35fecba37 100644
--- a/packages/excalidraw/scene/scrollbars.ts
+++ b/packages/excalidraw/scene/scrollbars.ts
@@ -2,24 +2,23 @@ import { getGlobalCSSVariable } from "@excalidraw/common";
import { getCommonBounds } from "@excalidraw/element/bounds";
-import type { ExcalidrawElement } from "@excalidraw/element/types";
-
import { getLanguage } from "../i18n";
import type { InteractiveCanvasAppState } from "../types";
-import type { ScrollBars } from "./types";
+import type { RenderableElementsMap, ScrollBars } from "./types";
export const SCROLLBAR_MARGIN = 4;
export const SCROLLBAR_WIDTH = 6;
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
+// The scrollbar represents where the viewport is in relationship to the scene
export const getScrollBars = (
- elements: readonly ExcalidrawElement[],
+ elements: RenderableElementsMap,
viewportWidth: number,
viewportHeight: number,
appState: InteractiveCanvasAppState,
): ScrollBars => {
- if (!elements.length) {
+ if (!elements.size) {
return {
horizontal: null,
vertical: null,
@@ -33,9 +32,6 @@ export const getScrollBars = (
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
- const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
- const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
-
const safeArea = {
top: parseInt(getGlobalCSSVariable("sat")) || 0,
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
@@ -46,10 +42,8 @@ export const getScrollBars = (
const isRTL = getLanguage().rtl;
// The viewport is the rectangle currently visible for the user
- const viewportMinX =
- -appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
- const viewportMinY =
- -appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
+ const viewportMinX = -appState.scrollX + safeArea.left;
+ const viewportMinY = -appState.scrollY + safeArea.top;
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
@@ -59,8 +53,43 @@ export const getScrollBars = (
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
- // The scrollbar represents where the viewport is in relationship to the scene
+ // the elements-only bbox
+ const sceneWidth = elementsMaxX - elementsMinX;
+ const sceneHeight = elementsMaxY - elementsMinY;
+ // scene (elements) bbox + the viewport bbox that extends outside of it
+ const extendedSceneWidth = sceneMaxX - sceneMinX;
+ const extendedSceneHeight = sceneMaxY - sceneMinY;
+
+ const scrollWidthOffset =
+ Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right) +
+ SCROLLBAR_WIDTH * 2;
+
+ const scrollbarWidth =
+ viewportWidth * (viewportWidthWithZoom / extendedSceneWidth) -
+ scrollWidthOffset;
+
+ const scrollbarHeightOffset =
+ Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom) +
+ SCROLLBAR_WIDTH * 2;
+
+ const scrollbarHeight =
+ viewportHeight * (viewportHeightWithZoom / extendedSceneHeight) -
+ scrollbarHeightOffset;
+ // NOTE the delta multiplier calculation isn't quite correct when viewport
+ // is extended outside the scene (elements) bbox as there's some small
+ // accumulation error. I'll let this be an exercise for others to fix. ^^
+ const horizontalDeltaMultiplier =
+ extendedSceneWidth > sceneWidth
+ ? (extendedSceneWidth * appState.zoom.value) /
+ (scrollbarWidth + scrollWidthOffset)
+ : viewportWidth / (scrollbarWidth + scrollWidthOffset);
+
+ const verticalDeltaMultiplier =
+ extendedSceneHeight > sceneHeight
+ ? (extendedSceneHeight * appState.zoom.value) /
+ (scrollbarHeight + scrollbarHeightOffset)
+ : viewportHeight / (scrollbarHeight + scrollbarHeightOffset);
return {
horizontal:
viewportMinX === sceneMinX && viewportMaxX === sceneMaxX
@@ -68,18 +97,17 @@ export const getScrollBars = (
: {
x:
Math.max(safeArea.left, SCROLLBAR_MARGIN) +
- ((viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX)) *
- viewportWidth,
+ SCROLLBAR_WIDTH +
+ ((viewportMinX - sceneMinX) / extendedSceneWidth) * viewportWidth,
y:
viewportHeight -
SCROLLBAR_WIDTH -
Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
- width:
- ((viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX)) *
- viewportWidth -
- Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
+ width: scrollbarWidth,
height: SCROLLBAR_WIDTH,
+ deltaMultiplier: horizontalDeltaMultiplier,
},
+
vertical:
viewportMinY === sceneMinY && viewportMaxY === sceneMaxY
? null
@@ -90,14 +118,13 @@ export const getScrollBars = (
SCROLLBAR_WIDTH -
Math.max(safeArea.right, SCROLLBAR_MARGIN),
y:
- ((viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY)) *
- viewportHeight +
- Math.max(safeArea.top, SCROLLBAR_MARGIN),
+ Math.max(safeArea.top, SCROLLBAR_MARGIN) +
+ SCROLLBAR_WIDTH +
+ ((viewportMinY - sceneMinY) / extendedSceneHeight) *
+ viewportHeight,
width: SCROLLBAR_WIDTH,
- height:
- ((viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY)) *
- viewportHeight -
- Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom),
+ height: scrollbarHeight,
+ deltaMultiplier: verticalDeltaMultiplier,
},
};
};
diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts
index 08b05a57d..12a5e27a8 100644
--- a/packages/excalidraw/scene/types.ts
+++ b/packages/excalidraw/scene/types.ts
@@ -130,12 +130,14 @@ export type ScrollBars = {
y: number;
width: number;
height: number;
+ deltaMultiplier: number;
} | null;
vertical: {
x: number;
y: number;
width: number;
height: number;
+ deltaMultiplier: number;
} | null;
};
diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap
index 89629b93e..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",
@@ -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 3f523d005..7b249da27 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",
@@ -7325,8 +7348,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"updated": 1,
"version": 7,
"width": 10,
- "x": -10,
- "y": -10,
+ "x": 0,
+ "y": 0,
}
`;
@@ -7399,8 +7422,8 @@ History {
"strokeWidth": 2,
"type": "arrow",
"width": 10,
- "x": -10,
- "y": -10,
+ "x": 0,
+ "y": 0,
},
"inserted": {
"isDeleted": true,
@@ -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",
@@ -12099,8 +12138,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"updated": 1,
"version": 3,
"width": 10,
- "x": 10,
- "y": 10,
+ "x": -10,
+ "y": -10,
}
`;
@@ -12153,8 +12192,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"updated": 1,
"version": 5,
"width": 50,
- "x": 60,
- "y": 0,
+ "x": 40,
+ "y": -20,
}
`;
@@ -12207,8 +12246,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"updated": 1,
"version": 4,
"width": 50,
- "x": 150,
- "y": -10,
+ "x": 130,
+ "y": -30,
}
`;
@@ -12262,8 +12301,8 @@ History {
"strokeWidth": 2,
"type": "rectangle",
"width": 10,
- "x": 10,
- "y": 10,
+ "x": -10,
+ "y": -10,
},
"inserted": {
"isDeleted": true,
@@ -12348,8 +12387,8 @@ History {
"strokeWidth": 2,
"type": "freedraw",
"width": 50,
- "x": 150,
- "y": -10,
+ "x": 130,
+ "y": -30,
},
"inserted": {
"isDeleted": true,
@@ -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",
@@ -15570,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",
@@ -16189,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",
@@ -16808,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",
@@ -17518,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",
@@ -18265,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",
@@ -18742,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",
@@ -19267,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",
@@ -19726,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 4001c3b17..5078a31a0 100644
--- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap
+++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap
@@ -1,40 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
-{
- "angle": 0,
- "backgroundColor": "transparent",
- "boundElements": null,
- "customData": undefined,
- "fillStyle": "solid",
- "frameId": null,
- "groupIds": [],
- "height": 50,
- "id": "id2",
- "index": "Zz",
- "isDeleted": false,
- "link": null,
- "locked": false,
- "opacity": 100,
- "roughness": 1,
- "roundness": {
- "type": 3,
- },
- "seed": 238820263,
- "strokeColor": "#1e1e1e",
- "strokeStyle": "solid",
- "strokeWidth": 2,
- "type": "rectangle",
- "updated": 1,
- "version": 6,
- "versionNonce": 1604849351,
- "width": 30,
- "x": 30,
- "y": 20,
-}
-`;
-
-exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
{
"angle": 0,
"backgroundColor": "transparent",
@@ -61,7 +27,41 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
"type": "rectangle",
"updated": 1,
"version": 5,
- "versionNonce": 23633383,
+ "versionNonce": 1505387817,
+ "width": 30,
+ "x": 30,
+ "y": 20,
+}
+`;
+
+exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
+{
+ "angle": 0,
+ "backgroundColor": "transparent",
+ "boundElements": null,
+ "customData": undefined,
+ "fillStyle": "solid",
+ "frameId": null,
+ "groupIds": [],
+ "height": 50,
+ "id": "id2",
+ "index": "a1",
+ "isDeleted": false,
+ "link": null,
+ "locked": false,
+ "opacity": 100,
+ "roughness": 1,
+ "roundness": {
+ "type": 3,
+ },
+ "seed": 1604849351,
+ "strokeColor": "#1e1e1e",
+ "strokeStyle": "solid",
+ "strokeWidth": 2,
+ "type": "rectangle",
+ "updated": 1,
+ "version": 7,
+ "versionNonce": 915032327,
"width": 30,
"x": -10,
"y": 60,
diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap
index 4e9c659d0..68d4d5d79 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",
@@ -2032,7 +2038,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"scrolledOutside": false,
"searchMatches": [],
"selectedElementIds": {
- "id0": true,
+ "id2": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
@@ -2122,8 +2128,16 @@ History {
HistoryEntry {
"appStateChange": AppStateChange {
"delta": Delta {
- "deleted": {},
- "inserted": {},
+ "deleted": {
+ "selectedElementIds": {
+ "id2": true,
+ },
+ },
+ "inserted": {
+ "selectedElementIds": {
+ "id0": true,
+ },
+ },
},
},
"elementsChange": ElementsChange {
@@ -2139,7 +2153,7 @@ History {
"frameId": null,
"groupIds": [],
"height": 10,
- "index": "Zz",
+ "index": "a1",
"isDeleted": false,
"link": null,
"locked": false,
@@ -2153,26 +2167,15 @@ History {
"strokeWidth": 2,
"type": "rectangle",
"width": 10,
- "x": 10,
- "y": 10,
+ "x": 20,
+ "y": 20,
},
"inserted": {
"isDeleted": true,
},
},
},
- "updated": Map {
- "id0" => Delta {
- "deleted": {
- "x": 20,
- "y": 20,
- },
- "inserted": {
- "x": 10,
- "y": 10,
- },
- },
- },
+ "updated": Map {},
},
},
],
@@ -2188,6 +2191,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
@@ -2368,6 +2372,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",
@@ -2688,6 +2693,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",
@@ -2934,6 +2940,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",
@@ -3177,6 +3184,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",
@@ -3407,6 +3415,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",
@@ -3663,6 +3672,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",
@@ -3974,6 +3984,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",
@@ -4396,6 +4407,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",
@@ -4679,6 +4691,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",
@@ -4932,6 +4945,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",
@@ -5142,6 +5156,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",
@@ -5341,6 +5356,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",
@@ -5723,6 +5739,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",
@@ -6013,6 +6030,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",
@@ -6821,6 +6839,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",
@@ -7151,6 +7170,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",
@@ -7427,6 +7447,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",
@@ -7661,6 +7682,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",
@@ -7898,6 +7920,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",
@@ -8078,6 +8101,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",
@@ -8258,6 +8282,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",
@@ -8438,6 +8463,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",
@@ -8661,6 +8687,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",
@@ -8883,6 +8910,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",
@@ -9077,6 +9105,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",
@@ -9300,6 +9329,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",
@@ -9480,6 +9510,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",
@@ -9702,6 +9733,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",
@@ -9882,6 +9914,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",
@@ -10076,6 +10109,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",
@@ -10256,6 +10290,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",
@@ -10340,13 +10375,13 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"scrolledOutside": false,
"searchMatches": [],
"selectedElementIds": {
- "id0": true,
- "id1": true,
- "id2": true,
+ "id6": true,
+ "id8": true,
+ "id9": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {
- "id4": true,
+ "id7": true,
},
"selectedLinearElement": null,
"selectionElement": null,
@@ -10610,8 +10645,26 @@ History {
HistoryEntry {
"appStateChange": AppStateChange {
"delta": Delta {
- "deleted": {},
- "inserted": {},
+ "deleted": {
+ "selectedElementIds": {
+ "id6": true,
+ "id8": true,
+ "id9": true,
+ },
+ "selectedGroupIds": {
+ "id7": true,
+ },
+ },
+ "inserted": {
+ "selectedElementIds": {
+ "id0": true,
+ "id1": true,
+ "id2": true,
+ },
+ "selectedGroupIds": {
+ "id4": true,
+ },
+ },
},
},
"elementsChange": ElementsChange {
@@ -10629,7 +10682,7 @@ History {
"id7",
],
"height": 10,
- "index": "Zx",
+ "index": "a3",
"isDeleted": false,
"link": null,
"locked": false,
@@ -10643,8 +10696,8 @@ History {
"strokeWidth": 2,
"type": "rectangle",
"width": 10,
- "x": 10,
- "y": 10,
+ "x": 20,
+ "y": 20,
},
"inserted": {
"isDeleted": true,
@@ -10662,7 +10715,7 @@ History {
"id7",
],
"height": 10,
- "index": "Zy",
+ "index": "a4",
"isDeleted": false,
"link": null,
"locked": false,
@@ -10676,8 +10729,8 @@ History {
"strokeWidth": 2,
"type": "rectangle",
"width": 10,
- "x": 30,
- "y": 10,
+ "x": 40,
+ "y": 20,
},
"inserted": {
"isDeleted": true,
@@ -10695,7 +10748,7 @@ History {
"id7",
],
"height": 10,
- "index": "Zz",
+ "index": "a5",
"isDeleted": false,
"link": null,
"locked": false,
@@ -10709,46 +10762,15 @@ History {
"strokeWidth": 2,
"type": "rectangle",
"width": 10,
- "x": 50,
- "y": 10,
+ "x": 60,
+ "y": 20,
},
"inserted": {
"isDeleted": true,
},
},
},
- "updated": Map {
- "id0" => Delta {
- "deleted": {
- "x": 20,
- "y": 20,
- },
- "inserted": {
- "x": 10,
- "y": 10,
- },
- },
- "id1" => Delta {
- "deleted": {
- "x": 40,
- "y": 20,
- },
- "inserted": {
- "x": 30,
- "y": 10,
- },
- },
- "id2" => Delta {
- "deleted": {
- "x": 60,
- "y": 20,
- },
- "inserted": {
- "x": 50,
- "y": 10,
- },
- },
- },
+ "updated": Map {},
},
},
],
@@ -10764,6 +10786,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",
@@ -11041,6 +11064,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",
@@ -11167,6 +11191,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",
@@ -11366,6 +11391,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",
@@ -11677,6 +11703,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",
@@ -12089,6 +12116,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",
@@ -12702,6 +12730,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",
@@ -12831,6 +12860,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",
@@ -13415,6 +13445,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",
@@ -13753,6 +13784,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",
@@ -14018,6 +14050,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",
@@ -14144,6 +14177,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",
@@ -14523,6 +14557,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "text",
@@ -14649,6 +14684,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/clipboard.test.tsx b/packages/excalidraw/tests/clipboard.test.tsx
index 0759afd94..7d0e3906c 100644
--- a/packages/excalidraw/tests/clipboard.test.tsx
+++ b/packages/excalidraw/tests/clipboard.test.tsx
@@ -307,6 +307,41 @@ describe("pasting & frames", () => {
});
});
+ it("should remove element from frame when pasted outside", async () => {
+ const frame = API.createElement({
+ type: "frame",
+ width: 100,
+ height: 100,
+ x: 0,
+ y: 0,
+ });
+ const rect = API.createElement({
+ type: "rectangle",
+ frameId: frame.id,
+ x: 10,
+ y: 10,
+ width: 50,
+ height: 50,
+ });
+
+ API.setElements([frame]);
+
+ const clipboardJSON = await serializeAsClipboardJSON({
+ elements: [rect],
+ files: null,
+ });
+
+ mouse.moveTo(150, 150);
+
+ pasteWithCtrlCmdV(clipboardJSON);
+
+ await waitFor(() => {
+ expect(h.elements.length).toBe(2);
+ expect(h.elements[1].type).toBe(rect.type);
+ expect(h.elements[1].frameId).toBe(null);
+ });
+ });
+
it("should filter out elements not overlapping frame", async () => {
const frame = API.createElement({
type: "frame",
diff --git a/packages/excalidraw/tests/cropElement.test.tsx b/packages/excalidraw/tests/cropElement.test.tsx
index 8011483fa..8764962fe 100644
--- a/packages/excalidraw/tests/cropElement.test.tsx
+++ b/packages/excalidraw/tests/cropElement.test.tsx
@@ -218,7 +218,7 @@ describe("Cropping and other features", async () => {
initialHeight / 2,
]);
Keyboard.keyDown(KEYS.ESCAPE);
- const duplicatedImage = duplicateElement(null, new Map(), image, {});
+ const duplicatedImage = duplicateElement(null, new Map(), image);
act(() => {
h.app.scene.insertElement(duplicatedImage);
});
diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts
index 09aa308a5..3a83f2763 100644
--- a/packages/excalidraw/tests/helpers/api.ts
+++ b/packages/excalidraw/tests/helpers/api.ts
@@ -444,7 +444,6 @@ export class API {
const text = API.createElement({
type: "text",
- id: "text2",
width: 50,
height: 20,
containerId: arrow.id,
diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts
index c328ae105..38070d38b 100644
--- a/packages/excalidraw/tests/helpers/ui.ts
+++ b/packages/excalidraw/tests/helpers/ui.ts
@@ -20,7 +20,7 @@ import {
isTextElement,
isFrameLikeElement,
} from "@excalidraw/element/typeChecks";
-import { KEYS, arrayToMap } from "@excalidraw/common";
+import { KEYS, arrayToMap, elementCenterPoint } from "@excalidraw/common";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
@@ -151,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),
@@ -166,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)) {
@@ -180,10 +180,17 @@ export class Pointer {
public clientX = 0;
public clientY = 0;
+ static activePointers: Pointer[] = [];
+ static resetAll() {
+ Pointer.activePointers.forEach((pointer) => pointer.reset());
+ }
+
constructor(
private readonly pointerType: "mouse" | "touch" | "pen",
private readonly pointerId = 1,
- ) {}
+ ) {
+ Pointer.activePointers.push(this);
+ }
reset() {
this.clientX = 0;
@@ -402,7 +409,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/lasso.test.tsx b/packages/excalidraw/tests/lasso.test.tsx
new file mode 100644
index 000000000..aa32b13d6
--- /dev/null
+++ b/packages/excalidraw/tests/lasso.test.tsx
@@ -0,0 +1,1812 @@
+/**
+ * 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,
+ type ElementsSegmentsMap,
+} 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";
+
+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(-352, -64);
+ const points = [
+ [0, 0],
+ [0.1015625, 0],
+ [3.80078125, -1.05859375],
+ [14.38671875, -5.10546875],
+ [26.828125, -10.70703125],
+ [38.17578125, -16.10546875],
+ [49.6328125, -21.59375],
+ [79.890625, -34.078125],
+ [111.5859375, -46.4140625],
+ [125.61328125, -51.265625],
+ [139.20703125, -55.81640625],
+ [151.046875, -60.27734375],
+ [160.86328125, -64.140625],
+ [170.15625, -67.51171875],
+ [181.0234375, -71.5234375],
+ [192.6796875, -75.79296875],
+ [204.66015625, -80.19921875],
+ [218.22265625, -85.6875],
+ [233.359375, -91.9375],
+ [264.22265625, -103.91796875],
+ [280.390625, -109.80859375],
+ [295.48046875, -114.99609375],
+ [309.453125, -120.28125],
+ [323.5546875, -126.125],
+ [339.26953125, -132.6796875],
+ [354.67578125, -139.64453125],
+ [370.86328125, -146.53125],
+ [384.70703125, -152.4921875],
+ [394.7109375, -157.6796875],
+ [405.6171875, -163.07421875],
+ [416.390625, -167.96484375],
+ [425.41796875, -171.6484375],
+ [433.26171875, -174.78515625],
+ [440.76953125, -177.68359375],
+ [447.4140625, -179.71875],
+ [453.3828125, -181.11328125],
+ [458.421875, -182.13671875],
+ [462.82421875, -182.5546875],
+ [467.2109375, -182.640625],
+ [472.09765625, -182.640625],
+ [481.9609375, -182.640625],
+ [487.23828125, -182.5859375],
+ [492.03515625, -181.91796875],
+ [496.76953125, -180.640625],
+ [501.43359375, -179.2734375],
+ [505.203125, -177.73046875],
+ [508.33984375, -176.08984375],
+ [511.8671875, -174.16796875],
+ [515.9140625, -172.09375],
+ [519.703125, -170.125],
+ [523.6796875, -167.8828125],
+ [528.109375, -165.3984375],
+ [532.01953125, -163.3125],
+ [535.28125, -161.65625],
+ [537.62890625, -159.7734375],
+ [539.0859375, -157.53125],
+ [540.1640625, -155.7421875],
+ [540.98046875, -154.2578125],
+ [541.87890625, -152.33203125],
+ [542.69140625, -150.0078125],
+ [543.25390625, -147.671875],
+ [543.90625, -145.125],
+ [544.66796875, -142.01171875],
+ [545.34375, -138.1484375],
+ [546.03515625, -132.72265625],
+ [546.41015625, -126.80078125],
+ [546.44921875, -121.25390625],
+ [546.38671875, -116.3046875],
+ [545.21484375, -112],
+ [541.50390625, -107.2421875],
+ [536.515625, -102.83203125],
+ [531.44140625, -98.95703125],
+ [526.39453125, -95.23046875],
+ [521.15234375, -91.9921875],
+ [514.38671875, -87.984375],
+ [506.953125, -83.19140625],
+ [499.1171875, -77.52734375],
+ [491.37109375, -71.6484375],
+ [484.85546875, -66.3984375],
+ [477.8203125, -60.21875],
+ [469.921875, -53.26953125],
+ [460.84765625, -45.6171875],
+ [451.796875, -38.359375],
+ [444.33984375, -32.48046875],
+ [438.4296875, -27.68359375],
+ [435.2109375, -24.84375],
+ [433.07421875, -23.23828125],
+ [429.7421875, -21.125],
+ [424.8984375, -17.546875],
+ [418.7421875, -13.01171875],
+ [411.84375, -8.3359375],
+ [404.80078125, -3.65625],
+ [398.23828125, 0.6171875],
+ [392.32421875, 4.74609375],
+ [386.21875, 9.69921875],
+ [379.7421875, 14.734375],
+ [373.6015625, 19.95703125],
+ [367.34375, 26.72265625],
+ [360.73828125, 34.48046875],
+ [354.1484375, 42.51953125],
+ [347.21484375, 51.19140625],
+ [340.59765625, 59.7265625],
+ [334.46875, 67.703125],
+ [328.9921875, 74.82421875],
+ [323.78515625, 81.6796875],
+ [318.6640625, 88.34375],
+ [314.2109375, 93.8984375],
+ [309.10546875, 100.66015625],
+ [304.17578125, 107.2734375],
+ [299.97265625, 112.421875],
+ [295.890625, 117.99609375],
+ [291.8828125, 123.4453125],
+ [288.0078125, 128.25],
+ [284.91796875, 132.265625],
+ [282.453125, 135.66796875],
+ [279.80078125, 139.16015625],
+ [276.7734375, 143.53515625],
+ [274.3515625, 147.6484375],
+ [272.0859375, 151.0546875],
+ [269.5546875, 154.37890625],
+ [267.71484375, 156.73828125],
+ [266.62890625, 158.484375],
+ [265.5546875, 160.03125],
+ [264.73828125, 161.30078125],
+ [264.16015625, 162.51953125],
+ [263.46484375, 163.734375],
+ [262.9140625, 164.9453125],
+ [262.05078125, 166.3046875],
+ [261.234375, 167.390625],
+ [260.46484375, 168.53515625],
+ [259.5703125, 169.6640625],
+ [258.9296875, 170.1875],
+ [258.9296875, 170.1875],
+ ] as LocalPoint[];
+
+ updatePath(startPoint, points);
+
+ const selectedElements = getSelectedElements(h.elements, h.state);
+ expect(selectedElements.length).toBe(1);
+ expect(selectedElements[0].type).toBe("frame");
+ });
+
+ it("Selects group if any group from group is selected", () => {
+ act(() => {
+ const elements = [
+ {
+ type: "line",
+ version: 594,
+ versionNonce: 1548428815,
+ isDeleted: false,
+ id: "FBFkTIUB1trLc6nEdp1Pu",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 170.81219641259787,
+ y: 391.1659993876855,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "#846358",
+ width: 66.16406551308279,
+ height: 78.24124358133415,
+ seed: 838106785,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ startBinding: null,
+ endBinding: null,
+ lastCommittedPoint: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ points: [
+ [0, 0],
+ [-12.922669045523984, 78.24124358133415],
+ [53.24139646755881, 78.24124358133415],
+ [41.35254094567674, 4.2871914291142],
+ [0, 0],
+ ],
+ index: "aJ",
+ },
+ {
+ type: "line",
+ version: 947,
+ versionNonce: 1038960225,
+ isDeleted: false,
+ id: "RsALsOjcB5dAyH4JNlfqJ",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 188.53119264021603,
+ y: 207.94959072391882,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "#2f9e44",
+ width: 369.2312846526558,
+ height: 192.4489303545334,
+ seed: 319685249,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ startBinding: null,
+ endBinding: null,
+ lastCommittedPoint: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ points: [
+ [0, 0],
+ [-184.8271826294887, 192.4489303545334],
+ [184.4041020231671, 192.4489303545334],
+ [0, 0],
+ ],
+ index: "aK",
+ },
+ {
+ type: "line",
+ version: 726,
+ versionNonce: 1463389231,
+ isDeleted: false,
+ id: "YNXwgpVIEUFgUZpJ564wo",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 184.66726071162367,
+ y: 123.16737006571739,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "#2f9e44",
+ width: 290.9653230160535,
+ height: 173.62827429793325,
+ seed: 1108085345,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ startBinding: null,
+ endBinding: null,
+ lastCommittedPoint: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ points: [
+ [0, 0],
+ [-142.34630272423374, 173.62827429793325],
+ [148.61902029181974, 173.62827429793325],
+ [0, 0],
+ ],
+ index: "aL",
+ },
+ {
+ type: "line",
+ version: 478,
+ versionNonce: 2081935937,
+ isDeleted: false,
+ id: "NV7XOz9ZIB8CbuqQIjt5k",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 189.05565121741444,
+ y: 54.65530340848173,
+ strokeColor: "#1e1e1e",
+ backgroundColor: "#2f9e44",
+ width: 194.196753378859,
+ height: 137.02921662223056,
+ seed: 398333505,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ startBinding: null,
+ endBinding: null,
+ lastCommittedPoint: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ points: [
+ [0, 0],
+ [-97.0316913876915, 135.70546644407042],
+ [97.1650619911675, 137.02921662223056],
+ [0, 0],
+ ],
+ index: "aM",
+ },
+ {
+ type: "ellipse",
+ version: 282,
+ versionNonce: 1337339471,
+ isDeleted: false,
+ id: "b7FzLnG0L3-50bqij9mGX",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 73.9036924826674,
+ y: 334.3607129519222,
+ strokeColor: "#000000",
+ backgroundColor: "#c2255c",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 654550561,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aN",
+ },
+ {
+ type: "ellipse",
+ version: 292,
+ versionNonce: 1355145761,
+ isDeleted: false,
+ id: "XzVfrVf3-sFJFPdOo51sb",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 138.21156391938035,
+ y: 380.4480208148999,
+ strokeColor: "#000000",
+ backgroundColor: "#f08c00",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 2060204545,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aO",
+ },
+ {
+ type: "ellipse",
+ version: 288,
+ versionNonce: 1889111151,
+ isDeleted: false,
+ id: "D4m0Ex4rPc1-8T-uv5vGh",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 208.9502224997646,
+ y: 331.14531938008656,
+ strokeColor: "#000000",
+ backgroundColor: "#6741d9",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 337072609,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aP",
+ },
+ {
+ type: "ellipse",
+ version: 296,
+ versionNonce: 686224897,
+ isDeleted: false,
+ id: "E0wxH4dAzQsv7Mj6OngC8",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 285.04787036654153,
+ y: 367.5864465275573,
+ strokeColor: "#000000",
+ backgroundColor: "#e8590c",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 670330305,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aQ",
+ },
+ {
+ type: "ellipse",
+ version: 290,
+ versionNonce: 1974216335,
+ isDeleted: false,
+ id: "yKv_UI6iqa6zjVgYtXVcg",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 113.56021320197334,
+ y: 228.25272508134577,
+ strokeColor: "#000000",
+ backgroundColor: "#228be6",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 495127969,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aR",
+ },
+ {
+ type: "ellipse",
+ version: 290,
+ versionNonce: 662343137,
+ isDeleted: false,
+ id: "udyW842HtUTlqjDEOxoPN",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 166.0783082086228,
+ y: 271.12463937248776,
+ strokeColor: "#000000",
+ backgroundColor: "#ffd43b",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 1274196353,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aS",
+ },
+ {
+ type: "ellipse",
+ version: 300,
+ versionNonce: 229014703,
+ isDeleted: false,
+ id: "R3VRfgkowIgnr5dFXwWXa",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 234.67337107445002,
+ y: 237.89890579685272,
+ strokeColor: "#000000",
+ backgroundColor: "#38d9a9",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 2021841249,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aT",
+ },
+ {
+ type: "ellipse",
+ version: 332,
+ versionNonce: 1670392257,
+ isDeleted: false,
+ id: "90W2w6zgGHdYda8UBiG2R",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 136.0679682048231,
+ y: 155.37047078640435,
+ strokeColor: "#000000",
+ backgroundColor: "#fa5252",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 344130881,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aU",
+ },
+ {
+ type: "ellipse",
+ version: 337,
+ versionNonce: 2083589839,
+ isDeleted: false,
+ id: "nTDHvOk2mXLUFNn--7JvS",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 176.7962867814079,
+ y: 102.85237577975542,
+ strokeColor: "#000000",
+ backgroundColor: "#9775fa",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 995276065,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aV",
+ },
+ {
+ type: "ellipse",
+ version: 313,
+ versionNonce: 1715947937,
+ isDeleted: false,
+ id: "iS2Q6cvQ5n_kxINfwu0HS",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 212.16561607160025,
+ y: 153.22687507184727,
+ strokeColor: "#000000",
+ backgroundColor: "#fab005",
+ width: 25.723148574685204,
+ height: 25.723148574685204,
+ seed: 1885432065,
+ groupIds: ["ts5VcKn3YXMmP8ipg8J5v", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: {
+ type: 2,
+ },
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ index: "aW",
+ },
+ {
+ type: "line",
+ version: 1590,
+ versionNonce: 2078563567,
+ isDeleted: false,
+ id: "X4-EPaJDEnPZKN1bWhFvs",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 158.19616469467843,
+ y: 72.35608879274483,
+ strokeColor: "#000000",
+ backgroundColor: "#fab005",
+ width: 84.29101925982515,
+ height: 84.66090652809709,
+ seed: 489595105,
+ groupIds: ["uRBC-GT117eEzaf2ehdX_", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: null,
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ startBinding: null,
+ endBinding: null,
+ lastCommittedPoint: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ points: [
+ [0, 0],
+ [20.062524675376327, -6.158183070239938],
+ [30.37468761946564, 12.304817735352257],
+ [40.379925616688446, -6.1461467434106165],
+ [60.1779773078079, -0.29196108982718233],
+ [54.38738874028317, -19.998927589317724],
+ [72.41919795710177, -30.209920701755497],
+ [54.27320673839876, -40.520131959038096],
+ [60.381292814514666, -60.13991664051316],
+ [40.445474389553596, -54.21058549574358],
+ [30.40022403822941, -72.35608879274483],
+ [20.373038782485533, -54.22107619387393],
+ [0.4211938029164521, -59.96466401524409],
+ [5.9466053348070105, -39.94020499773404],
+ [-11.871821302723378, -30.212694376435106],
+ [5.916318974536789, -20.128073448241587],
+ [0, 0],
+ ],
+ index: "aX",
+ },
+ {
+ type: "line",
+ version: 1719,
+ versionNonce: 1758424449,
+ isDeleted: false,
+ id: "MHWh6yM-hxZbKvIX473TA",
+ fillStyle: "solid",
+ strokeWidth: 2,
+ strokeStyle: "solid",
+ roughness: 1,
+ opacity: 100,
+ angle: 0,
+ x: 166.45958905094363,
+ y: 64.16037225967494,
+ strokeColor: "#000000",
+ backgroundColor: "#ffd43b",
+ width: 61.28316986803382,
+ height: 61.55209370467244,
+ seed: 1330240705,
+ groupIds: ["uRBC-GT117eEzaf2ehdX_", "-9NzH7Fa5JaHu4ArEFpa_"],
+ frameId: null,
+ roundness: null,
+ boundElements: [],
+ updated: 1740960278015,
+ link: null,
+ locked: false,
+ startBinding: null,
+ endBinding: null,
+ lastCommittedPoint: null,
+ startArrowhead: null,
+ endArrowhead: null,
+ points: [
+ [0, 0],
+ [14.586312023026041, -4.477262020152575],
+ [22.08369476864762, 8.946127856709843],
+ [29.357929973514253, -4.468511096647962],
+ [43.751958845119866, -0.2122681777946347],
+ [39.54195372315489, -14.540074226137776],
+ [52.651848904941595, -21.96390218462942],
+ [39.45893853270161, -29.459865970613833],
+ [43.89977789919855, -43.724287114968824],
+ [29.40558673003957, -39.41341021563198],
+ [22.102260835384786, -52.605965847962594],
+ [14.812069036519228, -39.4210374010807],
+ [0.30622587789485506, -43.5968709738604],
+ [4.323435973028119, -29.038234309343977],
+ [-8.631320963092225, -21.96591876454555],
+ [4.301416496013572, -14.633968779551441],
+ [0, 0],
+ ],
+ index: "aY",
+ },
+ ].map((e) => ({
+ ...e,
+ index: null,
+ angle: e.angle as Radians,
+ })) as ExcalidrawElement[];
+
+ h.elements = elements;
+ });
+
+ const startPoint = pointFrom(117, 463);
+ const points = [
+ [0, 0],
+ [0.09765625, 0],
+ [3.24609375, 0],
+ [6.9765625, 0],
+ [10.76171875, 0],
+ [14.03125, 0],
+ [23.24609375, 0.32421875],
+ [28.65625, 0.6484375],
+ [32.0546875, 0.6484375],
+ [35.4296875, 0.6484375],
+ [38.86328125, 0.3828125],
+ [41.9765625, -0.109375],
+ [45.0390625, -0.4296875],
+ [47.74609375, -0.5234375],
+ [49.953125, -0.73046875],
+ [52.12890625, -0.9375],
+ [54.25, -1.14453125],
+ [55.9921875, -1.3828125],
+ [57.67578125, -1.58984375],
+ [58.8125, -1.76953125],
+ [59.453125, -1.76953125],
+ [60.09375, -1.76953125],
+ [60.09375, -1.76953125],
+ ] as LocalPoint[];
+
+ updatePath(startPoint, points);
+
+ const selectedElements = getSelectedElements(h.elements, h.state);
+ expect(selectedElements.length).toBe(16);
+ expect(h.app.state.selectedGroupIds["-9NzH7Fa5JaHu4ArEFpa_"]).toBe(true);
+ });
+});
diff --git a/packages/excalidraw/tests/linearElementEditor.test.tsx b/packages/excalidraw/tests/linearElementEditor.test.tsx
index 741799d3b..861998584 100644
--- a/packages/excalidraw/tests/linearElementEditor.test.tsx
+++ b/packages/excalidraw/tests/linearElementEditor.test.tsx
@@ -1,3 +1,5 @@
+import { newArrowElement } from "@excalidraw/element/newElement";
+
import { pointCenter, pointFrom } from "@excalidraw/math";
import { act, queryByTestId, queryByText } from "@testing-library/react";
import React from "react";
@@ -19,7 +21,7 @@ import {
import * as textElementUtils from "@excalidraw/element/textElement";
import { wrapText } from "@excalidraw/element/textWrapping";
-import type { GlobalPoint } from "@excalidraw/math";
+import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
import type {
ExcalidrawElement,
@@ -164,6 +166,24 @@ describe("Test Linear Elements", () => {
Keyboard.keyPress(KEYS.DELETE);
};
+ it("should normalize the element points at creation", () => {
+ const element = newArrowElement({
+ type: "arrow",
+ points: [pointFrom(0.5, 0), pointFrom(100, 100)],
+ x: 0,
+ y: 0,
+ });
+ expect(element.points).toEqual([
+ pointFrom(0.5, 0),
+ pointFrom(100, 100),
+ ]);
+ new LinearElementEditor(element);
+ expect(element.points).toEqual([
+ pointFrom(0, 0),
+ pointFrom(99.5, 100),
+ ]);
+ });
+
it("should not drag line and add midpoint until dragged beyond a threshold", () => {
createTwoPointerLinearElement("line");
const line = h.elements[0] as ExcalidrawLinearElement;
diff --git a/packages/excalidraw/tests/test-utils.ts b/packages/excalidraw/tests/test-utils.ts
index b2b8aff9c..894d748dd 100644
--- a/packages/excalidraw/tests/test-utils.ts
+++ b/packages/excalidraw/tests/test-utils.ts
@@ -19,7 +19,7 @@ import type { AllPossibleKeys } from "@excalidraw/common/utility-types";
import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
-import { UI } from "./helpers/ui";
+import { Pointer, UI } from "./helpers/ui";
import * as toolQueries from "./queries/toolQueries";
import type { RenderResult, RenderOptions } from "@testing-library/react";
@@ -42,6 +42,10 @@ type TestRenderFn = (
) => Promise>;
const renderApp: TestRenderFn = async (ui, options) => {
+ // when tests reuse Pointer instances let's reset the last
+ // pointer poisitions so there's no leak between tests
+ Pointer.resetAll();
+
if (options?.localStorageData) {
initLocalStorage(options.localStorageData);
delete options.localStorageData;
diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts
index 31ce332f8..cba9fbea7 100644
--- a/packages/excalidraw/types.ts
+++ b/packages/excalidraw/types.ts
@@ -136,6 +136,7 @@ export type BinaryFiles = Record;
export type ToolType =
| "selection"
+ | "lasso"
| "rectangle"
| "diamond"
| "ellipse"
@@ -308,6 +309,8 @@ export interface AppState {
*/
lastActiveTool: ActiveTool | null;
locked: boolean;
+ // indicates if the current tool is temporarily switched on from the selection tool
+ fromSelection: boolean;
} & ActiveTool;
penMode: boolean;
penDetected: boolean;
@@ -598,6 +601,7 @@ export interface ExcalidrawProps {
) => JSX.Element | null;
aiEnabled?: boolean;
showDeprecatedFonts?: boolean;
+ renderScrollbars?: boolean;
}
export type SceneData = {
@@ -721,7 +725,8 @@ export type PointerDownState = Readonly<{
scrollbars: ReturnType;
// The previous pointer position
lastCoords: { x: number; y: number };
- // map of original elements data
+ // original element frozen snapshots so we can access the original
+ // element attribute values at time of pointerdown
originalElements: Map>;
resize: {
// Handle when resizing, might change during the pointer interaction
@@ -755,6 +760,9 @@ export type PointerDownState = Readonly<{
hasOccurred: boolean;
// Might change during the pointer interaction
offset: { x: number; y: number } | null;
+ // by default same as PointerDownState.origin. On alt-duplication, reset
+ // to current pointer position at time of duplication.
+ origin: { x: number; y: number };
};
// We need to have these in the state so that we can unsubscribe them
eventListeners: {
diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx
index 959c5a012..0ba1960d6 100644
--- a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx
+++ b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx
@@ -31,6 +31,7 @@ import {
mockBoundingClientRect,
restoreOriginalGetBoundingClientRect,
} from "../tests/test-utils";
+import { actionBindText } from "../actions";
unmountComponent();
@@ -1568,5 +1569,101 @@ describe("textWysiwyg", () => {
expect(text.containerId).toBe(null);
expect(text.text).toBe("Excalidraw");
});
+
+ it("should reset the text element angle to the container's when binding to rotated non-arrow container", async () => {
+ const text = API.createElement({
+ type: "text",
+ text: "Hello World!",
+ angle: 45,
+ });
+ const rectangle = API.createElement({
+ type: "rectangle",
+ width: 90,
+ height: 75,
+ angle: 30,
+ });
+
+ API.setElements([rectangle, text]);
+
+ API.setSelectedElements([rectangle, text]);
+
+ h.app.actionManager.executeAction(actionBindText);
+
+ expect(text.angle).toBe(30);
+ expect(rectangle.angle).toBe(30);
+ });
+
+ it("should reset the text element angle to 0 when binding to rotated arrow container", async () => {
+ const text = API.createElement({
+ type: "text",
+ text: "Hello World!",
+ angle: 45,
+ });
+ const arrow = API.createElement({
+ type: "arrow",
+ width: 90,
+ height: 75,
+ angle: 30,
+ });
+
+ API.setElements([arrow, text]);
+
+ API.setSelectedElements([arrow, text]);
+
+ h.app.actionManager.executeAction(actionBindText);
+
+ expect(text.angle).toBe(0);
+ expect(arrow.angle).toBe(30);
+ });
+
+ it("should keep the text label at 0 degrees when used as an arrow label", async () => {
+ const arrow = API.createElement({
+ type: "arrow",
+ width: 90,
+ height: 75,
+ angle: 30,
+ });
+
+ API.setElements([arrow]);
+ API.setSelectedElements([arrow]);
+
+ mouse.doubleClickAt(
+ arrow.x + arrow.width / 2,
+ arrow.y + arrow.height / 2,
+ );
+
+ const editor = await getTextEditor(textEditorSelector, true);
+
+ updateTextEditor(editor, "Hello World!");
+
+ Keyboard.exitTextEditor(editor);
+
+ expect(h.elements[1].angle).toBe(0);
+ });
+
+ it("should keep the text label at the same degrees when used as a non-arrow label", async () => {
+ const rectangle = API.createElement({
+ type: "rectangle",
+ width: 90,
+ height: 75,
+ angle: 30,
+ });
+
+ API.setElements([rectangle]);
+ API.setSelectedElements([rectangle]);
+
+ mouse.doubleClickAt(
+ rectangle.x + rectangle.width / 2,
+ rectangle.y + rectangle.height / 2,
+ );
+
+ const editor = await getTextEditor(textEditorSelector, true);
+
+ updateTextEditor(editor, "Hello World!");
+
+ Keyboard.exitTextEditor(editor);
+
+ expect(h.elements[1].angle).toBe(30);
+ });
});
});
diff --git a/packages/math/src/polygon.ts b/packages/math/src/polygon.ts
index 762c82dbf..a50d4e853 100644
--- a/packages/math/src/polygon.ts
+++ b/packages/math/src/polygon.ts
@@ -41,6 +41,34 @@ export const polygonIncludesPoint = (
return inside;
};
+export const polygonIncludesPointNonZero = (
+ point: Point,
+ polygon: Point[],
+): boolean => {
+ const [x, y] = point;
+ let windingNumber = 0;
+
+ for (let i = 0; i < polygon.length; i++) {
+ const j = (i + 1) % polygon.length;
+ const [xi, yi] = polygon[i];
+ const [xj, yj] = polygon[j];
+
+ if (yi <= y) {
+ if (yj > y) {
+ if ((xj - xi) * (y - yi) - (x - xi) * (yj - yi) > 0) {
+ windingNumber++;
+ }
+ }
+ } else if (yj <= y) {
+ if ((xj - xi) * (y - yi) - (x - xi) * (yj - yi) < 0) {
+ windingNumber--;
+ }
+ }
+ }
+
+ return windingNumber !== 0;
+};
+
export const pointOnPolygon = (
p: Point,
poly: Polygon,
diff --git a/packages/math/src/segment.ts b/packages/math/src/segment.ts
index e38978b7e..dade79039 100644
--- a/packages/math/src/segment.ts
+++ b/packages/math/src/segment.ts
@@ -160,13 +160,17 @@ export const distanceToLineSegment = (
*/
export function lineSegmentIntersectionPoints<
Point extends GlobalPoint | LocalPoint,
->(l: LineSegment, s: LineSegment): Point | null {
+>(
+ l: LineSegment,
+ s: LineSegment,
+ threshold?: number,
+): Point | null {
const candidate = linesIntersectAt(line(l[0], l[1]), line(s[0], s[1]));
if (
!candidate ||
- !pointOnLineSegment(candidate, s) ||
- !pointOnLineSegment(candidate, l)
+ !pointOnLineSegment(candidate, s, threshold) ||
+ !pointOnLineSegment(candidate, l, threshold)
) {
return null;
}
diff --git a/packages/math/src/types.ts b/packages/math/src/types.ts
index a2a575bd7..da7d5d6ab 100644
--- a/packages/math/src/types.ts
+++ b/packages/math/src/types.ts
@@ -138,3 +138,5 @@ export type Ellipse = {
} & {
_brand: "excalimath_ellipse";
};
+
+export type ElementsSegmentsMap = Map[]>;
diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap
index 54d4af4bc..91108a600 100644
--- a/packages/utils/tests/__snapshots__/export.test.ts.snap
+++ b/packages/utils/tests/__snapshots__/export.test.ts.snap
@@ -5,6 +5,7 @@ exports[`exportToSvg > with default arguments 1`] = `
"activeEmbeddable": null,
"activeTool": {
"customType": null,
+ "fromSelection": false,
"lastActiveTool": null,
"locked": false,
"type": "selection",
diff --git a/setupTests.ts b/setupTests.ts
index 2aec616b4..245c57326 100644
--- a/setupTests.ts
+++ b/setupTests.ts
@@ -94,11 +94,6 @@ vi.mock(
},
);
-vi.mock("nanoid", () => {
- return {
- nanoid: vi.fn(() => "test-id"),
- };
-});
// ReactDOM is located inside index.tsx file
// as a result, we need a place for it to render into
const element = document.createElement("div");
diff --git a/yarn.lock b/yarn.lock
index ccd0827fb..366a3f99f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1003,7 +1003,7 @@
"@babel/plugin-transform-modules-commonjs" "^7.25.9"
"@babel/plugin-transform-typescript" "^7.25.9"
-"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.6", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
+"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
version "7.26.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.9.tgz#aa4c6facc65b9cb3f87d75125ffd47781b475433"
integrity sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==
@@ -2220,13 +2220,6 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
-"@radix-ui/primitive@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.0.tgz#e1d8ef30b10ea10e69c76e896f608d9276352253"
- integrity sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==
- dependencies:
- "@babel/runtime" "^7.13.10"
-
"@radix-ui/primitive@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3"
@@ -2239,47 +2232,30 @@
dependencies:
"@radix-ui/react-primitive" "2.0.2"
-"@radix-ui/react-collection@1.0.1":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.1.tgz#259506f97c6703b36291826768d3c1337edd1de5"
- integrity sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==
+"@radix-ui/react-collection@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.2.tgz#b45eccca1cb902fd078b237316bd9fa81e621e15"
+ integrity sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==
dependencies:
- "@babel/runtime" "^7.13.10"
- "@radix-ui/react-compose-refs" "1.0.0"
- "@radix-ui/react-context" "1.0.0"
- "@radix-ui/react-primitive" "1.0.1"
- "@radix-ui/react-slot" "1.0.1"
-
-"@radix-ui/react-compose-refs@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
- integrity sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==
- dependencies:
- "@babel/runtime" "^7.13.10"
+ "@radix-ui/react-compose-refs" "1.1.1"
+ "@radix-ui/react-context" "1.1.1"
+ "@radix-ui/react-primitive" "2.0.2"
+ "@radix-ui/react-slot" "1.1.2"
"@radix-ui/react-compose-refs@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
-"@radix-ui/react-context@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0"
- integrity sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==
- dependencies:
- "@babel/runtime" "^7.13.10"
-
"@radix-ui/react-context@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
-"@radix-ui/react-direction@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.0.tgz#a2e0b552352459ecf96342c79949dd833c1e6e45"
- integrity sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==
- dependencies:
- "@babel/runtime" "^7.13.10"
+"@radix-ui/react-direction@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc"
+ integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==
"@radix-ui/react-dismissable-layer@1.1.5":
version "1.1.5"
@@ -2306,14 +2282,6 @@
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
-"@radix-ui/react-id@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e"
- integrity sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==
- dependencies:
- "@babel/runtime" "^7.13.10"
- "@radix-ui/react-use-layout-effect" "1.0.0"
-
"@radix-ui/react-id@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
@@ -2366,15 +2334,6 @@
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-layout-effect" "1.1.0"
-"@radix-ui/react-presence@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.0.tgz#814fe46df11f9a468808a6010e3f3ca7e0b2e84a"
- integrity sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==
- dependencies:
- "@babel/runtime" "^7.13.10"
- "@radix-ui/react-compose-refs" "1.0.0"
- "@radix-ui/react-use-layout-effect" "1.0.0"
-
"@radix-ui/react-presence@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc"
@@ -2383,14 +2342,6 @@
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-use-layout-effect" "1.1.0"
-"@radix-ui/react-primitive@1.0.1":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz#c1ebcce283dd2f02e4fbefdaa49d1cb13dbc990a"
- integrity sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==
- dependencies:
- "@babel/runtime" "^7.13.10"
- "@radix-ui/react-slot" "1.0.1"
-
"@radix-ui/react-primitive@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz#ac8b7854d87b0d7af388d058268d9a7eb64ca8ef"
@@ -2398,29 +2349,20 @@
dependencies:
"@radix-ui/react-slot" "1.1.2"
-"@radix-ui/react-roving-focus@1.0.2":
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de"
- integrity sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==
+"@radix-ui/react-roving-focus@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz#815d051a54299114a68db6eb8d34c41a3c0a646f"
+ integrity sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==
dependencies:
- "@babel/runtime" "^7.13.10"
- "@radix-ui/primitive" "1.0.0"
- "@radix-ui/react-collection" "1.0.1"
- "@radix-ui/react-compose-refs" "1.0.0"
- "@radix-ui/react-context" "1.0.0"
- "@radix-ui/react-direction" "1.0.0"
- "@radix-ui/react-id" "1.0.0"
- "@radix-ui/react-primitive" "1.0.1"
- "@radix-ui/react-use-callback-ref" "1.0.0"
- "@radix-ui/react-use-controllable-state" "1.0.0"
-
-"@radix-ui/react-slot@1.0.1":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz#e7868c669c974d649070e9ecbec0b367ee0b4d81"
- integrity sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==
- dependencies:
- "@babel/runtime" "^7.13.10"
- "@radix-ui/react-compose-refs" "1.0.0"
+ "@radix-ui/primitive" "1.1.1"
+ "@radix-ui/react-collection" "1.1.2"
+ "@radix-ui/react-compose-refs" "1.1.1"
+ "@radix-ui/react-context" "1.1.1"
+ "@radix-ui/react-direction" "1.1.0"
+ "@radix-ui/react-id" "1.1.0"
+ "@radix-ui/react-primitive" "2.0.2"
+ "@radix-ui/react-use-callback-ref" "1.1.0"
+ "@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-slot@1.1.2":
version "1.1.2"
@@ -2429,41 +2371,25 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
-"@radix-ui/react-tabs@1.0.2":
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz#8f5ec73ca41b151a413bdd6e00553408ff34ce07"
- integrity sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==
+"@radix-ui/react-tabs@1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz#c47c8202dc676dea47676215863d2ef9b141c17a"
+ integrity sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==
dependencies:
- "@babel/runtime" "^7.13.10"
- "@radix-ui/primitive" "1.0.0"
- "@radix-ui/react-context" "1.0.0"
- "@radix-ui/react-direction" "1.0.0"
- "@radix-ui/react-id" "1.0.0"
- "@radix-ui/react-presence" "1.0.0"
- "@radix-ui/react-primitive" "1.0.1"
- "@radix-ui/react-roving-focus" "1.0.2"
- "@radix-ui/react-use-controllable-state" "1.0.0"
-
-"@radix-ui/react-use-callback-ref@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz#9e7b8b6b4946fe3cbe8f748c82a2cce54e7b6a90"
- integrity sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==
- dependencies:
- "@babel/runtime" "^7.13.10"
+ "@radix-ui/primitive" "1.1.1"
+ "@radix-ui/react-context" "1.1.1"
+ "@radix-ui/react-direction" "1.1.0"
+ "@radix-ui/react-id" "1.1.0"
+ "@radix-ui/react-presence" "1.1.2"
+ "@radix-ui/react-primitive" "2.0.2"
+ "@radix-ui/react-roving-focus" "1.1.2"
+ "@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-callback-ref@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
-"@radix-ui/react-use-controllable-state@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz#a64deaafbbc52d5d407afaa22d493d687c538b7f"
- integrity sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==
- dependencies:
- "@babel/runtime" "^7.13.10"
- "@radix-ui/react-use-callback-ref" "1.0.0"
-
"@radix-ui/react-use-controllable-state@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
@@ -2478,13 +2404,6 @@
dependencies:
"@radix-ui/react-use-callback-ref" "1.1.0"
-"@radix-ui/react-use-layout-effect@1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz#2fc19e97223a81de64cd3ba1dc42ceffd82374dc"
- integrity sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==
- dependencies:
- "@babel/runtime" "^7.13.10"
-
"@radix-ui/react-use-layout-effect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
@@ -8851,16 +8770,8 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+ name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8962,14 +8873,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -10102,7 +10006,8 @@ workbox-window@7.3.0, workbox-window@^7.3.0:
"@types/trusted-types" "^2.0.2"
workbox-core "7.3.0"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+ name wrap-ansi-cjs
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -10120,15 +10025,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"