mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-03 20:34:40 +01:00 
			
		
		
		
	refactor: update collision from ga to vector geometry (#7636)
* new collision api * isPointOnShape * removed redundant code * new collision methods in app * curve shape takes starting point * clean up geometry * curve rotation * freedraw * inside curve * improve ellipse inside check * ellipse distance func * curve inside * include frame name bounds * replace previous private methods for getting elements at x,y * arrow bound text hit detection * keep iframes on top * remove dependence on old collision methods from app * remove old collision functions * move some hit functions outside of app * code refactor * type * text collision from inside * fix context menu test * highest z-index collision * fix 1px away binding test * strictly less * remove unused imports * lint * 'ignore' resize flipping test * more lint fix * skip 'flips while resizing' test * more test * fix merge errors * fix selection in resize test * added a bit more comment --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										66
									
								
								packages/utils/collision.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								packages/utils/collision.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
import { Point, Polygon, GeometricShape } from "./geometry/shape";
 | 
			
		||||
import {
 | 
			
		||||
  pointInEllipse,
 | 
			
		||||
  pointInPolygon,
 | 
			
		||||
  pointOnCurve,
 | 
			
		||||
  pointOnEllipse,
 | 
			
		||||
  pointOnLine,
 | 
			
		||||
  pointOnPolycurve,
 | 
			
		||||
  pointOnPolygon,
 | 
			
		||||
  pointOnPolyline,
 | 
			
		||||
  close,
 | 
			
		||||
} from "./geometry/geometry";
 | 
			
		||||
 | 
			
		||||
// check if the given point is considered on the given shape's border
 | 
			
		||||
export const isPointOnShape = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  shape: GeometricShape,
 | 
			
		||||
  tolerance = 0,
 | 
			
		||||
) => {
 | 
			
		||||
  // get the distance from the given point to the given element
 | 
			
		||||
  // check if the distance is within the given epsilon range
 | 
			
		||||
  switch (shape.type) {
 | 
			
		||||
    case "polygon":
 | 
			
		||||
      return pointOnPolygon(point, shape.data, tolerance);
 | 
			
		||||
    case "ellipse":
 | 
			
		||||
      return pointOnEllipse(point, shape.data, tolerance);
 | 
			
		||||
    case "line":
 | 
			
		||||
      return pointOnLine(point, shape.data, tolerance);
 | 
			
		||||
    case "polyline":
 | 
			
		||||
      return pointOnPolyline(point, shape.data, tolerance);
 | 
			
		||||
    case "curve":
 | 
			
		||||
      return pointOnCurve(point, shape.data, tolerance);
 | 
			
		||||
    case "polycurve":
 | 
			
		||||
      return pointOnPolycurve(point, shape.data, tolerance);
 | 
			
		||||
    default:
 | 
			
		||||
      throw Error(`shape ${shape} is not implemented`);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// check if the given point is considered inside the element's border
 | 
			
		||||
export const isPointInShape = (point: Point, shape: GeometricShape) => {
 | 
			
		||||
  switch (shape.type) {
 | 
			
		||||
    case "polygon":
 | 
			
		||||
      return pointInPolygon(point, shape.data);
 | 
			
		||||
    case "line":
 | 
			
		||||
      return false;
 | 
			
		||||
    case "curve":
 | 
			
		||||
      return false;
 | 
			
		||||
    case "ellipse":
 | 
			
		||||
      return pointInEllipse(point, shape.data);
 | 
			
		||||
    case "polyline": {
 | 
			
		||||
      const polygon = close(shape.data.flat()) as Polygon;
 | 
			
		||||
      return pointInPolygon(point, polygon);
 | 
			
		||||
    }
 | 
			
		||||
    case "polycurve": {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      throw Error(`shape ${shape} is not implemented`);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// check if the given element is in the given bounds
 | 
			
		||||
export const isPointInBounds = (point: Point, bounds: Polygon) => {
 | 
			
		||||
  return pointInPolygon(point, bounds);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										249
									
								
								packages/utils/geometry/geometry.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								packages/utils/geometry/geometry.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,249 @@
 | 
			
		||||
import {
 | 
			
		||||
  lineIntersectsLine,
 | 
			
		||||
  lineRotate,
 | 
			
		||||
  pointInEllipse,
 | 
			
		||||
  pointInPolygon,
 | 
			
		||||
  pointLeftofLine,
 | 
			
		||||
  pointOnCurve,
 | 
			
		||||
  pointOnEllipse,
 | 
			
		||||
  pointOnLine,
 | 
			
		||||
  pointOnPolygon,
 | 
			
		||||
  pointOnPolyline,
 | 
			
		||||
  pointRightofLine,
 | 
			
		||||
  pointRotate,
 | 
			
		||||
} from "./geometry";
 | 
			
		||||
import { Curve, Ellipse, Line, Point, Polygon, Polyline } from "./shape";
 | 
			
		||||
 | 
			
		||||
describe("point and line", () => {
 | 
			
		||||
  const line: Line = [
 | 
			
		||||
    [1, 0],
 | 
			
		||||
    [1, 2],
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  it("point on left or right of line", () => {
 | 
			
		||||
    expect(pointLeftofLine([0, 1], line)).toBe(true);
 | 
			
		||||
    expect(pointLeftofLine([1, 1], line)).toBe(false);
 | 
			
		||||
    expect(pointLeftofLine([2, 1], line)).toBe(false);
 | 
			
		||||
 | 
			
		||||
    expect(pointRightofLine([0, 1], line)).toBe(false);
 | 
			
		||||
    expect(pointRightofLine([1, 1], line)).toBe(false);
 | 
			
		||||
    expect(pointRightofLine([2, 1], line)).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("point on the line", () => {
 | 
			
		||||
    expect(pointOnLine([0, 1], line)).toBe(false);
 | 
			
		||||
    expect(pointOnLine([1, 1], line, 0)).toBe(true);
 | 
			
		||||
    expect(pointOnLine([2, 1], line)).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("point and polylines", () => {
 | 
			
		||||
  const polyline: Polyline = [
 | 
			
		||||
    [
 | 
			
		||||
      [1, 0],
 | 
			
		||||
      [1, 2],
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      [1, 2],
 | 
			
		||||
      [2, 2],
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      [2, 2],
 | 
			
		||||
      [2, 1],
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      [2, 1],
 | 
			
		||||
      [3, 1],
 | 
			
		||||
    ],
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  it("point on the line", () => {
 | 
			
		||||
    expect(pointOnPolyline([1, 0], polyline)).toBe(true);
 | 
			
		||||
    expect(pointOnPolyline([1, 2], polyline)).toBe(true);
 | 
			
		||||
    expect(pointOnPolyline([2, 2], polyline)).toBe(true);
 | 
			
		||||
    expect(pointOnPolyline([2, 1], polyline)).toBe(true);
 | 
			
		||||
    expect(pointOnPolyline([3, 1], polyline)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointOnPolyline([1, 1], polyline)).toBe(true);
 | 
			
		||||
    expect(pointOnPolyline([2, 1.5], polyline)).toBe(true);
 | 
			
		||||
    expect(pointOnPolyline([2.5, 1], polyline)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointOnPolyline([0, 1], polyline)).toBe(false);
 | 
			
		||||
    expect(pointOnPolyline([2.1, 1.5], polyline)).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("point on the line with rotation", () => {
 | 
			
		||||
    const truePoints = [
 | 
			
		||||
      [1, 0],
 | 
			
		||||
      [1, 2],
 | 
			
		||||
      [2, 2],
 | 
			
		||||
      [2, 1],
 | 
			
		||||
      [3, 1],
 | 
			
		||||
    ] as Point[];
 | 
			
		||||
 | 
			
		||||
    truePoints.forEach((point) => {
 | 
			
		||||
      const rotation = Math.random() * 360;
 | 
			
		||||
      const rotatedPoint = pointRotate(point, rotation);
 | 
			
		||||
      const rotatedPolyline: Polyline = polyline.map((line) =>
 | 
			
		||||
        lineRotate(line, rotation, [0, 0]),
 | 
			
		||||
      );
 | 
			
		||||
      expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const falsePoints = [
 | 
			
		||||
      [0, 1],
 | 
			
		||||
      [2.1, 1.5],
 | 
			
		||||
    ] as Point[];
 | 
			
		||||
 | 
			
		||||
    falsePoints.forEach((point) => {
 | 
			
		||||
      const rotation = Math.random() * 360;
 | 
			
		||||
      const rotatedPoint = pointRotate(point, rotation);
 | 
			
		||||
      const rotatedPolyline: Polyline = polyline.map((line) =>
 | 
			
		||||
        lineRotate(line, rotation, [0, 0]),
 | 
			
		||||
      );
 | 
			
		||||
      expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("point and polygon", () => {
 | 
			
		||||
  const polygon: Polygon = [
 | 
			
		||||
    [10, 10],
 | 
			
		||||
    [50, 10],
 | 
			
		||||
    [50, 50],
 | 
			
		||||
    [10, 50],
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  it("point on polygon", () => {
 | 
			
		||||
    expect(pointOnPolygon([30, 10], polygon)).toBe(true);
 | 
			
		||||
    expect(pointOnPolygon([50, 30], polygon)).toBe(true);
 | 
			
		||||
    expect(pointOnPolygon([30, 50], polygon)).toBe(true);
 | 
			
		||||
    expect(pointOnPolygon([10, 30], polygon)).toBe(true);
 | 
			
		||||
    expect(pointOnPolygon([30, 30], polygon)).toBe(false);
 | 
			
		||||
    expect(pointOnPolygon([30, 70], polygon)).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("point in polygon", () => {
 | 
			
		||||
    const polygon: Polygon = [
 | 
			
		||||
      [0, 0],
 | 
			
		||||
      [2, 0],
 | 
			
		||||
      [2, 2],
 | 
			
		||||
      [0, 2],
 | 
			
		||||
    ];
 | 
			
		||||
    expect(pointInPolygon([1, 1], polygon)).toBe(true);
 | 
			
		||||
    expect(pointInPolygon([3, 3], polygon)).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("point and curve", () => {
 | 
			
		||||
  const curve: Curve = [
 | 
			
		||||
    [1.4, 1.65],
 | 
			
		||||
    [1.9, 7.9],
 | 
			
		||||
    [5.9, 1.65],
 | 
			
		||||
    [6.44, 4.84],
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  it("point on curve", () => {
 | 
			
		||||
    expect(pointOnCurve(curve[0], curve)).toBe(true);
 | 
			
		||||
    expect(pointOnCurve(curve[3], curve)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointOnCurve([2, 4], curve, 0.1)).toBe(true);
 | 
			
		||||
    expect(pointOnCurve([4, 4.4], curve, 0.1)).toBe(true);
 | 
			
		||||
    expect(pointOnCurve([5.6, 3.85], curve, 0.1)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointOnCurve([5.6, 4], curve, 0.1)).toBe(false);
 | 
			
		||||
    expect(pointOnCurve(curve[1], curve, 0.1)).toBe(false);
 | 
			
		||||
    expect(pointOnCurve(curve[2], curve, 0.1)).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("point and ellipse", () => {
 | 
			
		||||
  const ellipse: Ellipse = {
 | 
			
		||||
    center: [0, 0],
 | 
			
		||||
    angle: 0,
 | 
			
		||||
    halfWidth: 2,
 | 
			
		||||
    halfHeight: 1,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  it("point on ellipse", () => {
 | 
			
		||||
    [
 | 
			
		||||
      [0, 1],
 | 
			
		||||
      [0, -1],
 | 
			
		||||
      [2, 0],
 | 
			
		||||
      [-2, 0],
 | 
			
		||||
    ].forEach((point) => {
 | 
			
		||||
      expect(pointOnEllipse(point as Point, ellipse)).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
    expect(pointOnEllipse([-1.4, 0.7], ellipse, 0.1)).toBe(true);
 | 
			
		||||
    expect(pointOnEllipse([-1.4, 0.71], ellipse, 0.01)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointOnEllipse([1.4, 0.7], ellipse, 0.1)).toBe(true);
 | 
			
		||||
    expect(pointOnEllipse([1.4, 0.71], ellipse, 0.01)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointOnEllipse([1, -0.86], ellipse, 0.1)).toBe(true);
 | 
			
		||||
    expect(pointOnEllipse([1, -0.86], ellipse, 0.01)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointOnEllipse([-1, -0.86], ellipse, 0.1)).toBe(true);
 | 
			
		||||
    expect(pointOnEllipse([-1, -0.86], ellipse, 0.01)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointOnEllipse([-1, 0.8], ellipse)).toBe(false);
 | 
			
		||||
    expect(pointOnEllipse([1, -0.8], ellipse)).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it("point in ellipse", () => {
 | 
			
		||||
    [
 | 
			
		||||
      [0, 1],
 | 
			
		||||
      [0, -1],
 | 
			
		||||
      [2, 0],
 | 
			
		||||
      [-2, 0],
 | 
			
		||||
    ].forEach((point) => {
 | 
			
		||||
      expect(pointInEllipse(point as Point, ellipse)).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(pointInEllipse([-1, 0.8], ellipse)).toBe(true);
 | 
			
		||||
    expect(pointInEllipse([1, -0.8], ellipse)).toBe(true);
 | 
			
		||||
 | 
			
		||||
    expect(pointInEllipse([-1, 1], ellipse)).toBe(false);
 | 
			
		||||
    expect(pointInEllipse([-1.4, 0.8], ellipse)).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("line and line", () => {
 | 
			
		||||
  const lineA: Line = [
 | 
			
		||||
    [1, 4],
 | 
			
		||||
    [3, 4],
 | 
			
		||||
  ];
 | 
			
		||||
  const lineB: Line = [
 | 
			
		||||
    [2, 1],
 | 
			
		||||
    [2, 7],
 | 
			
		||||
  ];
 | 
			
		||||
  const lineC: Line = [
 | 
			
		||||
    [1, 8],
 | 
			
		||||
    [3, 8],
 | 
			
		||||
  ];
 | 
			
		||||
  const lineD: Line = [
 | 
			
		||||
    [1, 8],
 | 
			
		||||
    [3, 8],
 | 
			
		||||
  ];
 | 
			
		||||
  const lineE: Line = [
 | 
			
		||||
    [1, 9],
 | 
			
		||||
    [3, 9],
 | 
			
		||||
  ];
 | 
			
		||||
  const lineF: Line = [
 | 
			
		||||
    [1, 2],
 | 
			
		||||
    [3, 4],
 | 
			
		||||
  ];
 | 
			
		||||
  const lineG: Line = [
 | 
			
		||||
    [0, 1],
 | 
			
		||||
    [2, 3],
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  it("intersection", () => {
 | 
			
		||||
    expect(lineIntersectsLine(lineA, lineB)).toBe(true);
 | 
			
		||||
    expect(lineIntersectsLine(lineA, lineC)).toBe(false);
 | 
			
		||||
    expect(lineIntersectsLine(lineB, lineC)).toBe(false);
 | 
			
		||||
    expect(lineIntersectsLine(lineC, lineD)).toBe(true);
 | 
			
		||||
    expect(lineIntersectsLine(lineE, lineD)).toBe(false);
 | 
			
		||||
    expect(lineIntersectsLine(lineF, lineG)).toBe(true);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										956
									
								
								packages/utils/geometry/geometry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										956
									
								
								packages/utils/geometry/geometry.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,956 @@
 | 
			
		||||
import { distance2d } from "../../excalidraw/math";
 | 
			
		||||
import {
 | 
			
		||||
  Point,
 | 
			
		||||
  Line,
 | 
			
		||||
  Polygon,
 | 
			
		||||
  Curve,
 | 
			
		||||
  Ellipse,
 | 
			
		||||
  Polycurve,
 | 
			
		||||
  Polyline,
 | 
			
		||||
} from "./shape";
 | 
			
		||||
 | 
			
		||||
const DEFAULT_THRESHOLD = 10e-5;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * utils
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// the two vectors are ao and bo
 | 
			
		||||
export const cross = (a: Point, b: Point, o: Point) => {
 | 
			
		||||
  return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isClosed = (polygon: Polygon) => {
 | 
			
		||||
  const first = polygon[0];
 | 
			
		||||
  const last = polygon[polygon.length - 1];
 | 
			
		||||
  return first[0] === last[0] && first[1] === last[1];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const close = (polygon: Polygon) => {
 | 
			
		||||
  return isClosed(polygon) ? polygon : [...polygon, polygon[0]];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * angles
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// convert radians to degress
 | 
			
		||||
export const angleToDegrees = (angle: number) => {
 | 
			
		||||
  return (angle * 180) / Math.PI;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// convert degrees to radians
 | 
			
		||||
export const angleToRadians = (angle: number) => {
 | 
			
		||||
  return (angle / 180) * Math.PI;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// return the angle of reflection given an angle of incidence and a surface angle in degrees
 | 
			
		||||
export const angleReflect = (incidenceAngle: number, surfaceAngle: number) => {
 | 
			
		||||
  const a = surfaceAngle * 2 - incidenceAngle;
 | 
			
		||||
  return a >= 360 ? a - 360 : a < 0 ? a + 360 : a;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * points
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const rotate = (point: Point, angle: number): Point => {
 | 
			
		||||
  return [
 | 
			
		||||
    point[0] * Math.cos(angle) - point[1] * Math.sin(angle),
 | 
			
		||||
    point[0] * Math.sin(angle) + point[1] * Math.cos(angle),
 | 
			
		||||
  ];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const isOrigin = (point: Point) => {
 | 
			
		||||
  return point[0] === 0 && point[1] === 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// rotate a given point about a given origin at the given angle
 | 
			
		||||
export const pointRotate = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  angle: number,
 | 
			
		||||
  origin?: Point,
 | 
			
		||||
): Point => {
 | 
			
		||||
  const r = angleToRadians(angle);
 | 
			
		||||
 | 
			
		||||
  if (!origin || isOrigin(origin)) {
 | 
			
		||||
    return rotate(point, r);
 | 
			
		||||
  }
 | 
			
		||||
  return rotate(point.map((c, i) => c - origin[i]) as Point, r).map(
 | 
			
		||||
    (c, i) => c + origin[i],
 | 
			
		||||
  ) as Point;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// translate a point by an angle (in degrees) and distance
 | 
			
		||||
export const pointTranslate = (point: Point, angle = 0, distance = 0) => {
 | 
			
		||||
  const r = angleToRadians(angle);
 | 
			
		||||
  return [
 | 
			
		||||
    point[0] + distance * Math.cos(r),
 | 
			
		||||
    point[1] + distance * Math.sin(r),
 | 
			
		||||
  ] as Point;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointInverse = (point: Point) => {
 | 
			
		||||
  return [-point[0], -point[1]] as Point;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointAdd = (pointA: Point, pointB: Point): Point => {
 | 
			
		||||
  return [pointA[0] + pointB[0], pointA[1] + pointB[1]];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const distanceToPoint = (p1: Point, p2: Point) => {
 | 
			
		||||
  return distance2d(...p1, ...p2);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * lines
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// return the angle of a line, in degrees
 | 
			
		||||
export const lineAngle = (line: Line) => {
 | 
			
		||||
  return angleToDegrees(
 | 
			
		||||
    Math.atan2(line[1][1] - line[0][1], line[1][0] - line[0][0]),
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// get the distance between the endpoints of a line segment
 | 
			
		||||
export const lineLength = (line: Line) => {
 | 
			
		||||
  return Math.sqrt(
 | 
			
		||||
    Math.pow(line[1][0] - line[0][0], 2) + Math.pow(line[1][1] - line[0][1], 2),
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// get the midpoint of a line segment
 | 
			
		||||
export const lineMidpoint = (line: Line) => {
 | 
			
		||||
  return [
 | 
			
		||||
    (line[0][0] + line[1][0]) / 2,
 | 
			
		||||
    (line[0][1] + line[1][1]) / 2,
 | 
			
		||||
  ] as Point;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// return the coordinates resulting from rotating the given line about an origin by an angle in degrees
 | 
			
		||||
// note that when the origin is not given, the midpoint of the given line is used as the origin
 | 
			
		||||
export const lineRotate = (line: Line, angle: number, origin?: Point): Line => {
 | 
			
		||||
  return line.map((point) =>
 | 
			
		||||
    pointRotate(point, angle, origin || lineMidpoint(line)),
 | 
			
		||||
  ) as Line;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// returns the coordinates resulting from translating a line by an angle in degrees and a distance.
 | 
			
		||||
export const lineTranslate = (line: Line, angle: number, distance: number) => {
 | 
			
		||||
  return line.map((point) => pointTranslate(point, angle, distance));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const lineInterpolate = (line: Line, clamp = false) => {
 | 
			
		||||
  const [[x1, y1], [x2, y2]] = line;
 | 
			
		||||
  return (t: number) => {
 | 
			
		||||
    const t0 = clamp ? (t < 0 ? 0 : t > 1 ? 1 : t) : t;
 | 
			
		||||
    return [(x2 - x1) * t0 + x1, (y2 - y1) * t0 + y1] as Point;
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * curves
 | 
			
		||||
 */
 | 
			
		||||
function clone(p: Point): Point {
 | 
			
		||||
  return [...p] as Point;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const curveToBezier = (
 | 
			
		||||
  pointsIn: readonly Point[],
 | 
			
		||||
  curveTightness = 0,
 | 
			
		||||
): Point[] => {
 | 
			
		||||
  const len = pointsIn.length;
 | 
			
		||||
  if (len < 3) {
 | 
			
		||||
    throw new Error("A curve must have at least three points.");
 | 
			
		||||
  }
 | 
			
		||||
  const out: Point[] = [];
 | 
			
		||||
  if (len === 3) {
 | 
			
		||||
    out.push(
 | 
			
		||||
      clone(pointsIn[0]),
 | 
			
		||||
      clone(pointsIn[1]),
 | 
			
		||||
      clone(pointsIn[2]),
 | 
			
		||||
      clone(pointsIn[2]),
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    const points: Point[] = [];
 | 
			
		||||
    points.push(pointsIn[0], pointsIn[0]);
 | 
			
		||||
    for (let i = 1; i < pointsIn.length; i++) {
 | 
			
		||||
      points.push(pointsIn[i]);
 | 
			
		||||
      if (i === pointsIn.length - 1) {
 | 
			
		||||
        points.push(pointsIn[i]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const b: Point[] = [];
 | 
			
		||||
    const s = 1 - curveTightness;
 | 
			
		||||
    out.push(clone(points[0]));
 | 
			
		||||
    for (let i = 1; i + 2 < points.length; i++) {
 | 
			
		||||
      const cachedVertArray = points[i];
 | 
			
		||||
      b[0] = [cachedVertArray[0], cachedVertArray[1]];
 | 
			
		||||
      b[1] = [
 | 
			
		||||
        cachedVertArray[0] + (s * points[i + 1][0] - s * points[i - 1][0]) / 6,
 | 
			
		||||
        cachedVertArray[1] + (s * points[i + 1][1] - s * points[i - 1][1]) / 6,
 | 
			
		||||
      ];
 | 
			
		||||
      b[2] = [
 | 
			
		||||
        points[i + 1][0] + (s * points[i][0] - s * points[i + 2][0]) / 6,
 | 
			
		||||
        points[i + 1][1] + (s * points[i][1] - s * points[i + 2][1]) / 6,
 | 
			
		||||
      ];
 | 
			
		||||
      b[3] = [points[i + 1][0], points[i + 1][1]];
 | 
			
		||||
      out.push(b[1], b[2], b[3]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return out;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const curveRotate = (curve: Curve, angle: number, origin: Point) => {
 | 
			
		||||
  return curve.map((p) => pointRotate(p, angle, origin));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const cubicBezierPoint = (t: number, controlPoints: Curve): Point => {
 | 
			
		||||
  const [p0, p1, p2, p3] = controlPoints;
 | 
			
		||||
 | 
			
		||||
  const x =
 | 
			
		||||
    Math.pow(1 - t, 3) * p0[0] +
 | 
			
		||||
    3 * Math.pow(1 - t, 2) * t * p1[0] +
 | 
			
		||||
    3 * (1 - t) * Math.pow(t, 2) * p2[0] +
 | 
			
		||||
    Math.pow(t, 3) * p3[0];
 | 
			
		||||
 | 
			
		||||
  const y =
 | 
			
		||||
    Math.pow(1 - t, 3) * p0[1] +
 | 
			
		||||
    3 * Math.pow(1 - t, 2) * t * p1[1] +
 | 
			
		||||
    3 * (1 - t) * Math.pow(t, 2) * p2[1] +
 | 
			
		||||
    Math.pow(t, 3) * p3[1];
 | 
			
		||||
 | 
			
		||||
  return [x, y];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const solveCubicEquation = (a: number, b: number, c: number, d: number) => {
 | 
			
		||||
  // This function solves the cubic equation ax^3 + bx^2 + cx + d = 0
 | 
			
		||||
  const roots: number[] = [];
 | 
			
		||||
 | 
			
		||||
  const discriminant =
 | 
			
		||||
    18 * a * b * c * d -
 | 
			
		||||
    4 * Math.pow(b, 3) * d +
 | 
			
		||||
    Math.pow(b, 2) * Math.pow(c, 2) -
 | 
			
		||||
    4 * a * Math.pow(c, 3) -
 | 
			
		||||
    27 * Math.pow(a, 2) * Math.pow(d, 2);
 | 
			
		||||
 | 
			
		||||
  if (discriminant >= 0) {
 | 
			
		||||
    const C = Math.cbrt((discriminant + Math.sqrt(discriminant)) / 2);
 | 
			
		||||
    const D = Math.cbrt((discriminant - Math.sqrt(discriminant)) / 2);
 | 
			
		||||
 | 
			
		||||
    const root1 = (-b - C - D) / (3 * a);
 | 
			
		||||
    const root2 = (-b + (C + D) / 2) / (3 * a);
 | 
			
		||||
    const root3 = (-b + (C + D) / 2) / (3 * a);
 | 
			
		||||
 | 
			
		||||
    roots.push(root1, root2, root3);
 | 
			
		||||
  } else {
 | 
			
		||||
    const realPart = -b / (3 * a);
 | 
			
		||||
 | 
			
		||||
    const root1 =
 | 
			
		||||
      2 * Math.sqrt(-b / (3 * a)) * Math.cos(Math.acos(realPart) / 3);
 | 
			
		||||
    const root2 =
 | 
			
		||||
      2 *
 | 
			
		||||
      Math.sqrt(-b / (3 * a)) *
 | 
			
		||||
      Math.cos((Math.acos(realPart) + 2 * Math.PI) / 3);
 | 
			
		||||
    const root3 =
 | 
			
		||||
      2 *
 | 
			
		||||
      Math.sqrt(-b / (3 * a)) *
 | 
			
		||||
      Math.cos((Math.acos(realPart) + 4 * Math.PI) / 3);
 | 
			
		||||
 | 
			
		||||
    roots.push(root1, root2, root3);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return roots;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const findClosestParameter = (point: Point, controlPoints: Curve) => {
 | 
			
		||||
  // This function finds the parameter t that minimizes the distance between the point
 | 
			
		||||
  // and any point on the cubic Bezier curve.
 | 
			
		||||
 | 
			
		||||
  const [p0, p1, p2, p3] = controlPoints;
 | 
			
		||||
 | 
			
		||||
  // Use the direct formula to find the parameter t
 | 
			
		||||
  const a = p3[0] - 3 * p2[0] + 3 * p1[0] - p0[0];
 | 
			
		||||
  const b = 3 * p2[0] - 6 * p1[0] + 3 * p0[0];
 | 
			
		||||
  const c = 3 * p1[0] - 3 * p0[0];
 | 
			
		||||
  const d = p0[0] - point[0];
 | 
			
		||||
 | 
			
		||||
  const rootsX = solveCubicEquation(a, b, c, d);
 | 
			
		||||
 | 
			
		||||
  // Do the same for the y-coordinate
 | 
			
		||||
  const e = p3[1] - 3 * p2[1] + 3 * p1[1] - p0[1];
 | 
			
		||||
  const f = 3 * p2[1] - 6 * p1[1] + 3 * p0[1];
 | 
			
		||||
  const g = 3 * p1[1] - 3 * p0[1];
 | 
			
		||||
  const h = p0[1] - point[1];
 | 
			
		||||
 | 
			
		||||
  const rootsY = solveCubicEquation(e, f, g, h);
 | 
			
		||||
 | 
			
		||||
  // Select the real root that is between 0 and 1 (inclusive)
 | 
			
		||||
  const validRootsX = rootsX.filter((root) => root >= 0 && root <= 1);
 | 
			
		||||
  const validRootsY = rootsY.filter((root) => root >= 0 && root <= 1);
 | 
			
		||||
 | 
			
		||||
  if (validRootsX.length === 0 || validRootsY.length === 0) {
 | 
			
		||||
    // No valid roots found, use the midpoint as a fallback
 | 
			
		||||
    return 0.5;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Choose the parameter t that minimizes the distance
 | 
			
		||||
  let minDistance = Infinity;
 | 
			
		||||
  let closestT = 0;
 | 
			
		||||
 | 
			
		||||
  for (const rootX of validRootsX) {
 | 
			
		||||
    for (const rootY of validRootsY) {
 | 
			
		||||
      const distance = Math.sqrt(
 | 
			
		||||
        (rootX - point[0]) ** 2 + (rootY - point[1]) ** 2,
 | 
			
		||||
      );
 | 
			
		||||
      if (distance < minDistance) {
 | 
			
		||||
        minDistance = distance;
 | 
			
		||||
        closestT = (rootX + rootY) / 2; // Use the average for a smoother result
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return closestT;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const cubicBezierDistance = (point: Point, controlPoints: Curve) => {
 | 
			
		||||
  // Calculate the closest point on the Bezier curve to the given point
 | 
			
		||||
  const t = findClosestParameter(point, controlPoints);
 | 
			
		||||
 | 
			
		||||
  // Calculate the coordinates of the closest point on the curve
 | 
			
		||||
  const [closestX, closestY] = cubicBezierPoint(t, controlPoints);
 | 
			
		||||
 | 
			
		||||
  // Calculate the distance between the given point and the closest point on the curve
 | 
			
		||||
  const distance = Math.sqrt(
 | 
			
		||||
    (point[0] - closestX) ** 2 + (point[1] - closestY) ** 2,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return distance;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * polygons
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const polygonRotate = (
 | 
			
		||||
  polygon: Polygon,
 | 
			
		||||
  angle: number,
 | 
			
		||||
  origin: Point,
 | 
			
		||||
) => {
 | 
			
		||||
  return polygon.map((p) => pointRotate(p, angle, origin));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonBounds = (polygon: Polygon) => {
 | 
			
		||||
  let xMin = Infinity;
 | 
			
		||||
  let xMax = -Infinity;
 | 
			
		||||
  let yMin = Infinity;
 | 
			
		||||
  let yMax = -Infinity;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = polygon.length; i < l; i++) {
 | 
			
		||||
    const p = polygon[i];
 | 
			
		||||
    const x = p[0];
 | 
			
		||||
    const y = p[1];
 | 
			
		||||
 | 
			
		||||
    if (x != null && isFinite(x) && y != null && isFinite(y)) {
 | 
			
		||||
      if (x < xMin) {
 | 
			
		||||
        xMin = x;
 | 
			
		||||
      }
 | 
			
		||||
      if (x > xMax) {
 | 
			
		||||
        xMax = x;
 | 
			
		||||
      }
 | 
			
		||||
      if (y < yMin) {
 | 
			
		||||
        yMin = y;
 | 
			
		||||
      }
 | 
			
		||||
      if (y > yMax) {
 | 
			
		||||
        yMax = y;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    [xMin, yMin],
 | 
			
		||||
    [xMax, yMax],
 | 
			
		||||
  ] as [Point, Point];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonCentroid = (vertices: Point[]) => {
 | 
			
		||||
  let a = 0;
 | 
			
		||||
  let x = 0;
 | 
			
		||||
  let y = 0;
 | 
			
		||||
  const l = vertices.length;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < l; i++) {
 | 
			
		||||
    const s = i === l - 1 ? 0 : i + 1;
 | 
			
		||||
    const v0 = vertices[i];
 | 
			
		||||
    const v1 = vertices[s];
 | 
			
		||||
    const f = v0[0] * v1[1] - v1[0] * v0[1];
 | 
			
		||||
 | 
			
		||||
    a += f;
 | 
			
		||||
    x += (v0[0] + v1[0]) * f;
 | 
			
		||||
    y += (v0[1] + v1[1]) * f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const d = a * 3;
 | 
			
		||||
 | 
			
		||||
  return [x / d, y / d] as Point;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonScale = (
 | 
			
		||||
  polygon: Polygon,
 | 
			
		||||
  scale: number,
 | 
			
		||||
  origin?: Point,
 | 
			
		||||
) => {
 | 
			
		||||
  if (!origin) {
 | 
			
		||||
    origin = polygonCentroid(polygon);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const p: Polygon = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = polygon.length; i < l; i++) {
 | 
			
		||||
    const v = polygon[i];
 | 
			
		||||
    const d = lineLength([origin, v]);
 | 
			
		||||
    const a = lineAngle([origin, v]);
 | 
			
		||||
 | 
			
		||||
    p[i] = pointTranslate(origin, a, d * scale);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return p;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonScaleX = (
 | 
			
		||||
  polygon: Polygon,
 | 
			
		||||
  scale: number,
 | 
			
		||||
  origin?: Point,
 | 
			
		||||
) => {
 | 
			
		||||
  if (!origin) {
 | 
			
		||||
    origin = polygonCentroid(polygon);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const p: Polygon = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = polygon.length; i < l; i++) {
 | 
			
		||||
    const v = polygon[i];
 | 
			
		||||
    const d = lineLength([origin, v]);
 | 
			
		||||
    const a = lineAngle([origin, v]);
 | 
			
		||||
    const t = pointTranslate(origin, a, d * scale);
 | 
			
		||||
 | 
			
		||||
    p[i] = [t[0], v[1]];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return p;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonScaleY = (
 | 
			
		||||
  polygon: Polygon,
 | 
			
		||||
  scale: number,
 | 
			
		||||
  origin?: Point,
 | 
			
		||||
) => {
 | 
			
		||||
  if (!origin) {
 | 
			
		||||
    origin = polygonCentroid(polygon);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const p: Polygon = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = polygon.length; i < l; i++) {
 | 
			
		||||
    const v = polygon[i];
 | 
			
		||||
    const d = lineLength([origin, v]);
 | 
			
		||||
    const a = lineAngle([origin, v]);
 | 
			
		||||
    const t = pointTranslate(origin, a, d * scale);
 | 
			
		||||
 | 
			
		||||
    p[i] = [v[0], t[1]];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return p;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonReflectX = (polygon: Polygon, reflectFactor = 1) => {
 | 
			
		||||
  const [[min], [max]] = polygonBounds(polygon);
 | 
			
		||||
  const p: Point[] = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = polygon.length; i < l; i++) {
 | 
			
		||||
    const [x, y] = polygon[i];
 | 
			
		||||
    const r: Point = [min + max - x, y];
 | 
			
		||||
 | 
			
		||||
    if (reflectFactor === 0) {
 | 
			
		||||
      p[i] = [x, y];
 | 
			
		||||
    } else if (reflectFactor === 1) {
 | 
			
		||||
      p[i] = r;
 | 
			
		||||
    } else {
 | 
			
		||||
      const t = lineInterpolate([[x, y], r]);
 | 
			
		||||
      p[i] = t(Math.max(Math.min(reflectFactor, 1), 0));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return p;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonReflectY = (polygon: Polygon, reflectFactor = 1) => {
 | 
			
		||||
  const [[, min], [, max]] = polygonBounds(polygon);
 | 
			
		||||
  const p: Point[] = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = polygon.length; i < l; i++) {
 | 
			
		||||
    const [x, y] = polygon[i];
 | 
			
		||||
    const r: Point = [x, min + max - y];
 | 
			
		||||
 | 
			
		||||
    if (reflectFactor === 0) {
 | 
			
		||||
      p[i] = [x, y];
 | 
			
		||||
    } else if (reflectFactor === 1) {
 | 
			
		||||
      p[i] = r;
 | 
			
		||||
    } else {
 | 
			
		||||
      const t = lineInterpolate([[x, y], r]);
 | 
			
		||||
      p[i] = t(Math.max(Math.min(reflectFactor, 1), 0));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return p;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonTranslate = (
 | 
			
		||||
  polygon: Polygon,
 | 
			
		||||
  angle: number,
 | 
			
		||||
  distance: number,
 | 
			
		||||
) => {
 | 
			
		||||
  return polygon.map((p) => pointTranslate(p, angle, distance));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * ellipses
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const ellipseAxes = (ellipse: Ellipse) => {
 | 
			
		||||
  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 = (ellipse: Ellipse) => {
 | 
			
		||||
  const { majorAxis, minorAxis } = ellipseAxes(ellipse);
 | 
			
		||||
 | 
			
		||||
  return Math.sqrt(majorAxis ** 2 - minorAxis ** 2);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const ellipseExtremes = (ellipse: Ellipse) => {
 | 
			
		||||
  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);
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    pointAdd([xAtYMax, yMax], center),
 | 
			
		||||
    pointAdd(pointInverse([xAtYMax, yMax]), center),
 | 
			
		||||
    pointAdd([xMax, yAtXMax], center),
 | 
			
		||||
    pointAdd([xMax, yAtXMax], center),
 | 
			
		||||
  ];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointRelativeToCenter = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  center: Point,
 | 
			
		||||
  angle: number,
 | 
			
		||||
): Point => {
 | 
			
		||||
  const translated = pointAdd(point, pointInverse(center));
 | 
			
		||||
  const rotated = pointRotate(translated, -angleToDegrees(angle));
 | 
			
		||||
 | 
			
		||||
  return rotated;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * relationships
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const topPointFirst = (line: Line) => {
 | 
			
		||||
  return line[1][1] > line[0][1] ? line : [line[1], line[0]];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointLeftofLine = (point: Point, line: Line) => {
 | 
			
		||||
  const t = topPointFirst(line);
 | 
			
		||||
  return cross(point, t[1], t[0]) < 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointRightofLine = (point: Point, line: Line) => {
 | 
			
		||||
  const t = topPointFirst(line);
 | 
			
		||||
  return cross(point, t[1], t[0]) > 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const distanceToSegment = (point: Point, line: Line) => {
 | 
			
		||||
  const [x, y] = point;
 | 
			
		||||
  const [[x1, y1], [x2, y2]] = line;
 | 
			
		||||
 | 
			
		||||
  const A = x - x1;
 | 
			
		||||
  const B = y - y1;
 | 
			
		||||
  const C = x2 - x1;
 | 
			
		||||
  const D = y2 - y1;
 | 
			
		||||
 | 
			
		||||
  const dot = A * C + B * D;
 | 
			
		||||
  const len_sq = C * C + D * D;
 | 
			
		||||
  let param = -1;
 | 
			
		||||
  if (len_sq !== 0) {
 | 
			
		||||
    param = dot / len_sq;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let xx;
 | 
			
		||||
  let yy;
 | 
			
		||||
 | 
			
		||||
  if (param < 0) {
 | 
			
		||||
    xx = x1;
 | 
			
		||||
    yy = y1;
 | 
			
		||||
  } else if (param > 1) {
 | 
			
		||||
    xx = x2;
 | 
			
		||||
    yy = y2;
 | 
			
		||||
  } else {
 | 
			
		||||
    xx = x1 + param * C;
 | 
			
		||||
    yy = y1 + param * D;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const dx = x - xx;
 | 
			
		||||
  const dy = y - yy;
 | 
			
		||||
  return Math.sqrt(dx * dx + dy * dy);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointOnLine = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  line: Line,
 | 
			
		||||
  threshold = DEFAULT_THRESHOLD,
 | 
			
		||||
) => {
 | 
			
		||||
  const distance = distanceToSegment(point, line);
 | 
			
		||||
 | 
			
		||||
  if (distance === 0) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return distance < threshold;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointOnPolyline = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  polyline: Polyline,
 | 
			
		||||
  threshold = DEFAULT_THRESHOLD,
 | 
			
		||||
) => {
 | 
			
		||||
  return polyline.some((line) => pointOnLine(point, line, threshold));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const lineIntersectsLine = (lineA: Line, lineB: Line) => {
 | 
			
		||||
  const [[a0x, a0y], [a1x, a1y]] = lineA;
 | 
			
		||||
  const [[b0x, b0y], [b1x, b1y]] = lineB;
 | 
			
		||||
 | 
			
		||||
  // shared points
 | 
			
		||||
  if (a0x === b0x && a0y === b0y) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (a1x === b1x && a1y === b1y) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // point on line
 | 
			
		||||
  if (pointOnLine(lineA[0], lineB) || pointOnLine(lineA[1], lineB)) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (pointOnLine(lineB[0], lineA) || pointOnLine(lineB[1], lineA)) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const denom = (b1y - b0y) * (a1x - a0x) - (b1x - b0x) * (a1y - a0y);
 | 
			
		||||
 | 
			
		||||
  if (denom === 0) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const deltaY = a0y - b0y;
 | 
			
		||||
  const deltaX = a0x - b0x;
 | 
			
		||||
  const numer0 = (b1x - b0x) * deltaY - (b1y - b0y) * deltaX;
 | 
			
		||||
  const numer1 = (a1x - a0x) * deltaY - (a1y - a0y) * deltaX;
 | 
			
		||||
  const quotA = numer0 / denom;
 | 
			
		||||
  const quotB = numer1 / denom;
 | 
			
		||||
 | 
			
		||||
  return quotA > 0 && quotA < 1 && quotB > 0 && quotB < 1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const lineIntersectsPolygon = (line: Line, polygon: Polygon) => {
 | 
			
		||||
  let intersects = false;
 | 
			
		||||
  const closed = close(polygon);
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = closed.length - 1; i < l; i++) {
 | 
			
		||||
    const v0 = closed[i];
 | 
			
		||||
    const v1 = closed[i + 1];
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      lineIntersectsLine(line, [v0, v1]) ||
 | 
			
		||||
      (pointOnLine(v0, line) && pointOnLine(v1, line))
 | 
			
		||||
    ) {
 | 
			
		||||
      intersects = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return intersects;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointInBezierEquation = (
 | 
			
		||||
  p0: Point,
 | 
			
		||||
  p1: Point,
 | 
			
		||||
  p2: Point,
 | 
			
		||||
  p3: Point,
 | 
			
		||||
  [mx, my]: Point,
 | 
			
		||||
  lineThreshold: number,
 | 
			
		||||
) => {
 | 
			
		||||
  // 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);
 | 
			
		||||
 | 
			
		||||
  const lineSegmentPoints: Point[] = [];
 | 
			
		||||
  let t = 0;
 | 
			
		||||
  while (t <= 1.0) {
 | 
			
		||||
    const tx = equation(t, 0);
 | 
			
		||||
    const ty = equation(t, 1);
 | 
			
		||||
 | 
			
		||||
    const diff = Math.sqrt(Math.pow(tx - mx, 2) + Math.pow(ty - my, 2));
 | 
			
		||||
 | 
			
		||||
    if (diff < lineThreshold) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    lineSegmentPoints.push([tx, ty]);
 | 
			
		||||
 | 
			
		||||
    t += 0.1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // check the distance from line segments to the given point
 | 
			
		||||
 | 
			
		||||
  return false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const cubicBezierEquation = (curve: Curve) => {
 | 
			
		||||
  const [p0, p1, p2, p3] = curve;
 | 
			
		||||
  // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3
 | 
			
		||||
  return (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);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polyLineFromCurve = (curve: Curve, segments = 10): Polyline => {
 | 
			
		||||
  const equation = cubicBezierEquation(curve);
 | 
			
		||||
  let startingPoint = [equation(0, 0), equation(0, 1)] as Point;
 | 
			
		||||
  const lineSegments: Polyline = [];
 | 
			
		||||
  let t = 0;
 | 
			
		||||
  const increment = 1 / segments;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0; i < segments; i++) {
 | 
			
		||||
    t += increment;
 | 
			
		||||
    if (t <= 1) {
 | 
			
		||||
      const nextPoint: Point = [equation(t, 0), equation(t, 1)];
 | 
			
		||||
      lineSegments.push([startingPoint, nextPoint]);
 | 
			
		||||
      startingPoint = nextPoint;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return lineSegments;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointOnCurve = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  curve: Curve,
 | 
			
		||||
  threshold = DEFAULT_THRESHOLD,
 | 
			
		||||
) => {
 | 
			
		||||
  return pointOnPolyline(point, polyLineFromCurve(curve), threshold);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointOnPolycurve = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  polycurve: Polycurve,
 | 
			
		||||
  threshold = DEFAULT_THRESHOLD,
 | 
			
		||||
) => {
 | 
			
		||||
  return polycurve.some((curve) => pointOnCurve(point, curve, threshold));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointInPolygon = (point: Point, polygon: Polygon) => {
 | 
			
		||||
  const x = point[0];
 | 
			
		||||
  const y = point[1];
 | 
			
		||||
  let inside = false;
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
 | 
			
		||||
    const xi = polygon[i][0];
 | 
			
		||||
    const yi = polygon[i][1];
 | 
			
		||||
    const xj = polygon[j][0];
 | 
			
		||||
    const yj = polygon[j][1];
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      ((yi > y && yj <= y) || (yi <= y && yj > y)) &&
 | 
			
		||||
      x < ((xj - xi) * (y - yi)) / (yj - yi) + xi
 | 
			
		||||
    ) {
 | 
			
		||||
      inside = !inside;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return inside;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointOnPolygon = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  polygon: Polygon,
 | 
			
		||||
  threshold = DEFAULT_THRESHOLD,
 | 
			
		||||
) => {
 | 
			
		||||
  let on = false;
 | 
			
		||||
  const closed = close(polygon);
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = closed.length - 1; i < l; i++) {
 | 
			
		||||
    if (pointOnLine(point, [closed[i], closed[i + 1]], threshold)) {
 | 
			
		||||
      on = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return on;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonInPolygon = (polygonA: Polygon, polygonB: Polygon) => {
 | 
			
		||||
  let inside = true;
 | 
			
		||||
  const closed = close(polygonA);
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = closed.length - 1; i < l; i++) {
 | 
			
		||||
    const v0 = closed[i];
 | 
			
		||||
 | 
			
		||||
    // Points test
 | 
			
		||||
    if (!pointInPolygon(v0, polygonB)) {
 | 
			
		||||
      inside = false;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Lines test
 | 
			
		||||
    if (lineIntersectsPolygon([v0, closed[i + 1]], polygonB)) {
 | 
			
		||||
      inside = false;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return inside;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const polygonIntersectPolygon = (
 | 
			
		||||
  polygonA: Polygon,
 | 
			
		||||
  polygonB: Polygon,
 | 
			
		||||
) => {
 | 
			
		||||
  let intersects = false;
 | 
			
		||||
  let onCount = 0;
 | 
			
		||||
  const closed = close(polygonA);
 | 
			
		||||
 | 
			
		||||
  for (let i = 0, l = closed.length - 1; i < l; i++) {
 | 
			
		||||
    const v0 = closed[i];
 | 
			
		||||
    const v1 = closed[i + 1];
 | 
			
		||||
 | 
			
		||||
    if (lineIntersectsPolygon([v0, v1], polygonB)) {
 | 
			
		||||
      intersects = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (pointOnPolygon(v0, polygonB)) {
 | 
			
		||||
      ++onCount;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (onCount === 2) {
 | 
			
		||||
      intersects = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return intersects;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const distanceToEllipse = (point: Point, ellipse: Ellipse) => {
 | 
			
		||||
  const { angle, halfWidth, halfHeight, center } = ellipse;
 | 
			
		||||
  const a = halfWidth;
 | 
			
		||||
  const b = halfHeight;
 | 
			
		||||
  const [rotatedPointX, rotatedPointY] = pointRelativeToCenter(
 | 
			
		||||
    point,
 | 
			
		||||
    center,
 | 
			
		||||
    angle,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  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 distanceToPoint([rotatedPointX, rotatedPointY], [minX, minY]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointOnEllipse = (
 | 
			
		||||
  point: Point,
 | 
			
		||||
  ellipse: Ellipse,
 | 
			
		||||
  threshold = DEFAULT_THRESHOLD,
 | 
			
		||||
) => {
 | 
			
		||||
  return distanceToEllipse(point, ellipse) <= threshold;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const pointInEllipse = (point: Point, ellipse: Ellipse) => {
 | 
			
		||||
  const { center, angle, halfWidth, halfHeight } = ellipse;
 | 
			
		||||
  const [rotatedPointX, rotatedPointY] = pointRelativeToCenter(
 | 
			
		||||
    point,
 | 
			
		||||
    center,
 | 
			
		||||
    angle,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    (rotatedPointX / halfWidth) * (rotatedPointX / halfWidth) +
 | 
			
		||||
      (rotatedPointY / halfHeight) * (rotatedPointY / halfHeight) <=
 | 
			
		||||
    1
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										278
									
								
								packages/utils/geometry/shape.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								packages/utils/geometry/shape.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,278 @@
 | 
			
		||||
/**
 | 
			
		||||
 * 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 {
 | 
			
		||||
  ExcalidrawDiamondElement,
 | 
			
		||||
  ExcalidrawEllipseElement,
 | 
			
		||||
  ExcalidrawEmbeddableElement,
 | 
			
		||||
  ExcalidrawFrameLikeElement,
 | 
			
		||||
  ExcalidrawFreeDrawElement,
 | 
			
		||||
  ExcalidrawIframeElement,
 | 
			
		||||
  ExcalidrawImageElement,
 | 
			
		||||
  ExcalidrawRectangleElement,
 | 
			
		||||
  ExcalidrawSelectionElement,
 | 
			
		||||
  ExcalidrawTextElement,
 | 
			
		||||
} from "../../excalidraw/element/types";
 | 
			
		||||
import { angleToDegrees, close, pointAdd, pointRotate } from "./geometry";
 | 
			
		||||
import { pointsOnBezierCurves } from "points-on-curve";
 | 
			
		||||
import type { Drawable, Op } from "roughjs/bin/core";
 | 
			
		||||
 | 
			
		||||
// a point is specified by its coordinate (x, y)
 | 
			
		||||
export type Point = [number, number];
 | 
			
		||||
export type Vector = Point;
 | 
			
		||||
 | 
			
		||||
// a line (segment) is defined by two endpoints
 | 
			
		||||
export type Line = [Point, Point];
 | 
			
		||||
 | 
			
		||||
// 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 = Line[];
 | 
			
		||||
 | 
			
		||||
// cubic bezier curve with four control points
 | 
			
		||||
export type Curve = [Point, Point, Point, Point];
 | 
			
		||||
 | 
			
		||||
// a polycurve is a curve consisting of ther curves, this corresponds to a complex
 | 
			
		||||
// curve on the canvas
 | 
			
		||||
export type Polycurve = Curve[];
 | 
			
		||||
 | 
			
		||||
// a polygon is a closed shape by connecting the given points
 | 
			
		||||
// rectangles and diamonds are modelled by polygons
 | 
			
		||||
export type Polygon = 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 = {
 | 
			
		||||
  center: Point;
 | 
			
		||||
  angle: number;
 | 
			
		||||
  halfWidth: number;
 | 
			
		||||
  halfHeight: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type GeometricShape =
 | 
			
		||||
  | {
 | 
			
		||||
      type: "line";
 | 
			
		||||
      data: Line;
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: "polygon";
 | 
			
		||||
      data: Polygon;
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: "curve";
 | 
			
		||||
      data: Curve;
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: "ellipse";
 | 
			
		||||
      data: Ellipse;
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: "polyline";
 | 
			
		||||
      data: Polyline;
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: "polycurve";
 | 
			
		||||
      data: Polycurve;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
type RectangularElement =
 | 
			
		||||
  | ExcalidrawRectangleElement
 | 
			
		||||
  | ExcalidrawDiamondElement
 | 
			
		||||
  | ExcalidrawFrameLikeElement
 | 
			
		||||
  | ExcalidrawEmbeddableElement
 | 
			
		||||
  | ExcalidrawImageElement
 | 
			
		||||
  | ExcalidrawIframeElement
 | 
			
		||||
  | ExcalidrawTextElement
 | 
			
		||||
  | ExcalidrawSelectionElement;
 | 
			
		||||
 | 
			
		||||
// polygon
 | 
			
		||||
export const getPolygonShape = (
 | 
			
		||||
  element: RectangularElement,
 | 
			
		||||
): GeometricShape => {
 | 
			
		||||
  const { angle, width, height, x, y } = element;
 | 
			
		||||
  const angleInDegrees = angleToDegrees(angle);
 | 
			
		||||
  const cx = x + width / 2;
 | 
			
		||||
  const cy = y + height / 2;
 | 
			
		||||
 | 
			
		||||
  const center: Point = [cx, cy];
 | 
			
		||||
 | 
			
		||||
  let data: Polygon = [];
 | 
			
		||||
 | 
			
		||||
  if (element.type === "diamond") {
 | 
			
		||||
    data = [
 | 
			
		||||
      pointRotate([cx, y], angleInDegrees, center),
 | 
			
		||||
      pointRotate([x + width, cy], angleInDegrees, center),
 | 
			
		||||
      pointRotate([cx, y + height], angleInDegrees, center),
 | 
			
		||||
      pointRotate([x, cy], angleInDegrees, center),
 | 
			
		||||
    ] as Polygon;
 | 
			
		||||
  } else {
 | 
			
		||||
    data = [
 | 
			
		||||
      pointRotate([x, y], angleInDegrees, center),
 | 
			
		||||
      pointRotate([x + width, y], angleInDegrees, center),
 | 
			
		||||
      pointRotate([x + width, y + height], angleInDegrees, center),
 | 
			
		||||
      pointRotate([x, y + height], angleInDegrees, center),
 | 
			
		||||
    ] as Polygon;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    type: "polygon",
 | 
			
		||||
    data,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// ellipse
 | 
			
		||||
export const getEllipseShape = (
 | 
			
		||||
  element: ExcalidrawEllipseElement,
 | 
			
		||||
): GeometricShape => {
 | 
			
		||||
  const { width, height, angle, x, y } = element;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    type: "ellipse",
 | 
			
		||||
    data: {
 | 
			
		||||
      center: [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 = (
 | 
			
		||||
  roughShape: Drawable,
 | 
			
		||||
  startingPoint: Point = [0, 0],
 | 
			
		||||
  angleInRadian: number,
 | 
			
		||||
  center: Point,
 | 
			
		||||
): GeometricShape => {
 | 
			
		||||
  const transform = (p: Point) =>
 | 
			
		||||
    pointRotate(
 | 
			
		||||
      [p[0] + startingPoint[0], p[1] + startingPoint[1]],
 | 
			
		||||
      angleToDegrees(angleInRadian),
 | 
			
		||||
      center,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  const ops = getCurvePathOps(roughShape);
 | 
			
		||||
  const polycurve: Polycurve = [];
 | 
			
		||||
  let p0: Point = [0, 0];
 | 
			
		||||
 | 
			
		||||
  for (const op of ops) {
 | 
			
		||||
    if (op.op === "move") {
 | 
			
		||||
      p0 = transform(op.data as Point);
 | 
			
		||||
    }
 | 
			
		||||
    if (op.op === "bcurveTo") {
 | 
			
		||||
      const p1: Point = transform([op.data[0], op.data[1]]);
 | 
			
		||||
      const p2: Point = transform([op.data[2], op.data[3]]);
 | 
			
		||||
      const p3: Point = transform([op.data[4], op.data[5]]);
 | 
			
		||||
      polycurve.push([p0, p1, p2, p3]);
 | 
			
		||||
      p0 = p3;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    type: "polycurve",
 | 
			
		||||
    data: polycurve,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const polylineFromPoints = (points: Point[]) => {
 | 
			
		||||
  let previousPoint = points[0];
 | 
			
		||||
  const polyline: Polyline = [];
 | 
			
		||||
 | 
			
		||||
  for (let i = 1; i < points.length; i++) {
 | 
			
		||||
    const nextPoint = points[i];
 | 
			
		||||
    polyline.push([previousPoint, nextPoint]);
 | 
			
		||||
    previousPoint = nextPoint;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return polyline;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getFreedrawShape = (
 | 
			
		||||
  element: ExcalidrawFreeDrawElement,
 | 
			
		||||
  center: Point,
 | 
			
		||||
  isClosed: boolean = false,
 | 
			
		||||
): GeometricShape => {
 | 
			
		||||
  const angle = angleToDegrees(element.angle);
 | 
			
		||||
  const transform = (p: Point) =>
 | 
			
		||||
    pointRotate(pointAdd(p, [element.x, element.y] as Point), angle, center);
 | 
			
		||||
 | 
			
		||||
  const polyline = polylineFromPoints(
 | 
			
		||||
    element.points.map((p) => transform(p as Point)),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return isClosed
 | 
			
		||||
    ? {
 | 
			
		||||
        type: "polygon",
 | 
			
		||||
        data: close(polyline.flat()) as Polygon,
 | 
			
		||||
      }
 | 
			
		||||
    : {
 | 
			
		||||
        type: "polyline",
 | 
			
		||||
        data: polyline,
 | 
			
		||||
      };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getClosedCurveShape = (
 | 
			
		||||
  roughShape: Drawable,
 | 
			
		||||
  startingPoint: Point = [0, 0],
 | 
			
		||||
  angleInRadian: number,
 | 
			
		||||
  center: Point,
 | 
			
		||||
): GeometricShape => {
 | 
			
		||||
  const ops = getCurvePathOps(roughShape);
 | 
			
		||||
  const transform = (p: Point) =>
 | 
			
		||||
    pointRotate(
 | 
			
		||||
      [p[0] + startingPoint[0], p[1] + startingPoint[1]],
 | 
			
		||||
      angleToDegrees(angleInRadian),
 | 
			
		||||
      center,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  const points: Point[] = [];
 | 
			
		||||
  let odd = false;
 | 
			
		||||
  for (const operation of ops) {
 | 
			
		||||
    if (operation.op === "move") {
 | 
			
		||||
      odd = !odd;
 | 
			
		||||
      if (odd) {
 | 
			
		||||
        points.push([operation.data[0], operation.data[1]]);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (operation.op === "bcurveTo") {
 | 
			
		||||
      if (odd) {
 | 
			
		||||
        points.push([operation.data[0], operation.data[1]]);
 | 
			
		||||
        points.push([operation.data[2], operation.data[3]]);
 | 
			
		||||
        points.push([operation.data[4], operation.data[5]]);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (operation.op === "lineTo") {
 | 
			
		||||
      if (odd) {
 | 
			
		||||
        points.push([operation.data[0], operation.data[1]]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const polygonPoints = pointsOnBezierCurves(points, 10, 5).map((p) =>
 | 
			
		||||
    transform(p),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    type: "polygon",
 | 
			
		||||
    data: polygonPoints,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
		Reference in New Issue
	
	Block a user