@mathigon/euclid
Version:
Euclidean geometry classes and tools for JavaScript
1,550 lines (1,530 loc) • 52.4 kB
JavaScript
// src/angle.ts
import { clamp as clamp4, nearlyEquals as nearlyEquals7 } from "@mathigon/fermat";
// src/arc.ts
import { clamp as clamp3, nearlyEquals as nearlyEquals4 } from "@mathigon/fermat";
// src/circle.ts
import { nearlyEquals as nearlyEquals3 } from "@mathigon/fermat";
// src/line.ts
import { clamp as clamp2, isBetween, nearlyEquals as nearlyEquals2 } from "@mathigon/fermat";
// src/point.ts
import { total } from "@mathigon/core";
import { clamp, lerp, nearlyEquals, Random, roundTo, square } from "@mathigon/fermat";
// src/utilities.ts
import { mod } from "@mathigon/fermat";
var TWO_PI = 2 * Math.PI;
function rad(p, c) {
const a = Math.atan2(p.y - (c ? c.y : 0), p.x - (c ? c.x : 0));
return mod(a, TWO_PI);
}
function findClosest(p, items) {
let q = void 0;
let d = Infinity;
let index = -1;
for (const [i, e] of items.entries()) {
const q1 = e.project(p);
const d1 = Point.distance(p, q1);
if (d1 < d) {
q = q1;
d = d1;
index = i;
}
}
return q ? [q, index] : void 0;
}
// src/point.ts
var Point = class _Point {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
this.type = "point";
}
get unitVector() {
if (nearlyEquals(this.length, 0)) return new _Point(1, 0);
return this.scale(1 / this.length);
}
get length() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
get inverse() {
return new _Point(-this.x, -this.y);
}
get flip() {
return new _Point(this.y, this.x);
}
get perpendicular() {
return new _Point(-this.y, this.x);
}
get array() {
return [this.x, this.y];
}
/** Finds the perpendicular distance between this point and a line. */
distanceFromLine(l) {
return _Point.distance(this, l.project(this));
}
/** Clamps this point to specific bounds. */
clamp(bounds, padding = 0) {
const x = clamp(this.x, bounds.xMin + padding, bounds.xMax - padding);
const y = clamp(this.y, bounds.yMin + padding, bounds.yMax - padding);
return new _Point(x, y);
}
changeCoordinates(originCoords, targetCoords) {
const x = targetCoords.xMin + (this.x - originCoords.xMin) / originCoords.dx * targetCoords.dx;
const y = targetCoords.yMin + (this.y - originCoords.yMin) / originCoords.dy * targetCoords.dy;
return new _Point(x, y);
}
add(p) {
return _Point.sum(this, p);
}
subtract(p) {
return _Point.difference(this, p);
}
round(inc = 1) {
return new _Point(roundTo(this.x, inc), roundTo(this.y, inc));
}
floor() {
return new _Point(Math.floor(this.x), Math.floor(this.y));
}
mod(x, y = x) {
return new _Point(this.x % x, this.y % y);
}
angle(c = ORIGIN) {
return rad(this, c);
}
// Snap to the x or y values of another point
snap(p, tolerance = 5) {
if (nearlyEquals(this.x, p.x, tolerance)) return new _Point(p.x, this.y);
if (nearlyEquals(this.y, p.y, tolerance)) return new _Point(this.x, p.y);
return this;
}
/** Calculates the average of multiple points. */
static average(...points) {
const x = total(points.map((p) => p.x)) / points.length;
const y = total(points.map((p) => p.y)) / points.length;
return new _Point(x, y);
}
/** Calculates the dot product of two points p1 and p2. */
static dot(p1, p2) {
return p1.x * p2.x + p1.y * p2.y;
}
static sum(p1, p2) {
return new _Point(p1.x + p2.x, p1.y + p2.y);
}
static difference(p1, p2) {
return new _Point(p1.x - p2.x, p1.y - p2.y);
}
/** Returns the Euclidean distance between two points p1 and p2. */
static distance(p1, p2) {
return Math.sqrt(square(p1.x - p2.x) + square(p1.y - p2.y));
}
/** Returns the Manhattan distance between two points p1 and p2. */
static manhattan(p1, p2) {
return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
}
/** Interpolates two points p1 and p2 by a factor of t. */
static interpolate(p1, p2, t = 0.5) {
return new _Point(lerp(p1.x, p2.x, t), lerp(p1.y, p2.y, t));
}
/** Interpolates a list of multiple points. */
static interpolateList(points, t = 0.5) {
const n = points.length - 1;
const a = Math.floor(clamp(t, 0, 1) * n);
return _Point.interpolate(points[a], points[a + 1], n * t - a);
}
/** Creates a point from polar coordinates. */
static fromPolar(angle, r = 1) {
return new _Point(r * Math.cos(angle), r * Math.sin(angle));
}
static random(b) {
const x = Random.uniform(b.xMin, b.xMax);
const y = Random.uniform(b.yMin, b.yMax);
return new _Point(x, y);
}
static equals(p1, p2, precision) {
return nearlyEquals(p1.x, p2.x, precision) && nearlyEquals(p1.y, p2.y, precision);
}
/** Check if p1, p2 and p3 lie on a straight line. */
static colinear(p1, p2, p3, tolerance) {
const dx1 = p1.x - p2.x;
const dy1 = p1.y - p2.y;
const dx2 = p2.x - p3.x;
const dy2 = p2.y - p3.y;
return nearlyEquals(dx1 * dy2, dx2 * dy1, tolerance);
}
// ---------------------------------------------------------------------------
/** Transforms this point using a 2x3 matrix m. */
transform(m) {
const x = m[0][0] * this.x + m[0][1] * this.y + m[0][2];
const y = m[1][0] * this.x + m[1][1] * this.y + m[1][2];
return new _Point(x, y);
}
/** Rotates this point by a given angle (in radians) around point `c`. */
rotate(angle, c = ORIGIN) {
if (nearlyEquals(angle, 0)) return this;
const x0 = this.x - c.x;
const y0 = this.y - c.y;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const x = x0 * cos - y0 * sin + c.x;
const y = x0 * sin + y0 * cos + c.y;
return new _Point(x, y);
}
/** Reflects this point across a line l. */
reflect(l) {
const v = l.p2.x - l.p1.x;
const w = l.p2.y - l.p1.y;
const x0 = this.x - l.p1.x;
const y0 = this.y - l.p1.y;
const mu = (v * y0 - w * x0) / (v * v + w * w);
const x = this.x + 2 * mu * w;
const y = this.y - 2 * mu * v;
return new _Point(x, y);
}
scale(sx, sy = sx) {
return new _Point(this.x * sx, this.y * sy);
}
shift(x, y = x) {
return new _Point(this.x + x, this.y + y);
}
translate(p) {
return this.shift(p.x, p.y);
}
equals(other, precision) {
return _Point.equals(this, other, precision);
}
toString() {
return `point(${this.x},${this.y})`;
}
};
var ORIGIN = new Point(0, 0);
// src/types.ts
function isPolygonLike(shape) {
return ["polygon", "polyline", "rectangle", "triangle"].includes(shape.type);
}
function isPolygon(shape) {
return ["polygon", "triangle"].includes(shape.type);
}
function isPolyline(shape) {
return shape.type === "polyline";
}
function isRectangle(shape) {
return shape.type === "rectangle";
}
function isLineLike(shape) {
return ["line", "ray", "segment"].includes(shape.type);
}
function isLine(shape) {
return shape.type === "line";
}
function isRay(shape) {
return shape.type === "ray";
}
function isSegment(shape) {
return shape.type === "segment";
}
function isCircle(shape) {
return shape.type === "circle";
}
function isEllipse(shape) {
return shape.type === "ellipse";
}
function isArc(shape) {
return shape.type === "arc";
}
function isSector(shape) {
return shape.type === "sector";
}
function isAngle(shape) {
return shape.type === "angle";
}
function isPoint(shape) {
return shape.type === "point";
}
// src/line.ts
var Line = class _Line {
constructor(p1, p2) {
this.p1 = p1;
this.p2 = p2;
this.type = "line";
}
/* The distance between the two points defining this line. */
get length() {
return Point.distance(this.p1, this.p2);
}
/* The squared distance between the two points defining this line. */
get lengthSquared() {
return (this.p1.x - this.p2.x) ** 2 + (this.p1.y - this.p2.y) ** 2;
}
/** The midpoint of this line. */
get midpoint() {
return Point.average(this.p1, this.p2);
}
/** The slope of this line. */
get slope() {
return (this.p2.y - this.p1.y) / (this.p2.x - this.p1.x);
}
/** The y-axis intercept of this line. */
get intercept() {
return this.p1.y - this.slope * this.p1.x;
}
/** The angle formed between this line and the x-axis. */
get angle() {
return rad(this.p2, this.p1);
}
/** The point representing a unit vector along this line. */
get unitVector() {
return this.p2.subtract(this.p1).unitVector;
}
/** The point representing the perpendicular vector of this line. */
get perpendicularVector() {
return new Point(this.p2.y - this.p1.y, this.p1.x - this.p2.x).unitVector;
}
/** Finds the line parallel to this one, going through point p. */
parallel(p) {
return new _Line(p, p.add(this.p2).subtract(this.p1));
}
/** Finds the line perpendicular to this one, going through point p. */
perpendicular(p) {
const q = this.line.project(p);
if (Point.equals(p, q)) return new _Line(q, q.add(this.perpendicularVector.scale(this.length / 2)));
return new _Line(q, p);
}
/** The perpendicular bisector of this line. */
get perpendicularBisector() {
return this.perpendicular(this.midpoint);
}
/** Squared distance between a point and a line. */
distanceSquared(p) {
const proj = this.project(p);
return (p.x - proj.x) ** 2 + (p.y - proj.y) ** 2;
}
get line() {
return this.type === "line" ? this : new _Line(this.p1, this.p2);
}
get ray() {
return isRay(this) ? this : new Ray(this.p1, this.p2);
}
get segment() {
return isSegment(this) ? this : new Segment(this.p1, this.p2);
}
// ---------------------------------------------------------------------------
/** Signed distance along the line (opposite of .at()). */
offset(p) {
const a = Point.difference(this.p2, this.p1);
const b = Point.difference(p, this.p1);
return Point.dot(a, b) / this.lengthSquared;
}
/** Projects a point `p` onto this line. */
project(p) {
return this.at(this.offset(p));
}
/** Returns which side of this line a point p is on (or 0 on the line). */
side(p, tolerance) {
const a = Point.difference(this.p2, this.p1);
const b = Point.difference(p, this.p1);
const d = b.x * a.y - b.y * a.x;
return nearlyEquals2(d, 0, tolerance) ? 0 : Math.sign(d);
}
/** Checks if a point p lies on this line. */
contains(p, tolerance) {
return this.side(p, tolerance) === 0;
}
/** Gets the point at a specific offset along the line (opposite of .offset()). */
at(t) {
return Point.interpolate(this.p1, this.p2, t);
}
// ---------------------------------------------------------------------------
transform(m) {
return new this.constructor(this.p1.transform(m), this.p2.transform(m));
}
/** Rotates this line by a given angle (in radians), optionally around point `c`. */
rotate(a, c = ORIGIN) {
if (nearlyEquals2(a, 0)) return this;
return new this.constructor(this.p1.rotate(a, c), this.p2.rotate(a, c));
}
reflect(l) {
return new this.constructor(this.p1.reflect(l), this.p2.reflect(l));
}
scale(sx, sy = sx) {
return new this.constructor(this.p1.scale(sx, sy), this.p2.scale(sx, sy));
}
shift(x, y = x) {
return new this.constructor(this.p1.shift(x, y), this.p2.shift(x, y));
}
translate(p) {
return this.shift(p.x, p.y);
}
equals(other, tolerance) {
return this.contains(other.p1, tolerance) && this.contains(other.p2, tolerance);
}
toString() {
return `line(${this.p1},${this.p2})`;
}
};
var Ray = class extends Line {
constructor() {
super(...arguments);
this.type = "ray";
}
equals(other, tolerance) {
if (other.type !== "ray") return false;
if (!this.p1.equals(other.p1, tolerance)) return false;
if (this.p2.equals(other.p2, tolerance)) return true;
return other.contains(this.p2, tolerance) || this.contains(other.p2, tolerance);
}
contains(p, tolerance) {
if (!super.contains(p, tolerance)) return false;
const offset = this.offset(p);
return nearlyEquals2(offset, 0, tolerance) || offset > 0;
}
toString() {
return `ray(${this.p1},${this.p2})`;
}
};
var Segment = class _Segment extends Line {
constructor() {
super(...arguments);
this.type = "segment";
}
contains(p, tolerance) {
if (!super.contains(p, tolerance)) return false;
if (this.p1.equals(p, tolerance) || this.p2.equals(p, tolerance)) return true;
if (nearlyEquals2(this.p1.x, this.p2.x, tolerance)) {
return isBetween(p.y, this.p1.y, this.p2.y);
} else {
return isBetween(p.x, this.p1.x, this.p2.x);
}
}
project(p) {
const a = Point.difference(this.p2, this.p1);
const b = Point.difference(p, this.p1);
const q = clamp2(Point.dot(a, b) / this.lengthSquared, 0, 1);
return this.p1.add(a.scale(q));
}
/** Contracts (or expands) a line by a specific ratio. */
contract(x) {
return new _Segment(this.at(x), this.at(1 - x));
}
equals(other, tolerance, oriented = false) {
if (other.type !== "segment") return false;
return this.p1.equals(other.p1, tolerance) && this.p2.equals(other.p2, tolerance) || !oriented && this.p1.equals(other.p2, tolerance) && this.p2.equals(other.p1, tolerance);
}
toString() {
return `segment(${this.p1},${this.p2})`;
}
};
// src/circle.ts
var Circle = class _Circle {
constructor(c = ORIGIN, r = 1) {
this.c = c;
this.r = r;
this.type = "circle";
}
/** The length of the circumference of this circle. */
get circumference() {
return TWO_PI * this.r;
}
/** The area of this circle. */
get area() {
return Math.PI * this.r ** 2;
}
get arc() {
const start = this.c.shift(this.r, 0);
return new Arc(this.c, start, TWO_PI);
}
tangentAt(t) {
const p1 = this.at(t);
const p2 = this.c.rotate(Math.PI / 2, p1);
return new Line(p1, p2);
}
collision(r) {
const tX = this.c.x < r.p.x ? r.p.x : this.c.x > r.p.x + r.w ? r.p.x + r.w : this.c.x;
const tY = this.c.y < r.p.y ? r.p.y : this.c.y > r.p.y + r.h ? r.p.y + r.h : this.c.y;
const d = Point.distance(this.c, new Point(tX, tY));
return d <= this.r;
}
// ---------------------------------------------------------------------------
project(p) {
const proj = p.subtract(this.c).unitVector.scale(this.r);
return Point.sum(this.c, proj);
}
at(t) {
const a = TWO_PI * t;
return this.c.shift(this.r * Math.cos(a), this.r * Math.sin(a));
}
offset(p) {
return rad(p, this.c) / TWO_PI;
}
contains(p) {
return Point.distance(p, this.c) <= this.r;
}
// ---------------------------------------------------------------------------
transform(m) {
const scale = Math.abs(m[0][0]) + Math.abs(m[1][1]);
return new _Circle(this.c.transform(m), this.r * scale / 2);
}
rotate(a, c = ORIGIN) {
if (nearlyEquals3(a, 0)) return this;
return new _Circle(this.c.rotate(a, c), this.r);
}
reflect(l) {
return new _Circle(this.c.reflect(l), this.r);
}
scale(sx, sy = sx) {
return new _Circle(this.c.scale(sx, sy), this.r * (sx + sy) / 2);
}
shift(x, y = x) {
return new _Circle(this.c.shift(x, y), this.r);
}
translate(p) {
return this.shift(p.x, p.y);
}
equals(other, tolerance) {
return nearlyEquals3(this.r, other.r, tolerance) && this.c.equals(other.c, tolerance);
}
toString() {
return `circle(${this.c},${this.r})`;
}
};
// src/arc.ts
var Arc = class {
constructor(c, start, angle) {
this.c = c;
this.start = start;
this.angle = angle;
this.type = "arc";
}
get circle() {
return new Circle(this.c, this.radius);
}
get radius() {
return Point.distance(this.c, this.start);
}
get end() {
return this.start.rotate(this.angle, this.c);
}
get startAngle() {
return rad(this.start, this.c);
}
contract(p) {
return new this.constructor(this.c, this.at(p / 2), this.angle * (1 - p));
}
get minor() {
if (this.angle <= Math.PI) return this;
return new this.constructor(this.c, this.end, TWO_PI - this.angle);
}
get major() {
if (this.angle >= Math.PI) return this;
return new this.constructor(this.c, this.end, TWO_PI - this.angle);
}
get center() {
return this.at(0.5);
}
// ---------------------------------------------------------------------------
project(p) {
const start = this.startAngle;
const end = start + this.angle;
let angle = rad(p, this.c);
if (end > TWO_PI && angle < end - TWO_PI) angle += TWO_PI;
angle = clamp3(angle, start, end);
return this.c.shift(this.radius, 0).rotate(angle, this.c);
}
at(t) {
return this.start.rotate(this.angle * t, this.c);
}
offset(p) {
return new Angle(this.start, this.c, p).rad / this.angle;
}
contains(p) {
return p.equals(this.project(p));
}
// ---------------------------------------------------------------------------
transform(m) {
return new this.constructor(
this.c.transform(m),
this.start.transform(m),
this.angle
);
}
/** Rotates this arc by a given angle (in radians), optionally around point `c`. */
rotate(a, c = ORIGIN) {
if (nearlyEquals4(a, 0)) return this;
return new this.constructor(
this.c.rotate(a, c),
this.start.rotate(a, c),
this.angle
);
}
reflect(l) {
return new this.constructor(
this.c.reflect(l),
this.start.reflect(l),
this.angle
);
}
scale(sx, sy = sx) {
return new this.constructor(
this.c.scale(sx, sy),
this.start.scale(sx, sy),
this.angle
);
}
shift(x, y = x) {
return new this.constructor(
this.c.shift(x, y),
this.start.shift(x, y),
this.angle
);
}
translate(p) {
return this.shift(p.x, p.y);
}
equals() {
return false;
}
toString() {
return `arc(${this.c},${this.start},${this.angle})`;
}
};
var Sector = class extends Arc {
constructor() {
super(...arguments);
this.type = "sector";
}
contains(p) {
return Point.distance(p, this.c) <= this.radius && new Angle(this.start, this.c, p).rad <= this.angle;
}
toString() {
return `sector(${this.c},${this.start},${this.angle})`;
}
};
// src/polygon.ts
import { last, tabulate } from "@mathigon/core";
import { nearlyEquals as nearlyEquals6 } from "@mathigon/fermat";
// src/intersection.ts
import { flatten } from "@mathigon/core";
import { isBetween as isBetween2, nearlyEquals as nearlyEquals5, square as square2, subsets } from "@mathigon/fermat";
function liesOnSegment(s, p) {
if (nearlyEquals5(s.p1.x, s.p2.x)) return isBetween2(p.y, s.p1.y, s.p2.y);
return isBetween2(p.x, s.p1.x, s.p2.x);
}
function liesOnRay(r, p) {
if (nearlyEquals5(r.p1.x, r.p2.x)) return (p.y - r.p1.y) / (r.p2.y - r.p1.y) > 0;
return (p.x - r.p1.x) / (r.p2.x - r.p1.x) > 0;
}
function liesOnArc(a, p) {
return isBetween2(a.offset(p), 0, 1);
}
function lineLineIntersection(l1, l2) {
const d1x = l1.p1.x - l1.p2.x;
const d1y = l1.p1.y - l1.p2.y;
const d2x = l2.p1.x - l2.p2.x;
const d2y = l2.p1.y - l2.p2.y;
const d = d1x * d2y - d1y * d2x;
if (nearlyEquals5(d, 0)) return [];
const q1 = l1.p1.x * l1.p2.y - l1.p1.y * l1.p2.x;
const q2 = l2.p1.x * l2.p2.y - l2.p1.y * l2.p2.x;
const x = q1 * d2x - d1x * q2;
const y = q1 * d2y - d1y * q2;
return [new Point(x / d, y / d)];
}
function circleCircleIntersection(c1, c2) {
const d = Point.distance(c1.c, c2.c);
if (d > c1.r + c2.r) return [];
if (d < Math.abs(c1.r - c2.r)) return [];
if (nearlyEquals5(d, 0) && nearlyEquals5(c1.r, c2.r)) return [];
if (nearlyEquals5(d, c1.r + c2.r)) return [new Line(c1.c, c2.c).midpoint];
const a = (square2(c1.r) - square2(c2.r) + square2(d)) / (2 * d);
const b = Math.sqrt(square2(c1.r) - square2(a));
const px = (c2.c.x - c1.c.x) * a / d + (c2.c.y - c1.c.y) * b / d + c1.c.x;
const py = (c2.c.y - c1.c.y) * a / d - (c2.c.x - c1.c.x) * b / d + c1.c.y;
const qx = (c2.c.x - c1.c.x) * a / d - (c2.c.y - c1.c.y) * b / d + c1.c.x;
const qy = (c2.c.y - c1.c.y) * a / d + (c2.c.x - c1.c.x) * b / d + c1.c.y;
return [new Point(px, py), new Point(qx, qy)];
}
function lineCircleIntersection(l, c) {
const dx = l.p2.x - l.p1.x;
const dy = l.p2.y - l.p1.y;
const dr2 = square2(dx) + square2(dy);
const cx = c.c.x;
const cy = c.c.y;
const D = (l.p1.x - cx) * (l.p2.y - cy) - (l.p2.x - cx) * (l.p1.y - cy);
const disc = square2(c.r) * dr2 - square2(D);
if (disc < 0) return [];
const xa = D * dy / dr2;
const ya = -D * dx / dr2;
if (nearlyEquals5(disc, 0)) return [c.c.shift(xa, ya)];
const xb = dx * (dy < 0 ? -1 : 1) * Math.sqrt(disc) / dr2;
const yb = Math.abs(dy) * Math.sqrt(disc) / dr2;
return [c.c.shift(xa + xb, ya + yb), c.c.shift(xa - xb, ya - yb)];
}
function simpleIntersection(a, b) {
let results = [];
const a1 = isArc(a) ? a.circle : a;
const b1 = isArc(b) ? b.circle : b;
if (isLineLike(a) && isLineLike(b)) {
results = lineLineIntersection(a, b);
} else if (isLineLike(a1) && isCircle(b1)) {
results = lineCircleIntersection(a1, b1);
} else if (isCircle(a1) && isLineLike(b1)) {
results = lineCircleIntersection(b1, a1);
} else if (isCircle(a1) && isCircle(b1)) {
results = circleCircleIntersection(a1, b1);
}
for (const x of [a, b]) {
if (isSegment(x)) results = results.filter((i) => liesOnSegment(x, i));
if (isRay(x)) results = results.filter((i) => liesOnRay(x, i));
if (isArc(x)) results = results.filter((i) => liesOnArc(x, i));
}
return results;
}
function intersections(...elements) {
if (elements.length < 2) return [];
if (elements.length > 2) {
return flatten(subsets(elements, 2).map((e) => intersections(...e)));
}
let [a, b] = elements;
if (isAngle(a)) a = a.shape(true);
if (isAngle(b)) b = b.shape(true);
if (isPolygonLike(b)) [a, b] = [b, a];
if (isPolygonLike(a)) {
const results = isLineLike(b) ? a.points.filter((p) => b.contains(p)) : [];
for (const e of a.edges) results.push(...intersections(e, b));
return results;
}
return simpleIntersection(a, b);
}
// src/polygon.ts
var Polygon = class _Polygon {
constructor(...points) {
this.type = "polygon";
this.points = points;
}
get circumference() {
if (this.points.length <= 1) return 0;
let length = Point.distance(this.points[0], last(this.points));
for (let i = 1; i < this.points.length; ++i) {
length += Point.distance(this.points[i - 1], this.points[i]);
}
return length;
}
/**
* The (signed) area of this polygon. The result is positive if the vertices
* are ordered clockwise, and negative otherwise.
*/
get signedArea() {
const p = this.points;
const n = p.length;
let A = p[n - 1].x * p[0].y - p[0].x * p[n - 1].y;
for (let i = 1; i < n; ++i) {
A += p[i - 1].x * p[i].y - p[i].x * p[i - 1].y;
}
return A / 2;
}
get area() {
return Math.abs(this.signedArea);
}
get centroid() {
const p = this.points;
const n = p.length;
let Cx = 0;
for (let i = 0; i < n; ++i) Cx += p[i].x;
let Cy = 0;
for (let i = 0; i < n; ++i) Cy += p[i].y;
return new Point(Cx / n, Cy / n);
}
get edges() {
const n = this.points.length;
const edges = [];
for (let i = 0; i < n; ++i) {
edges.push(new Segment(this.points[i], this.points[(i + 1) % n]));
}
return edges;
}
get radius() {
const c = this.centroid;
const radii = this.points.map((p) => Point.distance(p, c));
return Math.max(...radii);
}
/** The oriented version of this polygon (vertices in clockwise order). */
get oriented() {
if (this.signedArea >= 0) return this;
const points = [...this.points].reverse();
return new this.constructor(...points);
}
/** Checks if two polygons p1 and p2 collide. */
static collision(p1, p2, tolerance) {
if (p1.points.some((q) => p2.contains(q))) return true;
if (p2.points.some((q) => p1.contains(q))) return true;
for (const e1 of p1.edges) {
for (const e2 of p2.edges) {
if (intersections(e1, e2)[0]) return true;
}
}
return p1.equals(p2, tolerance);
}
/** Creates a regular polygon. */
static regular(n, radius = 1) {
const da = TWO_PI / n;
const a0 = Math.PI / 2 - da / 2;
const points = tabulate((i) => Point.fromPolar(a0 + da * i, radius), n);
return new _Polygon(...points);
}
/** Interpolates the points of two polygons */
static interpolate(p1, p2, t = 0.5) {
const points = p1.points.map(
(p, i) => Point.interpolate(p, p2.points[i], t)
);
return new _Polygon(...points);
}
static convexHull(...points) {
if (points.length <= 3) return new _Polygon(...points);
const sorted = points.sort((a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y);
const sortedReverse = sorted.slice(0).reverse();
const upper = [];
const lower = [];
for (const [source, target] of [[sorted, upper], [sortedReverse, lower]]) {
for (const p of source) {
while (target.length >= 2) {
const p1 = target[target.length - 1];
const p2 = target[target.length - 2];
if ((p1.x - p2.x) * (p.y - p2.y) >= (p.x - p2.x) * (p1.y - p2.y)) {
target.pop();
} else {
break;
}
}
target.push(p);
}
target.pop();
}
return new _Polygon(...upper.concat(lower));
}
// ---------------------------------------------------------------------------
/**
* Checks if a point p lies inside this polygon, by using a ray-casting
* algorithm and calculating the number of intersections.
*/
contains(p) {
let inside = false;
for (const e of this.edges) {
if (e.p1.equals(p) || e.contains(p)) return false;
if (e.p1.y > p.y === e.p2.y > p.y) continue;
const det = (e.p2.x - e.p1.x) / (e.p2.y - e.p1.y);
if (p.x < det * (p.y - e.p1.y) + e.p1.x) inside = !inside;
}
return inside;
}
at(t) {
if (t < 0) t += Math.floor(t);
const offset = t * this.circumference;
let cum = 0;
for (const e of this.edges) {
const l = e.length;
if (cum + l > offset) return e.at((offset - cum) / l);
cum += l;
}
return this.points[0];
}
offset(p) {
const edges = this.edges;
const proj = findClosest(p, this.edges) || [this.points[0], 0];
let offset = 0;
for (let i = 0; i < proj[1]; ++i) offset += edges[i].length;
offset += edges[proj[1]].offset(p) * edges[proj[1]].length;
return offset / this.circumference;
}
project(p) {
const proj = findClosest(p, this.edges);
return proj ? proj[0] : this.points[0];
}
/** Center this polygon on a given point or the origin */
centerAt(on = ORIGIN) {
return this.translate(on.subtract(this.centroid));
}
// ---------------------------------------------------------------------------
transform(m) {
return new this.constructor(...this.points.map((p) => p.transform(m)));
}
/** Rotates this polygon by a given angle (in radians), optionally around point `center`. */
rotate(a, center = ORIGIN) {
if (nearlyEquals6(a, 0)) return this;
const points = this.points.map((p) => p.rotate(a, center));
return new this.constructor(...points);
}
reflect(line) {
const points = this.points.map((p) => p.reflect(line));
return new this.constructor(...points);
}
scale(sx, sy = sx) {
const points = this.points.map((p) => p.scale(sx, sy));
return new this.constructor(...points);
}
shift(x, y = x) {
const points = this.points.map((p) => p.shift(x, y));
return new this.constructor(...points);
}
translate(p) {
return this.shift(p.x, p.y);
}
equals(other, tolerance, oriented) {
const n = this.points.length;
if (n !== other.points.length) return false;
const p1 = oriented ? this : this.oriented;
const p2 = oriented ? other : other.oriented;
for (let offset = 0; offset < n; ++offset) {
if (p1.points.every((p, i) => p.equals(p2.points[(i + offset) % n], tolerance))) {
return true;
}
}
return false;
}
toString() {
return `polygon(${this.points.join(",")})`;
}
};
var Polyline = class extends Polygon {
constructor() {
super(...arguments);
this.type = "polyline";
}
get circumference() {
return this.length;
}
get length() {
let length = 0;
for (let i = 1; i < this.points.length; ++i) {
length += Point.distance(this.points[i - 1], this.points[i]);
}
return length;
}
/** @returns {Segment[]} */
get edges() {
const edges = [];
for (let i = 0; i < this.points.length - 1; ++i) {
edges.push(new Segment(this.points[i], this.points[i + 1]));
}
return edges;
}
toString() {
return `polyline(${this.points.join(",")})`;
}
};
var Triangle = class extends Polygon {
constructor() {
super(...arguments);
this.type = "triangle";
}
get circumcircle() {
const [a, b, c] = this.points;
const d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
const ux = (a.x ** 2 + a.y ** 2) * (b.y - c.y) + (b.x ** 2 + b.y ** 2) * (c.y - a.y) + (c.x ** 2 + c.y ** 2) * (a.y - b.y);
const uy = (a.x ** 2 + a.y ** 2) * (c.x - b.x) + (b.x ** 2 + b.y ** 2) * (a.x - c.x) + (c.x ** 2 + c.y ** 2) * (b.x - a.x);
const center = new Point(ux / d, uy / d);
const radius = Point.distance(center, this.points[0]);
if (isNaN(radius) || radius > Number.MAX_SAFE_INTEGER) return;
return new Circle(center, radius);
}
get incircle() {
const edges = this.edges;
const sides = edges.map((e) => e.length);
const total2 = sides[0] + sides[1] + sides[2];
const [a, b, c] = this.points;
const ux = sides[1] * a.x + sides[2] * b.x + sides[0] * c.x;
const uy = sides[1] * a.y + sides[2] * b.y + sides[0] * c.y;
const center = new Point(ux / total2, uy / total2);
const radius = center.distanceFromLine(edges[0]);
return isNaN(radius) ? void 0 : new Circle(center, radius);
}
get orthocenter() {
const [a, b, c] = this.points;
const h1 = new Line(a, b).perpendicular(c);
const h2 = new Line(a, c).perpendicular(b);
return intersections(h1, h2)[0];
}
};
// src/angle.ts
var RAD_TO_DEG = 180 / Math.PI;
var DEG_TO_RAD = Math.PI / 180;
function toDeg(n) {
return n * RAD_TO_DEG;
}
function toRad(n) {
return n * DEG_TO_RAD;
}
var Angle = class _Angle {
constructor(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
this.type = "angle";
}
static fromDegrees(val) {
return _Angle.fromRadians(val * (Math.PI / 180));
}
static fromRadians(val) {
const p1 = new Point(1, 0);
const p2 = p1.rotate(val);
return new _Angle(p1, ORIGIN, p2);
}
/** Checks if `a` and `b` are roughly equivalent (by default, within one degree of eachother) */
static equals(a, b, precision = Math.PI / 360) {
return nearlyEquals7(a.rad, b.rad, precision);
}
/** The size, in radians, of this angle. */
get rad() {
const phiA = Math.atan2(this.a.y - this.b.y, this.a.x - this.b.x);
const phiC = Math.atan2(this.c.y - this.b.y, this.c.x - this.b.x);
let phi = phiC - phiA;
if (phi < 0) phi += TWO_PI;
return phi;
}
/** The size, in degrees, of this angle. */
get deg() {
return this.rad * 180 / Math.PI;
}
/** Checks if this angle is right-angled. */
get isRight() {
return nearlyEquals7(this.rad, Math.PI / 2, Math.PI / 360);
}
/** The bisector of this angle. */
get bisector() {
if (this.b.equals(this.a)) return void 0;
if (this.b.equals(this.c)) return void 0;
const phiA = Math.atan2(this.a.y - this.b.y, this.a.x - this.b.x);
const phiC = Math.atan2(this.c.y - this.b.y, this.c.x - this.b.x);
let phi = (phiA + phiC) / 2;
if (phiA > phiC) phi += Math.PI;
const x = Math.cos(phi) + this.b.x;
const y = Math.sin(phi) + this.b.y;
return new Line(this.b, new Point(x, y));
}
/** Returns the smaller one of this and its supplementary angle. */
get sup() {
return this.rad < Math.PI ? this : new _Angle(this.c, this.b, this.a);
}
/** Returns the Arc element corresponding to this angle. */
get arc() {
return new Arc(this.b, this.a, this.rad);
}
// ---------------------------------------------------------------------------
/** Radius of the arc or sector representing this angle. */
get radius() {
return 24 + 20 * (1 - clamp4(this.rad, 0, Math.PI) / Math.PI);
}
/** Shape object that can be used to draw this angle. */
shape(filled = true, radius, round) {
if (this.a.equals(this.b) || this.c.equals(this.b)) return new Polygon(ORIGIN);
const angled = this.isRight && !round;
if (!radius) radius = angled ? 20 : this.radius;
const ba = new Segment(this.b, this.a);
const a = ba.at(radius / ba.length);
if (angled) {
const bc = Point.difference(this.c, this.b).unitVector.scale(radius);
if (filled) return new Polygon(this.b, a, a.add(bc), this.b.add(bc));
return new Polyline(a, a.add(bc), this.b.add(bc));
}
if (filled) return new Sector(this.b, a, this.rad);
return new Arc(this.b, a, this.rad);
}
// ---------------------------------------------------------------------------
// These functions are just included for compatibility with GeoPath
project(p) {
return this.contains(p) ? p : this.shape(true).project(p);
}
at() {
return this.c;
}
offset() {
return 0;
}
contains(p) {
return this.shape(true).contains(p);
}
// ---------------------------------------------------------------------------
transform(m) {
return new _Angle(this.a.transform(m), this.b.transform(m), this.c.transform(m));
}
rotate(a, c) {
if (nearlyEquals7(a, 0)) return this;
return new _Angle(this.a.rotate(a, c), this.b.rotate(a, c), this.c.rotate(a, c));
}
reflect(l) {
return new _Angle(this.a.reflect(l), this.b.reflect(l), this.c.reflect(l));
}
scale(sx, sy = sx) {
return new _Angle(this.a.scale(sx, sy), this.b.scale(sx, sy), this.c.scale(sx, sy));
}
shift(x, y = x) {
return new _Angle(this.a.shift(x, y), this.b.shift(x, y), this.c.shift(x, y));
}
translate(p) {
return new _Angle(this.a.translate(p), this.b.translate(p), this.c.translate(p));
}
equals(a, precision) {
return _Angle.equals(a, this, precision);
}
toString() {
return `angle(${this.a},${this.b},${this.c})`;
}
};
// src/bounds.ts
import { isBetween as isBetween4 } from "@mathigon/fermat";
// src/rectangle.ts
import { isBetween as isBetween3, nearlyEquals as nearlyEquals8 } from "@mathigon/fermat";
var Rectangle = class _Rectangle {
constructor(p, w = 1, h = w) {
this.p = p;
this.w = w;
this.h = h;
this.type = "rectangle";
}
/** Creates the smallest rectangle containing all given points. */
static aroundPoints(points) {
let xMin = Infinity;
let xMax = -Infinity;
let yMin = Infinity;
let yMax = -Infinity;
for (const p of points) {
xMin = xMin < p.x ? xMin : p.x;
xMax = xMax > p.x ? xMax : p.x;
yMin = yMin < p.y ? yMin : p.y;
yMax = yMax > p.y ? yMax : p.y;
}
return new _Rectangle(new Point(xMin, yMin), xMax - xMin, yMax - yMin);
}
get center() {
return new Point(this.p.x + this.w / 2, this.p.y + this.h / 2);
}
get centroid() {
return this.center;
}
get circumference() {
return 2 * Math.abs(this.w) + 2 * Math.abs(this.h);
}
get area() {
return Math.abs(this.signedArea);
}
get signedArea() {
return this.w * this.h;
}
/** @returns {Segment[]} */
get edges() {
return this.polygon.edges;
}
/** @returns {Point[]} */
get points() {
return this.polygon.points;
}
/** A polygon class representing this rectangle. */
get polygon() {
const b = new Point(this.p.x + this.w, this.p.y);
const c = new Point(this.p.x + this.w, this.p.y + this.h);
const d = new Point(this.p.x, this.p.y + this.h);
return new Polygon(this.p, b, c, d);
}
get bounds() {
return new Bounds(this.p.x, this.p.x + this.w, this.p.y, this.p.y + this.h);
}
collision(r) {
return this.p.x < r.p.x + r.w && this.p.x + this.w > r.p.x && this.p.y < r.p.y + r.h && this.p.y + this.h > r.p.y || this.equals(r.polygon);
}
padding(top, right, bottom, left) {
return new _Rectangle(this.p.shift(-left, -top), this.w + left + right, this.h + top + bottom);
}
get unsigned() {
if (this.w > 0 && this.h > 0) return this;
const p = this.p.shift(this.w < 0 ? this.w : 0, this.h < 0 ? this.h : 0);
return new _Rectangle(p, Math.abs(this.w), Math.abs(this.h));
}
// ---------------------------------------------------------------------------
contains(p, tolerance) {
return isBetween3(p.x, this.p.x, this.p.x + this.w, tolerance) && isBetween3(p.y, this.p.y, this.p.y + this.h, tolerance);
}
project(p) {
let q = void 0;
for (const e of this.edges) {
const q1 = e.project(p);
if (!q || Point.distance(p, q1) < Point.distance(p, q)) q = q1;
}
return q;
}
at(t) {
return this.polygon.at(t);
}
offset(p) {
return this.polygon.offset(p);
}
get oriented() {
return this.polygon.oriented;
}
// ---------------------------------------------------------------------------
transform(m) {
return this.polygon.transform(m);
}
/** Rotates this rectangle by a given angle (in radians), optionally around point `c`. */
rotate(a, c = ORIGIN) {
if (nearlyEquals8(a, 0)) return this;
return this.polygon.rotate(a, c);
}
reflect(l) {
return this.polygon.reflect(l);
}
scale(sx, sy = sx) {
return new _Rectangle(this.p.scale(sx, sy), this.w * sx, this.h * sy);
}
shift(x, y = x) {
return new _Rectangle(this.p.shift(x, y), this.w, this.h);
}
translate(p) {
return this.shift(p.x, p.y);
}
equals(other) {
return this.polygon.equals(other);
}
toString() {
return `rectangle(${this.p},${this.w},${this.h})`;
}
};
// src/bounds.ts
var Bounds = class _Bounds {
/**
* Use the `errorHandling` option to decide how to deal with cases where the
* min and max values are in the wrong order.
*/
constructor(xMin, xMax, yMin, yMax, errorHandling) {
this.xMin = xMin;
this.xMax = xMax;
this.yMin = yMin;
this.yMax = yMax;
if (errorHandling === "swap") {
if (this.dx < 0) [this.xMin, this.xMax] = [xMax, xMin];
if (this.dy < 0) [this.yMin, this.yMax] = [yMax, yMin];
} else if (errorHandling === "center") {
if (this.dx < 0) this.xMin = this.xMax = (xMin + xMax) / 2;
if (this.dy < 0) this.yMin = this.yMax = (yMin + yMax) / 2;
}
}
contains(p) {
return this.containsX(p) && this.containsY(p);
}
containsX(p) {
return isBetween4(p.x, this.xMin, this.xMax);
}
containsY(p) {
return isBetween4(p.y, this.yMin, this.yMax);
}
resize(dx, dy) {
return new _Bounds(this.xMin, this.xMax + dx, this.yMin, this.yMax + dy);
}
get dx() {
return this.xMax - this.xMin;
}
get dy() {
return this.yMax - this.yMin;
}
get xRange() {
return [this.xMin, this.xMax];
}
get yRange() {
return [this.yMin, this.yMax];
}
extend(top, right = top, bottom = top, left = right) {
return new _Bounds(this.xMin - left, this.xMax + right, this.yMin - top, this.yMax + bottom);
}
get rect() {
return new Rectangle(new Point(this.xMin, this.yMin), this.dx, this.dy);
}
get center() {
return new Point(this.xMin + this.dx / 2, this.yMin + this.dy / 2);
}
get flip() {
return new _Bounds(this.yMin, this.yMax, this.xMin, this.xMax);
}
};
// src/draw-canvas.ts
function drawCanvas(ctx, obj, options = {}) {
if (isAngle(obj)) return drawCanvas(ctx, obj.shape(!!options.fill), options);
if (options.fill) ctx.fillStyle = options.fill;
if (options.opacity) ctx.globalAlpha = options.opacity;
if (options.stroke) {
ctx.strokeStyle = options.stroke;
ctx.lineWidth = options.strokeWidth || 1;
if (options.lineCap) ctx.lineCap = options.lineCap;
if (options.lineJoin) ctx.lineJoin = options.lineJoin;
}
ctx.beginPath();
if (isSegment(obj)) {
ctx.moveTo(obj.p1.x, obj.p1.y);
ctx.lineTo(obj.p2.x, obj.p2.y);
} else if (isLineLike(obj)) {
if (!options.box) return;
let [start, end] = intersections(obj, options.box);
if (isRay(obj)) end = obj.p1;
if (!start || !end) return;
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
} else if (isCircle(obj)) {
ctx.arc(obj.c.x, obj.c.y, obj.r, 0, TWO_PI);
} else if (isPolygonLike(obj)) {
const points = obj.points;
ctx.moveTo(points[0].x, points[0].y);
for (const p of points.slice(1)) ctx.lineTo(p.x, p.y);
ctx.closePath();
} else if (isPolyline(obj)) {
ctx.moveTo(obj.points[0].x, obj.points[0].y);
for (const p of obj.points.slice(1)) ctx.lineTo(p.x, p.y);
} else if (isEllipse(obj)) {
ctx.ellipse(obj.c.x, obj.c.y, obj.a, obj.b, obj.angle, 0, TWO_PI);
}
if (options.fill) ctx.fill();
if (options.stroke) ctx.stroke();
}
// src/draw-svg.ts
import { isOneOf } from "@mathigon/core";
import { clamp as clamp5 } from "@mathigon/fermat";
var CIRCLE_MAGIC = 4 * (Math.sqrt(2) - 1) / 3;
function drawArc(a, b, c) {
const orient = b.x * (c.y - a.y) + a.x * (b.y - c.y) + c.x * (a.y - b.y);
const sweep = orient > 0 ? 1 : 0;
const size = Point.distance(b, a);
return [a.x, `${a.y}A${size}`, size, 0, sweep, 1, c.x, c.y].join(",");
}
function drawPath(...points) {
return `M${points.map((p) => `${p.x},${p.y}`).join("L")}`;
}
function drawLineMark(x, type) {
const p = x.perpendicularVector.scale(6);
const n = x.unitVector.scale(3);
const m = x.midpoint;
switch (type) {
case "bar":
return drawPath(m.add(p), m.add(p.inverse));
case "bar2":
return drawPath(m.add(n).add(p), m.add(n).add(p.inverse)) + drawPath(m.add(n.inverse).add(p), m.add(n.inverse).add(p.inverse));
case "arrow":
return drawPath(
m.add(n.inverse).add(p),
m.add(n),
m.add(n.inverse).add(p.inverse)
);
case "arrow2":
return drawPath(
m.add(n.scale(-2)).add(p),
m,
m.add(n.scale(-2)).add(p.inverse)
) + drawPath(m.add(p), m.add(n.scale(2)), m.add(p.inverse));
default:
return "";
}
}
function arrowPath(start, normal) {
if (!start || !normal) return "";
const perp = normal.perpendicular;
const a = start.add(normal.scale(9)).add(perp.scale(9));
const b = start.add(normal.scale(9)).add(perp.scale(-9));
return drawPath(a, start, b);
}
function drawLineArrows(x, type) {
let path = "";
if (isOneOf(type, "start", "both")) {
path += arrowPath(x.p1, x.unitVector);
}
if (isOneOf(type, "end", "both")) {
path += arrowPath(x.p2, x.unitVector.inverse);
}
return path;
}
function drawArcArrows(x, type) {
let path = "";
if (isOneOf(type, "start", "both")) {
const normal = new Line(x.c, x.start).perpendicularVector.inverse;
path += arrowPath(x.start, normal);
}
if (isOneOf(type, "end", "both")) {
const normal = new Line(x.c, x.end).perpendicularVector;
path += arrowPath(x.end, normal);
}
return path;
}
function getBezierPoints(points, radius) {
const length0 = Point.distance(points[0], points[1]);
const length1 = Point.distance(points[1], points[2]);
const r1 = Math.max(0.1, length0 / 2);
const r2 = Math.max(0.1, length1 / 2);
const rad2 = Math.min(radius, r1, r2);
const d1 = rad2 / length0;
const d2 = rad2 / length1;
const shift = 1 - CIRCLE_MAGIC;
const p1 = Point.interpolate(points[0], points[1], clamp5(1 - d1, 0, 1));
const p2 = Point.interpolate(points[0], points[1], clamp5(1 - d1 * shift, 0, 1));
const p3 = Point.interpolate(points[1], points[2], clamp5(d2 * shift, 0, 1));
const p4 = Point.interpolate(points[1], points[2], clamp5(d2, 0, 1));
return [p1, p2, p3, p4];
}
function drawRoundedPath(points, radius, close = false) {
if (radius < 0) radius = 0;
let path = "M";
if (!close) {
path += `${points[0].x} ${points[0].y}`;
} else {
const p1 = points[points.length - 1];
const p2 = points[0];
const p3 = points[1];
const offsets = getBezierPoints([p1, p2, p3], radius);
path += `${offsets[3].x} ${offsets[3].y}`;
}
for (let index = 0; index < points.length; index++) {
if (index < points.length - 2 || close) {
const p1 = points[index];
const p2 = points[(index + 1) % points.length];
const p3 = points[(index + 2) % points.length];
const offsets = getBezierPoints([p1, p2, p3], radius).map((p) => `${p.x} ${p.y}`);
path += `L${offsets[0]}C${offsets[1]} ${offsets[2]} ${offsets[3]}`;
} else if (index === points.length - 2 && !close) {
path += `L${points[index + 1].x} ${points[index + 1].y}`;
}
}
return path;
}
function drawRoundedRect(rect, tl, tr = tl, br = tl, bl = tr) {
const { p, w, h } = rect;
return `M${p.x} ${p.y + tl}a${tl} ${tl} 0 0 1 ${tl} ${-tl}h${w - tl - tr}a${tr} ${tr} 0 0 1 ${tr} ${tr}v${h - tr - br}a${br} ${br} 0 0 1 ${-br} ${br}h${-w + bl + br}a${bl} ${bl} 0 0 1 ${-bl} ${-bl}Z`;
}
function drawSVG(obj, options = {}) {
if (isAngle(obj)) {
const shape = obj.shape(!!options.fill, options.size, options.round);
return drawSVG(shape, options);
}
if (isSegment(obj)) {
if (obj.p1.equals(obj.p2)) return "";
let line = drawPath(obj.p1, obj.p2);
if (options.mark) line += drawLineMark(obj, options.mark);
if (options.arrows) line += drawLineArrows(obj, options.arrows);
return line;
}
if (isRay(obj)) {
if (!options.box) return "";
const end = intersections(obj, options.box)[0];
if (!end) return "";
let line = drawPath(obj.p1, end);
if (options.mark) line += drawLineMark(obj, options.mark);
return line;
}
if (isLine(obj)) {
if (!options.box) return "";
const points = intersections(obj, options.box);
if (points.length < 2) return "";
let line = drawPath(points[0], points[1]);
if (options.mark) line += drawLineMark(obj, options.mark);
return line;
}
if (isCircle(obj)) {
return `M${obj.c.x - obj.r} ${obj.c.y}a${obj.r},${obj.r} 0 1 0 ${2 * obj.r} 0a${obj.r} ${obj.r} 0 1 0 ${-2 * obj.r} 0Z`;
}
if (isEllipse(obj)) {
const [u, v] = obj.majorVertices;
const rot = toDeg(obj.angle);
return `M${u.x} ${u.y}A${obj.a} ${obj.b} ${rot} 0 0 ${v.x} ${v.y}A${obj.a} ${obj.b} ${rot} 0 0 ${u.x} ${u.y}Z`;
}
if (isArc(obj)) {
let path = `M${drawArc(obj.start, obj.c, obj.end)}`;
if (options.arrows) path += drawArcArrows(obj, options.arrows);
return path;
}
if (isSector(obj)) {
return `M${obj.c.x} ${obj.c.y} L ${drawArc(obj.start, obj.c, obj.end)}Z`;
}
if (isPolyline(obj)) {
if (options.cornerRadius) return drawRoundedPath(obj.points, options.cornerRadius, false);
return drawPath(...obj.points);
}
if (isPolygon(obj) || isRectangle(obj) && options.cornerRadius) {
if (options.cornerRadius) {
return drawRoundedPath(obj.points, options.cornerRadius, true);
}
return `${drawPath(...obj.points)}Z`;
}
if (isRectangle(obj)) {
return `${drawPath(...obj.polygon.points)}Z`;
}
return "";
}
// src/ellipse.ts
import { nearlyEquals as nearlyEquals9, quadratic } from "@mathigon/fermat";
var Ellipse = class _Ellipse {
/**
* @param c Center of the ellipse
* @param a Major axis
* @param b Minor axis
* @param angle The rotation of the major axis of the ellipse.
*/
constructor(c, a, b, angle = 0) {
this.c = c;
this.type = "ellipse";
if (a < b) {
[a, b] = [b, a];
angle += Math.PI / 2;
}
this.a = a;
this.b = b;
this.angle = angle;
const f = Math.sqrt(a ** 2 - b ** 2);
this.f1 = this.c.add(new Point(-f, 0).rotate(angle));
this.f2 = this.c.add(new Point(f, 0).rotate(angle));
}
get rx() {
return nearlyEquals9(this.angle, 0) ? this.a : nearlyEquals9(this.angle, Math.PI / 2) ? this.b : void 0;
}
get ry() {
return nearlyEquals9(this.angle, 0) ? this.b : nearlyEquals9(this.angle, Math.PI / 2) ? this.a : void 0;
}
normalAt(p) {
return new Angle(this.f1, p, this.f2).bisector;
}
/** Intersection between an ellipse and a line. */
intersect(line) {
line = line.rotate(-this.angle, this.c);
const dx = line.p1.x - line.p2.x;
const dy = line.p1.y - line.p2.y;
const px = this.c.x - line.p1.x;
const py = this.c.y - line.p1.y;
const A = (dx / this.a) ** 2 + (dy / this.b) ** 2;
const B = 2 * px * dx / this.a ** 2 + 2 * py * dy / this.b ** 2;
const C = (px / this.a) ** 2 + (py / this.b) ** 2 - 1;
const points = quadratic(A, B, C);
return points.map((t) => line.at(t).rotate(this.angle, this.c));
}
/**
* Creates a new Ellipse. StringLength is the length of string from one foci
* to a point on the circumference, to the other foci.
*/
static fromFoci(f1, f2, stringLength) {
const c = Point.distance(f1, f2) / 2;
const a = stringLength / 2;
const b = Math.sqrt(a ** 2 - c ** 2);
const angle = new Line(f1, f2).angle;
return new _Ellipse(Point.interpolate(f1, f2), a, b, angle);
}
// ---------------------------------------------------------------------------
get majorVertices() {
return [
this.c.add(new Point(-this.a, 0).rotate(this.angle)),
this.c.add(new Point(this.a, 0).rotate(this.angle))
];
}
get minorVertices() {
return [
this.c.add(new Point(0, -this.b).rotate(this.angle)),
this.c.add(new Point(0, this.b).rotate(this.angle))
];
}
get extremes() {
const { a, b, angle } = this;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const sqSum = a ** 2 + b ** 2;
const sqDiff = (a ** 2 - b ** 2) * Math.cos(2 * angle);
const yMax = Math.sqrt((sqSum - sqDiff) / 2);
const xAtYMax = yMax * sqSum * sin * cos / (a ** 2 * sin ** 2 + b ** 2 * cos ** 2);
const xMax = Math.sqrt((sqSum + sqDiff) / 2);
const yAtXMax = xMax * sqSum * si