From 3cfcc7b489b359c86854f331131c41b8f583de74 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Wed, 14 Aug 2024 14:59:14 +0200 Subject: [PATCH] feat: split `gridSize` from enabled state & support custom `gridStep` (#8364) --- dev-docs/docs/codebase/json-schema.mdx | 2 +- packages/excalidraw/actions/actionCanvas.tsx | 3 +- .../actions/actionDuplicateSelection.tsx | 6 +- .../actions/actionToggleGridMode.tsx | 9 +- .../actions/actionToggleObjectsSnapMode.tsx | 2 +- packages/excalidraw/appState.ts | 8 +- packages/excalidraw/components/App.tsx | 77 +++--- packages/excalidraw/components/LayerUI.tsx | 2 +- .../components/Stats/CanvasGrid.tsx | 67 +++++ .../components/Stats/DragInput.scss | 3 +- .../excalidraw/components/Stats/DragInput.tsx | 62 +++-- .../excalidraw/components/Stats/index.tsx | 39 ++- packages/excalidraw/components/Stats/utils.ts | 3 +- .../components/canvases/StaticCanvas.tsx | 1 + packages/excalidraw/constants.ts | 5 +- packages/excalidraw/data/restore.ts | 41 ++-- packages/excalidraw/element/dragElements.ts | 11 +- .../excalidraw/element/linearElementEditor.ts | 30 ++- packages/excalidraw/math.ts | 14 +- packages/excalidraw/renderer/staticScene.ts | 63 +++-- packages/excalidraw/scene/index.ts | 6 +- packages/excalidraw/scene/normalize.ts | 15 ++ packages/excalidraw/scene/zoom.ts | 5 - .../__snapshots__/contextmenu.test.tsx.snap | 68 +++-- .../tests/__snapshots__/history.test.tsx.snap | 232 +++++++++++++----- .../regressionTests.test.tsx.snap | 208 ++++++++++++---- packages/excalidraw/tests/excalidraw.test.tsx | 10 +- .../tests/fixtures/diagramFixture.ts | 2 +- packages/excalidraw/types.ts | 15 +- .../utils/__snapshots__/export.test.ts.snap | 4 +- packages/utils/utils.unmocked.test.ts | 4 +- 31 files changed, 738 insertions(+), 279 deletions(-) create mode 100644 packages/excalidraw/components/Stats/CanvasGrid.tsx create mode 100644 packages/excalidraw/scene/normalize.ts diff --git a/dev-docs/docs/codebase/json-schema.mdx b/dev-docs/docs/codebase/json-schema.mdx index 75916c5f8..e4302dc4e 100644 --- a/dev-docs/docs/codebase/json-schema.mdx +++ b/dev-docs/docs/codebase/json-schema.mdx @@ -43,7 +43,7 @@ When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`) // editor state (canvas config, preferences, ...) "appState": { - "gridSize": null, + "gridSize": 20, "viewBackgroundColor": "#ffffff" }, diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx index 7d0599112..c7ff38e3f 100644 --- a/packages/excalidraw/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -105,6 +105,8 @@ export const actionClearCanvas = register({ exportBackground: appState.exportBackground, exportEmbedScene: appState.exportEmbedScene, gridSize: appState.gridSize, + gridStep: appState.gridStep, + gridModeEnabled: appState.gridModeEnabled, stats: appState.stats, pasteDialog: appState.pasteDialog, activeTool: @@ -294,7 +296,6 @@ export const zoomToFitBounds = ({ appState.height / commonBoundsHeight, ) * clamp(viewportZoomFactor, 0.1, 1); - // Apply clamping to newZoomValue to be between 10% and 3000% newZoomValue = getNormalizedZoom(newZoomValue); let appStateWidth = appState.width; diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index ca1ad7b3b..eb81e715d 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -15,7 +15,7 @@ import { import type { AppState } from "../types"; import { fixBindingsAfterDuplication } from "../element/binding"; import type { ActionResult } from "./types"; -import { GRID_SIZE } from "../constants"; +import { DEFAULT_GRID_SIZE } from "../constants"; import { bindTextToShapeAfterDuplication, getBoundTextElement, @@ -99,8 +99,8 @@ const duplicateElements = ( groupIdMap, element, { - x: element.x + GRID_SIZE / 2, - y: element.y + GRID_SIZE / 2, + x: element.x + DEFAULT_GRID_SIZE / 2, + y: element.y + DEFAULT_GRID_SIZE / 2, }, ); duplicatedElementsMap.set(newElement.id, newElement); diff --git a/packages/excalidraw/actions/actionToggleGridMode.tsx b/packages/excalidraw/actions/actionToggleGridMode.tsx index 529489382..3ab4c4ff9 100644 --- a/packages/excalidraw/actions/actionToggleGridMode.tsx +++ b/packages/excalidraw/actions/actionToggleGridMode.tsx @@ -1,6 +1,5 @@ import { CODES, KEYS } from "../keys"; import { register } from "./register"; -import { GRID_SIZE } from "../constants"; import type { AppState } from "../types"; import { gridIcon } from "../components/icons"; import { StoreAction } from "../store"; @@ -13,21 +12,21 @@ export const actionToggleGridMode = register({ viewMode: true, trackEvent: { category: "canvas", - predicate: (appState) => !appState.gridSize, + predicate: (appState) => appState.gridModeEnabled, }, perform(elements, appState) { return { appState: { ...appState, - gridSize: this.checked!(appState) ? null : GRID_SIZE, + gridModeEnabled: !this.checked!(appState), objectsSnapModeEnabled: false, }, storeAction: StoreAction.NONE, }; }, - checked: (appState: AppState) => appState.gridSize !== null, + checked: (appState: AppState) => appState.gridModeEnabled, predicate: (element, appState, props) => { - return typeof props.gridModeEnabled === "undefined"; + return props.gridModeEnabled === undefined; }, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE, }); diff --git a/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx b/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx index 586293d08..e27decf46 100644 --- a/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx +++ b/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx @@ -17,7 +17,7 @@ export const actionToggleObjectsSnapMode = register({ appState: { ...appState, objectsSnapModeEnabled: !this.checked!(appState), - gridSize: null, + gridModeEnabled: false, }, storeAction: StoreAction.NONE, }; diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index 12a0f43dd..75afd1bfa 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -5,9 +5,11 @@ import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, DEFAULT_TEXT_ALIGN, + DEFAULT_GRID_SIZE, EXPORT_SCALES, STATS_PANELS, THEME, + DEFAULT_GRID_STEP, } from "./constants"; import type { AppState, NormalizedZoomValue } from "./types"; @@ -59,7 +61,9 @@ export const getDefaultAppState = (): Omit< exportEmbedScene: false, exportWithDarkMode: false, fileHandle: null, - gridSize: null, + gridSize: DEFAULT_GRID_SIZE, + gridStep: DEFAULT_GRID_STEP, + gridModeEnabled: false, isBindingEnabled: true, defaultSidebarDockedPreference: false, isLoading: false, @@ -174,6 +178,8 @@ const APP_STATE_STORAGE_CONF = (< exportWithDarkMode: { browser: true, export: false, server: false }, fileHandle: { browser: false, export: false, server: false }, gridSize: { browser: true, export: true, server: true }, + gridStep: { browser: true, export: true, server: true }, + gridModeEnabled: { browser: true, export: true, server: true }, height: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false }, defaultSidebarDockedPreference: { diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 032f5b5c1..4cbac0888 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -60,7 +60,6 @@ import { ENV, EVENT, FRAME_STYLE, - GRID_SIZE, IMAGE_MIME_TYPES, IMAGE_RENDER_TIMEOUT, isBrave, @@ -258,6 +257,7 @@ import type { UnsubscribeCallback, EmbedsValidationStatus, ElementsPendingErasure, + NullableGridSize, } from "../types"; import { debounce, @@ -661,7 +661,7 @@ class App extends React.Component { viewModeEnabled, zenModeEnabled, objectsSnapModeEnabled, - gridSize: gridModeEnabled ? GRID_SIZE : null, + gridModeEnabled: gridModeEnabled ?? defaultAppState.gridModeEnabled, name, width: window.innerWidth, height: window.innerHeight, @@ -812,6 +812,18 @@ class App extends React.Component { } } + /** + * Returns gridSize taking into account `gridModeEnabled`. + * If disabled, returns null. + */ + public getEffectiveGridSize = () => { + return ( + this.props.gridModeEnabled ?? this.state.gridModeEnabled + ? this.state.gridSize + : null + ) as NullableGridSize; + }; + private getHTMLIFrameElement( element: ExcalidrawIframeLikeElement, ): HTMLIFrameElement | undefined { @@ -1684,7 +1696,9 @@ class App extends React.Component { renderConfig={{ imageCache: this.imageCache, isExporting: false, - renderGrid: true, + renderGrid: + this.props.gridModeEnabled ?? + this.state.gridModeEnabled, canvasBackgroundColor: this.state.viewBackgroundColor, embedsValidationStatus: this.embedsValidationStatus, @@ -2171,7 +2185,6 @@ class App extends React.Component { if (actionResult.appState || editingElement || this.state.contextMenu) { let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false; let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false; - let gridSize = actionResult?.appState?.gridSize || null; const theme = actionResult?.appState?.theme || this.props.theme || THEME.LIGHT; const name = actionResult?.appState?.name ?? this.state.name; @@ -2185,10 +2198,6 @@ class App extends React.Component { zenModeEnabled = this.props.zenModeEnabled; } - if (typeof this.props.gridModeEnabled !== "undefined") { - gridSize = this.props.gridModeEnabled ? GRID_SIZE : null; - } - editingElement = actionResult.appState?.editingElement || null; // make sure editingElement points to latest element reference @@ -2220,7 +2229,6 @@ class App extends React.Component { editingElement, viewModeEnabled, zenModeEnabled, - gridSize, theme, name, errorMessage, @@ -2777,12 +2785,6 @@ class App extends React.Component { this.setState({ theme: this.props.theme }); } - if (prevProps.gridModeEnabled !== this.props.gridModeEnabled) { - this.setState({ - gridSize: this.props.gridModeEnabled ? GRID_SIZE : null, - }); - } - this.excalidrawContainerRef.current?.classList.toggle( "theme--dark", this.state.theme === THEME.DARK, @@ -3185,7 +3187,7 @@ class App extends React.Component { const dx = x - elementsCenterX; const dy = y - elementsCenterY; - const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize); + const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize()); const newElements = duplicateElements( elements.map((element) => { @@ -3570,7 +3572,10 @@ class App extends React.Component { * Zooms on canvas viewport center */ zoomCanvas = ( - /** decimal fraction between 0.1 (10% zoom) and 30 (3000% zoom) */ + /** + * Decimal fraction, auto-clamped between MIN_ZOOM and MAX_ZOOM. + * 1 = 100% zoom, 2 = 200% zoom, 0.5 = 50% zoom + */ value: number, ) => { this.setState({ @@ -4148,10 +4153,10 @@ class App extends React.Component { ? elbowArrow.startBinding || elbowArrow.endBinding ? 0 : ELEMENT_TRANSLATE_AMOUNT - : (this.state.gridSize && + : (this.getEffectiveGridSize() && (event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT - : this.state.gridSize)) || + : this.getEffectiveGridSize())) || (event.shiftKey ? ELEMENT_SHIFT_TRANSLATE_AMOUNT : ELEMENT_TRANSLATE_AMOUNT); @@ -5496,7 +5501,7 @@ class App extends React.Component { event, scenePointerX, scenePointerY, - this.state, + this, this.scene.getNonDeletedElementsMap(), ); @@ -5586,7 +5591,7 @@ class App extends React.Component { scenePointerY, event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement) ? null - : this.state.gridSize, + : this.getEffectiveGridSize(), ); const [lastCommittedX, lastCommittedY] = @@ -6553,7 +6558,7 @@ class App extends React.Component { origin.y, event[KEYS.CTRL_OR_CMD] || isElbowArrowOnly ? null - : this.state.gridSize, + : this.getEffectiveGridSize(), ), ), scrollbars: isOverScrollBars( @@ -6730,7 +6735,7 @@ class App extends React.Component { this.state.editingLinearElement || this.state.selectedLinearElement; const ret = LinearElementEditor.handlePointerDown( event, - this.state, + this, this.store, pointerDownState.origin, linearElementEditor, @@ -7093,7 +7098,7 @@ class App extends React.Component { sceneY, this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null - : this.state.gridSize, + : this.getEffectiveGridSize(), ); const element = newIframeElement({ @@ -7133,7 +7138,7 @@ class App extends React.Component { sceneY, this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null - : this.state.gridSize, + : this.getEffectiveGridSize(), ); const embedLink = getEmbedLink(link); @@ -7186,7 +7191,7 @@ class App extends React.Component { sceneY, this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null - : this.state.gridSize, + : this.getEffectiveGridSize(), ); const topLayerFrame = addToFrameUnderCursor @@ -7283,7 +7288,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, pointerDownState.origin.y, - event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ @@ -7404,7 +7409,7 @@ class App extends React.Component { pointerDownState.origin.y, this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null - : this.state.gridSize, + : this.getEffectiveGridSize(), ); const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ @@ -7462,7 +7467,7 @@ class App extends React.Component { pointerDownState.origin.y, this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null - : this.state.gridSize, + : this.getEffectiveGridSize(), ); const constructorOpts = { @@ -7598,7 +7603,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerCoords.x, pointerCoords.y, - event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); // for arrows/lines, don't start dragging until a given threshold @@ -7645,7 +7650,7 @@ class App extends React.Component { const ret = LinearElementEditor.addMidpoint( this.state.selectedLinearElement, pointerCoords, - this.state, + this, !event[KEYS.CTRL_OR_CMD], elementsMap, ); @@ -7688,7 +7693,7 @@ class App extends React.Component { const didDrag = LinearElementEditor.handlePointDragging( event, - this.state, + this, pointerCoords.x, pointerCoords.y, (element, pointsSceneCoords) => { @@ -7822,7 +7827,7 @@ class App extends React.Component { dragOffset, this.scene, snapOffset, - event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); this.setState({ @@ -9794,7 +9799,7 @@ class App extends React.Component { let [gridX, gridY] = getGridPoint( pointerCoords.x, pointerCoords.y, - event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); const image = @@ -9898,7 +9903,7 @@ class App extends React.Component { let [resizeX, resizeY] = getGridPoint( pointerCoords.x - pointerDownState.resize.offset.x, pointerCoords.y - pointerDownState.resize.offset.y, - event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); const frameElementsOffsetsMap = new Map< @@ -9929,7 +9934,7 @@ class App extends React.Component { const [gridX, gridY] = getGridPoint( pointerCoords.x, pointerCoords.y, - event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(), ); const dragOffset = { diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx index dd3270670..cb641bb61 100644 --- a/packages/excalidraw/components/LayerUI.tsx +++ b/packages/excalidraw/components/LayerUI.tsx @@ -360,7 +360,7 @@ const LayerUI = ({ )} {shouldShowStats && ( { actionManager.executeAction(actionToggleStats); }} diff --git a/packages/excalidraw/components/Stats/CanvasGrid.tsx b/packages/excalidraw/components/Stats/CanvasGrid.tsx new file mode 100644 index 000000000..a08f709ae --- /dev/null +++ b/packages/excalidraw/components/Stats/CanvasGrid.tsx @@ -0,0 +1,67 @@ +import StatsDragInput from "./DragInput"; +import type Scene from "../../scene/Scene"; +import type { AppState } from "../../types"; +import { getStepSizedValue } from "./utils"; +import { getNormalizedGridStep } from "../../scene"; + +interface PositionProps { + property: "gridStep"; + scene: Scene; + appState: AppState; + setAppState: React.Component["setState"]; +} + +const STEP_SIZE = 5; + +const CanvasGrid = ({ + property, + scene, + appState, + setAppState, +}: PositionProps) => { + return ( + { + setAppState((state) => { + let nextGridStep; + + if (nextValue) { + nextGridStep = nextValue; + } else if (instantChange) { + nextGridStep = shouldChangeByStepSize + ? getStepSizedValue( + state.gridStep + STEP_SIZE * Math.sign(instantChange), + STEP_SIZE, + ) + : state.gridStep + instantChange; + } + + if (!nextGridStep) { + setInputValue(state.gridStep); + return null; + } + + nextGridStep = getNormalizedGridStep(nextGridStep); + setInputValue(nextGridStep); + return { + gridStep: nextGridStep, + }; + }); + }} + scene={scene} + value={appState.gridStep} + property={property} + appState={appState} + /> + ); +}; + +export default CanvasGrid; diff --git a/packages/excalidraw/components/Stats/DragInput.scss b/packages/excalidraw/components/Stats/DragInput.scss index 28ef9b0df..76b9d147b 100644 --- a/packages/excalidraw/components/Stats/DragInput.scss +++ b/packages/excalidraw/components/Stats/DragInput.scss @@ -18,7 +18,8 @@ flex-shrink: 0; border: 1px solid var(--default-border-color); border-right: 0; - width: 2rem; + padding: 0 0.5rem 0 0.75rem; + min-width: 1rem; height: 2rem; box-sizing: border-box; color: var(--popup-text-color); diff --git a/packages/excalidraw/components/Stats/DragInput.tsx b/packages/excalidraw/components/Stats/DragInput.tsx index 97dc57c24..d7120bdef 100644 --- a/packages/excalidraw/components/Stats/DragInput.tsx +++ b/packages/excalidraw/components/Stats/DragInput.tsx @@ -29,6 +29,7 @@ export type DragInputCallbackType< nextValue?: number; property: P; originalAppState: AppState; + setInputValue: (value: number) => void; }) => void; interface StatsDragInputProps< @@ -45,6 +46,8 @@ interface StatsDragInputProps< property: T; scene: Scene; appState: AppState; + /** how many px you need to drag to get 1 unit change */ + sensitivity?: number; } const StatsDragInput = < @@ -61,6 +64,7 @@ const StatsDragInput = < property, scene, appState, + sensitivity = 1, }: StatsDragInputProps) => { const app = useApp(); const inputRef = useRef(null); @@ -126,6 +130,7 @@ const StatsDragInput = < nextValue: rounded, property, originalAppState: appState, + setInputValue: (value) => setInputValue(String(value)), }); app.syncActionResult({ storeAction: StoreAction.CAPTURE }); } @@ -172,6 +177,8 @@ const StatsDragInput = < ref={labelRef} onPointerDown={(event) => { if (inputRef.current && editable) { + document.body.classList.add("excalidraw-cursor-resize"); + let startValue = Number(inputRef.current.value); if (isNaN(startValue)) { startValue = 0; @@ -196,35 +203,43 @@ const StatsDragInput = < const originalAppState: AppState = cloneJSON(appState); - let accumulatedChange: number | null = null; - - document.body.classList.add("excalidraw-cursor-resize"); + let accumulatedChange = 0; + let stepChange = 0; const onPointerMove = (event: PointerEvent) => { - if (!accumulatedChange) { - accumulatedChange = 0; - } - if ( lastPointer && originalElementsMap !== null && - originalElements !== null && - accumulatedChange !== null + originalElements !== null ) { const instantChange = event.clientX - lastPointer.x; - accumulatedChange += instantChange; - - dragInputCallback({ - accumulatedChange, - instantChange, - originalElements, - originalElementsMap, - shouldKeepAspectRatio: shouldKeepAspectRatio!!, - shouldChangeByStepSize: event.shiftKey, - property, - scene, - originalAppState, - }); + + if (instantChange !== 0) { + stepChange += instantChange; + + if (Math.abs(stepChange) >= sensitivity) { + stepChange = + Math.sign(stepChange) * + Math.floor(Math.abs(stepChange) / sensitivity); + + accumulatedChange += stepChange; + + dragInputCallback({ + accumulatedChange, + instantChange: stepChange, + originalElements, + originalElementsMap, + shouldKeepAspectRatio: shouldKeepAspectRatio!!, + shouldChangeByStepSize: event.shiftKey, + property, + scene, + originalAppState, + setInputValue: (value) => setInputValue(String(value)), + }); + + stepChange = 0; + } + } } lastPointer = { @@ -246,7 +261,8 @@ const StatsDragInput = < app.syncActionResult({ storeAction: StoreAction.CAPTURE }); lastPointer = null; - accumulatedChange = null; + accumulatedChange = 0; + stepChange = 0; originalElements = null; originalElementsMap = null; diff --git a/packages/excalidraw/components/Stats/index.tsx b/packages/excalidraw/components/Stats/index.tsx index c808717a5..642862e2e 100644 --- a/packages/excalidraw/components/Stats/index.tsx +++ b/packages/excalidraw/components/Stats/index.tsx @@ -2,7 +2,11 @@ import { useEffect, useMemo, useState, memo } from "react"; import { getCommonBounds } from "../../element/bounds"; import type { NonDeletedExcalidrawElement } from "../../element/types"; import { t } from "../../i18n"; -import type { AppState, ExcalidrawProps } from "../../types"; +import type { + AppClassProperties, + AppState, + ExcalidrawProps, +} from "../../types"; import { CloseIcon } from "../icons"; import { Island } from "../Island"; import { throttle } from "lodash"; @@ -16,17 +20,17 @@ import MultiFontSize from "./MultiFontSize"; import Position from "./Position"; import MultiPosition from "./MultiPosition"; import Collapsible from "./Collapsible"; -import type Scene from "../../scene/Scene"; import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App"; import { getAtomicUnits } from "./utils"; import { STATS_PANELS } from "../../constants"; import { isElbowArrow } from "../../element/typeChecks"; +import CanvasGrid from "./CanvasGrid"; import clsx from "clsx"; import "./Stats.scss"; interface StatsProps { - scene: Scene; + app: AppClassProperties; onClose: () => void; renderCustomStats: ExcalidrawProps["renderCustomStats"]; } @@ -35,11 +39,13 @@ const STATS_TIMEOUT = 50; export const Stats = (props: StatsProps) => { const appState = useExcalidrawAppState(); - const sceneNonce = props.scene.getSceneNonce() || 1; - const selectedElements = props.scene.getSelectedElements({ + const sceneNonce = props.app.scene.getSceneNonce() || 1; + const selectedElements = props.app.scene.getSelectedElements({ selectedElementIds: appState.selectedElementIds, includeBoundTextElement: false, }); + const gridModeEnabled = + props.app.props.gridModeEnabled ?? appState.gridModeEnabled; return ( { appState={appState} sceneNonce={sceneNonce} selectedElements={selectedElements} + gridModeEnabled={gridModeEnabled} /> ); }; @@ -97,17 +104,20 @@ Stats.StatsRows = StatsRows; export const StatsInner = memo( ({ - scene, + app, onClose, renderCustomStats, selectedElements, appState, sceneNonce, + gridModeEnabled, }: StatsProps & { sceneNonce: number; selectedElements: readonly NonDeletedExcalidrawElement[]; appState: AppState; + gridModeEnabled: boolean; }) => { + const scene = app.scene; const elements = scene.getNonDeletedElements(); const elementsMap = scene.getNonDeletedElementsMap(); const setAppState = useExcalidrawSetAppState(); @@ -189,6 +199,19 @@ export const StatsInner = memo(
{t("stats.height")}
{sceneDimension.height}
+ {gridModeEnabled && ( + <> + Canvas + + + + + )} {renderCustomStats?.(elements, appState)} @@ -362,7 +385,9 @@ export const StatsInner = memo( return ( prev.sceneNonce === next.sceneNonce && prev.selectedElements === next.selectedElements && - prev.appState.stats.panels === next.appState.stats.panels + prev.appState.stats.panels === next.appState.stats.panels && + prev.gridModeEnabled === next.gridModeEnabled && + prev.appState.gridStep === next.appState.gridStep ); }, ); diff --git a/packages/excalidraw/components/Stats/utils.ts b/packages/excalidraw/components/Stats/utils.ts index 5ea1f24fd..f2e9765dc 100644 --- a/packages/excalidraw/components/Stats/utils.ts +++ b/packages/excalidraw/components/Stats/utils.ts @@ -41,7 +41,8 @@ export type StatsInputProperty = | "width" | "height" | "angle" - | "fontSize"; + | "fontSize" + | "gridStep"; export const SMALLEST_DELTA = 0.01; diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx index 7577ee56a..ad19afdd8 100644 --- a/packages/excalidraw/components/canvases/StaticCanvas.tsx +++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx @@ -101,6 +101,7 @@ const getRelevantAppStateProps = ( exportScale: appState.exportScale, selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged, gridSize: appState.gridSize, + gridStep: appState.gridStep, frameRendering: appState.frameRendering, selectedElementIds: appState.selectedElementIds, frameToHighlight: appState.frameToHighlight, diff --git a/packages/excalidraw/constants.ts b/packages/excalidraw/constants.ts index d73ed0fba..27bbfb4e0 100644 --- a/packages/excalidraw/constants.ts +++ b/packages/excalidraw/constants.ts @@ -179,7 +179,8 @@ export const COLOR_VOICE_CALL = "#a2f1a6"; export const CANVAS_ONLY_ACTIONS = ["selectAll"]; -export const GRID_SIZE = 20; // TODO make it configurable? +export const DEFAULT_GRID_SIZE = 20; +export const DEFAULT_GRID_STEP = 5; export const IMAGE_MIME_TYPES = { svg: "image/svg+xml", @@ -234,7 +235,7 @@ export const VERSION_TIMEOUT = 30000; export const SCROLL_TIMEOUT = 100; export const ZOOM_STEP = 0.1; export const MIN_ZOOM = 0.1; -export const MAX_ZOOM = 30.0; +export const MAX_ZOOM = 30; export const HYPERLINK_TOOLTIP_DELAY = 300; // Report a user inactive after IDLE_THRESHOLD milliseconds diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index dd1c55da1..8fbe1e236 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -10,12 +10,7 @@ import type { PointBinding, StrokeRoundness, } from "../element/types"; -import type { - AppState, - BinaryFiles, - LibraryItem, - NormalizedZoomValue, -} from "../types"; +import type { AppState, BinaryFiles, LibraryItem } from "../types"; import type { ImportedDataState, LegacyAppState } from "./types"; import { getNonDeletedElements, @@ -39,11 +34,17 @@ import { ROUNDNESS, DEFAULT_SIDEBAR, DEFAULT_ELEMENT_PROPS, + DEFAULT_GRID_SIZE, + DEFAULT_GRID_STEP, } from "../constants"; import { getDefaultAppState } from "../appState"; import { LinearElementEditor } from "../element/linearElementEditor"; import { bumpVersion } from "../element/mutateElement"; -import { getUpdatedTimestamp, updateActiveTool } from "../utils"; +import { + getUpdatedTimestamp, + isFiniteNumber, + updateActiveTool, +} from "../utils"; import { arrayToMap } from "../utils"; import type { MarkOptional, Mutable } from "../utility-types"; import { detectLineHeight, getContainerElement } from "../element/textElement"; @@ -52,6 +53,11 @@ import { syncInvalidIndices } from "../fractionalIndex"; import { getSizeFromPoints } from "../points"; import { getLineHeight } from "../fonts"; import { normalizeFixedPoint } from "../element/binding"; +import { + getNormalizedGridSize, + getNormalizedGridStep, + getNormalizedZoom, +} from "../scene"; type RestoredAppState = Omit< AppState, @@ -614,19 +620,24 @@ export const restoreAppState = ( locked: nextAppState.activeTool.locked ?? false, }, // Migrates from previous version where appState.zoom was a number - zoom: - typeof appState.zoom === "number" - ? { - value: appState.zoom as NormalizedZoomValue, - } - : appState.zoom?.value - ? appState.zoom - : defaultAppState.zoom, + zoom: { + value: getNormalizedZoom( + isFiniteNumber(appState.zoom) + ? appState.zoom + : appState.zoom?.value ?? defaultAppState.zoom.value, + ), + }, openSidebar: // string (legacy) typeof (appState.openSidebar as any as string) === "string" ? { name: DEFAULT_SIDEBAR.name } : nextAppState.openSidebar, + gridSize: getNormalizedGridSize( + isFiniteNumber(appState.gridSize) ? appState.gridSize : DEFAULT_GRID_SIZE, + ), + gridStep: getNormalizedGridStep( + isFiniteNumber(appState.gridStep) ? appState.gridStep : DEFAULT_GRID_STEP, + ), }; }; diff --git a/packages/excalidraw/element/dragElements.ts b/packages/excalidraw/element/dragElements.ts index f456118b7..2621e598a 100644 --- a/packages/excalidraw/element/dragElements.ts +++ b/packages/excalidraw/element/dragElements.ts @@ -4,7 +4,12 @@ import { getCommonBounds } from "./bounds"; import { mutateElement } from "./mutateElement"; import { getPerfectElementSize } from "./sizeHelpers"; import type { NonDeletedExcalidrawElement } from "./types"; -import type { AppState, NormalizedZoomValue, PointerDownState } from "../types"; +import type { + AppState, + NormalizedZoomValue, + NullableGridSize, + PointerDownState, +} from "../types"; import { getBoundTextElement, getMinTextElementWidth } from "./textElement"; import { getGridPoint } from "../math"; import type Scene from "../scene/Scene"; @@ -26,7 +31,7 @@ export const dragSelectedElements = ( x: number; y: number; }, - gridSize: AppState["gridSize"], + gridSize: NullableGridSize, ) => { if ( _selectedElements.length === 1 && @@ -101,7 +106,7 @@ const calculateOffset = ( commonBounds: Bounds, dragOffset: { x: number; y: number }, snapOffset: { x: number; y: number }, - gridSize: AppState["gridSize"], + gridSize: NullableGridSize, ): { x: number; y: number } => { const [x, y] = commonBounds; let nextX = x + dragOffset.x + snapOffset.x; diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index 900b42a90..f671e2f2c 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -36,6 +36,8 @@ import type { AppState, PointerCoords, InteractiveCanvasAppState, + AppClassProperties, + NullableGridSize, } from "../types"; import { mutateElement } from "./mutateElement"; @@ -209,7 +211,7 @@ export class LinearElementEditor { /** @returns whether point was dragged */ static handlePointDragging( event: PointerEvent, - appState: AppState, + app: AppClassProperties, scenePointerX: number, scenePointerY: number, maybeSuggestBinding: ( @@ -279,7 +281,7 @@ export class LinearElementEditor { elementsMap, referencePoint, [scenePointerX, scenePointerY], - event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); LinearElementEditor.movePoints( @@ -299,7 +301,7 @@ export class LinearElementEditor { elementsMap, scenePointerX - linearElementEditor.pointerOffset.x, scenePointerY - linearElementEditor.pointerOffset.y, - event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); const deltaX = newDraggingPointPosition[0] - draggingPoint[0]; @@ -315,7 +317,7 @@ export class LinearElementEditor { elementsMap, scenePointerX - linearElementEditor.pointerOffset.x, scenePointerY - linearElementEditor.pointerOffset.y, - event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ) : ([ element.points[pointIndex][0] + deltaX, @@ -695,7 +697,7 @@ export class LinearElementEditor { static handlePointerDown( event: React.PointerEvent, - appState: AppState, + app: AppClassProperties, store: Store, scenePointer: { x: number; y: number }, linearElementEditor: LinearElementEditor, @@ -705,6 +707,7 @@ export class LinearElementEditor { hitElement: NonDeleted | null; linearElementEditor: LinearElementEditor | null; } { + const appState = app.state; const elementsMap = scene.getNonDeletedElementsMap(); const elements = scene.getNonDeletedElements(); @@ -752,7 +755,7 @@ export class LinearElementEditor { elementsMap, scenePointer.x, scenePointer.y, - event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ), ], }); @@ -876,9 +879,10 @@ export class LinearElementEditor { event: React.PointerEvent, scenePointerX: number, scenePointerY: number, - appState: AppState, + app: AppClassProperties, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, ): LinearElementEditor | null { + const appState = app.state; if (!appState.editingLinearElement) { return null; } @@ -915,7 +919,7 @@ export class LinearElementEditor { elementsMap, lastCommittedPoint, [scenePointerX, scenePointerY], - event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, + event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(), ); newPoint = [ @@ -930,7 +934,7 @@ export class LinearElementEditor { scenePointerY - appState.editingLinearElement.pointerOffset.y, event[KEYS.CTRL_OR_CMD] || isElbowArrow(element) ? null - : appState.gridSize, + : app.getEffectiveGridSize(), ); } @@ -1065,7 +1069,7 @@ export class LinearElementEditor { elementsMap: ElementsMap, scenePointerX: number, scenePointerY: number, - gridSize: number | null, + gridSize: NullableGridSize, ): Point { const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); @@ -1363,7 +1367,7 @@ export class LinearElementEditor { static addMidpoint( linearElementEditor: LinearElementEditor, pointerCoords: PointerCoords, - appState: AppState, + app: AppClassProperties, snapToGrid: boolean, elementsMap: ElementsMap, ) { @@ -1388,7 +1392,7 @@ export class LinearElementEditor { elementsMap, pointerCoords.x, pointerCoords.y, - snapToGrid && !isElbowArrow(element) ? appState.gridSize : null, + snapToGrid && !isElbowArrow(element) ? app.getEffectiveGridSize() : null, ); const points = [ ...element.points.slice(0, segmentMidpoint.index!), @@ -1485,7 +1489,7 @@ export class LinearElementEditor { elementsMap: ElementsMap, referencePoint: Point, scenePointer: Point, - gridSize: number | null, + gridSize: NullableGridSize, ) { const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates( element, diff --git a/packages/excalidraw/math.ts b/packages/excalidraw/math.ts index 0f84ce98a..32414efb1 100644 --- a/packages/excalidraw/math.ts +++ b/packages/excalidraw/math.ts @@ -1,4 +1,9 @@ -import type { NormalizedZoomValue, Point, Zoom } from "./types"; +import type { + NormalizedZoomValue, + NullableGridSize, + Point, + Zoom, +} from "./types"; import { DEFAULT_ADAPTIVE_RADIUS, LINE_CONFIRM_THRESHOLD, @@ -275,7 +280,7 @@ const doSegmentsIntersect = (p1: Point, q1: Point, p2: Point, q2: Point) => { export const getGridPoint = ( x: number, y: number, - gridSize: number | null, + gridSize: NullableGridSize, ): [number, number] => { if (gridSize) { return [ @@ -703,3 +708,8 @@ export const aabbsOverlapping = (a: Bounds, b: Bounds) => export const clamp = (value: number, min: number, max: number) => { return Math.min(Math.max(value, min), max); }; + +export const round = (value: number, precision: number) => { + const multiplier = Math.pow(10, precision); + return Math.round((value + Number.EPSILON) * multiplier) / multiplier; +}; diff --git a/packages/excalidraw/renderer/staticScene.ts b/packages/excalidraw/renderer/staticScene.ts index b2b8becd2..90c07e8cb 100644 --- a/packages/excalidraw/renderer/staticScene.ts +++ b/packages/excalidraw/renderer/staticScene.ts @@ -31,53 +31,77 @@ import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers"; import { throttleRAF } from "../utils"; import { getBoundTextElement } from "../element/textElement"; +const GridLineColor = { + Bold: "#dddddd", + Regular: "#e5e5e5", +} as const; + const strokeGrid = ( context: CanvasRenderingContext2D, + /** grid cell pixel size */ gridSize: number, + /** setting to 1 will disble bold lines */ + gridStep: number, scrollX: number, scrollY: number, zoom: Zoom, width: number, height: number, ) => { - const BOLD_LINE_FREQUENCY = 5; - - enum GridLineColor { - Bold = "#cccccc", - Regular = "#e5e5e5", - } - - const offsetX = - -Math.round(zoom.value / gridSize) * gridSize + (scrollX % gridSize); - const offsetY = - -Math.round(zoom.value / gridSize) * gridSize + (scrollY % gridSize); + const offsetX = (scrollX % gridSize) - gridSize; + const offsetY = (scrollY % gridSize) - gridSize; - const lineWidth = Math.min(1 / zoom.value, 1); + const actualGridSize = gridSize * zoom.value; const spaceWidth = 1 / zoom.value; - const lineDash = [lineWidth * 3, spaceWidth + (lineWidth + spaceWidth)]; context.save(); - context.lineWidth = lineWidth; + // Offset rendering by 0.5 to ensure that 1px wide lines are crisp. + // We only do this when zoomed to 100% because otherwise the offset is + // fractional, and also visibly offsets the elements. + // We also do this per-axis, as each axis may already be offset by 0.5. + if (zoom.value === 1) { + context.translate(offsetX % 1 ? 0 : 0.5, offsetY % 1 ? 0 : 0.5); + } + + // vertical lines for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) { const isBold = - Math.round(x - scrollX) % (BOLD_LINE_FREQUENCY * gridSize) === 0; + gridStep > 1 && Math.round(x - scrollX) % (gridStep * gridSize) === 0; + // don't render regular lines when zoomed out and they're barely visible + if (!isBold && actualGridSize < 10) { + continue; + } + + const lineWidth = Math.min(1 / zoom.value, isBold ? 4 : 1); + context.lineWidth = lineWidth; + const lineDash = [lineWidth * 3, spaceWidth + (lineWidth + spaceWidth)]; + context.beginPath(); context.setLineDash(isBold ? [] : lineDash); context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular; context.moveTo(x, offsetY - gridSize); - context.lineTo(x, offsetY + height + gridSize * 2); + context.lineTo(x, Math.ceil(offsetY + height + gridSize * 2)); context.stroke(); } + for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) { const isBold = - Math.round(y - scrollY) % (BOLD_LINE_FREQUENCY * gridSize) === 0; + gridStep > 1 && Math.round(y - scrollY) % (gridStep * gridSize) === 0; + if (!isBold && actualGridSize < 10) { + continue; + } + + const lineWidth = Math.min(1 / zoom.value, isBold ? 4 : 1); + context.lineWidth = lineWidth; + const lineDash = [lineWidth * 3, spaceWidth + (lineWidth + spaceWidth)]; + context.beginPath(); context.setLineDash(isBold ? [] : lineDash); context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular; context.moveTo(offsetX - gridSize, y); - context.lineTo(offsetX + width + gridSize * 2, y); + context.lineTo(Math.ceil(offsetX + width + gridSize * 2), y); context.stroke(); } context.restore(); @@ -199,10 +223,11 @@ const _renderStaticScene = ({ context.scale(appState.zoom.value, appState.zoom.value); // Grid - if (renderGrid && appState.gridSize) { + if (renderGrid) { strokeGrid( context, appState.gridSize, + appState.gridStep, appState.scrollX, appState.scrollY, appState.zoom, diff --git a/packages/excalidraw/scene/index.ts b/packages/excalidraw/scene/index.ts index 33399d79e..0f8b7ad85 100644 --- a/packages/excalidraw/scene/index.ts +++ b/packages/excalidraw/scene/index.ts @@ -15,4 +15,8 @@ export { getElementAtPosition, getElementsAtPosition, } from "./comparisons"; -export { getNormalizedZoom } from "./zoom"; +export { + getNormalizedZoom, + getNormalizedGridSize, + getNormalizedGridStep, +} from "./normalize"; diff --git a/packages/excalidraw/scene/normalize.ts b/packages/excalidraw/scene/normalize.ts new file mode 100644 index 000000000..11f68ca9b --- /dev/null +++ b/packages/excalidraw/scene/normalize.ts @@ -0,0 +1,15 @@ +import { MAX_ZOOM, MIN_ZOOM } from "../constants"; +import { clamp, round } from "../math"; +import type { NormalizedZoomValue } from "../types"; + +export const getNormalizedZoom = (zoom: number): NormalizedZoomValue => { + return clamp(round(zoom, 6), MIN_ZOOM, MAX_ZOOM) as NormalizedZoomValue; +}; + +export const getNormalizedGridSize = (gridStep: number) => { + return clamp(Math.round(gridStep), 1, 100); +}; + +export const getNormalizedGridStep = (gridStep: number) => { + return clamp(Math.round(gridStep), 1, 100); +}; diff --git a/packages/excalidraw/scene/zoom.ts b/packages/excalidraw/scene/zoom.ts index 7551dd319..5decf1149 100644 --- a/packages/excalidraw/scene/zoom.ts +++ b/packages/excalidraw/scene/zoom.ts @@ -1,10 +1,5 @@ -import { MIN_ZOOM } from "../constants"; import type { AppState, NormalizedZoomValue } from "../types"; -export const getNormalizedZoom = (zoom: number): NormalizedZoomValue => { - return Math.max(MIN_ZOOM, Math.min(zoom, 30)) as NormalizedZoomValue; -}; - export const getStateForZoom = ( { viewportX, diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 4bad1516e..a2b92bb11 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -831,7 +831,9 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -1034,7 +1036,9 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -1247,7 +1251,9 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -1575,7 +1581,9 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -1903,7 +1911,9 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -2116,7 +2126,9 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -2353,7 +2365,9 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -2651,7 +2665,9 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -3017,7 +3033,9 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -3489,7 +3507,9 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -3809,7 +3829,9 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -4129,7 +4151,9 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -5312,7 +5336,9 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -6436,7 +6462,9 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -7368,7 +7396,9 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -8277,7 +8307,9 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, @@ -9168,7 +9200,9 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 100, "isBindingEnabled": true, "isLoading": false, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index e47d946ca..07e69f2c3 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -48,7 +48,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -647,7 +649,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -1150,7 +1154,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -1515,7 +1521,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -1881,7 +1889,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -2145,7 +2155,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -2582,7 +2594,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -2878,7 +2892,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -3159,7 +3175,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -3450,7 +3468,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -3733,7 +3753,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -3965,7 +3987,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -4221,7 +4245,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -4491,7 +4517,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -4719,7 +4747,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -4947,7 +4977,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -5173,7 +5205,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -5399,7 +5433,9 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -5655,7 +5691,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -5983,7 +6021,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -6405,7 +6445,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -6780,7 +6822,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -7096,7 +7140,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -7391,7 +7437,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -7617,7 +7665,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -7969,7 +8019,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -8321,7 +8373,9 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -8722,7 +8776,9 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -9006,7 +9062,9 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -9268,7 +9326,9 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -9529,7 +9589,9 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -9757,7 +9819,9 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -10055,7 +10119,9 @@ exports[`history > multiplayer undo/redo > should override remotely added points "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -10392,7 +10458,9 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -10624,7 +10692,9 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -11074,7 +11144,9 @@ exports[`history > multiplayer undo/redo > should update history entries after r "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -11325,7 +11397,9 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -11561,7 +11635,9 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -11799,7 +11875,9 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -12197,7 +12275,9 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -12441,7 +12521,9 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -12679,7 +12761,9 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -12917,7 +13001,9 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -13161,7 +13247,9 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -13490,7 +13578,9 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -13659,7 +13749,9 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -13944,7 +14036,9 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -14208,7 +14302,9 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -14480,7 +14576,9 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -14638,7 +14736,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -15331,7 +15431,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -15948,7 +16050,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -16565,7 +16669,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -17274,7 +17380,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -18021,7 +18129,9 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -18492,7 +18602,9 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -19011,7 +19123,9 @@ exports[`history > singleplayer undo/redo > should support element creation, del "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, @@ -19464,7 +19578,9 @@ exports[`history > singleplayer undo/redo > should support linear element creati "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 0, "isBindingEnabled": true, "isLoading": false, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 2c8e4dde7..67bf5d52a 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -48,7 +48,9 @@ exports[`given element A and group of elements B and given both are selected whe "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -457,7 +459,9 @@ exports[`given element A and group of elements B and given both are selected whe "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -857,7 +861,9 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": false, "isLoading": false, @@ -1396,7 +1402,9 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -1594,7 +1602,9 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -1963,7 +1973,9 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -2197,7 +2209,9 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = ` "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -2371,7 +2385,9 @@ exports[`regression tests > can drag element that covers another element, while "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -2685,7 +2701,9 @@ exports[`regression tests > change the properties of a shape > [end of test] app "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -2925,7 +2943,9 @@ exports[`regression tests > click on an element and drag it > [dragged] appState "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -3162,7 +3182,9 @@ exports[`regression tests > click on an element and drag it > [end of test] appS "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -3386,7 +3408,9 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`] "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -3636,7 +3660,9 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -3941,7 +3967,9 @@ exports[`regression tests > deleting last but one element in editing group shoul "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -4349,7 +4377,9 @@ exports[`regression tests > deselects group of selected elements on pointer down "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -4626,7 +4656,9 @@ exports[`regression tests > deselects group of selected elements on pointer up w "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -4873,7 +4905,9 @@ exports[`regression tests > deselects selected element on pointer down when poin "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -5077,7 +5111,9 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -5270,7 +5306,9 @@ exports[`regression tests > double click to edit a group > [end of test] appStat "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -5646,7 +5684,9 @@ exports[`regression tests > drags selected elements from point inside common bou "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -5930,7 +5970,9 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1` "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -6732,7 +6774,9 @@ exports[`regression tests > given a group of selected elements with an element t "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -7056,7 +7100,9 @@ exports[`regression tests > given a selected element A and a not selected elemen "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -7326,7 +7372,9 @@ exports[`regression tests > given selected element A with lower z-index than uns "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -7554,7 +7602,9 @@ exports[`regression tests > given selected element A with lower z-index than uns "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -7785,7 +7835,9 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -7959,7 +8011,9 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -8133,7 +8187,9 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -8307,7 +8363,9 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -8523,7 +8581,9 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -8738,7 +8798,9 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -8926,7 +8988,9 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -9142,7 +9206,9 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -9316,7 +9382,9 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -9531,7 +9599,9 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -9705,7 +9775,9 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -9893,7 +9965,9 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -10067,7 +10141,9 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -10575,7 +10651,9 @@ exports[`regression tests > noop interaction after undo shouldn't create history "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -10846,7 +10924,9 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -10966,7 +11046,9 @@ exports[`regression tests > shift click on selected element should deselect it o "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -11159,7 +11241,9 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -11464,7 +11548,9 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -11870,7 +11956,9 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -12477,7 +12565,9 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -12600,7 +12690,9 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`] "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -13178,7 +13270,9 @@ exports[`regression tests > switches from group of selected elements to another "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -13510,7 +13604,9 @@ exports[`regression tests > switches selected element on pointer down > [end of "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -13769,7 +13865,9 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`] "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -13889,7 +13987,9 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -14262,7 +14362,9 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, @@ -14382,7 +14484,9 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "height": 768, "isBindingEnabled": true, "isLoading": false, diff --git a/packages/excalidraw/tests/excalidraw.test.tsx b/packages/excalidraw/tests/excalidraw.test.tsx index 9de22f9f6..6fbcf2adc 100644 --- a/packages/excalidraw/tests/excalidraw.test.tsx +++ b/packages/excalidraw/tests/excalidraw.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { fireEvent, GlobalTestState, toggleMenu, render } from "./test-utils"; import { Excalidraw, Footer, MainMenu } from "../index"; import { queryByText, queryByTestId } from "@testing-library/react"; -import { GRID_SIZE, THEME } from "../constants"; +import { THEME } from "../constants"; import { t } from "../i18n"; import { useMemo } from "react"; @@ -91,7 +91,7 @@ describe("", () => { describe("Test gridModeEnabled prop", () => { it('should show grid mode in context menu when gridModeEnabled is "undefined"', async () => { const { container } = await render(); - expect(h.state.gridSize).toBe(null); + expect(h.state.gridModeEnabled).toBe(false); expect( container.getElementsByClassName("disable-zen-mode--visible").length, @@ -103,14 +103,14 @@ describe("", () => { }); const contextMenu = document.querySelector(".context-menu"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Toggle grid")!); - expect(h.state.gridSize).toBe(GRID_SIZE); + expect(h.state.gridModeEnabled).toBe(true); }); it('should not show grid mode in context menu when gridModeEnabled is not "undefined"', async () => { const { container } = await render( , ); - expect(h.state.gridSize).toBe(null); + expect(h.state.gridModeEnabled).toBe(false); expect( container.getElementsByClassName("disable-zen-mode--visible").length, @@ -122,7 +122,7 @@ describe("", () => { }); const contextMenu = document.querySelector(".context-menu"); expect(queryByText(contextMenu as HTMLElement, "Show grid")).toBe(null); - expect(h.state.gridSize).toBe(null); + expect(h.state.gridModeEnabled).toBe(false); }); }); diff --git a/packages/excalidraw/tests/fixtures/diagramFixture.ts b/packages/excalidraw/tests/fixtures/diagramFixture.ts index 192442e35..72b909af8 100644 --- a/packages/excalidraw/tests/fixtures/diagramFixture.ts +++ b/packages/excalidraw/tests/fixtures/diagramFixture.ts @@ -12,7 +12,7 @@ export const diagramFixture = { elements: [diamondFixture, ellipseFixture, rectangleFixture], appState: { viewBackgroundColor: "#ffffff", - gridSize: null, + gridModeEnabled: false, }, files: {}, }; diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 0cff81641..3b107f26e 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -40,7 +40,7 @@ import type { FileSystemHandle } from "./data/filesystem"; import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; import type { ContextMenuItems } from "./components/ContextMenu"; import type { SnapLine } from "./snapping"; -import type { Merge, MaybePromise, ValueOf } from "./utility-types"; +import type { Merge, MaybePromise, ValueOf, MakeBrand } from "./utility-types"; import type { StoreActionType } from "./store"; export type Point = Readonly; @@ -176,6 +176,7 @@ export type StaticCanvasAppState = Readonly< exportScale: AppState["exportScale"]; selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"]; gridSize: AppState["gridSize"]; + gridStep: AppState["gridStep"]; frameRendering: AppState["frameRendering"]; currentHoveredFontFamily: AppState["currentHoveredFontFamily"]; } @@ -351,7 +352,10 @@ export interface AppState { toast: { message: string; closable?: boolean; duration?: number } | null; zenModeEnabled: boolean; theme: Theme; - gridSize: number | null; + /** grid cell px size */ + gridSize: number; + gridStep: number; + gridModeEnabled: boolean; viewModeEnabled: boolean; /** top-most selected groups (i.e. does not include nested groups) */ @@ -615,6 +619,7 @@ export type AppProps = Merge< * in the app, eg Manager. Factored out into a separate type to keep DRY. */ export type AppClassProperties = { props: AppProps; + state: AppState; interactiveCanvas: HTMLCanvasElement | null; /** static canvas */ canvas: HTMLCanvasElement; @@ -649,6 +654,7 @@ export type AppClassProperties = { getName: App["getName"]; dismissLinearEditor: App["dismissLinearEditor"]; flowChartCreator: App["flowChartCreator"]; + getEffectiveGridSize: App["getEffectiveGridSize"]; }; export type PointerDownState = Readonly<{ @@ -831,3 +837,8 @@ export type EmbedsValidationStatus = Map< export type ElementsPendingErasure = Set; export type PendingExcalidrawElements = ExcalidrawElement[]; + +/** Runtime gridSize value. Null indicates disabled grid. */ +export type NullableGridSize = + | (AppState["gridSize"] & MakeBrand<"NullableGridSize">) + | null; diff --git a/packages/utils/__snapshots__/export.test.ts.snap b/packages/utils/__snapshots__/export.test.ts.snap index 7521e9cd1..2c98135d8 100644 --- a/packages/utils/__snapshots__/export.test.ts.snap +++ b/packages/utils/__snapshots__/export.test.ts.snap @@ -49,7 +49,9 @@ exports[`exportToSvg > with default arguments 1`] = ` "outline": true, }, "frameToHighlight": null, - "gridSize": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, "isBindingEnabled": true, "isLoading": false, "isResizing": false, diff --git a/packages/utils/utils.unmocked.test.ts b/packages/utils/utils.unmocked.test.ts index ca4727855..0cf0bb8a4 100644 --- a/packages/utils/utils.unmocked.test.ts +++ b/packages/utils/utils.unmocked.test.ts @@ -19,7 +19,7 @@ describe("embedding scene data", () => { elements: sourceElements, appState: { viewBackgroundColor: "#ffffff", - gridSize: null, + gridModeEnabled: false, exportEmbedScene: true, }, files: null, @@ -50,7 +50,7 @@ describe("embedding scene data", () => { elements: sourceElements, appState: { viewBackgroundColor: "#ffffff", - gridSize: null, + gridModeEnabled: false, exportEmbedScene: true, }, files: null,