mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Refactoring bounds, arrows, bboxes
This commit is contained in:
parent
9eb08df3ea
commit
dff60e6f6f
10 changed files with 318 additions and 408 deletions
178
packages/excalidraw/element/arrow.ts
Normal file
178
packages/excalidraw/element/arrow.ts
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
import type { Drawable } from "roughjs/bin/core";
|
||||||
|
import {
|
||||||
|
degrees,
|
||||||
|
degreesToRadians,
|
||||||
|
point,
|
||||||
|
pointFromArray,
|
||||||
|
pointRotateRads,
|
||||||
|
radians,
|
||||||
|
type Degrees,
|
||||||
|
} from "../../math";
|
||||||
|
import type { Arrowhead, ExcalidrawLinearElement } from "./types";
|
||||||
|
import { getCurvePathOps } from "../../utils/geometry/shape";
|
||||||
|
import { invariant } from "../utils";
|
||||||
|
|
||||||
|
/** @returns number in degrees */
|
||||||
|
const getArrowheadAngle = (arrowhead: Arrowhead): Degrees => {
|
||||||
|
switch (arrowhead) {
|
||||||
|
case "bar":
|
||||||
|
return degrees(90);
|
||||||
|
case "arrow":
|
||||||
|
return degrees(20);
|
||||||
|
default:
|
||||||
|
return degrees(25);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns number in pixels */
|
||||||
|
const getArrowheadSize = (arrowhead: Arrowhead): number => {
|
||||||
|
switch (arrowhead) {
|
||||||
|
case "arrow":
|
||||||
|
return 25;
|
||||||
|
case "diamond":
|
||||||
|
case "diamond_outline":
|
||||||
|
return 12;
|
||||||
|
default:
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getArrowheadPoints = (
|
||||||
|
element: ExcalidrawLinearElement,
|
||||||
|
shape: Drawable[],
|
||||||
|
position: "start" | "end",
|
||||||
|
arrowhead: Arrowhead,
|
||||||
|
) => {
|
||||||
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
if (ops.length < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The index of the bCurve operation to examine.
|
||||||
|
const index = position === "start" ? 1 : ops.length - 1;
|
||||||
|
|
||||||
|
const data = ops[index].data;
|
||||||
|
|
||||||
|
invariant(data.length === 6, "Op data length is not 6");
|
||||||
|
|
||||||
|
const p3 = point(data[4], data[5]);
|
||||||
|
const p2 = point(data[2], data[3]);
|
||||||
|
const p1 = point(data[0], data[1]);
|
||||||
|
|
||||||
|
// We need to find p0 of the bezier curve.
|
||||||
|
// It is typically the last point of the previous
|
||||||
|
// curve; it can also be the position of moveTo operation.
|
||||||
|
const prevOp = ops[index - 1];
|
||||||
|
let p0 = point(0, 0);
|
||||||
|
if (prevOp.op === "move") {
|
||||||
|
const p = pointFromArray(prevOp.data);
|
||||||
|
invariant(p != null, "Op data is not a point");
|
||||||
|
p0 = p;
|
||||||
|
} else if (prevOp.op === "bcurveTo") {
|
||||||
|
p0 = point(prevOp.data[4], prevOp.data[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
||||||
|
const equation = (t: number, idx: number) =>
|
||||||
|
Math.pow(1 - t, 3) * p3[idx] +
|
||||||
|
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
||||||
|
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
||||||
|
p0[idx] * Math.pow(t, 3);
|
||||||
|
|
||||||
|
// Ee know the last point of the arrow (or the first, if start arrowhead).
|
||||||
|
const [x2, y2] = position === "start" ? p0 : p3;
|
||||||
|
|
||||||
|
// By using cubic bezier equation (B(t)) and the given parameters,
|
||||||
|
// we calculate a point that is closer to the last point.
|
||||||
|
// The value 0.3 is chosen arbitrarily and it works best for all
|
||||||
|
// the tested cases.
|
||||||
|
const [x1, y1] = [equation(0.3, 0), equation(0.3, 1)];
|
||||||
|
|
||||||
|
// Find the normalized direction vector based on the
|
||||||
|
// previously calculated points.
|
||||||
|
const distance = Math.hypot(x2 - x1, y2 - y1);
|
||||||
|
const nx = (x2 - x1) / distance;
|
||||||
|
const ny = (y2 - y1) / distance;
|
||||||
|
|
||||||
|
const size = getArrowheadSize(arrowhead);
|
||||||
|
|
||||||
|
let length = 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
// Length for -> arrows is based on the length of the last section
|
||||||
|
const [cx, cy] =
|
||||||
|
position === "end"
|
||||||
|
? element.points[element.points.length - 1]
|
||||||
|
: element.points[0];
|
||||||
|
const [px, py] =
|
||||||
|
element.points.length > 1
|
||||||
|
? position === "end"
|
||||||
|
? element.points[element.points.length - 2]
|
||||||
|
: element.points[1]
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
length = Math.hypot(cx - px, cy - py);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
|
||||||
|
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
|
||||||
|
const lengthMultiplier =
|
||||||
|
arrowhead === "diamond" || arrowhead === "diamond_outline" ? 0.25 : 0.5;
|
||||||
|
const minSize = Math.min(size, length * lengthMultiplier);
|
||||||
|
const xs = x2 - nx * minSize;
|
||||||
|
const ys = y2 - ny * minSize;
|
||||||
|
|
||||||
|
if (
|
||||||
|
arrowhead === "dot" ||
|
||||||
|
arrowhead === "circle" ||
|
||||||
|
arrowhead === "circle_outline"
|
||||||
|
) {
|
||||||
|
const diameter = Math.hypot(ys - y2, xs - x2) + element.strokeWidth - 2;
|
||||||
|
return [x2, y2, diameter];
|
||||||
|
}
|
||||||
|
|
||||||
|
const angle = getArrowheadAngle(arrowhead);
|
||||||
|
|
||||||
|
// Return points
|
||||||
|
const [x3, y3] = pointRotateRads(
|
||||||
|
point(xs, ys),
|
||||||
|
point(x2, y2),
|
||||||
|
radians((-angle * Math.PI) / 180),
|
||||||
|
);
|
||||||
|
const [x4, y4] = pointRotateRads(
|
||||||
|
point(xs, ys),
|
||||||
|
point(x2, y2),
|
||||||
|
degreesToRadians(angle),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (arrowhead === "diamond" || arrowhead === "diamond_outline") {
|
||||||
|
// point opposite to the arrowhead point
|
||||||
|
let ox;
|
||||||
|
let oy;
|
||||||
|
|
||||||
|
if (position === "start") {
|
||||||
|
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
||||||
|
|
||||||
|
[ox, oy] = pointRotateRads(
|
||||||
|
point(x2 + minSize * 2, y2),
|
||||||
|
point(x2, y2),
|
||||||
|
radians(Math.atan2(py - y2, px - x2)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const [px, py] =
|
||||||
|
element.points.length > 1
|
||||||
|
? element.points[element.points.length - 2]
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
[ox, oy] = pointRotateRads(
|
||||||
|
point(x2 - minSize * 2, y2),
|
||||||
|
point(x2, y2),
|
||||||
|
radians(Math.atan2(y2 - py, x2 - px)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [x2, y2, x3, y3, ox, oy, x4, y4];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [x2, y2, x3, y3, x4, y4];
|
||||||
|
};
|
|
@ -1,7 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
Arrowhead,
|
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
@ -23,16 +22,8 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { arrayToMap, invariant } from "../utils";
|
import { arrayToMap, invariant } from "../utils";
|
||||||
import type {
|
import type { GlobalPoint, LocalPoint } from "../../math";
|
||||||
Degrees,
|
|
||||||
GlobalPoint,
|
|
||||||
LineSegment,
|
|
||||||
LocalPoint,
|
|
||||||
Radians,
|
|
||||||
} from "../../math";
|
|
||||||
import {
|
import {
|
||||||
degreesToRadians,
|
|
||||||
lineSegment,
|
|
||||||
point,
|
point,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
pointFromArray,
|
pointFromArray,
|
||||||
|
@ -40,14 +31,7 @@ import {
|
||||||
pointRescaleFromTopLeft,
|
pointRescaleFromTopLeft,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
|
import { getCurvePathOps } from "../../utils/geometry/shape";
|
||||||
export type RectangleBox = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
angle: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
||||||
|
|
||||||
|
@ -68,7 +52,7 @@ export type SceneBounds = readonly [
|
||||||
sceneY2: number,
|
sceneY2: number,
|
||||||
];
|
];
|
||||||
|
|
||||||
export class ElementBounds {
|
class ElementBounds {
|
||||||
private static boundsCache = new WeakMap<
|
private static boundsCache = new WeakMap<
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
{
|
{
|
||||||
|
@ -241,117 +225,6 @@ export const getElementAbsoluteCoords = (
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* for a given element, `getElementLineSegments` returns line segments
|
|
||||||
* that can be used for visual collision detection (useful for frames)
|
|
||||||
* as opposed to bounding box collision detection
|
|
||||||
*/
|
|
||||||
export const getElementLineSegments = (
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
elementsMap: ElementsMap,
|
|
||||||
): LineSegment<GlobalPoint>[] => {
|
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
);
|
|
||||||
|
|
||||||
const center: GlobalPoint = point(cx, cy);
|
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
|
||||||
const segments: LineSegment<GlobalPoint>[] = [];
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (i < element.points.length - 1) {
|
|
||||||
segments.push(
|
|
||||||
lineSegment(
|
|
||||||
pointRotateRads(
|
|
||||||
point(
|
|
||||||
element.points[i][0] + element.x,
|
|
||||||
element.points[i][1] + element.y,
|
|
||||||
),
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
pointRotateRads(
|
|
||||||
point(
|
|
||||||
element.points[i + 1][0] + element.x,
|
|
||||||
element.points[i + 1][1] + element.y,
|
|
||||||
),
|
|
||||||
center,
|
|
||||||
element.angle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [nw, ne, sw, se, n, s, w, e] = (
|
|
||||||
[
|
|
||||||
[x1, y1],
|
|
||||||
[x2, y1],
|
|
||||||
[x1, y2],
|
|
||||||
[x2, y2],
|
|
||||||
[cx, y1],
|
|
||||||
[cx, y2],
|
|
||||||
[x1, cy],
|
|
||||||
[x2, cy],
|
|
||||||
] as GlobalPoint[]
|
|
||||||
).map((point) => pointRotateRads(point, center, element.angle));
|
|
||||||
|
|
||||||
if (element.type === "diamond") {
|
|
||||||
return [
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.type === "ellipse") {
|
|
||||||
return [
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
lineSegment(n, w),
|
|
||||||
lineSegment(n, e),
|
|
||||||
lineSegment(s, w),
|
|
||||||
lineSegment(s, e),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
lineSegment(nw, ne),
|
|
||||||
lineSegment(sw, se),
|
|
||||||
lineSegment(nw, sw),
|
|
||||||
lineSegment(ne, se),
|
|
||||||
lineSegment(nw, e),
|
|
||||||
lineSegment(sw, e),
|
|
||||||
lineSegment(ne, w),
|
|
||||||
lineSegment(se, w),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
|
|
||||||
*
|
|
||||||
* Rectangle here means any rectangular frame, not an excalidraw element.
|
|
||||||
*/
|
|
||||||
export const getRectangleBoxAbsoluteCoords = (boxSceneCoords: RectangleBox) => {
|
|
||||||
return [
|
|
||||||
boxSceneCoords.x,
|
|
||||||
boxSceneCoords.y,
|
|
||||||
boxSceneCoords.x + boxSceneCoords.width,
|
|
||||||
boxSceneCoords.y + boxSceneCoords.height,
|
|
||||||
boxSceneCoords.x + boxSceneCoords.width / 2,
|
|
||||||
boxSceneCoords.y + boxSceneCoords.height / 2,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDiamondPoints = (element: ExcalidrawElement) => {
|
export const getDiamondPoints = (element: ExcalidrawElement) => {
|
||||||
// Here we add +1 to avoid these numbers to be 0
|
// Here we add +1 to avoid these numbers to be 0
|
||||||
// otherwise rough.js will throw an error complaining about it
|
// otherwise rough.js will throw an error complaining about it
|
||||||
|
@ -367,15 +240,6 @@ export const getDiamondPoints = (element: ExcalidrawElement) => {
|
||||||
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
return [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCurvePathOps = (shape: Drawable): Op[] => {
|
|
||||||
for (const set of shape.sets) {
|
|
||||||
if (set.type === "path") {
|
|
||||||
return set.ops;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return shape.sets[0].ops;
|
|
||||||
};
|
|
||||||
|
|
||||||
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
|
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
|
||||||
const getBezierValueForT = (
|
const getBezierValueForT = (
|
||||||
t: number,
|
t: number,
|
||||||
|
@ -548,171 +412,6 @@ const getFreeDrawElementAbsoluteCoords = (
|
||||||
return [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2];
|
return [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @returns number in pixels */
|
|
||||||
export const getArrowheadSize = (arrowhead: Arrowhead): number => {
|
|
||||||
switch (arrowhead) {
|
|
||||||
case "arrow":
|
|
||||||
return 25;
|
|
||||||
case "diamond":
|
|
||||||
case "diamond_outline":
|
|
||||||
return 12;
|
|
||||||
default:
|
|
||||||
return 15;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @returns number in degrees */
|
|
||||||
export const getArrowheadAngle = (arrowhead: Arrowhead): Degrees => {
|
|
||||||
switch (arrowhead) {
|
|
||||||
case "bar":
|
|
||||||
return 90 as Degrees;
|
|
||||||
case "arrow":
|
|
||||||
return 20 as Degrees;
|
|
||||||
default:
|
|
||||||
return 25 as Degrees;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getArrowheadPoints = (
|
|
||||||
element: ExcalidrawLinearElement,
|
|
||||||
shape: Drawable[],
|
|
||||||
position: "start" | "end",
|
|
||||||
arrowhead: Arrowhead,
|
|
||||||
) => {
|
|
||||||
const ops = getCurvePathOps(shape[0]);
|
|
||||||
if (ops.length < 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The index of the bCurve operation to examine.
|
|
||||||
const index = position === "start" ? 1 : ops.length - 1;
|
|
||||||
|
|
||||||
const data = ops[index].data;
|
|
||||||
|
|
||||||
invariant(data.length === 6, "Op data length is not 6");
|
|
||||||
|
|
||||||
const p3 = point(data[4], data[5]);
|
|
||||||
const p2 = point(data[2], data[3]);
|
|
||||||
const p1 = point(data[0], data[1]);
|
|
||||||
|
|
||||||
// We need to find p0 of the bezier curve.
|
|
||||||
// It is typically the last point of the previous
|
|
||||||
// curve; it can also be the position of moveTo operation.
|
|
||||||
const prevOp = ops[index - 1];
|
|
||||||
let p0 = point(0, 0);
|
|
||||||
if (prevOp.op === "move") {
|
|
||||||
const p = pointFromArray(prevOp.data);
|
|
||||||
invariant(p != null, "Op data is not a point");
|
|
||||||
p0 = p;
|
|
||||||
} else if (prevOp.op === "bcurveTo") {
|
|
||||||
p0 = point(prevOp.data[4], prevOp.data[5]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
|
|
||||||
const equation = (t: number, idx: number) =>
|
|
||||||
Math.pow(1 - t, 3) * p3[idx] +
|
|
||||||
3 * t * Math.pow(1 - t, 2) * p2[idx] +
|
|
||||||
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
|
|
||||||
p0[idx] * Math.pow(t, 3);
|
|
||||||
|
|
||||||
// Ee know the last point of the arrow (or the first, if start arrowhead).
|
|
||||||
const [x2, y2] = position === "start" ? p0 : p3;
|
|
||||||
|
|
||||||
// By using cubic bezier equation (B(t)) and the given parameters,
|
|
||||||
// we calculate a point that is closer to the last point.
|
|
||||||
// The value 0.3 is chosen arbitrarily and it works best for all
|
|
||||||
// the tested cases.
|
|
||||||
const [x1, y1] = [equation(0.3, 0), equation(0.3, 1)];
|
|
||||||
|
|
||||||
// Find the normalized direction vector based on the
|
|
||||||
// previously calculated points.
|
|
||||||
const distance = Math.hypot(x2 - x1, y2 - y1);
|
|
||||||
const nx = (x2 - x1) / distance;
|
|
||||||
const ny = (y2 - y1) / distance;
|
|
||||||
|
|
||||||
const size = getArrowheadSize(arrowhead);
|
|
||||||
|
|
||||||
let length = 0;
|
|
||||||
|
|
||||||
{
|
|
||||||
// Length for -> arrows is based on the length of the last section
|
|
||||||
const [cx, cy] =
|
|
||||||
position === "end"
|
|
||||||
? element.points[element.points.length - 1]
|
|
||||||
: element.points[0];
|
|
||||||
const [px, py] =
|
|
||||||
element.points.length > 1
|
|
||||||
? position === "end"
|
|
||||||
? element.points[element.points.length - 2]
|
|
||||||
: element.points[1]
|
|
||||||
: [0, 0];
|
|
||||||
|
|
||||||
length = Math.hypot(cx - px, cy - py);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale down the arrowhead until we hit a certain size so that it doesn't look weird.
|
|
||||||
// This value is selected by minimizing a minimum size with the last segment of the arrowhead
|
|
||||||
const lengthMultiplier =
|
|
||||||
arrowhead === "diamond" || arrowhead === "diamond_outline" ? 0.25 : 0.5;
|
|
||||||
const minSize = Math.min(size, length * lengthMultiplier);
|
|
||||||
const xs = x2 - nx * minSize;
|
|
||||||
const ys = y2 - ny * minSize;
|
|
||||||
|
|
||||||
if (
|
|
||||||
arrowhead === "dot" ||
|
|
||||||
arrowhead === "circle" ||
|
|
||||||
arrowhead === "circle_outline"
|
|
||||||
) {
|
|
||||||
const diameter = Math.hypot(ys - y2, xs - x2) + element.strokeWidth - 2;
|
|
||||||
return [x2, y2, diameter];
|
|
||||||
}
|
|
||||||
|
|
||||||
const angle = getArrowheadAngle(arrowhead);
|
|
||||||
|
|
||||||
// Return points
|
|
||||||
const [x3, y3] = pointRotateRads(
|
|
||||||
point(xs, ys),
|
|
||||||
point(x2, y2),
|
|
||||||
((-angle * Math.PI) / 180) as Radians,
|
|
||||||
);
|
|
||||||
const [x4, y4] = pointRotateRads(
|
|
||||||
point(xs, ys),
|
|
||||||
point(x2, y2),
|
|
||||||
degreesToRadians(angle),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (arrowhead === "diamond" || arrowhead === "diamond_outline") {
|
|
||||||
// point opposite to the arrowhead point
|
|
||||||
let ox;
|
|
||||||
let oy;
|
|
||||||
|
|
||||||
if (position === "start") {
|
|
||||||
const [px, py] = element.points.length > 1 ? element.points[1] : [0, 0];
|
|
||||||
|
|
||||||
[ox, oy] = pointRotateRads(
|
|
||||||
point(x2 + minSize * 2, y2),
|
|
||||||
point(x2, y2),
|
|
||||||
Math.atan2(py - y2, px - x2) as Radians,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const [px, py] =
|
|
||||||
element.points.length > 1
|
|
||||||
? element.points[element.points.length - 2]
|
|
||||||
: [0, 0];
|
|
||||||
|
|
||||||
[ox, oy] = pointRotateRads(
|
|
||||||
point(x2 - minSize * 2, y2),
|
|
||||||
point(x2, y2),
|
|
||||||
Math.atan2(y2 - py, x2 - px) as Radians,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [x2, y2, x3, y3, ox, oy, x4, y4];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [x2, y2, x3, y3, x4, y4];
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateLinearElementShape = (
|
const generateLinearElementShape = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
): Drawable => {
|
): Drawable => {
|
||||||
|
|
|
@ -20,7 +20,6 @@ export {
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
getArrowheadPoints,
|
|
||||||
getClosestElementBounds,
|
getClosestElementBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
|
|
||||||
|
@ -55,6 +54,7 @@ export {
|
||||||
getNormalizedDimensions,
|
getNormalizedDimensions,
|
||||||
} from "./sizeHelpers";
|
} from "./sizeHelpers";
|
||||||
export { showSelectedShapeActions } from "./showSelectedShapeActions";
|
export { showSelectedShapeActions } from "./showSelectedShapeActions";
|
||||||
|
export { getArrowheadPoints } from "./arrow";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated unsafe, use hashElementsVersion instead
|
* @deprecated unsafe, use hashElementsVersion instead
|
||||||
|
|
|
@ -13,11 +13,7 @@ import type {
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||||
import type { Bounds } from "./bounds";
|
import type { Bounds } from "./bounds";
|
||||||
import {
|
import { getElementPointsCoords, getMinMaxXYFromCurvePathOps } from "./bounds";
|
||||||
getCurvePathOps,
|
|
||||||
getElementPointsCoords,
|
|
||||||
getMinMaxXYFromCurvePathOps,
|
|
||||||
} from "./bounds";
|
|
||||||
import type {
|
import type {
|
||||||
AppState,
|
AppState,
|
||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
|
@ -66,6 +62,7 @@ import {
|
||||||
mapIntervalToBezierT,
|
mapIntervalToBezierT,
|
||||||
} from "../shapes";
|
} from "../shapes";
|
||||||
import { getGridPoint } from "../snapping";
|
import { getGridPoint } from "../snapping";
|
||||||
|
import { getCurvePathOps } from "../../utils/geometry/shape";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
|
|
|
@ -25,12 +25,22 @@ import type {
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||||
import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
import type { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
||||||
import { getElementLineSegments } from "./element/bounds";
|
import { elementsOverlappingBBox } from "../utils/";
|
||||||
import { doLineSegmentsIntersect, elementsOverlappingBBox } from "../utils/";
|
import {
|
||||||
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
isFrameElement,
|
||||||
|
isFrameLikeElement,
|
||||||
|
isFreeDrawElement,
|
||||||
|
isLinearElement,
|
||||||
|
} from "./element/typeChecks";
|
||||||
import type { ReadonlySetLike } from "./utility-types";
|
import type { ReadonlySetLike } from "./utility-types";
|
||||||
import type { GlobalPoint } from "../math";
|
import type { GlobalPoint, LineSegment } from "../math";
|
||||||
import { isPointWithinBounds, point } from "../math";
|
import {
|
||||||
|
isPointWithinBounds,
|
||||||
|
lineSegment,
|
||||||
|
point,
|
||||||
|
pointRotateRads,
|
||||||
|
segmentsIntersectAt,
|
||||||
|
} from "../math";
|
||||||
|
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
|
@ -64,6 +74,101 @@ export const bindElementsToFramesAfterDuplication = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* for a given element, `getElementLineSegments` returns line segments
|
||||||
|
* that can be used for visual collision detection (useful for frames)
|
||||||
|
* as opposed to bounding box collision detection
|
||||||
|
*/
|
||||||
|
const getElementLineSegments = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
): LineSegment<GlobalPoint>[] => {
|
||||||
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const center: GlobalPoint = point(cx, cy);
|
||||||
|
|
||||||
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
|
const segments: LineSegment<GlobalPoint>[] = [];
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < element.points.length - 1) {
|
||||||
|
segments.push(
|
||||||
|
lineSegment(
|
||||||
|
pointRotateRads(
|
||||||
|
point(
|
||||||
|
element.points[i][0] + element.x,
|
||||||
|
element.points[i][1] + element.y,
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
pointRotateRads(
|
||||||
|
point(
|
||||||
|
element.points[i + 1][0] + element.x,
|
||||||
|
element.points[i + 1][1] + element.y,
|
||||||
|
),
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [nw, ne, sw, se, n, s, w, e] = (
|
||||||
|
[
|
||||||
|
[x1, y1],
|
||||||
|
[x2, y1],
|
||||||
|
[x1, y2],
|
||||||
|
[x2, y2],
|
||||||
|
[cx, y1],
|
||||||
|
[cx, y2],
|
||||||
|
[x1, cy],
|
||||||
|
[x2, cy],
|
||||||
|
] as GlobalPoint[]
|
||||||
|
).map((point) => pointRotateRads(point, center, element.angle));
|
||||||
|
|
||||||
|
if (element.type === "diamond") {
|
||||||
|
return [
|
||||||
|
lineSegment(n, w),
|
||||||
|
lineSegment(n, e),
|
||||||
|
lineSegment(s, w),
|
||||||
|
lineSegment(s, e),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.type === "ellipse") {
|
||||||
|
return [
|
||||||
|
lineSegment(n, w),
|
||||||
|
lineSegment(n, e),
|
||||||
|
lineSegment(s, w),
|
||||||
|
lineSegment(s, e),
|
||||||
|
lineSegment(n, w),
|
||||||
|
lineSegment(n, e),
|
||||||
|
lineSegment(s, w),
|
||||||
|
lineSegment(s, e),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
lineSegment(nw, ne),
|
||||||
|
lineSegment(sw, se),
|
||||||
|
lineSegment(nw, sw),
|
||||||
|
lineSegment(ne, se),
|
||||||
|
lineSegment(nw, e),
|
||||||
|
lineSegment(sw, e),
|
||||||
|
lineSegment(ne, w),
|
||||||
|
lineSegment(se, w),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
export function isElementIntersectingFrame(
|
export function isElementIntersectingFrame(
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
|
@ -75,7 +180,7 @@ export function isElementIntersectingFrame(
|
||||||
|
|
||||||
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
||||||
elementLineSegments.some((elementLineSegment) =>
|
elementLineSegments.some((elementLineSegment) =>
|
||||||
doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
segmentsIntersectAt(frameLineSegment, elementLineSegment),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
import type { Point as RoughPoint } from "roughjs/bin/geometry";
|
||||||
import type { Drawable, Options } from "roughjs/bin/core";
|
import type { Drawable, Options } from "roughjs/bin/core";
|
||||||
import type { RoughGenerator } from "roughjs/bin/generator";
|
import type { RoughGenerator } from "roughjs/bin/generator";
|
||||||
import { getDiamondPoints, getArrowheadPoints } from "../element";
|
import { getArrowheadPoints, getDiamondPoints } from "../element";
|
||||||
import type { ElementShapes } from "./types";
|
import type { ElementShapes } from "./types";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
|
22
packages/math/segment.test.ts
Normal file
22
packages/math/segment.test.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { point } from "./point";
|
||||||
|
import { lineSegment, segmentsIntersectAt } from "./segment";
|
||||||
|
import type { GlobalPoint, LineSegment } from "./types";
|
||||||
|
|
||||||
|
describe("segment intersects segment", () => {
|
||||||
|
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
|
||||||
|
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
|
||||||
|
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||||
|
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
||||||
|
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
|
||||||
|
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
|
||||||
|
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
|
||||||
|
|
||||||
|
it("intersection", () => {
|
||||||
|
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
|
||||||
|
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
|
||||||
|
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
|
||||||
|
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
|
||||||
|
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
|
||||||
|
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,71 +0,0 @@
|
||||||
import type { LineSegment } from "../math";
|
|
||||||
import {
|
|
||||||
vectorCross,
|
|
||||||
vectorFromPoint,
|
|
||||||
type GlobalPoint,
|
|
||||||
type LocalPoint,
|
|
||||||
} from "../math";
|
|
||||||
import type { Bounds } from "../excalidraw/element/bounds";
|
|
||||||
|
|
||||||
export function getBBox<P extends LocalPoint | GlobalPoint>(
|
|
||||||
line: LineSegment<P>,
|
|
||||||
): Bounds {
|
|
||||||
return [
|
|
||||||
Math.min(line[0][0], line[1][0]),
|
|
||||||
Math.min(line[0][1], line[1][1]),
|
|
||||||
Math.max(line[0][0], line[1][0]),
|
|
||||||
Math.max(line[0][1], line[1][1]),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doBBoxesIntersect(a: Bounds, b: Bounds) {
|
|
||||||
return a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
const EPSILON = 0.000001;
|
|
||||||
|
|
||||||
export function isPointOnLine<P extends GlobalPoint | LocalPoint>(
|
|
||||||
l: LineSegment<P>,
|
|
||||||
p: P,
|
|
||||||
) {
|
|
||||||
const p1 = vectorFromPoint(l[1], l[0]);
|
|
||||||
const p2 = vectorFromPoint(p, l[0]);
|
|
||||||
|
|
||||||
const r = vectorCross(p1, p2);
|
|
||||||
|
|
||||||
return Math.abs(r) < EPSILON;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPointRightOfLine<P extends GlobalPoint | LocalPoint>(
|
|
||||||
l: LineSegment<P>,
|
|
||||||
p: P,
|
|
||||||
) {
|
|
||||||
const p1 = vectorFromPoint(l[1], l[0]);
|
|
||||||
const p2 = vectorFromPoint(p, l[0]);
|
|
||||||
|
|
||||||
return vectorCross(p1, p2) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLineSegmentTouchingOrCrossingLine<
|
|
||||||
P extends GlobalPoint | LocalPoint,
|
|
||||||
>(a: LineSegment<P>, b: LineSegment<P>) {
|
|
||||||
return (
|
|
||||||
isPointOnLine(a, b[0]) ||
|
|
||||||
isPointOnLine(a, b[1]) ||
|
|
||||||
(isPointRightOfLine(a, b[0])
|
|
||||||
? !isPointRightOfLine(a, b[1])
|
|
||||||
: isPointRightOfLine(a, b[1]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
|
||||||
export function doLineSegmentsIntersect<P extends GlobalPoint | LocalPoint>(
|
|
||||||
a: LineSegment<P>,
|
|
||||||
b: LineSegment<P>,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
doBBoxesIntersect(getBBox(a), getBBox(b)) &&
|
|
||||||
isLineSegmentTouchingOrCrossingLine(a, b) &&
|
|
||||||
isLineSegmentTouchingOrCrossingLine(b, a)
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -89,22 +89,3 @@ describe("point and ellipse", () => {
|
||||||
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
|
expect(pointInEllipse(point(-1.4, 0.8), ellipse)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("line and line", () => {
|
|
||||||
const lineA: LineSegment<GlobalPoint> = lineSegment(point(1, 4), point(3, 4));
|
|
||||||
const lineB: LineSegment<GlobalPoint> = lineSegment(point(2, 1), point(2, 7));
|
|
||||||
const lineC: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
|
||||||
const lineD: LineSegment<GlobalPoint> = lineSegment(point(1, 8), point(3, 8));
|
|
||||||
const lineE: LineSegment<GlobalPoint> = lineSegment(point(1, 9), point(3, 9));
|
|
||||||
const lineF: LineSegment<GlobalPoint> = lineSegment(point(1, 2), point(3, 4));
|
|
||||||
const lineG: LineSegment<GlobalPoint> = lineSegment(point(0, 1), point(2, 3));
|
|
||||||
|
|
||||||
it("intersection", () => {
|
|
||||||
expect(segmentsIntersectAt(lineA, lineB)).toEqual([2, 4]);
|
|
||||||
expect(segmentsIntersectAt(lineA, lineC)).toBe(null);
|
|
||||||
expect(segmentsIntersectAt(lineB, lineC)).toBe(null);
|
|
||||||
expect(segmentsIntersectAt(lineC, lineD)).toBe(null); // Line overlapping line is not intersection!
|
|
||||||
expect(segmentsIntersectAt(lineE, lineD)).toBe(null);
|
|
||||||
expect(segmentsIntersectAt(lineF, lineG)).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from "./export";
|
export * from "./export";
|
||||||
export * from "./withinBounds";
|
export * from "./withinBounds";
|
||||||
export * from "./bbox";
|
|
||||||
export { getCommonBounds } from "../excalidraw/element/bounds";
|
export { getCommonBounds } from "../excalidraw/element/bounds";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue