grid support (1st iteration) (#1788)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Daishi Kato 2020-06-24 00:24:52 +09:00 committed by GitHub
parent b6bf011d0d
commit baa8fb6c14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 343 additions and 80 deletions

View File

@ -53,6 +53,7 @@ export const getDefaultAppState = (): AppState => {
shouldCacheIgnoreZoom: false,
showShortcutsDialog: false,
zenModeEnabled: false,
gridSize: null,
editingGroupId: null,
selectedGroupIds: {},
};
@ -81,5 +82,6 @@ export const clearAppStateForLocalStorage = (appState: AppState) => {
export const cleanAppStateForExport = (appState: AppState) => {
return {
viewBackgroundColor: appState.viewBackgroundColor,
gridSize: appState.gridSize,
};
};

View File

@ -27,6 +27,9 @@ import {
getResizeArrowDirection,
getResizeHandlerFromCoords,
isNonDeletedElement,
dragSelectedElements,
getDragOffsetXY,
dragNewElement,
} from "../element";
import {
getElementsWithinSelection,
@ -54,7 +57,7 @@ import { renderScene } from "../renderer";
import { AppState, GestureEvent, Gesture } from "../types";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { distance2d, isPathALoop } from "../math";
import { distance2d, isPathALoop, getGridPoint } from "../math";
import {
isWritableElement,
@ -72,6 +75,7 @@ import {
isArrowKey,
getResizeCenterPointKey,
getResizeWithSidesSameLengthKey,
getRotateWithDiscreteAngleKey,
} from "../keys";
import { findShapeByKey, shapesShortcutKeys } from "../shapes";
@ -109,6 +113,7 @@ import {
EVENT,
ENV,
CANVAS_ONLY_ACTIONS,
GRID_SIZE,
} from "../constants";
import {
INITAL_SCENE_UPDATE_TIMEOUT,
@ -834,6 +839,12 @@ class App extends React.Component<any, AppState> {
});
};
toggleGridMode = () => {
this.setState({
gridSize: this.state.gridSize ? null : GRID_SIZE,
});
};
private destroySocketClient = () => {
this.setState({
isCollaborating: false,
@ -1173,6 +1184,10 @@ class App extends React.Component<any, AppState> {
this.toggleZenMode();
}
if (event[KEYS.CTRL_OR_CMD] && event.keyCode === KEYS.GRID_KEY_CODE) {
this.toggleGridMode();
}
if (event.code === "KeyC" && event.altKey && event.shiftKey) {
this.copyToClipboardAsPng();
event.preventDefault();
@ -1186,9 +1201,12 @@ class App extends React.Component<any, AppState> {
const shape = findShapeByKey(event.key);
if (isArrowKey(event.key)) {
const step = event.shiftKey
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT;
const step =
(this.state.gridSize &&
(event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.state.gridSize)) ||
(event.shiftKey
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT);
globalSceneState.replaceAllElements(
globalSceneState.getElementsIncludingDeleted().map((el) => {
if (this.state.selectedElementIds[el.id]) {
@ -2013,6 +2031,11 @@ class App extends React.Component<any, AppState> {
const originX = x;
const originY = y;
const [originGridX, originGridY] = getGridPoint(
originX,
originY,
this.state.gridSize,
);
type ResizeTestType = ReturnType<typeof resizeTest>;
let resizeHandle: ResizeTestType = false;
@ -2023,6 +2046,7 @@ class App extends React.Component<any, AppState> {
let resizeArrowDirection: "origin" | "end" = "origin";
let isResizingElements = false;
let draggingOccurred = false;
let dragOffsetXY: [number, number] = [0, 0];
let hitElement: ExcalidrawElement | null = null;
let hitElementWasAddedToSelection = false;
@ -2106,6 +2130,20 @@ class App extends React.Component<any, AppState> {
hitElement ||
getElementAtPosition(elements, this.state, x, y, this.state.zoom);
if (hitElement && isNonDeletedElement(hitElement)) {
if (this.state.selectedElementIds[hitElement.id]) {
dragOffsetXY = getDragOffsetXY(selectedElements, x, y);
} else if (event.shiftKey) {
dragOffsetXY = getDragOffsetXY(
[...selectedElements, hitElement],
x,
y,
);
} else {
dragOffsetXY = getDragOffsetXY([hitElement], x, y);
}
}
// clear selection if shift is not clicked
if (
!(hitElement && this.state.selectedElementIds[hitElement.id]) &&
@ -2260,10 +2298,15 @@ class App extends React.Component<any, AppState> {
});
document.documentElement.style.cursor = CURSOR_TYPE.POINTER;
} else {
const [gridX, gridY] = getGridPoint(
x,
y,
this.state.elementType === "draw" ? null : this.state.gridSize,
);
const element = newLinearElement({
type: this.state.elementType,
x: x,
y: y,
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
@ -2291,10 +2334,11 @@ class App extends React.Component<any, AppState> {
});
}
} else {
const [gridX, gridY] = getGridPoint(x, y, this.state.gridSize);
const element = newElement({
type: this.state.elementType,
x: x,
y: y,
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
@ -2356,6 +2400,7 @@ class App extends React.Component<any, AppState> {
this.canvas,
window.devicePixelRatio,
);
const [gridX, gridY] = getGridPoint(x, y, this.state.gridSize);
// for arrows/lines, don't start dragging until a given threshold
// to ensure we don't create a 2-point arrow by mistake when
@ -2380,15 +2425,22 @@ class App extends React.Component<any, AppState> {
isResizing: resizeHandle && resizeHandle !== "rotation",
isRotating: resizeHandle === "rotation",
});
const [resizeX, resizeY] = getGridPoint(
x - resizeOffsetXY[0],
y - resizeOffsetXY[1],
this.state.gridSize,
);
if (
resizeElements(
resizeHandle,
setResizeHandle,
selectedElements,
resizeArrowDirection,
event,
x - resizeOffsetXY[0],
y - resizeOffsetXY[1],
getRotateWithDiscreteAngleKey(event),
getResizeWithSidesSameLengthKey(event),
getResizeCenterPointKey(event),
resizeX,
resizeY,
)
) {
return;
@ -2421,21 +2473,12 @@ class App extends React.Component<any, AppState> {
this.state,
);
if (selectedElements.length > 0) {
const { x, y } = viewportCoordsToSceneCoords(
event,
this.state,
this.canvas,
window.devicePixelRatio,
const [dragX, dragY] = getGridPoint(
x - dragOffsetXY[0],
y - dragOffsetXY[1],
this.state.gridSize,
);
selectedElements.forEach((element) => {
mutateElement(element, {
x: element.x + x - lastX,
y: element.y + y - lastY,
});
});
lastX = x;
lastY = y;
dragSelectedElements(selectedElements, dragX, dragY);
// We duplicate the selected element if alt is pressed on pointer move
if (event.altKey && !selectedElementWasDuplicated) {
@ -2460,9 +2503,14 @@ class App extends React.Component<any, AppState> {
groupIdMap,
element,
);
const [originDragX, originDragY] = getGridPoint(
originX - dragOffsetXY[0],
originY - dragOffsetXY[1],
this.state.gridSize,
);
mutateElement(duplicatedElement, {
x: duplicatedElement.x + (originX - lastX),
y: duplicatedElement.y + (originY - lastY),
x: duplicatedElement.x + (originDragX - dragX),
y: duplicatedElement.y + (originDragY - dragY),
});
nextElements.push(duplicatedElement);
elementsToAppend.push(element);
@ -2486,16 +2534,20 @@ class App extends React.Component<any, AppState> {
return;
}
let width = distance(originX, x);
let height = distance(originY, y);
if (isLinearElement(draggingElement)) {
draggingOccurred = true;
const points = draggingElement.points;
let dx = x - draggingElement.x;
let dy = y - draggingElement.y;
let dx: number;
let dy: number;
if (draggingElement.type === "draw") {
dx = x - draggingElement.x;
dy = y - draggingElement.y;
} else {
dx = gridX - draggingElement.x;
dy = gridY - draggingElement.y;
}
if (event.shiftKey && points.length === 2) {
if (getRotateWithDiscreteAngleKey(event) && points.length === 2) {
({ width: dx, height: dy } = getPerfectElementSize(
this.state.elementType,
dx,
@ -2516,35 +2568,32 @@ class App extends React.Component<any, AppState> {
});
}
}
} else if (draggingElement.type === "selection") {
dragNewElement(
draggingElement,
this.state.elementType,
originX,
originY,
x,
y,
distance(originX, x),
distance(originY, y),
getResizeWithSidesSameLengthKey(event),
getResizeCenterPointKey(event),
);
} else {
if (getResizeWithSidesSameLengthKey(event)) {
({ width, height } = getPerfectElementSize(
this.state.elementType,
width,
y < originY ? -height : height,
));
if (height < 0) {
height = -height;
}
}
let newX = x < originX ? originX - width : originX;
let newY = y < originY ? originY - height : originY;
if (getResizeCenterPointKey(event)) {
width += width;
height += height;
newX = originX - width / 2;
newY = originY - height / 2;
}
mutateElement(draggingElement, {
x: newX,
y: newY,
width: width,
height: height,
});
dragNewElement(
draggingElement,
this.state.elementType,
originGridX,
originGridY,
gridX,
gridY,
distance(originGridX, gridX),
distance(originGridY, gridY),
getResizeWithSidesSameLengthKey(event),
getResizeCenterPointKey(event),
);
}
if (this.state.elementType === "selection") {
@ -2857,6 +2906,10 @@ class App extends React.Component<any, AppState> {
...this.actionManager.getContextMenuItems((action) =>
CANVAS_ONLY_ACTIONS.includes(action.name),
),
{
label: t("labels.toggleGridMode"),
action: this.toggleGridMode,
},
],
top: event.clientY,
left: event.clientX,

View File

@ -247,6 +247,10 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("buttons.toggleZenMode")}
shortcuts={[getShortcutKey("Alt+Z")]}
/>
<Shortcut
label={t("buttons.toggleGridMode")}
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
/>
</ShortcutIsland>
</Column>
<Column>

View File

@ -68,3 +68,5 @@ export const FONT_FAMILY = {
} as const;
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
export const GRID_SIZE = 20; // TODO make it configurable?

View File

@ -0,0 +1,72 @@
import { NonDeletedExcalidrawElement } from "./types";
import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement";
import { SHAPES } from "../shapes";
import { getPerfectElementSize } from "./sizeHelpers";
export const dragSelectedElements = (
selectedElements: NonDeletedExcalidrawElement[],
pointerX: number,
pointerY: number,
) => {
const [x1, y1] = getCommonBounds(selectedElements);
selectedElements.forEach((element) => {
mutateElement(element, {
x: pointerX + element.x - x1,
y: pointerY + element.y - y1,
});
});
};
export const getDragOffsetXY = (
selectedElements: NonDeletedExcalidrawElement[],
x: number,
y: number,
): [number, number] => {
const [x1, y1] = getCommonBounds(selectedElements);
return [x - x1, y - y1];
};
export const dragNewElement = (
draggingElement: NonDeletedExcalidrawElement,
elementType: typeof SHAPES[number]["value"],
originX: number,
originY: number,
x: number,
y: number,
width: number,
height: number,
isResizeWithSidesSameLength: boolean,
isResizeCenterPoint: boolean,
) => {
if (isResizeWithSidesSameLength) {
({ width, height } = getPerfectElementSize(
elementType,
width,
y < originY ? -height : height,
));
if (height < 0) {
height = -height;
}
}
let newX = x < originX ? originX - width : originX;
let newY = y < originY ? originY - height : originY;
if (isResizeCenterPoint) {
width += width;
height += height;
newX = originX - width / 2;
newY = originY - height / 2;
}
if (width !== 0 && height !== 0) {
mutateElement(draggingElement, {
x: newX,
y: newY,
width: width,
height: height,
});
}
};

View File

@ -38,6 +38,11 @@ export {
getResizeOffsetXY,
getResizeArrowDirection,
} from "./resizeElements";
export {
dragSelectedElements,
getDragOffsetXY,
dragNewElement,
} from "./dragElements";
export { isTextElement, isExcalidrawElement } from "./typeChecks";
export { textWysiwyg } from "./textWysiwyg";
export { redrawTextBoundingBox } from "./textElement";

View File

@ -21,10 +21,6 @@ import {
getCursorForResizingElement,
normalizeResizeHandle,
} from "./resizeTest";
import {
getResizeCenterPointKey,
getResizeWithSidesSameLengthKey,
} from "../keys";
import { measureText, getFontString } from "../utils";
type ResizeTestType = ReturnType<typeof resizeTest>;
@ -34,14 +30,21 @@ export const resizeElements = (
setResizeHandle: (nextResizeHandle: ResizeTestType) => void,
selectedElements: NonDeletedExcalidrawElement[],
resizeArrowDirection: "origin" | "end",
event: PointerEvent, // XXX we want to make it independent?
isRotateWithDiscreteAngle: boolean,
isResizeWithSidesSameLength: boolean,
isResizeCenterPoint: boolean,
pointerX: number,
pointerY: number,
) => {
if (selectedElements.length === 1) {
const [element] = selectedElements;
if (resizeHandle === "rotation") {
rotateSingleElement(element, pointerX, pointerY, event.shiftKey);
rotateSingleElement(
element,
pointerX,
pointerY,
isRotateWithDiscreteAngle,
);
} else if (
isLinearElement(element) &&
element.points.length === 2 &&
@ -53,7 +56,7 @@ export const resizeElements = (
resizeSingleTwoPointElement(
element,
resizeArrowDirection,
event.shiftKey,
isRotateWithDiscreteAngle,
pointerX,
pointerY,
);
@ -67,7 +70,7 @@ export const resizeElements = (
resizeSingleTextElement(
element,
resizeHandle,
getResizeCenterPointKey(event),
isResizeCenterPoint,
pointerX,
pointerY,
);
@ -75,8 +78,8 @@ export const resizeElements = (
resizeSingleElement(
element,
resizeHandle,
getResizeWithSidesSameLengthKey(event),
getResizeCenterPointKey(event),
isResizeWithSidesSameLength,
isResizeCenterPoint,
pointerX,
pointerY,
);
@ -114,13 +117,13 @@ const rotateSingleElement = (
element: NonDeletedExcalidrawElement,
pointerX: number,
pointerY: number,
isAngleLocking: boolean,
isRotateWithDiscreteAngle: boolean,
) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
let angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
if (isAngleLocking) {
if (isRotateWithDiscreteAngle) {
angle += SHIFT_LOCKING_ANGLE / 2;
angle -= angle % SHIFT_LOCKING_ANGLE;
}
@ -133,14 +136,14 @@ const rotateSingleElement = (
const resizeSingleTwoPointElement = (
element: NonDeleted<ExcalidrawLinearElement>,
resizeArrowDirection: "origin" | "end",
isAngleLocking: boolean,
isRotateWithDiscreteAngle: boolean,
pointerX: number,
pointerY: number,
) => {
const pointOrigin = element.points[0]; // can assume always [0, 0]?
const pointEnd = element.points[1];
if (resizeArrowDirection === "end") {
if (isAngleLocking) {
if (isRotateWithDiscreteAngle) {
const { width, height } = getPerfectElementSize(
element.type,
pointerX - element.x,
@ -162,7 +165,7 @@ const resizeSingleTwoPointElement = (
}
} else {
// resizeArrowDirection === "origin"
if (isAngleLocking) {
if (isRotateWithDiscreteAngle) {
const { width, height } = getPerfectElementSize(
element.type,
element.x + pointEnd[0] - pointOrigin[0] - pointerX,
@ -232,6 +235,16 @@ const measureFontSizeFromWH = (
if (metrics.width - nextWidth < 1 && metrics.height - nextHeight < 1) {
return { size: nextFontSize, baseline: metrics.baseline };
}
// third measurement
scale *= 0.99; // just heuristics
nextFontSize = element.fontSize * scale;
metrics = measureText(
element.text,
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
);
if (metrics.width - nextWidth < 1 && metrics.height - nextHeight < 1) {
return { size: nextFontSize, baseline: metrics.baseline };
}
return null;
};

View File

@ -16,6 +16,7 @@ export const KEYS = {
F_KEY_CODE: 70,
ALT_KEY_CODE: 18,
Z_KEY_CODE: 90,
GRID_KEY_CODE: 222,
G_KEY_CODE: 71,
} as const;
@ -32,3 +33,6 @@ export const getResizeCenterPointKey = (event: MouseEvent | KeyboardEvent) =>
export const getResizeWithSidesSameLengthKey = (event: MouseEvent) =>
event.shiftKey;
export const getRotateWithDiscreteAngleKey = (event: MouseEvent) =>
event.shiftKey;

View File

@ -63,7 +63,8 @@
"madeWithExcalidraw": "Made with Excalidraw",
"group": "Group selection",
"ungroup": "Ungroup selection",
"collaborators": "Collaborators"
"collaborators": "Collaborators",
"toggleGridMode": "Toggle grid mode"
},
"buttons": {
"clearReset": "Reset the canvas",
@ -91,7 +92,8 @@
"createNewRoom": "Create new room",
"toggleFullScreen": "Toggle full screen",
"toggleZenMode": "Toggle zen mode",
"exitZenMode": "Exit zen mode"
"exitZenMode": "Exit zen mode",
"toggleGridMode": "Toggle grid mode"
},
"alerts": {
"clearReset": "This will clear the whole canvas. Are you sure?",

View File

@ -340,3 +340,17 @@ const doIntersect = (p1: Point, q1: Point, p2: Point, q2: Point) => {
return false;
};
export const getGridPoint = (
x: number,
y: number,
gridSize: number | null,
): [number, number] => {
if (gridSize) {
return [
Math.round(x / gridSize) * gridSize,
Math.round(y / gridSize) * gridSize,
];
}
return [x, y];
};

View File

@ -74,6 +74,29 @@ const strokeCircle = (
context.stroke();
};
const renderGrid = (
context: CanvasRenderingContext2D,
gridSize: number,
offsetX: number,
offsetY: number,
width: number,
height: number,
) => {
const origStrokeStyle = context.strokeStyle;
context.strokeStyle = "rgba(0,0,0,0.1)";
context.beginPath();
for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) {
context.moveTo(x, offsetY - gridSize);
context.lineTo(x, offsetY + height + gridSize * 2);
}
for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) {
context.moveTo(offsetX - gridSize, y);
context.lineTo(offsetX + width + gridSize * 2, y);
}
context.stroke();
context.strokeStyle = origStrokeStyle;
};
const renderLinearPointHandles = (
context: CanvasRenderingContext2D,
appState: AppState,
@ -167,6 +190,22 @@ export const renderScene = (
context.translate(zoomTranslationX, zoomTranslationY);
context.scale(sceneState.zoom, sceneState.zoom);
// Grid
if (appState.gridSize) {
renderGrid(
context,
appState.gridSize,
-Math.ceil(zoomTranslationX / sceneState.zoom / appState.gridSize) *
appState.gridSize +
(sceneState.scrollX % appState.gridSize),
-Math.ceil(zoomTranslationY / sceneState.zoom / appState.gridSize) *
appState.gridSize +
(sceneState.scrollY % appState.gridSize),
normalizedCanvasWidth / sceneState.zoom,
normalizedCanvasHeight / sceneState.zoom,
);
}
// Paint visible elements
const visibleElements = elements.filter((element) =>
isVisibleElement(

View File

@ -24,6 +24,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -421,6 +422,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -627,6 +629,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -750,6 +753,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -1009,6 +1013,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -1170,6 +1175,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -1369,6 +1375,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -1574,6 +1581,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -1880,6 +1888,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -2272,6 +2281,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -4057,6 +4067,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -4180,6 +4191,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -4303,6 +4315,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -4426,6 +4439,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -4571,6 +4585,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -4716,6 +4731,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -4861,6 +4877,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -5006,6 +5023,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -5129,6 +5147,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -5252,6 +5271,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -5397,6 +5417,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -5520,6 +5541,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -5665,6 +5687,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -6302,6 +6325,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -6508,6 +6532,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -6572,6 +6597,7 @@ Object {
"elementType": "rectangle",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -6634,6 +6660,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -7453,6 +7480,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -7849,6 +7877,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -8162,6 +8191,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -8396,6 +8426,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -8555,6 +8586,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -9323,6 +9355,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -9992,6 +10025,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -10566,6 +10600,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -11049,6 +11084,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -11488,6 +11524,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -11842,6 +11879,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -12115,6 +12153,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -12311,6 +12350,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -13130,6 +13170,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -13848,6 +13889,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -14469,6 +14511,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -14997,6 +15040,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -15267,6 +15311,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -15329,6 +15374,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -15452,6 +15498,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -15514,6 +15561,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -16165,6 +16213,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -16229,6 +16278,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -16654,6 +16704,7 @@ Object {
"elementType": "text",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,
@ -16727,6 +16778,7 @@ Object {
"elementType": "selection",
"errorMessage": null,
"exportBackground": true,
"gridSize": null,
"isCollaborating": false,
"isLoading": false,
"isResizing": false,

View File

@ -859,10 +859,10 @@ describe("regression tests", () => {
fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 });
const contextMenu = document.querySelector(".context-menu");
const options = contextMenu?.querySelectorAll(".context-menu-option");
const expectedOptions = ["Select all"];
const expectedOptions = ["Select all", "Toggle grid mode"];
expect(contextMenu).not.toBeNull();
expect(options?.length).toBe(1);
expect(options?.length).toBe(2);
expect(options?.item(0).textContent).toBe(expectedOptions[0]);
});

View File

@ -72,6 +72,7 @@ export type AppState = {
shouldCacheIgnoreZoom: boolean;
showShortcutsDialog: boolean;
zenModeEnabled: boolean;
gridSize: number | null;
/** top-most selected groups (i.e. does not include nested groups) */
selectedGroupIds: { [groupId: string]: boolean };