fix(editor): simplify and fix midpoint highlighting (#10832)

This commit is contained in:
David Luzar
2026-02-24 21:11:46 +01:00
committed by GitHub
parent 0b3a5e7cc4
commit 2874f9e48c
3 changed files with 62 additions and 51 deletions
@@ -265,6 +265,7 @@ const getRelevantAppStateProps = (
frameRendering: appState.frameRendering,
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
exportScale: appState.exportScale,
currentItemArrowType: appState.currentItemArrowType,
});
const areEqual = (
@@ -7,6 +7,7 @@ import {
type Radians,
bezierEquation,
pointRotateRads,
pointDistance,
} from "@excalidraw/math";
import {
@@ -37,14 +38,9 @@ import {
isImageElement,
isLinearElement,
isLineElement,
maxBindingDistance_simple,
isTextElement,
LinearElementEditor,
headingForPoint,
compareHeading,
HEADING_RIGHT,
HEADING_DOWN,
HEADING_LEFT,
HEADING_UP,
} from "@excalidraw/element";
import { renderSelectionElement } from "@excalidraw/element";
@@ -419,9 +415,8 @@ const renderBindingHighlightForBindableElement_simple = (
const arrow =
linearElement?.elementId &&
LinearElementEditor.getElement(linearElement?.elementId, elementsMap);
const insideBindable =
const cursorIsInsideBindable =
pointerCoords &&
arrow &&
hitElementItself({
point: pointerCoords,
element: suggestedBinding.element,
@@ -430,14 +425,17 @@ const renderBindingHighlightForBindableElement_simple = (
overrideShouldTestInside: true,
});
if (!insideBindable || isElbowArrow(arrow)) {
context.save();
context.translate(suggestedBinding.element.x, suggestedBinding.element.y);
const isElbow =
(arrow && isElbowArrow(arrow)) ||
(appState.activeTool.type === "arrow" &&
appState.currentItemArrowType === "elbow");
if (!cursorIsInsideBindable || isElbow) {
context.save();
const midpointRadius = 5 / appState.zoom.value;
const center = elementCenterPoint(suggestedBinding.element, elementsMap);
let midpoints: LocalPoint[];
let midpoints: GlobalPoint[];
if (suggestedBinding.element.type === "diamond") {
const center = elementCenterPoint(
suggestedBinding.element,
@@ -452,10 +450,7 @@ const renderBindingHighlightForBindableElement_simple = (
suggestedBinding.element.angle,
);
return pointFrom<LocalPoint>(
rotatedPoint[0] - suggestedBinding.element.x,
rotatedPoint[1] - suggestedBinding.element.y,
);
return pointFrom<GlobalPoint>(rotatedPoint[0], rotatedPoint[1]);
},
);
} else {
@@ -481,47 +476,53 @@ const renderBindingHighlightForBindableElement_simple = (
center,
suggestedBinding.element.angle,
);
return pointFrom<LocalPoint>(
rotatedPoint[0] - suggestedBinding.element.x,
rotatedPoint[1] - suggestedBinding.element.y,
);
return pointFrom<GlobalPoint>(rotatedPoint[0], rotatedPoint[1]);
});
}
const highlightedPoint =
suggestedBinding.midPoint &&
pointFrom<LocalPoint>(
suggestedBinding.midPoint[0] - suggestedBinding.element.x,
suggestedBinding.midPoint[1] - suggestedBinding.element.y,
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 target = [HEADING_RIGHT, HEADING_DOWN, HEADING_LEFT, HEADING_UP];
const midpointRadius = 4 / appState.zoom.value;
const highlightThreshold =
maxBindingDistance_simple(appState.zoom) +
suggestedBinding.element.strokeWidth / 2;
midpoints.forEach((midpoint, idx) => {
const isHighlighted =
highlightedPoint &&
compareHeading(
headingForPoint(
pointRotateRads(
pointFrom<GlobalPoint>(
highlightedPoint[0] + suggestedBinding.element.x,
highlightedPoint[1] + suggestedBinding.element.y,
),
center,
suggestedBinding.element.angle as Radians,
),
center,
),
target[idx],
);
(!cursorIsInsideBindable || isElbow) &&
hoveredMidpoint?.idx === idx &&
hoveredMidpoint.distance <= highlightThreshold;
if (!isHighlighted) {
context.fillStyle =
appState.theme === THEME.DARK
? `rgba(0, 0, 0, 0.5)`
: `rgba(65, 65, 65, 0.4)`;
context.beginPath();
context.arc(midpoint[0], midpoint[1], midpointRadius, 0, 2 * Math.PI);
context.fill();
} else {
// 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));
if (isHighlighted) {
context.fillStyle =
appState.theme === THEME.DARK
? `rgba(3, 93, 161, 1)`
@@ -530,6 +531,14 @@ const renderBindingHighlightForBindableElement_simple = (
context.beginPath();
context.arc(midpoint[0], midpoint[1], midpointRadius, 0, 2 * Math.PI);
context.fill();
} else if (isShown) {
context.fillStyle =
appState.theme === THEME.DARK
? `rgba(0, 0, 0, 0.8)`
: `rgba(65, 65, 65, 0.5)`;
context.beginPath();
context.arc(midpoint[0], midpoint[1], midpointRadius, 0, 2 * Math.PI);
context.fill();
}
});
+1
View File
@@ -244,6 +244,7 @@ export type InteractiveCanvasAppState = Readonly<
frameRendering: AppState["frameRendering"];
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
exportScale: AppState["exportScale"];
currentItemArrowType: AppState["currentItemArrowType"];
}
>;