fix: Working outline avoidance

This commit is contained in:
Mark Tolmacs
2025-11-22 19:15:34 +01:00
parent f5740db8b9
commit 2263b781fa
3 changed files with 107 additions and 74 deletions

View File

@@ -139,7 +139,7 @@ export const debugDrawPoints = (
}: {
x: number;
y: number;
points: LocalPoint[];
points: readonly LocalPoint[];
},
options?: any,
) => {

View File

@@ -31,6 +31,7 @@ import type { MapEntry, Mutable } from "@excalidraw/common/utility-types";
import {
doBoundsIntersect,
getCenterForBounds,
getElementAbsoluteCoords,
getElementBounds,
} from "./bounds";
import {
@@ -1201,13 +1202,33 @@ export const bindPointToSnapToElementOutline = (
startOrEnd: "start" | "end",
elementsMap: ElementsMap,
customIntersector?: LineSegment<GlobalPoint>,
originalArrow?: ExcalidrawArrowElement,
): GlobalPoint => {
const aabb = aabbForElement(bindableElement, elementsMap);
const point = LinearElementEditor.getPointAtIndexGlobalCoordinates(
arrowElement,
startOrEnd === "start" ? 0 : -1,
// const point = LinearElementEditor.getPointAtIndexGlobalCoordinates(
// arrowElement,
// startOrEnd === "start" ? 0 : -1,
// elementsMap,
// );
const [x1, y1, x2, y2] = getElementAbsoluteCoords(
originalArrow ?? arrowElement,
elementsMap,
);
const arrowCenter = pointFrom<GlobalPoint>((x1 + x2) / 2, (y1 + y2) / 2);
const point = pointRotateRads(
pointFrom<GlobalPoint>(
arrowElement.x +
arrowElement.points[
startOrEnd === "start" ? 0 : arrowElement.points.length - 1
][0],
arrowElement.y +
arrowElement.points[
startOrEnd === "start" ? 0 : arrowElement.points.length - 1
][1],
),
arrowCenter,
arrowElement.angle as Radians,
);
if (arrowElement.points.length < 2) {
// New arrow creation, so no snapping
@@ -1219,11 +1240,25 @@ export const bindPointToSnapToElementOutline = (
: point;
const elbowed = isElbowArrow(arrowElement);
const center = getCenterForBounds(aabb);
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
arrowElement,
startOrEnd === "start" ? 1 : -2,
elementsMap,
const adjacentPoint = pointRotateRads(
pointFrom<GlobalPoint>(
arrowElement.x +
arrowElement.points[
startOrEnd === "start" ? 1 : arrowElement.points.length - 2
][0],
arrowElement.y +
arrowElement.points[
startOrEnd === "start" ? 1 : arrowElement.points.length - 2
][1],
),
arrowCenter,
arrowElement.angle as Radians,
);
// const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
// arrowElement,
// startOrEnd === "start" ? 1 : -2,
// elementsMap,
// );
const bindingGap = getBindingGap(bindableElement, arrowElement);
let intersection: GlobalPoint | null = null;
@@ -1536,6 +1571,7 @@ export const updateBoundPoint = (
bindableElement: ExcalidrawBindableElement,
elementsMap: ElementsMap,
customIntersector?: LineSegment<GlobalPoint>,
originalArrow?: ExcalidrawArrowElement,
): LocalPoint | null => {
if (
binding == null ||
@@ -1615,54 +1651,50 @@ export const updateBoundPoint = (
const isNested = (arrowTooShort || isOverlapping) && isLargerThanOther;
const rotatedGlobal = pointRotateRads(
global,
elementCenterPoint(arrow, elementsMap),
-arrow.angle as Radians,
);
const maybeOutlineGlobal =
binding.mode === "orbit" && bindableElement
? isNested
? rotatedGlobal
? global
: bindPointToSnapToElementOutline(
{
...arrow,
id: randomId(),
x: pointIndex === 0 ? rotatedGlobal[0] : arrow.x,
y: pointIndex === 0 ? rotatedGlobal[1] : arrow.y,
points:
points: [
pointIndex === 0
? [
pointFrom<LocalPoint>(0, 0),
...arrow.points
.slice(1)
.map((p) =>
pointFrom<LocalPoint>(
p[0] - (rotatedGlobal[0] - arrow.x),
p[1] - (rotatedGlobal[1] - arrow.y),
),
),
]
: [
...arrow.points.slice(0, -1),
pointFrom<LocalPoint>(
rotatedGlobal[0] - arrow.x,
rotatedGlobal[1] - arrow.y,
),
],
? LinearElementEditor.createPointAt(
arrow,
elementsMap,
global[0],
global[1],
null,
)
: arrow.points[0],
...arrow.points.slice(1, -1),
pointIndex === arrow.points.length - 1
? LinearElementEditor.createPointAt(
arrow,
elementsMap,
global[0],
global[1],
null,
)
: arrow.points[arrow.points.length - 1],
],
},
bindableElement,
pointIndex === 0 ? "start" : "end",
elementsMap,
customIntersector,
originalArrow,
)
: global;
return LinearElementEditor.pointFromAbsoluteCoords(
return LinearElementEditor.createPointAt(
arrow,
maybeOutlineGlobal,
elementsMap,
maybeOutlineGlobal[0],
maybeOutlineGlobal[1],
null,
);
};

View File

@@ -22,6 +22,7 @@ import {
invariant,
isShallowEqual,
getFeatureFlag,
randomId,
} from "@excalidraw/common";
import {
@@ -1955,36 +1956,40 @@ export class LinearElementEditor {
let y1;
let x2;
let y2;
if (element.points.length < 2 || !ShapeCache.get(element)) {
// XXX this is just a poor estimate and not very useful
const { minX, minY, maxX, maxY } = element.points.reduce(
(limits, [x, y]) => {
limits.minY = Math.min(limits.minY, y);
limits.minX = Math.min(limits.minX, x);
// if (element.points.length < 2 || !ShapeCache.get(element)) {
// // XXX this is just a poor estimate and not very useful
// const { minX, minY, maxX, maxY } = element.points.reduce(
// (limits, [x, y]) => {
// limits.minY = Math.min(limits.minY, y);
// limits.minX = Math.min(limits.minX, x);
limits.maxX = Math.max(limits.maxX, x);
limits.maxY = Math.max(limits.maxY, y);
// limits.maxX = Math.max(limits.maxX, x);
// limits.maxY = Math.max(limits.maxY, y);
return limits;
},
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
);
x1 = minX + element.x;
y1 = minY + element.y;
x2 = maxX + element.x;
y2 = maxY + element.y;
} else {
const shape = ShapeCache.generateElementShape(element, null);
// return limits;
// },
// { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
// );
// x1 = minX + element.x;
// y1 = minY + element.y;
// x2 = maxX + element.x;
// y2 = maxY + element.y;
// } else {
const shape = ShapeCache.generateElementShape(element, {
isExporting: true,
canvasBackgroundColor: "traansparent",
embedsValidationStatus: new Map(),
});
// first element is always the curve
const ops = getCurvePathOps(shape[0]);
// first element is always the curve
const ops = getCurvePathOps(shape[0]);
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
x1 = minX + element.x;
y1 = minY + element.y;
x2 = maxX + element.x;
y2 = maxY + element.y;
}
const [minX, minY, maxX, maxY] = getMinMaxXYFromCurvePathOps(ops);
x1 = minX + element.x;
y1 = minY + element.y;
x2 = maxX + element.x;
y2 = maxY + element.y;
// }
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
coords = [x1, y1, x2, y2, cx, cy];
@@ -2257,16 +2262,10 @@ const pointDraggingUpdates = (
: element.points[element.points.length - 1];
const nextArrow = {
...element,
id: randomId(),
points: [
offsetStartLocalPoint,
...element.points
.slice(1, -1)
.map((p) =>
pointFrom<LocalPoint>(
p[0] - offsetStartLocalPoint[0],
p[1] - offsetStartLocalPoint[1],
),
),
...element.points.slice(1, -1),
offsetEndLocalPoint,
],
startBinding:
@@ -2321,12 +2320,13 @@ const pointDraggingUpdates = (
? nextArrow.points[0]
: endBindable
? updateBoundPoint(
nextArrow,
element,
"endBinding",
nextArrow.endBinding,
endBindable,
elementsMap,
customIntersector,
element,
) || nextArrow.points[nextArrow.points.length - 1]
: nextArrow.points[nextArrow.points.length - 1];
@@ -2352,12 +2352,13 @@ const pointDraggingUpdates = (
? nextArrow.points[nextArrow.points.length - 1]
: startBindable
? updateBoundPoint(
nextArrow,
element,
"startBinding",
nextArrow.startBinding,
startBindable,
elementsMap,
customIntersector,
element,
) || nextArrow.points[0]
: nextArrow.points[0];