mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-10-24 00:14:25 +02:00
Compare commits
1 Commits
mtolmacs/c
...
mtolmacs/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa3e1341fb |
@@ -23,7 +23,7 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": "18.0.0 - 22.x.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"vitest-canvas-mock": "0.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": "18.0.0 - 22.x.x"
|
||||
},
|
||||
"homepage": ".",
|
||||
"prettier": "@excalidraw/prettier-config",
|
||||
|
||||
@@ -1052,15 +1052,14 @@ export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
|
||||
|
||||
export function getFreedrawOutlineAsSegments(
|
||||
element: ExcalidrawFreeDrawElement,
|
||||
points: [number, number][],
|
||||
elementsMap: ElementsMap,
|
||||
points: readonly [number, number][],
|
||||
) {
|
||||
const bounds = getElementBounds(
|
||||
{
|
||||
...element,
|
||||
angle: 0 as Radians,
|
||||
},
|
||||
elementsMap,
|
||||
new Map(),
|
||||
);
|
||||
const center = pointFrom<GlobalPoint>(
|
||||
(bounds[0] + bounds[2]) / 2,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveOffsetPoints,
|
||||
lineSegment,
|
||||
perpendicularDistance,
|
||||
pointDistance,
|
||||
pointFrom,
|
||||
pointFromArray,
|
||||
@@ -481,3 +482,24 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
|
||||
|
||||
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,
|
||||
type ApplyToOptions,
|
||||
positionElementsOnGrid,
|
||||
shouldSkipFreedrawPoint,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
@@ -8922,10 +8923,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
const points = newElement.points;
|
||||
const dx = pointerCoords.x - newElement.x;
|
||||
const dy = pointerCoords.y - newElement.y;
|
||||
|
||||
const lastPoint = points.length > 0 && points[points.length - 1];
|
||||
const discardPoint =
|
||||
lastPoint && lastPoint[0] === dx && lastPoint[1] === dy;
|
||||
const discardPoint = shouldSkipFreedrawPoint(points, dx, dy, 0.5);
|
||||
|
||||
if (!discardPoint) {
|
||||
const pressures = newElement.simulatePressure
|
||||
|
||||
@@ -8,7 +8,7 @@ import { atom, useAtom } from "../editor-jotai";
|
||||
import { getLanguage, t } from "../i18n";
|
||||
|
||||
import Collapsible from "./Stats/Collapsible";
|
||||
import { useDevice, useExcalidrawContainer } from "./App";
|
||||
import { useDevice } from "./App";
|
||||
|
||||
import "./IconPicker.scss";
|
||||
|
||||
@@ -39,7 +39,6 @@ function Picker<T>({
|
||||
numberOfOptionsToAlwaysShow?: number;
|
||||
}) {
|
||||
const device = useDevice();
|
||||
const { container } = useExcalidrawContainer();
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
const pressedOption = options.find(
|
||||
@@ -162,7 +161,6 @@ function Picker<T>({
|
||||
sideOffset={isMobile ? 8 : 12}
|
||||
style={{ zIndex: "var(--zIndex-ui-styles-popup)" }}
|
||||
onKeyDown={handleKeyDown}
|
||||
collisionBoundary={container ?? undefined}
|
||||
>
|
||||
<div
|
||||
className={`picker`}
|
||||
|
||||
@@ -40,8 +40,6 @@ export const PropertiesPopover = React.forwardRef<
|
||||
ref,
|
||||
) => {
|
||||
const device = useDevice();
|
||||
const isMobilePortrait =
|
||||
device.editor.isMobile && !device.viewport.isLandscape;
|
||||
|
||||
return (
|
||||
<Popover.Portal container={container}>
|
||||
@@ -49,11 +47,18 @@ export const PropertiesPopover = React.forwardRef<
|
||||
ref={ref}
|
||||
className={clsx("focus-visible-none", className)}
|
||||
data-prevent-outside-click
|
||||
side={isMobilePortrait ? "bottom" : "right"}
|
||||
align={isMobilePortrait ? "center" : "start"}
|
||||
side={
|
||||
device.editor.isMobile && !device.viewport.isLandscape
|
||||
? "bottom"
|
||||
: "right"
|
||||
}
|
||||
align={
|
||||
device.editor.isMobile && !device.viewport.isLandscape
|
||||
? "center"
|
||||
: "start"
|
||||
}
|
||||
alignOffset={-16}
|
||||
sideOffset={20}
|
||||
collisionBoundary={container ?? undefined}
|
||||
style={{
|
||||
zIndex: "var(--zIndex-ui-styles-popup)",
|
||||
marginLeft: device.editor.isMobile ? "0.5rem" : undefined,
|
||||
|
||||
@@ -11,8 +11,6 @@ import { ToolButton } from "./ToolButton";
|
||||
|
||||
import "./ToolPopover.scss";
|
||||
|
||||
import { useExcalidrawContainer } from "./App";
|
||||
|
||||
import type { AppClassProperties } from "../types";
|
||||
|
||||
type ToolOption = {
|
||||
@@ -52,7 +50,6 @@ export const ToolPopover = ({
|
||||
const currentType = activeTool.type;
|
||||
const isActive = displayedOption.type === currentType;
|
||||
const SIDE_OFFSET = 32 / 2 + 10;
|
||||
const { container } = useExcalidrawContainer();
|
||||
|
||||
// if currentType is not in options, close popup
|
||||
if (!options.some((o) => o.type === currentType) && isPopupOpen) {
|
||||
@@ -93,7 +90,6 @@ export const ToolPopover = ({
|
||||
<Popover.Content
|
||||
className="tool-popover-content"
|
||||
sideOffset={SIDE_OFFSET}
|
||||
collisionBoundary={container ?? undefined}
|
||||
>
|
||||
{options.map(({ type, icon, title }) => (
|
||||
<ToolButton
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { isFiniteNumber, pointFrom } from "@excalidraw/math";
|
||||
import {
|
||||
curveSimplifyWithRDP,
|
||||
isFiniteNumber,
|
||||
pointFrom,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
@@ -18,7 +22,11 @@ import {
|
||||
normalizeLink,
|
||||
getLineHeight,
|
||||
} from "@excalidraw/common";
|
||||
import { getNonDeletedElements, isValidPolygon } from "@excalidraw/element";
|
||||
import {
|
||||
getNonDeletedElements,
|
||||
isFreeDrawElement,
|
||||
isValidPolygon,
|
||||
} from "@excalidraw/element";
|
||||
import { normalizeFixedPoint } from "@excalidraw/element";
|
||||
import {
|
||||
updateElbowArrowPoints,
|
||||
@@ -623,6 +631,12 @@ export const restoreElements = (
|
||||
(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
|
||||
|
||||
@@ -238,11 +238,7 @@ const eraserTest = (
|
||||
// which offers a good visual precision at various zoom levels
|
||||
if (isFreeDrawElement(element)) {
|
||||
const outlinePoints = getFreedrawOutlinePoints(element);
|
||||
const strokeSegments = getFreedrawOutlineAsSegments(
|
||||
element,
|
||||
outlinePoints,
|
||||
elementsMap,
|
||||
);
|
||||
const strokeSegments = getFreedrawOutlineAsSegments(element, outlinePoints);
|
||||
const tolerance = Math.max(2.25, 5 / zoom); // NOTE: Visually fine-tuned approximation
|
||||
|
||||
for (const seg of strokeSegments) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point";
|
||||
import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector";
|
||||
import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants";
|
||||
import { lineSegment } from "./segment";
|
||||
|
||||
import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types";
|
||||
|
||||
@@ -520,3 +521,77 @@ export function curvePointAtLength<P extends GlobalPoint | LocalPoint>(
|
||||
|
||||
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