mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-19 04:05:19 +01:00
feat: Simplify the freedraw element at creation or restoration
This commit is contained in:
@@ -1052,15 +1052,14 @@ export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
|
|||||||
|
|
||||||
export function getFreedrawOutlineAsSegments(
|
export function getFreedrawOutlineAsSegments(
|
||||||
element: ExcalidrawFreeDrawElement,
|
element: ExcalidrawFreeDrawElement,
|
||||||
points: [number, number][],
|
points: readonly [number, number][],
|
||||||
elementsMap: ElementsMap,
|
|
||||||
) {
|
) {
|
||||||
const bounds = getElementBounds(
|
const bounds = getElementBounds(
|
||||||
{
|
{
|
||||||
...element,
|
...element,
|
||||||
angle: 0 as Radians,
|
angle: 0 as Radians,
|
||||||
},
|
},
|
||||||
elementsMap,
|
new Map(),
|
||||||
);
|
);
|
||||||
const center = pointFrom<GlobalPoint>(
|
const center = pointFrom<GlobalPoint>(
|
||||||
(bounds[0] + bounds[2]) / 2,
|
(bounds[0] + bounds[2]) / 2,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
curveCatmullRomCubicApproxPoints,
|
curveCatmullRomCubicApproxPoints,
|
||||||
curveOffsetPoints,
|
curveOffsetPoints,
|
||||||
lineSegment,
|
lineSegment,
|
||||||
|
perpendicularDistance,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
pointFrom,
|
pointFrom,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
@@ -481,3 +482,24 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function shouldSkipFreedrawPoint(
|
||||||
|
points: readonly LocalPoint[],
|
||||||
|
dx: number,
|
||||||
|
dy: number,
|
||||||
|
epsilon: number = 0.5,
|
||||||
|
) {
|
||||||
|
const lastPoint = points.length > 0 && points[points.length - 1];
|
||||||
|
const nextToLastPoint = points.length > 1 && points[points.length - 2];
|
||||||
|
return (
|
||||||
|
(lastPoint && lastPoint[0] === dx && lastPoint[1] === dy) ||
|
||||||
|
// NOTE: Apply a simplification algorithm to reduce number of points
|
||||||
|
(nextToLastPoint && lastPoint
|
||||||
|
? perpendicularDistance(
|
||||||
|
pointFrom<LocalPoint>(dx, dy),
|
||||||
|
lineSegment(nextToLastPoint, lastPoint),
|
||||||
|
) < epsilon
|
||||||
|
: !points[0] ||
|
||||||
|
pointDistance(points[0], pointFrom<LocalPoint>(dx, dy)) < epsilon)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ import {
|
|||||||
StoreDelta,
|
StoreDelta,
|
||||||
type ApplyToOptions,
|
type ApplyToOptions,
|
||||||
positionElementsOnGrid,
|
positionElementsOnGrid,
|
||||||
|
shouldSkipFreedrawPoint,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
@@ -8922,10 +8923,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const points = newElement.points;
|
const points = newElement.points;
|
||||||
const dx = pointerCoords.x - newElement.x;
|
const dx = pointerCoords.x - newElement.x;
|
||||||
const dy = pointerCoords.y - newElement.y;
|
const dy = pointerCoords.y - newElement.y;
|
||||||
|
const discardPoint = shouldSkipFreedrawPoint(points, dx, dy, 0.5);
|
||||||
const lastPoint = points.length > 0 && points[points.length - 1];
|
|
||||||
const discardPoint =
|
|
||||||
lastPoint && lastPoint[0] === dx && lastPoint[1] === dy;
|
|
||||||
|
|
||||||
if (!discardPoint) {
|
if (!discardPoint) {
|
||||||
const pressures = newElement.simulatePressure
|
const pressures = newElement.simulatePressure
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { isFiniteNumber, pointFrom } from "@excalidraw/math";
|
import {
|
||||||
|
curveSimplifyWithRDP,
|
||||||
|
isFiniteNumber,
|
||||||
|
pointFrom,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
@@ -18,7 +22,11 @@ import {
|
|||||||
normalizeLink,
|
normalizeLink,
|
||||||
getLineHeight,
|
getLineHeight,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
import { getNonDeletedElements, isValidPolygon } from "@excalidraw/element";
|
import {
|
||||||
|
getNonDeletedElements,
|
||||||
|
isFreeDrawElement,
|
||||||
|
isValidPolygon,
|
||||||
|
} from "@excalidraw/element";
|
||||||
import { normalizeFixedPoint } from "@excalidraw/element";
|
import { normalizeFixedPoint } from "@excalidraw/element";
|
||||||
import {
|
import {
|
||||||
updateElbowArrowPoints,
|
updateElbowArrowPoints,
|
||||||
@@ -623,6 +631,12 @@ export const restoreElements = (
|
|||||||
(element as Mutable<ExcalidrawLinearElement>).endBinding = null;
|
(element as Mutable<ExcalidrawLinearElement>).endBinding = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFreeDrawElement(element)) {
|
||||||
|
Object.assign(element, {
|
||||||
|
points: curveSimplifyWithRDP(element.points, 0.5),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE (mtolmacs): Temporary fix for extremely large arrows
|
// NOTE (mtolmacs): Temporary fix for extremely large arrows
|
||||||
|
|||||||
@@ -238,11 +238,7 @@ const eraserTest = (
|
|||||||
// which offers a good visual precision at various zoom levels
|
// which offers a good visual precision at various zoom levels
|
||||||
if (isFreeDrawElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
const outlinePoints = getFreedrawOutlinePoints(element);
|
const outlinePoints = getFreedrawOutlinePoints(element);
|
||||||
const strokeSegments = getFreedrawOutlineAsSegments(
|
const strokeSegments = getFreedrawOutlineAsSegments(element, outlinePoints);
|
||||||
element,
|
|
||||||
outlinePoints,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
const tolerance = Math.max(2.25, 5 / zoom); // NOTE: Visually fine-tuned approximation
|
const tolerance = Math.max(2.25, 5 / zoom); // NOTE: Visually fine-tuned approximation
|
||||||
|
|
||||||
for (const seg of strokeSegments) {
|
for (const seg of strokeSegments) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
|
import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
|
||||||
import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
|
import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
|
||||||
import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants";
|
import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants";
|
||||||
|
import { lineSegment } from "./segment";
|
||||||
|
|
||||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||||
|
|
||||||
@@ -520,3 +521,77 @@ export function curvePointAtLength<P extends GlobalPoint | LocalPoint>(
|
|||||||
|
|
||||||
return bezierEquation(c, t);
|
return bezierEquation(c, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function perpendicularDistance<Point extends GlobalPoint | LocalPoint>(
|
||||||
|
point: Point,
|
||||||
|
[lineStart, lineEnd]: LineSegment<Point>,
|
||||||
|
): number {
|
||||||
|
const dx = lineEnd[0] - lineStart[0];
|
||||||
|
const dy = lineEnd[1] - lineStart[1];
|
||||||
|
const norm = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
if (norm === 0) {
|
||||||
|
return pointDistance(point, lineStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
const u =
|
||||||
|
((point[0] - lineStart[0]) * dx + (point[1] - lineStart[1]) * dy) / norm;
|
||||||
|
|
||||||
|
if (u < 0) {
|
||||||
|
return pointDistance(point, lineStart);
|
||||||
|
}
|
||||||
|
if (u > 1) {
|
||||||
|
return pointDistance(point, lineEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projX = lineStart[0] + u * dx;
|
||||||
|
const projY = lineStart[1] + u * dy;
|
||||||
|
|
||||||
|
return Math.sqrt(
|
||||||
|
(point[0] - projX) * (point[0] - projX) +
|
||||||
|
(point[1] - projY) * (point[1] - projY),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function curveSimplifyWithRDP<Point extends LocalPoint | GlobalPoint>(
|
||||||
|
points: readonly Point[],
|
||||||
|
epsilon: number = 0.5,
|
||||||
|
): readonly Point[] {
|
||||||
|
if (points.length <= 2) {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack: Array<{ start: number; end: number }> = [
|
||||||
|
{ start: 0, end: points.length - 1 },
|
||||||
|
];
|
||||||
|
const keep = new Set<number>();
|
||||||
|
|
||||||
|
keep.add(0);
|
||||||
|
keep.add(points.length - 1);
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const segment = stack.pop()!;
|
||||||
|
let maxDist = 0;
|
||||||
|
let maxIndex = -1;
|
||||||
|
|
||||||
|
for (let i = segment.start + 1; i < segment.end; i++) {
|
||||||
|
const dist = perpendicularDistance(
|
||||||
|
points[i],
|
||||||
|
lineSegment(points[segment.start], points[segment.end]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dist > maxDist) {
|
||||||
|
maxDist = dist;
|
||||||
|
maxIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxDist > epsilon && maxIndex !== -1) {
|
||||||
|
keep.add(maxIndex);
|
||||||
|
stack.push({ start: segment.start, end: maxIndex });
|
||||||
|
stack.push({ start: maxIndex, end: segment.end });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points.filter((_, index) => keep.has(index));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user