feat: improved freedraw (#3512)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Steve Ruiz 2021-05-09 16:42:10 +01:00 committed by GitHub
parent 198800136e
commit 49c6bdd520
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 786 additions and 247 deletions

View file

@ -4,7 +4,13 @@ import * as GADirection from "../gadirections";
import * as GALine from "../galines";
import * as GATransform from "../gatransforms";
import { isPathALoop, isPointInPolygon, rotate } from "../math";
import {
distance2d,
rotatePoint,
isPathALoop,
isPointInPolygon,
rotate,
} from "../math";
import { pointsOnBezierCurves } from "points-on-curve";
import {
@ -16,6 +22,7 @@ import {
ExcalidrawTextElement,
ExcalidrawEllipseElement,
NonDeleted,
ExcalidrawFreeDrawElement,
} from "./types";
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
@ -30,10 +37,17 @@ const isElementDraggableFromInside = (
if (element.type === "arrow") {
return false;
}
if (element.type === "freedraw") {
return true;
}
const isDraggableFromInside = element.backgroundColor !== "transparent";
if (element.type === "line" || element.type === "draw") {
if (element.type === "line") {
return isDraggableFromInside && isPathALoop(element.points);
}
return isDraggableFromInside;
};
@ -81,6 +95,7 @@ const isHittingElementNotConsideringBoundingBox = (
: isElementDraggableFromInside(element)
? isInsideCheck
: isNearCheck;
return hitTestPointAgainstElement({ element, point, threshold, check });
};
@ -151,6 +166,18 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
case "ellipse":
const distance = distanceToBindableElement(args.element, args.point);
return args.check(distance, args.threshold);
case "freedraw": {
if (
!args.check(
distanceToRectangle(args.element, args.point),
args.threshold,
)
) {
return false;
}
return hitTestFreeDrawElement(args.element, args.point, args.threshold);
}
case "arrow":
case "line":
case "draw":
@ -195,7 +222,10 @@ const isOutsideCheck = (distance: number, threshold: number): boolean => {
};
const distanceToRectangle = (
element: ExcalidrawRectangleElement | ExcalidrawTextElement,
element:
| ExcalidrawRectangleElement
| ExcalidrawTextElement
| ExcalidrawFreeDrawElement,
point: Point,
): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
@ -267,6 +297,71 @@ const ellipseParamsForTest = (
return [pointRel, tangent];
};
const hitTestFreeDrawElement = (
element: ExcalidrawFreeDrawElement,
point: Point,
threshold: number,
): boolean => {
// Check point-distance-to-line-segment for every segment in the
// element's points (its input points, not its outline points).
// This is... okay? It's plenty fast, but the GA library may
// have a faster option.
let x: number;
let y: number;
if (element.angle === 0) {
x = point[0] - element.x;
y = point[1] - element.y;
} else {
// Counter-rotate the point around center before testing
const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(element);
const rotatedPoint = rotatePoint(
point,
[minX + (maxX - minX) / 2, minY + (maxY - minY) / 2],
-element.angle,
);
x = rotatedPoint[0] - element.x;
y = rotatedPoint[1] - element.y;
}
let [A, B] = element.points;
let P: readonly [number, number];
// For freedraw dots
if (element.points.length === 2) {
return (
distance2d(A[0], A[1], x, y) < threshold ||
distance2d(B[0], B[1], x, y) < threshold
);
}
// For freedraw lines
for (let i = 1; i < element.points.length - 1; i++) {
const delta = [B[0] - A[0], B[1] - A[1]];
const length = Math.hypot(delta[1], delta[0]);
const U = [delta[0] / length, delta[1] / length];
const C = [x - A[0], y - A[1]];
const d = (C[0] * U[0] + C[1] * U[1]) / Math.hypot(U[1], U[0]);
P = [A[0] + U[0] * d, A[1] + U[1] * d];
const da = distance2d(P[0], P[1], A[0], A[1]);
const db = distance2d(P[0], P[1], B[0], B[1]);
P = db < da && da > length ? B : da < db && db > length ? A : P;
if (Math.hypot(y - P[1], x - P[0]) < threshold) {
return true;
}
A = B;
B = element.points[i + 1];
}
return false;
};
const hitTestLinear = (args: HitTestArgs): boolean => {
const { element, threshold } = args;
if (!getShapeForElement(element)) {