added notebook grid mode

This commit is contained in:
Nitish Patel 2025-03-02 15:33:58 +05:30
parent 68578556ff
commit ef9f15dfce
15 changed files with 123 additions and 1 deletions

View file

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

View file

@ -18,6 +18,7 @@ export const actionToggleObjectsSnapMode = register({
...appState, ...appState,
objectsSnapModeEnabled: !this.checked!(appState), objectsSnapModeEnabled: !this.checked!(appState),
gridModeEnabled: false, gridModeEnabled: false,
gridModeNotebookEnabled: false,
}, },
captureUpdate: CaptureUpdateAction.EVENTUALLY, captureUpdate: CaptureUpdateAction.EVENTUALLY,
}; };

View file

@ -78,6 +78,7 @@ export {
} from "./actionClipboard"; } from "./actionClipboard";
export { actionToggleGridMode } from "./actionToggleGridMode"; export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleGridNotebookMode } from "./actionToggleGridNotebookMode";
export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode"; export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";

View file

@ -26,6 +26,7 @@ export type ShortcutName =
| "group" | "group"
| "ungroup" | "ungroup"
| "gridMode" | "gridMode"
| "gridModeNotebook"
| "zenMode" | "zenMode"
| "objectsSnapMode" | "objectsSnapMode"
| "stats" | "stats"
@ -91,6 +92,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
group: [getShortcutKey("CtrlOrCmd+G")], group: [getShortcutKey("CtrlOrCmd+G")],
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")], ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
gridMode: [getShortcutKey("CtrlOrCmd+'")], gridMode: [getShortcutKey("CtrlOrCmd+'")],
gridModeNotebook: [getShortcutKey("CtrlOrCmd+;")],
zenMode: [getShortcutKey("Alt+Z")], zenMode: [getShortcutKey("Alt+Z")],
objectsSnapMode: [getShortcutKey("Alt+S")], objectsSnapMode: [getShortcutKey("Alt+S")],
stats: [getShortcutKey("Alt+/")], stats: [getShortcutKey("Alt+/")],

View file

@ -55,6 +55,7 @@ export type ActionName =
| "selectAll" | "selectAll"
| "pasteStyles" | "pasteStyles"
| "gridMode" | "gridMode"
| "gridModeNotebook"
| "zenMode" | "zenMode"
| "objectsSnapMode" | "objectsSnapMode"
| "stats" | "stats"

View file

@ -64,6 +64,7 @@ export const getDefaultAppState = (): Omit<
gridSize: DEFAULT_GRID_SIZE, gridSize: DEFAULT_GRID_SIZE,
gridStep: DEFAULT_GRID_STEP, gridStep: DEFAULT_GRID_STEP,
gridModeEnabled: false, gridModeEnabled: false,
gridModeNotebookEnabled: false,
isBindingEnabled: true, isBindingEnabled: true,
defaultSidebarDockedPreference: false, defaultSidebarDockedPreference: false,
isLoading: false, isLoading: false,
@ -184,6 +185,7 @@ const APP_STATE_STORAGE_CONF = (<
gridSize: { browser: true, export: true, server: true }, gridSize: { browser: true, export: true, server: true },
gridStep: { browser: true, export: true, server: true }, gridStep: { browser: true, export: true, server: true },
gridModeEnabled: { 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 }, height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false },
defaultSidebarDockedPreference: { defaultSidebarDockedPreference: {

View file

@ -26,6 +26,7 @@ import {
actionSendBackward, actionSendBackward,
actionSendToBack, actionSendToBack,
actionToggleGridMode, actionToggleGridMode,
actionToggleGridNotebookMode,
actionToggleStats, actionToggleStats,
actionToggleZenMode, actionToggleZenMode,
actionUnbindText, actionUnbindText,
@ -388,6 +389,7 @@ import {
SnapCache, SnapCache,
isGridModeEnabled, isGridModeEnabled,
getGridPoint, getGridPoint,
isGridModeNotebookEnabled,
} from "../snapping"; } from "../snapping";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError"; import BraveMeasureTextError from "./BraveMeasureTextError";
@ -1740,6 +1742,7 @@ class App extends React.Component<AppProps, AppState> {
imageCache: this.imageCache, imageCache: this.imageCache,
isExporting: false, isExporting: false,
renderGrid: isGridModeEnabled(this), renderGrid: isGridModeEnabled(this),
renderGridNotebook: isGridModeNotebookEnabled(this),
canvasBackgroundColor: canvasBackgroundColor:
this.state.viewBackgroundColor, this.state.viewBackgroundColor,
embedsValidationStatus: this.embedsValidationStatus, embedsValidationStatus: this.embedsValidationStatus,
@ -1759,6 +1762,7 @@ class App extends React.Component<AppProps, AppState> {
imageCache: this.imageCache, imageCache: this.imageCache,
isExporting: false, isExporting: false,
renderGrid: false, renderGrid: false,
renderGridNotebook: false,
canvasBackgroundColor: canvasBackgroundColor:
this.state.viewBackgroundColor, this.state.viewBackgroundColor,
embedsValidationStatus: embedsValidationStatus:
@ -10783,6 +10787,7 @@ class App extends React.Component<AppProps, AppState> {
return [ return [
...options, ...options,
actionToggleGridMode, actionToggleGridMode,
actionToggleGridNotebookMode,
actionToggleZenMode, actionToggleZenMode,
actionToggleViewMode, actionToggleViewMode,
actionToggleStats, actionToggleStats,
@ -10800,6 +10805,7 @@ class App extends React.Component<AppProps, AppState> {
actionUnlockAllElements, actionUnlockAllElements,
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionToggleGridMode, actionToggleGridMode,
actionToggleGridNotebookMode,
actionToggleObjectsSnapMode, actionToggleObjectsSnapMode,
actionToggleZenMode, actionToggleZenMode,
actionToggleViewMode, actionToggleViewMode,

View file

@ -287,6 +287,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.toggleGrid")} label={t("labels.toggleGrid")}
shortcuts={[getShortcutKey("CtrlOrCmd+'")]} shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
/> />
<Shortcut
label={t("labels.toggleGridNotebook")}
shortcuts={[getShortcutKey("CtrlOrCmd+;")]}
/>
<Shortcut <Shortcut
label={t("labels.viewMode")} label={t("labels.viewMode")}
shortcuts={[getShortcutKey("Alt+R")]} shortcuts={[getShortcutKey("Alt+R")]}

View file

@ -14,6 +14,7 @@ export const CODES = {
THREE: "Digit3", THREE: "Digit3",
NINE: "Digit9", NINE: "Digit9",
QUOTE: "Quote", QUOTE: "Quote",
SEMICOLON: "Semicolon",
ZERO: "Digit0", ZERO: "Digit0",
SLASH: "Slash", SLASH: "Slash",
C: "KeyC", C: "KeyC",

View file

@ -96,6 +96,7 @@
"ungroup": "Ungroup selection", "ungroup": "Ungroup selection",
"collaborators": "Collaborators", "collaborators": "Collaborators",
"toggleGrid": "Toggle grid", "toggleGrid": "Toggle grid",
"toggleGridNotebook": "Toggle grid notebook",
"addToLibrary": "Add to library", "addToLibrary": "Add to library",
"removeFromLibrary": "Remove from library", "removeFromLibrary": "Remove from library",
"libraryLoadingMessage": "Loading library…", "libraryLoadingMessage": "Loading library…",

View file

@ -108,6 +108,61 @@ const strokeGrid = (
} }
context.restore(); context.restore();
}; };
const strokeNotebookLines = (
context: CanvasRenderingContext2D,
/** Line spacing in pixels */
lineSpacing: number,
/** Determines when to draw a bold line (e.g., every 5th line) */
boldStep: number,
scrollY: number,
zoom: Zoom,
width: number,
height: number
) => {
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 = ( const frameClip = (
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
@ -219,7 +274,7 @@ const _renderStaticScene = ({
return; return;
} }
const { renderGrid = true, isExporting } = renderConfig; const { renderGrid = true, renderGridNotebook=true, isExporting } = renderConfig;
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
canvas, canvas,
@ -251,6 +306,16 @@ const _renderStaticScene = ({
normalizedWidth / appState.zoom.value, normalizedWidth / appState.zoom.value,
normalizedHeight / 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<string>(); const groupsToBeAddedToFrame = new Set<string>();

View file

@ -244,6 +244,7 @@ export const exportToCanvas = async (
canvasBackgroundColor: viewBackgroundColor, canvasBackgroundColor: viewBackgroundColor,
imageCache, imageCache,
renderGrid: false, renderGrid: false,
renderGridNotebook: false,
isExporting: true, isExporting: true,
// empty disables embeddable rendering // empty disables embeddable rendering
embedsValidationStatus: new Map(), embedsValidationStatus: new Map(),

View file

@ -29,6 +29,7 @@ export type StaticCanvasRenderConfig = {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
imageCache: AppClassProperties["imageCache"]; imageCache: AppClassProperties["imageCache"];
renderGrid: boolean; renderGrid: boolean;
renderGridNotebook: boolean;
/** when exporting the behavior is slightly different (e.g. we can't use /** when exporting the behavior is slightly different (e.g. we can't use
CSS filters), and we disable render optimizations for best output */ CSS filters), and we disable render optimizations for best output */
isExporting: boolean; isExporting: boolean;

View file

@ -154,6 +154,8 @@ export class SnapCache {
export const isGridModeEnabled = (app: AppClassProperties): boolean => export const isGridModeEnabled = (app: AppClassProperties): boolean =>
app.props.gridModeEnabled ?? app.state.gridModeEnabled; app.props.gridModeEnabled ?? app.state.gridModeEnabled;
export const isGridModeNotebookEnabled = (app: AppClassProperties): boolean =>
app.props.gridModeNotebookEnabled ?? app.state.gridModeNotebookEnabled;
export const isSnappingEnabled = ({ export const isSnappingEnabled = ({
event, event,

View file

@ -361,6 +361,7 @@ export interface AppState {
gridSize: number; gridSize: number;
gridStep: number; gridStep: number;
gridModeEnabled: boolean; gridModeEnabled: boolean;
gridModeNotebookEnabled: 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) */
@ -538,6 +539,7 @@ export interface ExcalidrawProps {
viewModeEnabled?: boolean; viewModeEnabled?: boolean;
zenModeEnabled?: boolean; zenModeEnabled?: boolean;
gridModeEnabled?: boolean; gridModeEnabled?: boolean;
gridModeNotebookEnabled?: boolean;
objectsSnapModeEnabled?: boolean; objectsSnapModeEnabled?: boolean;
libraryReturnUrl?: string; libraryReturnUrl?: string;
theme?: Theme; theme?: Theme;