feat: split `gridSize` from enabled state & support custom `gridStep` (#8364)

pull/8375/head
David Luzar 6 months ago committed by GitHub
parent 4320a3cf41
commit 3cfcc7b489
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -43,7 +43,7 @@ When saving an Excalidraw scene locally to a file, the JSON file (`.excalidraw`)
// editor state (canvas config, preferences, ...) // editor state (canvas config, preferences, ...)
"appState": { "appState": {
"gridSize": null, "gridSize": 20,
"viewBackgroundColor": "#ffffff" "viewBackgroundColor": "#ffffff"
}, },

@ -105,6 +105,8 @@ export const actionClearCanvas = register({
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
exportEmbedScene: appState.exportEmbedScene, exportEmbedScene: appState.exportEmbedScene,
gridSize: appState.gridSize, gridSize: appState.gridSize,
gridStep: appState.gridStep,
gridModeEnabled: appState.gridModeEnabled,
stats: appState.stats, stats: appState.stats,
pasteDialog: appState.pasteDialog, pasteDialog: appState.pasteDialog,
activeTool: activeTool:
@ -294,7 +296,6 @@ export const zoomToFitBounds = ({
appState.height / commonBoundsHeight, appState.height / commonBoundsHeight,
) * clamp(viewportZoomFactor, 0.1, 1); ) * clamp(viewportZoomFactor, 0.1, 1);
// Apply clamping to newZoomValue to be between 10% and 3000%
newZoomValue = getNormalizedZoom(newZoomValue); newZoomValue = getNormalizedZoom(newZoomValue);
let appStateWidth = appState.width; let appStateWidth = appState.width;

@ -15,7 +15,7 @@ import {
import type { AppState } from "../types"; import type { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding"; import { fixBindingsAfterDuplication } from "../element/binding";
import type { ActionResult } from "./types"; import type { ActionResult } from "./types";
import { GRID_SIZE } from "../constants"; import { DEFAULT_GRID_SIZE } from "../constants";
import { import {
bindTextToShapeAfterDuplication, bindTextToShapeAfterDuplication,
getBoundTextElement, getBoundTextElement,
@ -99,8 +99,8 @@ const duplicateElements = (
groupIdMap, groupIdMap,
element, element,
{ {
x: element.x + GRID_SIZE / 2, x: element.x + DEFAULT_GRID_SIZE / 2,
y: element.y + GRID_SIZE / 2, y: element.y + DEFAULT_GRID_SIZE / 2,
}, },
); );
duplicatedElementsMap.set(newElement.id, newElement); duplicatedElementsMap.set(newElement.id, newElement);

@ -1,6 +1,5 @@
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { GRID_SIZE } from "../constants";
import type { AppState } from "../types"; import type { AppState } from "../types";
import { gridIcon } from "../components/icons"; import { gridIcon } from "../components/icons";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
@ -13,21 +12,21 @@ export const actionToggleGridMode = register({
viewMode: true, viewMode: true,
trackEvent: { trackEvent: {
category: "canvas", category: "canvas",
predicate: (appState) => !appState.gridSize, predicate: (appState) => appState.gridModeEnabled,
}, },
perform(elements, appState) { perform(elements, appState) {
return { return {
appState: { appState: {
...appState, ...appState,
gridSize: this.checked!(appState) ? null : GRID_SIZE, gridModeEnabled: !this.checked!(appState),
objectsSnapModeEnabled: false, objectsSnapModeEnabled: false,
}, },
storeAction: StoreAction.NONE, storeAction: StoreAction.NONE,
}; };
}, },
checked: (appState: AppState) => appState.gridSize !== null, checked: (appState: AppState) => appState.gridModeEnabled,
predicate: (element, appState, props) => { predicate: (element, appState, props) => {
return typeof props.gridModeEnabled === "undefined"; return props.gridModeEnabled === undefined;
}, },
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE,
}); });

@ -17,7 +17,7 @@ export const actionToggleObjectsSnapMode = register({
appState: { appState: {
...appState, ...appState,
objectsSnapModeEnabled: !this.checked!(appState), objectsSnapModeEnabled: !this.checked!(appState),
gridSize: null, gridModeEnabled: false,
}, },
storeAction: StoreAction.NONE, storeAction: StoreAction.NONE,
}; };

@ -5,9 +5,11 @@ import {
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
DEFAULT_GRID_SIZE,
EXPORT_SCALES, EXPORT_SCALES,
STATS_PANELS, STATS_PANELS,
THEME, THEME,
DEFAULT_GRID_STEP,
} from "./constants"; } from "./constants";
import type { AppState, NormalizedZoomValue } from "./types"; import type { AppState, NormalizedZoomValue } from "./types";
@ -59,7 +61,9 @@ export const getDefaultAppState = (): Omit<
exportEmbedScene: false, exportEmbedScene: false,
exportWithDarkMode: false, exportWithDarkMode: false,
fileHandle: null, fileHandle: null,
gridSize: null, gridSize: DEFAULT_GRID_SIZE,
gridStep: DEFAULT_GRID_STEP,
gridModeEnabled: false,
isBindingEnabled: true, isBindingEnabled: true,
defaultSidebarDockedPreference: false, defaultSidebarDockedPreference: false,
isLoading: false, isLoading: false,
@ -174,6 +178,8 @@ const APP_STATE_STORAGE_CONF = (<
exportWithDarkMode: { browser: true, export: false, server: false }, exportWithDarkMode: { browser: true, export: false, server: false },
fileHandle: { browser: false, export: false, server: false }, fileHandle: { browser: false, export: false, server: false },
gridSize: { browser: true, export: true, server: true }, 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 }, height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false },
defaultSidebarDockedPreference: { defaultSidebarDockedPreference: {

@ -60,7 +60,6 @@ import {
ENV, ENV,
EVENT, EVENT,
FRAME_STYLE, FRAME_STYLE,
GRID_SIZE,
IMAGE_MIME_TYPES, IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT, IMAGE_RENDER_TIMEOUT,
isBrave, isBrave,
@ -258,6 +257,7 @@ import type {
UnsubscribeCallback, UnsubscribeCallback,
EmbedsValidationStatus, EmbedsValidationStatus,
ElementsPendingErasure, ElementsPendingErasure,
NullableGridSize,
} from "../types"; } from "../types";
import { import {
debounce, debounce,
@ -661,7 +661,7 @@ class App extends React.Component<AppProps, AppState> {
viewModeEnabled, viewModeEnabled,
zenModeEnabled, zenModeEnabled,
objectsSnapModeEnabled, objectsSnapModeEnabled,
gridSize: gridModeEnabled ? GRID_SIZE : null, gridModeEnabled: gridModeEnabled ?? defaultAppState.gridModeEnabled,
name, name,
width: window.innerWidth, width: window.innerWidth,
height: window.innerHeight, height: window.innerHeight,
@ -812,6 +812,18 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
/**
* 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( private getHTMLIFrameElement(
element: ExcalidrawIframeLikeElement, element: ExcalidrawIframeLikeElement,
): HTMLIFrameElement | undefined { ): HTMLIFrameElement | undefined {
@ -1684,7 +1696,9 @@ class App extends React.Component<AppProps, AppState> {
renderConfig={{ renderConfig={{
imageCache: this.imageCache, imageCache: this.imageCache,
isExporting: false, isExporting: false,
renderGrid: true, renderGrid:
this.props.gridModeEnabled ??
this.state.gridModeEnabled,
canvasBackgroundColor: canvasBackgroundColor:
this.state.viewBackgroundColor, this.state.viewBackgroundColor,
embedsValidationStatus: this.embedsValidationStatus, embedsValidationStatus: this.embedsValidationStatus,
@ -2171,7 +2185,6 @@ class App extends React.Component<AppProps, AppState> {
if (actionResult.appState || editingElement || this.state.contextMenu) { if (actionResult.appState || editingElement || this.state.contextMenu) {
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false; let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false; let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
let gridSize = actionResult?.appState?.gridSize || null;
const theme = const theme =
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT; actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
const name = actionResult?.appState?.name ?? this.state.name; const name = actionResult?.appState?.name ?? this.state.name;
@ -2185,10 +2198,6 @@ class App extends React.Component<AppProps, AppState> {
zenModeEnabled = this.props.zenModeEnabled; zenModeEnabled = this.props.zenModeEnabled;
} }
if (typeof this.props.gridModeEnabled !== "undefined") {
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
}
editingElement = actionResult.appState?.editingElement || null; editingElement = actionResult.appState?.editingElement || null;
// make sure editingElement points to latest element reference // make sure editingElement points to latest element reference
@ -2220,7 +2229,6 @@ class App extends React.Component<AppProps, AppState> {
editingElement, editingElement,
viewModeEnabled, viewModeEnabled,
zenModeEnabled, zenModeEnabled,
gridSize,
theme, theme,
name, name,
errorMessage, errorMessage,
@ -2777,12 +2785,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ theme: this.props.theme }); 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( this.excalidrawContainerRef.current?.classList.toggle(
"theme--dark", "theme--dark",
this.state.theme === THEME.DARK, this.state.theme === THEME.DARK,
@ -3185,7 +3187,7 @@ class App extends React.Component<AppProps, AppState> {
const dx = x - elementsCenterX; const dx = x - elementsCenterX;
const dy = y - elementsCenterY; const dy = y - elementsCenterY;
const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize); const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
const newElements = duplicateElements( const newElements = duplicateElements(
elements.map((element) => { elements.map((element) => {
@ -3570,7 +3572,10 @@ class App extends React.Component<AppProps, AppState> {
* Zooms on canvas viewport center * Zooms on canvas viewport center
*/ */
zoomCanvas = ( 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, value: number,
) => { ) => {
this.setState({ this.setState({
@ -4148,10 +4153,10 @@ class App extends React.Component<AppProps, AppState> {
? elbowArrow.startBinding || elbowArrow.endBinding ? elbowArrow.startBinding || elbowArrow.endBinding
? 0 ? 0
: ELEMENT_TRANSLATE_AMOUNT : ELEMENT_TRANSLATE_AMOUNT
: (this.state.gridSize && : (this.getEffectiveGridSize() &&
(event.shiftKey (event.shiftKey
? ELEMENT_TRANSLATE_AMOUNT ? ELEMENT_TRANSLATE_AMOUNT
: this.state.gridSize)) || : this.getEffectiveGridSize())) ||
(event.shiftKey (event.shiftKey
? ELEMENT_SHIFT_TRANSLATE_AMOUNT ? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT); : ELEMENT_TRANSLATE_AMOUNT);
@ -5496,7 +5501,7 @@ class App extends React.Component<AppProps, AppState> {
event, event,
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
this.state, this,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
); );
@ -5586,7 +5591,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerY, scenePointerY,
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement) event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement)
? null ? null
: this.state.gridSize, : this.getEffectiveGridSize(),
); );
const [lastCommittedX, lastCommittedY] = const [lastCommittedX, lastCommittedY] =
@ -6553,7 +6558,7 @@ class App extends React.Component<AppProps, AppState> {
origin.y, origin.y,
event[KEYS.CTRL_OR_CMD] || isElbowArrowOnly event[KEYS.CTRL_OR_CMD] || isElbowArrowOnly
? null ? null
: this.state.gridSize, : this.getEffectiveGridSize(),
), ),
), ),
scrollbars: isOverScrollBars( scrollbars: isOverScrollBars(
@ -6730,7 +6735,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.editingLinearElement || this.state.selectedLinearElement; this.state.editingLinearElement || this.state.selectedLinearElement;
const ret = LinearElementEditor.handlePointerDown( const ret = LinearElementEditor.handlePointerDown(
event, event,
this.state, this,
this.store, this.store,
pointerDownState.origin, pointerDownState.origin,
linearElementEditor, linearElementEditor,
@ -7093,7 +7098,7 @@ class App extends React.Component<AppProps, AppState> {
sceneY, sceneY,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
? null ? null
: this.state.gridSize, : this.getEffectiveGridSize(),
); );
const element = newIframeElement({ const element = newIframeElement({
@ -7133,7 +7138,7 @@ class App extends React.Component<AppProps, AppState> {
sceneY, sceneY,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
? null ? null
: this.state.gridSize, : this.getEffectiveGridSize(),
); );
const embedLink = getEmbedLink(link); const embedLink = getEmbedLink(link);
@ -7186,7 +7191,7 @@ class App extends React.Component<AppProps, AppState> {
sceneY, sceneY,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
? null ? null
: this.state.gridSize, : this.getEffectiveGridSize(),
); );
const topLayerFrame = addToFrameUnderCursor const topLayerFrame = addToFrameUnderCursor
@ -7283,7 +7288,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
); );
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
@ -7404,7 +7409,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
? null ? null
: this.state.gridSize, : this.getEffectiveGridSize(),
); );
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
@ -7462,7 +7467,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD]
? null ? null
: this.state.gridSize, : this.getEffectiveGridSize(),
); );
const constructorOpts = { const constructorOpts = {
@ -7598,7 +7603,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
pointerCoords.x, pointerCoords.x,
pointerCoords.y, 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 // for arrows/lines, don't start dragging until a given threshold
@ -7645,7 +7650,7 @@ class App extends React.Component<AppProps, AppState> {
const ret = LinearElementEditor.addMidpoint( const ret = LinearElementEditor.addMidpoint(
this.state.selectedLinearElement, this.state.selectedLinearElement,
pointerCoords, pointerCoords,
this.state, this,
!event[KEYS.CTRL_OR_CMD], !event[KEYS.CTRL_OR_CMD],
elementsMap, elementsMap,
); );
@ -7688,7 +7693,7 @@ class App extends React.Component<AppProps, AppState> {
const didDrag = LinearElementEditor.handlePointDragging( const didDrag = LinearElementEditor.handlePointDragging(
event, event,
this.state, this,
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
(element, pointsSceneCoords) => { (element, pointsSceneCoords) => {
@ -7822,7 +7827,7 @@ class App extends React.Component<AppProps, AppState> {
dragOffset, dragOffset,
this.scene, this.scene,
snapOffset, snapOffset,
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
); );
this.setState({ this.setState({
@ -9794,7 +9799,7 @@ class App extends React.Component<AppProps, AppState> {
let [gridX, gridY] = getGridPoint( let [gridX, gridY] = getGridPoint(
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
); );
const image = const image =
@ -9898,7 +9903,7 @@ class App extends React.Component<AppProps, AppState> {
let [resizeX, resizeY] = getGridPoint( let [resizeX, resizeY] = getGridPoint(
pointerCoords.x - pointerDownState.resize.offset.x, pointerCoords.x - pointerDownState.resize.offset.x,
pointerCoords.y - pointerDownState.resize.offset.y, 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< const frameElementsOffsetsMap = new Map<
@ -9929,7 +9934,7 @@ class App extends React.Component<AppProps, AppState> {
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize, event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
); );
const dragOffset = { const dragOffset = {

@ -360,7 +360,7 @@ const LayerUI = ({
)} )}
{shouldShowStats && ( {shouldShowStats && (
<Stats <Stats
scene={app.scene} app={app}
onClose={() => { onClose={() => {
actionManager.executeAction(actionToggleStats); actionManager.executeAction(actionToggleStats);
}} }}

@ -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<any, AppState>["setState"];
}
const STEP_SIZE = 5;
const CanvasGrid = ({
property,
scene,
appState,
setAppState,
}: PositionProps) => {
return (
<StatsDragInput
label="Grid step"
sensitivity={8}
elements={[]}
dragInputCallback={({
nextValue,
instantChange,
shouldChangeByStepSize,
setInputValue,
}) => {
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;

@ -18,7 +18,8 @@
flex-shrink: 0; flex-shrink: 0;
border: 1px solid var(--default-border-color); border: 1px solid var(--default-border-color);
border-right: 0; border-right: 0;
width: 2rem; padding: 0 0.5rem 0 0.75rem;
min-width: 1rem;
height: 2rem; height: 2rem;
box-sizing: border-box; box-sizing: border-box;
color: var(--popup-text-color); color: var(--popup-text-color);

@ -29,6 +29,7 @@ export type DragInputCallbackType<
nextValue?: number; nextValue?: number;
property: P; property: P;
originalAppState: AppState; originalAppState: AppState;
setInputValue: (value: number) => void;
}) => void; }) => void;
interface StatsDragInputProps< interface StatsDragInputProps<
@ -45,6 +46,8 @@ interface StatsDragInputProps<
property: T; property: T;
scene: Scene; scene: Scene;
appState: AppState; appState: AppState;
/** how many px you need to drag to get 1 unit change */
sensitivity?: number;
} }
const StatsDragInput = < const StatsDragInput = <
@ -61,6 +64,7 @@ const StatsDragInput = <
property, property,
scene, scene,
appState, appState,
sensitivity = 1,
}: StatsDragInputProps<T, E>) => { }: StatsDragInputProps<T, E>) => {
const app = useApp(); const app = useApp();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -126,6 +130,7 @@ const StatsDragInput = <
nextValue: rounded, nextValue: rounded,
property, property,
originalAppState: appState, originalAppState: appState,
setInputValue: (value) => setInputValue(String(value)),
}); });
app.syncActionResult({ storeAction: StoreAction.CAPTURE }); app.syncActionResult({ storeAction: StoreAction.CAPTURE });
} }
@ -172,6 +177,8 @@ const StatsDragInput = <
ref={labelRef} ref={labelRef}
onPointerDown={(event) => { onPointerDown={(event) => {
if (inputRef.current && editable) { if (inputRef.current && editable) {
document.body.classList.add("excalidraw-cursor-resize");
let startValue = Number(inputRef.current.value); let startValue = Number(inputRef.current.value);
if (isNaN(startValue)) { if (isNaN(startValue)) {
startValue = 0; startValue = 0;
@ -196,35 +203,43 @@ const StatsDragInput = <
const originalAppState: AppState = cloneJSON(appState); const originalAppState: AppState = cloneJSON(appState);
let accumulatedChange: number | null = null; let accumulatedChange = 0;
let stepChange = 0;
document.body.classList.add("excalidraw-cursor-resize");
const onPointerMove = (event: PointerEvent) => { const onPointerMove = (event: PointerEvent) => {
if (!accumulatedChange) {
accumulatedChange = 0;
}
if ( if (
lastPointer && lastPointer &&
originalElementsMap !== null && originalElementsMap !== null &&
originalElements !== null && originalElements !== null
accumulatedChange !== null
) { ) {
const instantChange = event.clientX - lastPointer.x; const instantChange = event.clientX - lastPointer.x;
accumulatedChange += instantChange;
if (instantChange !== 0) {
dragInputCallback({ stepChange += instantChange;
accumulatedChange,
instantChange, if (Math.abs(stepChange) >= sensitivity) {
originalElements, stepChange =
originalElementsMap, Math.sign(stepChange) *
shouldKeepAspectRatio: shouldKeepAspectRatio!!, Math.floor(Math.abs(stepChange) / sensitivity);
shouldChangeByStepSize: event.shiftKey,
property, accumulatedChange += stepChange;
scene,
originalAppState, dragInputCallback({
}); accumulatedChange,
instantChange: stepChange,
originalElements,
originalElementsMap,
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
shouldChangeByStepSize: event.shiftKey,
property,
scene,
originalAppState,
setInputValue: (value) => setInputValue(String(value)),
});
stepChange = 0;
}
}
} }
lastPointer = { lastPointer = {
@ -246,7 +261,8 @@ const StatsDragInput = <
app.syncActionResult({ storeAction: StoreAction.CAPTURE }); app.syncActionResult({ storeAction: StoreAction.CAPTURE });
lastPointer = null; lastPointer = null;
accumulatedChange = null; accumulatedChange = 0;
stepChange = 0;
originalElements = null; originalElements = null;
originalElementsMap = null; originalElementsMap = null;

@ -2,7 +2,11 @@ import { useEffect, useMemo, useState, memo } from "react";
import { getCommonBounds } from "../../element/bounds"; import { getCommonBounds } from "../../element/bounds";
import type { NonDeletedExcalidrawElement } from "../../element/types"; import type { NonDeletedExcalidrawElement } from "../../element/types";
import { t } from "../../i18n"; import { t } from "../../i18n";
import type { AppState, ExcalidrawProps } from "../../types"; import type {
AppClassProperties,
AppState,
ExcalidrawProps,
} from "../../types";
import { CloseIcon } from "../icons"; import { CloseIcon } from "../icons";
import { Island } from "../Island"; import { Island } from "../Island";
import { throttle } from "lodash"; import { throttle } from "lodash";
@ -16,17 +20,17 @@ import MultiFontSize from "./MultiFontSize";
import Position from "./Position"; import Position from "./Position";
import MultiPosition from "./MultiPosition"; import MultiPosition from "./MultiPosition";
import Collapsible from "./Collapsible"; import Collapsible from "./Collapsible";
import type Scene from "../../scene/Scene";
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App"; import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
import { getAtomicUnits } from "./utils"; import { getAtomicUnits } from "./utils";
import { STATS_PANELS } from "../../constants"; import { STATS_PANELS } from "../../constants";
import { isElbowArrow } from "../../element/typeChecks"; import { isElbowArrow } from "../../element/typeChecks";
import CanvasGrid from "./CanvasGrid";
import clsx from "clsx"; import clsx from "clsx";
import "./Stats.scss"; import "./Stats.scss";
interface StatsProps { interface StatsProps {
scene: Scene; app: AppClassProperties;
onClose: () => void; onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"]; renderCustomStats: ExcalidrawProps["renderCustomStats"];
} }
@ -35,11 +39,13 @@ const STATS_TIMEOUT = 50;
export const Stats = (props: StatsProps) => { export const Stats = (props: StatsProps) => {
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
const sceneNonce = props.scene.getSceneNonce() || 1; const sceneNonce = props.app.scene.getSceneNonce() || 1;
const selectedElements = props.scene.getSelectedElements({ const selectedElements = props.app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds, selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: false, includeBoundTextElement: false,
}); });
const gridModeEnabled =
props.app.props.gridModeEnabled ?? appState.gridModeEnabled;
return ( return (
<StatsInner <StatsInner
@ -47,6 +53,7 @@ export const Stats = (props: StatsProps) => {
appState={appState} appState={appState}
sceneNonce={sceneNonce} sceneNonce={sceneNonce}
selectedElements={selectedElements} selectedElements={selectedElements}
gridModeEnabled={gridModeEnabled}
/> />
); );
}; };
@ -97,17 +104,20 @@ Stats.StatsRows = StatsRows;
export const StatsInner = memo( export const StatsInner = memo(
({ ({
scene, app,
onClose, onClose,
renderCustomStats, renderCustomStats,
selectedElements, selectedElements,
appState, appState,
sceneNonce, sceneNonce,
gridModeEnabled,
}: StatsProps & { }: StatsProps & {
sceneNonce: number; sceneNonce: number;
selectedElements: readonly NonDeletedExcalidrawElement[]; selectedElements: readonly NonDeletedExcalidrawElement[];
appState: AppState; appState: AppState;
gridModeEnabled: boolean;
}) => { }) => {
const scene = app.scene;
const elements = scene.getNonDeletedElements(); const elements = scene.getNonDeletedElements();
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const setAppState = useExcalidrawSetAppState(); const setAppState = useExcalidrawSetAppState();
@ -189,6 +199,19 @@ export const StatsInner = memo(
<div>{t("stats.height")}</div> <div>{t("stats.height")}</div>
<div>{sceneDimension.height}</div> <div>{sceneDimension.height}</div>
</StatsRow> </StatsRow>
{gridModeEnabled && (
<>
<StatsRow heading>Canvas</StatsRow>
<StatsRow>
<CanvasGrid
property="gridStep"
scene={scene}
appState={appState}
setAppState={setAppState}
/>
</StatsRow>
</>
)}
</StatsRows> </StatsRows>
{renderCustomStats?.(elements, appState)} {renderCustomStats?.(elements, appState)}
@ -362,7 +385,9 @@ export const StatsInner = memo(
return ( return (
prev.sceneNonce === next.sceneNonce && prev.sceneNonce === next.sceneNonce &&
prev.selectedElements === next.selectedElements && 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
); );
}, },
); );

@ -41,7 +41,8 @@ export type StatsInputProperty =
| "width" | "width"
| "height" | "height"
| "angle" | "angle"
| "fontSize"; | "fontSize"
| "gridStep";
export const SMALLEST_DELTA = 0.01; export const SMALLEST_DELTA = 0.01;

@ -101,6 +101,7 @@ const getRelevantAppStateProps = (
exportScale: appState.exportScale, exportScale: appState.exportScale,
selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged, selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged,
gridSize: appState.gridSize, gridSize: appState.gridSize,
gridStep: appState.gridStep,
frameRendering: appState.frameRendering, frameRendering: appState.frameRendering,
selectedElementIds: appState.selectedElementIds, selectedElementIds: appState.selectedElementIds,
frameToHighlight: appState.frameToHighlight, frameToHighlight: appState.frameToHighlight,

@ -179,7 +179,8 @@ export const COLOR_VOICE_CALL = "#a2f1a6";
export const CANVAS_ONLY_ACTIONS = ["selectAll"]; 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 = { export const IMAGE_MIME_TYPES = {
svg: "image/svg+xml", svg: "image/svg+xml",
@ -234,7 +235,7 @@ export const VERSION_TIMEOUT = 30000;
export const SCROLL_TIMEOUT = 100; export const SCROLL_TIMEOUT = 100;
export const ZOOM_STEP = 0.1; export const ZOOM_STEP = 0.1;
export const MIN_ZOOM = 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; export const HYPERLINK_TOOLTIP_DELAY = 300;
// Report a user inactive after IDLE_THRESHOLD milliseconds // Report a user inactive after IDLE_THRESHOLD milliseconds

@ -10,12 +10,7 @@ import type {
PointBinding, PointBinding,
StrokeRoundness, StrokeRoundness,
} from "../element/types"; } from "../element/types";
import type { import type { AppState, BinaryFiles, LibraryItem } from "../types";
AppState,
BinaryFiles,
LibraryItem,
NormalizedZoomValue,
} from "../types";
import type { ImportedDataState, LegacyAppState } from "./types"; import type { ImportedDataState, LegacyAppState } from "./types";
import { import {
getNonDeletedElements, getNonDeletedElements,
@ -39,11 +34,17 @@ import {
ROUNDNESS, ROUNDNESS,
DEFAULT_SIDEBAR, DEFAULT_SIDEBAR,
DEFAULT_ELEMENT_PROPS, DEFAULT_ELEMENT_PROPS,
DEFAULT_GRID_SIZE,
DEFAULT_GRID_STEP,
} from "../constants"; } from "../constants";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement"; import { bumpVersion } from "../element/mutateElement";
import { getUpdatedTimestamp, updateActiveTool } from "../utils"; import {
getUpdatedTimestamp,
isFiniteNumber,
updateActiveTool,
} from "../utils";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import type { MarkOptional, Mutable } from "../utility-types"; import type { MarkOptional, Mutable } from "../utility-types";
import { detectLineHeight, getContainerElement } from "../element/textElement"; import { detectLineHeight, getContainerElement } from "../element/textElement";
@ -52,6 +53,11 @@ import { syncInvalidIndices } from "../fractionalIndex";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
import { getLineHeight } from "../fonts"; import { getLineHeight } from "../fonts";
import { normalizeFixedPoint } from "../element/binding"; import { normalizeFixedPoint } from "../element/binding";
import {
getNormalizedGridSize,
getNormalizedGridStep,
getNormalizedZoom,
} from "../scene";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@ -614,19 +620,24 @@ export const restoreAppState = (
locked: nextAppState.activeTool.locked ?? false, locked: nextAppState.activeTool.locked ?? false,
}, },
// Migrates from previous version where appState.zoom was a number // Migrates from previous version where appState.zoom was a number
zoom: zoom: {
typeof appState.zoom === "number" value: getNormalizedZoom(
? { isFiniteNumber(appState.zoom)
value: appState.zoom as NormalizedZoomValue, ? appState.zoom
} : appState.zoom?.value ?? defaultAppState.zoom.value,
: appState.zoom?.value ),
? appState.zoom },
: defaultAppState.zoom,
openSidebar: openSidebar:
// string (legacy) // string (legacy)
typeof (appState.openSidebar as any as string) === "string" typeof (appState.openSidebar as any as string) === "string"
? { name: DEFAULT_SIDEBAR.name } ? { name: DEFAULT_SIDEBAR.name }
: nextAppState.openSidebar, : nextAppState.openSidebar,
gridSize: getNormalizedGridSize(
isFiniteNumber(appState.gridSize) ? appState.gridSize : DEFAULT_GRID_SIZE,
),
gridStep: getNormalizedGridStep(
isFiniteNumber(appState.gridStep) ? appState.gridStep : DEFAULT_GRID_STEP,
),
}; };
}; };

@ -4,7 +4,12 @@ import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers"; import { getPerfectElementSize } from "./sizeHelpers";
import type { NonDeletedExcalidrawElement } from "./types"; 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 { getBoundTextElement, getMinTextElementWidth } from "./textElement";
import { getGridPoint } from "../math"; import { getGridPoint } from "../math";
import type Scene from "../scene/Scene"; import type Scene from "../scene/Scene";
@ -26,7 +31,7 @@ export const dragSelectedElements = (
x: number; x: number;
y: number; y: number;
}, },
gridSize: AppState["gridSize"], gridSize: NullableGridSize,
) => { ) => {
if ( if (
_selectedElements.length === 1 && _selectedElements.length === 1 &&
@ -101,7 +106,7 @@ const calculateOffset = (
commonBounds: Bounds, commonBounds: Bounds,
dragOffset: { x: number; y: number }, dragOffset: { x: number; y: number },
snapOffset: { x: number; y: number }, snapOffset: { x: number; y: number },
gridSize: AppState["gridSize"], gridSize: NullableGridSize,
): { x: number; y: number } => { ): { x: number; y: number } => {
const [x, y] = commonBounds; const [x, y] = commonBounds;
let nextX = x + dragOffset.x + snapOffset.x; let nextX = x + dragOffset.x + snapOffset.x;

@ -36,6 +36,8 @@ import type {
AppState, AppState,
PointerCoords, PointerCoords,
InteractiveCanvasAppState, InteractiveCanvasAppState,
AppClassProperties,
NullableGridSize,
} from "../types"; } from "../types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
@ -209,7 +211,7 @@ export class LinearElementEditor {
/** @returns whether point was dragged */ /** @returns whether point was dragged */
static handlePointDragging( static handlePointDragging(
event: PointerEvent, event: PointerEvent,
appState: AppState, app: AppClassProperties,
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
maybeSuggestBinding: ( maybeSuggestBinding: (
@ -279,7 +281,7 @@ export class LinearElementEditor {
elementsMap, elementsMap,
referencePoint, referencePoint,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
); );
LinearElementEditor.movePoints( LinearElementEditor.movePoints(
@ -299,7 +301,7 @@ export class LinearElementEditor {
elementsMap, elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y, 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]; const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
@ -315,7 +317,7 @@ export class LinearElementEditor {
elementsMap, elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y, 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, element.points[pointIndex][0] + deltaX,
@ -695,7 +697,7 @@ export class LinearElementEditor {
static handlePointerDown( static handlePointerDown(
event: React.PointerEvent<HTMLElement>, event: React.PointerEvent<HTMLElement>,
appState: AppState, app: AppClassProperties,
store: Store, store: Store,
scenePointer: { x: number; y: number }, scenePointer: { x: number; y: number },
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
@ -705,6 +707,7 @@ export class LinearElementEditor {
hitElement: NonDeleted<ExcalidrawElement> | null; hitElement: NonDeleted<ExcalidrawElement> | null;
linearElementEditor: LinearElementEditor | null; linearElementEditor: LinearElementEditor | null;
} { } {
const appState = app.state;
const elementsMap = scene.getNonDeletedElementsMap(); const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements(); const elements = scene.getNonDeletedElements();
@ -752,7 +755,7 @@ export class LinearElementEditor {
elementsMap, elementsMap,
scenePointer.x, scenePointer.x,
scenePointer.y, 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<HTMLCanvasElement>, event: React.PointerEvent<HTMLCanvasElement>,
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
appState: AppState, app: AppClassProperties,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap, elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
): LinearElementEditor | null { ): LinearElementEditor | null {
const appState = app.state;
if (!appState.editingLinearElement) { if (!appState.editingLinearElement) {
return null; return null;
} }
@ -915,7 +919,7 @@ export class LinearElementEditor {
elementsMap, elementsMap,
lastCommittedPoint, lastCommittedPoint,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : app.getEffectiveGridSize(),
); );
newPoint = [ newPoint = [
@ -930,7 +934,7 @@ export class LinearElementEditor {
scenePointerY - appState.editingLinearElement.pointerOffset.y, scenePointerY - appState.editingLinearElement.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element) event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
? null ? null
: appState.gridSize, : app.getEffectiveGridSize(),
); );
} }
@ -1065,7 +1069,7 @@ export class LinearElementEditor {
elementsMap: ElementsMap, elementsMap: ElementsMap,
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
gridSize: number | null, gridSize: NullableGridSize,
): Point { ): Point {
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize); const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
@ -1363,7 +1367,7 @@ export class LinearElementEditor {
static addMidpoint( static addMidpoint(
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
pointerCoords: PointerCoords, pointerCoords: PointerCoords,
appState: AppState, app: AppClassProperties,
snapToGrid: boolean, snapToGrid: boolean,
elementsMap: ElementsMap, elementsMap: ElementsMap,
) { ) {
@ -1388,7 +1392,7 @@ export class LinearElementEditor {
elementsMap, elementsMap,
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
snapToGrid && !isElbowArrow(element) ? appState.gridSize : null, snapToGrid && !isElbowArrow(element) ? app.getEffectiveGridSize() : null,
); );
const points = [ const points = [
...element.points.slice(0, segmentMidpoint.index!), ...element.points.slice(0, segmentMidpoint.index!),
@ -1485,7 +1489,7 @@ export class LinearElementEditor {
elementsMap: ElementsMap, elementsMap: ElementsMap,
referencePoint: Point, referencePoint: Point,
scenePointer: Point, scenePointer: Point,
gridSize: number | null, gridSize: NullableGridSize,
) { ) {
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates( const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
element, element,

@ -1,4 +1,9 @@
import type { NormalizedZoomValue, Point, Zoom } from "./types"; import type {
NormalizedZoomValue,
NullableGridSize,
Point,
Zoom,
} from "./types";
import { import {
DEFAULT_ADAPTIVE_RADIUS, DEFAULT_ADAPTIVE_RADIUS,
LINE_CONFIRM_THRESHOLD, LINE_CONFIRM_THRESHOLD,
@ -275,7 +280,7 @@ const doSegmentsIntersect = (p1: Point, q1: Point, p2: Point, q2: Point) => {
export const getGridPoint = ( export const getGridPoint = (
x: number, x: number,
y: number, y: number,
gridSize: number | null, gridSize: NullableGridSize,
): [number, number] => { ): [number, number] => {
if (gridSize) { if (gridSize) {
return [ return [
@ -703,3 +708,8 @@ export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
export const clamp = (value: number, min: number, max: number) => { export const clamp = (value: number, min: number, max: number) => {
return Math.min(Math.max(value, min), max); 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;
};

@ -31,53 +31,77 @@ import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
import { throttleRAF } from "../utils"; import { throttleRAF } from "../utils";
import { getBoundTextElement } from "../element/textElement"; import { getBoundTextElement } from "../element/textElement";
const GridLineColor = {
Bold: "#dddddd",
Regular: "#e5e5e5",
} as const;
const strokeGrid = ( const strokeGrid = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
/** grid cell pixel size */
gridSize: number, gridSize: number,
/** setting to 1 will disble bold lines */
gridStep: number,
scrollX: number, scrollX: number,
scrollY: number, scrollY: number,
zoom: Zoom, zoom: Zoom,
width: number, width: number,
height: number, height: number,
) => { ) => {
const BOLD_LINE_FREQUENCY = 5; const offsetX = (scrollX % gridSize) - gridSize;
const offsetY = (scrollY % gridSize) - gridSize;
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 lineWidth = Math.min(1 / zoom.value, 1); const actualGridSize = gridSize * zoom.value;
const spaceWidth = 1 / zoom.value; const spaceWidth = 1 / zoom.value;
const lineDash = [lineWidth * 3, spaceWidth + (lineWidth + spaceWidth)];
context.save(); 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) { for (let x = offsetX; x < offsetX + width + gridSize * 2; x += gridSize) {
const isBold = 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.beginPath();
context.setLineDash(isBold ? [] : lineDash); context.setLineDash(isBold ? [] : lineDash);
context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular; context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular;
context.moveTo(x, offsetY - gridSize); context.moveTo(x, offsetY - gridSize);
context.lineTo(x, offsetY + height + gridSize * 2); context.lineTo(x, Math.ceil(offsetY + height + gridSize * 2));
context.stroke(); context.stroke();
} }
for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) { for (let y = offsetY; y < offsetY + height + gridSize * 2; y += gridSize) {
const isBold = 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.beginPath();
context.setLineDash(isBold ? [] : lineDash); context.setLineDash(isBold ? [] : lineDash);
context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular; context.strokeStyle = isBold ? GridLineColor.Bold : GridLineColor.Regular;
context.moveTo(offsetX - gridSize, y); context.moveTo(offsetX - gridSize, y);
context.lineTo(offsetX + width + gridSize * 2, y); context.lineTo(Math.ceil(offsetX + width + gridSize * 2), y);
context.stroke(); context.stroke();
} }
context.restore(); context.restore();
@ -199,10 +223,11 @@ const _renderStaticScene = ({
context.scale(appState.zoom.value, appState.zoom.value); context.scale(appState.zoom.value, appState.zoom.value);
// Grid // Grid
if (renderGrid && appState.gridSize) { if (renderGrid) {
strokeGrid( strokeGrid(
context, context,
appState.gridSize, appState.gridSize,
appState.gridStep,
appState.scrollX, appState.scrollX,
appState.scrollY, appState.scrollY,
appState.zoom, appState.zoom,

@ -15,4 +15,8 @@ export {
getElementAtPosition, getElementAtPosition,
getElementsAtPosition, getElementsAtPosition,
} from "./comparisons"; } from "./comparisons";
export { getNormalizedZoom } from "./zoom"; export {
getNormalizedZoom,
getNormalizedGridSize,
getNormalizedGridStep,
} from "./normalize";

@ -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);
};

@ -1,10 +1,5 @@
import { MIN_ZOOM } from "../constants";
import type { AppState, NormalizedZoomValue } from "../types"; 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 = ( export const getStateForZoom = (
{ {
viewportX, viewportX,

@ -831,7 +831,9 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1034,7 +1036,9 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1247,7 +1251,9 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1575,7 +1581,9 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1903,7 +1911,9 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2116,7 +2126,9 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2353,7 +2365,9 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2651,7 +2665,9 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3017,7 +3033,9 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3489,7 +3507,9 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3809,7 +3829,9 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -4129,7 +4151,9 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5312,7 +5336,9 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -6436,7 +6462,9 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7368,7 +7396,9 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -8277,7 +8307,9 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9168,7 +9200,9 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 100, "height": 100,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,

@ -48,7 +48,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -647,7 +649,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1150,7 +1154,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1515,7 +1521,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1881,7 +1889,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2145,7 +2155,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2582,7 +2594,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2878,7 +2892,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3159,7 +3175,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3450,7 +3468,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3733,7 +3753,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3965,7 +3987,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -4221,7 +4245,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -4491,7 +4517,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -4719,7 +4747,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -4947,7 +4977,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5173,7 +5205,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5399,7 +5433,9 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5655,7 +5691,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5983,7 +6021,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -6405,7 +6445,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -6780,7 +6822,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7096,7 +7140,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7391,7 +7437,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7617,7 +7665,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7969,7 +8019,9 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -8321,7 +8373,9 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -8722,7 +8776,9 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9006,7 +9062,9 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9268,7 +9326,9 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9529,7 +9589,9 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9757,7 +9819,9 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -10055,7 +10119,9 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -10392,7 +10458,9 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -10624,7 +10692,9 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -11074,7 +11144,9 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -11325,7 +11397,9 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -11561,7 +11635,9 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -11799,7 +11875,9 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -12197,7 +12275,9 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -12441,7 +12521,9 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -12679,7 +12761,9 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -12917,7 +13001,9 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -13161,7 +13247,9 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -13490,7 +13578,9 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -13659,7 +13749,9 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -13944,7 +14036,9 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -14208,7 +14302,9 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -14480,7 +14576,9 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -14638,7 +14736,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -15331,7 +15431,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -15948,7 +16050,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -16565,7 +16669,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -17274,7 +17380,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -18021,7 +18129,9 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -18492,7 +18602,9 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -19011,7 +19123,9 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -19464,7 +19578,9 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 0, "height": 0,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,

@ -48,7 +48,9 @@ exports[`given element A and group of elements B and given both are selected whe
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -457,7 +459,9 @@ exports[`given element A and group of elements B and given both are selected whe
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -857,7 +861,9 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": false, "isBindingEnabled": false,
"isLoading": false, "isLoading": false,
@ -1396,7 +1402,9 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1594,7 +1602,9 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -1963,7 +1973,9 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2197,7 +2209,9 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2371,7 +2385,9 @@ exports[`regression tests > can drag element that covers another element, while
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2685,7 +2701,9 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -2925,7 +2943,9 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3162,7 +3182,9 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3386,7 +3408,9 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3636,7 +3660,9 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -3941,7 +3967,9 @@ exports[`regression tests > deleting last but one element in editing group shoul
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -4349,7 +4377,9 @@ exports[`regression tests > deselects group of selected elements on pointer down
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -4626,7 +4656,9 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -4873,7 +4905,9 @@ exports[`regression tests > deselects selected element on pointer down when poin
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5077,7 +5111,9 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5270,7 +5306,9 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5646,7 +5684,9 @@ exports[`regression tests > drags selected elements from point inside common bou
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -5930,7 +5970,9 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -6732,7 +6774,9 @@ exports[`regression tests > given a group of selected elements with an element t
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7056,7 +7100,9 @@ exports[`regression tests > given a selected element A and a not selected elemen
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7326,7 +7372,9 @@ exports[`regression tests > given selected element A with lower z-index than uns
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7554,7 +7602,9 @@ exports[`regression tests > given selected element A with lower z-index than uns
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7785,7 +7835,9 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -7959,7 +8011,9 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -8133,7 +8187,9 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -8307,7 +8363,9 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -8523,7 +8581,9 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -8738,7 +8798,9 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -8926,7 +8988,9 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9142,7 +9206,9 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9316,7 +9382,9 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9531,7 +9599,9 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9705,7 +9775,9 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -9893,7 +9965,9 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -10067,7 +10141,9 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -10575,7 +10651,9 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -10846,7 +10924,9 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -10966,7 +11046,9 @@ exports[`regression tests > shift click on selected element should deselect it o
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -11159,7 +11241,9 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -11464,7 +11548,9 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -11870,7 +11956,9 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -12477,7 +12565,9 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -12600,7 +12690,9 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -13178,7 +13270,9 @@ exports[`regression tests > switches from group of selected elements to another
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -13510,7 +13604,9 @@ exports[`regression tests > switches selected element on pointer down > [end of
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -13769,7 +13865,9 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -13889,7 +13987,9 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -14262,7 +14362,9 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
@ -14382,7 +14484,9 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"height": 768, "height": 768,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,

@ -2,7 +2,7 @@ import React from "react";
import { fireEvent, GlobalTestState, toggleMenu, render } from "./test-utils"; import { fireEvent, GlobalTestState, toggleMenu, render } from "./test-utils";
import { Excalidraw, Footer, MainMenu } from "../index"; import { Excalidraw, Footer, MainMenu } from "../index";
import { queryByText, queryByTestId } from "@testing-library/react"; import { queryByText, queryByTestId } from "@testing-library/react";
import { GRID_SIZE, THEME } from "../constants"; import { THEME } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import { useMemo } from "react"; import { useMemo } from "react";
@ -91,7 +91,7 @@ describe("<Excalidraw/>", () => {
describe("Test gridModeEnabled prop", () => { describe("Test gridModeEnabled prop", () => {
it('should show grid mode in context menu when gridModeEnabled is "undefined"', async () => { it('should show grid mode in context menu when gridModeEnabled is "undefined"', async () => {
const { container } = await render(<Excalidraw />); const { container } = await render(<Excalidraw />);
expect(h.state.gridSize).toBe(null); expect(h.state.gridModeEnabled).toBe(false);
expect( expect(
container.getElementsByClassName("disable-zen-mode--visible").length, container.getElementsByClassName("disable-zen-mode--visible").length,
@ -103,14 +103,14 @@ describe("<Excalidraw/>", () => {
}); });
const contextMenu = document.querySelector(".context-menu"); const contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Toggle grid")!); 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 () => { it('should not show grid mode in context menu when gridModeEnabled is not "undefined"', async () => {
const { container } = await render( const { container } = await render(
<Excalidraw gridModeEnabled={false} />, <Excalidraw gridModeEnabled={false} />,
); );
expect(h.state.gridSize).toBe(null); expect(h.state.gridModeEnabled).toBe(false);
expect( expect(
container.getElementsByClassName("disable-zen-mode--visible").length, container.getElementsByClassName("disable-zen-mode--visible").length,
@ -122,7 +122,7 @@ describe("<Excalidraw/>", () => {
}); });
const contextMenu = document.querySelector(".context-menu"); const contextMenu = document.querySelector(".context-menu");
expect(queryByText(contextMenu as HTMLElement, "Show grid")).toBe(null); expect(queryByText(contextMenu as HTMLElement, "Show grid")).toBe(null);
expect(h.state.gridSize).toBe(null); expect(h.state.gridModeEnabled).toBe(false);
}); });
}); });

@ -12,7 +12,7 @@ export const diagramFixture = {
elements: [diamondFixture, ellipseFixture, rectangleFixture], elements: [diamondFixture, ellipseFixture, rectangleFixture],
appState: { appState: {
viewBackgroundColor: "#ffffff", viewBackgroundColor: "#ffffff",
gridSize: null, gridModeEnabled: false,
}, },
files: {}, files: {},
}; };

@ -40,7 +40,7 @@ import type { FileSystemHandle } from "./data/filesystem";
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import type { ContextMenuItems } from "./components/ContextMenu"; import type { ContextMenuItems } from "./components/ContextMenu";
import type { SnapLine } from "./snapping"; 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"; import type { StoreActionType } from "./store";
export type Point = Readonly<RoughPoint>; export type Point = Readonly<RoughPoint>;
@ -176,6 +176,7 @@ export type StaticCanvasAppState = Readonly<
exportScale: AppState["exportScale"]; exportScale: AppState["exportScale"];
selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"]; selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"];
gridSize: AppState["gridSize"]; gridSize: AppState["gridSize"];
gridStep: AppState["gridStep"];
frameRendering: AppState["frameRendering"]; frameRendering: AppState["frameRendering"];
currentHoveredFontFamily: AppState["currentHoveredFontFamily"]; currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
} }
@ -351,7 +352,10 @@ export interface AppState {
toast: { message: string; closable?: boolean; duration?: number } | null; toast: { message: string; closable?: boolean; duration?: number } | null;
zenModeEnabled: boolean; zenModeEnabled: boolean;
theme: Theme; theme: Theme;
gridSize: number | null; /** grid cell px size */
gridSize: number;
gridStep: number;
gridModeEnabled: boolean;
viewModeEnabled: boolean; viewModeEnabled: boolean;
/** top-most selected groups (i.e. does not include nested groups) */ /** 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. */ * in the app, eg Manager. Factored out into a separate type to keep DRY. */
export type AppClassProperties = { export type AppClassProperties = {
props: AppProps; props: AppProps;
state: AppState;
interactiveCanvas: HTMLCanvasElement | null; interactiveCanvas: HTMLCanvasElement | null;
/** static canvas */ /** static canvas */
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
@ -649,6 +654,7 @@ export type AppClassProperties = {
getName: App["getName"]; getName: App["getName"];
dismissLinearEditor: App["dismissLinearEditor"]; dismissLinearEditor: App["dismissLinearEditor"];
flowChartCreator: App["flowChartCreator"]; flowChartCreator: App["flowChartCreator"];
getEffectiveGridSize: App["getEffectiveGridSize"];
}; };
export type PointerDownState = Readonly<{ export type PointerDownState = Readonly<{
@ -831,3 +837,8 @@ export type EmbedsValidationStatus = Map<
export type ElementsPendingErasure = Set<ExcalidrawElement["id"]>; export type ElementsPendingErasure = Set<ExcalidrawElement["id"]>;
export type PendingExcalidrawElements = ExcalidrawElement[]; export type PendingExcalidrawElements = ExcalidrawElement[];
/** Runtime gridSize value. Null indicates disabled grid. */
export type NullableGridSize =
| (AppState["gridSize"] & MakeBrand<"NullableGridSize">)
| null;

@ -49,7 +49,9 @@ exports[`exportToSvg > with default arguments 1`] = `
"outline": true, "outline": true,
}, },
"frameToHighlight": null, "frameToHighlight": null,
"gridSize": null, "gridModeEnabled": false,
"gridSize": 20,
"gridStep": 5,
"isBindingEnabled": true, "isBindingEnabled": true,
"isLoading": false, "isLoading": false,
"isResizing": false, "isResizing": false,

@ -19,7 +19,7 @@ describe("embedding scene data", () => {
elements: sourceElements, elements: sourceElements,
appState: { appState: {
viewBackgroundColor: "#ffffff", viewBackgroundColor: "#ffffff",
gridSize: null, gridModeEnabled: false,
exportEmbedScene: true, exportEmbedScene: true,
}, },
files: null, files: null,
@ -50,7 +50,7 @@ describe("embedding scene data", () => {
elements: sourceElements, elements: sourceElements,
appState: { appState: {
viewBackgroundColor: "#ffffff", viewBackgroundColor: "#ffffff",
gridSize: null, gridModeEnabled: false,
exportEmbedScene: true, exportEmbedScene: true,
}, },
files: null, files: null,

Loading…
Cancel
Save