diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index 9354dcd1ea..30287c219f 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -588,13 +588,13 @@ const getDiagonalsForBindableElement = ( return [diagonalOne, diagonalTwo]; }; -export const getSnapOutlineMidPoint = ( +export const getHighlightedMidpointIndex = ( point: GlobalPoint, element: ExcalidrawBindableElement, elementsMap: ElementsMap, zoom: AppState["zoom"], arrow: { elbowed: boolean }, -): GlobalPoint | undefined => { +): number => { const center = elementCenterPoint(element, elementsMap); const TOLERANCE = 0.05; const maxDistance = maxBindingDistance_simple(zoom) + element.strokeWidth / 2; @@ -610,9 +610,8 @@ export const getSnapOutlineMidPoint = ( const bindingGap = getBindingGap(element, arrow); - // Too close to the center makes it hard to resolve direction precisely if (pointDistance(center, nonRotated) < bindingGap) { - return undefined; + return -1; } if ( @@ -620,45 +619,25 @@ export const getSnapOutlineMidPoint = ( nonRotated[1] > center[1] - verticalThreshold && nonRotated[1] < center[1] + verticalThreshold ) { - // LEFT - return pointRotateRads( - pointFrom(x - bindingGap, center[1]), - center, - angle, - ); + return 2; } else if ( nonRotated[1] <= y + height / 2 && nonRotated[0] > center[0] - horizontalThreshold && nonRotated[0] < center[0] + horizontalThreshold ) { - // TOP - return pointRotateRads( - pointFrom(center[0], y - bindingGap), - center, - angle, - ); + return 3; } else if ( nonRotated[0] >= x + width / 2 && nonRotated[1] > center[1] - verticalThreshold && nonRotated[1] < center[1] + verticalThreshold ) { - // RIGHT - return pointRotateRads( - pointFrom(x + width + bindingGap, center[1]), - center, - angle, - ); + return 0; } else if ( nonRotated[1] >= y + height / 2 && nonRotated[0] > center[0] - horizontalThreshold && nonRotated[0] < center[0] + horizontalThreshold ) { - // DOWN - return pointRotateRads( - pointFrom(center[0], y + height + bindingGap), - center, - angle, - ); + return 1; } else if (element.type === "diamond") { const distance = bindingGap; const topLeft = pointFrom( @@ -678,100 +657,117 @@ export const getSnapOutlineMidPoint = ( y + (3 * height) / 4 + distance, ); - if ( - pointDistance(topLeft, nonRotated) < - Math.max(horizontalThreshold, verticalThreshold) - ) { - return pointRotateRads(topLeft, center, angle); - } - if ( - pointDistance(topRight, nonRotated) < - Math.max(horizontalThreshold, verticalThreshold) - ) { - return pointRotateRads(topRight, center, angle); - } if ( pointDistance(bottomLeft, nonRotated) < Math.max(horizontalThreshold, verticalThreshold) ) { - return pointRotateRads(bottomLeft, center, angle); + return 1; } if ( pointDistance(bottomRight, nonRotated) < Math.max(horizontalThreshold, verticalThreshold) ) { - return pointRotateRads(bottomRight, center, angle); + return 0; + } + if ( + pointDistance(topLeft, nonRotated) < + Math.max(horizontalThreshold, verticalThreshold) + ) { + return 2; + } + if ( + pointDistance(topRight, nonRotated) < + Math.max(horizontalThreshold, verticalThreshold) + ) { + return 3; } } - return undefined; + return -1; } + const baseMidpoints = getAllMidpoints(element, elementsMap); + + for (let i = 0; i < baseMidpoints.length; i++) { + const threshold = i % 2 === 0 ? horizontalThreshold : verticalThreshold; + + if ( + pointDistance(baseMidpoints[i], point) <= threshold && + !hitElementItself({ + point, + element, + threshold: 0, + elementsMap, + overrideShouldTestInside: true, + }) + ) { + return i; + } + } + + return -1; +}; + +export const getSnapOutlineMidPoint = ( + point: GlobalPoint, + element: ExcalidrawBindableElement, + elementsMap: ElementsMap, + zoom: AppState["zoom"], + arrow: { elbowed: boolean }, +): GlobalPoint | undefined => { + const center = elementCenterPoint(element, elementsMap); + const baseMidpoints = getAllMidpoints(element, elementsMap); + const sideMidpoints = element.type === "diamond" - ? getDiamondBaseCorners(element) - .map((curve) => { - const point = bezierEquation(curve, 0.5); - const rotatedPoint = pointRotateRads(point, center, element.angle); + ? baseMidpoints.map((midpoint) => { + return pointFrom( + midpoint[0] + (midpoint[0] - center[0]) * 0.1, + midpoint[1] + (midpoint[1] - center[1]) * 0.1, + ); + }) + : baseMidpoints; - return pointFrom(rotatedPoint[0], rotatedPoint[1]); - }) - .map((midpoint) => { - return pointFrom( - midpoint[0] + (midpoint[0] - center[0]) * 0.1, - midpoint[1] + (midpoint[1] - center[1]) * 0.1, - ); - }) - : [ - // RIGHT midpoint - pointRotateRads( - pointFrom( - element.x + element.width, - element.y + element.height / 2, - ), - center, - element.angle, - ), - // BOTTOM midpoint - pointRotateRads( - pointFrom( - element.x + element.width / 2, - element.y + element.height, - ), - center, - element.angle, - ), - // LEFT midpoint - pointRotateRads( - pointFrom(element.x, element.y + element.height / 2), - center, - element.angle, - ), - // TOP midpoint - pointRotateRads( - pointFrom(element.x + element.width / 2, element.y), - center, - element.angle, - ), - ]; + const idx = getHighlightedMidpointIndex( + point, + element, + elementsMap, + zoom, + arrow, + ); - return sideMidpoints - .map((midpoint, i) => { - const threshold = i % 2 === 0 ? horizontalThreshold : verticalThreshold; + if (idx === -1) { + return undefined; + } - return pointDistance(midpoint, point) <= threshold ? midpoint : undefined; - }) - .find( - (midpoint) => - midpoint && - !hitElementItself({ - point, - element, - threshold: 0, - elementsMap, - overrideShouldTestInside: true, - }), - ); + return sideMidpoints[idx]; +}; + +export const getAllMidpoints = ( + element: ExcalidrawBindableElement, + elementsMap: ElementsMap, +): GlobalPoint[] => { + const center = elementCenterPoint(element, elementsMap); + + if (element.type === "diamond") { + return getDiamondBaseCorners(element).map((curve) => { + const point = bezierEquation(curve, 0.5); + return pointRotateRads(point, center, element.angle); + }); + } + + return [ + pointFrom(element.width, element.height / 2), + pointFrom(element.width / 2, element.height), + pointFrom(0, element.height / 2), + pointFrom(element.width / 2, 0), + ].map(([x, y]) => + pointRotateRads( + pointFrom(element.x + x, element.y + y), + center, + element.angle, + ), + ); }; export const projectFixedPointOntoDiagonal = ( diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 30cadb0ef7..19b3840acb 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -7,7 +7,6 @@ import { type Radians, bezierEquation, pointRotateRads, - pointDistance, } from "@excalidraw/math"; import { @@ -24,7 +23,8 @@ import { deconstructDiamondElement, deconstructRectanguloidElement, elementCenterPoint, - getDiamondBaseCorners, + getAllMidpoints, + getHighlightedMidpointIndex, FOCUS_POINT_SIZE, getOmitSidesForEditorInterface, getTransformHandles, @@ -38,7 +38,6 @@ import { isImageElement, isLinearElement, isLineElement, - maxBindingDistance_simple, isTextElement, LinearElementEditor, getActiveTextElement, @@ -441,94 +440,23 @@ const renderBindingHighlightForBindableElement_simple = ( if (!cursorIsInsideBindable || isElbow) { context.save(); - const center = elementCenterPoint(suggestedBinding.element, elementsMap); + const midpoints = getAllMidpoints(suggestedBinding.element, elementsMap); - let midpoints: GlobalPoint[]; - if (suggestedBinding.element.type === "diamond") { - const center = elementCenterPoint( - suggestedBinding.element, - elementsMap, - ); - midpoints = getDiamondBaseCorners(suggestedBinding.element).map( - (curve) => { - const point = bezierEquation(curve, 0.5); - const rotatedPoint = pointRotateRads( - point, - center, - suggestedBinding.element.angle, - ); - - return pointFrom(rotatedPoint[0], rotatedPoint[1]); - }, - ); - } else { - const basePoints = [ - { - x: suggestedBinding.element.width, - y: suggestedBinding.element.height / 2, - }, // RIGHT - { - x: suggestedBinding.element.width / 2, - y: suggestedBinding.element.height, - }, // BOTTOM - { x: 0, y: suggestedBinding.element.height / 2 }, // LEFT - { x: suggestedBinding.element.width / 2, y: 0 }, // TOP - ]; - midpoints = basePoints.map((point) => { - const globalPoint = pointFrom( - point.x + suggestedBinding.element.x, - point.y + suggestedBinding.element.y, - ); - const rotatedPoint = pointRotateRads( - globalPoint, - center, - suggestedBinding.element.angle, - ); - return pointFrom(rotatedPoint[0], rotatedPoint[1]); - }); - } - - const hoveredMidpoint = - pointerCoords && - midpoints.reduce( - ( - closestIdx: { - idx: number; - distance: number; - }, - point, - idx, - ) => { - const distance = pointDistance(point, pointerCoords); - if (idx === -1 || distance < closestIdx.distance) { - return { idx, distance }; - } - return closestIdx; - }, - { - idx: -1, - distance: Infinity, - }, - ); + const highlightedIdx = pointerCoords + ? getHighlightedMidpointIndex( + pointerCoords, + suggestedBinding.element, + elementsMap, + appState.zoom, + { elbowed: isElbow }, + ) + : -1; const midpointRadius = 4 / appState.zoom.value; - const highlightThreshold = - maxBindingDistance_simple(appState.zoom) + - suggestedBinding.element.strokeWidth / 2; midpoints.forEach((midpoint, idx) => { const isHighlighted = - (!cursorIsInsideBindable || isElbow) && - hoveredMidpoint?.idx === idx && - hoveredMidpoint.distance <= highlightThreshold; - - // also render midpoint if cursor close but not highlighted - // (for elbows, always show all points) - const isShown = - !isHighlighted && - (isElbow || - (idx === hoveredMidpoint?.idx && - hoveredMidpoint.distance <= highlightThreshold * 2)); + highlightedIdx === idx && (!cursorIsInsideBindable || isElbow); if (isHighlighted) { context.fillStyle = @@ -539,7 +467,7 @@ const renderBindingHighlightForBindableElement_simple = ( context.beginPath(); context.arc(midpoint[0], midpoint[1], midpointRadius, 0, 2 * Math.PI); context.fill(); - } else if (isShown) { + } else { context.fillStyle = appState.theme === THEME.DARK ? `rgba(0, 0, 0, 0.8)`