diff --git a/packages/excalidraw/actions/actionToggleGridNotebookMode.tsx b/packages/excalidraw/actions/actionToggleGridNotebookMode.tsx new file mode 100644 index 000000000..ed618c1f1 --- /dev/null +++ b/packages/excalidraw/actions/actionToggleGridNotebookMode.tsx @@ -0,0 +1,32 @@ +import { CODES, KEYS } from "../keys"; +import { register } from "./register"; +import type { AppState } from "../types"; +import { gridIcon } from "../components/icons"; +import { CaptureUpdateAction } from "../store"; + +export const actionToggleGridNotebookMode = register({ + name: "gridModeNotebook", + icon: gridIcon, + keywords: ["snap"], + label: "labels.toggleGridNotebook", + viewMode: true, + trackEvent: { + category: "canvas", + predicate: (appState) => appState.gridModeNotebookEnabled, + }, + perform(elements, appState) { + return { + appState: { + ...appState, + gridModeNotebookEnabled: !this.checked!(appState), + objectsSnapModeEnabled: false, + }, + captureUpdate: CaptureUpdateAction.EVENTUALLY, + }; + }, + checked: (appState: AppState) => appState.gridModeNotebookEnabled, + predicate: (element, appState, props) => { + return props.gridModeNotebookEnabled === undefined; + }, + keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.SEMICOLON, +}); diff --git a/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx b/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx index d51124132..f0d7eeb65 100644 --- a/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx +++ b/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx @@ -19,6 +19,7 @@ export const actionToggleObjectsSnapMode = register({ ...appState, objectsSnapModeEnabled: !this.checked!(appState), gridModeEnabled: false, + gridModeNotebookEnabled: false, }, captureUpdate: CaptureUpdateAction.EVENTUALLY, }; diff --git a/packages/excalidraw/actions/index.ts b/packages/excalidraw/actions/index.ts index a556bfbea..e8a24ad9f 100644 --- a/packages/excalidraw/actions/index.ts +++ b/packages/excalidraw/actions/index.ts @@ -78,6 +78,7 @@ export { } from "./actionClipboard"; export { actionToggleGridMode } from "./actionToggleGridMode"; +export { actionToggleGridNotebookMode } from "./actionToggleGridNotebookMode"; export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode"; diff --git a/packages/excalidraw/actions/shortcuts.ts b/packages/excalidraw/actions/shortcuts.ts index 89a7c5ae2..d7fc0269c 100644 --- a/packages/excalidraw/actions/shortcuts.ts +++ b/packages/excalidraw/actions/shortcuts.ts @@ -27,6 +27,7 @@ export type ShortcutName = | "group" | "ungroup" | "gridMode" + | "gridModeNotebook" | "zenMode" | "objectsSnapMode" | "stats" @@ -92,6 +93,7 @@ const shortcutMap: Record = { group: [getShortcutKey("CtrlOrCmd+G")], ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")], gridMode: [getShortcutKey("CtrlOrCmd+'")], + gridModeNotebook: [getShortcutKey("CtrlOrCmd+;")], zenMode: [getShortcutKey("Alt+Z")], objectsSnapMode: [getShortcutKey("Alt+S")], stats: [getShortcutKey("Alt+/")], diff --git a/packages/excalidraw/actions/types.ts b/packages/excalidraw/actions/types.ts index c30f53f5e..623dbc206 100644 --- a/packages/excalidraw/actions/types.ts +++ b/packages/excalidraw/actions/types.ts @@ -55,6 +55,7 @@ export type ActionName = | "selectAll" | "pasteStyles" | "gridMode" + | "gridModeNotebook" | "zenMode" | "objectsSnapMode" | "stats" diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index acda8468b..f829bccba 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -65,6 +65,7 @@ export const getDefaultAppState = (): Omit< gridSize: DEFAULT_GRID_SIZE, gridStep: DEFAULT_GRID_STEP, gridModeEnabled: false, + gridModeNotebookEnabled: false, isBindingEnabled: true, defaultSidebarDockedPreference: false, isLoading: false, @@ -185,6 +186,7 @@ const APP_STATE_STORAGE_CONF = (< gridSize: { browser: true, export: true, server: true }, gridStep: { browser: true, export: true, server: true }, gridModeEnabled: { browser: true, export: true, server: true }, + gridModeNotebookEnabled: { browser: true, export: true, server: true }, height: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false }, defaultSidebarDockedPreference: { diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 8239493ba..b2dcaa359 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -42,6 +42,7 @@ import { actionSendBackward, actionSendToBack, actionToggleGridMode, + actionToggleGridNotebookMode, actionToggleStats, actionToggleZenMode, actionUnbindText, @@ -336,6 +337,7 @@ import { SnapCache, isGridModeEnabled, getGridPoint, + isGridModeNotebookEnabled, } from "../snapping"; import { convertToExcalidrawElements } from "../data/transform"; import { Renderer } from "../scene/Renderer"; @@ -1739,6 +1741,7 @@ class App extends React.Component { imageCache: this.imageCache, isExporting: false, renderGrid: isGridModeEnabled(this), + renderGridNotebook: isGridModeNotebookEnabled(this), canvasBackgroundColor: this.state.viewBackgroundColor, embedsValidationStatus: this.embedsValidationStatus, @@ -1758,6 +1761,7 @@ class App extends React.Component { imageCache: this.imageCache, isExporting: false, renderGrid: false, + renderGridNotebook: false, canvasBackgroundColor: this.state.viewBackgroundColor, embedsValidationStatus: @@ -10750,6 +10754,7 @@ class App extends React.Component { return [ ...options, actionToggleGridMode, + actionToggleGridNotebookMode, actionToggleZenMode, actionToggleViewMode, actionToggleStats, @@ -10767,6 +10772,7 @@ class App extends React.Component { actionUnlockAllElements, CONTEXT_MENU_SEPARATOR, actionToggleGridMode, + actionToggleGridNotebookMode, actionToggleObjectsSnapMode, actionToggleZenMode, actionToggleViewMode, diff --git a/packages/excalidraw/components/HelpDialog.tsx b/packages/excalidraw/components/HelpDialog.tsx index 19ecaa57e..72b9e3dfe 100644 --- a/packages/excalidraw/components/HelpDialog.tsx +++ b/packages/excalidraw/components/HelpDialog.tsx @@ -291,6 +291,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { label={t("labels.toggleGrid")} shortcuts={[getShortcutKey("CtrlOrCmd+'")]} /> + { + const offsetY = (scrollY % lineSpacing) - lineSpacing; + const actualLineSpacing = lineSpacing * zoom.value; + + context.save(); + + if (zoom.value === 1) { + context.translate(0, offsetY % 1 ? 0 : 0.5); + } + + for ( + let y = offsetY; + y < offsetY + height + lineSpacing * 2; + y += lineSpacing + ) { + const isBold = boldStep > 1 && Math.round(y - scrollY) % (boldStep * lineSpacing) === 0; + + if (!isBold && actualLineSpacing < 10) { + continue; + } + + const lineWidth = Math.min(1 / zoom.value, isBold ? 2 : 1); + context.lineWidth = lineWidth; + context.setLineDash([]); // Notebook lines are solid + + context.beginPath(); + context.strokeStyle = isBold + ? "rgba(0, 0, 150, 0.2)" + : "rgba(0, 0, 255, 0.1)"; // Slightly darker bold lines + context.moveTo(0, y); + context.lineTo(width, y); + context.stroke(); + } + + // Optional: Add a red left margin line + context.strokeStyle = "rgba(255, 0, 0, 0.3)"; // Light red margin line + context.lineWidth = 1; + context.beginPath(); + context.moveTo(40, 0); // Adjust margin position + context.lineTo(40, height); + context.stroke(); + + context.restore(); +}; + const frameClip = ( frame: ExcalidrawFrameLikeElement, @@ -220,7 +275,7 @@ const _renderStaticScene = ({ return; } - const { renderGrid = true, isExporting } = renderConfig; + const { renderGrid = true, renderGridNotebook=true, isExporting } = renderConfig; const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( canvas, @@ -252,6 +307,16 @@ const _renderStaticScene = ({ normalizedWidth / appState.zoom.value, normalizedHeight / appState.zoom.value, ); + } else if (renderGridNotebook) { + strokeNotebookLines( + context, + appState.gridSize, + appState.gridStep, + appState.scrollY, + appState.zoom, + normalizedWidth / appState.zoom.value, + normalizedHeight / appState.zoom.value, + ); } const groupsToBeAddedToFrame = new Set(); diff --git a/packages/excalidraw/scene/export.ts b/packages/excalidraw/scene/export.ts index 8ca35de2c..7042835b9 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -246,6 +246,7 @@ export const exportToCanvas = async ( canvasBackgroundColor: viewBackgroundColor, imageCache, renderGrid: false, + renderGridNotebook: false, isExporting: true, // empty disables embeddable rendering embedsValidationStatus: new Map(), diff --git a/packages/excalidraw/scene/types.ts b/packages/excalidraw/scene/types.ts index 3c198d153..cfb27777a 100644 --- a/packages/excalidraw/scene/types.ts +++ b/packages/excalidraw/scene/types.ts @@ -29,6 +29,7 @@ export type StaticCanvasRenderConfig = { // --------------------------------------------------------------------------- imageCache: AppClassProperties["imageCache"]; renderGrid: boolean; + renderGridNotebook: boolean; /** when exporting the behavior is slightly different (e.g. we can't use CSS filters), and we disable render optimizations for best output */ isExporting: boolean; diff --git a/packages/excalidraw/snapping.ts b/packages/excalidraw/snapping.ts index 7c85f4112..9885a033c 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -157,6 +157,8 @@ export class SnapCache { export const isGridModeEnabled = (app: AppClassProperties): boolean => app.props.gridModeEnabled ?? app.state.gridModeEnabled; +export const isGridModeNotebookEnabled = (app: AppClassProperties): boolean => + app.props.gridModeNotebookEnabled ?? app.state.gridModeNotebookEnabled; export const isSnappingEnabled = ({ event, diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 64c0ac298..2ff491b40 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -361,6 +361,7 @@ export interface AppState { gridSize: number; gridStep: number; gridModeEnabled: boolean; + gridModeNotebookEnabled: boolean; viewModeEnabled: boolean; /** top-most selected groups (i.e. does not include nested groups) */ @@ -538,6 +539,7 @@ export interface ExcalidrawProps { viewModeEnabled?: boolean; zenModeEnabled?: boolean; gridModeEnabled?: boolean; + gridModeNotebookEnabled?: boolean; objectsSnapModeEnabled?: boolean; libraryReturnUrl?: string; theme?: Theme;