From baa8fb6c14c49fdd0b6ff0b4a5ab768bb39071f9 Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Wed, 24 Jun 2020 00:24:52 +0900 Subject: [PATCH] grid support (1st iteration) (#1788) Co-authored-by: dwelle --- src/appState.ts | 2 + src/components/App.tsx | 175 ++++++++++++------ src/components/ShortcutsDialog.tsx | 4 + src/constants.ts | 2 + src/element/dragElements.ts | 72 +++++++ src/element/index.ts | 5 + src/element/resizeElements.ts | 43 +++-- src/keys.ts | 4 + src/locales/en.json | 6 +- src/math.ts | 14 ++ src/renderer/renderScene.ts | 39 ++++ .../regressionTests.test.tsx.snap | 52 ++++++ src/tests/regressionTests.test.tsx | 4 +- src/types.ts | 1 + 14 files changed, 343 insertions(+), 80 deletions(-) create mode 100644 src/element/dragElements.ts diff --git a/src/appState.ts b/src/appState.ts index 39b3a491..61c0d4f5 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -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, }; }; diff --git a/src/components/App.tsx b/src/components/App.tsx index 37c8f1c4..a6c58cef 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -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 { }); }; + 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 { 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 { 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 { const originX = x; const originY = y; + const [originGridX, originGridY] = getGridPoint( + originX, + originY, + this.state.gridSize, + ); type ResizeTestType = ReturnType; let resizeHandle: ResizeTestType = false; @@ -2023,6 +2046,7 @@ class App extends React.Component { 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 { 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 { }); 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 { }); } } 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 { 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 { 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 { 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 { 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 { 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 { }); } } + } 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 { ...this.actionManager.getContextMenuItems((action) => CANVAS_ONLY_ACTIONS.includes(action.name), ), + { + label: t("labels.toggleGridMode"), + action: this.toggleGridMode, + }, ], top: event.clientY, left: event.clientX, diff --git a/src/components/ShortcutsDialog.tsx b/src/components/ShortcutsDialog.tsx index b00196f0..6603b7f7 100644 --- a/src/components/ShortcutsDialog.tsx +++ b/src/components/ShortcutsDialog.tsx @@ -247,6 +247,10 @@ export const ShortcutsDialog = ({ onClose }: { onClose?: () => void }) => { label={t("buttons.toggleZenMode")} shortcuts={[getShortcutKey("Alt+Z")]} /> + diff --git a/src/constants.ts b/src/constants.ts index 83499f63..2d468aff 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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? diff --git a/src/element/dragElements.ts b/src/element/dragElements.ts new file mode 100644 index 00000000..b32fab0a --- /dev/null +++ b/src/element/dragElements.ts @@ -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, + }); + } +}; diff --git a/src/element/index.ts b/src/element/index.ts index 23379b3d..165bb0d4 100644 --- a/src/element/index.ts +++ b/src/element/index.ts @@ -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"; diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index f7e38673..159921a7 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -21,10 +21,6 @@ import { getCursorForResizingElement, normalizeResizeHandle, } from "./resizeTest"; -import { - getResizeCenterPointKey, - getResizeWithSidesSameLengthKey, -} from "../keys"; import { measureText, getFontString } from "../utils"; type ResizeTestType = ReturnType; @@ -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, 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; }; diff --git a/src/keys.ts b/src/keys.ts index 3b181ad1..10e82bcb 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -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; diff --git a/src/locales/en.json b/src/locales/en.json index 416b144f..195192c6 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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?", diff --git a/src/math.ts b/src/math.ts index 44164e9d..11ca66bb 100644 --- a/src/math.ts +++ b/src/math.ts @@ -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]; +}; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 4af2042a..5cc00b9c 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -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( diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 15f65206..1429d3ea 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -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, diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx index 794edaed..3209000d 100644 --- a/src/tests/regressionTests.test.tsx +++ b/src/tests/regressionTests.test.tsx @@ -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]); }); diff --git a/src/types.ts b/src/types.ts index 9e205d2c..92a075fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 };