diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index a98c90daf..758a59502 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -35,10 +35,8 @@ import { import { bindingBorderTest, getHoveredElementForBinding, - getHoveredElementForBindingAndIfItsPrecise, hitElementItself, intersectElementWithLineSegment, - maxBindingDistanceFromOutline, } from "./collision"; import { distanceToElement } from "./distance"; import { @@ -114,7 +112,6 @@ export type BindingStrategy = }; export const FIXED_BINDING_DISTANCE = 5; -export const BINDING_HIGHLIGHT_THICKNESS = 10; export const shouldEnableBindingForPointerEvent = ( event: React.PointerEvent, @@ -202,7 +199,6 @@ const bindOrUnbindBindingElementEdge = ( const getOriginalBindingsIfStillCloseToBindingEnds = ( linearElement: NonDeleted, elementsMap: NonDeletedSceneElementsMap, - zoom?: AppState["zoom"], ): (NonDeleted | null)[] => (["start", "end"] as const).map((edge) => { const coors = tupleToCoors( @@ -224,7 +220,6 @@ const getOriginalBindingsIfStillCloseToBindingEnds = ( element, pointFrom(coors.x, coors.y), elementsMap, - zoom, ) ) { return element; @@ -328,30 +323,16 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( draggingPoints.get(startDragged ? startIdx : endIdx)!.point, elementsMap, ); - const { hovered, hit } = getHoveredElementForBindingAndIfItsPrecise( - point, - elements, - elementsMap, - appState.zoom, - true, - ); + const hit = getHoveredElementForBinding(point, elements, elementsMap); // With new arrows this handles the binding at arrow creation if (startDragged) { - if (hovered) { - if (hit) { - start = { - element: hovered, - mode: "inside", - focusPoint: point, - }; - } else { - start = { - element: hovered, - mode: "orbit", - focusPoint: point, - }; - } + if (hit) { + start = { + element: hit, + mode: "inside", + focusPoint: point, + }; } else { start = { mode: null }; } @@ -365,41 +346,24 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( appState?.selectedLinearElement?.pointerDownState.arrowOriginalStartPoint; // Inside -> inside binding - if (hovered && hit && arrow.startBinding?.elementId === hovered.id) { + if (hit && arrow.startBinding?.elementId === hit.id) { const center = pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, + hit.x + hit.width / 2, + hit.y + hit.height / 2, ); return { start: { mode: "inside", - element: hovered, + element: hit, focusPoint: arrowOriginalStartPoint ?? center, }, - end: { mode: "inside", element: hovered, focusPoint: point }, - }; - } - - // Inside -> orbit binding - if (hovered && !hit && arrow.startBinding?.elementId === hovered.id) { - const center = pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, - ); - - return { - start: { - mode: globalBindMode === "inside" ? "inside" : "orbit", - element: hovered, - focusPoint: arrowOriginalStartPoint ?? center, - }, - end: { mode: null }, + end: { mode: "inside", element: hit, focusPoint: point }, }; } // Inside -> outside binding - if (arrow.startBinding && arrow.startBinding.elementId !== hovered?.id) { + if (arrow.startBinding && arrow.startBinding.elementId !== hit?.id) { const otherElement = elementsMap.get( arrow.startBinding.elementId, ) as ExcalidrawBindableElement; @@ -423,14 +387,14 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( // We are hovering another element with the end point let current: BindingStrategy; - if (hovered) { + if (hit) { const isInsideBinding = globalBindMode === "inside"; current = { mode: isInsideBinding ? "inside" : "orbit", - element: hovered, + element: hit, focusPoint: isInsideBinding ? point - : snapToCenter(hovered, elementsMap, point), + : snapToCenter(hit, elementsMap, point), }; } else { current = { mode: null }; @@ -444,13 +408,13 @@ const bindingStrategyForNewSimpleArrowEndpointDragging = ( // No start binding if (!arrow.startBinding) { - if (hovered) { + if (hit) { const isInsideBinding = - globalBindMode === "inside" || isAlwaysInsideBinding(hovered); + globalBindMode === "inside" || isAlwaysInsideBinding(hit); end = { mode: isInsideBinding ? "inside" : "orbit", - element: hovered, + element: hit, focusPoint: point, }; } else { @@ -471,7 +435,6 @@ const bindingStrategyForSimpleArrowEndpointDragging = ( oppositeBinding: FixedPointBinding | null, elementsMap: NonDeletedSceneElementsMap, elements: readonly Ordered[], - zoom: AppState["zoom"], globalBindMode?: AppState["bindMode"], opts?: { newArrow?: boolean; @@ -481,23 +444,14 @@ const bindingStrategyForSimpleArrowEndpointDragging = ( let current: BindingStrategy = { mode: undefined }; let other: BindingStrategy = { mode: undefined }; - const { hovered, hit } = getHoveredElementForBindingAndIfItsPrecise( - point, - elements, - elementsMap, - zoom, - true, - ); + const hit = getHoveredElementForBinding(point, elements, elementsMap); // If the global bind mode is in free binding mode, just bind // where the pointer is and keep the other end intact - if ( - globalBindMode === "inside" || - (hovered && isAlwaysInsideBinding(hovered)) - ) { - current = hovered + if (globalBindMode === "inside" || (hit && isAlwaysInsideBinding(hit))) { + current = hit ? { - element: hovered, + element: hit, focusPoint: point, mode: "inside", } @@ -508,90 +462,58 @@ const bindingStrategyForSimpleArrowEndpointDragging = ( // Dragged point is outside of any bindable element // so we break any existing binding - if (!hovered) { + if (!hit) { return { current: { mode: null }, other }; } - // Dragged point is on the binding gap of a bindable element - if (!hit) { - // If the opposite binding (if exists) is on the same element - if (oppositeBinding) { - if (oppositeBinding.elementId === hovered.id) { - return { current: { mode: null }, other }; - } - // The opposite binding is on a different element - // eslint-disable-next-line no-else-return - else { - current = { - element: hovered, - mode: "orbit", - focusPoint: opts?.newArrow - ? pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, - ) - : point, - }; - - return { current, other }; - } - } - - // No opposite binding or the opposite binding is on a different element - current = { element: hovered, mode: "orbit", focusPoint: point }; - } // The dragged point is inside the hovered bindable element - else { - // The opposite binding is on the same element - // eslint-disable-next-line no-lonely-if - if (oppositeBinding) { - if (oppositeBinding.elementId === hovered.id) { - // The opposite binding is on the binding gap of the same element - if (oppositeBinding.mode !== "inside") { - current = { element: hovered, mode: "orbit", focusPoint: point }; - other = { mode: null }; - return { current, other }; - } - // The opposite binding is inside the same element - // eslint-disable-next-line no-else-return - else { - current = { element: hovered, mode: "inside", focusPoint: point }; + // The opposite binding is on the same element + // eslint-disable-next-line no-lonely-if + if (oppositeBinding) { + if (oppositeBinding.elementId === hit.id) { + // The opposite binding is on the binding gap of the same element + if (oppositeBinding.mode !== "inside") { + current = { element: hit, mode: "orbit", focusPoint: point }; + other = { mode: null }; - return { current, other }; - } + return { current, other }; } - // The opposite binding is on a different element + // The opposite binding is inside the same element // eslint-disable-next-line no-else-return else { - current = { - element: hovered, - mode: "orbit", - focusPoint: opts?.newArrow - ? pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, - ) - : point, - }; + current = { element: hit, mode: "inside", focusPoint: point }; return { current, other }; } } - // The opposite binding is on a different element or no binding + // The opposite binding is on a different element + // eslint-disable-next-line no-else-return else { current = { - element: hovered, + element: hit, mode: "orbit", focusPoint: opts?.newArrow ? pointFrom( - hovered.x + hovered.width / 2, - hovered.y + hovered.height / 2, + hit.x + hit.width / 2, + hit.y + hit.height / 2, ) : point, }; + + return { current, other }; } } + // The opposite binding is on a different element or no binding + else { + current = { + element: hit, + mode: "orbit", + focusPoint: opts?.newArrow + ? pointFrom(hit.x + hit.width / 2, hit.y + hit.height / 2) + : point, + }; + } // Must return as only one endpoint is dragged, therefore // the end binding strategy might accidentally gets overriden @@ -654,7 +576,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = ( p, elements, elementsMap, - appState.zoom, ); const current: BindingStrategy = hoveredElement ? { @@ -702,7 +623,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = ( arrow.endBinding, elementsMap, elements, - appState.zoom, globalBindMode, opts, ); @@ -724,7 +644,6 @@ export const getBindingStrategyForDraggingBindingElementEndpoints = ( arrow.startBinding, elementsMap, elements, - appState.zoom, globalBindMode, opts, ); @@ -753,7 +672,6 @@ export const bindOrUnbindBindingElements = ( export const getSuggestedBindingsForBindingElements = ( selectedElements: NonDeleted[], elementsMap: NonDeletedSceneElementsMap, - zoom: AppState["zoom"], ): SuggestedBinding[] => { // HOT PATH: Bail out if selected elements list is too large if (selectedElements.length > 50) { @@ -764,11 +682,7 @@ export const getSuggestedBindingsForBindingElements = ( selectedElements .filter(isArrowElement) .flatMap((element) => - getOriginalBindingsIfStillCloseToBindingEnds( - element, - elementsMap, - zoom, - ), + getOriginalBindingsIfStillCloseToBindingEnds(element, elementsMap), ) .filter( (element): element is NonDeleted => @@ -790,7 +704,6 @@ export const maybeSuggestBindingsForBindingElementAtCoords = ( linearElement: NonDeleted, startOrEndOrBoth: "start" | "end" | "both", scene: Scene, - zoom: AppState["zoom"], ): ExcalidrawBindableElement[] => { const startCoords = LinearElementEditor.getPointAtIndexGlobalCoordinates( linearElement, @@ -806,13 +719,11 @@ export const maybeSuggestBindingsForBindingElementAtCoords = ( startCoords, scene.getNonDeletedElements(), scene.getNonDeletedElementsMap(), - zoom, ); const endHovered = getHoveredElementForBinding( endCoords, scene.getNonDeletedElements(), scene.getNonDeletedElementsMap(), - zoom, ); const suggestedBindings = []; @@ -1080,7 +991,6 @@ export const getHeadingForElbowArrowSnap = ( aabb: Bounds | undefined | null, origPoint: GlobalPoint, elementsMap: ElementsMap, - zoom?: AppState["zoom"], ): Heading => { const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p)); @@ -1089,14 +999,8 @@ export const getHeadingForElbowArrowSnap = ( } const d = distanceToElement(bindableElement, elementsMap, origPoint); - const bindDistance = maxBindingDistanceFromOutline( - bindableElement, - bindableElement.width, - bindableElement.height, - zoom, - ); - const distance = d > bindDistance ? null : d; + const distance = d > 0 ? null : d; if (!distance) { return vectorToHeading( diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index a7cda59d4..306b64bd3 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -25,7 +25,7 @@ import type { Radians, } from "@excalidraw/math"; -import type { AppState, FrameNameBounds } from "@excalidraw/excalidraw/types"; +import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; import { isPathALoop } from "./utils"; import { @@ -58,8 +58,6 @@ import { LinearElementEditor } from "./linearElementEditor"; import { distanceToElement } from "./distance"; -import { BINDING_HIGHLIGHT_THICKNESS, FIXED_BINDING_DISTANCE } from "./binding"; - import type { ElementsMap, ExcalidrawBindableElement, @@ -206,40 +204,12 @@ export const hitElementBoundText = ( return isPointInElement(point, boundTextElement, elementsMap); }; -export const maxBindingDistanceFromOutline = ( - element: ExcalidrawElement, - elementWidth: number, - elementHeight: number, - zoom?: AppState["zoom"], -): number => { - const zoomValue = zoom?.value && zoom.value < 1 ? zoom.value : 1; - - // Aligns diamonds with rectangles - const shapeRatio = element.type === "diamond" ? 1 / Math.sqrt(2) : 1; - const smallerDimension = shapeRatio * Math.min(elementWidth, elementHeight); - - return Math.max( - 16, - // bigger bindable boundary for bigger elements - Math.min(0.25 * smallerDimension, 32), - // keep in sync with the zoomed highlight - BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE, - ); -}; - export const bindingBorderTest = ( element: NonDeleted, [x, y]: Readonly, elementsMap: NonDeletedSceneElementsMap, - zoom?: AppState["zoom"], ): boolean => { const p = pointFrom(x, y); - const threshold = maxBindingDistanceFromOutline( - element, - element.width, - element.height, - zoom, - ); const shouldTestInside = // disable fullshape snapping for frame elements so we // can bind to frame children @@ -247,12 +217,7 @@ export const bindingBorderTest = ( // PERF: Run a cheap test to see if the binding element // is even close to the element - const bounds = [ - x - threshold, - y - threshold, - x + threshold, - y + threshold, - ] as Bounds; + const bounds = [x - 1, y - 1, x + 1, y + 1] as Bounds; const elementBounds = getElementBounds(element, elementsMap); if (!doBoundsIntersect(bounds, elementBounds)) { return false; @@ -267,15 +232,14 @@ export const bindingBorderTest = ( const distance = distanceToElement(element, elementsMap, p); return shouldTestInside - ? intersections.length === 0 || distance <= threshold - : intersections.length > 0 && distance <= threshold; + ? intersections.length === 0 + : intersections.length > 0 && distance <= 1; }; export const getHoveredElementForBinding = ( point: Readonly, elements: readonly Ordered[], elementsMap: NonDeletedSceneElementsMap, - zoom?: AppState["zoom"], ): NonDeleted | null => { const candidateElements: NonDeleted[] = []; // We need to to hit testing from front (end of the array) to back (beginning of the array) @@ -291,7 +255,7 @@ export const getHoveredElementForBinding = ( if ( isBindableElement(element, false) && - bindingBorderTest(element, point, elementsMap, zoom) + bindingBorderTest(element, point, elementsMap) ) { candidateElements.push(element); } @@ -313,36 +277,6 @@ export const getHoveredElementForBinding = ( .pop() as NonDeleted; }; -export const getHoveredElementForBindingAndIfItsPrecise = ( - point: GlobalPoint, - elements: readonly Ordered[], - elementsMap: NonDeletedSceneElementsMap, - zoom: AppState["zoom"], - shouldTestInside: boolean = true, -): { - hovered: NonDeleted | null; - hit: boolean; -} => { - const hoveredElement = getHoveredElementForBinding( - point, - elements, - elementsMap, - zoom, - ); - // TODO: Optimize this to avoid recalculating the point - element distance - const hit = - !!hoveredElement && - hitElementItself({ - element: hoveredElement, - elementsMap, - point, - threshold: 0, - overrideShouldTestInside: shouldTestInside, - }); - - return { hovered: hoveredElement, hit }; -}; - /** * Intersect a line with an element for binding test * diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 6cdeb7f63..520fdb577 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -1194,19 +1194,9 @@ const getElbowArrowData = ( if (options?.isDragging) { const elements = Array.from(elementsMap.values()); hoveredStartElement = - getHoveredElement( - origStartGlobalPoint, - elementsMap, - elements, - options?.zoom, - ) || null; + getHoveredElement(origStartGlobalPoint, elementsMap, elements) || null; hoveredEndElement = - getHoveredElement( - origEndGlobalPoint, - elementsMap, - elements, - options?.zoom, - ) || null; + getHoveredElement(origEndGlobalPoint, elementsMap, elements) || null; } else { hoveredStartElement = arrow.startBinding ? getBindableElementForId(arrow.startBinding.elementId, elementsMap) || @@ -2249,9 +2239,8 @@ const getHoveredElement = ( origPoint: GlobalPoint, elementsMap: NonDeletedSceneElementsMap, elements: readonly Ordered[], - zoom?: AppState["zoom"], ) => { - return getHoveredElementForBinding(origPoint, elements, elementsMap, zoom); + return getHoveredElementForBinding(origPoint, elements, elementsMap); }; const gridAddressesEqual = (a: GridAddress, b: GridAddress): boolean => diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 53a85178f..b1236d9eb 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -465,7 +465,6 @@ export class LinearElementEditor { ? "start" : "end", app.scene, - app.state.zoom, ); } } @@ -2001,7 +2000,6 @@ const pointDraggingUpdates = ( newGlobalPointPosition, elements, elementsMap, - app.state.zoom, ); const otherGlobalPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( @@ -2011,12 +2009,7 @@ const pointDraggingUpdates = ( ); const otherPointInsideElement = !!hoveredElement && - !!bindingBorderTest( - hoveredElement, - otherGlobalPoint, - elementsMap, - app.state.zoom, - ); + !!bindingBorderTest(hoveredElement, otherGlobalPoint, elementsMap); if ( isBindingEnabled(app.state) && diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 645bee030..9e9e8fc66 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -4513,7 +4513,6 @@ class App extends React.Component { (element) => element.id !== elbowArrow?.id || step !== 0, ), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ), }); @@ -4742,7 +4741,6 @@ class App extends React.Component { pointFrom(scenePointer.x, scenePointer.y), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); if (hoveredElement && !this.bindModeHandler) { @@ -6074,7 +6072,6 @@ class App extends React.Component { newElement, "end", this.scene, - this.state.zoom, ), }); } else { @@ -6179,7 +6176,6 @@ class App extends React.Component { pointFrom(scenePointerX, scenePointerY), this.scene.getNonDeletedElements(), elementsMap, - this.state.zoom, ); // Timed bind mode handler for arrow elements @@ -7967,7 +7963,6 @@ class App extends React.Component { ), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); this.setState({ @@ -8173,7 +8168,6 @@ class App extends React.Component { lastGlobalPoint, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); // clicking inside commit zone → finalize arrow @@ -8294,7 +8288,6 @@ class App extends React.Component { point, this.scene.getNonDeletedElements(), elementsMap, - this.state.zoom, ); this.scene.mutateElement(element, { @@ -8740,7 +8733,6 @@ class App extends React.Component { pointFrom(pointerCoords.x, pointerCoords.y), this.scene.getNonDeletedElements(), elementsMap, - this.state.zoom, ); // Timed bind mode handler for arrow elements @@ -9117,7 +9109,6 @@ class App extends React.Component { suggestedBindings: getSuggestedBindingsForBindingElements( selectedElements, this.scene.getNonDeletedElementsMap(), - this.state.zoom, ), }); } @@ -9427,7 +9418,6 @@ class App extends React.Component { newElement, "end", this.scene, - this.state.zoom, ), }); } @@ -10978,7 +10968,6 @@ class App extends React.Component { pointFrom(pointerCoords.x, pointerCoords.y), this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); this.setState({ suggestedBindings: @@ -11601,7 +11590,6 @@ class App extends React.Component { const suggestedBindings = getSuggestedBindingsForBindingElements( selectedElements, this.scene.getNonDeletedElementsMap(), - this.state.zoom, ); const elementsToHighlight = new Set(); diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index dc1411b4e..cdbe2a993 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -16,10 +16,7 @@ import { throttleRAF, } from "@excalidraw/common"; -import { - FIXED_BINDING_DISTANCE, - maxBindingDistanceFromOutline, -} from "@excalidraw/element"; +import { FIXED_BINDING_DISTANCE } from "@excalidraw/element"; import { LinearElementEditor } from "@excalidraw/element"; import { getOmitSidesForDevice, @@ -197,12 +194,7 @@ const renderBindingHighlightForBindableElement = ( elementsMap: ElementsMap, zoom: InteractiveCanvasAppState["zoom"], ) => { - const padding = maxBindingDistanceFromOutline( - element, - element.width, - element.height, - zoom, - ); + const padding = 5; context.fillStyle = "rgba(0,0,0,.05)"; @@ -251,14 +243,9 @@ const renderBindingHighlightForSuggestedPointBinding = ( elementsMap: ElementsMap, zoom: InteractiveCanvasAppState["zoom"], ) => { - const [element, startOrEnd, bindableElement] = suggestedBinding; + const [element, startOrEnd] = suggestedBinding; - const threshold = maxBindingDistanceFromOutline( - bindableElement, - bindableElement.width, - bindableElement.height, - zoom, - ); + const threshold = 0; context.strokeStyle = "rgba(0,0,0,0)"; context.fillStyle = "rgba(0,0,0,.05)";