Move path related function into the math package

This commit is contained in:
Mark Tolmacs 2024-09-23 18:08:17 +02:00
parent 1b56cf90fb
commit 9eb08df3ea
No known key found for this signature in database
9 changed files with 72 additions and 46 deletions

View file

@ -15,8 +15,7 @@ import { isBindingElement, isLinearElement } from "../element/typeChecks";
import type { AppState } from "../types"; import type { AppState } from "../types";
import { resetCursor } from "../cursor"; import { resetCursor } from "../cursor";
import { StoreAction } from "../store"; import { StoreAction } from "../store";
import { point } from "../../math"; import { pathIsALoop, point } from "../../math";
import { isPathALoop } from "../shapes";
export const actionFinalize = register({ export const actionFinalize = register({
name: "finalize", name: "finalize",
@ -104,7 +103,7 @@ export const actionFinalize = register({
// If the multi point line closes the loop, // If the multi point line closes the loop,
// set the last point to first point. // set the last point to first point.
// This ensures that loop remains closed at different scales. // This ensures that loop remains closed at different scales.
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value); const isLoop = pathIsALoop(multiPointElement.points, appState.zoom.value);
if ( if (
multiPointElement.type === "line" || multiPointElement.type === "line" ||
multiPointElement.type === "freedraw" multiPointElement.type === "freedraw"

View file

@ -229,7 +229,6 @@ import {
getBoundTextShape, getBoundTextShape,
getCornerRadius, getCornerRadius,
getElementShape, getElementShape,
isPathALoop,
} from "../shapes"; } from "../shapes";
import { getSelectionBoxShape } from "../../utils/geometry/shape"; import { getSelectionBoxShape } from "../../utils/geometry/shape";
import { isPointInShape } from "../../utils/collision"; import { isPointInShape } from "../../utils/collision";
@ -449,6 +448,7 @@ import type {
ViewportPoint, ViewportPoint,
} from "../../math"; } from "../../math";
import { import {
pathIsALoop,
point, point,
pointCenter, pointCenter,
pointDistance, pointDistance,
@ -5605,7 +5605,9 @@ class App extends React.Component<AppProps, AppState> {
)); ));
} }
if (isPathALoop(points, this.state.zoom.value)) { if (
pathIsALoop(points, LINE_CONFIRM_THRESHOLD / this.state.zoom.value)
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} }
if (isElbowArrow(multiElement)) { if (isElbowArrow(multiElement)) {
@ -7206,7 +7208,10 @@ class App extends React.Component<AppProps, AppState> {
// finalize if completing a loop // finalize if completing a loop
if ( if (
multiElement.type === "line" && multiElement.type === "line" &&
isPathALoop(multiElement.points, this.state.zoom.value) pathIsALoop(
multiElement.points,
LINE_CONFIRM_THRESHOLD / this.state.zoom.value,
)
) { ) {
mutateElement(multiElement, { mutateElement(multiElement, {
lastCommittedPoint: lastCommittedPoint:

View file

@ -15,9 +15,10 @@ import {
isImageElement, isImageElement,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { getBoundTextShape, isPathALoop } from "../shapes"; import { getBoundTextShape } from "../shapes";
import type { GlobalPoint, LocalPoint, Polygon } from "../../math"; import type { GlobalPoint, LocalPoint, Polygon } from "../../math";
import { isPointWithinBounds, point } from "../../math"; import { pathIsALoop, isPointWithinBounds, point } from "../../math";
import { LINE_CONFIRM_THRESHOLD } from "../constants";
export const shouldTestInside = (element: ExcalidrawElement) => { export const shouldTestInside = (element: ExcalidrawElement) => {
if (element.type === "arrow") { if (element.type === "arrow") {
@ -31,11 +32,17 @@ export const shouldTestInside = (element: ExcalidrawElement) => {
isTextElement(element); isTextElement(element);
if (element.type === "line") { if (element.type === "line") {
return isDraggableFromInside && isPathALoop(element.points); return (
isDraggableFromInside &&
pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD)
);
} }
if (element.type === "freedraw") { if (element.type === "freedraw") {
return isDraggableFromInside && isPathALoop(element.points); return (
isDraggableFromInside &&
pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD)
);
} }
return isDraggableFromInside || isImageElement(element); return isDraggableFromInside || isImageElement(element);

View file

@ -39,7 +39,7 @@ import {
} from "./typeChecks"; } from "./typeChecks";
import { KEYS, shouldRotateWithDiscreteAngle } from "../keys"; import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { DRAGGING_THRESHOLD } from "../constants"; import { DRAGGING_THRESHOLD, LINE_CONFIRM_THRESHOLD } from "../constants";
import type { Mutable } from "../utility-types"; import type { Mutable } from "../utility-types";
import { ShapeCache } from "../scene/ShapeCache"; import { ShapeCache } from "../scene/ShapeCache";
import type { Store } from "../store"; import type { Store } from "../store";
@ -57,12 +57,12 @@ import {
pointDistance, pointDistance,
pointSubtract, pointSubtract,
pointFromPair, pointFromPair,
pathIsALoop,
} from "../../math"; } from "../../math";
import { import {
getBezierCurveLength, getBezierCurveLength,
getBezierXY, getBezierXY,
getControlPointsForBezierCurve, getControlPointsForBezierCurve,
isPathALoop,
mapIntervalToBezierT, mapIntervalToBezierT,
} from "../shapes"; } from "../shapes";
import { getGridPoint } from "../snapping"; import { getGridPoint } from "../snapping";
@ -418,7 +418,12 @@ export class LinearElementEditor {
selectedPoint === 0 || selectedPoint === 0 ||
selectedPoint === element.points.length - 1 selectedPoint === element.points.length - 1
) { ) {
if (isPathALoop(element.points, appState.zoom.value)) { if (
pathIsALoop(
element.points,
LINE_CONFIRM_THRESHOLD / appState.zoom.value,
)
) {
LinearElementEditor.movePoints( LinearElementEditor.movePoints(
element, element,
[ [

View file

@ -2,6 +2,7 @@ import type { Drawable } from "roughjs/bin/core";
import type { RoughSVG } from "roughjs/bin/svg"; import type { RoughSVG } from "roughjs/bin/svg";
import { import {
FRAME_STYLE, FRAME_STYLE,
LINE_CONFIRM_THRESHOLD,
MAX_DECIMALS_FOR_SVG_EXPORT, MAX_DECIMALS_FOR_SVG_EXPORT,
MIME_TYPES, MIME_TYPES,
SVG_NS, SVG_NS,
@ -36,7 +37,8 @@ import type { AppState, BinaryFiles } from "../types";
import { getFontFamilyString, isRTL, isTestEnv } from "../utils"; import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement"; import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
import { getVerticalOffset } from "../fonts"; import { getVerticalOffset } from "../fonts";
import { getCornerRadius, isPathALoop } from "../shapes"; import { getCornerRadius } from "../shapes";
import { pathIsALoop } from "../../math";
const roughSVGDrawWithPrecision = ( const roughSVGDrawWithPrecision = (
rsvg: RoughSVG, rsvg: RoughSVG,
@ -341,7 +343,7 @@ const renderElementToSvg = (
); );
if ( if (
element.type === "line" && element.type === "line" &&
isPathALoop(element.points) && pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD) &&
element.backgroundColor !== "transparent" element.backgroundColor !== "transparent"
) { ) {
node.setAttribute("fill-rule", "evenodd"); node.setAttribute("fill-rule", "evenodd");

View file

@ -13,7 +13,7 @@ import type {
import { generateFreeDrawShape } from "../renderer/renderElement"; import { generateFreeDrawShape } from "../renderer/renderElement";
import { isTransparent, assertNever } from "../utils"; import { isTransparent, assertNever } from "../utils";
import { simplify } from "points-on-curve"; import { simplify } from "points-on-curve";
import { ROUGHNESS } from "../constants"; import { LINE_CONFIRM_THRESHOLD, ROUGHNESS } from "../constants";
import { import {
isElbowArrow, isElbowArrow,
isEmbeddableElement, isEmbeddableElement,
@ -24,12 +24,13 @@ import {
import { canChangeRoundness } from "./comparisons"; import { canChangeRoundness } from "./comparisons";
import type { EmbedsValidationStatus } from "../types"; import type { EmbedsValidationStatus } from "../types";
import { import {
pathIsALoop,
point, point,
pointDistance, pointDistance,
type GlobalPoint, type GlobalPoint,
type LocalPoint, type LocalPoint,
} from "../../math"; } from "../../math";
import { getCornerRadius, isPathALoop } from "../shapes"; import { getCornerRadius } from "../shapes";
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
@ -107,7 +108,7 @@ export const generateRoughOptions = (
} }
case "line": case "line":
case "freedraw": { case "freedraw": {
if (isPathALoop(element.points)) { if (pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD)) {
options.fillStyle = element.fillStyle; options.fillStyle = element.fillStyle;
options.fill = options.fill =
element.backgroundColor === "transparent" element.backgroundColor === "transparent"
@ -473,7 +474,7 @@ export const _generateElementShape = (
let shape: ElementShapes[typeof element.type]; let shape: ElementShapes[typeof element.type];
generateFreeDrawShape(element); generateFreeDrawShape(element);
if (isPathALoop(element.points)) { if (pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD)) {
// generate rough polygon to fill freedraw shape // generate rough polygon to fill freedraw shape
const simplifiedPoints = simplify(element.points, 0.75); const simplifiedPoints = simplify(element.points, 0.75);
shape = generator.curve(simplifiedPoints as [number, number][], { shape = generator.curve(simplifiedPoints as [number, number][], {

View file

@ -1,4 +1,4 @@
import type { ViewportPoint } from "../math"; import type { GenericPoint, ViewportPoint } from "../math";
import { import {
isPoint, isPoint,
point, point,
@ -33,7 +33,6 @@ import {
import { import {
DEFAULT_ADAPTIVE_RADIUS, DEFAULT_ADAPTIVE_RADIUS,
DEFAULT_PROPORTIONAL_RADIUS, DEFAULT_PROPORTIONAL_RADIUS,
LINE_CONFIRM_THRESHOLD,
ROUNDNESS, ROUNDNESS,
} from "./constants"; } from "./constants";
import { getElementAbsoluteCoords } from "./element"; import { getElementAbsoluteCoords } from "./element";
@ -49,7 +48,6 @@ import type {
} from "./element/types"; } from "./element/types";
import { KEYS } from "./keys"; import { KEYS } from "./keys";
import { ShapeCache } from "./scene/ShapeCache"; import { ShapeCache } from "./scene/ShapeCache";
import type { NormalizedZoomValue, Zoom } from "./types";
import { invariant } from "./utils"; import { invariant } from "./utils";
export const SHAPES = [ export const SHAPES = [
@ -222,9 +220,7 @@ export const getBoundTextShape = <Point extends GlobalPoint | LocalPoint>(
return null; return null;
}; };
export const getControlPointsForBezierCurve = < export const getControlPointsForBezierCurve = <P extends GenericPoint>(
P extends GlobalPoint | LocalPoint,
>(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
endPoint: P, endPoint: P,
) => { ) => {
@ -266,7 +262,7 @@ export const getControlPointsForBezierCurve = <
return controlPoints; return controlPoints;
}; };
export const getBezierXY = <P extends GlobalPoint | LocalPoint>( export const getBezierXY = <P extends GenericPoint>(
p0: P, p0: P,
p1: P, p1: P,
p2: P, p2: P,
@ -283,7 +279,7 @@ export const getBezierXY = <P extends GlobalPoint | LocalPoint>(
return point(tx, ty); return point(tx, ty);
}; };
const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>( const getPointsInBezierCurve = <P extends GenericPoint>(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
endPoint: P, endPoint: P,
) => { ) => {
@ -313,7 +309,7 @@ const getPointsInBezierCurve = <P extends GlobalPoint | LocalPoint>(
return pointsOnCurve; return pointsOnCurve;
}; };
const getBezierCurveArcLengths = <P extends GlobalPoint | LocalPoint>( const getBezierCurveArcLengths = <P extends GenericPoint>(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
endPoint: P, endPoint: P,
) => { ) => {
@ -476,21 +472,3 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => {
return 0; return 0;
}; };
// Checks if the first and last point are close enough
// to be considered a loop
export const isPathALoop = (
points: ExcalidrawLinearElement["points"],
/** supply if you want the loop detection to account for current zoom */
zoomValue: Zoom["value"] = 1 as NormalizedZoomValue,
): boolean => {
if (points.length >= 3) {
const [first, last] = [points[0], points[points.length - 1]];
const distance = pointDistance(first, last);
// Adjusting LINE_CONFIRM_THRESHOLD to current zoom so that when zoomed in
// really close we make the threshold smaller, and vice versa.
return distance <= LINE_CONFIRM_THRESHOLD / zoomValue;
}
return false;
};

View file

@ -2,9 +2,11 @@ export * from "./arc";
export * from "./angle"; export * from "./angle";
export * from "./curve"; export * from "./curve";
export * from "./line"; export * from "./line";
export * from "./path";
export * from "./point"; export * from "./point";
export * from "./polygon"; export * from "./polygon";
export * from "./range"; export * from "./range";
export * from "./rectangle";
export * from "./segment"; export * from "./segment";
export * from "./triangle"; export * from "./triangle";
export * from "./types"; export * from "./types";

27
packages/math/path.ts Normal file
View file

@ -0,0 +1,27 @@
import { pointDistance } from "./point";
import type { LocalPoint } from "./types";
/**
* Checks if the first and last point are close enough to be considered a loop
*
* @param points
* @param threshold
* @returns
*/
export const pathIsALoop = (
points: readonly LocalPoint[],
/** supply if you want the loop detection to account for current zoom */
threshold: number,
//zoomValue: Zoom["value"] = 1 as NormalizedZoomValue,
): boolean => {
if (points.length >= 3) {
const [first, last] = [points[0], points[points.length - 1]];
const distance = pointDistance(first, last);
// Adjusting LINE_CONFIRM_THRESHOLD to current zoom so that when zoomed in
// really close we make the threshold smaller, and vice versa.
return distance <= threshold; // LINE_CONFIRM_THRESHOLD / zoomValue;
}
return false;
};