Compare commits

..

1 Commits

Author SHA1 Message Date
dwelle 0bd3a077bf feat(editor): remove non-overlapping frame children on restore 2026-06-14 14:10:24 +02:00
63 changed files with 853 additions and 1997 deletions
-7
View File
@@ -1,7 +0,0 @@
# VITE_DEBUG_DOM
# When "true", testing-library failures (waitFor / getBy*) include the full
# serialized DOM in the error message. It's off by default because it's noisy.
#
# Flip it to "true" (or use `VITE_DEBUG_DOM=true yarn test`) when you need to
# inspect the DOM of a failing test.
VITE_DEBUG_DOM=false
-7
View File
@@ -82,13 +82,6 @@ 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: {
+1 -2
View File
@@ -57,8 +57,7 @@
"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: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:packages": "yarn build:common && yarn build:fractional-indexing && 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",
+4 -43
View File
@@ -404,47 +404,11 @@ export const ROUGHNESS = {
cartoonist: 2,
} as const;
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"]>
> = {
export const STROKE_WIDTH = {
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, // 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";
extraBold: 4,
} as const;
export const DEFAULT_ELEMENT_PROPS: {
strokeColor: ExcalidrawElement["strokeColor"];
@@ -459,7 +423,7 @@ export const DEFAULT_ELEMENT_PROPS: {
strokeColor: COLOR_PALETTE.black,
backgroundColor: COLOR_PALETTE.transparent,
fillStyle: "solid",
strokeWidth: STROKE_WIDTH[DEFAULT_ELEMENT_STROKE_WIDTH_KEY],
strokeWidth: 2,
strokeStyle: "solid",
roughness: ROUGHNESS.artist,
opacity: 100,
@@ -550,6 +514,3 @@ 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.2;
+1 -2
View File
@@ -1,8 +1,7 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "../"
"outDir": "./dist/types"
},
"include": ["src/**/*", "global.d.ts"],
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
+8 -14
View File
@@ -643,13 +643,10 @@ const getBindingStrategyForDraggingBindingElementEndpoints_simple = (
let start: BindingStrategy = { mode: undefined };
let end: BindingStrategy = { mode: undefined };
if (arrow.points.length < 2) {
console.error(
"Attempting to bind a linear element with less than 2 points",
);
// a single-point can't be bound -> cancel
return { start: { mode: undefined }, end: { mode: undefined } };
}
invariant(
arrow.points.length > 1,
"Do not attempt to bind linear elements with a single point",
);
// If none of the ends are dragged, we don't change anything
if (!startDragged && !endDragged) {
@@ -893,13 +890,10 @@ const getBindingStrategyForDraggingBindingElementEndpoints_complex = (
let start: BindingStrategy = { mode: undefined };
let end: BindingStrategy = { mode: undefined };
if (arrow.points.length < 2) {
console.error(
"Attempting to bind a linear element with less than 2 points",
);
// a single-point can't be bound -> cancel
return { start: { mode: undefined }, end: { mode: undefined } };
}
invariant(
arrow.points.length > 1,
"Do not attempt to bind linear elements with a single point",
);
// If none of the ends are dragged, we don't change anything
if (!startDragged && !endDragged) {
-2
View File
@@ -38,8 +38,6 @@ 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,7 +4,6 @@ import {
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
DEFAULT_STROKE_STREAMLINE,
VERTICAL_ALIGN,
randomInteger,
randomId,
@@ -445,7 +444,6 @@ export const newFreeDrawElement = (
type: "freedraw";
points?: ExcalidrawFreeDrawElement["points"];
simulatePressure: boolean;
strokeOptions?: ExcalidrawFreeDrawElement["strokeOptions"];
pressures?: ExcalidrawFreeDrawElement["pressures"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawFreeDrawElement> => {
@@ -454,10 +452,6 @@ export const newFreeDrawElement = (
points: opts.points || [],
pressures: opts.pressures || [],
simulatePressure: opts.simulatePressure,
strokeOptions: opts.strokeOptions ?? {
variability: "variable",
streamline: DEFAULT_STROKE_STREAMLINE,
},
};
};
+51 -38
View File
@@ -889,10 +889,8 @@ export const renderElement = (
case "embeddable": {
if (renderConfig.isExporting) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
const cx = centerX + appState.scrollX;
const cy = centerY + appState.scrollY;
const cx = (x1 + x2) / 2 + appState.scrollX;
const cy = (y1 + y2) / 2 + appState.scrollY;
let shiftX = (x2 - x1) / 2 - (element.x - x1);
let shiftY = (y2 - y1) / 2 - (element.y - y1);
if (isTextElement(element)) {
@@ -914,49 +912,64 @@ export const renderElement = (
const boundTextElement = getBoundTextElement(element, elementsMap);
if (isArrowElement(element) && boundTextElement) {
// Draw arrow directly as vector and clear label hole separately.
// This avoids temp-canvas bitmap blit which introduces resampling blur.
const tempCanvas = document.createElement("canvas");
const tempCanvasContext = tempCanvas.getContext("2d")!;
// Take max dimensions of arrow canvas so that when canvas is rotated
// the arrow doesn't get clipped
const maxDim = Math.max(distance(x1, x2), distance(y1, y2));
const padding = getCanvasPadding(element);
tempCanvas.width =
maxDim * appState.exportScale + padding * 10 * appState.exportScale;
tempCanvas.height =
maxDim * appState.exportScale + padding * 10 * appState.exportScale;
tempCanvasContext.translate(
tempCanvas.width / 2,
tempCanvas.height / 2,
);
tempCanvasContext.scale(appState.exportScale, appState.exportScale);
// Shift the canvas to left most point of the arrow
shiftX = element.width / 2 - (element.x - x1);
shiftY = element.height / 2 - (element.y - y1);
context.save();
context.rotate(element.angle);
context.translate(-shiftX, -shiftY);
drawElementOnCanvas(element, rc, context, renderConfig);
context.restore();
tempCanvasContext.rotate(element.angle);
const tempRc = rough.canvas(tempCanvas);
tempCanvasContext.translate(-shiftX, -shiftY);
drawElementOnCanvas(element, tempRc, tempCanvasContext, renderConfig);
tempCanvasContext.translate(shiftX, shiftY);
tempCanvasContext.rotate(-element.angle);
// Shift the canvas to center of bound text
const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
boundTextElement,
elementsMap,
);
const holeX =
boundTextCx -
centerX -
boundTextElement.width / 2 -
BOUND_TEXT_PADDING;
const holeY =
boundTextCy -
centerY -
boundTextElement.height / 2 -
BOUND_TEXT_PADDING;
const holeWidth = boundTextElement.width + BOUND_TEXT_PADDING * 2;
const holeHeight = boundTextElement.height + BOUND_TEXT_PADDING * 2;
const boundTextShiftX = (x1 + x2) / 2 - boundTextCx;
const boundTextShiftY = (y1 + y2) / 2 - boundTextCy;
tempCanvasContext.translate(-boundTextShiftX, -boundTextShiftY);
const isTransparentHole =
"viewBackgroundColor" in appState &&
(appState.viewBackgroundColor === "transparent" ||
!appState.viewBackgroundColor);
if (!isTransparentHole) {
context.save();
context.fillStyle = applyDarkModeFilter(
renderConfig.canvasBackgroundColor,
renderConfig.theme === THEME.DARK,
);
context.fillRect(holeX, holeY, holeWidth, holeHeight);
context.restore();
} else {
context.clearRect(holeX, holeY, holeWidth, holeHeight);
}
// Clear the bound text area
tempCanvasContext.clearRect(
-boundTextElement.width / 2,
-boundTextElement.height / 2,
boundTextElement.width,
boundTextElement.height,
);
context.scale(1 / appState.exportScale, 1 / appState.exportScale);
context.drawImage(
tempCanvas,
-tempCanvas.width / 2,
-tempCanvas.height / 2,
tempCanvas.width,
tempCanvas.height,
);
} else {
context.rotate(element.angle);
+7 -69
View File
@@ -1,6 +1,5 @@
import { simplify } from "points-on-curve";
import { getStroke } from "perfect-freehand";
import { LaserPointer } from "@excalidraw/laser-pointer";
import {
type GeometricShape,
@@ -25,7 +24,6 @@ import {
COLOR_PALETTE,
LINE_POLYGON_POINT_MERGE_DISTANCE,
applyDarkModeFilter,
DEFAULT_STROKE_STREAMLINE,
} from "@excalidraw/common";
import { RoughGenerator } from "roughjs/bin/generator";
@@ -1173,87 +1171,27 @@ const getFreeDrawSvgPath = (element: ExcalidrawFreeDrawElement) => {
) as SVGPathString;
};
/**
* 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,
} as const;
const CONSTANT_WIDTH_FREEDRAW = {
/** Stroke size relative to `strokeWidth` for uniform (laser) strokes. */
SIZE_FACTOR: 1.4,
} as const;
const getFreedrawStreamline = (element: ExcalidrawFreeDrawElement) =>
element.strokeOptions?.streamline ?? DEFAULT_STROKE_STREAMLINE;
/**
* Pressure-sensitive (variable width) freedraw outline, rendered with
* perfect-freehand. This is the original Excalidraw freedraw look.
*/
const getVariableWidthFreedrawOutline = (
export const getFreedrawOutlinePoints = (
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]] as [number, number, number],
)
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
: [[0, 0, 0.5]];
return getStroke(inputPoints as number[][], {
simulatePressure: element.simulatePressure,
size: element.strokeWidth * VARIABLE_WIDTH_FREEDRAW.SIZE_FACTOR,
thinning: VARIABLE_WIDTH_FREEDRAW.THINNING,
smoothing: VARIABLE_WIDTH_FREEDRAW.SMOOTHING,
streamline: getFreedrawStreamline(element),
size: element.strokeWidth * 4.25,
thinning: 0.6,
smoothing: 0.5,
streamline: 0.5,
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,20 +384,12 @@ 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" };
-60
View File
@@ -178,64 +178,6 @@ describe("binding for simple arrows", () => {
});
});
describe("self-binding (both ends to the same element) single-click finalize", () => {
// rect spans x:200..400, y:200..400; orbit ring is ~15px outside the outline
const INSIDE: [number, number] = [250, 250];
const ORBIT_LEFT: [number, number] = [187, 300];
const ORBIT_RIGHT: [number, number] = [413, 300];
const MIDDLE: [number, number] = [550, 100];
beforeEach(async () => {
mouse.reset();
await act(() => setLanguage(defaultLang));
await render(<Excalidraw handleKeyboardGlobally={true} />);
UI.createElement("rectangle", {
x: 200,
y: 200,
width: 200,
height: 200,
});
});
const drawSelfArrow = (start: [number, number], end: [number, number]) => {
UI.clickTool("arrow");
mouse.reset();
mouse.clickAt(...start);
mouse.moveTo(...MIDDLE);
mouse.clickAt(...MIDDLE); // commit a middle point so it's a multi-point arrow
mouse.moveTo(...end);
mouse.clickAt(...end); // single click at the end
};
it("orbit -> orbit finalizes on a single click", () => {
drawSelfArrow(ORBIT_LEFT, ORBIT_RIGHT);
const arrow = h.elements[h.elements.length - 1] as ExcalidrawArrowElement;
expect(h.state.multiElement).toBe(null);
expect(h.state.activeTool.type).toBe("selection");
expect(arrow.startBinding?.elementId).toBe(arrow.endBinding?.elementId);
expect(arrow.endBinding?.elementId).not.toBe(undefined);
});
it("inside -> orbit finalizes on a single click", () => {
drawSelfArrow(INSIDE, ORBIT_RIGHT);
const arrow = h.elements[h.elements.length - 1] as ExcalidrawArrowElement;
expect(h.state.multiElement).toBe(null);
expect(h.state.activeTool.type).toBe("selection");
expect(arrow.startBinding?.elementId).toBe(arrow.endBinding?.elementId);
expect(arrow.endBinding?.elementId).not.toBe(undefined);
});
it("inside -> inside keep in multi-point mode (no single-click finalize)", () => {
drawSelfArrow(INSIDE, [INSIDE[0] + 50, INSIDE[1] + 50]); // end dropped inside the rect
// ambiguous → must be confirmed with a second click, so still in progress
expect(h.state.multiElement).not.toBe(null);
expect(h.state.activeTool.type).toBe("arrow");
});
});
describe("when arrow is outside of shape", () => {
beforeEach(async () => {
mouse.reset();
@@ -461,7 +403,6 @@ describe("binding for simple arrows", () => {
mouse.moveTo(340, 251);
mouse.moveTo(410, 251);
mouse.clickAt(410, 251);
mouse.clickAt(410, 251);
const arrow = h.elements[h.elements.length - 1] as any;
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
@@ -506,7 +447,6 @@ describe("binding for simple arrows", () => {
mouse.moveTo(350, 251);
mouse.moveTo(410, 251);
mouse.clickAt(410, 251);
mouse.clickAt(410, 251);
const arrow = API.getSelectedElement() as ExcalidrawArrowElement;
+105
View File
@@ -3,6 +3,7 @@ import {
Excalidraw,
} from "@excalidraw/excalidraw";
import { arrayToMap } from "@excalidraw/common";
import { degreesToRadians, pointFrom } from "@excalidraw/math";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { Keyboard, Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
@@ -14,6 +15,8 @@ import {
import { getSelectedElements } from "@excalidraw/excalidraw/scene";
import type { Degrees } from "@excalidraw/math";
import { elementOverlapsWithFrame } from "../src/frame";
import type {
@@ -154,6 +157,108 @@ describe("adding elements to frames", () => {
).toBe(true);
});
it("uses precise geometry (not the bounding box) to detect frame overlap", () => {
// a 300x200 frame at the origin, with shapes positioned just outside each
// corner so that their bounding box clips the frame but their actual
// outline does not. The precise `isElementIntersectingFrame` must report
// no overlap.
const overlapFrame = API.createElement({
type: "frame",
x: 0,
y: 0,
width: 300,
height: 200,
});
// clips the bottom-right corner; the rhombus tip stays out
const diamond = API.createElement({
type: "diamond",
x: 290,
y: 175,
width: 100,
height: 100,
});
// clips the bottom-left corner; rotated (NOT 45°, which would degenerate
// into an axis-aligned square whose shape fills its AABB)
const diamondRotated = API.createElement({
type: "diamond",
x: -50,
y: 195,
width: 100,
height: 100,
angle: degreesToRadians(45 as Degrees),
});
// clips the top-left corner; the curve doesn't reach the corner
const ellipse = API.createElement({
type: "ellipse",
x: -90,
y: -90,
width: 100,
height: 100,
});
// clips the top edge; the polyline weaves outside the frame
const line = API.createElement({
type: "line",
x: 130,
y: -20,
width: 293,
height: 183,
points: [
pointFrom(0, 0),
pointFrom(-165, -96),
pointFrom(-293, -16),
pointFrom(-162, 87),
],
});
// clips the bottom-left corner; the stroke stays outside
const freedraw = API.createElement({
type: "freedraw",
x: -15,
y: 180,
width: 207,
height: 209,
points: [
pointFrom(0, 0),
pointFrom(-69, 99),
pointFrom(-33, 183),
pointFrom(48, 209),
pointFrom(124, 195),
pointFrom(138, 145),
pointFrom(123, 85),
],
});
// clips the top-right corner, but its rounded corner is cut away from
// exactly that corner — so its actual outline stays outside
const roundedRect = API.createElement({
type: "rectangle",
x: 295,
y: -100,
width: 160,
height: 110,
roundness: { type: 3 },
});
API.setElements([
overlapFrame,
diamond,
diamondRotated,
ellipse,
line,
freedraw,
roundedRect,
]);
const elementsMap = arrayToMap(h.elements);
const f = overlapFrame as ExcalidrawFrameLikeElement;
expect(elementOverlapsWithFrame(diamond, f, elementsMap)).toBe(false);
expect(elementOverlapsWithFrame(diamondRotated, f, elementsMap)).toBe(
false,
);
expect(elementOverlapsWithFrame(ellipse, f, elementsMap)).toBe(false);
expect(elementOverlapsWithFrame(line, f, elementsMap)).toBe(false);
expect(elementOverlapsWithFrame(freedraw, f, elementsMap)).toBe(false);
expect(elementOverlapsWithFrame(roundedRect, f, elementsMap)).toBe(false);
});
it("should not add a newly created element to a frame behind a non-frame element", () => {
const cover = API.createElement({
id: "cover",
-1
View File
@@ -1,7 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"rootDir": "../",
"outDir": "./dist/types"
},
"include": ["src/**/*", "global.d.ts"],
@@ -5,7 +5,6 @@ import {
VERTICAL_ALIGN,
arrayToMap,
getFontString,
getStrokeWidthByKey,
} from "@excalidraw/common";
import {
getOriginalContainerHeightFromCache,
@@ -250,10 +249,7 @@ export const actionWrapTextInContainer = register({
fillStyle: appState.currentItemFillStyle,
strokeColor: appState.currentItemStrokeColor,
roughness: appState.currentItemRoughness,
strokeWidth: getStrokeWidthByKey(
"rectangle",
appState.currentItemStrokeWidthKey,
),
strokeWidth: appState.currentItemStrokeWidth,
strokeStyle: appState.currentItemStrokeStyle,
roundness:
appState.currentItemRoundness === "round"
+1 -39
View File
@@ -54,7 +54,6 @@ export const actionFinalize = register<FormData>({
label: "",
trackEvent: false,
perform: (elements, appState, data, app) => {
let shouldCommit = true;
let newElements = elements;
const { interactiveCanvas, focusContainer, scene } = app;
const elementsMap = scene.getNonDeletedElementsMap();
@@ -223,44 +222,9 @@ export const actionFinalize = register<FormData>({
!lastCommittedPoint ||
points[points.length - 1] !== lastCommittedPoint
) {
shouldCommit = false;
scene.mutateElement(element, {
points: element.points.slice(0, -1),
});
if (
isBindingElement(element) &&
element.endBinding &&
// after slicing the trailing point a <2-point arrow may be left
element.points.length > 1
) {
const newArrow = !!appState.newElement;
const draggedPoints: PointsPositionUpdates = new Map([
[
element.points.length - 1,
{
point: element.points[element.points.length - 1],
isDragging: false,
},
],
]);
const globalPoint =
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
-1,
elementsMap,
);
bindOrUnbindBindingElement(
element,
draggedPoints,
globalPoint[0],
globalPoint[1],
scene,
appState,
{
newArrow,
},
);
}
}
}
@@ -380,9 +344,7 @@ export const actionFinalize = register<FormData>({
selectedLinearElement,
},
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
captureUpdate: shouldCommit
? CaptureUpdateAction.IMMEDIATELY
: CaptureUpdateAction.NEVER,
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
keyTest: (event, appState) =>
@@ -1,9 +1,8 @@
import { fireEvent, queryByTestId } from "@testing-library/react";
import { queryByTestId } from "@testing-library/react";
import {
COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
FREEDRAW_STROKE_WIDTH,
FONT_FAMILY,
STROKE_WIDTH,
} from "@excalidraw/common";
@@ -129,62 +128,6 @@ 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",
@@ -192,7 +135,7 @@ describe("element locking", () => {
});
const rect2 = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.medium,
strokeWidth: STROKE_WIDTH.bold,
});
API.setElements([rect1, rect2]);
API.setSelectedElements([rect1, rect2]);
@@ -202,17 +145,17 @@ describe("element locking", () => {
queryByTestId(document.body, `strokeWidth-thin`),
).not.toBeChecked();
expect(
queryByTestId(document.body, `strokeWidth-medium`),
queryByTestId(document.body, `strokeWidth-bold`),
).not.toBeChecked();
expect(
queryByTestId(document.body, `strokeWidth-bold`),
queryByTestId(document.body, `strokeWidth-extraBold`),
).not.toBeChecked();
});
it("should show properties of different element types when selected", () => {
const rect = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.medium,
strokeWidth: STROKE_WIDTH.bold,
});
const text = API.createElement({
type: "text",
@@ -221,7 +164,7 @@ describe("element locking", () => {
API.setElements([rect, text]);
API.setSelectedElements([rect, text]);
expect(queryByTestId(document.body, `strokeWidth-medium`)).toBeChecked();
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
expect(queryByTestId(document.body, `font-family-code`)).toHaveClass(
"active",
);
+19 -128
View File
@@ -12,7 +12,7 @@ import {
DEFAULT_FONT_SIZE,
FONT_FAMILY,
ROUNDNESS,
STROKE_WIDTH_KEYS,
STROKE_WIDTH,
VERTICAL_ALIGN,
KEYS,
randomInteger,
@@ -20,11 +20,9 @@ import {
getFontFamilyString,
getLineHeight,
isTransparent,
getStrokeWidthByKey,
reduceToCommonValue,
invariant,
FONT_SIZES,
type StrokeWidthKey,
} from "@excalidraw/common";
import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element";
@@ -72,11 +70,9 @@ import type {
ElementsMap,
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
FontFamilyValues,
StrokeVariability,
TextAlign,
VerticalAlign,
} from "@excalidraw/element/types";
@@ -87,7 +83,6 @@ import type { CaptureUpdateActionType } from "@excalidraw/element";
import { trackEvent } from "../analytics";
import { RadioSelection } from "../components/RadioSelection";
import { ToolButton } from "../components/ToolButton";
import { ColorPicker } from "../components/ColorPicker/ColorPicker";
import { FontPicker } from "../components/FontPicker/FontPicker";
import { IconPicker } from "../components/IconPicker";
@@ -136,8 +131,6 @@ import {
ArrowheadCardinalityOneOrManyIcon,
ArrowheadCardinalityZeroOrManyIcon,
ArrowheadCardinalityZeroOrOneIcon,
strokeVariabilityConstantIcon,
strokeVariabilityVariableIcon,
} from "../components/icons";
import { Fonts } from "../fonts";
@@ -197,11 +190,7 @@ export const changeProperty = (
export const getFormValue = function <T extends Primitive>(
elements: readonly ExcalidrawElement[],
app: AppClassProperties,
/**
* input value (usually the element attribute value,
* but depends on what the action's PanelComponent input expects)
*/
getValue: (element: ExcalidrawElement) => T,
getAttribute: (element: ExcalidrawElement) => T,
elementPredicate: true | ((element: ExcalidrawElement) => boolean),
defaultValue: T | ((isSomeElementSelected: boolean) => T),
): T {
@@ -211,7 +200,7 @@ export const getFormValue = function <T extends Primitive>(
let ret: T | null = null;
if (editingTextElement) {
ret = getValue(editingTextElement);
ret = getAttribute(editingTextElement);
}
if (!ret) {
@@ -225,7 +214,7 @@ export const getFormValue = function <T extends Primitive>(
: selectedElements.filter((el) => elementPredicate(el));
ret =
reduceToCommonValue(targetElements, getValue) ??
reduceToCommonValue(targetElements, getAttribute) ??
(typeof defaultValue === "function"
? defaultValue(true)
: defaultValue);
@@ -555,37 +544,20 @@ export const actionChangeFillStyle = register<ExcalidrawElement["fillStyle"]>({
},
});
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>({
export const actionChangeStrokeWidth = register<
ExcalidrawElement["strokeWidth"]
>({
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: getStrokeWidthForElement(el, value),
strokeWidth: value,
}),
),
appState: { ...appState, currentItemStrokeWidthKey: value },
appState: { ...appState, currentItemStrokeWidth: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
@@ -593,35 +565,35 @@ export const actionChangeStrokeWidth = register<StrokeWidthKey>({
<fieldset>
<legend>{t("labels.strokeWidth")}</legend>
<div className="buttonList">
<RadioSelection<StrokeWidthKey>
<RadioSelection
group="stroke-width"
options={[
{
value: "thin",
value: STROKE_WIDTH.thin,
text: t("labels.thin"),
icon: StrokeWidthBaseIcon,
testId: "strokeWidth-thin",
},
{
value: "medium",
text: t("labels.medium"),
value: STROKE_WIDTH.bold,
text: t("labels.bold"),
icon: StrokeWidthBoldIcon,
testId: "strokeWidth-medium",
testId: "strokeWidth-bold",
},
{
value: "bold",
text: t("labels.bold"),
value: STROKE_WIDTH.extraBold,
text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon,
testId: "strokeWidth-bold",
testId: "strokeWidth-extraBold",
},
]}
value={getFormValue(
elements,
app,
getStrokeWidthKeyForElement,
(element) => element.strokeWidth,
(element) => element.hasOwnProperty("strokeWidth"),
(hasSelection) =>
hasSelection ? null : appState.currentItemStrokeWidthKey,
hasSelection ? null : appState.currentItemStrokeWidth,
)}
onChange={(value) => updateData(value)}
/>
@@ -684,87 +656,6 @@ 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;
// in the compact UI the pressure setting is rendered as a single button
// that cycles between the two variability modes on click
if (data?.cycle) {
const isVariable = strokeVariability === "variable";
return (
<ToolButton
type="button"
icon={
isVariable
? strokeVariabilityVariableIcon
: strokeVariabilityConstantIcon
}
title={t("labels.pressure")}
aria-label={t("labels.pressure")}
onClick={() => updateData(isVariable ? "constant" : "variable")}
/>
);
}
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,7 +13,6 @@ export {
actionChangeStrokeWidth,
actionChangeFillStyle,
actionChangeSloppiness,
actionChangeFreedrawMode,
actionChangeOpacity,
actionChangeFontSize,
actionChangeFontFamily,
-1
View File
@@ -68,7 +68,6 @@ export type ActionName =
| "changeStrokeWidth"
| "changeStrokeShape"
| "changeSloppiness"
| "changeFreedrawMode"
| "changeStrokeStyle"
| "changeArrowhead"
| "changeArrowType"
+2 -9
View File
@@ -4,7 +4,6 @@ import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_ELEMENT_STROKE_WIDTH_KEY,
DEFAULT_TEXT_ALIGN,
DEFAULT_GRID_SIZE,
EXPORT_SCALES,
@@ -35,13 +34,12 @@ 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,
currentItemStrokeWidthKey: DEFAULT_ELEMENT_STROKE_WIDTH_KEY,
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
currentHoveredFontFamily: null,
cursorButton: "up",
@@ -169,15 +167,10 @@ 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 },
currentItemStrokeWidthKey: { browser: true, export: false, server: false },
currentItemStrokeWidth: { 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 },
+11 -46
View File
@@ -41,7 +41,6 @@ import {
canHaveArrowheads,
getTargetElements,
hasBackground,
hasFreedrawMode,
hasStrokeStyle,
hasStrokeWidth,
} from "../scene";
@@ -202,9 +201,9 @@ export const SelectedShapeActions = ({
targetElements.some((element) => hasStrokeWidth(element.type))) &&
renderAction("changeStrokeWidth")}
{(hasFreedrawMode(appState.activeTool.type) ||
targetElements.some((element) => hasFreedrawMode(element.type))) &&
renderAction("changeFreedrawMode")}
{(appState.activeTool.type === "freedraw" ||
targetElements.some((element) => element.type === "freedraw")) &&
renderAction("changeStrokeShape")}
{(hasStrokeStyle(appState.activeTool.type) ||
targetElements.some((element) => hasStrokeStyle(element.type))) && (
@@ -395,17 +394,6 @@ const CombinedShapeProperties = ({
hasStrokeWidth(element.type),
)) &&
renderAction("changeStrokeWidth")}
{
/* in compact UI the freedraw pressure setting is rendered as a
standalone cycle button in the compact actions list; we render
it in the combined properties popup as well for clarity
*/
(hasFreedrawMode(appState.activeTool.type) ||
targetElements.some((element) =>
hasFreedrawMode(element.type),
)) &&
renderAction("changeFreedrawMode")
}
{(hasStrokeStyle(appState.activeTool.type) ||
targetElements.some((element) =>
hasStrokeStyle(element.type),
@@ -838,14 +826,6 @@ export const CompactShapeActions = ({
</div>
)}
{/* Freedraw pressure: standalone button cycling the variability mode */}
{(hasFreedrawMode(appState.activeTool.type) ||
targetElements.some((element) => hasFreedrawMode(element.type))) && (
<div className="compact-action-item">
{renderAction("changeFreedrawMode", { cycle: true })}
</div>
)}
<CombinedShapeProperties
appState={appState}
renderAction={renderAction}
@@ -1074,11 +1054,6 @@ export const ShapesSwitcher = ({
const isFullStylesPanel = stylesPanelMode === "full";
const isCompactStylesPanel = stylesPanelMode === "compact";
// a pen detected on a tool button's pointer-down, to be applied (enabling
// pen mode) only after the tap's `change` has committed — see the tool
// button handlers below
const pendingPenDetectionRef = useRef(false);
const SELECTION_TOOLS = [
{
type: "selection",
@@ -1177,13 +1152,8 @@ export const ShapesSwitcher = ({
aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`}
onPointerDown={({ pointerType }) => {
// Detect the pen here (pointerType is reliable on pointer-down)
// but DON'T enable pen mode yet: calling setState mid-gesture
// re-renders the controlled radio and, on iOS/iPadOS, aborts
// the ensuing click so the tool isn't selected on the first pen
// tap. Defer it until the tap's `change` has committed (below).
if (!app.state.penDetected && pointerType === "pen") {
pendingPenDetectionRef.current = true;
app.togglePenMode(true);
}
if (value === "selection") {
@@ -1194,21 +1164,16 @@ export const ShapesSwitcher = ({
}
}
}}
onChange={() => {
onChange={({ pointerType }) => {
if (app.state.activeTool.type !== value) {
trackEvent("toolbar", value, "ui");
}
app.setActiveTool({ type: value });
// Apply the pen detection captured on pointer-down now that the
// tool is selected. rAF keeps the resulting re-render out of the
// `change` event itself. We rely on the pointer-down detection
// rather than this handler's pointerType because the latter is
// unreliable on iOS (its backing ref is cleared before the
// delayed click fires).
if (pendingPenDetectionRef.current) {
pendingPenDetectionRef.current = false;
requestAnimationFrame(() => app.togglePenMode(true));
if (value === "image") {
app.setActiveTool({
type: value,
});
} else {
app.setActiveTool({ type: value });
}
}}
/>
+102 -104
View File
@@ -27,8 +27,6 @@ import {
KEYS,
APP_NAME,
CURSOR_TYPE,
DEFAULT_STROKE_STREAMLINE,
DEFAULT_STROKE_STREAMLINE_PRECISE,
DEFAULT_TRANSFORM_HANDLE_SPACING,
DEFAULT_VERTICAL_ALIGN,
DRAGGING_THRESHOLD,
@@ -111,7 +109,6 @@ import {
setDesktopUIMode,
isSelectionLikeTool,
oneOf,
getStrokeWidthByKey,
} from "@excalidraw/common";
import {
@@ -263,7 +260,6 @@ import {
getUncroppedWidthAndHeight,
getActiveTextElement,
isEligibleFrameChildType,
getBindingStrategyForDraggingBindingElementEndpoints,
} from "@excalidraw/element";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
@@ -4137,7 +4133,7 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth("text"),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roundness: null,
roughness: this.state.currentItemRoughness,
@@ -4308,9 +4304,6 @@ class App extends React.Component<AppProps, AppState> {
return {
penMode: force ?? !prevState.penMode,
penDetected: true,
currentItemStrokeVariability: !prevState.penDetected
? "variable"
: prevState.currentItemStrokeVariability,
};
});
};
@@ -6310,7 +6303,7 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth("text"),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
@@ -7119,7 +7112,45 @@ class App extends React.Component<AppProps, AppState> {
setCursorForShape(this.interactiveCanvas, this.state);
if (lastPoint === lastCommittedPoint) {
if (
const hoveredElement =
isArrowElement(this.state.newElement) &&
isBindingEnabled(this.state) &&
getHoveredElementForBinding(
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
maxBindingDistance_simple(this.state.zoom),
);
if (hoveredElement) {
this.actionManager.executeAction(actionFinalize, "ui", {
event: event.nativeEvent,
sceneCoords: {
x: scenePointerX,
y: scenePointerY,
},
});
this.setState({ suggestedBinding: null });
if (!this.state.activeTool.locked) {
resetCursor(this.interactiveCanvas);
this.setState((prevState) => ({
newElement: null,
activeTool: updateActiveTool(this.state, {
type: this.state.preferredSelectionTool.type,
}),
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[multiElement.id]: true,
},
prevState,
),
selectedLinearElement: new LinearElementEditor(
multiElement,
this.scene.getNonDeletedElementsMap(),
),
}));
}
} else if (
// if we haven't yet created a temp point and we're beyond commit-zone
// threshold, add a point
pointDistance(
@@ -7127,24 +7158,6 @@ class App extends React.Component<AppProps, AppState> {
lastPoint,
) >= LINE_CONFIRM_THRESHOLD
) {
this.store.scheduleCapture();
flushSync(() => {
invariant(
this.state.selectedLinearElement?.initialState,
"initialState must be set",
);
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
lastCommittedPoint: points[points.length - 1],
selectedPointsIndices: [multiElement.points.length],
initialState: {
...this.state.selectedLinearElement.initialState,
lastClickedPoint: multiElement.points.length,
},
},
});
});
this.scene.mutateElement(
multiElement,
{
@@ -7155,6 +7168,21 @@ class App extends React.Component<AppProps, AppState> {
},
{ informMutation: false, isDragging: false },
);
invariant(
this.state.selectedLinearElement?.initialState,
"initialState must be set",
);
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
lastCommittedPoint: points[points.length - 1],
selectedPointsIndices: [multiElement.points.length - 1],
initialState: {
...this.state.selectedLinearElement.initialState,
lastClickedPoint: multiElement.points.length - 1,
},
},
});
} else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
// in this branch, we're inside the commit zone, and no uncommitted
@@ -7780,7 +7808,6 @@ class App extends React.Component<AppProps, AppState> {
return {
penMode: true,
penDetected: true,
currentItemStrokeVariability: "variable",
};
});
}
@@ -8999,8 +9026,6 @@ 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,
@@ -9008,24 +9033,15 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth("freedraw"),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
roundness: null,
simulatePressure,
strokeOptions: {
variability: strokeVariability,
streamline:
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],
});
@@ -9076,7 +9092,7 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: "transparent",
backgroundColor: "transparent",
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth("iframe"),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
roundness: this.getCurrentItemRoundness("iframe"),
@@ -9129,7 +9145,7 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: "transparent",
backgroundColor: "transparent",
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth("embeddable"),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
roundness: this.getCurrentItemRoundness("embeddable"),
@@ -9176,7 +9192,7 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth("image"),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
roundness: null,
@@ -9248,58 +9264,32 @@ class App extends React.Component<AppProps, AppState> {
const { x: rx, y: ry } = multiElement;
const { lastCommittedPoint } = selectedLinearElement;
const sceneCoords = viewportCoordsToSceneCoords(event, this.state);
const { start, end } =
isBindingElement(multiElement) && isBindingEnabled(this.state)
? getBindingStrategyForDraggingBindingElementEndpoints(
multiElement,
new Map([
[
multiElement.points.length - 1,
{
point: multiElement.points[multiElement.points.length - 1],
isDragging: false,
},
],
]),
sceneCoords.x,
sceneCoords.y,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.state,
{
newArrow: Boolean(this.state.newElement),
zoom: this.state.zoom,
},
)
: { end: { mode: undefined } };
const elementsMap = this.scene.getNonDeletedElementsMap();
// Auto-confirm when both ends bind to the SAME element and the end point
// lands on the outline rather than inside it
const endOutsideSameElement =
start?.mode != null &&
end.mode != null &&
start.element.id === end.element.id &&
!isPointInElement(end.focusPoint, end.element, elementsMap);
const boundOutsideFromElsewhere =
end.mode === "orbit" &&
multiElement.startBinding?.elementId !== end.element?.id;
const lastCommittedPointIsInsideCommitZone =
lastCommittedPoint &&
pointDistance(
pointFrom(
pointerDownState.origin.x - rx,
pointerDownState.origin.y - ry,
const hoveredElementForBinding =
isBindingEnabled(this.state) &&
getHoveredElementForBinding(
pointFrom<GlobalPoint>(
this.lastPointerMoveCoords?.x ??
rx + multiElement.points[multiElement.points.length - 1][0],
this.lastPointerMoveCoords?.y ??
ry + multiElement.points[multiElement.points.length - 1][1],
),
lastCommittedPoint,
) < LINE_CONFIRM_THRESHOLD;
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
);
// clicking inside commit zone → finalize arrow
if (
boundOutsideFromElsewhere || // Outside -> orbit: Bind immediately
endOutsideSameElement || // End outside the start's element: Bind immediately
(multiElement.points.length > 1 && lastCommittedPointIsInsideCommitZone)
(isBindingElement(multiElement) && hoveredElementForBinding) ||
(multiElement.points.length > 1 &&
lastCommittedPoint &&
pointDistance(
pointFrom(
pointerDownState.origin.x - rx,
pointerDownState.origin.y - ry,
),
lastCommittedPoint,
) < LINE_CONFIRM_THRESHOLD)
) {
this.actionManager.executeAction(actionFinalize, "ui", {
event: event.nativeEvent,
@@ -9354,7 +9344,7 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth(elementType),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
@@ -9381,7 +9371,7 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth(elementType),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
@@ -9518,13 +9508,6 @@ 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,
@@ -9548,7 +9531,7 @@ class App extends React.Component<AppProps, AppState> {
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.getCurrentItemStrokeWidth(elementType),
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
@@ -10881,6 +10864,13 @@ class App extends React.Component<AppProps, AppState> {
}
if (isLinearElement(newElement)) {
if (
newElement!.points.length > 1 &&
newElement.points[1][0] !== 0 &&
newElement.points[1][1] !== 0
) {
this.store.scheduleCapture();
}
const pointerCoords = viewportCoordsToSceneCoords(
childEvent,
this.state,
@@ -10918,15 +10908,23 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.executeAction(actionFinalize);
} else {
// Movement out of commit area will create the point
const dx = pointerCoords.x - newElement.x;
const dy = pointerCoords.y - newElement.y;
this.scene.mutateElement(
newElement,
{
points: [newElement.points[0], pointFrom<LocalPoint>(dx, dy)],
},
{ informMutation: false, isDragging: false },
);
this.setState({
multiElement: newElement,
newElement,
});
}
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
this.store.scheduleCapture();
if (isLinearElement(newElement)) {
this.actionManager.executeAction(actionFinalize, "ui", {
event: childEvent,
@@ -120,24 +120,6 @@
}
}
// on tablet, the pen mode button is rendered as a separate floating button
// below the compact actions menu (see LayerUI.tsx)
.App-menu_top__left > .ToolIcon__penMode {
justify-self: center;
.ToolIcon__icon {
width: var(--lg-button-size);
height: var(--lg-button-size);
background-color: var(--island-bg-color);
box-shadow: var(--shadow-island);
}
// no shadow while pen mode is active (the active fill is enough)
.ToolIcon_type_checkbox:checked + .ToolIcon__icon {
box-shadow: none;
}
}
.disable-view-mode {
display: flex;
justify-content: center;
+10 -30
View File
@@ -235,6 +235,8 @@ const LayerUI = ({
);
const renderSelectedShapeActions = () => {
const isCompactMode = isCompactStylesPanel;
return (
<Section
heading="selectedShapeActions"
@@ -242,7 +244,7 @@ const LayerUI = ({
"transition-left": appState.zenModeEnabled,
})}
>
{isCompactStylesPanel ? (
{isCompactMode ? (
<Island
className={clsx("compact-shape-actions-island")}
padding={0}
@@ -310,23 +312,6 @@ const LayerUI = ({
>
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</div>
{/* in compact UI the pen mode button lives outside the toolbar, as
a separate floating button below the compact actions menu
(same as we render it on mobile); shown alongside the compact
actions island, i.e. when a drawing tool or elements are
selected */}
{isCompactStylesPanel &&
!appState.viewModeEnabled &&
shouldRenderSelectedShapeActions && (
<PenModeButton
zenModeEnabled={appState.zenModeEnabled}
checked={appState.penMode}
onChange={() => onPenModeToggle(null)}
title={t("toolBar.penMode")}
isMobile
penDetected={appState.penDetected}
/>
)}
</Stack.Col>
{!appState.viewModeEnabled &&
appState.openDialog?.name !== "elementLinkSelector" && (
@@ -358,18 +343,13 @@ const LayerUI = ({
/>
{heading}
<Stack.Row gap={spacing.toolbarInnerRowGap}>
{/* in compact UI the pen mode button is rendered
as a separate floating button below the compact
actions menu */}
{!isCompactStylesPanel && (
<PenModeButton
zenModeEnabled={appState.zenModeEnabled}
checked={appState.penMode}
onChange={() => onPenModeToggle(null)}
title={t("toolBar.penMode")}
penDetected={appState.penDetected}
/>
)}
<PenModeButton
zenModeEnabled={appState.zenModeEnabled}
checked={appState.penMode}
onChange={() => onPenModeToggle(null)}
title={t("toolBar.penMode")}
penDetected={appState.penDetected}
/>
<LockButton
checked={appState.activeTool.locked}
onChange={onLockToggle}
-68
View File
@@ -1249,74 +1249,6 @@ 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" />
+92 -56
View File
@@ -3,7 +3,6 @@ import { isFiniteNumber, isValidPoint, pointFrom } from "@excalidraw/math";
import {
type CombineBrandsIfNeeded,
DEFAULT_FONT_FAMILY,
DEFAULT_STROKE_STREAMLINE,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
FONT_FAMILY,
@@ -19,9 +18,6 @@ import {
getSizeFromPoints,
normalizeLink,
getLineHeight,
STROKE_WIDTH,
STROKE_WIDTH_KEYS,
type StrokeWidthKey,
} from "@excalidraw/common";
import {
calculateFixedPointForNonElbowArrowBinding,
@@ -58,6 +54,11 @@ import { getNormalizedDimensions } from "@excalidraw/element";
import { isInvisiblySmallElement } from "@excalidraw/element";
import {
elementOverlapsWithFrame,
isFrameLikeElement,
} from "@excalidraw/element";
import type { LocalPoint, Radians } from "@excalidraw/math";
import type {
@@ -74,7 +75,6 @@ import type {
FontFamilyValues,
NonDeletedSceneElementsMap,
OrderedExcalidrawElement,
StrokeVariability,
StrokeRoundness,
} from "@excalidraw/element/types";
@@ -193,43 +193,6 @@ 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[
@@ -525,7 +488,6 @@ export const restoreElement = (
return restoreElementWithProperties(element, {
points,
simulatePressure: element.simulatePressure,
strokeOptions: restoreFreedrawStrokeOptions(element.strokeOptions),
pressures,
});
}
@@ -787,20 +749,78 @@ const repairBoundElement = (
};
/**
* Remove an element's frameId if its containing frame is non-existent
* Remove an element's frameId if:
* - its containing frame is non-existent (or isn't actually a frame), or
* - the element no longer (precisely) overlaps its containing frame.
*
* Frame membership is treated atomically per group: a grouped element keeps its
* frameId as long as ANY element of its (outermost) group still overlaps the
* frame, so we never split a group across frame membership. Bound text inherits
* its container's geometry/membership.
*
* NOTE mutates elements.
*/
const repairFrameMembership = (
element: Mutable<ExcalidrawElement>,
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
/** elements bucketed by outermost groupId (see `restoreElements`) */
elementsByGroup: Map<string, Mutable<ExcalidrawElement>[]>,
/** memoizes the overlap decision per `${frameId}:${groupId}` */
checkedFrameGroups: Map<string, boolean>,
) => {
if (element.frameId) {
const containingFrame = elementsMap.get(element.frameId);
if (!element.frameId) {
return;
}
if (!containingFrame) {
element.frameId = null;
const containingFrame = elementsMap.get(element.frameId);
// frame no longer exists (or the frameId doesn't point to a frame)
if (!containingFrame || !isFrameLikeElement(containingFrame)) {
element.frameId = null;
return;
}
// don't recompute geometry for deleted elements
if (element.isDeleted) {
return;
}
// a bound text follows its container's geometry / membership
const geometryElement =
isTextElement(element) && element.containerId
? elementsMap.get(element.containerId) ?? element
: element;
const outerGroupId =
geometryElement.groupIds[geometryElement.groupIds.length - 1];
let overlapsFrame: boolean;
if (outerGroupId) {
const cacheKey = `${containingFrame.id}:${outerGroupId}`;
const cached = checkedFrameGroups.get(cacheKey);
if (cached !== undefined) {
overlapsFrame = cached;
} else {
// group membership is atomic — keep the element as long as ANY element
// of its group still overlaps the frame, so we never split a group.
const groupElements = elementsByGroup.get(outerGroupId) ?? [
geometryElement,
];
overlapsFrame = groupElements.some((groupElement) =>
elementOverlapsWithFrame(groupElement, containingFrame, elementsMap),
);
checkedFrameGroups.set(cacheKey, overlapsFrame);
}
} else {
overlapsFrame = elementOverlapsWithFrame(
geometryElement,
containingFrame,
elementsMap,
);
}
if (!overlapsFrame) {
element.frameId = null;
}
};
@@ -879,9 +899,32 @@ export const restoreElements = <T extends ExcalidrawElement>(
// repair binding. Mutates elements.
const restoredElementsMap = arrayToMap(restoredElements);
// index elements by their outermost groupId once, so frame-membership repair
// can resolve a group's members in O(1) instead of scanning all elements per
// group (getElementsInGroup) — a big win on large, heavily-grouped scenes.
const elementsByGroup = new Map<string, Mutable<ExcalidrawElement>[]>();
for (const element of restoredElements) {
const outerGroupId = element.groupIds[element.groupIds.length - 1];
if (outerGroupId) {
const members = elementsByGroup.get(outerGroupId);
if (members) {
members.push(element);
} else {
elementsByGroup.set(outerGroupId, [element]);
}
}
}
const checkedFrameGroups = new Map<string, boolean>();
for (const element of restoredElements) {
if (element.frameId) {
repairFrameMembership(element, restoredElementsMap);
repairFrameMembership(
element,
restoredElementsMap,
elementsByGroup,
checkedFrameGroups,
);
}
if (isTextElement(element) && element.containerId) {
@@ -1099,13 +1142,6 @@ 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",
+1 -2
View File
@@ -10,7 +10,6 @@ import {
applyDarkModeFilter,
DEFAULT_IMAGE_OPTIONS,
DEFAULT_UI_OPTIONS,
getStrokeWidthByKey,
isShallowEqual,
} from "@excalidraw/common";
@@ -451,4 +450,4 @@ export function useExcalidrawStateValue(
export { _useOnAppStateChange as useOnExcalidrawStateChange };
export { applyDarkModeFilter, getStrokeWidthByKey };
export { applyDarkModeFilter };
-3
View File
@@ -35,9 +35,6 @@
"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,7 +9,6 @@ export {
hasBackground,
hasStrokeWidth,
hasStrokeStyle,
hasFreedrawMode,
canHaveArrowheads,
canChangeRoundness,
} from "@excalidraw/element";
@@ -904,8 +904,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1104,8 +1103,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1319,8 +1317,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1651,8 +1648,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1983,8 +1979,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2198,8 +2193,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2440,8 +2434,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2739,8 +2732,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3112,8 +3104,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#e03131",
"currentItemStrokeStyle": "dotted",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "bold",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3223,11 +3214,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"seed": 1278240551,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
"strokeWidth": 4,
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1349943049,
"versionNonce": 1402203177,
"width": 20,
"x": -10,
"y": 0,
@@ -3252,14 +3243,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"opacity": 60,
"roughness": 2,
"roundness": null,
"seed": 406373543,
"seed": 1898319239,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
"strokeWidth": 4,
"strokeWidth": 2,
"type": "rectangle",
"updated": 1,
"version": 10,
"versionNonce": 1402203177,
"version": 9,
"versionNonce": 941653321,
"width": 20,
"x": 20,
"y": 30,
@@ -3268,7 +3259,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`] = `17`;
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] redo stack 1`] = `[]`;
@@ -3468,11 +3459,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"updated": {
"id3": {
"deleted": {
"strokeWidth": 4,
"strokeStyle": "dotted",
"version": 7,
},
"inserted": {
"strokeWidth": 2,
"strokeStyle": "solid",
"version": 6,
},
},
@@ -3493,11 +3484,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"updated": {
"id3": {
"deleted": {
"strokeStyle": "dotted",
"roughness": 2,
"version": 8,
},
"inserted": {
"strokeStyle": "solid",
"roughness": 1,
"version": 7,
},
},
@@ -3518,11 +3509,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"updated": {
"id3": {
"deleted": {
"roughness": 2,
"opacity": 60,
"version": 9,
},
"inserted": {
"roughness": 1,
"opacity": 100,
"version": 8,
},
},
@@ -3530,31 +3521,6 @@ 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 {
@@ -3582,7 +3548,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roughness": 2,
"strokeColor": "#e03131",
"strokeStyle": "dotted",
"strokeWidth": 4,
"version": 4,
},
"inserted": {
@@ -3592,13 +3557,12 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"roughness": 1,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"version": 3,
},
},
},
},
"id": "id21",
"id": "id19",
},
]
`;
@@ -3633,8 +3597,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3957,8 +3920,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -4281,8 +4243,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -5567,8 +5528,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -6785,8 +6745,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -7743,8 +7702,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8744,8 +8702,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9736,8 +9693,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -30,8 +30,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -665,8 +664,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1228,8 +1226,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1589,8 +1586,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1952,8 +1948,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2216,8 +2211,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2704,8 +2698,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3008,8 +3001,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3328,8 +3320,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3623,8 +3614,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3910,8 +3900,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -4146,8 +4135,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -4404,8 +4392,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -4676,8 +4663,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -4906,8 +4892,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -5136,8 +5121,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -5384,8 +5368,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -5641,8 +5624,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -5900,8 +5882,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -6230,8 +6211,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -6658,8 +6638,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -7033,8 +7012,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -7346,8 +7324,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -7640,8 +7617,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -7871,8 +7847,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8224,8 +8199,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8577,8 +8551,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8984,8 +8957,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9111,12 +9083,8 @@ 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": 1,
"strokeWidth": 2,
"type": "freedraw",
"updated": 1,
"version": 7,
@@ -9217,12 +9185,8 @@ 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": 1,
"strokeWidth": 2,
"type": "freedraw",
"version": 7,
"width": 50,
@@ -9272,8 +9236,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9537,8 +9500,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9803,8 +9765,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -10036,8 +9997,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -10334,8 +10294,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -10653,8 +10612,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -10890,8 +10848,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -11332,7 +11289,7 @@ exports[`history > multiplayer undo/redo > should support undo and redo when esc
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -11816,8 +11773,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -12077,8 +12033,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -12313,8 +12268,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -12551,8 +12505,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#e03131",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -12703,12 +12656,8 @@ 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": 1,
"strokeWidth": 2,
"type": "freedraw",
"updated": 1,
"version": 5,
@@ -12757,12 +12706,8 @@ 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": 1,
"strokeWidth": 2,
"type": "freedraw",
"updated": 1,
"version": 4,
@@ -12900,12 +12845,8 @@ 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": 1,
"strokeWidth": 2,
"type": "freedraw",
"version": 4,
"width": 50,
@@ -12955,8 +12896,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -13166,8 +13106,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -13374,8 +13313,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -13676,8 +13614,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -13975,8 +13912,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -14221,8 +14157,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -14459,8 +14394,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -14697,8 +14631,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -14945,8 +14878,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -15277,8 +15209,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -15448,8 +15379,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -15733,8 +15663,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -15997,8 +15926,7 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -16151,8 +16079,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -16434,8 +16361,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -16597,8 +16523,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -17347,8 +17272,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -17995,8 +17919,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -18643,8 +18566,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -19394,8 +19316,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -20164,8 +20085,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -20645,8 +20565,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -21157,8 +21076,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -21617,8 +21535,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -30,8 +30,7 @@ exports[`given element A and group of elements B and given both are selected whe
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -457,8 +456,7 @@ exports[`given element A and group of elements B and given both are selected whe
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -874,8 +872,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1441,8 +1438,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1649,8 +1645,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2034,8 +2029,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2280,8 +2274,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2461,8 +2454,7 @@ exports[`regression tests > can drag element that covers another element, while
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -2787,8 +2779,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1971c2",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3043,8 +3034,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3285,8 +3275,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3522,8 +3511,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -3781,8 +3769,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -4096,8 +4083,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -4533,8 +4519,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
@@ -4817,8 +4802,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -5094,8 +5078,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
@@ -5303,8 +5286,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -5504,8 +5486,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -5898,8 +5879,7 @@ exports[`regression tests > drags selected elements from point inside common bou
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -6196,8 +6176,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -6935,12 +6914,8 @@ 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": 1,
"strokeWidth": 2,
"type": "freedraw",
"version": 4,
"width": 50,
@@ -6990,8 +6965,7 @@ exports[`regression tests > given a group of selected elements with an element t
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -7325,8 +7299,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -7605,8 +7578,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -7841,8 +7813,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8082,8 +8053,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8263,8 +8233,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8444,8 +8413,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8625,8 +8593,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -8859,8 +8826,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9091,8 +9057,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9233,12 +9198,8 @@ 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": 1,
"strokeWidth": 2,
"type": "freedraw",
"version": 4,
"width": 30,
@@ -9288,8 +9249,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9522,8 +9482,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9703,8 +9662,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -9935,8 +9893,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -10116,8 +10073,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -10258,12 +10214,8 @@ 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": 1,
"strokeWidth": 2,
"type": "freedraw",
"version": 4,
"width": 30,
@@ -10313,8 +10265,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -10494,8 +10445,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -11026,8 +10976,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -11307,8 +11256,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
@@ -11431,8 +11379,7 @@ exports[`regression tests > shift click on selected element should deselect it o
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -11632,8 +11579,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -11952,8 +11898,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -12382,8 +12327,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -13023,8 +12967,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -13150,8 +13093,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -13782,8 +13724,7 @@ exports[`regression tests > switches from group of selected elements to another
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
@@ -14122,8 +14063,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
@@ -14387,8 +14327,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
@@ -14511,8 +14450,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -14877,8 +14815,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -15001,8 +14938,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -1,6 +1,6 @@
import React from "react";
import { CODES, STROKE_WIDTH } from "@excalidraw/common";
import { CODES } 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(STROKE_WIDTH.bold);
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
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, STROKE_WIDTH, reseed } from "@excalidraw/common";
import { KEYS, 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(STROKE_WIDTH.bold);
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
expect(firstRect.strokeStyle).toBe("dotted");
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
expect(firstRect.opacity).toBe(60);
@@ -240,12 +240,8 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
"seed": Any<Number>,
"simulatePressure": true,
"strokeColor": "#1e1e1e",
"strokeOptions": {
"streamline": "0.50000",
"variability": "variable",
},
"strokeStyle": "solid",
"strokeWidth": 1,
"strokeWidth": 2,
"type": "freedraw",
"updated": 1,
"version": 2,
+204 -78
View File
@@ -1,7 +1,12 @@
import { pointFrom } from "@excalidraw/math";
import { vi } from "vitest";
import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "@excalidraw/common";
import {
arrayToMap,
DEFAULT_SIDEBAR,
FONT_FAMILY,
ROUNDNESS,
} from "@excalidraw/common";
import { newElementWith } from "@excalidraw/element";
import * as sizeHelpers from "@excalidraw/element";
@@ -193,53 +198,6 @@ 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" });
@@ -687,21 +645,6 @@ 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";
@@ -750,21 +693,6 @@ 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(),
@@ -1217,3 +1145,201 @@ describe("repairing bindings", () => {
]);
});
});
describe("repairing frame membership", () => {
const makeFrame = () =>
API.createElement({
type: "frame",
id: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
});
it("keeps frameId for an element fully inside the frame", () => {
const frame = makeFrame();
const inside = API.createElement({
type: "rectangle",
id: "inside",
x: 10,
y: 10,
width: 20,
height: 20,
frameId: frame.id,
});
const restoredElementsMap = arrayToMap(
restore.restoreElements([frame, inside], null, { repairBindings: true }),
);
expect(restoredElementsMap.get("inside")?.frameId).toBe(frame.id);
});
it("keeps frameId for an element intersecting the frame border", () => {
const frame = makeFrame();
// spans x:[90,130] — crosses the frame's right edge (x=100)
const straddling = API.createElement({
type: "rectangle",
id: "straddling",
x: 90,
y: 10,
width: 40,
height: 20,
frameId: frame.id,
});
const restoredElementsMap = arrayToMap(
restore.restoreElements([frame, straddling], null, {
repairBindings: true,
}),
);
expect(restoredElementsMap.get("straddling")?.frameId).toBe(frame.id);
});
it("removes frameId for an element fully outside the frame", () => {
const frame = makeFrame();
const outside = API.createElement({
type: "rectangle",
id: "outside",
x: 200,
y: 200,
width: 20,
height: 20,
frameId: frame.id,
});
const restoredElementsMap = arrayToMap(
restore.restoreElements([frame, outside], null, { repairBindings: true }),
);
expect(restoredElementsMap.get("outside")?.frameId).toBe(null);
});
it("removes frameId when the containing frame no longer exists", () => {
const orphan = API.createElement({
type: "rectangle",
id: "orphan",
x: 10,
y: 10,
frameId: "non-existent-frame",
});
const restoredElementsMap = arrayToMap(
restore.restoreElements([orphan], null, { repairBindings: true }),
);
expect(restoredElementsMap.get("orphan")?.frameId).toBe(null);
});
it("does NOT touch frameId when repairBindings is off", () => {
const frame = makeFrame();
const outside = API.createElement({
type: "rectangle",
id: "outside",
x: 200,
y: 200,
width: 20,
height: 20,
frameId: frame.id,
});
const restoredElementsMap = arrayToMap(
restore.restoreElements([frame, outside], null, {
repairBindings: false,
}),
);
expect(restoredElementsMap.get("outside")?.frameId).toBe(frame.id);
});
it("keeps a group atomic: members stay if ANY group member overlaps", () => {
const frame = makeFrame();
const insideMember = API.createElement({
type: "rectangle",
id: "insideMember",
x: 10,
y: 10,
width: 20,
height: 20,
groupIds: ["g1"],
frameId: frame.id,
});
const outsideMember = API.createElement({
type: "rectangle",
id: "outsideMember",
x: 300,
y: 300,
width: 20,
height: 20,
groupIds: ["g1"],
frameId: frame.id,
});
const restoredElementsMap = arrayToMap(
restore.restoreElements([frame, insideMember, outsideMember], null, {
repairBindings: true,
}),
);
// the whole group overlaps (via insideMember), so neither is evicted
expect(restoredElementsMap.get("insideMember")?.frameId).toBe(frame.id);
expect(restoredElementsMap.get("outsideMember")?.frameId).toBe(frame.id);
});
it("removes frameId for a whole group when no member overlaps", () => {
const frame = makeFrame();
const a = API.createElement({
type: "rectangle",
id: "a",
x: 200,
y: 200,
width: 20,
height: 20,
groupIds: ["g1"],
frameId: frame.id,
});
const b = API.createElement({
type: "rectangle",
id: "b",
x: 300,
y: 300,
width: 20,
height: 20,
groupIds: ["g1"],
frameId: frame.id,
});
const restoredElementsMap = arrayToMap(
restore.restoreElements([frame, a, b], null, { repairBindings: true }),
);
expect(restoredElementsMap.get("a")?.frameId).toBe(null);
expect(restoredElementsMap.get("b")?.frameId).toBe(null);
});
it("bound text follows its container's frame membership", () => {
const frameOutside = makeFrame();
const container = API.createElement({
type: "rectangle",
id: "container",
x: 300,
y: 300,
width: 40,
height: 40,
frameId: frameOutside.id,
boundElements: [{ type: "text", id: "label" }],
});
const label = API.createElement({
type: "text",
id: "label",
x: 305,
y: 305,
containerId: container.id,
frameId: frameOutside.id,
});
const restoredElementsMap = arrayToMap(
restore.restoreElements([frameOutside, container, label], null, {
repairBindings: true,
}),
);
// container is fully outside -> both it and its bound text leave the frame
expect(restoredElementsMap.get("container")?.frameId).toBe(null);
expect(restoredElementsMap.get("label")?.frameId).toBe(null);
});
});
@@ -1,57 +0,0 @@
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");
});
});
+2 -13
View File
@@ -4,12 +4,7 @@ import util from "util";
import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math";
import {
DEFAULT_VERTICAL_ALIGN,
ROUNDNESS,
assertNever,
getStrokeWidthByKey,
} from "@excalidraw/common";
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
import {
newArrowElement,
@@ -205,9 +200,6 @@ 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;
@@ -266,9 +258,7 @@ export class API {
backgroundColor:
rest.backgroundColor ?? appState.currentItemBackgroundColor,
fillStyle: rest.fillStyle ?? appState.currentItemFillStyle,
strokeWidth:
rest.strokeWidth ??
getStrokeWidthByKey(type, appState.currentItemStrokeWidthKey),
strokeWidth: rest.strokeWidth ?? appState.currentItemStrokeWidth,
strokeStyle: rest.strokeStyle ?? appState.currentItemStrokeStyle,
roundness: (
rest.roundness === undefined
@@ -327,7 +317,6 @@ export class API {
type: type as "freedraw",
simulatePressure: true,
points: rest.points,
strokeOptions: rest.strokeOptions,
...base,
});
break;
+52
View File
@@ -0,0 +1,52 @@
import type {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "@excalidraw/element/types";
import { exportToSvg } from "../../scene/export";
/**
* Test-only helper: render the supplied elements to an SVG string for visual
* debugging (e.g. to eyeball frame-membership / overlap geometry).
*
* Frames are drawn with their outline but are NOT clipped, and every element's
* `frameId` is ignored, so elements that fall outside a frame stay visible and
* within the exported viewBox instead of being clipped or cropped away.
*
* Pass `outFile` to also write the SVG to disk open it in a browser to view.
*
* @example
* await exportElementsToSVG([frame, diamond], {
* outFile: "/tmp/frame-debug.svg",
* });
*/
export const exportElementsToSVG = async (
elements: readonly ExcalidrawElement[],
opts?: { outFile?: string; padding?: number; background?: string },
): Promise<string> => {
// ignore frame membership so nothing gets clipped/cropped out of view
const rootElements = elements.map(
(element) => ({ ...element, frameId: null } as NonDeletedExcalidrawElement),
);
const svg = await exportToSvg(
rootElements,
{
exportBackground: true,
viewBackgroundColor: opts?.background ?? "#ffffff",
exportPadding: opts?.padding ?? 24,
frameRendering: { enabled: true, name: true, outline: true, clip: false },
},
null,
{ skipInliningFonts: true },
);
const svgString = svg.outerHTML;
if (opts?.outFile) {
const { writeFileSync } = await import("fs");
writeFileSync(opts.outFile, svgString);
}
return svgString;
};
@@ -24,7 +24,7 @@ exports[`exportToSvg > with default arguments 1`] = `
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
-1
View File
@@ -1,7 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"rootDir": "../",
"outDir": "./dist/types"
},
"include": ["**/*"],
+1 -4
View File
@@ -4,7 +4,6 @@ import type {
throttleRAF,
MIME_TYPES,
EditorInterface,
StrokeWidthKey,
} from "@excalidraw/common";
import type { LinearElementEditor } from "@excalidraw/element";
@@ -34,7 +33,6 @@ import type {
ExcalidrawNonSelectionElement,
BindMode,
ExcalidrawTextElement,
StrokeVariability,
} from "@excalidraw/element/types";
import type {
@@ -364,10 +362,9 @@ export interface AppState {
currentItemStrokeColor: string;
currentItemBackgroundColor: string;
currentItemFillStyle: ExcalidrawElement["fillStyle"];
currentItemStrokeWidthKey: StrokeWidthKey;
currentItemStrokeWidth: number;
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
currentItemRoughness: number;
currentItemStrokeVariability: StrokeVariability;
currentItemOpacity: number;
currentItemFontFamily: FontFamilyValues;
currentItemFontSize: number;
+1 -2
View File
@@ -1,8 +1,7 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "../"
"outDir": "./dist/types"
},
"include": ["src/**/*", "global.d.ts"],
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
-21
View File
@@ -1,21 +0,0 @@
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
@@ -1,23 +0,0 @@
# 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
@@ -1,34 +0,0 @@
{
"name": "@excalidraw/laser-pointer",
"version": "1.3.1",
"description": "Generate outline for laser pointer tool",
"type": "module",
"types": "./dist/types/laser-pointer/src/index.d.ts",
"main": "./dist/prod/index.js",
"module": "./dist/prod/index.js",
"exports": {
".": {
"types": "./dist/types/laser-pointer/src/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
@@ -1,2 +0,0 @@
export * from "./state";
export type { Point } from "./math";
-105
View File
@@ -1,105 +0,0 @@
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
@@ -1,42 +0,0 @@
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
@@ -1,377 +0,0 @@
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;
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/types",
"rootDir": "../"
},
"include": ["src/**/*", "global.d.ts"],
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"]
}
-1
View File
@@ -1,7 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"rootDir": "../",
"outDir": "./dist/types"
},
"include": ["src/**/*", "global.d.ts"],
+1 -3
View File
@@ -6,7 +6,7 @@
"declaration": true,
"allowSyntheticDefaultImports": true,
"module": "ESNext",
"moduleResolution": "bundler",
"moduleResolution": "Node",
"resolveJsonModule": true,
"jsx": "react-jsx",
"emitDeclarationOnly": true,
@@ -17,8 +17,6 @@
"@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,8 +30,7 @@ exports[`exportToSvg > with default arguments 1`] = `
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeVariability": "constant",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
@@ -23,7 +23,7 @@ exports[`exportToSvg > with default arguments 1`] = `
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidthKey": "medium",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
-1
View File
@@ -1,7 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"rootDir": "../",
"outDir": "./dist/types"
},
"include": ["src/**/*", "global.d.ts"],
-16
View File
@@ -3,7 +3,6 @@ import fs from "fs";
// vitest.setup.ts
import "vitest-canvas-mock";
import "@testing-library/jest-dom";
import { configure } from "@testing-library/react";
import { vi } from "vitest";
import polyfill from "./packages/excalidraw/polyfill";
@@ -17,21 +16,6 @@ import {
Object.assign(globalThis, testPolyfills);
PolyfillLocalStorage();
// By default testing-library dumps the entire serialized DOM into the error
// message whenever a `waitFor`/`getBy*` fails, which floods the test output
// (often hundreds of lines of HTML per failure). Strip it out unless
// VITE_DEBUG_DOM is enabled (see .env.test), e.g. `VITE_DEBUG_DOM=true yarn test`.
const debugDom = ["true", "1"].includes(process.env.VITE_DEBUG_DOM ?? "");
if (!debugDom) {
configure({
getElementError: (message) => {
const error = new Error(message ?? undefined);
error.name = "TestingLibraryElementError";
return error;
},
});
}
vi.mock("@excalidraw/common", async (importOriginal) => {
const module = await importOriginal<typeof import("@excalidraw/common")>();
+1 -3
View File
@@ -12,7 +12,7 @@
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "ESNext",
"moduleResolution": "bundler",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
@@ -27,8 +27,6 @@
"@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,4 +1,5 @@
{
"public": true,
"headers": [
{
"source": "/(.*)",
-13
View File
@@ -59,17 +59,6 @@ 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
@@ -82,8 +71,6 @@ export default defineConfig({
setupFiles: ["./setupTests.ts"],
globals: true,
environment: "jsdom",
// don't list skipped tests in the failure tree — keeps output readable
hideSkippedTests: true,
coverage: {
reporter: ["text", "json-summary", "json", "html", "lcovonly"],
// Since v2, it ignores empty lines by default and we need to disable it as it affects the coverage
+5
View File
@@ -1521,6 +1521,11 @@
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"