diff --git a/excalidraw-app/vite.config.mts b/excalidraw-app/vite.config.mts index dfb213ef3a..21469ebc94 100644 --- a/excalidraw-app/vite.config.mts +++ b/excalidraw-app/vite.config.mts @@ -82,6 +82,13 @@ export default defineConfig(({ mode }) => { "../packages/fractional-indexing/src/index.ts", ), }, + { + find: /^@excalidraw\/laser-pointer$/, + replacement: path.resolve( + __dirname, + "../packages/laser-pointer/src/index.ts", + ), + }, ], }, build: { diff --git a/package.json b/package.json index df5c4e8a79..c91ee030c3 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,8 @@ "build:excalidraw": "yarn --cwd ./packages/excalidraw build:esm", "build:math": "yarn --cwd ./packages/math build:esm", "build:fractional-indexing": "yarn --cwd ./packages/fractional-indexing build:esm", - "build:packages": "yarn build:common && yarn build:fractional-indexing && yarn build:math && yarn build:element && yarn build:excalidraw", + "build:laser-pointer": "yarn --cwd ./packages/laser-pointer build:esm", + "build:packages": "yarn build:common && yarn build:fractional-indexing && yarn build:laser-pointer && yarn build:math && yarn build:element && yarn build:excalidraw", "build:version": "yarn --cwd ./excalidraw-app build:version", "build": "yarn --cwd ./excalidraw-app build", "build:preview": "yarn --cwd ./excalidraw-app build:preview", diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 0e94df5af6..d13006f446 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -404,11 +404,47 @@ export const ROUGHNESS = { cartoonist: 2, } as const; -export const STROKE_WIDTH = { +export type StrokeWidthKey = "thin" | "medium" | "bold"; + +export const STROKE_WIDTH_KEYS: readonly StrokeWidthKey[] = [ + "thin", + "medium", + "bold", +]; + +export const STROKE_WIDTH: Readonly< + Record +> = { thin: 1, + medium: 2, + bold: 4, + extraBold: 8, // unused (may be introduced in the future) +}; + +// freedraw schema 2.0 uses thinner stroke, but to maintain backwards and +// forwards compatibility, instead of changing the shape renderer, we scale +// the stroke width by 1/2 (previous, thin was 1, medium 2 etc.) +// +// note that in the UI, STROKE_WIDTH.thin == FREEDRAW_STROKE_WIDTH.thin still +export const FREEDRAW_STROKE_WIDTH: Readonly< + Record +> = { + thin: 0.5, + medium: 1, bold: 2, - extraBold: 4, -} as const; + extraBold: 4, // legacy (may be used again in the future) +}; + +export const getStrokeWidthByKey = ( + elementType: ExcalidrawElement["type"], + strokeWidthKey: StrokeWidthKey, +): ExcalidrawElement["strokeWidth"] => { + return elementType === "freedraw" + ? FREEDRAW_STROKE_WIDTH[strokeWidthKey] + : STROKE_WIDTH[strokeWidthKey]; +}; + +export const DEFAULT_ELEMENT_STROKE_WIDTH_KEY: StrokeWidthKey = "medium"; export const DEFAULT_ELEMENT_PROPS: { strokeColor: ExcalidrawElement["strokeColor"]; @@ -423,7 +459,7 @@ export const DEFAULT_ELEMENT_PROPS: { strokeColor: COLOR_PALETTE.black, backgroundColor: COLOR_PALETTE.transparent, fillStyle: "solid", - strokeWidth: 2, + strokeWidth: STROKE_WIDTH[DEFAULT_ELEMENT_STROKE_WIDTH_KEY], strokeStyle: "solid", roughness: ROUGHNESS.artist, opacity: 100, @@ -514,3 +550,6 @@ export const BIND_MODE_TIMEOUT = 700; // ms export const MOBILE_ACTION_BUTTON_BG = { background: "var(--mobile-action-button-bg)", } as const; + +export const DEFAULT_STROKE_STREAMLINE = 0.5; +export const DEFAULT_STROKE_STREAMLINE_PRECISE = 0.3; diff --git a/packages/element/src/comparisons.ts b/packages/element/src/comparisons.ts index 148b2ea62b..12e456f0e3 100644 --- a/packages/element/src/comparisons.ts +++ b/packages/element/src/comparisons.ts @@ -38,6 +38,8 @@ export const hasStrokeStyle = (type: ElementOrToolType) => type === "arrow" || type === "line"; +export const hasFreedrawMode = (type: ElementOrToolType) => type === "freedraw"; + export const canChangeRoundness = (type: ElementOrToolType) => type === "rectangle" || type === "iframe" || diff --git a/packages/element/src/newElement.ts b/packages/element/src/newElement.ts index ec50a81ff2..a2658aabd9 100644 --- a/packages/element/src/newElement.ts +++ b/packages/element/src/newElement.ts @@ -4,6 +4,7 @@ import { DEFAULT_FONT_SIZE, DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, + DEFAULT_STROKE_STREAMLINE, VERTICAL_ALIGN, randomInteger, randomId, @@ -444,6 +445,7 @@ export const newFreeDrawElement = ( type: "freedraw"; points?: ExcalidrawFreeDrawElement["points"]; simulatePressure: boolean; + strokeOptions?: ExcalidrawFreeDrawElement["strokeOptions"]; pressures?: ExcalidrawFreeDrawElement["pressures"]; } & ElementConstructorOpts, ): NonDeleted => { @@ -452,6 +454,10 @@ export const newFreeDrawElement = ( points: opts.points || [], pressures: opts.pressures || [], simulatePressure: opts.simulatePressure, + strokeOptions: opts.strokeOptions ?? { + variability: "variable", + streamline: DEFAULT_STROKE_STREAMLINE, + }, }; }; diff --git a/packages/element/src/shape.ts b/packages/element/src/shape.ts index 3209c4c89d..a0e5f42c69 100644 --- a/packages/element/src/shape.ts +++ b/packages/element/src/shape.ts @@ -1,5 +1,6 @@ import { simplify } from "points-on-curve"; import { getStroke } from "perfect-freehand"; +import { LaserPointer } from "@excalidraw/laser-pointer"; import { type GeometricShape, @@ -24,6 +25,8 @@ import { COLOR_PALETTE, LINE_POLYGON_POINT_MERGE_DISTANCE, applyDarkModeFilter, + DEFAULT_STROKE_STREAMLINE, + DEFAULT_STROKE_STREAMLINE_PRECISE, } from "@excalidraw/common"; import { RoughGenerator } from "roughjs/bin/generator"; @@ -1171,27 +1174,92 @@ const getFreeDrawSvgPath = (element: ExcalidrawFreeDrawElement) => { ) as SVGPathString; }; -export const getFreedrawOutlinePoints = ( +/** + * Freedraw stroke geometry tuning constants. + * + * These factors are not derived analytically — they were tuned empirically by + * visually comparing rendered strokes until they matched the desired feel. + * Treat them as magic numbers backed by visual verification. + */ +const VARIABLE_WIDTH_FREEDRAW = { + /** Stroke size relative to `strokeWidth` for pressure-sensitive strokes. */ + SIZE_FACTOR: 4.25, + THINNING: 0.6, + SMOOTHING: 0.5, + STREAMLINE: DEFAULT_STROKE_STREAMLINE, +} as const; + +const CONSTANT_WIDTH_FREEDRAW = { + /** Stroke size relative to `strokeWidth` for uniform (laser) strokes. */ + SIZE_FACTOR: 1.4, + STREAMLINE: DEFAULT_STROKE_STREAMLINE_PRECISE, +} as const; + +const getFreedrawStreamline = (element: ExcalidrawFreeDrawElement) => + element.strokeOptions?.streamline ?? + (element.strokeOptions?.variability === "constant" + ? CONSTANT_WIDTH_FREEDRAW.STREAMLINE + : VARIABLE_WIDTH_FREEDRAW.STREAMLINE); + +/** + * Pressure-sensitive (variable width) freedraw outline, rendered with + * perfect-freehand. This is the original Excalidraw freedraw look. + */ +const getVariableWidthFreedrawOutline = ( element: ExcalidrawFreeDrawElement, -) => { +): [number, number][] => { // If input points are empty (should they ever be?) return a dot const inputPoints = element.simulatePressure ? element.points : element.points.length - ? element.points.map(([x, y], i) => [x, y, element.pressures[i]]) + ? element.points.map( + ([x, y], i) => [x, y, element.pressures[i]] as [number, number, number], + ) : [[0, 0, 0.5]]; return getStroke(inputPoints as number[][], { simulatePressure: element.simulatePressure, - size: element.strokeWidth * 4.25, - thinning: 0.6, - smoothing: 0.5, - streamline: 0.5, + size: element.strokeWidth * VARIABLE_WIDTH_FREEDRAW.SIZE_FACTOR, + thinning: VARIABLE_WIDTH_FREEDRAW.THINNING, + smoothing: VARIABLE_WIDTH_FREEDRAW.SMOOTHING, + streamline: getFreedrawStreamline(element), easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine last: true, }) as [number, number][]; }; +const createLaserPointer = (element: ExcalidrawFreeDrawElement) => + new LaserPointer({ + size: element.strokeWidth * CONSTANT_WIDTH_FREEDRAW.SIZE_FACTOR, + streamline: getFreedrawStreamline(element), + simplify: 0, + sizeMapping: (details) => Math.max(0.1, details.pressure), + }); + +/** + * Uniform (constant width) freedraw outline, rendered with the laser-pointer + * geometry. Pressure is pinned to 1 so the stroke keeps a constant width. + */ +const getConstantWidthFreedrawOutline = ( + element: ExcalidrawFreeDrawElement, +): [number, number][] => { + const laserPointer = createLaserPointer(element); + element.points.map(([x, y]) => laserPointer.addPoint([x, y, 1])); + + return laserPointer + .getStrokeOutline() + .map(([x, y]) => [x, y] as [number, number]); +}; + +export const getFreedrawOutlinePoints = ( + element: ExcalidrawFreeDrawElement, +): [number, number][] => { + // Unknown/absent variability falls back to the original variable rendering. + return element.strokeOptions?.variability === "constant" + ? getConstantWidthFreedrawOutline(element) + : getVariableWidthFreedrawOutline(element); +}; + const med = (A: number[], B: number[]) => { return [(A[0] + B[0]) / 2, (A[1] + B[1]) / 2]; }; diff --git a/packages/element/src/types.ts b/packages/element/src/types.ts index 8d280f3141..1797fbae9c 100644 --- a/packages/element/src/types.ts +++ b/packages/element/src/types.ts @@ -384,12 +384,20 @@ export type ExcalidrawElbowArrowElement = Merge< } >; +export type StrokeVariability = "variable" | "constant"; + +export type StrokeOptions = Readonly<{ + variability: StrokeVariability; + streamline: number; +}>; + export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase & Readonly<{ type: "freedraw"; points: readonly LocalPoint[]; pressures: readonly number[]; simulatePressure: boolean; + strokeOptions: StrokeOptions; }>; export type FileId = string & { _brand: "FileId" }; diff --git a/packages/excalidraw/actions/actionBoundText.tsx b/packages/excalidraw/actions/actionBoundText.tsx index 383668c756..1d19c412d8 100644 --- a/packages/excalidraw/actions/actionBoundText.tsx +++ b/packages/excalidraw/actions/actionBoundText.tsx @@ -5,6 +5,7 @@ import { VERTICAL_ALIGN, arrayToMap, getFontString, + getStrokeWidthByKey, } from "@excalidraw/common"; import { getOriginalContainerHeightFromCache, @@ -249,7 +250,10 @@ export const actionWrapTextInContainer = register({ fillStyle: appState.currentItemFillStyle, strokeColor: appState.currentItemStrokeColor, roughness: appState.currentItemRoughness, - strokeWidth: appState.currentItemStrokeWidth, + strokeWidth: getStrokeWidthByKey( + "rectangle", + appState.currentItemStrokeWidthKey, + ), strokeStyle: appState.currentItemStrokeStyle, roundness: appState.currentItemRoundness === "round" diff --git a/packages/excalidraw/actions/actionCanvas.tsx b/packages/excalidraw/actions/actionCanvas.tsx index 9c8fea322f..d1bce4aee6 100644 --- a/packages/excalidraw/actions/actionCanvas.tsx +++ b/packages/excalidraw/actions/actionCanvas.tsx @@ -112,6 +112,9 @@ export const actionClearCanvas = register({ theme: appState.theme, penMode: appState.penMode, penDetected: appState.penDetected, + currentItemStrokeVariability: appState.penDetected + ? "variable" + : "constant", exportBackground: appState.exportBackground, exportEmbedScene: appState.exportEmbedScene, gridSize: appState.gridSize, diff --git a/packages/excalidraw/actions/actionProperties.test.tsx b/packages/excalidraw/actions/actionProperties.test.tsx index 38419ce827..ddcc667652 100644 --- a/packages/excalidraw/actions/actionProperties.test.tsx +++ b/packages/excalidraw/actions/actionProperties.test.tsx @@ -1,8 +1,9 @@ -import { queryByTestId } from "@testing-library/react"; +import { fireEvent, queryByTestId } from "@testing-library/react"; import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS, + FREEDRAW_STROKE_WIDTH, FONT_FAMILY, STROKE_WIDTH, } from "@excalidraw/common"; @@ -128,6 +129,62 @@ describe("element locking", () => { expect(thinStrokeWidthButton).toBeChecked(); }); + it("should highlight common stroke width key across freedraw and non-freedraw elements", () => { + const rect = API.createElement({ + type: "rectangle", + strokeWidth: STROKE_WIDTH.medium, + }); + const freedraw = API.createElement({ + type: "freedraw", + strokeWidth: FREEDRAW_STROKE_WIDTH.medium, + }); + API.setElements([rect, freedraw]); + API.setSelectedElements([rect, freedraw]); + + expect(queryByTestId(document.body, `strokeWidth-medium`)).toBeChecked(); + }); + + it("should apply stroke width by element type", () => { + const rect = API.createElement({ + type: "rectangle", + strokeWidth: STROKE_WIDTH.thin, + }); + const freedraw = API.createElement({ + type: "freedraw", + strokeWidth: FREEDRAW_STROKE_WIDTH.thin, + }); + API.setElements([rect, freedraw]); + API.setSelectedElements([rect, freedraw]); + + const boldStrokeWidthButton = queryByTestId( + document.body, + `strokeWidth-bold`, + ); + expect(boldStrokeWidthButton).not.toBe(null); + fireEvent.click(boldStrokeWidthButton!); + + const selectedElements = API.getSelectedElements(); + const selectedRect = selectedElements.find( + (element) => element.type === "rectangle", + ); + const selectedFreedraw = selectedElements.find( + (element) => element.type === "freedraw", + ); + + expect(selectedRect?.strokeWidth).toBe(STROKE_WIDTH.bold); + expect(selectedFreedraw?.strokeWidth).toBe(FREEDRAW_STROKE_WIDTH.bold); + }); + + it("should create new elements with stroke width by element type", () => { + API.setAppState({ currentItemStrokeWidthKey: "bold" }); + + const rect = API.createElement({ type: "rectangle" }); + const freedraw = API.createElement({ type: "freedraw" }); + + expect(rect.strokeWidth).toBe(STROKE_WIDTH.bold); + expect(freedraw.strokeWidth).toBe(FREEDRAW_STROKE_WIDTH.bold); + }); + it("should not highlight any stroke width button if no common style", () => { const rect1 = API.createElement({ type: "rectangle", @@ -135,7 +192,7 @@ describe("element locking", () => { }); const rect2 = API.createElement({ type: "rectangle", - strokeWidth: STROKE_WIDTH.bold, + strokeWidth: STROKE_WIDTH.medium, }); API.setElements([rect1, rect2]); API.setSelectedElements([rect1, rect2]); @@ -145,17 +202,17 @@ describe("element locking", () => { queryByTestId(document.body, `strokeWidth-thin`), ).not.toBeChecked(); expect( - queryByTestId(document.body, `strokeWidth-bold`), + queryByTestId(document.body, `strokeWidth-medium`), ).not.toBeChecked(); expect( - queryByTestId(document.body, `strokeWidth-extraBold`), + queryByTestId(document.body, `strokeWidth-bold`), ).not.toBeChecked(); }); it("should show properties of different element types when selected", () => { const rect = API.createElement({ type: "rectangle", - strokeWidth: STROKE_WIDTH.bold, + strokeWidth: STROKE_WIDTH.medium, }); const text = API.createElement({ type: "text", @@ -164,7 +221,7 @@ describe("element locking", () => { API.setElements([rect, text]); API.setSelectedElements([rect, text]); - expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked(); + expect(queryByTestId(document.body, `strokeWidth-medium`)).toBeChecked(); expect(queryByTestId(document.body, `font-family-code`)).toHaveClass( "active", ); diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index a046db6ea8..09e7bb7369 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -12,7 +12,7 @@ import { DEFAULT_FONT_SIZE, FONT_FAMILY, ROUNDNESS, - STROKE_WIDTH, + STROKE_WIDTH_KEYS, VERTICAL_ALIGN, KEYS, randomInteger, @@ -20,9 +20,11 @@ import { getFontFamilyString, getLineHeight, isTransparent, + getStrokeWidthByKey, reduceToCommonValue, invariant, FONT_SIZES, + type StrokeWidthKey, } from "@excalidraw/common"; import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element"; @@ -70,9 +72,11 @@ import type { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, + ExcalidrawFreeDrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, FontFamilyValues, + StrokeVariability, TextAlign, VerticalAlign, } from "@excalidraw/element/types"; @@ -131,6 +135,8 @@ import { ArrowheadCardinalityOneOrManyIcon, ArrowheadCardinalityZeroOrManyIcon, ArrowheadCardinalityZeroOrOneIcon, + strokeVariabilityConstantIcon, + strokeVariabilityVariableIcon, } from "../components/icons"; import { Fonts } from "../fonts"; @@ -190,7 +196,11 @@ export const changeProperty = ( export const getFormValue = function ( elements: readonly ExcalidrawElement[], app: AppClassProperties, - getAttribute: (element: ExcalidrawElement) => T, + /** + * input value (usually the element attribute value, + * but depends on what the action's PanelComponent input expects) + */ + getValue: (element: ExcalidrawElement) => T, elementPredicate: true | ((element: ExcalidrawElement) => boolean), defaultValue: T | ((isSomeElementSelected: boolean) => T), ): T { @@ -200,7 +210,7 @@ export const getFormValue = function ( let ret: T | null = null; if (editingTextElement) { - ret = getAttribute(editingTextElement); + ret = getValue(editingTextElement); } if (!ret) { @@ -214,7 +224,7 @@ export const getFormValue = function ( : selectedElements.filter((el) => elementPredicate(el)); ret = - reduceToCommonValue(targetElements, getAttribute) ?? + reduceToCommonValue(targetElements, getValue) ?? (typeof defaultValue === "function" ? defaultValue(true) : defaultValue); @@ -544,20 +554,37 @@ export const actionChangeFillStyle = register({ }, }); -export const actionChangeStrokeWidth = register< - ExcalidrawElement["strokeWidth"] ->({ +const getStrokeWidthKeyForElement = ( + element: ExcalidrawElement, +): StrokeWidthKey | null => { + return ( + STROKE_WIDTH_KEYS.find( + (key) => getStrokeWidthByKey(element.type, key) === element.strokeWidth, + ) ?? null + ); +}; + +const getStrokeWidthForElement = ( + element: ExcalidrawElement, + strokeWidthKey: StrokeWidthKey, +): ExcalidrawElement["strokeWidth"] => { + return getStrokeWidthByKey(element.type, strokeWidthKey); +}; + +export const actionChangeStrokeWidth = register({ name: "changeStrokeWidth", label: "labels.strokeWidth", trackEvent: false, perform: (elements, appState, value) => { + invariant(value, "actionChangeStrokeWidth: value must be defined"); + return { elements: changeProperty(elements, appState, (el) => newElementWith(el, { - strokeWidth: value, + strokeWidth: getStrokeWidthForElement(el, value), }), ), - appState: { ...appState, currentItemStrokeWidth: value }, + appState: { ...appState, currentItemStrokeWidthKey: value }, captureUpdate: CaptureUpdateAction.IMMEDIATELY, }; }, @@ -565,35 +592,35 @@ export const actionChangeStrokeWidth = register<
{t("labels.strokeWidth")}
- group="stroke-width" options={[ { - value: STROKE_WIDTH.thin, + value: "thin", text: t("labels.thin"), icon: StrokeWidthBaseIcon, testId: "strokeWidth-thin", }, { - value: STROKE_WIDTH.bold, - text: t("labels.bold"), + value: "medium", + text: t("labels.medium"), icon: StrokeWidthBoldIcon, - testId: "strokeWidth-bold", + testId: "strokeWidth-medium", }, { - value: STROKE_WIDTH.extraBold, - text: t("labels.extraBold"), + value: "bold", + text: t("labels.bold"), icon: StrokeWidthExtraBoldIcon, - testId: "strokeWidth-extraBold", + testId: "strokeWidth-bold", }, ]} value={getFormValue( elements, app, - (element) => element.strokeWidth, + getStrokeWidthKeyForElement, (element) => element.hasOwnProperty("strokeWidth"), (hasSelection) => - hasSelection ? null : appState.currentItemStrokeWidth, + hasSelection ? null : appState.currentItemStrokeWidthKey, )} onChange={(value) => updateData(value)} /> @@ -656,6 +683,68 @@ export const actionChangeSloppiness = register({ ), }); +export const actionChangeFreedrawMode = register({ + name: "changeFreedrawMode", + label: "labels.pressure", + trackEvent: false, + perform: (elements, appState, value) => { + const variability = value || "constant"; + + return { + elements: changeProperty(elements, appState, (el) => { + if (el.type !== "freedraw") { + return el; + } + return newElementWith(el, { + strokeOptions: { + ...el.strokeOptions, + variability, + }, + }) as ExcalidrawElement; + }), + appState: { ...appState, currentItemStrokeVariability: variability }, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + }; + }, + PanelComponent: ({ elements, appState, updateData, app, data }) => { + const strokeVariability = + getFormValue( + elements, + app, + (element) => + (element as ExcalidrawFreeDrawElement).strokeOptions?.variability, + (element) => element.type === "freedraw", + (hasSelection) => + hasSelection ? null : appState.currentItemStrokeVariability, + ) ?? appState.currentItemStrokeVariability; + + return ( +
+ {t("labels.pressure")} +
+ + group="strokeOptions.variability" + options={[ + { + value: "constant", + text: t("labels.pressure_constant"), + icon: strokeVariabilityConstantIcon, + }, + { + value: "variable", + text: t("labels.pressure_variable"), + icon: strokeVariabilityVariableIcon, + }, + ]} + value={strokeVariability} + onChange={(value) => updateData(value)} + /> +
+
+ ); + }, +}); + export const actionChangeStrokeStyle = register< ExcalidrawElement["strokeStyle"] >({ diff --git a/packages/excalidraw/actions/index.ts b/packages/excalidraw/actions/index.ts index d630c6ecaa..0d378b5c49 100644 --- a/packages/excalidraw/actions/index.ts +++ b/packages/excalidraw/actions/index.ts @@ -13,6 +13,7 @@ export { actionChangeStrokeWidth, actionChangeFillStyle, actionChangeSloppiness, + actionChangeFreedrawMode, actionChangeOpacity, actionChangeFontSize, actionChangeFontFamily, diff --git a/packages/excalidraw/actions/types.ts b/packages/excalidraw/actions/types.ts index 02c67d34f3..54c46c02f4 100644 --- a/packages/excalidraw/actions/types.ts +++ b/packages/excalidraw/actions/types.ts @@ -68,6 +68,7 @@ export type ActionName = | "changeStrokeWidth" | "changeStrokeShape" | "changeSloppiness" + | "changeFreedrawMode" | "changeStrokeStyle" | "changeArrowhead" | "changeArrowType" diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index a64eed8ca7..3c4dd53658 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -4,6 +4,7 @@ import { DEFAULT_ELEMENT_PROPS, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, + DEFAULT_ELEMENT_STROKE_WIDTH_KEY, DEFAULT_TEXT_ALIGN, DEFAULT_GRID_SIZE, EXPORT_SCALES, @@ -34,12 +35,13 @@ export const getDefaultAppState = (): Omit< currentItemFontSize: DEFAULT_FONT_SIZE, currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity, currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness, + currentItemStrokeVariability: "constant", currentItemStartArrowhead: null, currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor, currentItemRoundness: isTestEnv() ? "sharp" : "round", currentItemArrowType: ARROW_TYPE.round, currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle, - currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth, + currentItemStrokeWidthKey: DEFAULT_ELEMENT_STROKE_WIDTH_KEY, currentItemTextAlign: DEFAULT_TEXT_ALIGN, currentHoveredFontFamily: null, cursorButton: "up", @@ -167,10 +169,15 @@ const APP_STATE_STORAGE_CONF = (< }, currentItemOpacity: { browser: true, export: false, server: false }, currentItemRoughness: { browser: true, export: false, server: false }, + currentItemStrokeVariability: { + browser: true, + export: false, + server: false, + }, currentItemStartArrowhead: { browser: true, export: false, server: false }, currentItemStrokeColor: { browser: true, export: false, server: false }, currentItemStrokeStyle: { browser: true, export: false, server: false }, - currentItemStrokeWidth: { browser: true, export: false, server: false }, + currentItemStrokeWidthKey: { browser: true, export: false, server: false }, currentItemTextAlign: { browser: true, export: false, server: false }, currentHoveredFontFamily: { browser: false, export: false, server: false }, cursorButton: { browser: true, export: false, server: false }, diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index be065d826d..8e3f86e6c4 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -41,6 +41,7 @@ import { canHaveArrowheads, getTargetElements, hasBackground, + hasFreedrawMode, hasStrokeStyle, hasStrokeWidth, } from "../scene"; @@ -201,9 +202,9 @@ export const SelectedShapeActions = ({ targetElements.some((element) => hasStrokeWidth(element.type))) && renderAction("changeStrokeWidth")} - {(appState.activeTool.type === "freedraw" || - targetElements.some((element) => element.type === "freedraw")) && - renderAction("changeStrokeShape")} + {(hasFreedrawMode(appState.activeTool.type) || + targetElements.some((element) => hasFreedrawMode(element.type))) && + renderAction("changeFreedrawMode")} {(hasStrokeStyle(appState.activeTool.type) || targetElements.some((element) => hasStrokeStyle(element.type))) && ( @@ -394,6 +395,11 @@ const CombinedShapeProperties = ({ hasStrokeWidth(element.type), )) && renderAction("changeStrokeWidth")} + {(hasFreedrawMode(appState.activeTool.type) || + targetElements.some((element) => + hasFreedrawMode(element.type), + )) && + renderAction("changeFreedrawMode")} {(hasStrokeStyle(appState.activeTool.type) || targetElements.some((element) => hasStrokeStyle(element.type), diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 2b04162491..38b88a5806 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -27,6 +27,8 @@ import { KEYS, APP_NAME, CURSOR_TYPE, + DEFAULT_STROKE_STREAMLINE, + DEFAULT_STROKE_STREAMLINE_PRECISE, DEFAULT_TRANSFORM_HANDLE_SPACING, DEFAULT_VERTICAL_ALIGN, DRAGGING_THRESHOLD, @@ -109,6 +111,7 @@ import { setDesktopUIMode, isSelectionLikeTool, oneOf, + getStrokeWidthByKey, } from "@excalidraw/common"; import { @@ -4134,7 +4137,7 @@ class App extends React.Component { strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth("text"), strokeStyle: this.state.currentItemStrokeStyle, roundness: null, roughness: this.state.currentItemRoughness, @@ -4305,6 +4308,7 @@ class App extends React.Component { return { penMode: force ?? !prevState.penMode, penDetected: true, + currentItemStrokeVariability: "variable", }; }); }; @@ -6304,7 +6308,7 @@ class App extends React.Component { strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth("text"), strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, @@ -7774,6 +7778,7 @@ class App extends React.Component { return { penMode: true, penDetected: true, + currentItemStrokeVariability: "variable", }; }); } @@ -8992,6 +8997,8 @@ class App extends React.Component { const simulatePressure = event.pressure === 0.5; + const strokeVariability = this.state.currentItemStrokeVariability; + const element = newFreeDrawElement({ type: elementType, x: gridX, @@ -8999,15 +9006,24 @@ class App extends React.Component { strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth("freedraw"), strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, roundness: null, simulatePressure, + strokeOptions: { + variability: strokeVariability, + streamline: + strokeVariability === "constant" && event.pointerType !== "mouse" + ? DEFAULT_STROKE_STREAMLINE_PRECISE + : DEFAULT_STROKE_STREAMLINE, + }, locked: false, frameId: topLayerFrame ? topLayerFrame.id : null, points: [pointFrom(0, 0)], + // pressures are only consumed when rendering a real-pressure stroke, so + // skip persisting them while pressure is being simulated pressures: simulatePressure ? [] : [event.pressure], }); @@ -9058,7 +9074,7 @@ class App extends React.Component { strokeColor: "transparent", backgroundColor: "transparent", fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth("iframe"), strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, roundness: this.getCurrentItemRoundness("iframe"), @@ -9111,7 +9127,7 @@ class App extends React.Component { strokeColor: "transparent", backgroundColor: "transparent", fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth("embeddable"), strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, roundness: this.getCurrentItemRoundness("embeddable"), @@ -9158,7 +9174,7 @@ class App extends React.Component { strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth("image"), strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, roundness: null, @@ -9336,7 +9352,7 @@ class App extends React.Component { strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth(elementType), strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, @@ -9363,7 +9379,7 @@ class App extends React.Component { strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth(elementType), strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, @@ -9500,6 +9516,13 @@ class App extends React.Component { : null; } + private getCurrentItemStrokeWidth(elementType: ExcalidrawElement["type"]) { + return getStrokeWidthByKey( + elementType, + this.state.currentItemStrokeWidthKey, + ); + } + private createGenericElementOnPointerDown = ( elementType: ExcalidrawGenericElement["type"] | "embeddable", pointerDownState: PointerDownState, @@ -9523,7 +9546,7 @@ class App extends React.Component { strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, - strokeWidth: this.state.currentItemStrokeWidth, + strokeWidth: this.getCurrentItemStrokeWidth(elementType), strokeStyle: this.state.currentItemStrokeStyle, roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, diff --git a/packages/excalidraw/components/icons.tsx b/packages/excalidraw/components/icons.tsx index 7e2400728d..45b92da43e 100644 --- a/packages/excalidraw/components/icons.tsx +++ b/packages/excalidraw/components/icons.tsx @@ -1249,6 +1249,74 @@ export const SloppinessCartoonistIcon = createIcon( modifiedTablerIconProps, ); +export const strokeVariabilityConstantIcon = createIcon( + + + + + + , + tablerIconProps, +); + +export const strokeVariabilityVariableIcon = createIcon( + + + + + + , + tablerIconProps, +); + export const EdgeSharpIcon = createIcon( diff --git a/packages/excalidraw/data/restore.ts b/packages/excalidraw/data/restore.ts index 58c2ec26e2..db75c71099 100644 --- a/packages/excalidraw/data/restore.ts +++ b/packages/excalidraw/data/restore.ts @@ -3,6 +3,7 @@ import { isFiniteNumber, isValidPoint, pointFrom } from "@excalidraw/math"; import { type CombineBrandsIfNeeded, DEFAULT_FONT_FAMILY, + DEFAULT_STROKE_STREAMLINE, DEFAULT_TEXT_ALIGN, DEFAULT_VERTICAL_ALIGN, FONT_FAMILY, @@ -18,6 +19,9 @@ import { getSizeFromPoints, normalizeLink, getLineHeight, + STROKE_WIDTH, + STROKE_WIDTH_KEYS, + type StrokeWidthKey, } from "@excalidraw/common"; import { calculateFixedPointForNonElbowArrowBinding, @@ -70,6 +74,7 @@ import type { FontFamilyValues, NonDeletedSceneElementsMap, OrderedExcalidrawElement, + StrokeVariability, StrokeRoundness, } from "@excalidraw/element/types"; @@ -188,6 +193,43 @@ export type RestoredDataState = { files: BinaryFiles; }; +const ALLOWED_STROKE_VARIABILITIES = new Set([ + "constant", + "variable", +]); + +const restoreStrokeVariability = ( + variability: unknown, + defaultValue: StrokeVariability, +): StrokeVariability => { + return typeof variability === "string" && + ALLOWED_STROKE_VARIABILITIES.has(variability as StrokeVariability) + ? (variability as StrokeVariability) + : defaultValue; +}; + +const getStrokeWidthKey = (strokeWidth: unknown): StrokeWidthKey | null => { + return isFiniteNumber(strokeWidth) + ? STROKE_WIDTH_KEYS.find((key) => STROKE_WIDTH[key] === strokeWidth) ?? null + : null; +}; + +const restoreFreedrawStrokeOptions = ( + strokeOptions: unknown, +): { variability: StrokeVariability; streamline: number } => { + const options = + strokeOptions && typeof strokeOptions === "object" + ? (strokeOptions as { variability?: unknown; streamline?: unknown }) + : null; + + return { + variability: restoreStrokeVariability(options?.variability, "variable"), + streamline: isFiniteNumber(options?.streamline) + ? options?.streamline + : DEFAULT_STROKE_STREAMLINE, + }; +}; + const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => { if (Object.keys(FONT_FAMILY).includes(fontFamilyName)) { return FONT_FAMILY[ @@ -483,6 +525,7 @@ export const restoreElement = ( return restoreElementWithProperties(element, { points, simulatePressure: element.simulatePressure, + strokeOptions: restoreFreedrawStrokeOptions(element.strokeOptions), pressures, }); } @@ -1056,6 +1099,13 @@ export const restoreAppState = ( nextAppState.boxSelectionMode = boxSelectionMode; } + // legacy + if ((appState as any).currentItemStrokeWidth !== undefined) { + nextAppState.currentItemStrokeWidthKey = + getStrokeWidthKey((appState as any).currentItemStrokeWidth) ?? + defaultAppState.currentItemStrokeWidthKey; + } + return { ...nextAppState, cursorButton: localAppState?.cursorButton || "up", diff --git a/packages/excalidraw/index.tsx b/packages/excalidraw/index.tsx index 02ed37779d..bd9f769c45 100644 --- a/packages/excalidraw/index.tsx +++ b/packages/excalidraw/index.tsx @@ -10,6 +10,7 @@ import { applyDarkModeFilter, DEFAULT_IMAGE_OPTIONS, DEFAULT_UI_OPTIONS, + getStrokeWidthByKey, isShallowEqual, } from "@excalidraw/common"; @@ -450,4 +451,4 @@ export function useExcalidrawStateValue( export { _useOnAppStateChange as useOnExcalidrawStateChange }; -export { applyDarkModeFilter }; +export { applyDarkModeFilter, getStrokeWidthByKey }; diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index 05d3bf702e..d8c045d6c4 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -35,6 +35,9 @@ "strokeStyle_dashed": "Dashed", "strokeStyle_dotted": "Dotted", "sloppiness": "Sloppiness", + "pressure": "Pressure", + "pressure_constant": "Constant", + "pressure_variable": "Variable", "opacity": "Opacity", "textAlign": "Text align", "edges": "Edges", diff --git a/packages/excalidraw/scene/index.ts b/packages/excalidraw/scene/index.ts index 60f7314b4a..ba6142c875 100644 --- a/packages/excalidraw/scene/index.ts +++ b/packages/excalidraw/scene/index.ts @@ -9,6 +9,7 @@ export { hasBackground, hasStrokeWidth, hasStrokeStyle, + hasFreedrawMode, canHaveArrowheads, canChangeRoundness, } from "@excalidraw/element"; diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 7162ed5f91..4237c50cb5 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -904,7 +904,8 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1103,7 +1104,8 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1317,7 +1319,8 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1648,7 +1651,8 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1979,7 +1983,8 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2193,7 +2198,8 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2434,7 +2440,8 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2732,7 +2739,8 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3104,7 +3112,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "currentItemStartArrowhead": null, "currentItemStrokeColor": "#e03131", "currentItemStrokeStyle": "dotted", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "bold", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3214,11 +3223,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "seed": 1278240551, "strokeColor": "#e03131", "strokeStyle": "dotted", - "strokeWidth": 2, + "strokeWidth": 4, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1402203177, + "versionNonce": 1349943049, "width": 20, "x": -10, "y": 0, @@ -3243,14 +3252,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "opacity": 60, "roughness": 2, "roundness": null, - "seed": 1898319239, + "seed": 406373543, "strokeColor": "#e03131", "strokeStyle": "dotted", - "strokeWidth": 2, + "strokeWidth": 4, "type": "rectangle", "updated": 1, - "version": 9, - "versionNonce": 941653321, + "version": 10, + "versionNonce": 1402203177, "width": 20, "x": 20, "y": 30, @@ -3259,7 +3268,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `16`; +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `17`; exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] redo stack 1`] = `[]`; @@ -3459,11 +3468,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "updated": { "id3": { "deleted": { - "strokeStyle": "dotted", + "strokeWidth": 4, "version": 7, }, "inserted": { - "strokeStyle": "solid", + "strokeWidth": 2, "version": 6, }, }, @@ -3484,11 +3493,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "updated": { "id3": { "deleted": { - "roughness": 2, + "strokeStyle": "dotted", "version": 8, }, "inserted": { - "roughness": 1, + "strokeStyle": "solid", "version": 7, }, }, @@ -3509,11 +3518,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "updated": { "id3": { "deleted": { - "opacity": 60, + "roughness": 2, "version": 9, }, "inserted": { - "opacity": 100, + "roughness": 1, "version": 8, }, }, @@ -3521,6 +3530,31 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s }, "id": "id17", }, + { + "appState": AppStateDelta { + "delta": Delta { + "deleted": {}, + "inserted": {}, + }, + }, + "elements": { + "added": {}, + "removed": {}, + "updated": { + "id3": { + "deleted": { + "opacity": 60, + "version": 10, + }, + "inserted": { + "opacity": 100, + "version": 9, + }, + }, + }, + }, + "id": "id19", + }, { "appState": AppStateDelta { "delta": Delta { @@ -3548,6 +3582,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roughness": 2, "strokeColor": "#e03131", "strokeStyle": "dotted", + "strokeWidth": 4, "version": 4, }, "inserted": { @@ -3557,12 +3592,13 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roughness": 1, "strokeColor": "#1e1e1e", "strokeStyle": "solid", + "strokeWidth": 2, "version": 3, }, }, }, }, - "id": "id19", + "id": "id21", }, ] `; @@ -3597,7 +3633,8 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3920,7 +3957,8 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -4243,7 +4281,8 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -5528,7 +5567,8 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -6745,7 +6785,8 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -7702,7 +7743,8 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8702,7 +8744,8 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9693,7 +9736,8 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index fd57911e3e..c48e3aea0a 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -30,7 +30,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -664,7 +665,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1226,7 +1228,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1586,7 +1589,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1948,7 +1952,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2211,7 +2216,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2698,7 +2704,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3001,7 +3008,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3320,7 +3328,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3614,7 +3623,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3900,7 +3910,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -4135,7 +4146,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -4392,7 +4404,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -4663,7 +4676,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -4892,7 +4906,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -5121,7 +5136,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -5368,7 +5384,8 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -5624,7 +5641,8 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -5882,7 +5900,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -6211,7 +6230,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -6638,7 +6658,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -7012,7 +7033,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -7324,7 +7346,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -7617,7 +7640,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -7847,7 +7871,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8199,7 +8224,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8551,7 +8577,8 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8957,7 +8984,8 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9083,8 +9111,12 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "roundness": null, "simulatePressure": false, "strokeColor": "#1e1e1e", + "strokeOptions": { + "streamline": "0.50000", + "variability": "constant", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "updated": 1, "version": 7, @@ -9185,8 +9217,12 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "roundness": null, "simulatePressure": false, "strokeColor": "#1e1e1e", + "strokeOptions": { + "streamline": "0.50000", + "variability": "constant", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "version": 7, "width": 50, @@ -9236,7 +9272,8 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9500,7 +9537,8 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9765,7 +9803,8 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9997,7 +10036,8 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -10294,7 +10334,8 @@ exports[`history > multiplayer undo/redo > should override remotely added points "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -10612,7 +10653,8 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -10848,7 +10890,8 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -11289,7 +11332,7 @@ exports[`history > multiplayer undo/redo > should support undo and redo when esc "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -11773,7 +11816,8 @@ exports[`history > multiplayer undo/redo > should update history entries after r "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -12033,7 +12077,8 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -12268,7 +12313,8 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -12505,7 +12551,8 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "currentItemStartArrowhead": null, "currentItemStrokeColor": "#e03131", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -12656,8 +12703,12 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "roundness": null, "simulatePressure": false, "strokeColor": "#1e1e1e", + "strokeOptions": { + "streamline": "0.50000", + "variability": "constant", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "updated": 1, "version": 5, @@ -12706,8 +12757,12 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "roundness": null, "simulatePressure": false, "strokeColor": "#e03131", + "strokeOptions": { + "streamline": "0.50000", + "variability": "constant", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "updated": 1, "version": 4, @@ -12845,8 +12900,12 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "roundness": null, "simulatePressure": false, "strokeColor": "#e03131", + "strokeOptions": { + "streamline": "0.50000", + "variability": "constant", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "version": 4, "width": 50, @@ -12896,7 +12955,8 @@ exports[`history > singleplayer undo/redo > should create new history entry on e "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -13106,7 +13166,8 @@ exports[`history > singleplayer undo/redo > should create new history entry on e "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -13313,7 +13374,8 @@ exports[`history > singleplayer undo/redo > should create new history entry on i "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -13614,7 +13676,8 @@ exports[`history > singleplayer undo/redo > should create new history entry on i "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -13912,7 +13975,8 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -14157,7 +14221,8 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -14394,7 +14459,8 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -14631,7 +14697,8 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -14878,7 +14945,8 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -15209,7 +15277,8 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -15379,7 +15448,8 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -15663,7 +15733,8 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -15926,7 +15997,8 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -16079,7 +16151,8 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -16361,7 +16434,8 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -16523,7 +16597,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -17272,7 +17347,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -17919,7 +17995,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -18566,7 +18643,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -19316,7 +19394,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -20085,7 +20164,8 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -20565,7 +20645,8 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -21076,7 +21157,8 @@ exports[`history > singleplayer undo/redo > should support element creation, del "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -21535,7 +21617,8 @@ exports[`history > singleplayer undo/redo > should support linear element creati "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 3f836165fc..9353a224de 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -30,7 +30,8 @@ exports[`given element A and group of elements B and given both are selected whe "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -456,7 +457,8 @@ exports[`given element A and group of elements B and given both are selected whe "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -872,7 +874,8 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1438,7 +1441,8 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -1645,7 +1649,8 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2029,7 +2034,8 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2274,7 +2280,8 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = ` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2454,7 +2461,8 @@ exports[`regression tests > can drag element that covers another element, while "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -2779,7 +2787,8 @@ exports[`regression tests > change the properties of a shape > [end of test] app "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1971c2", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3034,7 +3043,8 @@ exports[`regression tests > click on an element and drag it > [dragged] appState "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3275,7 +3285,8 @@ exports[`regression tests > click on an element and drag it > [end of test] appS "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3511,7 +3522,8 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`] "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -3769,7 +3781,8 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -4083,7 +4096,8 @@ exports[`regression tests > deleting last but one element in editing group shoul "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -4519,7 +4533,8 @@ exports[`regression tests > deselects group of selected elements on pointer down "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, @@ -4802,7 +4817,8 @@ exports[`regression tests > deselects group of selected elements on pointer up w "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -5078,7 +5094,8 @@ exports[`regression tests > deselects selected element on pointer down when poin "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, @@ -5286,7 +5303,8 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -5486,7 +5504,8 @@ exports[`regression tests > double click to edit a group > [end of test] appStat "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -5879,7 +5898,8 @@ exports[`regression tests > drags selected elements from point inside common bou "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -6176,7 +6196,8 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -6914,8 +6935,12 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "roundness": null, "simulatePressure": false, "strokeColor": "#1e1e1e", + "strokeOptions": { + "streamline": "0.50000", + "variability": "constant", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "version": 4, "width": 50, @@ -6965,7 +6990,8 @@ exports[`regression tests > given a group of selected elements with an element t "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -7299,7 +7325,8 @@ exports[`regression tests > given a selected element A and a not selected elemen "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -7578,7 +7605,8 @@ exports[`regression tests > given selected element A with lower z-index than uns "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -7813,7 +7841,8 @@ exports[`regression tests > given selected element A with lower z-index than uns "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8053,7 +8082,8 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8233,7 +8263,8 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8413,7 +8444,8 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8593,7 +8625,8 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -8826,7 +8859,8 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9057,7 +9091,8 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9198,8 +9233,12 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta "roundness": null, "simulatePressure": false, "strokeColor": "#1e1e1e", + "strokeOptions": { + "streamline": "0.50000", + "variability": "constant", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "version": 4, "width": 30, @@ -9249,7 +9288,8 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9482,7 +9522,8 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9662,7 +9703,8 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -9893,7 +9935,8 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -10073,7 +10116,8 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -10214,8 +10258,12 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta "roundness": null, "simulatePressure": false, "strokeColor": "#1e1e1e", + "strokeOptions": { + "streamline": "0.50000", + "variability": "constant", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "version": 4, "width": 30, @@ -10265,7 +10313,8 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -10445,7 +10494,8 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -10976,7 +11026,8 @@ exports[`regression tests > noop interaction after undo shouldn't create history "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -11256,7 +11307,8 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, @@ -11379,7 +11431,8 @@ exports[`regression tests > shift click on selected element should deselect it o "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -11579,7 +11632,8 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -11898,7 +11952,8 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -12327,7 +12382,8 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -12967,7 +13023,8 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -13093,7 +13150,8 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`] "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -13724,7 +13782,8 @@ exports[`regression tests > switches from group of selected elements to another "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, @@ -14063,7 +14122,8 @@ exports[`regression tests > switches selected element on pointer down > [end of "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, @@ -14327,7 +14387,8 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`] "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "down", "defaultSidebarDockedPreference": false, @@ -14450,7 +14511,8 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -14815,7 +14877,8 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, @@ -14938,7 +15001,8 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, diff --git a/packages/excalidraw/tests/actionStyles.test.tsx b/packages/excalidraw/tests/actionStyles.test.tsx index e81e9e4e40..ce4ff46ec6 100644 --- a/packages/excalidraw/tests/actionStyles.test.tsx +++ b/packages/excalidraw/tests/actionStyles.test.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { CODES } from "@excalidraw/common"; +import { CODES, STROKE_WIDTH } from "@excalidraw/common"; import { copiedStyles } from "../actions/actionStyles"; import { Excalidraw } from "../index"; @@ -78,7 +78,7 @@ describe("actionStyles", () => { expect(firstRect.strokeColor).toBe("#e03131"); expect(firstRect.backgroundColor).toBe("#a5d8ff"); expect(firstRect.fillStyle).toBe("cross-hatch"); - expect(firstRect.strokeWidth).toBe(2); // Bold: 2 + expect(firstRect.strokeWidth).toBe(STROKE_WIDTH.bold); expect(firstRect.strokeStyle).toBe("dotted"); expect(firstRect.roughness).toBe(2); // Cartoonist: 2 expect(firstRect.opacity).toBe(60); diff --git a/packages/excalidraw/tests/contextmenu.test.tsx b/packages/excalidraw/tests/contextmenu.test.tsx index 3aa50090a8..67c5f8820e 100644 --- a/packages/excalidraw/tests/contextmenu.test.tsx +++ b/packages/excalidraw/tests/contextmenu.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { vi } from "vitest"; -import { KEYS, reseed } from "@excalidraw/common"; +import { KEYS, STROKE_WIDTH, reseed } from "@excalidraw/common"; import { setDateTimeForTests } from "@excalidraw/common"; @@ -378,7 +378,7 @@ describe("contextMenu element", () => { expect(firstRect.strokeColor).toBe("#e03131"); expect(firstRect.backgroundColor).toBe("#a5d8ff"); expect(firstRect.fillStyle).toBe("cross-hatch"); - expect(firstRect.strokeWidth).toBe(2); // Bold: 2 + expect(firstRect.strokeWidth).toBe(STROKE_WIDTH.bold); expect(firstRect.strokeStyle).toBe("dotted"); expect(firstRect.roughness).toBe(2); // Cartoonist: 2 expect(firstRect.opacity).toBe(60); diff --git a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap index b1d267c3ba..0854e48d36 100644 --- a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap +++ b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap @@ -240,8 +240,12 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = ` "seed": Any, "simulatePressure": true, "strokeColor": "#1e1e1e", + "strokeOptions": { + "streamline": "0.50000", + "variability": "variable", + }, "strokeStyle": "solid", - "strokeWidth": 2, + "strokeWidth": 1, "type": "freedraw", "updated": 1, "version": 2, diff --git a/packages/excalidraw/tests/data/restore.test.ts b/packages/excalidraw/tests/data/restore.test.ts index aa634d32dc..df38fc1133 100644 --- a/packages/excalidraw/tests/data/restore.test.ts +++ b/packages/excalidraw/tests/data/restore.test.ts @@ -193,6 +193,53 @@ describe("restoreElements", () => { expect(restoredFreedraw.pressures).toEqual([0.1, 0.4]); }); + it("should restore freedraw stroke variability", () => { + const freedrawElement = API.createElement({ + type: "freedraw", + id: "id-freedraw-mode", + points: [pointFrom(0, 0), pointFrom(10, 10)], + }); + + const [missing, bogusString, bogusNumber, valid, variable] = + restore.restoreElements( + [ + { ...freedrawElement, id: "missing", strokeOptions: undefined }, + { + ...freedrawElement, + id: "bogusString", + strokeOptions: { variability: "scribble" }, + }, + { + ...freedrawElement, + id: "bogusNumber", + strokeOptions: { variability: 42 }, + }, + { + ...freedrawElement, + id: "valid", + strokeOptions: { variability: "constant", streamline: 0.8 }, + }, + { + ...freedrawElement, + id: "variable", + strokeOptions: { variability: "variable", streamline: 0.8 }, + }, + ] as any, + null, + ) as ExcalidrawFreeDrawElement[]; + + expect(missing.strokeOptions?.variability).toBe("variable"); + expect(bogusString.strokeOptions?.variability).toBe("variable"); + expect(bogusNumber.strokeOptions?.variability).toBe("variable"); + expect(valid.strokeOptions?.variability).toBe("constant"); + expect(variable.strokeOptions?.variability).toBe("variable"); + expect(missing.strokeOptions?.streamline).toBe(0.5); + expect(bogusString.strokeOptions?.streamline).toBe(0.5); + expect(bogusNumber.strokeOptions?.streamline).toBe(0.5); + expect(valid.strokeOptions?.streamline).toBe(0.8); + expect(variable.strokeOptions?.streamline).toBe(0.8); + }); + it("should restore line and draw elements correctly", () => { const lineElement = API.createElement({ type: "line", id: "id-line01" }); @@ -640,6 +687,21 @@ describe("restoreElements", () => { }); describe("restoreAppState", () => { + it("should restore freedraw mode app state values", () => { + expect( + restore.restoreAppState( + { currentItemStrokeVariability: "constant" } as any, + null, + ).currentItemStrokeVariability, + ).toBe("constant"); + expect( + restore.restoreAppState( + { currentItemStrokeVariability: "variable" } as any, + null, + ).currentItemStrokeVariability, + ).toBe("variable"); + }); + it("when appState is null it should return the local app state property", () => { const stubLocalAppState = getDefaultAppState(); stubLocalAppState.cursorButton = "down"; @@ -688,6 +750,21 @@ describe("restoreAppState", () => { expect(restoredAppState.name).toBe(stubImportedAppState.name); }); + it("should migrate legacy current item stroke width to stroke width key", () => { + const stubImportedAppState = { + ...getDefaultAppState(), + currentItemStrokeWidth: 4, + currentItemStrokeWidthKey: undefined, + } as any; + + const restoredAppState = restore.restoreAppState( + stubImportedAppState, + null, + ); + + expect(restoredAppState.currentItemStrokeWidthKey).toBe("bold"); + }); + it("should restore with current app state when imported data state is undefined", () => { const stubImportedAppState = { ...getDefaultAppState(), diff --git a/packages/excalidraw/tests/freedrawMode.test.tsx b/packages/excalidraw/tests/freedrawMode.test.tsx new file mode 100644 index 0000000000..150219b54e --- /dev/null +++ b/packages/excalidraw/tests/freedrawMode.test.tsx @@ -0,0 +1,57 @@ +import type { ExcalidrawFreeDrawElement } from "@excalidraw/element/types"; + +import { Excalidraw } from "../index"; + +import { API } from "./helpers/api"; +import { UI } from "./helpers/ui"; +import { act, fireEvent, render, screen } from "./test-utils"; + +const { h } = window; + +describe("freedraw mode action", () => { + beforeEach(async () => { + await render(); + }); + + afterEach(async () => { + // https://github.com/floating-ui/floating-ui/issues/1908#issuecomment-1301553793 + await act(async () => {}); + }); + + it("applies currentItemStrokeVariability to newly drawn freedraw elements", () => { + // default app state draws constant-width strokes + expect(h.state.currentItemStrokeVariability).toBe("constant"); + + UI.createElement("freedraw", { x: 0, y: 0 }); + + expect( + (h.elements[0] as ExcalidrawFreeDrawElement).strokeOptions?.variability, + ).toBe("constant"); + expect( + (h.elements[0] as ExcalidrawFreeDrawElement).strokeOptions?.streamline, + ).toBe(0.5); + }); + + it("toggling the radio updates both the selected element and the default", () => { + const element = UI.createElement("freedraw", { x: 0, y: 0 }); + API.setSelectedElements([element.get()]); + + fireEvent.click(screen.getByTitle("Variable")); + expect( + (h.elements[0] as ExcalidrawFreeDrawElement).strokeOptions?.variability, + ).toBe("variable"); + expect( + (h.elements[0] as ExcalidrawFreeDrawElement).strokeOptions?.streamline, + ).toBe(0.5); + expect(h.state.currentItemStrokeVariability).toBe("variable"); + + fireEvent.click(screen.getByTitle("Constant")); + expect( + (h.elements[0] as ExcalidrawFreeDrawElement).strokeOptions?.variability, + ).toBe("constant"); + expect( + (h.elements[0] as ExcalidrawFreeDrawElement).strokeOptions?.streamline, + ).toBe(0.5); + expect(h.state.currentItemStrokeVariability).toBe("constant"); + }); +}); diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index 5628228639..4faafcf82c 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -4,7 +4,12 @@ import util from "util"; import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math"; -import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common"; +import { + DEFAULT_VERTICAL_ALIGN, + ROUNDNESS, + assertNever, + getStrokeWidthByKey, +} from "@excalidraw/common"; import { newArrowElement, @@ -200,6 +205,9 @@ export class API { ? ExcalidrawTextElement["containerId"] : never; points?: T extends "arrow" | "line" | "freedraw" ? readonly LocalPoint[] : never; + strokeOptions?: T extends "freedraw" + ? ExcalidrawFreeDrawElement["strokeOptions"] + : never; locked?: boolean; fileId?: T extends "image" ? string : never; scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never; @@ -258,7 +266,9 @@ export class API { backgroundColor: rest.backgroundColor ?? appState.currentItemBackgroundColor, fillStyle: rest.fillStyle ?? appState.currentItemFillStyle, - strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth, + strokeWidth: + rest.strokeWidth ?? + getStrokeWidthByKey(type, appState.currentItemStrokeWidthKey), strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle, roundness: ( rest.roundness === undefined @@ -317,6 +327,7 @@ export class API { type: type as "freedraw", simulatePressure: true, points: rest.points, + strokeOptions: rest.strokeOptions, ...base, }); break; diff --git a/packages/excalidraw/tests/packages/__snapshots__/utils.test.ts.snap b/packages/excalidraw/tests/packages/__snapshots__/utils.test.ts.snap index 610d97eb32..0230be7590 100644 --- a/packages/excalidraw/tests/packages/__snapshots__/utils.test.ts.snap +++ b/packages/excalidraw/tests/packages/__snapshots__/utils.test.ts.snap @@ -24,7 +24,7 @@ exports[`exportToSvg > with default arguments 1`] = ` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 605b5f83ca..0457591a75 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -4,6 +4,7 @@ import type { throttleRAF, MIME_TYPES, EditorInterface, + StrokeWidthKey, } from "@excalidraw/common"; import type { LinearElementEditor } from "@excalidraw/element"; @@ -33,6 +34,7 @@ import type { ExcalidrawNonSelectionElement, BindMode, ExcalidrawTextElement, + StrokeVariability, } from "@excalidraw/element/types"; import type { @@ -362,9 +364,10 @@ export interface AppState { currentItemStrokeColor: string; currentItemBackgroundColor: string; currentItemFillStyle: ExcalidrawElement["fillStyle"]; - currentItemStrokeWidth: number; + currentItemStrokeWidthKey: StrokeWidthKey; currentItemStrokeStyle: ExcalidrawElement["strokeStyle"]; currentItemRoughness: number; + currentItemStrokeVariability: StrokeVariability; currentItemOpacity: number; currentItemFontFamily: FontFamilyValues; currentItemFontSize: number; diff --git a/packages/laser-pointer/LICENSE b/packages/laser-pointer/LICENSE new file mode 100644 index 0000000000..ffed06cc43 --- /dev/null +++ b/packages/laser-pointer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Excalidraw + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/laser-pointer/README.md b/packages/laser-pointer/README.md new file mode 100644 index 0000000000..823aa5f42f --- /dev/null +++ b/packages/laser-pointer/README.md @@ -0,0 +1,23 @@ +# Laser Pointer + +## Usage + + import { LaserPointer } from '@excalidraw/laser-pointer' + + const stroke = new LaserPointer(options) + + stroke.addPoint([100, 200, 1]) + stroke.close() + + const outline = stroke.getStrokeOutline() + +## Options + +| Property | Type | Default | Description | +| --- | --- | --- | --- | +| `size` | `number` | `2` | Radius of the stroke. | +| `streamline` | `number` | `0.42` | Interpolate input points to reduce jitter. | +| `simplify` | `number` | `0.1` | Reduce stroke size by sacrificing precision. | +| `simplifyPhase` | `"input" \| "output" \| "tail" ` | `"output"` | Decides when the simplification algorithm should be applied. | +| `sizeMapping` | `(details: SizeMappingDetails) => number` | `() => 1` | Maps each point to a value between `0.0` and `1.0`. | +| `keepHead` | `boolean` | `false` | Whether size mapping should influence the head of the stroke. | diff --git a/packages/laser-pointer/global.d.ts b/packages/laser-pointer/global.d.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/laser-pointer/package.json b/packages/laser-pointer/package.json new file mode 100644 index 0000000000..dca2e094c7 --- /dev/null +++ b/packages/laser-pointer/package.json @@ -0,0 +1,34 @@ +{ + "name": "@excalidraw/laser-pointer", + "version": "1.3.1", + "description": "Generate outline for laser pointer tool", + "type": "module", + "types": "./dist/types/index.d.ts", + "main": "./dist/prod/index.js", + "module": "./dist/prod/index.js", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "development": "./dist/dev/index.js", + "production": "./dist/prod/index.js", + "default": "./dist/prod/index.js" + } + }, + "files": [ + "dist/*" + ], + "keywords": [ + "excalidraw", + "laserpointer" + ], + "bugs": "https://github.com/excalidraw/excalidraw/issues", + "repository": "https://github.com/excalidraw/excalidraw", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "scripts": { + "gen:types": "rimraf types && tsc", + "build:esm": "rimraf dist && node ../../scripts/buildBase.js && yarn gen:types" + } +} diff --git a/packages/laser-pointer/src/index.ts b/packages/laser-pointer/src/index.ts new file mode 100644 index 0000000000..2b2103e717 --- /dev/null +++ b/packages/laser-pointer/src/index.ts @@ -0,0 +1,2 @@ +export * from "./state"; +export type { Point } from "./math"; diff --git a/packages/laser-pointer/src/math.ts b/packages/laser-pointer/src/math.ts new file mode 100644 index 0000000000..c0cdfdc5eb --- /dev/null +++ b/packages/laser-pointer/src/math.ts @@ -0,0 +1,105 @@ +export type Point = [x: number, y: number, r: number]; + +export function add([ax, ay, ar]: Point, [bx, by, br]: Point): Point { + return [ax + bx, ay + by, ar + br]; +} + +export function sub([ax, ay, ar]: Point, [bx, by, br]: Point): Point { + return [ax - bx, ay - by, ar - br]; +} + +export function smul([x, y, r]: Point, s: number): Point { + return [x * s, y * s, r * s]; +} + +export function norm([x, y, r]: Point): Point { + return [x / Math.sqrt(x ** 2 + y ** 2), y / Math.sqrt(x ** 2 + y ** 2), r]; +} + +export function rot([x, y, r]: Point, rad: number): Point { + return [ + Math.cos(rad) * x - Math.sin(rad) * y, + Math.sin(rad) * x + Math.cos(rad) * y, + r, + ]; +} + +export function plerp(a: Point, b: Point, t: number): Point { + return add(a, smul(sub(b, a), t)); +} + +export function lerp(a: number, b: number, t: number): number { + return a + (b - a) * t; +} + +export function angle(p: Point, p1: Point, p2: Point) { + return ( + Math.atan2(p2[1] - p[1], p2[0] - p[0]) - + Math.atan2(p1[1] - p[1], p1[0] - p[0]) + ); +} + +export function normAngle(a: number) { + return Math.atan2(Math.sin(a), Math.cos(a)); +} + +export function mag([x, y]: Point) { + return Math.sqrt(x ** 2 + y ** 2); +} + +export function dist([ax, ay]: Point, [bx, by]: Point): number { + return Math.sqrt((bx - ax) ** 2 + (by - ay) ** 2); +} + +export function getCircleAndPerpendicularLineIntersectionsAtPoint( + point: Point, + direction: Point, + radius: number, +): [Point, Point] { + return [ + add(point, smul(norm(rot(direction, Math.PI / 2)), radius)), + add(point, smul(norm(rot(direction, -Math.PI / 2)), radius)), + ]; +} + +export function runLength(ps: Point[]): number { + if (ps.length < 2) { + return 0; + } + + let len = 0; + + for (let i = 1; i <= ps.length - 1; i++) { + len += dist(ps[i - 1], ps[i]); + } + + len += dist(ps[ps.length - 2], ps[ps.length - 1]); + + return len; +} + +export const clamp = (v: number, min: number, max: number) => + Math.max(min, Math.min(max, v)); + +export function distancePointToSegment(p3: Point, p1: Point, p2: Point) { + const sMag = dist(p1, p2); + + if (sMag === 0) { + return dist(p3, p1); + } + + const u = clamp( + ((p3[0] - p1[0]) * (p2[0] - p1[0]) + (p3[1] - p1[1]) * (p2[1] - p1[1])) / + sMag ** 2, + 0, + 1, + ); + + const pi: Point = [ + p1[0] + u * (p2[0] - p1[0]), + p1[1] + u * (p2[1] - p1[1]), + p3[2], + ]; + + return dist(pi, p3); +} diff --git a/packages/laser-pointer/src/simplify.ts b/packages/laser-pointer/src/simplify.ts new file mode 100644 index 0000000000..95d0ddbdaa --- /dev/null +++ b/packages/laser-pointer/src/simplify.ts @@ -0,0 +1,42 @@ +import { type Point, distancePointToSegment } from "./math"; + +export function douglasPeucker(points: Point[], epsilon: number): Point[] { + if (epsilon === 0) { + return points; + } + + if (points.length <= 2) { + return points; + } + + const first = points[0]; + const last = points[points.length - 1]; + + const [maxDistance, maxIndex] = points.reduce( + ([maxDistance, maxIndex], point, index) => { + const distance = distancePointToSegment(point, first, last); + + return distance > maxDistance + ? [distance, index] + : [maxDistance, maxIndex]; + }, + [0, -1], + ); + + if (maxDistance >= epsilon) { + const maxIndexPoint = points[maxIndex]; + + return [ + ...douglasPeucker( + [first, ...points.slice(1, maxIndex), maxIndexPoint], + epsilon, + ).slice(0, -1), + maxIndexPoint, + ...douglasPeucker( + [maxIndexPoint, ...points.slice(maxIndex, -1), last], + epsilon, + ).slice(1), + ]; + } + return [first, last]; +} diff --git a/packages/laser-pointer/src/state.ts b/packages/laser-pointer/src/state.ts new file mode 100644 index 0000000000..3da4919960 --- /dev/null +++ b/packages/laser-pointer/src/state.ts @@ -0,0 +1,377 @@ +import * as m from "./math"; +import { douglasPeucker } from "./simplify"; + +import type { Point } from "./math"; + +export type SizeMappingDetails = { + pressure: number; + runningLength: number; + currentIndex: number; + totalLength: number; +}; + +export type LaserPointerOptions = { + size: number; + + streamline: number; + simplify: number; + simplifyPhase: "tail" | "output" | "input"; + + keepHead: boolean; + + sizeMapping: (details: SizeMappingDetails) => number; +}; + +export class LaserPointer { + static defaults: LaserPointerOptions = { + size: 2, + streamline: 0.45, + simplify: 0.1, + simplifyPhase: "output", + keepHead: false, + + sizeMapping: () => 1, + }; + + static constants = { + cornerDetectionMaxAngle: 75, + cornerDetectionVariance: (s: number) => (s > 35 ? 0.5 : 1), + maxTailLength: 50, + }; + + options: LaserPointerOptions; + constructor(options: Partial) { + this.options = Object.assign({}, LaserPointer.defaults, options); + } + + originalPoints: Point[] = []; + + private stablePoints: Point[] = []; + private tailPoints: Point[] = []; + + private isFresh = true; + + private get lastPoint(): Point { + return ( + this.tailPoints[this.tailPoints.length - 1] ?? + this.stablePoints[this.stablePoints.length - 1] + ); + } + + addPoint(point: Point) { + const lastPoint = this.originalPoints[this.originalPoints.length - 1]; + + if (lastPoint && lastPoint[0] === point[0] && lastPoint[1] === point[1]) { + return; + } + + this.originalPoints.push(point); + + if (this.isFresh) { + this.isFresh = false; + this.stablePoints.push(point); + return; + } + + if (this.options.streamline > 0) { + point = m.plerp(this.lastPoint, point, 1 - this.options.streamline); + } + + this.tailPoints.push(point); + + if (m.runLength(this.tailPoints) > LaserPointer.constants.maxTailLength) { + this.stabilizeTail(); + } + } + + close() { + this.stabilizeTail(); + } + + stabilizeTail() { + if (this.options.simplify > 0 && this.options.simplifyPhase === "tail") { + throw new Error("Not implemented yet"); + } else { + this.stablePoints.push(...this.tailPoints); + this.tailPoints = []; + } + } + + private getSize( + sizeOverride: number | undefined, + pressure: number, + index: number, + totalLength: number, + runningLength: number, + ) { + return ( + (sizeOverride ?? this.options.size) * + this.options.sizeMapping({ + pressure, + runningLength, + currentIndex: index, + totalLength, + }) + ); + } + + getStrokeOutline(sizeOverride?: number | undefined): Point[] { + if (this.isFresh) { + return []; + } + + let points = [...this.stablePoints, ...this.tailPoints]; + + if (this.options.simplify > 0 && this.options.simplifyPhase === "input") { + points = douglasPeucker(points, this.options.simplify); + } + + const len = points.length; + + if (len === 0) { + return []; + } + + if (len === 1) { + const c = points[0]; + + const size = this.getSize(sizeOverride, c[2], 0, len, 0); + + if (size < 0.5) { + return []; + } + + const ps: Point[] = []; + + for (let theta = 0; theta <= Math.PI * 2; theta += Math.PI / 16) { + ps.push(m.add(c, m.smul(m.rot([1, 0, 0] as Point, theta), size))); + } + + ps.push( + m.add( + c, + m.smul( + [1, 0, 0] as Point, + this.getSize(sizeOverride, c[2], 0, len, 0), + ), + ), + ); + + return ps; + } + + if (len === 2) { + const c = points[0]; + const n = points[1]; + + const cSize = this.getSize(sizeOverride, c[2], 0, len, 0); + const nSize = this.getSize(sizeOverride, n[2], 0, len, 0); + + if (cSize < 0.5 || nSize < 0.5) { + return []; + } + + const ps: Point[] = []; + + const pAngle = m.angle(c, [c[0], c[1] - 100, c[2]] as Point, n); + + for ( + let theta = pAngle; + theta <= Math.PI + pAngle; + theta += Math.PI / 16 + ) { + ps.push(m.add(c, m.smul(m.rot([1, 0, 0] as Point, theta), cSize))); + } + + for ( + let theta = Math.PI + pAngle; + theta <= Math.PI * 2 + pAngle; + theta += Math.PI / 16 + ) { + ps.push(m.add(n, m.smul(m.rot([1, 0, 0] as Point, theta), nSize))); + } + + ps.push(ps[0]); + + return ps; + } + + const forwardPoints: Point[] = []; + const backwardPoints: Point[] = []; + + let speed = 0; + let prevSpeed = 0; + + let visibleStartIndex = 0; + let runningLength = 0; + + for (let i = 1; i < len - 1; i++) { + const p = points[i - 1]; + const c = points[i]; + const n = points[i + 1]; + + const pressure = c[2]; + + const d = m.dist(p, c); + runningLength += d; + speed = prevSpeed + (d - prevSpeed) * 0.2; + + const cSize = this.getSize(sizeOverride, pressure, i, len, runningLength); + + if (cSize === 0) { + visibleStartIndex = i + 1; + continue; + } + + const dirPC = m.norm(m.sub(p, c)); + const dirNC = m.norm(m.sub(n, c)); + const p1dirPC = m.rot(dirPC, Math.PI / 2); + const p2dirPC = m.rot(dirPC, -Math.PI / 2); + const p1dirNC = m.rot(dirNC, Math.PI / 2); + const p2dirNC = m.rot(dirNC, -Math.PI / 2); + + const p1PC = m.add(c, m.smul(p1dirPC, cSize)); + const p2PC = m.add(c, m.smul(p2dirPC, cSize)); + const p1NC = m.add(c, m.smul(p1dirNC, cSize)); + const p2NC = m.add(c, m.smul(p2dirNC, cSize)); + + const ftdir = m.add(p1dirPC, p2dirNC); + const btdir = m.add(p2dirPC, p1dirNC); + + const paPC = m.add( + c, + m.smul(m.mag(ftdir) === 0 ? dirPC : m.norm(ftdir), cSize), + ); + const paNC = m.add( + c, + m.smul(m.mag(btdir) === 0 ? dirNC : m.norm(btdir), cSize), + ); + + const cAngle = m.normAngle(m.angle(c, p, n)); + const D_ANGLE = + (LaserPointer.constants.cornerDetectionMaxAngle / 180) * + Math.PI * + LaserPointer.constants.cornerDetectionVariance(speed); + + if (Math.abs(cAngle) < D_ANGLE) { + const tAngle = Math.abs(m.normAngle(Math.PI - cAngle)); // turn angle + + if (tAngle === 0) { + continue; + } + + if (cAngle < 0) { + backwardPoints.push(p2PC, paNC); + + for (let theta = 0; theta <= tAngle; theta += tAngle / 4) { + forwardPoints.push(m.add(c, m.rot(m.smul(p1dirPC, cSize), theta))); + } + + for (let theta = tAngle; theta >= 0; theta -= tAngle / 4) { + backwardPoints.push(m.add(c, m.rot(m.smul(p1dirPC, cSize), theta))); + } + + backwardPoints.push(paNC, p1NC); + } else { + forwardPoints.push(p1PC, paPC); + + for (let theta = 0; theta <= tAngle; theta += tAngle / 4) { + backwardPoints.push( + m.add(c, m.rot(m.smul(p1dirPC, -cSize), -theta)), + ); + } + + for (let theta = tAngle; theta >= 0; theta -= tAngle / 4) { + forwardPoints.push( + m.add(c, m.rot(m.smul(p1dirPC, -cSize), -theta)), + ); + } + forwardPoints.push(paPC, p2NC); + } + } else { + forwardPoints.push(paPC); + backwardPoints.push(paNC); + } + + prevSpeed = speed; + } + + if (visibleStartIndex >= len - 2) { + if (this.options.keepHead) { + const c = points[len - 1]; + + const ps: Point[] = []; + + for (let theta = 0; theta <= Math.PI * 2; theta += Math.PI / 16) { + ps.push( + m.add( + c, + m.smul(m.rot([1, 0, 0] as Point, theta), this.options.size), + ), + ); + } + + ps.push(m.add(c, m.smul([1, 0, 0] as Point, this.options.size))); + + return ps; + } + + return []; + } + + const first = points[visibleStartIndex]; + const second = points[visibleStartIndex + 1]; + const penultimate = points[len - 2]; + const ultimate = points[len - 1]; + + const dirFS = m.norm(m.sub(second, first)); + const dirPU = m.norm(m.sub(penultimate, ultimate)); + + const ppdirFS = m.rot(dirFS, -Math.PI / 2); + const ppdirPU = m.rot(dirPU, Math.PI / 2); + + const startCapSize = this.getSize(sizeOverride, first[2], 0, len, 0); + const startCap: Point[] = []; + + const endCapSize = this.options.keepHead + ? this.options.size + : this.getSize(sizeOverride, penultimate[2], len - 2, len, runningLength); + + const endCap: Point[] = []; + + // Lowered threshold to 0.1, + // ensuring virtually all strokes get proper rounded caps for visual consistency. + if (startCapSize > 0.1) { + for (let theta = 0; theta <= Math.PI; theta += Math.PI / 16) { + startCap.unshift( + m.add(first, m.rot(m.smul(ppdirFS, startCapSize), -theta)), + ); + } + + startCap.unshift(m.add(first, m.smul(ppdirFS, -startCapSize))); + } else { + startCap.push(first); + } + + for (let theta = 0; theta <= Math.PI * 3; theta += Math.PI / 16) { + endCap.push(m.add(ultimate, m.rot(m.smul(ppdirPU, -endCapSize), -theta))); + } + + const strokeOutline = [ + ...startCap, + ...forwardPoints, + ...endCap.reverse(), + ...backwardPoints.reverse(), + ]; + + if (startCap.length > 0) { + strokeOutline.push(startCap[0]); + } + + if (this.options.simplify > 0 && this.options.simplifyPhase === "output") { + return douglasPeucker(strokeOutline, this.options.simplify); + } + + return strokeOutline; + } +} diff --git a/packages/laser-pointer/tsconfig.json b/packages/laser-pointer/tsconfig.json new file mode 100644 index 0000000000..6450145b1c --- /dev/null +++ b/packages/laser-pointer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist/types" + }, + "include": ["src/**/*", "global.d.ts"], + "exclude": ["**/*.test.*", "tests", "types", "examples", "dist"] +} diff --git a/packages/tsconfig.base.json b/packages/tsconfig.base.json index 1bd75695b5..0475a9bcea 100644 --- a/packages/tsconfig.base.json +++ b/packages/tsconfig.base.json @@ -17,6 +17,8 @@ "@excalidraw/element/*": ["./element/src/*"], "@excalidraw/excalidraw": ["./excalidraw/index.tsx"], "@excalidraw/excalidraw/*": ["./excalidraw/*"], + "@excalidraw/laser-pointer": ["./laser-pointer/src/index.ts"], + "@excalidraw/laser-pointer/*": ["./laser-pointer/src/*"], "@excalidraw/math": ["./math/src/index.ts"], "@excalidraw/math/*": ["./math/src/*"], "@excalidraw/utils": ["./utils/src/index.ts"], diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap index 67c2273766..e808df6e22 100644 --- a/packages/utils/tests/__snapshots__/export.test.ts.snap +++ b/packages/utils/tests/__snapshots__/export.test.ts.snap @@ -30,7 +30,8 @@ exports[`exportToSvg > with default arguments 1`] = ` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeVariability": "constant", + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, diff --git a/packages/utils/tests/__snapshots__/utils.test.ts.snap b/packages/utils/tests/__snapshots__/utils.test.ts.snap index fdcb71295c..b5a0469838 100644 --- a/packages/utils/tests/__snapshots__/utils.test.ts.snap +++ b/packages/utils/tests/__snapshots__/utils.test.ts.snap @@ -23,7 +23,7 @@ exports[`exportToSvg > with default arguments 1`] = ` "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", - "currentItemStrokeWidth": 2, + "currentItemStrokeWidthKey": "medium", "currentItemTextAlign": "left", "cursorButton": "up", "defaultSidebarDockedPreference": false, diff --git a/tsconfig.json b/tsconfig.json index 5948a866aa..da95861337 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,6 +27,8 @@ "@excalidraw/element/*": ["./packages/element/src/*"], "@excalidraw/fractional-indexing": ["./packages/fractional-indexing/src/index.ts"], "@excalidraw/fractional-indexing/*": ["./packages/fractional-indexing/src/*"], + "@excalidraw/laser-pointer": ["./packages/laser-pointer/src/index.ts"], + "@excalidraw/laser-pointer/*": ["./packages/laser-pointer/src/*"], "@excalidraw/math": ["./packages/math/src/index.ts"], "@excalidraw/math/*": ["./packages/math/src/*"], "@excalidraw/utils": ["./packages/utils/src/index.ts"], diff --git a/vercel.json b/vercel.json index 6949dfcef2..faa0838227 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,4 @@ { - "public": true, "headers": [ { "source": "/(.*)", diff --git a/vitest.config.mts b/vitest.config.mts index 50d737c86b..13874fbbba 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -59,6 +59,17 @@ export default defineConfig({ "./packages/fractional-indexing/src/$1", ), }, + { + find: /^@excalidraw\/laser-pointer$/, + replacement: path.resolve( + __dirname, + "./packages/laser-pointer/src/index.ts", + ), + }, + { + find: /^@excalidraw\/laser-pointer\/(.*?)/, + replacement: path.resolve(__dirname, "./packages/laser-pointer/src/$1"), + }, ], }, //@ts-ignore diff --git a/yarn.lock b/yarn.lock index 09381a0264..e2eb9e911b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1521,11 +1521,6 @@ resolved "https://registry.yarnpkg.com/@excalidraw/eslint-config/-/eslint-config-1.0.3.tgz#2122ef7413ae77874ae9848ce0f1c6b3f0d8bbbd" integrity sha512-GemHNF5Z6ga0BWBSX7GJaNBUchLu6RwTcAB84eX1MeckRNhNasAsPCdelDlFalz27iS4RuYEQh0bPE8SRxJgbQ== -"@excalidraw/laser-pointer@1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@excalidraw/laser-pointer/-/laser-pointer-1.3.1.tgz#7c40836598e8e6ad91f01057883ed8b88fb9266c" - integrity sha512-psA1z1N2qeAfsORdXc9JmD2y4CmDwmuMRxnNdJHZexIcPwaNEyIpNcelw+QkL9rz9tosaN9krXuKaRqYpRAR6g== - "@excalidraw/markdown-to-text@0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"