UNPKG

@js-draw/math

Version:
262 lines (261 loc) 10.8 kB
import Rect2 from './Rect2.mjs'; import { Vec2 } from '../Vec2.mjs'; import Parameterized2DShape from './Parameterized2DShape.mjs'; /** * Represents a line segment. A `LineSegment2` is immutable. * * @example * ```ts,runnable,console * import {LineSegment2, Vec2} from '@js-draw/math'; * const l = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 2)); * console.log('length: ', l.length); * console.log('direction: ', l.direction); * console.log('bounding box: ', l.bbox); * ``` */ export class LineSegment2 extends Parameterized2DShape { /** Creates a new `LineSegment2` from its endpoints. */ constructor(point1, point2) { super(); this.point1 = point1; this.point2 = point2; this.bbox = Rect2.bboxOf([point1, point2]); this.direction = point2.minus(point1); this.length = this.direction.magnitude(); // Normalize if (this.length > 0) { this.direction = this.direction.times(1 / this.length); } } /** * Returns the smallest line segment that contains all points in `points`, or `null` * if no such line segment exists. * * @example * ```ts,runnable,console * import {LineSegment2, Vec2} from '@js-draw/math'; * console.log(LineSegment2.ofSmallestContainingPoints([Vec2.of(1, 0), Vec2.of(0, 1)])); * ``` */ static ofSmallestContainingPoints(points) { if (points.length <= 1) return null; const sorted = [...points].sort((a, b) => (a.x !== b.x ? a.x - b.x : a.y - b.y)); const line = new LineSegment2(sorted[0], sorted[sorted.length - 1]); for (const point of sorted) { if (!line.containsPoint(point)) { return null; } } return line; } // Accessors to make LineSegment2 compatible with bezier-js's // interface /** Alias for `point1`. */ get p1() { return this.point1; } /** Alias for `point2`. */ get p2() { return this.point2; } get center() { return this.point1.lerp(this.point2, 0.5); } /** * Gets a point a **distance** `t` along this line. * * @deprecated */ get(t) { return this.point1.plus(this.direction.times(t)); } /** * Returns a point a fraction, `t`, along this line segment. * Thus, `segment.at(0)` returns `segment.p1` and `segment.at(1)` returns * `segment.p2`. * * `t` should be in `[0, 1]`. */ at(t) { return this.get(t * this.length); } normalAt(_t) { return this.direction.orthog(); } tangentAt(_t) { return this.direction; } splitAt(t) { if (t <= 0 || t >= 1) { return [this]; } return [new LineSegment2(this.point1, this.at(t)), new LineSegment2(this.at(t), this.point2)]; } /** * Returns the intersection of this with another line segment. * * **WARNING**: The parameter value returned by this method does not range from 0 to 1 and * is currently a length. * This will change in a future release. * @deprecated */ intersection(other) { // TODO(v2.0.0): Make this return a `t` value from `0` to `1`. // We want x₁(t) = x₂(t) and y₁(t) = y₂(t) // Observe that // x = this.point1.x + this.direction.x · t₁ // = other.point1.x + other.direction.x · t₂ // Thus, // t₁ = (x - this.point1.x) / this.direction.x // = (y - this.point1.y) / this.direction.y // and // t₂ = (x - other.point1.x) / other.direction.x // (and similarly for y) // // Letting o₁ₓ = this.point1.x, o₂ₓ = other.point1.x, // d₁ᵧ = this.direction.y, ... // // We can substitute these into the equations for y: // y = o₁ᵧ + d₁ᵧ · (x - o₁ₓ) / d₁ₓ // = o₂ᵧ + d₂ᵧ · (x - o₂ₓ) / d₂ₓ // ⇒ o₁ᵧ - o₂ᵧ = d₂ᵧ · (x - o₂ₓ) / d₂ₓ - d₁ᵧ · (x - o₁ₓ) / d₁ₓ // = (d₂ᵧ/d₂ₓ)(x) - (d₂ᵧ/d₂ₓ)(o₂ₓ) - (d₁ᵧ/d₁ₓ)(x) + (d₁ᵧ/d₁ₓ)(o₁ₓ) // = (x)(d₂ᵧ/d₂ₓ - d₁ᵧ/d₁ₓ) - (d₂ᵧ/d₂ₓ)(o₂ₓ) + (d₁ᵧ/d₁ₓ)(o₁ₓ) // ⇒ (x)(d₂ᵧ/d₂ₓ - d₁ᵧ/d₁ₓ) = o₁ᵧ - o₂ᵧ + (d₂ᵧ/d₂ₓ)(o₂ₓ) - (d₁ᵧ/d₁ₓ)(o₁ₓ) // ⇒ x = (o₁ᵧ - o₂ᵧ + (d₂ᵧ/d₂ₓ)(o₂ₓ) - (d₁ᵧ/d₁ₓ)(o₁ₓ))/(d₂ᵧ/d₂ₓ - d₁ᵧ/d₁ₓ) // = (d₁ₓd₂ₓ)(o₁ᵧ - o₂ᵧ + (d₂ᵧ/d₂ₓ)(o₂ₓ) - (d₁ᵧ/d₁ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ) // = ((o₁ᵧ - o₂ᵧ)((d₁ₓd₂ₓ)) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ) // ⇒ y = o₁ᵧ + d₁ᵧ · (x - o₁ₓ) / d₁ₓ = ... let resultPoint, resultT; // Consider very-near-vertical lines to be vertical --- not doing so can lead to // precision error when dividing by this.direction.x. const small = 4e-13; if (Math.abs(this.direction.x) < small) { // Vertical line: Where does the other have x = this.point1.x? // x = o₁ₓ = o₂ₓ + d₂ₓ · (y - o₂ᵧ) / d₂ᵧ // ⇒ (o₁ₓ - o₂ₓ)(d₂ᵧ/d₂ₓ) + o₂ᵧ = y // Avoid division by zero if (other.direction.x === 0 || this.direction.y === 0) { return null; } const xIntersect = this.point1.x; const yIntersect = ((this.point1.x - other.point1.x) * other.direction.y) / other.direction.x + other.point1.y; resultPoint = Vec2.of(xIntersect, yIntersect); resultT = (yIntersect - this.point1.y) / this.direction.y; } else { // From above, // x = ((o₁ᵧ - o₂ᵧ)(d₁ₓd₂ₓ) + (d₂ᵧd₁ₓ)(o₂ₓ) - (d₁ᵧd₂ₓ)(o₁ₓ))/(d₂ᵧd₁ₓ - d₁ᵧd₂ₓ) const numerator = (this.point1.y - other.point1.y) * this.direction.x * other.direction.x + this.direction.x * other.direction.y * other.point1.x - this.direction.y * other.direction.x * this.point1.x; const denominator = other.direction.y * this.direction.x - this.direction.y * other.direction.x; // Avoid dividing by zero. It means there is no intersection if (denominator === 0) { return null; } const xIntersect = numerator / denominator; const t1 = (xIntersect - this.point1.x) / this.direction.x; const yIntersect = this.point1.y + this.direction.y * t1; resultPoint = Vec2.of(xIntersect, yIntersect); resultT = (xIntersect - this.point1.x) / this.direction.x; } // Ensure the result is in this/the other segment. const resultToP1 = resultPoint.distanceTo(this.point1); const resultToP2 = resultPoint.distanceTo(this.point2); const resultToP3 = resultPoint.distanceTo(other.point1); const resultToP4 = resultPoint.distanceTo(other.point2); if (resultToP1 > this.length || resultToP2 > this.length || resultToP3 > other.length || resultToP4 > other.length) { return null; } return { point: resultPoint, t: resultT, }; } intersects(other) { return this.intersection(other) !== null; } argIntersectsLineSegment(lineSegment) { const intersection = this.intersection(lineSegment); if (intersection) { return [intersection.t / this.length]; } return []; } /** * Returns the points at which this line segment intersects the * given line segment. * * Note that {@link intersects} returns *whether* this line segment intersects another * line segment. This method, by contrast, returns **the point** at which the intersection * occurs, if such a point exists. */ intersectsLineSegment(lineSegment) { const intersection = this.intersection(lineSegment); if (intersection) { return [intersection.point]; } return []; } // Returns the closest point on this to [target] closestPointTo(target) { return this.nearestPointTo(target).point; } nearestPointTo(target) { // Distance from P1 along this' direction. const projectedDistFromP1 = target.minus(this.p1).dot(this.direction); const projectedDistFromP2 = this.length - projectedDistFromP1; const projection = this.p1.plus(this.direction.times(projectedDistFromP1)); if (projectedDistFromP1 > 0 && projectedDistFromP1 < this.length) { return { point: projection, parameterValue: projectedDistFromP1 / this.length }; } if (Math.abs(projectedDistFromP2) < Math.abs(projectedDistFromP1)) { return { point: this.p2, parameterValue: 1 }; } else { return { point: this.p1, parameterValue: 0 }; } } /** * Returns the distance from this line segment to `target`. * * Because a line segment has no interior, this signed distance is equivalent to * the full distance between `target` and this line segment. */ signedDistance(target) { return this.closestPointTo(target).minus(target).magnitude(); } /** Returns a copy of this line segment transformed by the given `affineTransfm`. */ transformedBy(affineTransfm) { return new LineSegment2(affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2)); } /** @inheritdoc */ getTightBoundingBox() { return this.bbox; } toString() { return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`; } /** * Returns `true` iff this is equivalent to `other`. * * **Options**: * - `tolerance`: The maximum difference between endpoints. (Default: 0) * - `ignoreDirection`: Allow matching a version of `this` with opposite direction. (Default: `true`) */ eq(other, options) { if (!(other instanceof LineSegment2)) { return false; } const tolerance = options?.tolerance; const ignoreDirection = options?.ignoreDirection ?? true; return ((other.p1.eq(this.p1, tolerance) && other.p2.eq(this.p2, tolerance)) || (ignoreDirection && other.p1.eq(this.p2, tolerance) && other.p2.eq(this.p1, tolerance))); } } export default LineSegment2;