Precise hit testing
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
parent
6b5fb30d69
commit
04e1bf0bc4
@ -27,8 +27,6 @@ import {
|
|||||||
PRECISION,
|
PRECISION,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { isPointOnShape } from "@excalidraw/utils/collision";
|
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
|
|
||||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||||
@ -41,7 +39,7 @@ import {
|
|||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { intersectElementWithLineSegment } from "./collision";
|
import { intersectElementWithLineSegment } from "./collision";
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
headingForPointFromElement,
|
headingForPointFromElement,
|
||||||
headingIsHorizontal,
|
headingIsHorizontal,
|
||||||
@ -63,7 +61,7 @@ import {
|
|||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
|
|
||||||
import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes";
|
import { aabbForElement } from "./shapes";
|
||||||
import { updateElbowArrowPoints } from "./elbowArrow";
|
import { updateElbowArrowPoints } from "./elbowArrow";
|
||||||
|
|
||||||
import type { Scene } from "./Scene";
|
import type { Scene } from "./Scene";
|
||||||
@ -704,7 +702,7 @@ const calculateFocusAndGap = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint),
|
||||||
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)),
|
gap: Math.max(1, distanceToElement(hoveredElement, edgePoint)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -898,7 +896,7 @@ const getDistanceForBinding = (
|
|||||||
bindableElement: ExcalidrawBindableElement,
|
bindableElement: ExcalidrawBindableElement,
|
||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const distance = distanceToBindableElement(bindableElement, point);
|
const distance = distanceToElement(bindableElement, point);
|
||||||
const bindDistance = maxBindingGap(
|
const bindDistance = maxBindingGap(
|
||||||
bindableElement,
|
bindableElement,
|
||||||
bindableElement.width,
|
bindableElement.width,
|
||||||
@ -1548,14 +1546,22 @@ export const bindingBorderTest = (
|
|||||||
zoom?: AppState["zoom"],
|
zoom?: AppState["zoom"],
|
||||||
fullShape?: boolean,
|
fullShape?: boolean,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
const p = pointFrom<GlobalPoint>(x, y);
|
||||||
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
|
const shouldTestInside =
|
||||||
const shape = getElementShape(element, elementsMap);
|
// disable fullshape snapping for frame elements so we
|
||||||
return (
|
// can bind to frame children
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
(fullShape || !isBindingFallthroughEnabled(element)) &&
|
||||||
(fullShape === true &&
|
!isFrameLikeElement(element);
|
||||||
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
const intersections = intersectElementWithLineSegment(
|
||||||
|
element,
|
||||||
|
lineSegment(elementCenterPoint(element), p),
|
||||||
);
|
);
|
||||||
|
const distance = distanceToElement(element, p);
|
||||||
|
|
||||||
|
return shouldTestInside
|
||||||
|
? intersections.length === 0 || distance <= threshold
|
||||||
|
: intersections.length > 0 && distance <= threshold;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maxBindingGap = (
|
export const maxBindingGap = (
|
||||||
|
@ -16,24 +16,18 @@ import {
|
|||||||
} from "@excalidraw/math/ellipse";
|
} from "@excalidraw/math/ellipse";
|
||||||
|
|
||||||
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision";
|
||||||
import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import type {
|
import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math";
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
|
||||||
Polygon,
|
|
||||||
Radians,
|
|
||||||
} from "@excalidraw/math";
|
|
||||||
|
|
||||||
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
import type { FrameNameBounds } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { getBoundTextShape, isPathALoop } from "./shapes";
|
import { isPathALoop } from "./shapes";
|
||||||
import { getElementBounds } from "./bounds";
|
import { getElementBounds } from "./bounds";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
@ -41,12 +35,15 @@ import {
|
|||||||
deconstructRectanguloidElement,
|
deconstructRectanguloidElement,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
|
import { getBoundTextElement } from "./textElement";
|
||||||
|
|
||||||
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawRectangleElement,
|
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@ -72,45 +69,40 @@ export const shouldTestInside = (element: ExcalidrawElement) => {
|
|||||||
return isDraggableFromInside || isImageElement(element);
|
return isDraggableFromInside || isImageElement(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
|
export type HitTestArgs = {
|
||||||
x: number;
|
point: GlobalPoint;
|
||||||
y: number;
|
|
||||||
element: ExcalidrawElement;
|
element: ExcalidrawElement;
|
||||||
shape: GeometricShape<Point>;
|
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
frameNameBound?: FrameNameBounds | null;
|
frameNameBound?: FrameNameBounds | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
export const hitElementItself = ({
|
||||||
x,
|
point,
|
||||||
y,
|
|
||||||
element,
|
element,
|
||||||
shape,
|
|
||||||
threshold = 10,
|
threshold = 10,
|
||||||
frameNameBound = null,
|
frameNameBound = null,
|
||||||
}: HitTestArgs<Point>) => {
|
}: HitTestArgs) => {
|
||||||
let hit = shouldTestInside(element)
|
let hit = shouldTestInside(element)
|
||||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||||
// we would need `onShape` as well to include the "borders"
|
// we would need `onShape` as well to include the "borders"
|
||||||
isPointInShape(pointFrom(x, y), shape) ||
|
isPointInShape(point, element) ||
|
||||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
isPointOnShape(point, element, threshold)
|
||||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
: isPointOnShape(point, element, threshold);
|
||||||
|
|
||||||
// hit test against a frame's name
|
// hit test against a frame's name
|
||||||
if (!hit && frameNameBound) {
|
if (!hit && frameNameBound) {
|
||||||
hit = isPointInShape(pointFrom(x, y), {
|
const x1 = frameNameBound.x - threshold;
|
||||||
type: "polygon",
|
const y1 = frameNameBound.y - threshold;
|
||||||
data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement)
|
const x2 = frameNameBound.x + frameNameBound.width + threshold;
|
||||||
.data as Polygon<Point>,
|
const y2 = frameNameBound.y + frameNameBound.height + threshold;
|
||||||
});
|
hit = isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2));
|
||||||
}
|
}
|
||||||
|
|
||||||
return hit;
|
return hit;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBox = (
|
export const hitElementBoundingBox = (
|
||||||
x: number,
|
point: GlobalPoint,
|
||||||
y: number,
|
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
tolerance = 0,
|
tolerance = 0,
|
||||||
@ -120,37 +112,45 @@ export const hitElementBoundingBox = (
|
|||||||
y1 -= tolerance;
|
y1 -= tolerance;
|
||||||
x2 += tolerance;
|
x2 += tolerance;
|
||||||
y2 += tolerance;
|
y2 += tolerance;
|
||||||
return isPointWithinBounds(
|
return isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2));
|
||||||
pointFrom(x1, y1),
|
|
||||||
pointFrom(x, y),
|
|
||||||
pointFrom(x2, y2),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundingBoxOnly = <
|
export const hitElementBoundingBoxOnly = (
|
||||||
Point extends GlobalPoint | LocalPoint,
|
hitArgs: HitTestArgs,
|
||||||
>(
|
|
||||||
hitArgs: HitTestArgs<Point>,
|
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
!hitElementItself(hitArgs) &&
|
!hitElementItself(hitArgs) &&
|
||||||
// bound text is considered part of the element (even if it's outside the bounding box)
|
// bound text is considered part of the element (even if it's outside the bounding box)
|
||||||
!hitElementBoundText(
|
!hitElementBoundText(hitArgs.point, hitArgs.element, elementsMap) &&
|
||||||
hitArgs.x,
|
hitElementBoundingBox(hitArgs.point, hitArgs.element, elementsMap)
|
||||||
hitArgs.y,
|
|
||||||
getBoundTextShape(hitArgs.element, elementsMap),
|
|
||||||
) &&
|
|
||||||
hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hitElementBoundText = <Point extends GlobalPoint | LocalPoint>(
|
export const hitElementBoundText = (
|
||||||
x: number,
|
point: GlobalPoint,
|
||||||
y: number,
|
element: ExcalidrawElement,
|
||||||
textShape: GeometricShape<Point> | null,
|
elementsMap: ElementsMap,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return !!textShape && isPointInShape(pointFrom(x, y), textShape);
|
const boundTextElementCandidate = getBoundTextElement(element, elementsMap);
|
||||||
|
|
||||||
|
if (!boundTextElementCandidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const boundTextElement = isLinearElement(element)
|
||||||
|
? {
|
||||||
|
...boundTextElementCandidate,
|
||||||
|
// arrow's bound text accurate position is not stored in the element's property
|
||||||
|
// but rather calculated and returned from the following static method
|
||||||
|
...LinearElementEditor.getBoundTextElementPosition(
|
||||||
|
element,
|
||||||
|
boundTextElementCandidate,
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: boundTextElementCandidate;
|
||||||
|
|
||||||
|
return isPointInShape(point, boundTextElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
curvePointDistance,
|
curvePointDistance,
|
||||||
distanceToLineSegment,
|
distanceToLineSegment,
|
||||||
|
lineSegment,
|
||||||
|
pointFrom,
|
||||||
pointRotateRads,
|
pointRotateRads,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
@ -16,17 +18,18 @@ import {
|
|||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawBindableElement,
|
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawRectanguloidElement,
|
ExcalidrawRectanguloidElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const distanceToBindableElement = (
|
export const distanceToElement = (
|
||||||
element: ExcalidrawBindableElement,
|
element: ExcalidrawElement,
|
||||||
p: GlobalPoint,
|
p: GlobalPoint,
|
||||||
): number => {
|
): number => {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
|
case "selection":
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
@ -39,6 +42,23 @@ export const distanceToBindableElement = (
|
|||||||
return distanceToDiamondElement(element, p);
|
return distanceToDiamondElement(element, p);
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
return distanceToEllipseElement(element, p);
|
return distanceToEllipseElement(element, p);
|
||||||
|
case "line":
|
||||||
|
case "arrow":
|
||||||
|
case "freedraw":
|
||||||
|
return element.points.reduce((acc, point, idx) => {
|
||||||
|
if (idx === 0) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
const prevPoint = element.points[idx - 1];
|
||||||
|
const segment = lineSegment(
|
||||||
|
pointFrom<GlobalPoint>(
|
||||||
|
element.x + prevPoint[0],
|
||||||
|
element.y + prevPoint[1],
|
||||||
|
),
|
||||||
|
pointFrom<GlobalPoint>(element.x + point[0], element.y + point[1]),
|
||||||
|
);
|
||||||
|
return Math.min(acc, distanceToLineSegment(p, segment));
|
||||||
|
}, Infinity);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ import {
|
|||||||
snapToMid,
|
snapToMid,
|
||||||
getHoveredElementForBinding,
|
getHoveredElementForBinding,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { distanceToBindableElement } from "./distance";
|
import { distanceToElement } from "./distance";
|
||||||
import {
|
import {
|
||||||
compareHeading,
|
compareHeading,
|
||||||
flipHeading,
|
flipHeading,
|
||||||
@ -2234,8 +2234,7 @@ const getGlobalPoint = (
|
|||||||
|
|
||||||
// NOTE: Resize scales the binding position point too, so we need to update it
|
// NOTE: Resize scales the binding position point too, so we need to update it
|
||||||
return Math.abs(
|
return Math.abs(
|
||||||
distanceToBindableElement(element, fixedGlobalPoint) -
|
distanceToElement(element, fixedGlobalPoint) - FIXED_BINDING_DISTANCE,
|
||||||
FIXED_BINDING_DISTANCE,
|
|
||||||
) > 0.01
|
) > 0.01
|
||||||
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
? bindPointToSnapToElementOutline(arrow, element, startOrEnd)
|
||||||
: fixedGlobalPoint;
|
: fixedGlobalPoint;
|
||||||
@ -2257,7 +2256,7 @@ const getBindPointHeading = (
|
|||||||
hoveredElement &&
|
hoveredElement &&
|
||||||
aabbForElement(
|
aabbForElement(
|
||||||
hoveredElement,
|
hoveredElement,
|
||||||
Array(4).fill(distanceToBindableElement(hoveredElement, p)) as [
|
Array(4).fill(distanceToElement(hoveredElement, p)) as [
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
|
@ -195,7 +195,8 @@ export type ExcalidrawRectanguloidElement =
|
|||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawIframeLikeElement
|
| ExcalidrawIframeLikeElement
|
||||||
| ExcalidrawFrameLikeElement
|
| ExcalidrawFrameLikeElement
|
||||||
| ExcalidrawEmbeddableElement;
|
| ExcalidrawEmbeddableElement
|
||||||
|
| ExcalidrawSelectionElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
* ExcalidrawElement should be JSON serializable and (eventually) contain
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
vectorNormalize,
|
vectorNormalize,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||||
import { getSelectionBoxShape } from "@excalidraw/utils/shape";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
@ -170,12 +169,7 @@ import {
|
|||||||
isInvisiblySmallElement,
|
isInvisiblySmallElement,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import { getCornerRadius, isPathALoop } from "@excalidraw/element";
|
||||||
getBoundTextShape,
|
|
||||||
getCornerRadius,
|
|
||||||
getElementShape,
|
|
||||||
isPathALoop,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createSrcDoc,
|
createSrcDoc,
|
||||||
@ -5141,13 +5135,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// If we're hitting element with highest z-index only on its bounding box
|
// If we're hitting element with highest z-index only on its bounding box
|
||||||
// while also hitting other element figure, the latter should be considered.
|
// while also hitting other element figure, the latter should be considered.
|
||||||
return hitElementItself({
|
return hitElementItself({
|
||||||
x,
|
point: pointFrom(x, y),
|
||||||
y,
|
|
||||||
element: elementWithHighestZIndex,
|
element: elementWithHighestZIndex,
|
||||||
shape: getElementShape(
|
|
||||||
elementWithHighestZIndex,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
// when overlapping, we would like to be more precise
|
// when overlapping, we would like to be more precise
|
||||||
// this also avoids the need to update past tests
|
// this also avoids the need to update past tests
|
||||||
threshold: this.getElementHitThreshold() / 2,
|
threshold: this.getElementHitThreshold() / 2,
|
||||||
@ -5229,34 +5218,26 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.selectedElementIds[element.id] &&
|
this.state.selectedElementIds[element.id] &&
|
||||||
shouldShowBoundingBox([element], this.state)
|
shouldShowBoundingBox([element], this.state)
|
||||||
) {
|
) {
|
||||||
const selectionShape = getSelectionBoxShape(
|
|
||||||
element,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
isImageElement(element) ? 0 : this.getElementHitThreshold(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// if hitting the bounding box, return early
|
// if hitting the bounding box, return early
|
||||||
// but if not, we should check for other cases as well (e.g. frame name)
|
// but if not, we should check for other cases as well (e.g. frame name)
|
||||||
if (isPointInShape(pointFrom(x, y), selectionShape)) {
|
if (isPointInShape(pointFrom(x, y), element)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// take bound text element into consideration for hit collision as well
|
// take bound text element into consideration for hit collision as well
|
||||||
const hitBoundTextOfElement = hitElementBoundText(
|
const hitBoundTextOfElement = hitElementBoundText(
|
||||||
x,
|
pointFrom(x, y),
|
||||||
y,
|
element,
|
||||||
getBoundTextShape(element, this.scene.getNonDeletedElementsMap()),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
if (hitBoundTextOfElement) {
|
if (hitBoundTextOfElement) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hitElementItself({
|
return hitElementItself({
|
||||||
x,
|
point: pointFrom(x, y),
|
||||||
y,
|
|
||||||
element,
|
element,
|
||||||
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()),
|
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
frameNameBound: isFrameLikeElement(element)
|
frameNameBound: isFrameLikeElement(element)
|
||||||
? this.frameNameBoundsCache.get(element)
|
? this.frameNameBoundsCache.get(element)
|
||||||
@ -5285,13 +5266,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (
|
if (
|
||||||
isArrowElement(elements[index]) &&
|
isArrowElement(elements[index]) &&
|
||||||
hitElementItself({
|
hitElementItself({
|
||||||
x,
|
point: pointFrom(x, y),
|
||||||
y,
|
|
||||||
element: elements[index],
|
element: elements[index],
|
||||||
shape: getElementShape(
|
|
||||||
elements[index],
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@ -5637,13 +5613,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
hasBoundTextElement(container) ||
|
hasBoundTextElement(container) ||
|
||||||
!isTransparent(container.backgroundColor) ||
|
!isTransparent(container.backgroundColor) ||
|
||||||
hitElementItself({
|
hitElementItself({
|
||||||
x: sceneX,
|
point: pointFrom(sceneX, sceneY),
|
||||||
y: sceneY,
|
|
||||||
element: container,
|
element: container,
|
||||||
shape: getElementShape(
|
|
||||||
container,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@ -6316,13 +6287,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let segmentMidPointHoveredCoords = null;
|
let segmentMidPointHoveredCoords = null;
|
||||||
if (
|
if (
|
||||||
hitElementItself({
|
hitElementItself({
|
||||||
x: scenePointerX,
|
point: pointFrom(scenePointerX, scenePointerY),
|
||||||
y: scenePointerY,
|
|
||||||
element,
|
element,
|
||||||
shape: getElementShape(
|
|
||||||
element,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||||
@ -9696,13 +9662,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
((hitElement &&
|
((hitElement &&
|
||||||
hitElementBoundingBoxOnly(
|
hitElementBoundingBoxOnly(
|
||||||
{
|
{
|
||||||
x: pointerDownState.origin.x,
|
point: pointFrom(
|
||||||
y: pointerDownState.origin.y,
|
pointerDownState.origin.x,
|
||||||
element: hitElement,
|
pointerDownState.origin.y,
|
||||||
shape: getElementShape(
|
|
||||||
hitElement,
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
),
|
),
|
||||||
|
element: hitElement,
|
||||||
threshold: this.getElementHitThreshold(),
|
threshold: this.getElementHitThreshold(),
|
||||||
frameNameBound: isFrameLikeElement(hitElement)
|
frameNameBound: isFrameLikeElement(hitElement)
|
||||||
? this.frameNameBoundsCache.get(hitElement)
|
? this.frameNameBoundsCache.get(hitElement)
|
||||||
|
@ -463,7 +463,7 @@ const shouldHideLinkPopup = (
|
|||||||
|
|
||||||
const threshold = 15 / appState.zoom.value;
|
const threshold = 15 / appState.zoom.value;
|
||||||
// hitbox to prevent hiding when hovered in element bounding box
|
// hitbox to prevent hiding when hovered in element bounding box
|
||||||
if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) {
|
if (hitElementBoundingBox(pointFrom(sceneX, sceneY), element, elementsMap)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
|
@ -92,7 +92,7 @@ export const isPointHittingLink = (
|
|||||||
if (
|
if (
|
||||||
!isMobile &&
|
!isMobile &&
|
||||||
appState.viewModeEnabled &&
|
appState.viewModeEnabled &&
|
||||||
hitElementBoundingBox(x, y, element, elementsMap)
|
hitElementBoundingBox(pointFrom(x, y), element, elementsMap)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
|
|
||||||
import { getElementsInGroup } from "@excalidraw/element";
|
import { getElementsInGroup } from "@excalidraw/element";
|
||||||
|
|
||||||
import { getElementShape } from "@excalidraw/element";
|
|
||||||
import { shouldTestInside } from "@excalidraw/element";
|
import { shouldTestInside } from "@excalidraw/element";
|
||||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||||
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
||||||
@ -208,15 +207,8 @@ const eraserTest = (
|
|||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
app: App,
|
app: App,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
let shape = shapesCache.get(element.id);
|
|
||||||
|
|
||||||
if (!shape) {
|
|
||||||
shape = getElementShape<GlobalPoint>(element, elementsMap);
|
|
||||||
shapesCache.set(element.id, shape);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
||||||
if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
|
if (shouldTestInside(element) && isPointInShape(lastPoint, element)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,16 +187,10 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
zoom: InteractiveCanvasAppState["zoom"],
|
zoom: InteractiveCanvasAppState["zoom"],
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
|
||||||
const width = x2 - x1;
|
|
||||||
const height = y2 - y1;
|
|
||||||
|
|
||||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
|
||||||
context.fillStyle = "rgba(0,0,0,.05)";
|
|
||||||
|
|
||||||
// To ensure the binding highlight doesn't overlap the element itself
|
|
||||||
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||||
|
|
||||||
|
context.fillStyle = "rgba(0,0,0,.05)";
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "text":
|
case "text":
|
||||||
@ -211,9 +205,12 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
drawHighlightForDiamondWithRotation(context, padding, element);
|
drawHighlightForDiamondWithRotation(context, padding, element);
|
||||||
break;
|
break;
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
context.lineWidth =
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
maxBindingGap(element, element.width, element.height, zoom) -
|
const width = x2 - x1;
|
||||||
FIXED_BINDING_DISTANCE;
|
const height = y2 - y1;
|
||||||
|
|
||||||
|
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||||
|
context.lineWidth = padding - FIXED_BINDING_DISTANCE;
|
||||||
|
|
||||||
strokeEllipseWithRotation(
|
strokeEllipseWithRotation(
|
||||||
context,
|
context,
|
||||||
|
@ -3,69 +3,45 @@ import {
|
|||||||
pointFrom,
|
pointFrom,
|
||||||
polygonIncludesPoint,
|
polygonIncludesPoint,
|
||||||
pointOnLineSegment,
|
pointOnLineSegment,
|
||||||
pointOnPolygon,
|
|
||||||
polygonFromPoints,
|
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
type Polygon,
|
type Polygon,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import { intersectElementWithLineSegment } from "@excalidraw/element/collision";
|
||||||
|
|
||||||
|
import { elementCenterPoint } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { distanceToElement } from "@excalidraw/element/distance";
|
||||||
|
|
||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Curve } from "@excalidraw/math";
|
import type { Curve } from "@excalidraw/math";
|
||||||
|
|
||||||
import { pointInEllipse, pointOnEllipse } from "./shape";
|
import type { Polyline } from "./shape";
|
||||||
|
|
||||||
import type { Polycurve, Polyline, GeometricShape } from "./shape";
|
|
||||||
|
|
||||||
// check if the given point is considered on the given shape's border
|
// check if the given point is considered on the given shape's border
|
||||||
export const isPointOnShape = <Point extends GlobalPoint | LocalPoint>(
|
export const isPointOnShape = (
|
||||||
point: Point,
|
point: GlobalPoint,
|
||||||
shape: GeometricShape<Point>,
|
element: ExcalidrawElement,
|
||||||
tolerance = 0,
|
tolerance = 1,
|
||||||
) => {
|
) => {
|
||||||
// get the distance from the given point to the given element
|
const distance = distanceToElement(element, point);
|
||||||
// check if the distance is within the given epsilon range
|
|
||||||
switch (shape.type) {
|
return distance <= tolerance;
|
||||||
case "polygon":
|
|
||||||
return pointOnPolygon(point, shape.data, tolerance);
|
|
||||||
case "ellipse":
|
|
||||||
return pointOnEllipse(point, shape.data, tolerance);
|
|
||||||
case "line":
|
|
||||||
return pointOnLineSegment(point, shape.data, tolerance);
|
|
||||||
case "polyline":
|
|
||||||
return pointOnPolyline(point, shape.data, tolerance);
|
|
||||||
case "curve":
|
|
||||||
return pointOnCurve(point, shape.data, tolerance);
|
|
||||||
case "polycurve":
|
|
||||||
return pointOnPolycurve(point, shape.data, tolerance);
|
|
||||||
default:
|
|
||||||
throw Error(`shape ${shape} is not implemented`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// check if the given point is considered inside the element's border
|
// check if the given point is considered inside the element's border
|
||||||
export const isPointInShape = <Point extends GlobalPoint | LocalPoint>(
|
export const isPointInShape = (
|
||||||
point: Point,
|
point: GlobalPoint,
|
||||||
shape: GeometricShape<Point>,
|
element: ExcalidrawElement,
|
||||||
) => {
|
) => {
|
||||||
switch (shape.type) {
|
const intersections = intersectElementWithLineSegment(
|
||||||
case "polygon":
|
element,
|
||||||
return polygonIncludesPoint(point, shape.data);
|
lineSegment(elementCenterPoint(element), point),
|
||||||
case "line":
|
);
|
||||||
return false;
|
|
||||||
case "curve":
|
return intersections.length === 0;
|
||||||
return false;
|
|
||||||
case "ellipse":
|
|
||||||
return pointInEllipse(point, shape.data);
|
|
||||||
case "polyline": {
|
|
||||||
const polygon = polygonFromPoints(shape.data.flat());
|
|
||||||
return polygonIncludesPoint(point, polygon);
|
|
||||||
}
|
|
||||||
case "polycurve": {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw Error(`shape ${shape} is not implemented`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// check if the given element is in the given bounds
|
// check if the given element is in the given bounds
|
||||||
@ -76,14 +52,6 @@ export const isPointInBounds = <Point extends GlobalPoint | LocalPoint>(
|
|||||||
return polygonIncludesPoint(point, bounds);
|
return polygonIncludesPoint(point, bounds);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pointOnPolycurve = <Point extends LocalPoint | GlobalPoint>(
|
|
||||||
point: Point,
|
|
||||||
polycurve: Polycurve<Point>,
|
|
||||||
tolerance: number,
|
|
||||||
) => {
|
|
||||||
return polycurve.some((curve) => pointOnCurve(point, curve, tolerance));
|
|
||||||
};
|
|
||||||
|
|
||||||
const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
|
const cubicBezierEquation = <Point extends LocalPoint | GlobalPoint>(
|
||||||
curve: Curve<Point>,
|
curve: Curve<Point>,
|
||||||
) => {
|
) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user