feat: image cropping (#8613)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
pull/8372/merge
Ryan Di 3 months ago committed by GitHub
parent eb09b48ae6
commit e957c8e9ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,55 @@
import { register } from "./register";
import { cropIcon } from "../components/icons";
import { StoreAction } from "../store";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { isImageElement } from "../element/typeChecks";
import type { ExcalidrawImageElement } from "../element/types";
export const actionToggleCropEditor = register({
name: "cropEditor",
label: "helpDialog.cropStart",
icon: cropIcon,
viewMode: true,
trackEvent: { category: "menu" },
keywords: ["image", "crop"],
perform(elements, appState, _, app) {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})[0] as ExcalidrawImageElement;
return {
appState: {
...appState,
isCropping: false,
croppingElementId: selectedElement.id,
},
storeAction: StoreAction.CAPTURE,
};
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
if (
!appState.croppingElementId &&
selectedElements.length === 1 &&
isImageElement(selectedElements[0])
) {
return true;
}
return false;
},
PanelComponent: ({ appState, updateData, app }) => {
const label = t("helpDialog.cropStart");
return (
<ToolButton
type="button"
icon={cropIcon}
title={label}
aria-label={label}
onClick={() => updateData(null)}
/>
);
},
});

@ -88,3 +88,5 @@ export { actionToggleElementLock } from "./actionElementLock";
export { actionToggleLinearEditor } from "./actionLinearEditor"; export { actionToggleLinearEditor } from "./actionLinearEditor";
export { actionToggleSearchMenu } from "./actionToggleSearchMenu"; export { actionToggleSearchMenu } from "./actionToggleSearchMenu";
export { actionToggleCropEditor } from "./actionCropEditor";

@ -134,7 +134,8 @@ export type ActionName =
| "commandPalette" | "commandPalette"
| "autoResize" | "autoResize"
| "elementStats" | "elementStats"
| "searchMenu"; | "searchMenu"
| "cropEditor";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];

@ -116,6 +116,8 @@ export const getDefaultAppState = (): Omit<
objectsSnapModeEnabled: false, objectsSnapModeEnabled: false,
userToFollow: null, userToFollow: null,
followedBy: new Set(), followedBy: new Set(),
isCropping: false,
croppingElementId: null,
searchMatches: [], searchMatches: [],
}; };
}; };
@ -237,6 +239,8 @@ const APP_STATE_STORAGE_CONF = (<
objectsSnapModeEnabled: { browser: true, export: false, server: false }, objectsSnapModeEnabled: { browser: true, export: false, server: false },
userToFollow: { browser: false, export: false, server: false }, userToFollow: { browser: false, export: false, server: false },
followedBy: { browser: false, export: false, server: false }, followedBy: { browser: false, export: false, server: false },
isCropping: { browser: false, export: false, server: false },
croppingElementId: { browser: false, export: false, server: false },
searchMatches: { browser: false, export: false, server: false }, searchMatches: { browser: false, export: false, server: false },
}); });

@ -17,13 +17,16 @@ import {
hasBoundTextElement, hasBoundTextElement,
isBindableElement, isBindableElement,
isBoundToContainer, isBoundToContainer,
isImageElement,
isTextElement, isTextElement,
} from "./element/typeChecks"; } from "./element/typeChecks";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElement, ExcalidrawTextElement,
NonDeleted, NonDeleted,
Ordered,
OrderedExcalidrawElement, OrderedExcalidrawElement,
SceneElementsMap, SceneElementsMap,
} from "./element/types"; } from "./element/types";
@ -626,6 +629,18 @@ export class AppStateChange implements Change<AppState> {
); );
break; break;
case "croppingElementId": {
const croppingElementId = nextAppState[key];
const element =
croppingElementId && nextElements.get(croppingElementId);
if (element && !element.isDeleted) {
visibleDifferenceFlag.value = true;
} else {
nextAppState[key] = null;
}
break;
}
case "editingGroupId": case "editingGroupId":
const editingGroupId = nextAppState[key]; const editingGroupId = nextAppState[key];
@ -756,6 +771,7 @@ export class AppStateChange implements Change<AppState> {
selectedElementIds, selectedElementIds,
editingLinearElementId, editingLinearElementId,
selectedLinearElementId, selectedLinearElementId,
croppingElementId,
...standaloneProps ...standaloneProps
} = delta as ObservedAppState; } = delta as ObservedAppState;
@ -779,7 +795,10 @@ export class AppStateChange implements Change<AppState> {
} }
} }
type ElementPartial = Omit<ElementUpdate<OrderedExcalidrawElement>, "seed">; type ElementPartial<T extends ExcalidrawElement = ExcalidrawElement> = Omit<
ElementUpdate<Ordered<T>>,
"seed"
>;
/** /**
* Elements change is a low level primitive to capture a change between two sets of elements. * Elements change is a low level primitive to capture a change between two sets of elements.
@ -1216,6 +1235,18 @@ export class ElementsChange implements Change<SceneElementsMap> {
}); });
} }
if (isImageElement(element)) {
const _delta = delta as Delta<ElementPartial<ExcalidrawImageElement>>;
// we want to override `crop` only if modified so that we don't reset
// when undoing/redoing unrelated change
if (_delta.deleted.crop || _delta.inserted.crop) {
Object.assign(directlyApplicablePartial, {
// apply change verbatim
crop: _delta.inserted.crop ?? null,
});
}
}
if (!flags.containsVisibleDifference) { if (!flags.containsVisibleDifference) {
// strip away fractional as even if it would be different, it doesn't have to result in visible change // strip away fractional as even if it would be different, it doesn't have to result in visible change
const { index, ...rest } = directlyApplicablePartial; const { index, ...rest } = directlyApplicablePartial;

@ -26,6 +26,7 @@ import { trackEvent } from "../analytics";
import { import {
hasBoundTextElement, hasBoundTextElement,
isElbowArrow, isElbowArrow,
isImageElement,
isLinearElement, isLinearElement,
isTextElement, isTextElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
@ -127,6 +128,11 @@ export const SelectedShapeActions = ({
isLinearElement(targetElements[0]) && isLinearElement(targetElements[0]) &&
!isElbowArrow(targetElements[0]); !isElbowArrow(targetElements[0]);
const showCropEditorAction =
!appState.croppingElementId &&
targetElements.length === 1 &&
isImageElement(targetElements[0]);
return ( return (
<div className="panelColumn"> <div className="panelColumn">
<div> <div>
@ -245,6 +251,7 @@ export const SelectedShapeActions = ({
{renderAction("group")} {renderAction("group")}
{renderAction("ungroup")} {renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")} {showLinkIcon && renderAction("hyperlink")}
{showCropEditorAction && renderAction("cropEditor")}
{showLineEditorAction && renderAction("toggleLinearEditor")} {showLineEditorAction && renderAction("toggleLinearEditor")}
</div> </div>
</fieldset> </fieldset>

@ -35,6 +35,7 @@ import {
actionToggleElementLock, actionToggleElementLock,
actionToggleLinearEditor, actionToggleLinearEditor,
actionToggleObjectsSnapMode, actionToggleObjectsSnapMode,
actionToggleCropEditor,
} from "../actions"; } from "../actions";
import { createRedoAction, createUndoAction } from "../actions/actionHistory"; import { createRedoAction, createUndoAction } from "../actions/actionHistory";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
@ -445,7 +446,19 @@ import {
} from "../element/flowchart"; } from "../element/flowchart";
import { searchItemInFocusAtom } from "./SearchMenu"; import { searchItemInFocusAtom } from "./SearchMenu";
import type { LocalPoint, Radians } from "../../math"; import type { LocalPoint, Radians } from "../../math";
import { pointFrom, pointDistance, vector } from "../../math"; import {
clamp,
pointFrom,
pointDistance,
vector,
pointRotateRads,
vectorScale,
vectorFromPoint,
vectorSubtract,
vectorDot,
vectorNormalize,
} from "../../math";
import { cropElement } from "../element/cropElement";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
@ -589,6 +602,7 @@ class App extends React.Component<AppProps, AppState> {
lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null = lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null =
null; null;
lastPointerMoveEvent: PointerEvent | null = null; lastPointerMoveEvent: PointerEvent | null = null;
lastPointerMoveCoords: { x: number; y: number } | null = null;
lastViewportPosition = { x: 0, y: 0 }; lastViewportPosition = { x: 0, y: 0 };
animationFrameHandler = new AnimationFrameHandler(); animationFrameHandler = new AnimationFrameHandler();
@ -3924,6 +3938,28 @@ class App extends React.Component<AppProps, AppState> {
} }
if (!isInputLike(event.target)) { if (!isInputLike(event.target)) {
if (
(event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
this.state.croppingElementId
) {
this.finishImageCropping();
return;
}
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElementsMap(),
this.state,
);
if (
selectedElements.length === 1 &&
isImageElement(selectedElements[0]) &&
event.key === KEYS.ENTER
) {
this.startImageCropping(selectedElements[0]);
return;
}
if ( if (
event.key === KEYS.ESCAPE && event.key === KEYS.ESCAPE &&
this.flowChartCreator.isCreatingChart this.flowChartCreator.isCreatingChart
@ -4911,7 +4947,7 @@ class App extends React.Component<AppProps, AppState> {
const selectionShape = getSelectionBoxShape( const selectionShape = getSelectionBoxShape(
element, element,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
this.getElementHitThreshold(), isImageElement(element) ? 0 : this.getElementHitThreshold(),
); );
return isPointInShape(pointFrom(x, y), selectionShape); return isPointInShape(pointFrom(x, y), selectionShape);
@ -5140,6 +5176,22 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private startImageCropping = (image: ExcalidrawImageElement) => {
this.store.shouldCaptureIncrement();
this.setState({
croppingElementId: image.id,
});
};
private finishImageCropping = () => {
if (this.state.croppingElementId) {
this.store.shouldCaptureIncrement();
this.setState({
croppingElementId: null,
});
}
};
private handleCanvasDoubleClick = ( private handleCanvasDoubleClick = (
event: React.MouseEvent<HTMLCanvasElement>, event: React.MouseEvent<HTMLCanvasElement>,
) => { ) => {
@ -5171,6 +5223,11 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
this.startImageCropping(selectedElements[0]);
return;
}
resetCursor(this.interactiveCanvas); resetCursor(this.interactiveCanvas);
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
@ -6740,12 +6797,25 @@ class App extends React.Component<AppProps, AppState> {
this.device, this.device,
); );
if (elementWithTransformHandleType != null) { if (elementWithTransformHandleType != null) {
if (
elementWithTransformHandleType.transformHandleType === "rotation"
) {
this.setState({
resizingElement: elementWithTransformHandleType.element,
});
pointerDownState.resize.handleType =
elementWithTransformHandleType.transformHandleType;
} else if (this.state.croppingElementId) {
pointerDownState.resize.handleType =
elementWithTransformHandleType.transformHandleType;
} else {
this.setState({ this.setState({
resizingElement: elementWithTransformHandleType.element, resizingElement: elementWithTransformHandleType.element,
}); });
pointerDownState.resize.handleType = pointerDownState.resize.handleType =
elementWithTransformHandleType.transformHandleType; elementWithTransformHandleType.transformHandleType;
} }
}
} else if (selectedElements.length > 1) { } else if (selectedElements.length > 1) {
pointerDownState.resize.handleType = getTransformHandleTypeFromCoords( pointerDownState.resize.handleType = getTransformHandleTypeFromCoords(
getCommonBounds(selectedElements), getCommonBounds(selectedElements),
@ -6811,6 +6881,13 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
); );
if (
this.state.croppingElementId &&
pointerDownState.hit.element?.id !== this.state.croppingElementId
) {
this.finishImageCropping();
}
if (pointerDownState.hit.element) { if (pointerDownState.hit.element) {
// Early return if pointer is hitting link icon // Early return if pointer is hitting link icon
const hitLinkElement = this.getElementLinkAtPosition( const hitLinkElement = this.getElementLinkAtPosition(
@ -7612,6 +7689,11 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
) { ) {
return withBatchedUpdatesThrottled((event: PointerEvent) => { return withBatchedUpdatesThrottled((event: PointerEvent) => {
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
const lastPointerCoords =
this.lastPointerMoveCoords ?? pointerDownState.origin;
this.lastPointerMoveCoords = pointerCoords;
// We need to initialize dragOffsetXY only after we've updated // We need to initialize dragOffsetXY only after we've updated
// `state.selectedElementIds` on pointerDown. Doing it here in pointerMove // `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
// event handler should hopefully ensure we're already working with // event handler should hopefully ensure we're already working with
@ -7634,8 +7716,6 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
if (isEraserActive(this.state)) { if (isEraserActive(this.state)) {
this.handleEraser(event, pointerDownState, pointerCoords); this.handleEraser(event, pointerDownState, pointerCoords);
return; return;
@ -7672,6 +7752,9 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.resize.isResizing) { if (pointerDownState.resize.isResizing) {
pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y; pointerDownState.lastCoords.y = pointerCoords.y;
if (this.maybeHandleCrop(pointerDownState, event)) {
return true;
}
if (this.maybeHandleResize(pointerDownState, event)) { if (this.maybeHandleResize(pointerDownState, event)) {
return true; return true;
} }
@ -7845,6 +7928,96 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
// #region move crop region
if (this.state.croppingElementId) {
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(this.state.croppingElementId);
if (
croppingElement &&
isImageElement(croppingElement) &&
croppingElement.crop !== null &&
pointerDownState.hit.element === croppingElement
) {
const crop = croppingElement.crop;
const image =
isInitializedImageElement(croppingElement) &&
this.imageCache.get(croppingElement.fileId)?.image;
if (image && !(image instanceof Promise)) {
const instantDragOffset = vectorScale(
vector(
pointerCoords.x - lastPointerCoords.x,
pointerCoords.y - lastPointerCoords.y,
),
Math.max(this.state.zoom.value, 2),
);
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
croppingElement,
elementsMap,
);
const topLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y1),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const topRight = vectorFromPoint(
pointRotateRads(
pointFrom(x2, y1),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const bottomLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y2),
pointFrom(cx, cy),
croppingElement.angle,
),
);
const topEdge = vectorNormalize(
vectorSubtract(topRight, topLeft),
);
const leftEdge = vectorNormalize(
vectorSubtract(bottomLeft, topLeft),
);
// project instantDrafOffset onto leftEdge and topEdge to decompose
const offsetVector = vector(
vectorDot(instantDragOffset, topEdge),
vectorDot(instantDragOffset, leftEdge),
);
const nextCrop = {
...crop,
x: clamp(
crop.x -
offsetVector[0] * Math.sign(croppingElement.scale[0]),
0,
image.naturalWidth - crop.width,
),
y: clamp(
crop.y -
offsetVector[1] * Math.sign(croppingElement.scale[1]),
0,
image.naturalHeight - crop.height,
),
};
mutateElement(croppingElement, {
crop: nextCrop,
});
return;
}
}
}
// Snap cache *must* be synchronously popuplated before initial drag, // Snap cache *must* be synchronously popuplated before initial drag,
// otherwise the first drag even will not snap, causing a jump before // otherwise the first drag even will not snap, causing a jump before
// it snaps to its position if previously snapped already. // it snaps to its position if previously snapped already.
@ -7978,6 +8151,7 @@ class App extends React.Component<AppProps, AppState> {
this.maybeCacheVisibleGaps(event, selectedElements, true); this.maybeCacheVisibleGaps(event, selectedElements, true);
this.maybeCacheReferenceSnapPoints(event, selectedElements, true); this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
} }
return; return;
} }
} }
@ -8226,15 +8400,18 @@ class App extends React.Component<AppProps, AppState> {
const { const {
newElement, newElement,
resizingElement, resizingElement,
croppingElementId,
multiElement, multiElement,
activeTool, activeTool,
isResizing, isResizing,
isRotating, isRotating,
isCropping,
} = this.state; } = this.state;
this.setState((prevState) => ({ this.setState((prevState) => ({
isResizing: false, isResizing: false,
isRotating: false, isRotating: false,
isCropping: false,
resizingElement: null, resizingElement: null,
selectionElement: null, selectionElement: null,
frameToHighlight: null, frameToHighlight: null,
@ -8244,6 +8421,8 @@ class App extends React.Component<AppProps, AppState> {
originSnapOffset: null, originSnapOffset: null,
})); }));
this.lastPointerMoveCoords = null;
SnapCache.setReferenceSnapPoints(null); SnapCache.setReferenceSnapPoints(null);
SnapCache.setVisibleGaps(null); SnapCache.setVisibleGaps(null);
@ -8726,6 +8905,20 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
// click outside the cropping region to exit
if (
// not in the cropping mode at all
!croppingElementId ||
// in the cropping mode
(croppingElementId &&
// not cropping and no hit element
((!hitElement && !isCropping) ||
// hitting something else
(hitElement && hitElement.id !== croppingElementId)))
) {
this.finishImageCropping();
}
const pointerStart = this.lastPointerDownEvent; const pointerStart = this.lastPointerDownEvent;
const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent; const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent;
@ -8981,7 +9174,12 @@ class App extends React.Component<AppProps, AppState> {
this.store.shouldCaptureIncrement(); this.store.shouldCaptureIncrement();
} }
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) { if (
pointerDownState.drag.hasOccurred ||
isResizing ||
isRotating ||
isCropping
) {
// We only allow binding via linear elements, specifically via dragging // We only allow binding via linear elements, specifically via dragging
// the endpoints ("start" or "end"). // the endpoints ("start" or "end").
const linearElements = this.scene const linearElements = this.scene
@ -9195,7 +9393,7 @@ class App extends React.Component<AppProps, AppState> {
/** /**
* inserts image into elements array and rerenders * inserts image into elements array and rerenders
*/ */
private insertImageElement = async ( insertImageElement = async (
imageElement: ExcalidrawImageElement, imageElement: ExcalidrawImageElement,
imageFile: File, imageFile: File,
showCursorImagePreview?: boolean, showCursorImagePreview?: boolean,
@ -9348,7 +9546,7 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private initializeImageDimensions = ( initializeImageDimensions = (
imageElement: ExcalidrawImageElement, imageElement: ExcalidrawImageElement,
forceNaturalSize = false, forceNaturalSize = false,
) => { ) => {
@ -9396,7 +9594,13 @@ class App extends React.Component<AppProps, AppState> {
const x = imageElement.x + imageElement.width / 2 - width / 2; const x = imageElement.x + imageElement.width / 2 - width / 2;
const y = imageElement.y + imageElement.height / 2 - height / 2; const y = imageElement.y + imageElement.height / 2 - height / 2;
mutateElement(imageElement, { x, y, width, height }); mutateElement(imageElement, {
x,
y,
width,
height,
crop: null,
});
} }
}; };
@ -9935,6 +10139,83 @@ class App extends React.Component<AppProps, AppState> {
} }
}; };
private maybeHandleCrop = (
pointerDownState: PointerDownState,
event: MouseEvent | KeyboardEvent,
): boolean => {
// to crop, we must already be in the cropping mode, where croppingElement has been set
if (!this.state.croppingElementId) {
return false;
}
const transformHandleType = pointerDownState.resize.handleType;
const pointerCoords = pointerDownState.lastCoords;
const [x, y] = getGridPoint(
pointerCoords.x - pointerDownState.resize.offset.x,
pointerCoords.y - pointerDownState.resize.offset.y,
this.getEffectiveGridSize(),
);
const croppingElement = this.scene
.getNonDeletedElementsMap()
.get(this.state.croppingElementId);
if (
transformHandleType &&
croppingElement &&
isImageElement(croppingElement)
) {
const croppingAtStateStart = pointerDownState.originalElements.get(
croppingElement.id,
);
const image =
isInitializedImageElement(croppingElement) &&
this.imageCache.get(croppingElement.fileId)?.image;
if (
croppingAtStateStart &&
isImageElement(croppingAtStateStart) &&
image &&
!(image instanceof Promise)
) {
mutateElement(
croppingElement,
cropElement(
croppingElement,
transformHandleType,
image.naturalWidth,
image.naturalHeight,
x,
y,
event.shiftKey
? croppingAtStateStart.width / croppingAtStateStart.height
: undefined,
),
);
updateBoundElements(
croppingElement,
this.scene.getNonDeletedElementsMap(),
{
oldSize: {
width: croppingElement.width,
height: croppingElement.height,
},
},
);
this.setState({
isCropping: transformHandleType && transformHandleType !== "rotation",
});
}
return true;
}
return false;
};
private maybeHandleResize = ( private maybeHandleResize = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
event: MouseEvent | KeyboardEvent, event: MouseEvent | KeyboardEvent,
@ -9951,7 +10232,9 @@ class App extends React.Component<AppProps, AppState> {
// Frames cannot be rotated. // Frames cannot be rotated.
(selectedFrames.length > 0 && transformHandleType === "rotation") || (selectedFrames.length > 0 && transformHandleType === "rotation") ||
// Elbow arrows cannot be transformed (resized or rotated). // Elbow arrows cannot be transformed (resized or rotated).
(selectedElements.length === 1 && isElbowArrow(selectedElements[0])) (selectedElements.length === 1 && isElbowArrow(selectedElements[0])) ||
// Do not resize when in crop mode
this.state.croppingElementId
) { ) {
return false; return false;
} }
@ -10126,6 +10409,8 @@ class App extends React.Component<AppProps, AppState> {
actionSelectAllElementsInFrame, actionSelectAllElementsInFrame,
actionRemoveAllElementsFromFrame, actionRemoveAllElementsFromFrame,
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionToggleCropEditor,
CONTEXT_MENU_SEPARATOR,
...options, ...options,
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionCopyStyles, actionCopyStyles,

@ -279,6 +279,7 @@ function CommandPaletteInner({
actionManager.actions.increaseFontSize, actionManager.actions.increaseFontSize,
actionManager.actions.decreaseFontSize, actionManager.actions.decreaseFontSize,
actionManager.actions.toggleLinearEditor, actionManager.actions.toggleLinearEditor,
actionManager.actions.cropEditor,
actionLink, actionLink,
].map((action: Action) => ].map((action: Action) =>
actionToCommand( actionToCommand(

@ -222,6 +222,16 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
]} ]}
isOr={false} isOr={false}
/> />
<Shortcut
label={t("helpDialog.cropStart")}
shortcuts={[t("helpDialog.doubleClick"), getShortcutKey("Enter")]}
isOr={true}
/>
<Shortcut
label={t("helpDialog.cropFinish")}
shortcuts={[getShortcutKey("Enter"), getShortcutKey("Escape")]}
isOr={true}
/>
<Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} /> <Shortcut label={t("toolBar.lock")} shortcuts={[KEYS.Q]} />
<Shortcut <Shortcut
label={t("helpDialog.preventBinding")} label={t("helpDialog.preventBinding")}

@ -100,6 +100,14 @@ const getHints = ({
return t("hints.text_editing"); return t("hints.text_editing");
} }
if (appState.croppingElementId) {
return t("hints.leaveCropEditor");
}
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
return t("hints.enterCropEditor");
}
if (activeTool.type === "selection") { if (activeTool.type === "selection") {
if ( if (
appState.selectionElement && appState.selectionElement &&

@ -203,6 +203,8 @@ const getRelevantAppStateProps = (
snapLines: appState.snapLines, snapLines: appState.snapLines,
zenModeEnabled: appState.zenModeEnabled, zenModeEnabled: appState.zenModeEnabled,
editingTextElement: appState.editingTextElement, editingTextElement: appState.editingTextElement,
isCropping: appState.isCropping,
croppingElementId: appState.croppingElementId,
searchMatches: appState.searchMatches, searchMatches: appState.searchMatches,
}); });

@ -107,6 +107,7 @@ const getRelevantAppStateProps = (
frameToHighlight: appState.frameToHighlight, frameToHighlight: appState.frameToHighlight,
editingGroupId: appState.editingGroupId, editingGroupId: appState.editingGroupId,
currentHoveredFontFamily: appState.currentHoveredFontFamily, currentHoveredFontFamily: appState.currentHoveredFontFamily,
croppingElementId: appState.croppingElementId,
}); });
const areEqual = ( const areEqual = (

@ -2147,3 +2147,12 @@ export const upIcon = createIcon(
</g>, </g>,
tablerIconProps, tablerIconProps,
); );
export const cropIcon = createIcon(
<g strokeWidth="1.25">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8 5v10a1 1 0 0 0 1 1h10" />
<path d="M5 8h10a1 1 0 0 1 1 1v10" />
</g>,
tablerIconProps,
);

@ -258,6 +258,7 @@ const restoreElement = (
status: element.status || "pending", status: element.status || "pending",
fileId: element.fileId, fileId: element.fileId,
scale: element.scale || [1, 1], scale: element.scale || [1, 1],
crop: element.crop ?? null,
}); });
case "line": case "line":
// @ts-ignore LEGACY type // @ts-ignore LEGACY type

@ -0,0 +1,587 @@
import { type Point } from "points-on-curve";
import {
type Radians,
pointFrom,
pointCenter,
pointRotateRads,
vectorFromPoint,
vectorNormalize,
vectorSubtract,
vectorAdd,
vectorScale,
pointFromVector,
clamp,
isCloseTo,
} from "../../math";
import type { TransformHandleType } from "./transformHandles";
import type {
ElementsMap,
ExcalidrawElement,
ExcalidrawImageElement,
ImageCrop,
NonDeleted,
} from "./types";
import {
getElementAbsoluteCoords,
getResizedElementAbsoluteCoords,
} from "./bounds";
const MINIMAL_CROP_SIZE = 10;
export const cropElement = (
element: ExcalidrawImageElement,
transformHandle: TransformHandleType,
naturalWidth: number,
naturalHeight: number,
pointerX: number,
pointerY: number,
widthAspectRatio?: number,
) => {
const { width: uncroppedWidth, height: uncroppedHeight } =
getUncroppedWidthAndHeight(element);
const naturalWidthToUncropped = naturalWidth / uncroppedWidth;
const naturalHeightToUncropped = naturalHeight / uncroppedHeight;
const croppedLeft = (element.crop?.x ?? 0) / naturalWidthToUncropped;
const croppedTop = (element.crop?.y ?? 0) / naturalHeightToUncropped;
/**
* uncropped width
* **
* | (x,y) (natural) |
* | ** |
* | |///////| height | uncropped height
* | ** |
* | width (natural) |
* **
*/
const rotatedPointer = pointRotateRads(
pointFrom(pointerX, pointerY),
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
-element.angle as Radians,
);
pointerX = rotatedPointer[0];
pointerY = rotatedPointer[1];
let nextWidth = element.width;
let nextHeight = element.height;
let crop: ImageCrop | null = element.crop ?? {
x: 0,
y: 0,
width: naturalWidth,
height: naturalHeight,
naturalWidth,
naturalHeight,
};
const previousCropHeight = crop.height;
const previousCropWidth = crop.width;
const isFlippedByX = element.scale[0] === -1;
const isFlippedByY = element.scale[1] === -1;
let changeInHeight = pointerY - element.y;
let changeInWidth = pointerX - element.x;
if (transformHandle.includes("n")) {
nextHeight = clamp(
element.height - changeInHeight,
MINIMAL_CROP_SIZE,
isFlippedByY ? uncroppedHeight - croppedTop : element.height + croppedTop,
);
}
if (transformHandle.includes("s")) {
changeInHeight = pointerY - element.y - element.height;
nextHeight = clamp(
element.height + changeInHeight,
MINIMAL_CROP_SIZE,
isFlippedByY ? element.height + croppedTop : uncroppedHeight - croppedTop,
);
}
if (transformHandle.includes("e")) {
changeInWidth = pointerX - element.x - element.width;
nextWidth = clamp(
element.width + changeInWidth,
MINIMAL_CROP_SIZE,
isFlippedByX ? element.width + croppedLeft : uncroppedWidth - croppedLeft,
);
}
if (transformHandle.includes("w")) {
nextWidth = clamp(
element.width - changeInWidth,
MINIMAL_CROP_SIZE,
isFlippedByX ? uncroppedWidth - croppedLeft : element.width + croppedLeft,
);
}
const updateCropWidthAndHeight = (crop: ImageCrop) => {
crop.height = nextHeight * naturalHeightToUncropped;
crop.width = nextWidth * naturalWidthToUncropped;
};
updateCropWidthAndHeight(crop);
const adjustFlipForHandle = (
handle: TransformHandleType,
crop: ImageCrop,
) => {
updateCropWidthAndHeight(crop);
if (handle.includes("n")) {
if (!isFlippedByY) {
crop.y += previousCropHeight - crop.height;
}
}
if (handle.includes("s")) {
if (isFlippedByY) {
crop.y += previousCropHeight - crop.height;
}
}
if (handle.includes("e")) {
if (isFlippedByX) {
crop.x += previousCropWidth - crop.width;
}
}
if (handle.includes("w")) {
if (!isFlippedByX) {
crop.x += previousCropWidth - crop.width;
}
}
};
switch (transformHandle) {
case "n": {
if (widthAspectRatio) {
const distanceToLeft = croppedLeft + element.width / 2;
const distanceToRight =
uncroppedWidth - croppedLeft - element.width / 2;
const MAX_WIDTH = Math.min(distanceToLeft, distanceToRight) * 2;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
adjustFlipForHandle(transformHandle, crop);
if (widthAspectRatio) {
crop.x += (previousCropWidth - crop.width) / 2;
}
break;
}
case "s": {
if (widthAspectRatio) {
const distanceToLeft = croppedLeft + element.width / 2;
const distanceToRight =
uncroppedWidth - croppedLeft - element.width / 2;
const MAX_WIDTH = Math.min(distanceToLeft, distanceToRight) * 2;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
adjustFlipForHandle(transformHandle, crop);
if (widthAspectRatio) {
crop.x += (previousCropWidth - crop.width) / 2;
}
break;
}
case "w": {
if (widthAspectRatio) {
const distanceToTop = croppedTop + element.height / 2;
const distanceToBottom =
uncroppedHeight - croppedTop - element.height / 2;
const MAX_HEIGHT = Math.min(distanceToTop, distanceToBottom) * 2;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
}
adjustFlipForHandle(transformHandle, crop);
if (widthAspectRatio) {
crop.y += (previousCropHeight - crop.height) / 2;
}
break;
}
case "e": {
if (widthAspectRatio) {
const distanceToTop = croppedTop + element.height / 2;
const distanceToBottom =
uncroppedHeight - croppedTop - element.height / 2;
const MAX_HEIGHT = Math.min(distanceToTop, distanceToBottom) * 2;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
}
adjustFlipForHandle(transformHandle, crop);
if (widthAspectRatio) {
crop.y += (previousCropHeight - crop.height) / 2;
}
break;
}
case "ne": {
if (widthAspectRatio) {
if (changeInWidth > -changeInHeight) {
const MAX_HEIGHT = isFlippedByY
? uncroppedHeight - croppedTop
: croppedTop + element.height;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
} else {
const MAX_WIDTH = isFlippedByX
? croppedLeft + element.width
: uncroppedWidth - croppedLeft;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
}
adjustFlipForHandle(transformHandle, crop);
break;
}
case "nw": {
if (widthAspectRatio) {
if (changeInWidth < changeInHeight) {
const MAX_HEIGHT = isFlippedByY
? uncroppedHeight - croppedTop
: croppedTop + element.height;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
} else {
const MAX_WIDTH = isFlippedByX
? uncroppedWidth - croppedLeft
: croppedLeft + element.width;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
}
adjustFlipForHandle(transformHandle, crop);
break;
}
case "se": {
if (widthAspectRatio) {
if (changeInWidth > changeInHeight) {
const MAX_HEIGHT = isFlippedByY
? croppedTop + element.height
: uncroppedHeight - croppedTop;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
} else {
const MAX_WIDTH = isFlippedByX
? croppedLeft + element.width
: uncroppedWidth - croppedLeft;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
}
adjustFlipForHandle(transformHandle, crop);
break;
}
case "sw": {
if (widthAspectRatio) {
if (-changeInWidth > changeInHeight) {
const MAX_HEIGHT = isFlippedByY
? croppedTop + element.height
: uncroppedHeight - croppedTop;
nextHeight = clamp(
nextWidth / widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_HEIGHT,
);
nextWidth = nextHeight * widthAspectRatio;
} else {
const MAX_WIDTH = isFlippedByX
? uncroppedWidth - croppedLeft
: croppedLeft + element.width;
nextWidth = clamp(
nextHeight * widthAspectRatio,
MINIMAL_CROP_SIZE,
MAX_WIDTH,
);
nextHeight = nextWidth / widthAspectRatio;
}
}
adjustFlipForHandle(transformHandle, crop);
break;
}
default:
break;
}
const newOrigin = recomputeOrigin(
element,
transformHandle,
nextWidth,
nextHeight,
!!widthAspectRatio,
);
// reset crop to null if we're back to orig size
if (
isCloseTo(crop.width, crop.naturalWidth) &&
isCloseTo(crop.height, crop.naturalHeight)
) {
crop = null;
}
return {
x: newOrigin[0],
y: newOrigin[1],
width: nextWidth,
height: nextHeight,
crop,
};
};
const recomputeOrigin = (
stateAtCropStart: NonDeleted<ExcalidrawElement>,
transformHandle: TransformHandleType,
width: number,
height: number,
shouldMaintainAspectRatio?: boolean,
) => {
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
stateAtCropStart,
stateAtCropStart.width,
stateAtCropStart.height,
true,
);
const startTopLeft = pointFrom(x1, y1);
const startBottomRight = pointFrom(x2, y2);
const startCenter: any = pointCenter(startTopLeft, startBottomRight);
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
getResizedElementAbsoluteCoords(stateAtCropStart, width, height, true);
const newBoundsWidth = newBoundsX2 - newBoundsX1;
const newBoundsHeight = newBoundsY2 - newBoundsY1;
// Calculate new topLeft based on fixed corner during resize
let newTopLeft = [...startTopLeft] as [number, number];
if (["n", "w", "nw"].includes(transformHandle)) {
newTopLeft = [
startBottomRight[0] - Math.abs(newBoundsWidth),
startBottomRight[1] - Math.abs(newBoundsHeight),
];
}
if (transformHandle === "ne") {
const bottomLeft = [startTopLeft[0], startBottomRight[1]];
newTopLeft = [bottomLeft[0], bottomLeft[1] - Math.abs(newBoundsHeight)];
}
if (transformHandle === "sw") {
const topRight = [startBottomRight[0], startTopLeft[1]];
newTopLeft = [topRight[0] - Math.abs(newBoundsWidth), topRight[1]];
}
if (shouldMaintainAspectRatio) {
if (["s", "n"].includes(transformHandle)) {
newTopLeft[0] = startCenter[0] - newBoundsWidth / 2;
}
if (["e", "w"].includes(transformHandle)) {
newTopLeft[1] = startCenter[1] - newBoundsHeight / 2;
}
}
// adjust topLeft to new rotation point
const angle = stateAtCropStart.angle;
const rotatedTopLeft = pointRotateRads(newTopLeft, startCenter, angle);
const newCenter: Point = [
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
];
const rotatedNewCenter = pointRotateRads(newCenter, startCenter, angle);
newTopLeft = pointRotateRads(
rotatedTopLeft,
rotatedNewCenter,
-angle as Radians,
);
const newOrigin = [...newTopLeft];
newOrigin[0] += stateAtCropStart.x - newBoundsX1;
newOrigin[1] += stateAtCropStart.y - newBoundsY1;
return newOrigin;
};
// refer to https://link.excalidraw.com/l/6rfy1007QOo/6stx5PmRn0k
export const getUncroppedImageElement = (
element: ExcalidrawImageElement,
elementsMap: ElementsMap,
) => {
if (element.crop) {
const { width, height } = getUncroppedWidthAndHeight(element);
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
);
const topLeftVector = vectorFromPoint(
pointRotateRads(pointFrom(x1, y1), pointFrom(cx, cy), element.angle),
);
const topRightVector = vectorFromPoint(
pointRotateRads(pointFrom(x2, y1), pointFrom(cx, cy), element.angle),
);
const topEdgeNormalized = vectorNormalize(
vectorSubtract(topRightVector, topLeftVector),
);
const bottomLeftVector = vectorFromPoint(
pointRotateRads(pointFrom(x1, y2), pointFrom(cx, cy), element.angle),
);
const leftEdgeVector = vectorSubtract(bottomLeftVector, topLeftVector);
const leftEdgeNormalized = vectorNormalize(leftEdgeVector);
const { cropX, cropY } = adjustCropPosition(element.crop, element.scale);
const rotatedTopLeft = vectorAdd(
vectorAdd(
topLeftVector,
vectorScale(
topEdgeNormalized,
(-cropX * width) / element.crop.naturalWidth,
),
),
vectorScale(
leftEdgeNormalized,
(-cropY * height) / element.crop.naturalHeight,
),
);
const center = pointFromVector(
vectorAdd(
vectorAdd(rotatedTopLeft, vectorScale(topEdgeNormalized, width / 2)),
vectorScale(leftEdgeNormalized, height / 2),
),
);
const unrotatedTopLeft = pointRotateRads(
pointFromVector(rotatedTopLeft),
center,
-element.angle as Radians,
);
const uncroppedElement: ExcalidrawImageElement = {
...element,
x: unrotatedTopLeft[0],
y: unrotatedTopLeft[1],
width,
height,
crop: null,
};
return uncroppedElement;
}
return element;
};
export const getUncroppedWidthAndHeight = (element: ExcalidrawImageElement) => {
if (element.crop) {
const width =
element.width / (element.crop.width / element.crop.naturalWidth);
const height =
element.height / (element.crop.height / element.crop.naturalHeight);
return {
width,
height,
};
}
return {
width: element.width,
height: element.height,
};
};
const adjustCropPosition = (
crop: ImageCrop,
scale: ExcalidrawImageElement["scale"],
) => {
let cropX = crop.x;
let cropY = crop.y;
const flipX = scale[0] === -1;
const flipY = scale[1] === -1;
if (flipX) {
cropX = crop.naturalWidth - Math.abs(cropX) - crop.width;
}
if (flipY) {
cropY = crop.naturalHeight - Math.abs(cropY) - crop.height;
}
return {
cropX,
cropY,
};
};

@ -16,6 +16,7 @@ import {
isArrowElement, isArrowElement,
isElbowArrow, isElbowArrow,
isFrameLikeElement, isFrameLikeElement,
isImageElement,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { getFontString } from "../utils"; import { getFontString } from "../utils";
@ -251,6 +252,14 @@ export const dragNewElement = ({
} }
if (width !== 0 && height !== 0) { if (width !== 0 && height !== 0) {
let imageInitialDimension = null;
if (isImageElement(newElement)) {
imageInitialDimension = {
initialWidth: width,
initialHeight: height,
};
}
mutateElement( mutateElement(
newElement, newElement,
{ {
@ -259,6 +268,7 @@ export const dragNewElement = ({
width, width,
height, height,
...textAutoResize, ...textAutoResize,
...imageInitialDimension,
}, },
informMutation, informMutation,
); );

@ -477,6 +477,7 @@ export const newImageElement = (
status?: ExcalidrawImageElement["status"]; status?: ExcalidrawImageElement["status"];
fileId?: ExcalidrawImageElement["fileId"]; fileId?: ExcalidrawImageElement["fileId"];
scale?: ExcalidrawImageElement["scale"]; scale?: ExcalidrawImageElement["scale"];
crop?: ExcalidrawImageElement["crop"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawImageElement> => { ): NonDeleted<ExcalidrawImageElement> => {
return { return {
@ -487,6 +488,7 @@ export const newImageElement = (
status: opts.status ?? "pending", status: opts.status ?? "pending",
fileId: opts.fileId ?? null, fileId: opts.fileId ?? null,
scale: opts.scale ?? [1, 1], scale: opts.scale ?? [1, 1],
crop: opts.crop ?? null,
}; };
}; };

@ -20,7 +20,7 @@ import type { AppState, Device, Zoom } from "../types";
import type { Bounds } from "./bounds"; import type { Bounds } from "./bounds";
import { getElementAbsoluteCoords } from "./bounds"; import { getElementAbsoluteCoords } from "./bounds";
import { SIDE_RESIZING_THRESHOLD } from "../constants"; import { SIDE_RESIZING_THRESHOLD } from "../constants";
import { isLinearElement } from "./typeChecks"; import { isImageElement, isLinearElement } from "./typeChecks";
import type { GlobalPoint, LineSegment, LocalPoint } from "../../math"; import type { GlobalPoint, LineSegment, LocalPoint } from "../../math";
import { import {
pointFrom, pointFrom,
@ -90,7 +90,11 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
// do not resize from the sides for linear elements with only two points // do not resize from the sides for linear elements with only two points
if (!(isLinearElement(element) && element.points.length <= 2)) { if (!(isLinearElement(element) && element.points.length <= 2)) {
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value; const SPACING = isImageElement(element)
? 0
: SIDE_RESIZING_THRESHOLD / zoom.value;
const ZOOMED_SIDE_RESIZING_THRESHOLD =
SIDE_RESIZING_THRESHOLD / zoom.value;
const sides = getSelectionBorders( const sides = getSelectionBorders(
pointFrom(x1 - SPACING, y1 - SPACING), pointFrom(x1 - SPACING, y1 - SPACING),
pointFrom(x2 + SPACING, y2 + SPACING), pointFrom(x2 + SPACING, y2 + SPACING),
@ -104,7 +108,7 @@ export const resizeTest = <Point extends GlobalPoint | LocalPoint>(
pointOnLineSegment( pointOnLineSegment(
pointFrom(x, y), pointFrom(x, y),
side as LineSegment<Point>, side as LineSegment<Point>,
SPACING, ZOOMED_SIDE_RESIZING_THRESHOLD,
) )
) { ) {
return dir as TransformHandleType; return dir as TransformHandleType;

@ -11,6 +11,7 @@ import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
import { import {
isElbowArrow, isElbowArrow,
isFrameLikeElement, isFrameLikeElement,
isImageElement,
isLinearElement, isLinearElement,
} from "./typeChecks"; } from "./typeChecks";
import { import {
@ -129,6 +130,7 @@ export const getTransformHandlesFromCoords = (
pointerType: PointerType, pointerType: PointerType,
omitSides: { [T in TransformHandleType]?: boolean } = {}, omitSides: { [T in TransformHandleType]?: boolean } = {},
margin = 4, margin = 4,
spacing = DEFAULT_TRANSFORM_HANDLE_SPACING,
): TransformHandles => { ): TransformHandles => {
const size = transformHandleSizes[pointerType]; const size = transformHandleSizes[pointerType];
const handleWidth = size / zoom.value; const handleWidth = size / zoom.value;
@ -140,8 +142,7 @@ export const getTransformHandlesFromCoords = (
const width = x2 - x1; const width = x2 - x1;
const height = y2 - y1; const height = y2 - y1;
const dashedLineMargin = margin / zoom.value; const dashedLineMargin = margin / zoom.value;
const centeringOffset = const centeringOffset = (size - spacing * 2) / (2 * zoom.value);
(size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value);
const transformHandles: TransformHandles = { const transformHandles: TransformHandles = {
nw: omitSides.nw nw: omitSides.nw
@ -301,8 +302,10 @@ export const getTransformHandles = (
rotation: true, rotation: true,
}; };
} }
const dashedLineMargin = isLinearElement(element) const margin = isLinearElement(element)
? DEFAULT_TRANSFORM_HANDLE_SPACING + 8 ? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
: isImageElement(element)
? 0
: DEFAULT_TRANSFORM_HANDLE_SPACING; : DEFAULT_TRANSFORM_HANDLE_SPACING;
return getTransformHandlesFromCoords( return getTransformHandlesFromCoords(
getElementAbsoluteCoords(element, elementsMap, true), getElementAbsoluteCoords(element, elementsMap, true),
@ -310,7 +313,8 @@ export const getTransformHandles = (
zoom, zoom,
pointerType, pointerType,
omitSides, omitSides,
dashedLineMargin, margin,
isImageElement(element) ? 0 : undefined,
); );
}; };

@ -132,6 +132,15 @@ export type IframeData =
| { type: "document"; srcdoc: (theme: Theme) => string } | { type: "document"; srcdoc: (theme: Theme) => string }
); );
export type ImageCrop = {
x: number;
y: number;
width: number;
height: number;
naturalWidth: number;
naturalHeight: number;
};
export type ExcalidrawImageElement = _ExcalidrawElementBase & export type ExcalidrawImageElement = _ExcalidrawElementBase &
Readonly<{ Readonly<{
type: "image"; type: "image";
@ -140,6 +149,8 @@ export type ExcalidrawImageElement = _ExcalidrawElementBase &
status: "pending" | "saved" | "error"; status: "pending" | "saved" | "error";
/** X and Y scale factors <-1, 1>, used for image axis flipping */ /** X and Y scale factors <-1, 1>, used for image axis flipping */
scale: [number, number]; scale: [number, number];
/** whether an element is cropped */
crop: ImageCrop | null;
}>; }>;
export type InitializedExcalidrawImageElement = MarkNonNullable< export type InitializedExcalidrawImageElement = MarkNonNullable<

@ -328,7 +328,9 @@
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging", "deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
"eraserRevert": "Hold Alt to revert the elements marked for deletion", "eraserRevert": "Hold Alt to revert the elements marked for deletion",
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.", "firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.",
"disableSnapping": "Hold CtrlOrCmd to disable snapping" "disableSnapping": "Hold CtrlOrCmd to disable snapping",
"enterCropEditor": "Double click the image or press ENTER to crop the image",
"leaveCropEditor": "Click outside the image or press ENTER or ESCAPE to finish cropping"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Cannot show preview", "cannotShowPreview": "Cannot show preview",
@ -399,7 +401,9 @@
"zoomToSelection": "Zoom to selection", "zoomToSelection": "Zoom to selection",
"toggleElementLock": "Lock/unlock selection", "toggleElementLock": "Lock/unlock selection",
"movePageUpDown": "Move page up/down", "movePageUpDown": "Move page up/down",
"movePageLeftRight": "Move page left/right" "movePageLeftRight": "Move page left/right",
"cropStart": "Crop image",
"cropFinish": "Finish image cropping"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Clear canvas" "title": "Clear canvas"

@ -54,6 +54,7 @@ import oc from "open-color";
import { import {
isElbowArrow, isElbowArrow,
isFrameLikeElement, isFrameLikeElement,
isImageElement,
isLinearElement, isLinearElement,
isTextElement, isTextElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
@ -62,6 +63,7 @@ import type {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
ExcalidrawImageElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
ExcalidrawTextElement, ExcalidrawTextElement,
GroupId, GroupId,
@ -307,38 +309,42 @@ const renderBindingHighlightForSuggestedPointBinding = (
}); });
}; };
const renderSelectionBorder = ( type ElementSelectionBorder = {
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
elementProperties: {
angle: number; angle: number;
elementX1: number; x1: number;
elementY1: number; y1: number;
elementX2: number; x2: number;
elementY2: number; y2: number;
selectionColors: string[]; selectionColors: string[];
dashed?: boolean; dashed?: boolean;
cx: number; cx: number;
cy: number; cy: number;
activeEmbeddable: boolean; activeEmbeddable: boolean;
}, padding?: number;
};
const renderSelectionBorder = (
context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState,
elementProperties: ElementSelectionBorder,
) => { ) => {
const { const {
angle, angle,
elementX1, x1,
elementY1, y1,
elementX2, x2,
elementY2, y2,
selectionColors, selectionColors,
cx, cx,
cy, cy,
dashed, dashed,
activeEmbeddable, activeEmbeddable,
} = elementProperties; } = elementProperties;
const elementWidth = elementX2 - elementX1; const elementWidth = x2 - x1;
const elementHeight = elementY2 - elementY1; const elementHeight = y2 - y1;
const padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2; const padding =
elementProperties.padding ?? DEFAULT_TRANSFORM_HANDLE_SPACING * 2;
const linePadding = padding / appState.zoom.value; const linePadding = padding / appState.zoom.value;
const lineWidth = 8 / appState.zoom.value; const lineWidth = 8 / appState.zoom.value;
@ -360,8 +366,8 @@ const renderSelectionBorder = (
context.lineDashOffset = (lineWidth + spaceWidth) * index; context.lineDashOffset = (lineWidth + spaceWidth) * index;
strokeRectWithRotation( strokeRectWithRotation(
context, context,
elementX1 - linePadding, x1 - linePadding,
elementY1 - linePadding, y1 - linePadding,
elementWidth + linePadding * 2, elementWidth + linePadding * 2,
elementHeight + linePadding * 2, elementHeight + linePadding * 2,
cx, cx,
@ -433,18 +439,17 @@ const renderElementsBoxHighlight = (
); );
const getSelectionFromElements = (elements: ExcalidrawElement[]) => { const getSelectionFromElements = (elements: ExcalidrawElement[]) => {
const [elementX1, elementY1, elementX2, elementY2] = const [x1, y1, x2, y2] = getCommonBounds(elements);
getCommonBounds(elements);
return { return {
angle: 0, angle: 0,
elementX1, x1,
elementX2, x2,
elementY1, y1,
elementY2, y2,
selectionColors: ["rgb(0,118,255)"], selectionColors: ["rgb(0,118,255)"],
dashed: false, dashed: false,
cx: elementX1 + (elementX2 - elementX1) / 2, cx: x1 + (x2 - x1) / 2,
cy: elementY1 + (elementY2 - elementY1) / 2, cy: y1 + (y2 - y1) / 2,
activeEmbeddable: false, activeEmbeddable: false,
}; };
}; };
@ -594,6 +599,111 @@ const renderTransformHandles = (
}); });
}; };
const renderCropHandles = (
context: CanvasRenderingContext2D,
renderConfig: InteractiveCanvasRenderConfig,
appState: InteractiveCanvasAppState,
croppingElement: ExcalidrawImageElement,
elementsMap: ElementsMap,
): void => {
const [x1, y1, , , cx, cy] = getElementAbsoluteCoords(
croppingElement,
elementsMap,
);
const LINE_WIDTH = 3;
const LINE_LENGTH = 20;
const ZOOMED_LINE_WIDTH = LINE_WIDTH / appState.zoom.value;
const ZOOMED_HALF_LINE_WIDTH = ZOOMED_LINE_WIDTH / 2;
const HALF_WIDTH = cx - x1 + ZOOMED_LINE_WIDTH;
const HALF_HEIGHT = cy - y1 + ZOOMED_LINE_WIDTH;
const HORIZONTAL_LINE_LENGTH = Math.min(
LINE_LENGTH / appState.zoom.value,
HALF_WIDTH,
);
const VERTICAL_LINE_LENGTH = Math.min(
LINE_LENGTH / appState.zoom.value,
HALF_HEIGHT,
);
context.save();
context.fillStyle = renderConfig.selectionColor;
context.strokeStyle = renderConfig.selectionColor;
context.lineWidth = ZOOMED_LINE_WIDTH;
const handles: Array<
[
[number, number],
[number, number],
[number, number],
[number, number],
[number, number],
]
> = [
[
// x, y
[-HALF_WIDTH, -HALF_HEIGHT],
// horizontal line: first start and to
[0, ZOOMED_HALF_LINE_WIDTH],
[HORIZONTAL_LINE_LENGTH, ZOOMED_HALF_LINE_WIDTH],
// vertical line: second start and to
[ZOOMED_HALF_LINE_WIDTH, 0],
[ZOOMED_HALF_LINE_WIDTH, VERTICAL_LINE_LENGTH],
],
[
[HALF_WIDTH - ZOOMED_HALF_LINE_WIDTH, -HALF_HEIGHT],
[ZOOMED_HALF_LINE_WIDTH, ZOOMED_HALF_LINE_WIDTH],
[
-HORIZONTAL_LINE_LENGTH + ZOOMED_HALF_LINE_WIDTH,
ZOOMED_HALF_LINE_WIDTH,
],
[0, 0],
[0, VERTICAL_LINE_LENGTH],
],
[
[-HALF_WIDTH, HALF_HEIGHT],
[0, -ZOOMED_HALF_LINE_WIDTH],
[HORIZONTAL_LINE_LENGTH, -ZOOMED_HALF_LINE_WIDTH],
[ZOOMED_HALF_LINE_WIDTH, 0],
[ZOOMED_HALF_LINE_WIDTH, -VERTICAL_LINE_LENGTH],
],
[
[HALF_WIDTH - ZOOMED_HALF_LINE_WIDTH, HALF_HEIGHT],
[ZOOMED_HALF_LINE_WIDTH, -ZOOMED_HALF_LINE_WIDTH],
[
-HORIZONTAL_LINE_LENGTH + ZOOMED_HALF_LINE_WIDTH,
-ZOOMED_HALF_LINE_WIDTH,
],
[0, 0],
[0, -VERTICAL_LINE_LENGTH],
],
];
handles.forEach((handle) => {
const [[x, y], [x1s, y1s], [x1t, y1t], [x2s, y2s], [x2t, y2t]] = handle;
context.save();
context.translate(cx, cy);
context.rotate(croppingElement.angle);
context.beginPath();
context.moveTo(x + x1s, y + y1s);
context.lineTo(x + x1t, y + y1t);
context.stroke();
context.beginPath();
context.moveTo(x + x2s, y + y2s);
context.lineTo(x + x2t, y + y2t);
context.stroke();
context.restore();
});
context.restore();
};
const renderTextBox = ( const renderTextBox = (
text: NonDeleted<ExcalidrawTextElement>, text: NonDeleted<ExcalidrawTextElement>,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
@ -671,7 +781,7 @@ const _renderInteractiveScene = ({
} }
// Paint selection element // Paint selection element
if (appState.selectionElement) { if (appState.selectionElement && !appState.isCropping) {
try { try {
renderSelectionElement( renderSelectionElement(
appState.selectionElement, appState.selectionElement,
@ -783,18 +893,7 @@ const _renderInteractiveScene = ({
// Optimisation for finding quickly relevant element ids // Optimisation for finding quickly relevant element ids
const locallySelectedIds = arrayToMap(selectedElements); const locallySelectedIds = arrayToMap(selectedElements);
const selections: { const selections: ElementSelectionBorder[] = [];
angle: number;
elementX1: number;
elementY1: number;
elementX2: number;
elementY2: number;
selectionColors: string[];
dashed?: boolean;
cx: number;
cy: number;
activeEmbeddable: boolean;
}[] = [];
for (const element of elementsMap.values()) { for (const element of elementsMap.values()) {
const selectionColors = []; const selectionColors = [];
@ -833,14 +932,17 @@ const _renderInteractiveScene = ({
} }
if (selectionColors.length) { if (selectionColors.length) {
const [elementX1, elementY1, elementX2, elementY2, cx, cy] = const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
getElementAbsoluteCoords(element, elementsMap, true); element,
elementsMap,
true,
);
selections.push({ selections.push({
angle: element.angle, angle: element.angle,
elementX1, x1,
elementY1, y1,
elementX2, x2,
elementY2, y2,
selectionColors, selectionColors,
dashed: !!remoteClients, dashed: !!remoteClients,
cx, cx,
@ -848,24 +950,28 @@ const _renderInteractiveScene = ({
activeEmbeddable: activeEmbeddable:
appState.activeEmbeddable?.element === element && appState.activeEmbeddable?.element === element &&
appState.activeEmbeddable.state === "active", appState.activeEmbeddable.state === "active",
padding:
element.id === appState.croppingElementId ||
isImageElement(element)
? 0
: undefined,
}); });
} }
} }
const addSelectionForGroupId = (groupId: GroupId) => { const addSelectionForGroupId = (groupId: GroupId) => {
const groupElements = getElementsInGroup(elementsMap, groupId); const groupElements = getElementsInGroup(elementsMap, groupId);
const [elementX1, elementY1, elementX2, elementY2] = const [x1, y1, x2, y2] = getCommonBounds(groupElements);
getCommonBounds(groupElements);
selections.push({ selections.push({
angle: 0, angle: 0,
elementX1, x1,
elementX2, x2,
elementY1, y1,
elementY2, y2,
selectionColors: [oc.black], selectionColors: [oc.black],
dashed: true, dashed: true,
cx: elementX1 + (elementX2 - elementX1) / 2, cx: x1 + (x2 - x1) / 2,
cy: elementY1 + (elementY2 - elementY1) / 2, cy: y1 + (y2 - y1) / 2,
activeEmbeddable: false, activeEmbeddable: false,
}); });
}; };
@ -900,7 +1006,9 @@ const _renderInteractiveScene = ({
!appState.viewModeEnabled && !appState.viewModeEnabled &&
showBoundingBox && showBoundingBox &&
// do not show transform handles when text is being edited // do not show transform handles when text is being edited
!isTextElement(appState.editingTextElement) !isTextElement(appState.editingTextElement) &&
// do not show transform handles when image is being cropped
!appState.croppingElementId
) { ) {
renderTransformHandles( renderTransformHandles(
context, context,
@ -910,6 +1018,20 @@ const _renderInteractiveScene = ({
selectedElements[0].angle, selectedElements[0].angle,
); );
} }
if (appState.croppingElementId && !appState.isCropping) {
const croppingElement = elementsMap.get(appState.croppingElementId);
if (croppingElement && isImageElement(croppingElement)) {
renderCropHandles(
context,
renderConfig,
appState,
croppingElement,
elementsMap,
);
}
}
} else if (selectedElements.length > 1 && !appState.isRotating) { } else if (selectedElements.length > 1 && !appState.isRotating) {
const dashedLinePadding = const dashedLinePadding =
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value; (DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;

@ -17,6 +17,7 @@ import {
isArrowElement, isArrowElement,
hasBoundTextElement, hasBoundTextElement,
isMagicFrameElement, isMagicFrameElement,
isImageElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
import { getElementAbsoluteCoords } from "../element/bounds"; import { getElementAbsoluteCoords } from "../element/bounds";
import type { RoughCanvas } from "roughjs/bin/canvas"; import type { RoughCanvas } from "roughjs/bin/canvas";
@ -61,6 +62,7 @@ import { ShapeCache } from "../scene/ShapeCache";
import { getVerticalOffset } from "../fonts"; import { getVerticalOffset } from "../fonts";
import { isRightAngleRads } from "../../math"; import { isRightAngleRads } from "../../math";
import { getCornerRadius } from "../shapes"; import { getCornerRadius } from "../shapes";
import { getUncroppedImageElement } from "../element/cropElement";
// using a stronger invert (100% vs our regular 93%) and saturate // using a stronger invert (100% vs our regular 93%) and saturate
// as a temp hack to make images in dark theme look closer to original // as a temp hack to make images in dark theme look closer to original
@ -434,8 +436,22 @@ const drawElementOnCanvas = (
); );
context.clip(); context.clip();
} }
const { x, y, width, height } = element.crop
? element.crop
: {
x: 0,
y: 0,
width: img.naturalWidth,
height: img.naturalHeight,
};
context.drawImage( context.drawImage(
img, img,
x,
y,
width,
height,
0 /* hardcoded for the selection box*/, 0 /* hardcoded for the selection box*/,
0, 0,
element.width, element.width,
@ -921,13 +937,52 @@ export const renderElement = (
context.imageSmoothingEnabled = false; context.imageSmoothingEnabled = false;
} }
if (
element.id === appState.croppingElementId &&
isImageElement(elementWithCanvas.element) &&
elementWithCanvas.element.crop !== null
) {
context.save();
context.globalAlpha = 0.1;
const uncroppedElementCanvas = generateElementCanvas(
getUncroppedImageElement(elementWithCanvas.element, elementsMap),
allElementsMap,
appState.zoom,
renderConfig,
appState,
);
if (uncroppedElementCanvas) {
drawElementFromCanvas( drawElementFromCanvas(
elementWithCanvas, uncroppedElementCanvas,
context,
renderConfig,
appState,
allElementsMap,
);
}
context.restore();
}
const _elementWithCanvas = generateElementCanvas(
elementWithCanvas.element,
allElementsMap,
appState.zoom,
renderConfig,
appState,
);
if (_elementWithCanvas) {
drawElementFromCanvas(
_elementWithCanvas,
context, context,
renderConfig, renderConfig,
appState, appState,
allElementsMap, allElementsMap,
); );
}
// reset // reset
context.imageSmoothingEnabled = currentImageSmoothingStatus; context.imageSmoothingEnabled = currentImageSmoothingStatus;

@ -37,6 +37,7 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement"; import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
import { getVerticalOffset } from "../fonts"; import { getVerticalOffset } from "../fonts";
import { getCornerRadius, isPathALoop } from "../shapes"; import { getCornerRadius, isPathALoop } from "../shapes";
import { getUncroppedWidthAndHeight } from "../element/cropElement";
const roughSVGDrawWithPrecision = ( const roughSVGDrawWithPrecision = (
rsvg: RoughSVG, rsvg: RoughSVG,
@ -417,11 +418,27 @@ const renderElementToSvg = (
symbol.id = symbolId; symbol.id = symbolId;
const image = svgRoot.ownerDocument!.createElementNS(SVG_NS, "image"); const image = svgRoot.ownerDocument!.createElementNS(SVG_NS, "image");
image.setAttribute("href", fileData.dataURL);
image.setAttribute("preserveAspectRatio", "none");
if (element.crop) {
const { width: uncroppedWidth, height: uncroppedHeight } =
getUncroppedWidthAndHeight(element);
symbol.setAttribute(
"viewBox",
`${
element.crop.x / (element.crop.naturalWidth / uncroppedWidth)
} ${
element.crop.y / (element.crop.naturalHeight / uncroppedHeight)
} ${width} ${height}`,
);
image.setAttribute("width", `${uncroppedWidth}`);
image.setAttribute("height", `${uncroppedHeight}`);
} else {
image.setAttribute("width", "100%"); image.setAttribute("width", "100%");
image.setAttribute("height", "100%"); image.setAttribute("height", "100%");
image.setAttribute("href", fileData.dataURL); }
image.setAttribute("preserveAspectRatio", "none");
symbol.appendChild(image); symbol.appendChild(image);

@ -21,6 +21,7 @@ export const getObservedAppState = (appState: AppState): ObservedAppState => {
selectedGroupIds: appState.selectedGroupIds, selectedGroupIds: appState.selectedGroupIds,
editingLinearElementId: appState.editingLinearElement?.elementId || null, editingLinearElementId: appState.editingLinearElement?.elementId || null,
selectedLinearElementId: appState.selectedLinearElement?.elementId || null, selectedLinearElementId: appState.selectedLinearElement?.elementId || null,
croppingElementId: appState.croppingElementId,
}; };
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, { Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {

@ -116,6 +116,50 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@ -794,6 +838,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"left": 30, "left": 30,
"top": 40, "top": 40,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -836,6 +881,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1000,6 +1046,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1042,6 +1089,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1216,6 +1264,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1258,6 +1307,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1547,6 +1597,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1589,6 +1640,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1878,6 +1930,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1920,6 +1973,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2094,6 +2148,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2136,6 +2191,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2334,6 +2390,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2376,6 +2433,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2635,6 +2693,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2677,6 +2736,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3004,6 +3064,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3046,6 +3107,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3479,6 +3541,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3521,6 +3584,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3802,6 +3866,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3844,6 +3909,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4125,6 +4191,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4167,6 +4234,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4633,6 +4701,50 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@ -5311,6 +5423,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"left": -17, "left": -17,
"top": -7, "top": -7,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -5353,6 +5466,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -5760,6 +5874,50 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@ -6438,6 +6596,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"left": -17, "left": -17,
"top": -7, "top": -7,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -6480,6 +6639,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7373,6 +7533,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"left": -19, "left": -19,
"top": -9, "top": -9,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -7415,6 +7576,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7607,6 +7769,50 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@ -8285,6 +8491,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"left": -17, "left": -17,
"top": -7, "top": -7,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8327,6 +8534,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8501,6 +8709,50 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
}, },
}, },
"separator", "separator",
{
"PanelComponent": [Function],
"icon": <svg
aria-hidden="true"
className=""
fill="none"
focusable="false"
role="img"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
>
<g
strokeWidth="1.25"
>
<path
d="M0 0h24v24H0z"
fill="none"
stroke="none"
/>
<path
d="M8 5v10a1 1 0 0 0 1 1h10"
/>
<path
d="M5 8h10a1 1 0 0 1 1 1v10"
/>
</g>
</svg>,
"keywords": [
"image",
"crop",
],
"label": "helpDialog.cropStart",
"name": "cropEditor",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "menu",
},
"viewMode": true,
},
"separator",
{ {
"icon": <svg "icon": <svg
aria-hidden="true" aria-hidden="true"
@ -9179,6 +9431,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"left": 80, "left": 80,
"top": 90, "top": 90,
}, },
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9221,6 +9474,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"gridStep": 5, "gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,

File diff suppressed because one or more lines are too long

@ -11,6 +11,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -53,6 +54,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -613,6 +615,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -655,6 +658,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1119,6 +1123,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1161,6 +1166,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1487,6 +1493,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1529,6 +1536,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1856,6 +1864,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1898,6 +1907,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2123,6 +2133,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2165,6 +2176,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2563,6 +2575,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2605,6 +2618,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2862,6 +2876,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2904,6 +2919,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3146,6 +3162,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3188,6 +3205,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3440,6 +3458,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3482,6 +3501,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3726,6 +3746,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3768,6 +3789,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3961,6 +3983,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4003,6 +4026,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4220,6 +4244,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4262,6 +4287,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4493,6 +4519,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4535,6 +4562,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4724,6 +4752,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4766,6 +4795,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4955,6 +4985,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4997,6 +5028,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -5184,6 +5216,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -5226,6 +5259,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -5413,6 +5447,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -5455,6 +5490,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -5672,6 +5708,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -5714,6 +5751,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -6003,6 +6041,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -6045,6 +6084,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -6428,6 +6468,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -6470,6 +6511,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -6806,6 +6848,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -6848,6 +6891,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7125,6 +7169,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -7167,6 +7212,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7423,6 +7469,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -7465,6 +7512,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7652,6 +7700,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -7694,6 +7743,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8007,6 +8057,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8049,6 +8100,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8362,6 +8414,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8404,6 +8457,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8766,6 +8820,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8808,6 +8863,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9053,6 +9109,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9095,6 +9152,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9318,6 +9376,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9360,6 +9419,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9582,6 +9642,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9624,6 +9685,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9813,6 +9875,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9855,6 +9918,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -10114,6 +10178,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -10156,6 +10221,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -10454,6 +10520,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -10496,6 +10563,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -10689,6 +10757,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -10731,6 +10800,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -11142,6 +11212,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -11184,6 +11255,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -11396,6 +11468,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -11438,6 +11511,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -11635,6 +11709,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -11677,6 +11752,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -11876,6 +11952,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -11918,6 +11995,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -12277,6 +12355,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -12319,6 +12398,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -12524,6 +12604,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -12566,6 +12647,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -12765,6 +12847,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -12807,6 +12890,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -13006,6 +13090,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -13048,6 +13133,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -13253,6 +13339,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -13295,6 +13382,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -13585,6 +13673,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -13627,6 +13716,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -13757,6 +13847,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -13799,6 +13890,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -14045,6 +14137,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -14087,6 +14180,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -14312,6 +14406,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -14354,6 +14449,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -14587,6 +14683,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -14629,6 +14726,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -14748,6 +14846,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -14790,6 +14889,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -15444,6 +15544,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -15486,6 +15587,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -16064,6 +16166,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -16106,6 +16209,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -16684,6 +16788,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -16726,6 +16831,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -17396,6 +17502,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -17438,6 +17545,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -18146,6 +18254,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -18188,6 +18297,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -18620,6 +18730,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -18662,6 +18773,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -19142,6 +19254,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -19184,6 +19297,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -19598,6 +19712,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -19640,6 +19755,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"gridStep": 5, "gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,

@ -11,6 +11,7 @@ exports[`given element A and group of elements B and given both are selected whe
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -53,6 +54,7 @@ exports[`given element A and group of elements B and given both are selected whe
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -423,6 +425,7 @@ exports[`given element A and group of elements B and given both are selected whe
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -465,6 +468,7 @@ exports[`given element A and group of elements B and given both are selected whe
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -826,6 +830,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -868,6 +873,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": false, "isBindingEnabled": false,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1368,6 +1374,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1410,6 +1417,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1569,6 +1577,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1611,6 +1620,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -1941,6 +1951,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -1983,6 +1994,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2178,6 +2190,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2220,6 +2233,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2355,6 +2369,7 @@ exports[`regression tests > can drag element that covers another element, while
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2397,6 +2412,7 @@ exports[`regression tests > can drag element that covers another element, while
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2672,6 +2688,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2714,6 +2731,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -2915,6 +2933,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -2957,6 +2976,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3155,6 +3175,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3197,6 +3218,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3382,6 +3404,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3424,6 +3447,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3635,6 +3659,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3677,6 +3702,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -3943,6 +3969,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -3985,6 +4012,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4354,6 +4382,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4396,6 +4425,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4634,6 +4664,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4676,6 +4707,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -4884,6 +4916,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -4926,6 +4959,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -5091,6 +5125,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -5133,6 +5168,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -5287,6 +5323,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -5329,6 +5366,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -5666,6 +5704,7 @@ exports[`regression tests > drags selected elements from point inside common bou
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -5708,6 +5747,7 @@ exports[`regression tests > drags selected elements from point inside common bou
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -5953,6 +5993,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -5995,6 +6036,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -6758,6 +6800,7 @@ exports[`regression tests > given a group of selected elements with an element t
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -6800,6 +6843,7 @@ exports[`regression tests > given a group of selected elements with an element t
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7085,6 +7129,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -7127,6 +7172,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7358,6 +7404,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -7400,6 +7447,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7589,6 +7637,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -7631,6 +7680,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -7823,6 +7873,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -7865,6 +7916,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8000,6 +8052,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8042,6 +8095,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8177,6 +8231,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8219,6 +8274,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8354,6 +8410,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8396,6 +8453,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8574,6 +8632,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8616,6 +8675,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8793,6 +8853,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -8835,6 +8896,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -8984,6 +9046,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9026,6 +9089,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9204,6 +9268,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9246,6 +9311,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9381,6 +9447,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9423,6 +9490,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9600,6 +9668,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9642,6 +9711,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9777,6 +9847,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -9819,6 +9890,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -9968,6 +10040,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -10010,6 +10083,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -10145,6 +10219,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -10187,6 +10262,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -10656,6 +10732,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -10698,6 +10775,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -10930,6 +11008,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -10972,6 +11051,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -11053,6 +11133,7 @@ exports[`regression tests > shift click on selected element should deselect it o
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -11095,6 +11176,7 @@ exports[`regression tests > shift click on selected element should deselect it o
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -11249,6 +11331,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -11291,6 +11374,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -11557,6 +11641,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -11599,6 +11684,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -11966,6 +12052,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -12008,6 +12095,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -12576,6 +12664,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -12618,6 +12707,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -12702,6 +12792,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -12744,6 +12835,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -13283,6 +13375,7 @@ exports[`regression tests > switches from group of selected elements to another
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -13325,6 +13418,7 @@ exports[`regression tests > switches from group of selected elements to another
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -13618,6 +13712,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -13660,6 +13755,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -13880,6 +13976,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -13922,6 +14019,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -14003,6 +14101,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -14045,6 +14144,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -14379,6 +14479,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -14421,6 +14522,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,
@ -14502,6 +14604,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -14544,6 +14647,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"gridStep": 5, "gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,

@ -0,0 +1,342 @@
import React from "react";
import ReactDOM from "react-dom";
import { vi } from "vitest";
import { Keyboard, Pointer, UI } from "./helpers/ui";
import type { ExcalidrawImageElement, ImageCrop } from "../element/types";
import { act, GlobalTestState, render } from "./test-utils";
import { Excalidraw, exportToCanvas, exportToSvg } from "..";
import { API } from "./helpers/api";
import type { NormalizedZoomValue } from "../types";
import { KEYS } from "../keys";
import { duplicateElement } from "../element";
import { cloneJSON } from "../utils";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
const { h } = window;
const mouse = new Pointer("mouse");
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
mouse.reset();
localStorage.clear();
sessionStorage.clear();
vi.clearAllMocks();
Object.assign(document, {
elementFromPoint: () => GlobalTestState.canvas,
});
await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />);
API.setAppState({
zoom: {
value: 1 as NormalizedZoomValue,
},
});
const image = API.createElement({ type: "image", width: 200, height: 100 });
API.setElements([image]);
API.setAppState({
selectedElementIds: {
[image.id]: true,
},
});
});
const generateRandomNaturalWidthAndHeight = (image: ExcalidrawImageElement) => {
const initialWidth = image.width;
const initialHeight = image.height;
const scale = 1 + Math.random() * 5;
return {
naturalWidth: initialWidth * scale,
naturalHeight: initialHeight * scale,
};
};
const compareCrops = (cropA: ImageCrop, cropB: ImageCrop) => {
(Object.keys(cropA) as [keyof ImageCrop]).forEach((key) => {
const propA = cropA[key];
const propB = cropB[key];
expect(propA as number).toBeCloseTo(propB as number);
});
};
describe("Enter and leave the crop editor", () => {
it("enter the editor by double clicking", () => {
const image = h.elements[0];
expect(h.state.croppingElementId).toBe(null);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
expect(h.state.croppingElementId).toBe(image.id);
});
it("enter the editor by pressing enter", () => {
const image = h.elements[0];
expect(h.state.croppingElementId).toBe(null);
Keyboard.keyDown(KEYS.ENTER);
expect(h.state.croppingElementId).not.toBe(null);
expect(h.state.croppingElementId).toBe(image.id);
});
it("leave the editor by clicking outside", () => {
const image = h.elements[0];
Keyboard.keyDown(KEYS.ENTER);
expect(h.state.croppingElementId).not.toBe(null);
mouse.click(image.x - 20, image.y - 20);
expect(h.state.croppingElementId).toBe(null);
});
it("leave the editor by pressing escape", () => {
const image = h.elements[0];
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
Keyboard.keyDown(KEYS.ESCAPE);
expect(h.state.croppingElementId).toBe(null);
});
});
describe("Crop an image", () => {
it("Cropping changes the dimension", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth / 2, 0]);
expect(image.width).toBeLessThan(initialWidth);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, initialHeight / 2]);
expect(image.height).toBeLessThan(initialHeight);
});
it("Cropping has minimal sizes", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth, 0]);
expect(image.width).toBeLessThan(initialWidth);
expect(image.width).toBeGreaterThan(0);
UI.crop(image, "w", naturalWidth, naturalHeight, [-initialWidth, 0]);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, initialHeight]);
expect(image.height).toBeLessThan(initialHeight);
expect(image.height).toBeGreaterThan(0);
});
it("Preserve aspect ratio", async () => {
let image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth / 3, 0]);
let resizedWidth = image.width;
let resizedHeight = image.height;
// max height, cropping should not change anything
UI.crop(
image,
"w",
naturalWidth,
naturalHeight,
[-initialWidth / 3, 0],
true,
);
expect(image.width).toBe(resizedWidth);
expect(image.height).toBe(resizedHeight);
// re-crop to initial state
UI.crop(image, "w", naturalWidth, naturalHeight, [-initialWidth / 3, 0]);
// change crop height and width
UI.crop(image, "s", naturalWidth, naturalHeight, [0, -initialHeight / 2]);
UI.crop(image, "e", naturalWidth, naturalHeight, [-initialWidth / 3, 0]);
resizedWidth = image.width;
resizedHeight = image.height;
// test corner handle aspect ratio preserving
UI.crop(image, "se", naturalWidth, naturalHeight, [initialWidth, 0], true);
expect(image.width / image.height).toBe(resizedWidth / resizedHeight);
expect(image.width).toBeLessThanOrEqual(initialWidth);
expect(image.height).toBeLessThanOrEqual(initialHeight);
// reset
image = API.createElement({ type: "image", width: 200, height: 100 });
API.setElements([image]);
API.setAppState({
selectedElementIds: {
[image.id]: true,
},
});
// 50 x 50 square
UI.crop(image, "nw", naturalWidth, naturalHeight, [150, 50]);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, -100], true);
expect(image.width).toEqual(image.height);
// image is at the corner, not space to its right to expand, should not be able to resize
expect(image.height).toBeCloseTo(50);
UI.crop(image, "nw", naturalWidth, naturalHeight, [-150, -100], true);
expect(image.width).toEqual(image.height);
// max height should be reached
expect(image.height).toEqual(initialHeight);
expect(image.width).toBe(initialHeight);
});
});
describe("Cropping and other features", async () => {
it("Cropping works independently of duplication", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const duplicatedImage = duplicateElement(null, new Map(), image, {});
act(() => {
h.app.scene.insertElement(duplicatedImage);
});
expect(duplicatedImage.width).toBe(image.width);
expect(duplicatedImage.height).toBe(image.height);
UI.crop(duplicatedImage, "nw", naturalWidth, naturalHeight, [
-initialWidth / 2,
-initialHeight / 2,
]);
expect(duplicatedImage.width).toBe(initialWidth);
expect(duplicatedImage.height).toBe(initialHeight);
const resizedWidth = image.width;
const resizedHeight = image.height;
expect(image.width).not.toBe(duplicatedImage.width);
expect(image.height).not.toBe(duplicatedImage.height);
UI.crop(duplicatedImage, "se", naturalWidth, naturalHeight, [
-initialWidth / 1.5,
-initialHeight / 1.5,
]);
expect(duplicatedImage.width).not.toBe(initialWidth);
expect(image.width).toBe(resizedWidth);
expect(duplicatedImage.height).not.toBe(initialHeight);
expect(image.height).toBe(resizedHeight);
});
it("Resizing should not affect crop", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
const cropBeforeResizing = image.crop;
const cropBeforeResizingCloned = cloneJSON(image.crop) as ImageCrop;
expect(cropBeforeResizing).not.toBe(null);
UI.crop(image, "e", naturalWidth, naturalHeight, [200, 0]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
UI.resize(image, "s", [0, -100]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
UI.resize(image, "ne", [-50, -50]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
});
it("Flipping does not change crop", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const cropBeforeResizing = image.crop;
const cropBeforeResizingCloned = cloneJSON(image.crop) as ImageCrop;
API.executeAction(actionFlipHorizontal);
expect(image.crop).toBe(cropBeforeResizing);
compareCrops(cropBeforeResizingCloned, image.crop!);
API.executeAction(actionFlipVertical);
expect(image.crop).toBe(cropBeforeResizing);
compareCrops(cropBeforeResizingCloned, image.crop!);
});
it("Exports should preserve crops", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 4,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const widthToHeightRatio = image.width / image.height;
const canvas = await exportToCanvas({
elements: [image],
appState: h.state,
files: h.app.files,
exportPadding: 0,
});
const exportedCanvasRatio = canvas.width / canvas.height;
expect(widthToHeightRatio).toBeCloseTo(exportedCanvasRatio);
const svg = await exportToSvg({
elements: [image],
appState: h.state,
files: h.app.files,
exportPadding: 0,
});
const svgWidth = svg.getAttribute("width");
const svgHeight = svg.getAttribute("height");
expect(svgWidth).toBeDefined();
expect(svgHeight).toBeDefined();
const exportedSvgRatio = Number(svgWidth) / Number(svgHeight);
expect(widthToHeightRatio).toBeCloseTo(exportedSvgRatio);
});
});

@ -1,4 +1,3 @@
import type { ToolType } from "../../types";
import type { import type {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
@ -9,6 +8,7 @@ import type {
ExcalidrawDiamondElement, ExcalidrawDiamondElement,
ExcalidrawTextContainer, ExcalidrawTextContainer,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
} from "../../element/types"; } from "../../element/types";
import type { TransformHandleType } from "../../element/transformHandles"; import type { TransformHandleType } from "../../element/transformHandles";
import { import {
@ -35,6 +35,8 @@ import { arrayToMap } from "../../utils";
import { createTestHook } from "../../components/App"; import { createTestHook } from "../../components/App";
import type { GlobalPoint, LocalPoint, Radians } from "../../../math"; import type { GlobalPoint, LocalPoint, Radians } from "../../../math";
import { pointFrom, pointRotateRads } from "../../../math"; import { pointFrom, pointRotateRads } from "../../../math";
import { cropElement } from "../../element/cropElement";
import type { ToolType } from "../../types";
// so that window.h is available when App.tsx is not imported as well. // so that window.h is available when App.tsx is not imported as well.
createTestHook(); createTestHook();
@ -561,6 +563,38 @@ export class UI {
return transform(element, handle, mouseMove, keyboardModifiers); return transform(element, handle, mouseMove, keyboardModifiers);
} }
static crop(
element: ExcalidrawImageElement,
handle: TransformHandleDirection,
naturalWidth: number,
naturalHeight: number,
mouseMove: [deltaX: number, deltaY: number],
keepAspectRatio = false,
) {
const handleCoords = getTransformHandles(
element,
h.state.zoom,
arrayToMap(h.elements),
"mouse",
{},
)[handle]!;
const clientX = handleCoords[0] + handleCoords[2] / 2;
const clientY = handleCoords[1] + handleCoords[3] / 2;
const mutations = cropElement(
element,
handle,
naturalWidth,
naturalHeight,
clientX + mouseMove[0],
clientY + mouseMove[1],
keepAspectRatio ? element.width / element.height : undefined,
);
API.updateElement(element, mutations);
}
static rotate( static rotate(
element: ExcalidrawElement | ExcalidrawElement[], element: ExcalidrawElement | ExcalidrawElement[],
mouseMove: [deltaX: number, deltaY: number], mouseMove: [deltaX: number, deltaY: number],

@ -176,6 +176,8 @@ export type StaticCanvasAppState = Readonly<
gridStep: AppState["gridStep"]; gridStep: AppState["gridStep"];
frameRendering: AppState["frameRendering"]; frameRendering: AppState["frameRendering"];
currentHoveredFontFamily: AppState["currentHoveredFontFamily"]; currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
// Cropping
croppingElementId: AppState["croppingElementId"];
} }
>; >;
@ -198,6 +200,9 @@ export type InteractiveCanvasAppState = Readonly<
snapLines: AppState["snapLines"]; snapLines: AppState["snapLines"];
zenModeEnabled: AppState["zenModeEnabled"]; zenModeEnabled: AppState["zenModeEnabled"];
editingTextElement: AppState["editingTextElement"]; editingTextElement: AppState["editingTextElement"];
// Cropping
isCropping: AppState["isCropping"];
croppingElementId: AppState["croppingElementId"];
// Search matches // Search matches
searchMatches: AppState["searchMatches"]; searchMatches: AppState["searchMatches"];
} }
@ -219,6 +224,7 @@ export type ObservedElementsAppState = {
editingLinearElementId: LinearElementEditor["elementId"] | null; editingLinearElementId: LinearElementEditor["elementId"] | null;
// Right now it's coupled to `editingLinearElement`, ideally it should not be really needed as we already have selectedElementIds & editingLinearElementId // Right now it's coupled to `editingLinearElement`, ideally it should not be really needed as we already have selectedElementIds & editingLinearElementId
selectedLinearElementId: LinearElementEditor["elementId"] | null; selectedLinearElementId: LinearElementEditor["elementId"] | null;
croppingElementId: AppState["croppingElementId"];
}; };
export interface AppState { export interface AppState {
@ -386,6 +392,11 @@ export interface AppState {
userToFollow: UserToFollow | null; userToFollow: UserToFollow | null;
/** the socket ids of the users following the current user */ /** the socket ids of the users following the current user */
followedBy: Set<SocketId>; followedBy: Set<SocketId>;
/** image cropping */
isCropping: boolean;
croppingElementId: ExcalidrawElement["id"] | null;
searchMatches: readonly SearchMatch[]; searchMatches: readonly SearchMatch[];
} }

@ -28,3 +28,6 @@ export const average = (a: number, b: number) => (a + b) / 2;
export const isFiniteNumber = (value: any): value is number => { export const isFiniteNumber = (value: any): value is number => {
return typeof value === "number" && Number.isFinite(value); return typeof value === "number" && Number.isFinite(value);
}; };
export const isCloseTo = (a: number, b: number, precision = PRECISION) =>
Math.abs(a - b) < precision;

@ -139,3 +139,10 @@ export const vectorNormalize = (v: Vector): Vector => {
return vector(v[0] / m, v[1] / m); return vector(v[0] / m, v[1] / m);
}; };
/**
* Project the first vector onto the second vector
*/
export const vectorProjection = (a: Vector, b: Vector) => {
return vectorScale(b, vectorDot(a, b) / vectorDot(b, b));
};

@ -11,6 +11,7 @@ exports[`exportToSvg > with default arguments 1`] = `
}, },
"collaborators": Map {}, "collaborators": Map {},
"contextMenu": null, "contextMenu": null,
"croppingElementId": null,
"currentChartType": "bar", "currentChartType": "bar",
"currentHoveredFontFamily": null, "currentHoveredFontFamily": null,
"currentItemArrowType": "round", "currentItemArrowType": "round",
@ -53,6 +54,7 @@ exports[`exportToSvg > with default arguments 1`] = `
"gridSize": 20, "gridSize": 20,
"gridStep": 5, "gridStep": 5,
"isBindingEnabled": true, "isBindingEnabled": true,
"isCropping": false,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,
"isRotating": false, "isRotating": false,

@ -105,7 +105,7 @@ console.error = (...args) => {
// the react's act() warning usually doesn't contain any useful stack trace // the react's act() warning usually doesn't contain any useful stack trace
// so we're catching the log and re-logging the message with the test name, // so we're catching the log and re-logging the message with the test name,
// also stripping the actual component stack trace as it's not useful // also stripping the actual component stack trace as it's not useful
if (args[0]?.includes("act(")) { if (args[0]?.includes?.("act(")) {
_consoleError( _consoleError(
yellow( yellow(
`<<< WARNING: test "${ `<<< WARNING: test "${

Loading…
Cancel
Save