include arrowhead in detection (wip)
This commit is contained in:
parent
49fbad32ac
commit
4672bb948c
@ -233,7 +233,7 @@ import {
|
||||
findShapeByKey,
|
||||
getBoundTextShape,
|
||||
getCornerRadius,
|
||||
getElementShape,
|
||||
getElementShapes,
|
||||
isPathALoop,
|
||||
} from "../shapes";
|
||||
import { getSelectionBoxShape } from "../../utils/geometry/shape";
|
||||
@ -5009,7 +5009,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
x,
|
||||
y,
|
||||
element: elementWithHighestZIndex,
|
||||
shape: getElementShape(
|
||||
shapes: getElementShapes(
|
||||
elementWithHighestZIndex,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
@ -5121,7 +5121,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
x,
|
||||
y,
|
||||
element,
|
||||
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()),
|
||||
shapes: getElementShapes(element, this.scene.getNonDeletedElementsMap()),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
frameNameBound: isFrameLikeElement(element)
|
||||
? this.frameNameBoundsCache.get(element)
|
||||
@ -5153,7 +5153,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
x,
|
||||
y,
|
||||
element: elements[index],
|
||||
shape: getElementShape(
|
||||
shapes: getElementShapes(
|
||||
elements[index],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
@ -5437,7 +5437,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
x: sceneX,
|
||||
y: sceneY,
|
||||
element: container,
|
||||
shape: getElementShape(
|
||||
shapes: getElementShapes(
|
||||
container,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
@ -6211,7 +6211,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
x: scenePointerX,
|
||||
y: scenePointerY,
|
||||
element,
|
||||
shape: getElementShape(
|
||||
shapes: getElementShapes(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
@ -9344,7 +9344,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
x: pointerDownState.origin.x,
|
||||
y: pointerDownState.origin.y,
|
||||
element: hitElement,
|
||||
shape: getElementShape(
|
||||
shapes: getElementShapes(
|
||||
hitElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
|
@ -52,7 +52,7 @@ import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { arrayToMap, tupleToCoors } from "../utils";
|
||||
import { KEYS } from "../keys";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import { aabbForElement, getElementShape, pointInsideBounds } from "../shapes";
|
||||
import { aabbForElement, getElementShapes, pointInsideBounds } from "../shapes";
|
||||
import {
|
||||
compareHeading,
|
||||
HEADING_DOWN,
|
||||
@ -1406,9 +1406,9 @@ export const bindingBorderTest = (
|
||||
): boolean => {
|
||||
const threshold = maxBindingGap(element, element.width, element.height, zoom);
|
||||
|
||||
const shape = getElementShape(element, elementsMap);
|
||||
const shapes = getElementShapes(element, elementsMap);
|
||||
return (
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold) ||
|
||||
shapes.some((shape) => isPointOnShape(pointFrom(x, y), shape, threshold)) ||
|
||||
(fullShape === true &&
|
||||
pointInsideBounds(pointFrom(x, y), aabbForElement(element)))
|
||||
);
|
||||
|
@ -45,7 +45,7 @@ export type HitTestArgs<Point extends GlobalPoint | LocalPoint> = {
|
||||
x: number;
|
||||
y: number;
|
||||
element: ExcalidrawElement;
|
||||
shape: GeometricShape<Point>;
|
||||
shapes: GeometricShape<Point>[];
|
||||
threshold?: number;
|
||||
frameNameBound?: FrameNameBounds | null;
|
||||
};
|
||||
@ -54,16 +54,18 @@ export const hitElementItself = <Point extends GlobalPoint | LocalPoint>({
|
||||
x,
|
||||
y,
|
||||
element,
|
||||
shape,
|
||||
shapes,
|
||||
threshold = 10,
|
||||
frameNameBound = null,
|
||||
}: HitTestArgs<Point>) => {
|
||||
let hit = shouldTestInside(element)
|
||||
? // Since `inShape` tests STRICTLY againt the insides of a shape
|
||||
// we would need `onShape` as well to include the "borders"
|
||||
isPointInShape(pointFrom(x, y), shape) ||
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
||||
: isPointOnShape(pointFrom(x, y), shape, threshold);
|
||||
const testInside = shouldTestInside(element);
|
||||
|
||||
let hit = shapes.some((shape) =>
|
||||
testInside || shape.isClosed
|
||||
? isPointInShape(pointFrom(x, y), shape) ||
|
||||
isPointOnShape(pointFrom(x, y), shape, threshold)
|
||||
: isPointOnShape(pointFrom(x, y), shape, threshold),
|
||||
);
|
||||
|
||||
// hit test against a frame's name
|
||||
if (!hit && frameNameBound) {
|
||||
|
@ -7,6 +7,8 @@ import {
|
||||
pointsEqual,
|
||||
type GlobalPoint,
|
||||
type LocalPoint,
|
||||
polygonFromPoints,
|
||||
pointAdd,
|
||||
} from "../math";
|
||||
import {
|
||||
getClosedCurveShape,
|
||||
@ -141,10 +143,10 @@ export const findShapeByKey = (key: string) => {
|
||||
* get the pure geometric shape of an excalidraw element
|
||||
* which is then used for hit detection
|
||||
*/
|
||||
export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
export const getElementShapes = <Point extends GlobalPoint | LocalPoint>(
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): GeometricShape<Point> => {
|
||||
): GeometricShape<Point>[] => {
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "diamond":
|
||||
@ -155,40 +157,96 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
case "iframe":
|
||||
case "text":
|
||||
case "selection":
|
||||
return getPolygonShape(element);
|
||||
return [getPolygonShape(element)];
|
||||
case "arrow":
|
||||
case "line": {
|
||||
const roughShape =
|
||||
ShapeCache.get(element)?.[0] ??
|
||||
ShapeCache.generateElementShape(element, null)[0];
|
||||
const [curve, ...arrowheads] =
|
||||
ShapeCache.get(element) ??
|
||||
ShapeCache.generateElementShape(element, null);
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const center = pointFrom<Point>(cx, cy);
|
||||
const startingPoint = pointFrom<Point>(element.x, element.y);
|
||||
|
||||
return shouldTestInside(element)
|
||||
? getClosedCurveShape<Point>(
|
||||
if (shouldTestInside(element)) {
|
||||
return [
|
||||
getClosedCurveShape<Point>(
|
||||
element,
|
||||
roughShape,
|
||||
pointFrom<Point>(element.x, element.y),
|
||||
curve,
|
||||
startingPoint,
|
||||
element.angle,
|
||||
pointFrom(cx, cy),
|
||||
)
|
||||
: getCurveShape<Point>(
|
||||
roughShape,
|
||||
pointFrom<Point>(element.x, element.y),
|
||||
element.angle,
|
||||
pointFrom(cx, cy),
|
||||
center,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// otherwise return the curve shape (and also the shape of its arrowheads)
|
||||
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":
|
||||
return getEllipseShape(element);
|
||||
return [getEllipseShape(element)];
|
||||
|
||||
case "freedraw": {
|
||||
const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap);
|
||||
return getFreedrawShape(
|
||||
element,
|
||||
pointFrom(cx, cy),
|
||||
shouldTestInside(element),
|
||||
);
|
||||
return [
|
||||
getFreedrawShape(element, pointFrom(cx, cy), shouldTestInside(element)),
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -201,21 +259,23 @@ export const getBoundTextShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
|
||||
if (boundTextElement) {
|
||||
if (element.type === "arrow") {
|
||||
return getElementShape(
|
||||
{
|
||||
...boundTextElement,
|
||||
// arrow's bound text accurate position is not stored in the element's property
|
||||
// but rather calculated and returned from the following static method
|
||||
...LinearElementEditor.getBoundTextElementPosition(
|
||||
element,
|
||||
boundTextElement,
|
||||
elementsMap,
|
||||
),
|
||||
},
|
||||
elementsMap,
|
||||
return (
|
||||
getElementShapes<Point>(
|
||||
{
|
||||
...boundTextElement,
|
||||
// arrow's bound text accurate position is not stored in the element's property
|
||||
// but rather calculated and returned from the following static method
|
||||
...LinearElementEditor.getBoundTextElementPosition(
|
||||
element,
|
||||
boundTextElement,
|
||||
elementsMap,
|
||||
),
|
||||
},
|
||||
elementsMap,
|
||||
)[0] ?? null
|
||||
);
|
||||
}
|
||||
return getElementShape(boundTextElement, elementsMap);
|
||||
return getElementShapes<Point>(boundTextElement, elementsMap)[0] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -73,7 +73,7 @@ export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
|
||||
halfHeight: number;
|
||||
};
|
||||
|
||||
export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
|
||||
export type GeometricShape<Point extends GlobalPoint | LocalPoint> = (
|
||||
| {
|
||||
type: "line";
|
||||
data: LineSegment<Point>;
|
||||
@ -97,7 +97,10 @@ export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
|
||||
| {
|
||||
type: "polycurve";
|
||||
data: Polycurve<Point>;
|
||||
};
|
||||
}
|
||||
) & {
|
||||
isClosed?: boolean;
|
||||
};
|
||||
|
||||
type RectangularElement =
|
||||
| ExcalidrawRectangleElement
|
||||
@ -203,9 +206,9 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
|
||||
// linear
|
||||
export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
|
||||
roughShape: Drawable,
|
||||
startingPoint: Point = pointFrom(0, 0),
|
||||
angleInRadian: Radians,
|
||||
center: Point,
|
||||
startingPoint: Point = pointFrom(0, 0),
|
||||
): GeometricShape<Point> => {
|
||||
const transform = (p: Point): Point =>
|
||||
pointRotateRads(
|
||||
|
Loading…
x
Reference in New Issue
Block a user