feat(editor): LaserPointer based freedraw (#11507)

Introduces constant width freedraw mode, keeping the original variable mode as default.

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
Márk Tolmács
2026-06-24 09:16:22 +02:00
committed by GitHub
parent 0642e72cfa
commit cd514d72d6
48 changed files with 1635 additions and 225 deletions
+7
View File
@@ -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: {
+2 -1
View File
@@ -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",
+43 -4
View File
@@ -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<StrokeWidthKey | "extraBold", ExcalidrawElement["strokeWidth"]>
> = {
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<StrokeWidthKey | "extraBold", ExcalidrawElement["strokeWidth"]>
> = {
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;
+2
View File
@@ -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" ||
+6
View File
@@ -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<ExcalidrawFreeDrawElement> => {
@@ -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,
},
};
};
+75 -7
View File
@@ -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];
};
+8
View File
@@ -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" };
@@ -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"
@@ -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,
@@ -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",
);
+108 -19
View File
@@ -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 <T extends Primitive>(
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 <T extends Primitive>(
let ret: T | null = null;
if (editingTextElement) {
ret = getAttribute(editingTextElement);
ret = getValue(editingTextElement);
}
if (!ret) {
@@ -214,7 +224,7 @@ export const getFormValue = function <T extends Primitive>(
: 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<ExcalidrawElement["fillStyle"]>({
},
});
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<StrokeWidthKey>({
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<
<fieldset>
<legend>{t("labels.strokeWidth")}</legend>
<div className="buttonList">
<RadioSelection
<RadioSelection<StrokeWidthKey>
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<ExcalidrawElement["roughness"]>({
),
});
export const actionChangeFreedrawMode = register<StrokeVariability>({
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 (
<fieldset>
<legend>{t("labels.pressure")}</legend>
<div className="buttonList">
<RadioSelection<StrokeVariability>
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)}
/>
</div>
</fieldset>
);
},
});
export const actionChangeStrokeStyle = register<
ExcalidrawElement["strokeStyle"]
>({
+1
View File
@@ -13,6 +13,7 @@ export {
actionChangeStrokeWidth,
actionChangeFillStyle,
actionChangeSloppiness,
actionChangeFreedrawMode,
actionChangeOpacity,
actionChangeFontSize,
actionChangeFontFamily,
+1
View File
@@ -68,6 +68,7 @@ export type ActionName =
| "changeStrokeWidth"
| "changeStrokeShape"
| "changeSloppiness"
| "changeFreedrawMode"
| "changeStrokeStyle"
| "changeArrowhead"
| "changeArrowType"
+9 -2
View File
@@ -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 },
+9 -3
View File
@@ -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),
+32 -9
View File
@@ -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<AppProps, AppState> {
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<AppProps, AppState> {
return {
penMode: force ?? !prevState.penMode,
penDetected: true,
currentItemStrokeVariability: "variable",
};
});
};
@@ -6304,7 +6308,7 @@ class App extends React.Component<AppProps, AppState> {
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<AppProps, AppState> {
return {
penMode: true,
penDetected: true,
currentItemStrokeVariability: "variable",
};
});
}
@@ -8992,6 +8997,8 @@ class App extends React.Component<AppProps, AppState> {
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<AppProps, AppState> {
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<LocalPoint>(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<AppProps, AppState> {
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<AppProps, AppState> {
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<AppProps, AppState> {
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<AppProps, AppState> {
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<AppProps, AppState> {
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<AppProps, AppState> {
: 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<AppProps, AppState> {
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,
+68
View File
@@ -1249,6 +1249,74 @@ export const SloppinessCartoonistIcon = createIcon(
modifiedTablerIconProps,
);
export const strokeVariabilityConstantIcon = createIcon(
<g>
<path
d="M4 12 C 5 8, 6 8, 8 12"
fill="none"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 12 C 9 16, 10 16, 12 12"
fill="none"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 12 C 14 8, 15 8, 16 12"
fill="none"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 12 C 17 16, 18 16, 19 12"
fill="none"
strokeWidth="1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>,
tablerIconProps,
);
export const strokeVariabilityVariableIcon = createIcon(
<g>
<path
d="M4 12 C 5 8, 6 8, 8 12"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 12 C 9 16, 10 16, 12 12"
fill="none"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 12 C 14 8, 15 8, 16 12"
fill="none"
strokeWidth="2.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 12 C 17 16, 18 16, 19 12"
fill="none"
strokeWidth="3.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>,
tablerIconProps,
);
export const EdgeSharpIcon = createIcon(
<svg strokeWidth="1.5">
<path d="M3.33334 9.99998V6.66665C3.33334 6.04326 3.33403 4.9332 3.33539 3.33646C4.95233 3.33436 6.06276 3.33331 6.66668 3.33331H10" />
+50
View File
@@ -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<StrokeVariability>([
"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",
+2 -1
View File
@@ -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 };
+3
View File
@@ -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",
+1
View File
@@ -9,6 +9,7 @@ export {
hasBackground,
hasStrokeWidth,
hasStrokeStyle,
hasFreedrawMode,
canHaveArrowheads,
canChangeRoundness,
} from "@excalidraw/element";
@@ -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,
@@ -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,
@@ -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,
@@ -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);
@@ -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);
@@ -240,8 +240,12 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
"seed": Any<Number>,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
"strokeOptions": {
"streamline": "0.50000",
"variability": "variable",
},
"strokeStyle": "solid",
"strokeWidth": 2,
"strokeWidth": 1,
"type": "freedraw",
"updated": 1,
"version": 2,
@@ -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(),
@@ -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(<Excalidraw handleKeyboardGlobally={true} />);
});
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");
});
});
+13 -2
View File
@@ -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;
@@ -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,
+4 -1
View File
@@ -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;
+21
View File
@@ -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.
+23
View File
@@ -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. |
View File
+34
View File
@@ -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"
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from "./state";
export type { Point } from "./math";
+105
View File
@@ -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);
}
+42
View File
@@ -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];
}
+377
View File
@@ -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<LaserPointerOptions>) {
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;
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/types"
},
"include": ["src/**/*", "global.d.ts"],
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
}
+2
View File
@@ -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"],
@@ -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,
@@ -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,
+2
View File
@@ -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"],
-1
View File
@@ -1,5 +1,4 @@
{
"public": true,
"headers": [
{
"source": "/(.*)",
+11
View File
@@ -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
-5
View File
@@ -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"