From ef9f15dfcebb404bfcf6b603042bb1220b1d67bf Mon Sep 17 00:00:00 2001 From: Nitish Patel Date: Sun, 2 Mar 2025 15:33:58 +0530 Subject: [PATCH] added notebook grid mode --- .../actions/actionToggleGridNotebookMode.tsx | 32 +++++++++ .../actions/actionToggleObjectsSnapMode.tsx | 1 + packages/excalidraw/actions/index.ts | 1 + packages/excalidraw/actions/shortcuts.ts | 2 + packages/excalidraw/actions/types.ts | 1 + packages/excalidraw/appState.ts | 2 + packages/excalidraw/components/App.tsx | 6 ++ packages/excalidraw/components/HelpDialog.tsx | 4 ++ packages/excalidraw/keys.ts | 1 + packages/excalidraw/locales/en.json | 1 + packages/excalidraw/renderer/staticScene.ts | 67 ++++++++++++++++++- packages/excalidraw/scene/export.ts | 1 + packages/excalidraw/scene/types.ts | 1 + packages/excalidraw/snapping.ts | 2 + packages/excalidraw/types.ts | 2 + 15 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 packages/excalidraw/actions/actionToggleGridNotebookMode.tsx 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 1ae3cbe0b..da4b373fb 100644 --- a/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx +++ b/packages/excalidraw/actions/actionToggleObjectsSnapMode.tsx @@ -18,6 +18,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 451609dff..3fcea1a15 100644 --- a/packages/excalidraw/actions/shortcuts.ts +++ b/packages/excalidraw/actions/shortcuts.ts @@ -26,6 +26,7 @@ export type ShortcutName = | "group" | "ungroup" | "gridMode" + | "gridModeNotebook" | "zenMode" | "objectsSnapMode" | "stats" @@ -91,6 +92,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 71ac9f4ab..4c15a8f1b 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 644949e7c..a0395002f 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -64,6 +64,7 @@ export const getDefaultAppState = (): Omit< gridSize: DEFAULT_GRID_SIZE, gridStep: DEFAULT_GRID_STEP, gridModeEnabled: false, + gridModeNotebookEnabled: false, isBindingEnabled: true, defaultSidebarDockedPreference: false, isLoading: false, @@ -184,6 +185,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 15dabb5fa..8b01e2fcd 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -26,6 +26,7 @@ import { actionSendBackward, actionSendToBack, actionToggleGridMode, + actionToggleGridNotebookMode, actionToggleStats, actionToggleZenMode, actionUnbindText, @@ -388,6 +389,7 @@ import { SnapCache, isGridModeEnabled, getGridPoint, + isGridModeNotebookEnabled, } from "../snapping"; import { actionWrapTextInContainer } from "../actions/actionBoundText"; import BraveMeasureTextError from "./BraveMeasureTextError"; @@ -1740,6 +1742,7 @@ class App extends React.Component { imageCache: this.imageCache, isExporting: false, renderGrid: isGridModeEnabled(this), + renderGridNotebook: isGridModeNotebookEnabled(this), canvasBackgroundColor: this.state.viewBackgroundColor, embedsValidationStatus: this.embedsValidationStatus, @@ -1759,6 +1762,7 @@ class App extends React.Component { imageCache: this.imageCache, isExporting: false, renderGrid: false, + renderGridNotebook: false, canvasBackgroundColor: this.state.viewBackgroundColor, embedsValidationStatus: @@ -10783,6 +10787,7 @@ class App extends React.Component { return [ ...options, actionToggleGridMode, + actionToggleGridNotebookMode, actionToggleZenMode, actionToggleViewMode, actionToggleStats, @@ -10800,6 +10805,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 926096c69..2935f7fde 100644 --- a/packages/excalidraw/components/HelpDialog.tsx +++ b/packages/excalidraw/components/HelpDialog.tsx @@ -287,6 +287,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, @@ -219,7 +274,7 @@ const _renderStaticScene = ({ return; } - const { renderGrid = true, isExporting } = renderConfig; + const { renderGrid = true, renderGridNotebook=true, isExporting } = renderConfig; const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( canvas, @@ -251,6 +306,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 7b3325690..7d779b8dd 100644 --- a/packages/excalidraw/scene/export.ts +++ b/packages/excalidraw/scene/export.ts @@ -244,6 +244,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 c0bfd1bba..50167478d 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 1b661516e..e6656f0d1 100644 --- a/packages/excalidraw/snapping.ts +++ b/packages/excalidraw/snapping.ts @@ -154,6 +154,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 0562736cd..8bc1bbbef 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;