mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Move path related function into the math package
This commit is contained in:
parent
1b56cf90fb
commit
9eb08df3ea
9 changed files with 72 additions and 46 deletions
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
[
|
[
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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][], {
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
|
@ -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
27
packages/math/path.ts
Normal 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;
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue