mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-26 08:24:20 +01:00
Compare commits
1 Commits
mtolmacs/c
...
mtolmacs/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa3e1341fb |
@@ -23,7 +23,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": "18.0.0 - 22.x.x"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/random-username": "1.0.0",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
"vitest-canvas-mock": "0.3.3"
|
"vitest-canvas-mock": "0.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": "18.0.0 - 22.x.x"
|
||||||
},
|
},
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { atom, useAtom } from "../editor-jotai";
|
|||||||
import { getLanguage, t } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
|
|
||||||
import Collapsible from "./Stats/Collapsible";
|
import Collapsible from "./Stats/Collapsible";
|
||||||
import { useDevice, useExcalidrawContainer } from "./App";
|
import { useDevice } from "./App";
|
||||||
|
|
||||||
import "./IconPicker.scss";
|
import "./IconPicker.scss";
|
||||||
|
|
||||||
@@ -39,7 +39,6 @@ function Picker<T>({
|
|||||||
numberOfOptionsToAlwaysShow?: number;
|
numberOfOptionsToAlwaysShow?: number;
|
||||||
}) {
|
}) {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const { container } = useExcalidrawContainer();
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||||
const pressedOption = options.find(
|
const pressedOption = options.find(
|
||||||
@@ -162,7 +161,6 @@ function Picker<T>({
|
|||||||
sideOffset={isMobile ? 8 : 12}
|
sideOffset={isMobile ? 8 : 12}
|
||||||
style={{ zIndex: "var(--zIndex-ui-styles-popup)" }}
|
style={{ zIndex: "var(--zIndex-ui-styles-popup)" }}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
collisionBoundary={container ?? undefined}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`picker`}
|
className={`picker`}
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ export const PropertiesPopover = React.forwardRef<
|
|||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const isMobilePortrait =
|
|
||||||
device.editor.isMobile && !device.viewport.isLandscape;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Portal container={container}>
|
<Popover.Portal container={container}>
|
||||||
@@ -49,11 +47,18 @@ export const PropertiesPopover = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx("focus-visible-none", className)}
|
className={clsx("focus-visible-none", className)}
|
||||||
data-prevent-outside-click
|
data-prevent-outside-click
|
||||||
side={isMobilePortrait ? "bottom" : "right"}
|
side={
|
||||||
align={isMobilePortrait ? "center" : "start"}
|
device.editor.isMobile && !device.viewport.isLandscape
|
||||||
|
? "bottom"
|
||||||
|
: "right"
|
||||||
|
}
|
||||||
|
align={
|
||||||
|
device.editor.isMobile && !device.viewport.isLandscape
|
||||||
|
? "center"
|
||||||
|
: "start"
|
||||||
|
}
|
||||||
alignOffset={-16}
|
alignOffset={-16}
|
||||||
sideOffset={20}
|
sideOffset={20}
|
||||||
collisionBoundary={container ?? undefined}
|
|
||||||
style={{
|
style={{
|
||||||
zIndex: "var(--zIndex-ui-styles-popup)",
|
zIndex: "var(--zIndex-ui-styles-popup)",
|
||||||
marginLeft: device.editor.isMobile ? "0.5rem" : undefined,
|
marginLeft: device.editor.isMobile ? "0.5rem" : undefined,
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import { ToolButton } from "./ToolButton";
|
|||||||
|
|
||||||
import "./ToolPopover.scss";
|
import "./ToolPopover.scss";
|
||||||
|
|
||||||
import { useExcalidrawContainer } from "./App";
|
|
||||||
|
|
||||||
import type { AppClassProperties } from "../types";
|
import type { AppClassProperties } from "../types";
|
||||||
|
|
||||||
type ToolOption = {
|
type ToolOption = {
|
||||||
@@ -52,7 +50,6 @@ export const ToolPopover = ({
|
|||||||
const currentType = activeTool.type;
|
const currentType = activeTool.type;
|
||||||
const isActive = displayedOption.type === currentType;
|
const isActive = displayedOption.type === currentType;
|
||||||
const SIDE_OFFSET = 32 / 2 + 10;
|
const SIDE_OFFSET = 32 / 2 + 10;
|
||||||
const { container } = useExcalidrawContainer();
|
|
||||||
|
|
||||||
// if currentType is not in options, close popup
|
// if currentType is not in options, close popup
|
||||||
if (!options.some((o) => o.type === currentType) && isPopupOpen) {
|
if (!options.some((o) => o.type === currentType) && isPopupOpen) {
|
||||||
@@ -93,7 +90,6 @@ export const ToolPopover = ({
|
|||||||
<Popover.Content
|
<Popover.Content
|
||||||
className="tool-popover-content"
|
className="tool-popover-content"
|
||||||
sideOffset={SIDE_OFFSET}
|
sideOffset={SIDE_OFFSET}
|
||||||
collisionBoundary={container ?? undefined}
|
|
||||||
>
|
>
|
||||||
{options.map(({ type, icon, title }) => (
|
{options.map(({ type, icon, title }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
|
|||||||
@@ -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