From 9eb08df3ea9bf71abd8ed6199112544aa80591cd Mon Sep 17 00:00:00 2001 From: Mark Tolmacs Date: Mon, 23 Sep 2024 18:08:17 +0200 Subject: [PATCH] Move path related function into the math package --- .../excalidraw/actions/actionFinalize.tsx | 5 ++- packages/excalidraw/components/App.tsx | 11 +++++-- packages/excalidraw/element/collision.ts | 15 ++++++--- .../excalidraw/element/linearElementEditor.ts | 11 +++++-- .../excalidraw/renderer/staticSvgScene.ts | 6 ++-- packages/excalidraw/scene/Shape.ts | 9 +++--- packages/excalidraw/shapes.tsx | 32 +++---------------- packages/math/index.ts | 2 ++ packages/math/path.ts | 27 ++++++++++++++++ 9 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 packages/math/path.ts diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index c6cc142f3c..6184b4294a 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -15,8 +15,7 @@ import { isBindingElement, isLinearElement } from "../element/typeChecks"; import type { AppState } from "../types"; import { resetCursor } from "../cursor"; import { StoreAction } from "../store"; -import { point } from "../../math"; -import { isPathALoop } from "../shapes"; +import { pathIsALoop, point } from "../../math"; export const actionFinalize = register({ name: "finalize", @@ -104,7 +103,7 @@ export const actionFinalize = register({ // If the multi point line closes the loop, // set the last point to first point. // 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 ( multiPointElement.type === "line" || multiPointElement.type === "freedraw" diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 71a4d05546..14c6d4344a 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -229,7 +229,6 @@ import { getBoundTextShape, getCornerRadius, getElementShape, - isPathALoop, } from "../shapes"; import { getSelectionBoxShape } from "../../utils/geometry/shape"; import { isPointInShape } from "../../utils/collision"; @@ -449,6 +448,7 @@ import type { ViewportPoint, } from "../../math"; import { + pathIsALoop, point, pointCenter, pointDistance, @@ -5605,7 +5605,9 @@ class App extends React.Component { )); } - if (isPathALoop(points, this.state.zoom.value)) { + if ( + pathIsALoop(points, LINE_CONFIRM_THRESHOLD / this.state.zoom.value) + ) { setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } if (isElbowArrow(multiElement)) { @@ -7206,7 +7208,10 @@ class App extends React.Component { // finalize if completing a loop if ( multiElement.type === "line" && - isPathALoop(multiElement.points, this.state.zoom.value) + pathIsALoop( + multiElement.points, + LINE_CONFIRM_THRESHOLD / this.state.zoom.value, + ) ) { mutateElement(multiElement, { lastCommittedPoint: diff --git a/packages/excalidraw/element/collision.ts b/packages/excalidraw/element/collision.ts index e7c7f334bd..773364e2db 100644 --- a/packages/excalidraw/element/collision.ts +++ b/packages/excalidraw/element/collision.ts @@ -15,9 +15,10 @@ import { isImageElement, isTextElement, } from "./typeChecks"; -import { getBoundTextShape, isPathALoop } from "../shapes"; +import { getBoundTextShape } from "../shapes"; 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) => { if (element.type === "arrow") { @@ -31,11 +32,17 @@ export const shouldTestInside = (element: ExcalidrawElement) => { isTextElement(element); if (element.type === "line") { - return isDraggableFromInside && isPathALoop(element.points); + return ( + isDraggableFromInside && + pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD) + ); } if (element.type === "freedraw") { - return isDraggableFromInside && isPathALoop(element.points); + return ( + isDraggableFromInside && + pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD) + ); } return isDraggableFromInside || isImageElement(element); diff --git a/packages/excalidraw/element/linearElementEditor.ts b/packages/excalidraw/element/linearElementEditor.ts index 357bd76206..cdb1738fb5 100644 --- a/packages/excalidraw/element/linearElementEditor.ts +++ b/packages/excalidraw/element/linearElementEditor.ts @@ -39,7 +39,7 @@ import { } from "./typeChecks"; import { KEYS, shouldRotateWithDiscreteAngle } from "../keys"; 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 { ShapeCache } from "../scene/ShapeCache"; import type { Store } from "../store"; @@ -57,12 +57,12 @@ import { pointDistance, pointSubtract, pointFromPair, + pathIsALoop, } from "../../math"; import { getBezierCurveLength, getBezierXY, getControlPointsForBezierCurve, - isPathALoop, mapIntervalToBezierT, } from "../shapes"; import { getGridPoint } from "../snapping"; @@ -418,7 +418,12 @@ export class LinearElementEditor { selectedPoint === 0 || selectedPoint === element.points.length - 1 ) { - if (isPathALoop(element.points, appState.zoom.value)) { + if ( + pathIsALoop( + element.points, + LINE_CONFIRM_THRESHOLD / appState.zoom.value, + ) + ) { LinearElementEditor.movePoints( element, [ diff --git a/packages/excalidraw/renderer/staticSvgScene.ts b/packages/excalidraw/renderer/staticSvgScene.ts index f0bf989670..07ff610ebf 100644 --- a/packages/excalidraw/renderer/staticSvgScene.ts +++ b/packages/excalidraw/renderer/staticSvgScene.ts @@ -2,6 +2,7 @@ import type { Drawable } from "roughjs/bin/core"; import type { RoughSVG } from "roughjs/bin/svg"; import { FRAME_STYLE, + LINE_CONFIRM_THRESHOLD, MAX_DECIMALS_FOR_SVG_EXPORT, MIME_TYPES, SVG_NS, @@ -36,7 +37,8 @@ import type { AppState, BinaryFiles } from "../types"; import { getFontFamilyString, isRTL, isTestEnv } from "../utils"; import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement"; import { getVerticalOffset } from "../fonts"; -import { getCornerRadius, isPathALoop } from "../shapes"; +import { getCornerRadius } from "../shapes"; +import { pathIsALoop } from "../../math"; const roughSVGDrawWithPrecision = ( rsvg: RoughSVG, @@ -341,7 +343,7 @@ const renderElementToSvg = ( ); if ( element.type === "line" && - isPathALoop(element.points) && + pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD) && element.backgroundColor !== "transparent" ) { node.setAttribute("fill-rule", "evenodd"); diff --git a/packages/excalidraw/scene/Shape.ts b/packages/excalidraw/scene/Shape.ts index fad0f4f938..192b61ef04 100644 --- a/packages/excalidraw/scene/Shape.ts +++ b/packages/excalidraw/scene/Shape.ts @@ -13,7 +13,7 @@ import type { import { generateFreeDrawShape } from "../renderer/renderElement"; import { isTransparent, assertNever } from "../utils"; import { simplify } from "points-on-curve"; -import { ROUGHNESS } from "../constants"; +import { LINE_CONFIRM_THRESHOLD, ROUGHNESS } from "../constants"; import { isElbowArrow, isEmbeddableElement, @@ -24,12 +24,13 @@ import { import { canChangeRoundness } from "./comparisons"; import type { EmbedsValidationStatus } from "../types"; import { + pathIsALoop, point, pointDistance, type GlobalPoint, type LocalPoint, } from "../../math"; -import { getCornerRadius, isPathALoop } from "../shapes"; +import { getCornerRadius } from "../shapes"; const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; @@ -107,7 +108,7 @@ export const generateRoughOptions = ( } case "line": case "freedraw": { - if (isPathALoop(element.points)) { + if (pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD)) { options.fillStyle = element.fillStyle; options.fill = element.backgroundColor === "transparent" @@ -473,7 +474,7 @@ export const _generateElementShape = ( let shape: ElementShapes[typeof element.type]; generateFreeDrawShape(element); - if (isPathALoop(element.points)) { + if (pathIsALoop(element.points, LINE_CONFIRM_THRESHOLD)) { // generate rough polygon to fill freedraw shape const simplifiedPoints = simplify(element.points, 0.75); shape = generator.curve(simplifiedPoints as [number, number][], { diff --git a/packages/excalidraw/shapes.tsx b/packages/excalidraw/shapes.tsx index eb8403a667..21df33a088 100644 --- a/packages/excalidraw/shapes.tsx +++ b/packages/excalidraw/shapes.tsx @@ -1,4 +1,4 @@ -import type { ViewportPoint } from "../math"; +import type { GenericPoint, ViewportPoint } from "../math"; import { isPoint, point, @@ -33,7 +33,6 @@ import { import { DEFAULT_ADAPTIVE_RADIUS, DEFAULT_PROPORTIONAL_RADIUS, - LINE_CONFIRM_THRESHOLD, ROUNDNESS, } from "./constants"; import { getElementAbsoluteCoords } from "./element"; @@ -49,7 +48,6 @@ import type { } from "./element/types"; import { KEYS } from "./keys"; import { ShapeCache } from "./scene/ShapeCache"; -import type { NormalizedZoomValue, Zoom } from "./types"; import { invariant } from "./utils"; export const SHAPES = [ @@ -222,9 +220,7 @@ export const getBoundTextShape = ( return null; }; -export const getControlPointsForBezierCurve = < - P extends GlobalPoint | LocalPoint, ->( +export const getControlPointsForBezierCurve =

( element: NonDeleted, endPoint: P, ) => { @@ -266,7 +262,7 @@ export const getControlPointsForBezierCurve = < return controlPoints; }; -export const getBezierXY =

( +export const getBezierXY =

( p0: P, p1: P, p2: P, @@ -283,7 +279,7 @@ export const getBezierXY =

( return point(tx, ty); }; -const getPointsInBezierCurve =

( +const getPointsInBezierCurve =

( element: NonDeleted, endPoint: P, ) => { @@ -313,7 +309,7 @@ const getPointsInBezierCurve =

( return pointsOnCurve; }; -const getBezierCurveArcLengths =

( +const getBezierCurveArcLengths =

( element: NonDeleted, endPoint: P, ) => { @@ -476,21 +472,3 @@ export const getCornerRadius = (x: number, element: ExcalidrawElement) => { 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; -}; diff --git a/packages/math/index.ts b/packages/math/index.ts index 05ec5158fc..a9ea48a6e0 100644 --- a/packages/math/index.ts +++ b/packages/math/index.ts @@ -2,9 +2,11 @@ export * from "./arc"; export * from "./angle"; export * from "./curve"; export * from "./line"; +export * from "./path"; export * from "./point"; export * from "./polygon"; export * from "./range"; +export * from "./rectangle"; export * from "./segment"; export * from "./triangle"; export * from "./types"; diff --git a/packages/math/path.ts b/packages/math/path.ts new file mode 100644 index 0000000000..1bd53084fa --- /dev/null +++ b/packages/math/path.ts @@ -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; +};