include arrowhead in detection (wip)

This commit is contained in:
Ryan Di 2025-01-10 19:23:08 +11:00
parent 49fbad32ac
commit 4672bb948c
5 changed files with 122 additions and 57 deletions

View File

@ -233,7 +233,7 @@ import {
findShapeByKey, findShapeByKey,
getBoundTextShape, getBoundTextShape,
getCornerRadius, getCornerRadius,
getElementShape, getElementShapes,
isPathALoop, isPathALoop,
} from "../shapes"; } from "../shapes";
import { getSelectionBoxShape } from "../../utils/geometry/shape"; import { getSelectionBoxShape } from "../../utils/geometry/shape";
@ -5009,7 +5009,7 @@ class App extends React.Component<AppProps, AppState> {
x, x,
y, y,
element: elementWithHighestZIndex, element: elementWithHighestZIndex,
shape: getElementShape( shapes: getElementShapes(
elementWithHighestZIndex, elementWithHighestZIndex,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
), ),
@ -5121,7 +5121,7 @@ class App extends React.Component<AppProps, AppState> {
x, x,
y, y,
element, element,
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()), shapes: getElementShapes(element, this.scene.getNonDeletedElementsMap()),
threshold: this.getElementHitThreshold(), threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(element) frameNameBound: isFrameLikeElement(element)
? this.frameNameBoundsCache.get(element) ? this.frameNameBoundsCache.get(element)
@ -5153,7 +5153,7 @@ class App extends React.Component<AppProps, AppState> {
x, x,
y, y,
element: elements[index], element: elements[index],
shape: getElementShape( shapes: getElementShapes(
elements[index], elements[index],
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
), ),
@ -5437,7 +5437,7 @@ class App extends React.Component<AppProps, AppState> {
x: sceneX, x: sceneX,
y: sceneY, y: sceneY,
element: container, element: container,
shape: getElementShape( shapes: getElementShapes(
container, container,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
), ),
@ -6211,7 +6211,7 @@ class App extends React.Component<AppProps, AppState> {
x: scenePointerX, x: scenePointerX,
y: scenePointerY, y: scenePointerY,
element, element,
shape: getElementShape( shapes: getElementShapes(
element, element,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
), ),
@ -9344,7 +9344,7 @@ class App extends React.Component<AppProps, AppState> {
x: pointerDownState.origin.x, x: pointerDownState.origin.x,
y: pointerDownState.origin.y, y: pointerDownState.origin.y,
element: hitElement, element: hitElement,
shape: getElementShape( shapes: getElementShapes(
hitElement, hitElement,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElementsMap(),
), ),

View File

@ -52,7 +52,7 @@ import { LinearElementEditor } from "./linearElementEditor";
import { arrayToMap, tupleToCoors } from "../utils"; import { arrayToMap, tupleToCoors } from "../utils";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement"; import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes"; import { aabbForElement, getElementShapes, pointInsideBounds } from "../shapes";
import { import {
compareHeading, compareHeading,
HEADING_DOWN, HEADING_DOWN,
@ -1406,9 +1406,9 @@ export const bindingBorderTest = (
): boolean => { ): boolean => {
const threshold = maxBindingGap(element, element.width, element.height, zoom); const threshold = maxBindingGap(element, element.width, element.height, zoom);
const shape = getElementShape(element, elementsMap); const shapes = getElementShapes(element, elementsMap);
return ( return (
isPointOnShape(pointFrom(x, y), shape, threshold) || shapes.some((shape) => isPointOnShape(pointFrom(x, y), shape, threshold)) ||
(fullShape === true && (fullShape === true &&
pointInsideBounds(pointFrom(x, y), aabbForElement(element))) pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
); );

View File

@ -45,7 +45,7 @@ export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
x: number; x: number;
y: number; y: number;
element: ExcalidrawElement; element: ExcalidrawElement;
shape: GeometricShape<Point>; shapes: GeometricShape<Point>[];
threshold?: number; threshold?: number;
frameNameBound?: FrameNameBounds | null; frameNameBound?: FrameNameBounds | null;
}; };
@ -54,16 +54,18 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
x, x,
y, y,
element, element,
shape, shapes,
threshold = 10, threshold = 10,
frameNameBound = null, frameNameBound = null,
}: HitTestArgs<Point>) => { }: HitTestArgs<Point>) => {
let hit = shouldTestInside(element) const testInside = shouldTestInside(element);
? // Since `inShape` tests STRICTLY againt the insides of a shape
// we would need `onShape` as well to include the "borders" let hit = shapes.some((shape) =>
isPointInShape(pointFrom(x, y), shape) || testInside || shape.isClosed
isPointOnShape(pointFrom(x, y), shape, threshold) ? isPointInShape(pointFrom(x, y), shape) ||
: isPointOnShape(pointFrom(x, y), shape, threshold); isPointOnShape(pointFrom(x, y), shape, threshold)
: isPointOnShape(pointFrom(x, y), shape, threshold),
);
// hit test against a frame's name // hit test against a frame's name
if (!hit && frameNameBound) { if (!hit && frameNameBound) {

View File

@ -7,6 +7,8 @@ import {
pointsEqual, pointsEqual,
type GlobalPoint, type GlobalPoint,
type LocalPoint, type LocalPoint,
polygonFromPoints,
pointAdd,
} from "../math"; } from "../math";
import { import {
getClosedCurveShape, getClosedCurveShape,
@ -141,10 +143,10 @@ export const findShapeByKey = (key: string) => {
* get the pure geometric shape of an excalidraw element * get the pure geometric shape of an excalidraw element
* which is then used for hit detection * which is then used for hit detection
*/ */
export const getElementShape = <Point extends GlobalPoint | LocalPoint>( export const getElementShapes = <Point extends GlobalPoint | LocalPoint>(
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap, elementsMap: ElementsMap,
): GeometricShape<Point> => { ): GeometricShape<Point>[] => {
switch (element.type) { switch (element.type) {
case "rectangle": case "rectangle":
case "diamond": case "diamond":
@ -155,40 +157,96 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
case "iframe": case "iframe":
case "text": case "text":
case "selection": case "selection":
return getPolygonShape(element); return [getPolygonShape(element)];
case "arrow": case "arrow":
case "line": { case "line": {
const roughShape = const [curve, ...arrowheads] =
ShapeCache.get(element)?.[0] ?? ShapeCache.get(element) ??
ShapeCache.generateElementShape(element, null)[0]; ShapeCache.generateElementShape(element, null);
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
const center = pointFrom<Point>(cx, cy);
const startingPoint = pointFrom<Point>(element.x, element.y);
return shouldTestInside(element) if (shouldTestInside(element)) {
? getClosedCurveShape<Point>( return [
getClosedCurveShape<Point>(
element, element,
roughShape, curve,
pointFrom<Point>(element.x, element.y), startingPoint,
element.angle, element.angle,
pointFrom(cx, cy), center,
) ),
: getCurveShape<Point>( ];
roughShape, }
pointFrom<Point>(element.x, element.y),
element.angle, // otherwise return the curve shape (and also the shape of its arrowheads)
pointFrom(cx, cy), const arrowheadShapes: GeometricShape<Point>[] = [];
for (const arrowhead of arrowheads) {
if (arrowhead.shape === "polygon") {
const ops = arrowhead.sets[0].ops;
const otherPoints = ops.slice(1);
const arrowheadShape: GeometricShape<Point> = {
type: "polygon",
data: polygonFromPoints(
otherPoints.map((otherPoint) =>
pointAdd(
pointFrom<Point>(otherPoint.data[0], otherPoint.data[1]),
pointFrom<Point>(element.x, element.y),
),
),
),
isClosed: true,
};
arrowheadShapes.push(arrowheadShape);
}
if (arrowhead.shape === "circle") {
// TODO: close curve into polygon / ellipse
arrowheadShapes.push({
...getCurveShape<Point>(
arrowhead,
element.angle,
center,
startingPoint,
),
isClosed: true,
});
}
if (arrowhead.shape === "line") {
arrowheadShapes.push(
getCurveShape<Point>(
arrowhead,
element.angle,
center,
startingPoint,
),
); );
}
}
return [
getCurveShape<Point>(
curve,
element.angle,
pointFrom(cx, cy),
startingPoint,
),
...arrowheadShapes,
];
} }
case "ellipse": case "ellipse":
return getEllipseShape(element); return [getEllipseShape(element)];
case "freedraw": { case "freedraw": {
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
return getFreedrawShape( return [
element, getFreedrawShape(element, pointFrom(cx, cy), shouldTestInside(element)),
pointFrom(cx, cy), ];
shouldTestInside(element),
);
} }
} }
}; };
@ -201,21 +259,23 @@ export const getBoundTextShape = <Point extends GlobalPoint | LocalPoint>(
if (boundTextElement) { if (boundTextElement) {
if (element.type === "arrow") { if (element.type === "arrow") {
return getElementShape( return (
{ getElementShapes<Point>(
...boundTextElement, {
// arrow's bound text accurate position is not stored in the element's property ...boundTextElement,
// but rather calculated and returned from the following static method // arrow's bound text accurate position is not stored in the element's property
...LinearElementEditor.getBoundTextElementPosition( // but rather calculated and returned from the following static method
element, ...LinearElementEditor.getBoundTextElementPosition(
boundTextElement, element,
elementsMap, boundTextElement,
), elementsMap,
}, ),
elementsMap, },
elementsMap,
)[0] ?? null
); );
} }
return getElementShape(boundTextElement, elementsMap); return getElementShapes<Point>(boundTextElement, elementsMap)[0] ?? null;
} }
return null; return null;

View File

@ -73,7 +73,7 @@ export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
halfHeight: number; halfHeight: number;
}; };
export type GeometricShape<Point extends GlobalPoint | LocalPoint> = export type GeometricShape<Point extends GlobalPoint | LocalPoint> = (
| { | {
type: "line"; type: "line";
data: LineSegment<Point>; data: LineSegment<Point>;
@ -97,7 +97,10 @@ export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
| { | {
type: "polycurve"; type: "polycurve";
data: Polycurve<Point>; data: Polycurve<Point>;
}; }
) & {
isClosed?: boolean;
};
type RectangularElement = type RectangularElement =
| ExcalidrawRectangleElement | ExcalidrawRectangleElement
@ -203,9 +206,9 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
// linear // linear
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>( export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
roughShape: Drawable, roughShape: Drawable,
startingPoint: Point = pointFrom(0, 0),
angleInRadian: Radians, angleInRadian: Radians,
center: Point, center: Point,
startingPoint: Point = pointFrom(0, 0),
): GeometricShape<Point> => { ): GeometricShape<Point> => {
const transform = (p: Point): Point => const transform = (p: Point): Point =>
pointRotateRads( pointRotateRads(