From 4d04e3e9a19251a61f5da68199d82de96ef12ca3 Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Mon, 27 Oct 2025 16:25:31 +0100 Subject: [PATCH] chore: Additional master merge Signed-off-by: Mark Tolmacs --- packages/element/src/freedraw.ts | 99 ++++++++++++++++++- .../tests/__snapshots__/history.test.tsx.snap | 30 +++--- .../regressionTests.test.tsx.snap | 18 ++-- .../data/__snapshots__/restore.test.ts.snap | 6 +- 4 files changed, 124 insertions(+), 29 deletions(-) diff --git a/packages/element/src/freedraw.ts b/packages/element/src/freedraw.ts index 06ca82e7ea..c3ff228dcd 100644 --- a/packages/element/src/freedraw.ts +++ b/packages/element/src/freedraw.ts @@ -1,12 +1,29 @@ import { LaserPointer, type Point } from "@excalidraw/laser-pointer"; -import { clamp, round, type LocalPoint } from "@excalidraw/math"; +import { + clamp, + lineSegment, + pointFrom, + pointRotateRads, + round, + type LocalPoint, +} from "@excalidraw/math"; import getStroke from "perfect-freehand"; +import { invariant } from "@excalidraw/common"; + +import type { GlobalPoint, Radians } from "@excalidraw/math"; + +import { getElementBounds } from "./bounds"; + import type { StrokeOptions } from "perfect-freehand"; -import type { ExcalidrawFreeDrawElement, PointerType } from "./types"; +import type { + ElementsMap, + ExcalidrawFreeDrawElement, + PointerType, +} from "./types"; export const STROKE_OPTIONS: Record< PointerType | "default", @@ -276,3 +293,81 @@ const _legacy_getSvgPathFromStroke = (points: number[][]): string => { .join(" ") .replace(TO_FIXED_PRECISION, "$1"); }; + +export function getFreedrawOutlineAsSegments( + element: ExcalidrawFreeDrawElement, + points: [number, number][], + elementsMap: ElementsMap, +) { + const bounds = getElementBounds( + { + ...element, + angle: 0 as Radians, + }, + elementsMap, + ); + const center = pointFrom( + (bounds[0] + bounds[2]) / 2, + (bounds[1] + bounds[3]) / 2, + ); + + invariant(points.length >= 2, "Freepath outline must have at least 2 points"); + + return points.slice(2).reduce( + (acc, curr) => { + acc.push( + lineSegment( + acc[acc.length - 1][1], + pointRotateRads( + pointFrom(curr[0] + element.x, curr[1] + element.y), + center, + element.angle, + ), + ), + ); + return acc; + }, + [ + lineSegment( + pointRotateRads( + pointFrom( + points[0][0] + element.x, + points[0][1] + element.y, + ), + center, + element.angle, + ), + pointRotateRads( + pointFrom( + points[1][0] + element.x, + points[1][1] + element.y, + ), + center, + element.angle, + ), + ), + ], + ); +} + +export function getFreedrawOutlinePoints(element: ExcalidrawFreeDrawElement) { + // 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]]) + : [[0, 0, 0.5]]; + + // Consider changing the options for simulated pressure vs real pressure + const options: StrokeOptions = { + simulatePressure: element.simulatePressure, + 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: !!element.lastCommittedPoint, // LastCommittedPoint is added on pointerup + }; + + return getStroke(inputPoints as number[][], options) as [number, number][]; +} diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 0b50b1f01e..8da42aaf11 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -9050,13 +9050,13 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "backgroundColor": "transparent", "boundElements": null, "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.35000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 50, "id": "id0", @@ -9157,13 +9157,13 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "backgroundColor": "transparent", "boundElements": null, "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.35000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 50, "index": "a0", @@ -12234,13 +12234,13 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "backgroundColor": "transparent", "boundElements": null, "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.35000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 10, "id": "id5", @@ -12289,13 +12289,13 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "backgroundColor": "transparent", "boundElements": null, "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.35000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 10, "id": "id9", @@ -12434,13 +12434,13 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "backgroundColor": "transparent", "boundElements": null, "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.35000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 10, "index": "a2", diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index d20306b59a..dfcb2e7200 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -7029,13 +7029,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "backgroundColor": "transparent", "boundElements": null, "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.35000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 10, "index": "a7", @@ -9352,13 +9352,13 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta "backgroundColor": "transparent", "boundElements": null, "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.35000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 30, "index": "a0", @@ -10387,13 +10387,13 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta "backgroundColor": "transparent", "boundElements": null, "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.35000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 30, "index": "a0", diff --git a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap index 36aef9598f..6f445615d0 100644 --- a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap +++ b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap @@ -168,13 +168,13 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = ` "backgroundColor": "transparent", "boundElements": [], "customData": undefined, - "drawingConfigs": { + "fillStyle": "solid", + "frameId": null, + "freedrawOptions": { "fixedStrokeWidth": true, "simplify": "0.10000", "streamline": "0.25000", }, - "fillStyle": "solid", - "frameId": null, "groupIds": [], "height": 100, "id": "id-freedraw01",