From bbcca06b94550fcb03c0563be5f08cea7f4211fe Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Wed, 17 Apr 2024 19:31:12 +0800 Subject: [PATCH] fix: collision regressions from vector geometry rewrite (#7902) --- packages/excalidraw/components/App.tsx | 24 ++++++++++++------- packages/utils/geometry/shape.ts | 33 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 6a0fd1031..8ceb362a5 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -230,6 +230,7 @@ import { getEllipseShape, getFreedrawShape, getPolygonShape, + getSelectionBoxShape, } from "../../utils/geometry/shape"; import { isPointInShape } from "../../utils/collision"; import { @@ -416,7 +417,6 @@ import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils"; import { getRenderOpacity } from "../renderer/renderElement"; import { hitElementBoundText, - hitElementBoundingBox, hitElementBoundingBoxOnly, hitElementItself, shouldTestInside, @@ -4462,10 +4462,18 @@ class App extends React.Component { // 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. - return isPointInShape( - [x, y], - this.getElementShape(elementWithHighestZIndex), - ) + return hitElementItself({ + x, + y, + element: elementWithHighestZIndex, + shape: this.getElementShape(elementWithHighestZIndex), + // when overlapping, we would like to be more precise + // this also avoids the need to update past tests + threshold: this.getHitThreshold() / 2, + frameNameBound: isFrameLikeElement(elementWithHighestZIndex) + ? this.frameNameBoundsCache.get(elementWithHighestZIndex) + : null, + }) ? elementWithHighestZIndex : allHitElements[allHitElements.length - 2]; } @@ -4540,13 +4548,13 @@ class App extends React.Component { this.state.selectedElementIds[element.id] && shouldShowBoundingBox([element], this.state) ) { - return hitElementBoundingBox( - x, - y, + const selectionShape = getSelectionBoxShape( element, this.scene.getNonDeletedElementsMap(), this.getHitThreshold(), ); + + return isPointInShape([x, y], selectionShape); } // take bound text element into consideration for hit collision as well diff --git a/packages/utils/geometry/shape.ts b/packages/utils/geometry/shape.ts index 87c0fe099..53ab9ff8e 100644 --- a/packages/utils/geometry/shape.ts +++ b/packages/utils/geometry/shape.ts @@ -12,8 +12,11 @@ * to pure shapes */ +import { getElementAbsoluteCoords } from "../../excalidraw/element"; import { + ElementsMap, ExcalidrawDiamondElement, + ExcalidrawElement, ExcalidrawEllipseElement, ExcalidrawEmbeddableElement, ExcalidrawFrameLikeElement, @@ -133,6 +136,36 @@ export const getPolygonShape = ( }; }; +// return the selection box for an element, possibly rotated as well +export const getSelectionBoxShape = ( + element: ExcalidrawElement, + elementsMap: ElementsMap, + padding = 10, +) => { + let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( + element, + elementsMap, + true, + ); + + x1 -= padding; + x2 += padding; + y1 -= padding; + y2 += padding; + + const angleInDegrees = angleToDegrees(element.angle); + const center: Point = [cx, cy]; + const topLeft = pointRotate([x1, y1], angleInDegrees, center); + const topRight = pointRotate([x2, y1], angleInDegrees, center); + const bottomLeft = pointRotate([x1, y2], angleInDegrees, center); + const bottomRight = pointRotate([x2, y2], angleInDegrees, center); + + return { + type: "polygon", + data: [topLeft, topRight, bottomRight, bottomLeft], + } as GeometricShape; +}; + // ellipse export const getEllipseShape = ( element: ExcalidrawEllipseElement,