123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- /**
- * this file defines pure geometric shapes
- *
- * for instance, a cubic bezier curve is specified by its four control points and
- * an ellipse is defined by its center, angle, semi major axis and semi minor axis
- * (but in semi-width and semi-height so it's more relevant to Excalidraw)
- *
- * the idea with pure shapes is so that we can provide collision and other geoemtric methods not depending on
- * the specifics of roughjs or elements in Excalidraw; instead, we can focus on the pure shapes themselves
- *
- * also included in this file are methods for converting an Excalidraw element or a Drawable from roughjs
- * to pure shapes
- */
- import type { Curve, LineSegment, Polygon, Radians } from "../../math";
- import {
- curve,
- lineSegment,
- point,
- pointDistance,
- pointFromArray,
- pointFromVector,
- pointRotateRads,
- polygon,
- polygonFromPoints,
- PRECISION,
- segmentsIntersectAt,
- vector,
- vectorAdd,
- vectorFromPoint,
- vectorScale,
- type GlobalPoint,
- type LocalPoint,
- } from "../../math";
- import { getElementAbsoluteCoords } from "../../excalidraw/element";
- import type {
- ElementsMap,
- ExcalidrawBindableElement,
- ExcalidrawDiamondElement,
- ExcalidrawElement,
- ExcalidrawEllipseElement,
- ExcalidrawEmbeddableElement,
- ExcalidrawFrameLikeElement,
- ExcalidrawFreeDrawElement,
- ExcalidrawIframeElement,
- ExcalidrawImageElement,
- ExcalidrawLinearElement,
- ExcalidrawRectangleElement,
- ExcalidrawSelectionElement,
- ExcalidrawTextElement,
- } from "../../excalidraw/element/types";
- import { pointsOnBezierCurves } from "points-on-curve";
- import type { Drawable, Op } from "roughjs/bin/core";
- import { invariant } from "../../excalidraw/utils";
- // a polyline (made up term here) is a line consisting of other line segments
- // this corresponds to a straight line element in the editor but it could also
- // be used to model other elements
- export type Polyline<Point extends GlobalPoint | LocalPoint> =
- LineSegment<Point>[];
- // a polycurve is a curve consisting of ther curves, this corresponds to a complex
- // curve on the canvas
- export type Polycurve<Point extends GlobalPoint | LocalPoint> = Curve<Point>[];
- // an ellipse is specified by its center, angle, and its major and minor axes
- // but for the sake of simplicity, we've used halfWidth and halfHeight instead
- // in replace of semi major and semi minor axes
- export type Ellipse<Point extends GlobalPoint | LocalPoint> = {
- center: Point;
- angle: Radians;
- halfWidth: number;
- halfHeight: number;
- };
- export type GeometricShape<Point extends GlobalPoint | LocalPoint> =
- | {
- type: "line";
- data: LineSegment<Point>;
- }
- | {
- type: "polygon";
- data: Polygon<Point>;
- }
- | {
- type: "curve";
- data: Curve<Point>;
- }
- | {
- type: "ellipse";
- data: Ellipse<Point>;
- }
- | {
- type: "polyline";
- data: Polyline<Point>;
- }
- | {
- type: "polycurve";
- data: Polycurve<Point>;
- };
- type RectangularElement =
- | ExcalidrawRectangleElement
- | ExcalidrawDiamondElement
- | ExcalidrawFrameLikeElement
- | ExcalidrawEmbeddableElement
- | ExcalidrawImageElement
- | ExcalidrawIframeElement
- | ExcalidrawTextElement
- | ExcalidrawSelectionElement;
- // polygon
- export const getPolygonShape = <Point extends GlobalPoint | LocalPoint>(
- element: RectangularElement,
- ): GeometricShape<Point> => {
- const { angle, width, height, x, y } = element;
- const cx = x + width / 2;
- const cy = y + height / 2;
- const center: Point = point(cx, cy);
- let data: Polygon<Point>;
- if (element.type === "diamond") {
- data = polygon(
- pointRotateRads(point(cx, y), center, angle),
- pointRotateRads(point(x + width, cy), center, angle),
- pointRotateRads(point(cx, y + height), center, angle),
- pointRotateRads(point(x, cy), center, angle),
- );
- } else {
- data = polygon(
- pointRotateRads(point(x, y), center, angle),
- pointRotateRads(point(x + width, y), center, angle),
- pointRotateRads(point(x + width, y + height), center, angle),
- pointRotateRads(point(x, y + height), center, angle),
- );
- }
- return {
- type: "polygon",
- data,
- };
- };
- // return the selection box for an element, possibly rotated as well
- export const getSelectionBoxShape = <Point extends GlobalPoint | LocalPoint>(
- element: ExcalidrawElement,
- elementsMap: ElementsMap,
- padding = 10,
- ) => {
- let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
- element,
- elementsMap,
- true,
- );
- x1 -= padding;
- x2 += padding;
- y1 -= padding;
- y2 += padding;
- //const angleInDegrees = angleToDegrees(element.angle);
- const center = point(cx, cy);
- const topLeft = pointRotateRads(point(x1, y1), center, element.angle);
- const topRight = pointRotateRads(point(x2, y1), center, element.angle);
- const bottomLeft = pointRotateRads(point(x1, y2), center, element.angle);
- const bottomRight = pointRotateRads(point(x2, y2), center, element.angle);
- return {
- type: "polygon",
- data: [topLeft, topRight, bottomRight, bottomLeft],
- } as GeometricShape<Point>;
- };
- // ellipse
- export const getEllipseShape = <Point extends GlobalPoint | LocalPoint>(
- element: ExcalidrawEllipseElement,
- ): GeometricShape<Point> => {
- const { width, height, angle, x, y } = element;
- return {
- type: "ellipse",
- data: {
- center: point(x + width / 2, y + height / 2),
- angle,
- halfWidth: width / 2,
- halfHeight: height / 2,
- },
- };
- };
- export const getCurvePathOps = (shape: Drawable): Op[] => {
- for (const set of shape.sets) {
- if (set.type === "path") {
- return set.ops;
- }
- }
- return shape.sets[0].ops;
- };
- // linear
- export const getCurveShape = <Point extends GlobalPoint | LocalPoint>(
- roughShape: Drawable,
- startingPoint: Point = point(0, 0),
- angleInRadian: Radians,
- center: Point,
- ): GeometricShape<Point> => {
- const transform = (p: Point): Point =>
- pointRotateRads(
- point(p[0] + startingPoint[0], p[1] + startingPoint[1]),
- center,
- angleInRadian,
- );
- const ops = getCurvePathOps(roughShape);
- const polycurve: Polycurve<Point> = [];
- let p0 = point<Point>(0, 0);
- for (const op of ops) {
- if (op.op === "move") {
- const p = pointFromArray<Point>(op.data);
- invariant(p != null, "Ops data is not a point");
- p0 = transform(p);
- }
- if (op.op === "bcurveTo") {
- const p1 = transform(point<Point>(op.data[0], op.data[1]));
- const p2 = transform(point<Point>(op.data[2], op.data[3]));
- const p3 = transform(point<Point>(op.data[4], op.data[5]));
- polycurve.push(curve<Point>(p0, p1, p2, p3));
- p0 = p3;
- }
- }
- return {
- type: "polycurve",
- data: polycurve,
- };
- };
- const polylineFromPoints = <Point extends GlobalPoint | LocalPoint>(
- points: Point[],
- ): Polyline<Point> => {
- let previousPoint: Point = points[0];
- const polyline: LineSegment<Point>[] = [];
- for (let i = 1; i < points.length; i++) {
- const nextPoint = points[i];
- polyline.push(lineSegment<Point>(previousPoint, nextPoint));
- previousPoint = nextPoint;
- }
- return polyline;
- };
- export const getFreedrawShape = <Point extends GlobalPoint | LocalPoint>(
- element: ExcalidrawFreeDrawElement,
- center: Point,
- isClosed: boolean = false,
- ): GeometricShape<Point> => {
- const transform = (p: Point) =>
- pointRotateRads(
- pointFromVector(
- vectorAdd(vectorFromPoint(p), vector(element.x, element.y)),
- ),
- center,
- element.angle,
- );
- const polyline = polylineFromPoints(
- element.points.map((p) => transform(p as Point)),
- );
- return (
- isClosed
- ? {
- type: "polygon",
- data: polygonFromPoints(polyline.flat()),
- }
- : {
- type: "polyline",
- data: polyline,
- }
- ) as GeometricShape<Point>;
- };
- export const getClosedCurveShape = <Point extends GlobalPoint | LocalPoint>(
- element: ExcalidrawLinearElement,
- roughShape: Drawable,
- startingPoint: Point = point<Point>(0, 0),
- angleInRadian: Radians,
- center: Point,
- ): GeometricShape<Point> => {
- const transform = (p: Point) =>
- pointRotateRads(
- point(p[0] + startingPoint[0], p[1] + startingPoint[1]),
- center,
- angleInRadian,
- );
- if (element.roundness === null) {
- return {
- type: "polygon",
- data: polygonFromPoints(
- element.points.map((p) => transform(p as Point)) as Point[],
- ),
- };
- }
- const ops = getCurvePathOps(roughShape);
- const points: Point[] = [];
- let odd = false;
- for (const operation of ops) {
- if (operation.op === "move") {
- odd = !odd;
- if (odd) {
- points.push(point(operation.data[0], operation.data[1]));
- }
- } else if (operation.op === "bcurveTo") {
- if (odd) {
- points.push(point(operation.data[0], operation.data[1]));
- points.push(point(operation.data[2], operation.data[3]));
- points.push(point(operation.data[4], operation.data[5]));
- }
- } else if (operation.op === "lineTo") {
- if (odd) {
- points.push(point(operation.data[0], operation.data[1]));
- }
- }
- }
- const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
- transform(p as Point),
- ) as Point[];
- return {
- type: "polygon",
- data: polygonFromPoints<Point>(polygonPoints),
- };
- };
- /**
- * Determine intersection of a rectangular shaped element and a
- * line segment.
- *
- * @param element The rectangular element to test against
- * @param segment The segment intersecting the element
- * @param gap Optional value to inflate the shape before testing
- * @returns An array of intersections
- */
- // TODO: Replace with final rounded rectangle code
- export const segmentIntersectRectangleElement = <
- Point extends LocalPoint | GlobalPoint,
- >(
- element: ExcalidrawBindableElement,
- segment: LineSegment<Point>,
- gap: number = 0,
- ): Point[] => {
- const bounds = [
- element.x - gap,
- element.y - gap,
- element.x + element.width + gap,
- element.y + element.height + gap,
- ];
- const center = point(
- (bounds[0] + bounds[2]) / 2,
- (bounds[1] + bounds[3]) / 2,
- );
- return [
- lineSegment(
- pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
- pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
- ),
- lineSegment(
- pointRotateRads(point(bounds[2], bounds[1]), center, element.angle),
- pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
- ),
- lineSegment(
- pointRotateRads(point(bounds[2], bounds[3]), center, element.angle),
- pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
- ),
- lineSegment(
- pointRotateRads(point(bounds[0], bounds[3]), center, element.angle),
- pointRotateRads(point(bounds[0], bounds[1]), center, element.angle),
- ),
- ]
- .map((s) => segmentsIntersectAt(segment, s))
- .filter((i): i is Point => !!i);
- };
- const distanceToEllipse = <Point extends LocalPoint | GlobalPoint>(
- p: Point,
- ellipse: Ellipse<Point>,
- ) => {
- const { angle, halfWidth, halfHeight, center } = ellipse;
- const a = halfWidth;
- const b = halfHeight;
- const translatedPoint = vectorAdd(
- vectorFromPoint(p),
- vectorScale(vectorFromPoint(center), -1),
- );
- const [rotatedPointX, rotatedPointY] = pointRotateRads(
- pointFromVector(translatedPoint),
- point(0, 0),
- -angle as Radians,
- );
- const px = Math.abs(rotatedPointX);
- const py = Math.abs(rotatedPointY);
- let tx = 0.707;
- let ty = 0.707;
- for (let i = 0; i < 3; i++) {
- const x = a * tx;
- const y = b * ty;
- const ex = ((a * a - b * b) * tx ** 3) / a;
- const ey = ((b * b - a * a) * ty ** 3) / b;
- const rx = x - ex;
- const ry = y - ey;
- const qx = px - ex;
- const qy = py - ey;
- const r = Math.hypot(ry, rx);
- const q = Math.hypot(qy, qx);
- tx = Math.min(1, Math.max(0, ((qx * r) / q + ex) / a));
- ty = Math.min(1, Math.max(0, ((qy * r) / q + ey) / b));
- const t = Math.hypot(ty, tx);
- tx /= t;
- ty /= t;
- }
- const [minX, minY] = [
- a * tx * Math.sign(rotatedPointX),
- b * ty * Math.sign(rotatedPointY),
- ];
- return pointDistance(point(rotatedPointX, rotatedPointY), point(minX, minY));
- };
- export const pointOnEllipse = <Point extends LocalPoint | GlobalPoint>(
- point: Point,
- ellipse: Ellipse<Point>,
- threshold = PRECISION,
- ) => {
- return distanceToEllipse(point, ellipse) <= threshold;
- };
- export const pointInEllipse = <Point extends LocalPoint | GlobalPoint>(
- p: Point,
- ellipse: Ellipse<Point>,
- ) => {
- const { center, angle, halfWidth, halfHeight } = ellipse;
- const translatedPoint = vectorAdd(
- vectorFromPoint(p),
- vectorScale(vectorFromPoint(center), -1),
- );
- const [rotatedPointX, rotatedPointY] = pointRotateRads(
- pointFromVector(translatedPoint),
- point(0, 0),
- -angle as Radians,
- );
- return (
- (rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
- (rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
- 1
- );
- };
- export const ellipseAxes = <Point extends LocalPoint | GlobalPoint>(
- ellipse: Ellipse<Point>,
- ) => {
- const widthGreaterThanHeight = ellipse.halfWidth > ellipse.halfHeight;
- const majorAxis = widthGreaterThanHeight
- ? ellipse.halfWidth * 2
- : ellipse.halfHeight * 2;
- const minorAxis = widthGreaterThanHeight
- ? ellipse.halfHeight * 2
- : ellipse.halfWidth * 2;
- return {
- majorAxis,
- minorAxis,
- };
- };
- export const ellipseFocusToCenter = <Point extends LocalPoint | GlobalPoint>(
- ellipse: Ellipse<Point>,
- ) => {
- const { majorAxis, minorAxis } = ellipseAxes(ellipse);
- return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
- };
- export const ellipseExtremes = <Point extends LocalPoint | GlobalPoint>(
- ellipse: Ellipse<Point>,
- ) => {
- const { center, angle } = ellipse;
- const { majorAxis, minorAxis } = ellipseAxes(ellipse);
- const cos = Math.cos(angle);
- const sin = Math.sin(angle);
- const sqSum = majorAxis ** 2 + minorAxis ** 2;
- const sqDiff = (majorAxis ** 2 - minorAxis ** 2) * Math.cos(2 * angle);
- const yMax = Math.sqrt((sqSum - sqDiff) / 2);
- const xAtYMax =
- (yMax * sqSum * sin * cos) /
- (majorAxis ** 2 * sin ** 2 + minorAxis ** 2 * cos ** 2);
- const xMax = Math.sqrt((sqSum + sqDiff) / 2);
- const yAtXMax =
- (xMax * sqSum * sin * cos) /
- (majorAxis ** 2 * cos ** 2 + minorAxis ** 2 * sin ** 2);
- const centerVector = vectorFromPoint(center);
- return [
- vectorAdd(vector(xAtYMax, yMax), centerVector),
- vectorAdd(vectorScale(vector(xAtYMax, yMax), -1), centerVector),
- vectorAdd(vector(xMax, yAtXMax), centerVector),
- vectorAdd(vector(xMax, yAtXMax), centerVector),
- ];
- };
|