├── .prettierrc ├── .gitignore ├── src ├── internal │ ├── const.ts │ ├── _lerp.ts │ ├── _clamp.ts │ ├── _dot.ts │ ├── _dotPerp.ts │ ├── _intersectionDNE.ts │ ├── _intersectionSwapTs.ts │ ├── _arrayReset.ts │ ├── _swapAndReorderIntersections.ts │ ├── _rayTransformByOrtho.ts │ ├── _lookAt.ts │ └── _polylineIntersectHelper.ts ├── __test__ │ ├── testTypes.ts │ ├── boxFunctions │ │ ├── boxAlloc.spec.ts │ │ ├── boxReset.spec.ts │ │ ├── boxClone.spec.ts │ │ ├── boxEncapsulate.spec.ts │ │ ├── boxIsEmpty.spec.ts │ │ ├── boxContainsBox.spec.ts │ │ ├── boxGetOutCode.spec.ts │ │ ├── boxTransformBy.spec.ts │ │ ├── boxUnion.spec.ts │ │ ├── boxIntersection.spec.ts │ │ ├── boxContainsPoint.spec.ts │ │ ├── boxIntersectsBox.spec.ts │ │ └── boxEnclosingPoints.spec.ts │ ├── vecFunctions │ │ ├── vecAlloc.spec.ts │ │ ├── vecDot.spec.ts │ │ ├── vecGetLength.spec.ts │ │ ├── vecAdd.spec.ts │ │ ├── vecGetLengthSq.spec.ts │ │ ├── vecReset.spec.ts │ │ ├── vecSubtract.spec.ts │ │ ├── vecDistance.spec.ts │ │ ├── vecDistanceSq.spec.ts │ │ ├── vecPerp.spec.ts │ │ ├── vecScale.spec.ts │ │ ├── vecNormalize.spec.ts │ │ ├── vecTransformBy.spec.ts │ │ ├── vecClone.spec.ts │ │ └── vecLerp.spec.ts │ ├── pointIntersectionResultFunctions │ │ ├── intersectionResultAlloc.spec.ts │ │ ├── intersectionResultReset.spec.ts │ │ └── intersectionResultClone.spec.ts │ ├── mat2dFunctions │ │ ├── mat2dAlloc.spec.ts │ │ ├── mat2dIdentity.spec.ts │ │ ├── mat2dDeterminant.spec.ts │ │ ├── mat2dScale.spec.ts │ │ ├── mat2dReset.spec.ts │ │ ├── mat2dFromTranslation.spec.ts │ │ ├── mat2dTranslate.spec.ts │ │ ├── mat2dFromRotation.spec.ts │ │ ├── mat2dIsOrthogonal.spec.ts │ │ ├── mat2dMulMat2d.spec.ts │ │ ├── mat2dClone.spec.ts │ │ ├── mat2dIsTranslationOnly.spec.ts │ │ ├── mat2dInvert.spec.ts │ │ └── mat2dRotate.spec.ts │ ├── rayFunctions │ │ ├── rayIntersectPolyline.spec.ts │ │ └── rayIntersectSegment.spec.ts │ └── helpers.ts ├── vecFunctions │ ├── _vec.ts │ ├── vecDot.ts │ ├── vecGetLength.ts │ ├── vecReset.ts │ ├── vecGetLengthSq.ts │ ├── vecAdd.ts │ ├── vecClone.ts │ ├── vecDistanceSq.ts │ ├── vecDistance.ts │ ├── vecSubtract.ts │ ├── vecScale.ts │ ├── vecPerp.ts │ ├── vecCross.ts │ ├── vecNormalize.ts │ ├── vecLerp.ts │ ├── vecTransformBy.ts │ └── vecAlloc.ts ├── rayFunctions │ ├── _ray.ts │ ├── rayClone.ts │ ├── rayContainsPoint.ts │ ├── rayProjectPoint.ts │ ├── rayGetPointAtT.ts │ ├── rayLookAt.ts │ ├── rayReset.ts │ ├── rayAlloc.ts │ ├── rayTransformBy.ts │ ├── rayIntersectPolyline.ts │ ├── rayWhichSide.ts │ ├── rayNearestDistanceSqToPoint.ts │ ├── rayIntersectSegment.ts │ └── rayIntersectRay.ts ├── boxFunctions │ ├── _box.ts │ ├── boxScale.ts │ ├── boxContainsBox.ts │ ├── boxTranslate.ts │ ├── boxClone.ts │ ├── boxUnion.ts │ ├── boxGrow.ts │ ├── boxEncapsulate.ts │ ├── boxIsEmpty.ts │ ├── boxContainsPoint.ts │ ├── boxIntersection.ts │ ├── boxIntersectsBox.ts │ ├── boxEnclosingPoints.ts │ ├── boxAlloc.ts │ ├── boxGetOutCode.ts │ ├── boxReset.ts │ └── boxTransformBy.ts ├── segmentFunctions │ ├── _segment.ts │ ├── segmentGetLengthSq.ts │ ├── segmentGetLength.ts │ ├── segmentGetEndpoint0.ts │ ├── segmentGetEndpoint1.ts │ ├── segmentReverse.ts │ ├── segmentReset.ts │ ├── segmentGetPointAtT.ts │ ├── segmentAlloc.ts │ ├── segmentIntersectPolyline.ts │ ├── segmentIntersectRay.ts │ ├── segmentNearestDistanceSqToPoint.ts │ └── segmentIntersectSegment.ts ├── mat2dFunctions │ ├── _mat2d.ts │ ├── mat2dIdentity.ts │ ├── mat2dDeterminant.ts │ ├── mat2dClone.ts │ ├── mat2dIsTranslationOnly.ts │ ├── mat2dFromTranslation.ts │ ├── mat2dFromRotation.ts │ ├── mat2dTranslate.ts │ ├── mat2dInvert.ts │ ├── mat2dIsOrthogonal.ts │ ├── mat2dScale.ts │ ├── mat2dReset.ts │ ├── mat2dAlloc.ts │ ├── mat2dRotate.ts │ └── mat2dMulMat2d.ts ├── polylineFunctions │ ├── polylineContainsPoint.ts │ ├── polylineGetNumVertices.ts │ ├── polylineGetSegmentLength.ts │ ├── polylineGetSegment.ts │ ├── polylineGetVertex.ts │ ├── polylineGetLength.ts │ ├── polylineGetNumSegments.ts │ ├── polylineClose.ts │ ├── polylineIsClosed.ts │ ├── polylineGetBounds.ts │ ├── polylineContainsPointInside.ts │ ├── polylineAlloc.ts │ ├── polylineGetTAtDistance.ts │ ├── polylineGetPointAtT.ts │ ├── polylineTransformBy.ts │ ├── polylineGetDistanceAtT.ts │ ├── polylineIntersectRay.ts │ ├── polylineIntersectSegment.ts │ ├── polylineNearestDistanceSqToPoint.ts │ └── polylineTrim.ts ├── nearestPointResultFunctions │ ├── nearestPointResultAlloc.ts │ ├── nearestPointResultClone.ts │ └── nearestPointResultReset.ts ├── intersectionResultFunctions │ ├── intersectionResultAlloc.ts │ ├── intersectionResultClone.ts │ └── intersectionResultReset.ts ├── const.ts ├── index.ts └── types.ts ├── tsconfig.esm.json ├── tsconfig.jest.json ├── .editorconfig ├── tslint.json ├── rollup.esm.js ├── rollup.commonjs.js ├── jest.config.js ├── tsconfig.json ├── LICENSE ├── package.json ├── .circleci └── config.yml └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | .vscode 4 | commonjs 5 | esm 6 | node_modules 7 | Thumbs.db 8 | -------------------------------------------------------------------------------- /src/internal/const.ts: -------------------------------------------------------------------------------- 1 | export const EPSILON = 2 ** -16; 2 | export const EPSILON_SQ = 2 ** -32; 3 | -------------------------------------------------------------------------------- /src/__test__/testTypes.ts: -------------------------------------------------------------------------------- 1 | export type IMat2dValuesArray = readonly [number, number, number, number, number, number]; 2 | -------------------------------------------------------------------------------- /src/internal/_lerp.ts: -------------------------------------------------------------------------------- 1 | export function _lerp(a: number, b: number, r: number) { 2 | return a * (1 - r) + b * r; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2020" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "incremental": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/internal/_clamp.ts: -------------------------------------------------------------------------------- 1 | export function _clamp(value: number, min: number, max: number) { 2 | return value < min ? min : value > max ? max : value; 3 | } 4 | -------------------------------------------------------------------------------- /src/internal/_dot.ts: -------------------------------------------------------------------------------- 1 | import { Ray, Vec } from "../types"; 2 | 3 | export function _dot(ray: Ray, vec: Vec) { 4 | return ray.dirX * (vec.x - ray.x0) + ray.dirY * (vec.y - ray.y0); 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/internal/_dotPerp.ts: -------------------------------------------------------------------------------- 1 | import { Ray, Vec } from "../types"; 2 | 3 | export function _dotPerp(ray: Ray, point: Vec) { 4 | return ray.dirX * (point.y - ray.y0) - ray.dirY * (point.x - ray.x0); 5 | } 6 | -------------------------------------------------------------------------------- /src/vecFunctions/_vec.ts: -------------------------------------------------------------------------------- 1 | import { vecAlloc } from "./vecAlloc"; 2 | 3 | export function _vec(x: number, y: number) { 4 | const out = vecAlloc(); 5 | out.x = x; 6 | out.y = y; 7 | return out; 8 | } 9 | -------------------------------------------------------------------------------- /src/internal/_intersectionDNE.ts: -------------------------------------------------------------------------------- 1 | import { IntersectionResult } from "../types"; 2 | 3 | export function _intersectionDNE(out: IntersectionResult) { 4 | out.exists = false; 5 | out.x = out.y = out.t0 = out.t1 = NaN; 6 | return out; 7 | } 8 | -------------------------------------------------------------------------------- /src/internal/_intersectionSwapTs.ts: -------------------------------------------------------------------------------- 1 | import { IntersectionResult } from "../types"; 2 | 3 | export function _intersectionSwapTs(out: IntersectionResult) { 4 | const tmp = out.t0; 5 | out.t0 = out.t1; 6 | out.t1 = tmp; 7 | return out; 8 | } 9 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "class-name": false, 5 | "member-ordering": false, 6 | "no-implicit-dependencies": [true, "dev"], 7 | "no-submodule-imports": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxAlloc.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxAlloc } from "../../boxFunctions/boxAlloc"; 2 | 3 | it("boxAlloc", () => { 4 | expect(boxAlloc()).toEqual({ 5 | minX: NaN, 6 | minY: NaN, 7 | maxX: NaN, 8 | maxY: NaN, 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/rayFunctions/_ray.ts: -------------------------------------------------------------------------------- 1 | import { rayAlloc } from "./rayAlloc"; 2 | 3 | export function _ray(x0: number, y0: number, dirX: number, dirY: number) { 4 | const out = rayAlloc(); 5 | out.x0 = x0; 6 | out.y0 = y0; 7 | out.dirX = dirX; 8 | out.dirY = dirY; 9 | return out; 10 | } 11 | -------------------------------------------------------------------------------- /rollup.esm.js: -------------------------------------------------------------------------------- 1 | import ts from "@wessberg/rollup-plugin-ts"; 2 | 3 | export default { 4 | input: "src/index.ts", 5 | output: { 6 | file: "esm/index.js", 7 | format: "es", 8 | }, 9 | plugins: [ 10 | ts({ 11 | tsconfig: "tsconfig.esm.json", 12 | }), 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /src/boxFunctions/_box.ts: -------------------------------------------------------------------------------- 1 | import { boxAlloc } from "./boxAlloc"; 2 | 3 | export function _box(minX: number, minY: number, maxX: number, maxY: number) { 4 | const out = boxAlloc(); 5 | out.minX = minX; 6 | out.minY = minY; 7 | out.maxX = maxX; 8 | out.maxY = maxY; 9 | return out; 10 | } 11 | -------------------------------------------------------------------------------- /src/segmentFunctions/_segment.ts: -------------------------------------------------------------------------------- 1 | import { segmentAlloc } from "./segmentAlloc"; 2 | 3 | export function _segment(x0: number, y0: number, x1: number, y1: number) { 4 | const out = segmentAlloc(); 5 | out.x0 = x0; 6 | out.y0 = y0; 7 | out.x1 = x1; 8 | out.y1 = y1; 9 | return out; 10 | } 11 | -------------------------------------------------------------------------------- /rollup.commonjs.js: -------------------------------------------------------------------------------- 1 | import ts from "@wessberg/rollup-plugin-ts"; 2 | 3 | export default { 4 | input: "src/index.ts", 5 | output: { 6 | file: "commonjs/index.js", 7 | format: "commonjs", 8 | }, 9 | plugins: [ 10 | ts({ 11 | tsconfig: "tsconfig.json", 12 | }), 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /src/mat2dFunctions/_mat2d.ts: -------------------------------------------------------------------------------- 1 | import { mat2dAlloc } from "./mat2dAlloc"; 2 | 3 | export function _mat2d(a: number, b: number, c: number, d: number, tx: number, ty: number) { 4 | const out = mat2dAlloc(); 5 | out.a = a; 6 | out.b = b; 7 | out.c = c; 8 | out.d = d; 9 | out.tx = tx; 10 | out.ty = ty; 11 | return out; 12 | } 13 | -------------------------------------------------------------------------------- /src/vecFunctions/vecDot.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | 3 | /** 4 | * Computes the dot product of the two vectors, i.e. `u.x * v.x + u.y * v.y`. 5 | * 6 | * @param u the first vector 7 | * @param v the vector to dot with the first 8 | */ 9 | export function vecDot(u: Vec, v: Vec) { 10 | return u.x * v.x + u.y * v.y; 11 | } 12 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dIdentity.ts: -------------------------------------------------------------------------------- 1 | import { mat2dAlloc } from "./mat2dAlloc"; 2 | import { mat2dReset } from "./mat2dReset"; 3 | 4 | /** 5 | * Returns the identity affine matrix, `[1 0 0 1 0 0]` 6 | * 7 | * @param out 8 | */ 9 | export function mat2dIdentity(out = mat2dAlloc()) { 10 | return mat2dReset(1, 0, 0, 1, 0, 0, out); 11 | } 12 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecAlloc.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecAlloc } from "../../vecFunctions/vecAlloc"; 2 | import { vecReset } from "../../vecFunctions/vecReset"; 3 | import { expectVecEqualsApprox } from "../helpers"; 4 | 5 | describe("vecAlloc", () => { 6 | it("returns (NaN, NaN)", () => { 7 | expectVecEqualsApprox(vecAlloc(), vecReset(NaN, NaN)); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dDeterminant.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d } from "../types"; 2 | 3 | /** 4 | * Computes the determinant of the affine matrix 5 | * 6 | * The determinant in 2D space is given by `a * d - b * c`. 7 | * 8 | * @param mat matrix to take determinant of 9 | */ 10 | export function mat2dDeterminant(mat: Mat2d) { 11 | return mat.a * mat.d - mat.b * mat.c; 12 | } 13 | -------------------------------------------------------------------------------- /src/vecFunctions/vecGetLength.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | 3 | /** 4 | * Computes the straight-line length (i.e. Euclidean norm) of the given vector. 5 | * 6 | * Equivalent to `√(v.x² + v.y²)`. 7 | * 8 | * @param v the vector whose length should be measured 9 | */ 10 | export function vecGetLength(v: Vec) { 11 | return Math.sqrt(v.x * v.x + v.y * v.y); 12 | } 13 | -------------------------------------------------------------------------------- /src/vecFunctions/vecReset.ts: -------------------------------------------------------------------------------- 1 | import { vecAlloc } from "./vecAlloc"; 2 | 3 | /** 4 | * Construct a new vector given an `x` and `y` value. 5 | * 6 | * @param x x-coordinate of the vector 7 | * @param y y-coordinate of the vector 8 | * @param out 9 | */ 10 | export function vecReset(x: number, y: number, out = vecAlloc()) { 11 | out.x = x; 12 | out.y = y; 13 | return out; 14 | } 15 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineContainsPoint.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON_SQ } from "../internal/const"; 2 | import { Polyline, Vec } from "../types"; 3 | import { polylineNearestDistanceSqToPoint } from "./polylineNearestDistanceSqToPoint"; 4 | 5 | export function polylineContainsPoint(polyline: Polyline, point: Vec) { 6 | return polylineNearestDistanceSqToPoint(polyline, point).distanceValue < EPSILON_SQ; 7 | } 8 | -------------------------------------------------------------------------------- /src/vecFunctions/vecGetLengthSq.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | 3 | /** 4 | * Computes the squared straight-line length (i.e. square of the Euclidean norm) of the given vector. 5 | * 6 | * Equivalent to `v.x² + v.y²`. 7 | * 8 | * @param v the vector whose squared length should be measured 9 | */ 10 | export function vecGetLengthSq(v: Vec) { 11 | return v.x * v.x + v.y * v.y; 12 | } 13 | -------------------------------------------------------------------------------- /src/__test__/pointIntersectionResultFunctions/intersectionResultAlloc.spec.ts: -------------------------------------------------------------------------------- 1 | import { intersectionResultAlloc } from "../../intersectionResultFunctions/intersectionResultAlloc"; 2 | import { expectIntersectionDNE } from "../helpers"; 3 | 4 | describe("intersectionResultAlloc", () => { 5 | it("returns (false, NaN, NaN, NaN, NaN)", () => { 6 | expectIntersectionDNE(intersectionResultAlloc()); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/vecFunctions/vecAdd.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | import { vecAlloc } from "./vecAlloc"; 3 | import { vecReset } from "./vecReset"; 4 | 5 | /** 6 | * Computes the result of adding the two given vectors. 7 | * 8 | * @param a 9 | * @param b 10 | * @param out 11 | */ 12 | export function vecAdd(a: Vec, b: Vec, out = vecAlloc()) { 13 | return vecReset(a.x + b.x, a.y + b.y, out); 14 | } 15 | -------------------------------------------------------------------------------- /src/vecFunctions/vecClone.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | import { vecAlloc } from "./vecAlloc"; 3 | import { vecReset } from "./vecReset"; 4 | 5 | /** 6 | * Copies the values from the given vector into a new vector. 7 | * 8 | * @param vec the vector to copy 9 | * @param out 10 | */ 11 | export function vecClone(vec: Vec, out = vecAlloc()) { 12 | return vecReset(vec.x, vec.y, out); 13 | } 14 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecDot.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecDot } from "../../vecFunctions/vecDot"; 2 | import { _vecValues } from "../helpers"; 3 | 4 | describe("vecDot", () => { 5 | it.each` 6 | v0 | v1 | result 7 | ${[2, 4]} | ${[3, 10]} | ${46} 8 | `("$v0 $v1 => $result", ({ v0, v1, result }) => { 9 | expect(vecDot(_vecValues(v0), _vecValues(v1))).toBe(result); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/internal/_arrayReset.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:use-named-parameter 2 | export function _arrayReset(out: T[], ...vals: T[]): void; 3 | export function _arrayReset() { 4 | const out = arguments[0] as any[]; 5 | const len = arguments.length - 1; 6 | if (out.length !== len) { 7 | out.length = len; 8 | } 9 | 10 | for (let i = 0; i < len; i++) { 11 | out[i] = arguments[i + 1]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dAlloc.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dAlloc } from "../../mat2dFunctions/mat2dAlloc"; 2 | import { mat2dReset } from "../../mat2dFunctions/mat2dReset"; 3 | import { expectMat2dEqualsApprox } from "../helpers"; 4 | 5 | describe("mat2dAlloc", () => { 6 | it("returns all NaNs", () => { 7 | expectMat2dEqualsApprox(mat2dAlloc(), mat2dReset(NaN, NaN, NaN, NaN, NaN, NaN)); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dIdentity.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dIdentity } from "../../mat2dFunctions/mat2dIdentity"; 2 | import { mat2dReset } from "../../mat2dFunctions/mat2dReset"; 3 | import { expectMat2dEqualsApprox } from "../helpers"; 4 | 5 | describe("mat2dIdentity", () => { 6 | it("returns [1 0 0 1 0 0]", () => { 7 | expectMat2dEqualsApprox(mat2dIdentity(), mat2dReset(1, 0, 0, 1, 0, 0)); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetNumVertices.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | 3 | /** 4 | * Returns the number of vertices in this polyline 5 | * 6 | * Always equal to `poly.length / 2`. This function makes no attempt to remove repeated 7 | * or NaN vertices, for instance. 8 | * 9 | * @param poly 10 | */ 11 | export function polylineGetNumVertices(poly: Polyline) { 12 | return poly.length / 2; 13 | } 14 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentGetLengthSq.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "../types"; 2 | 3 | /** 4 | * Computes the squared length of the line segment 5 | * 6 | * This is simply `(x1 - x0)² + (y1 - y0)²`. 7 | * 8 | * @param segment segment to measure 9 | */ 10 | export function segmentGetLengthSq(segment: Segment) { 11 | const dx = segment.x1 - segment.x0; 12 | const dy = segment.y1 - segment.y0; 13 | return dx * dx + dy * dy; 14 | } 15 | -------------------------------------------------------------------------------- /src/vecFunctions/vecDistanceSq.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | 3 | /** 4 | * Computes the squared straight-line (i.e. Euclidean) distance between the two points 5 | * 6 | * @param u the first point 7 | * @param v the second point to which squared distance should be measured 8 | */ 9 | export function vecDistanceSq(u: Vec, v: Vec) { 10 | const dx = v.x - u.x; 11 | const dy = v.y - u.y; 12 | return dx * dx + dy * dy; 13 | } 14 | -------------------------------------------------------------------------------- /src/rayFunctions/rayClone.ts: -------------------------------------------------------------------------------- 1 | import { Ray } from "../types"; 2 | import { rayAlloc } from "./rayAlloc"; 3 | import { rayReset } from "./rayReset"; 4 | 5 | /** 6 | * Copies the values from the given ray into a new ray. 7 | * 8 | * @param ray source ray from which values should be copied 9 | * @param out 10 | */ 11 | export function rayClone(ray: Ray, out = rayAlloc()) { 12 | return rayReset(ray.x0, ray.y0, ray.dirX, ray.dirY, out); 13 | } 14 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentGetLength.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "../types"; 2 | 3 | /** 4 | * Computes the length of the line segment 5 | * 6 | * This is simply `√((x1 - x0)² + (y1 - y0)²)`. 7 | * 8 | * @param segment segment to measure 9 | */ 10 | export function segmentGetLength(segment: Segment) { 11 | const dx = segment.x1 - segment.x0; 12 | const dy = segment.y1 - segment.y0; 13 | return Math.sqrt(dx * dx + dy * dy); 14 | } 15 | -------------------------------------------------------------------------------- /src/vecFunctions/vecDistance.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | 3 | /** 4 | * Computes the straight-line (Euclidean) distance between the two points 5 | * 6 | * @param u the first point 7 | * @param v the second point, to which distance should be measured from the first 8 | */ 9 | export function vecDistance(u: Vec, v: Vec) { 10 | const dx = v.x - u.x; 11 | const dy = v.y - u.y; 12 | return Math.sqrt(dx * dx + dy * dy); 13 | } 14 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecGetLength.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecGetLength } from "../../vecFunctions/vecGetLength"; 2 | import { _vecValues } from "../helpers"; 3 | 4 | describe("vecGetLength", () => { 5 | it.each` 6 | vec | result 7 | ${[0, 0]} | ${0} 8 | ${[0, -4]} | ${4} 9 | ${[12, 16]} | ${20} 10 | `("$vec => $result", ({ vec, result }) => { 11 | expect(vecGetLength(_vecValues(vec))).toBe(result); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dClone.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d } from "../types"; 2 | import { mat2dAlloc } from "./mat2dAlloc"; 3 | import { mat2dReset } from "./mat2dReset"; 4 | 5 | /** 6 | * Copies the values from the given matrix into a new matrix. 7 | * 8 | * @param mat the matrix to copy 9 | * @param out 10 | */ 11 | export function mat2dClone(mat: Mat2d, out = mat2dAlloc()) { 12 | return mat2dReset(mat.a, mat.b, mat.c, mat.d, mat.tx, mat.ty, out); 13 | } 14 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecAdd.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecAdd } from "../../vecFunctions/vecAdd"; 2 | import { expectVecEqualsApprox, _vecValues } from "../helpers"; 3 | 4 | describe("vecAdd", () => { 5 | it.each` 6 | v0 | v1 | result 7 | ${[4, 5]} | ${[20, 30]} | ${[24, 35]} 8 | `("$v0 $v1 => $result", ({ v0, v1, result }) => { 9 | expectVecEqualsApprox(vecAdd(_vecValues(v0), _vecValues(v1)), _vecValues(result)); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecGetLengthSq.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecGetLengthSq } from "../../vecFunctions/vecGetLengthSq"; 2 | import { _vecValues } from "../helpers"; 3 | 4 | describe("vecGetLengthSq", () => { 5 | it.each` 6 | vec | result 7 | ${[0, 0]} | ${0} 8 | ${[0, -4]} | ${16} 9 | ${[12, 16]} | ${400} 10 | `("$vec => $result", ({ vec, result }) => { 11 | expect(vecGetLengthSq(_vecValues(vec))).toBe(result); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/vecFunctions/vecSubtract.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | import { vecAlloc } from "./vecAlloc"; 3 | import { vecReset } from "./vecReset"; 4 | 5 | /** 6 | * Computes `u - v`, i.e. subtracting the second vector from the first. 7 | * 8 | * @param u the first vector 9 | * @param v the second vector 10 | * @param out 11 | */ 12 | export function vecSubtract(u: Vec, v: Vec, out = vecAlloc()) { 13 | return vecReset(u.x - v.x, u.y - v.y, out); 14 | } 15 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dIsTranslationOnly.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d } from "../types"; 2 | 3 | /** 4 | * Returns whether the matrix corresponds to only a translation. 5 | * 6 | * Affine translation matrices have the form 7 | * 8 | * ``` 9 | * ⎡1 0 tx⎤ 10 | * ⎣0 1 ty⎦ 11 | * ``` 12 | * 13 | * @param mat the matrix to inspect 14 | */ 15 | export function mat2dIsTranslationOnly(mat: Mat2d) { 16 | return mat.a === 1 && mat.b === 0 && mat.c === 0 && mat.d === 1; 17 | } 18 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetSegmentLength.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | 3 | /** 4 | * Computes the length of one of a polyline's segments by index, starting at 0. 5 | * 6 | * @param poly 7 | * @param idx 8 | */ 9 | export function polylineGetSegmentLength(poly: Polyline, idx: number) { 10 | const l = 2 * idx; 11 | const dx = poly[l + 2] - poly[l]; 12 | const dy = poly[l + 3] - poly[l + 1]; 13 | return Math.sqrt(dx * dx + dy * dy); 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["ts", "tsx", "js"], 3 | testRegex: "\\.(test|spec)\\.tsx?$", 4 | transform: { 5 | "\\.(js|jsx|ts|tsx)$": "ts-jest", 6 | }, 7 | transformIgnorePatterns: ["/node_modules/(?!(lodash-es|@foundry|@workshop|@palantir|@acme|@gotham|@tron)/)"], 8 | globals: { 9 | "ts-jest": { 10 | tsconfig: "tsconfig.jest.json", 11 | }, 12 | }, 13 | verbose: true, 14 | reporters: ["default"], 15 | }; 16 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecReset.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecAlloc } from "../../vecFunctions/vecAlloc"; 2 | import { vecReset } from "../../vecFunctions/vecReset"; 3 | 4 | describe("vecReset", () => { 5 | it("copies components", () => { 6 | expect(vecReset(4, 5)).toEqual({ 7 | x: 4, 8 | y: 5, 9 | }); 10 | }); 11 | 12 | it("returns `out` if given", () => { 13 | const out = vecAlloc(); 14 | expect(vecReset(4, 5, out)).toBe(out); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecSubtract.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecSubtract } from "../../vecFunctions/vecSubtract"; 2 | import { expectVecEqualsApprox, _vecValues } from "../helpers"; 3 | 4 | describe("vecSubtract", () => { 5 | it.each` 6 | v0 | v1 | result 7 | ${[4, 5]} | ${[20, 30]} | ${[-16, -25]} 8 | `("$v0 $v1 => $result", ({ v0, v1, result }) => { 9 | expectVecEqualsApprox(vecSubtract(_vecValues(v0), _vecValues(v1)), _vecValues(result)); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dFromTranslation.ts: -------------------------------------------------------------------------------- 1 | import { mat2dAlloc } from "./mat2dAlloc"; 2 | import { mat2dReset } from "./mat2dReset"; 3 | /** 4 | * Computes the affine transform corresponding to a given (tx, ty) translation 5 | * 6 | * @param tx the x translation component 7 | * @param ty the y translation component 8 | * @param out 9 | */ 10 | export function mat2dFromTranslation(tx: number, ty: number, out = mat2dAlloc()) { 11 | return mat2dReset(1, 0, 0, 1, tx, ty, out); 12 | } 13 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentGetEndpoint0.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "../types"; 2 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 3 | import { vecReset } from "../vecFunctions/vecReset"; 4 | 5 | /** 6 | * Retrieves the starting endpoint (_t_ = 0) of the segment, as a vector. 7 | * 8 | * @param segment segment to inspect 9 | * @param out 10 | */ 11 | export function segmentGetEndpoint0(segment: Segment, out = vecAlloc()) { 12 | return vecReset(segment.x0, segment.y0, out); 13 | } 14 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentGetEndpoint1.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "../types"; 2 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 3 | import { vecReset } from "../vecFunctions/vecReset"; 4 | 5 | /** 6 | * Retrives the ending endpoint (_t_ = 1) of the segment, as a vector. 7 | * 8 | * @param segment segment to inspect 9 | * @param out 10 | */ 11 | export function segmentGetEndpoint1(segment: Segment, out = vecAlloc()) { 12 | return vecReset(segment.x1, segment.y1, out); 13 | } 14 | -------------------------------------------------------------------------------- /src/vecFunctions/vecScale.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | import { vecAlloc } from "./vecAlloc"; 3 | import { vecReset } from "./vecReset"; 4 | 5 | /** 6 | * Scales both coordinates of this vector by a given scalar. 7 | * 8 | * @param v the vector to scale 9 | * @param scalar the value by which the vector's components should be scaled 10 | * @param out 11 | */ 12 | export function vecScale(v: Vec, scalar: number, out = vecAlloc()) { 13 | return vecReset(v.x * scalar, v.y * scalar, out); 14 | } 15 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecDistance.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecDistance } from "../../vecFunctions/vecDistance"; 2 | import { _vecValues } from "../helpers"; 3 | 4 | describe("vecDistance", () => { 5 | it.each` 6 | v0 | v1 | result 7 | ${[2, 4]} | ${[2, 4]} | ${0} 8 | ${[0, 4]} | ${[0, -5]} | ${9} 9 | ${[10, 10]} | ${[16, 18]} | ${10} 10 | `("$v0 $v1 => $result", ({ v0, v1, result }) => { 11 | expect(vecDistance(_vecValues(v0), _vecValues(v1))).toBe(result); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/internal/_swapAndReorderIntersections.ts: -------------------------------------------------------------------------------- 1 | import { IntersectionResult } from "../types"; 2 | import { _intersectionSwapTs } from "./_intersectionSwapTs"; 3 | 4 | function sortByT0Increasing(a: IntersectionResult, b: IntersectionResult) { 5 | return a.t0 < b.t0 ? -1 : a.t0 > b.t0 ? 1 : 0; 6 | } 7 | 8 | export function _swapAndReorderIntersections(intersections: IntersectionResult[]) { 9 | intersections.forEach(_intersectionSwapTs); 10 | intersections.sort(sortByT0Increasing); 11 | return intersections; 12 | } 13 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentReverse.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "../types"; 2 | import { segmentAlloc } from "./segmentAlloc"; 3 | import { segmentReset } from "./segmentReset"; 4 | 5 | /** 6 | * Computes the reverse of the segment, i.e. swapping its starting vertex and ending vertex. 7 | * 8 | * @param segment the segment to reverse 9 | * @param out 10 | */ 11 | export function segmentReverse(segment: Segment, out = segmentAlloc()) { 12 | return segmentReset(segment.x1, segment.y1, segment.x0, segment.y0, out); 13 | } 14 | -------------------------------------------------------------------------------- /src/vecFunctions/vecPerp.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | import { vecAlloc } from "./vecAlloc"; 3 | import { vecReset } from "./vecReset"; 4 | 5 | /** 6 | * Computes the perp of the given vector, as defined by `vecPerp(a, b) = (-b, a)`. 7 | * This is equivalent to a counter-clockwise rotation in the standard plane. 8 | * 9 | * @param vec the vector whose perp should be calculated 10 | * @param out 11 | */ 12 | export function vecPerp(vec: Vec, out = vecAlloc()) { 13 | return vecReset(-vec.y, vec.x, out); 14 | } 15 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxReset.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxAlloc } from "../../boxFunctions/boxAlloc"; 2 | import { boxReset } from "../../boxFunctions/boxReset"; 3 | 4 | describe("boxReset", () => { 5 | it("sets components", () => { 6 | expect(boxReset(4, 5, 6, 7)).toEqual({ 7 | minX: 4, 8 | minY: 5, 9 | maxX: 6, 10 | maxY: 7, 11 | }); 12 | }); 13 | 14 | it("returns `out` if given", () => { 15 | const out = boxAlloc(); 16 | expect(boxReset(4, 5, 6, 7, out)).toBe(out); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecDistanceSq.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecDistanceSq } from "../../vecFunctions/vecDistanceSq"; 2 | import { _vecValues } from "../helpers"; 3 | 4 | describe("vecDistanceSq", () => { 5 | it.each` 6 | v0 | v1 | result 7 | ${[2, 4]} | ${[2, 4]} | ${0} 8 | ${[0, 4]} | ${[0, -5]} | ${81} 9 | ${[10, 10]} | ${[16, 18]} | ${100} 10 | `("$v0 $v1 => $result", ({ v0, v1, result }) => { 11 | expect(vecDistanceSq(_vecValues(v0), _vecValues(v1))).toBe(result); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecPerp.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecPerp } from "../../vecFunctions/vecPerp"; 2 | import { expectVecEqualsApprox, _vecValues } from "../helpers"; 3 | 4 | describe("vecPerp", () => { 5 | it.each` 6 | vec | result 7 | ${[4, 5]} | ${[-5, 4]} 8 | ${[-2, -3]} | ${[3, -2]} 9 | ${[0, 0]} | ${[0, 0]} 10 | ${[NaN, NaN]} | ${[NaN, NaN]} 11 | `("$vec => $result", ({ vec, result }) => { 12 | expectVecEqualsApprox(vecPerp(_vecValues(vec)), _vecValues(result)); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dDeterminant.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dDeterminant } from "../../mat2dFunctions/mat2dDeterminant"; 2 | import { _mat2dValues } from "../helpers"; 3 | 4 | describe("mat2dDeterminant", () => { 5 | it.each` 6 | mat | result 7 | ${[1, 0, 0, 1, 0, 0]} | ${1} 8 | ${[2, 0, 0, 2, 10, 10]} | ${4} 9 | ${[4, -7, 8, 5, NaN, NaN]} | ${76} 10 | `("$mat => $result", ({ mat, result }) => { 11 | expect(mat2dDeterminant(_mat2dValues(mat))).toBe(result); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/boxFunctions/boxScale.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | import { boxAlloc } from "./boxAlloc"; 3 | import { boxReset } from "./boxReset"; 4 | 5 | /** 6 | * Scales a box by a fixed scalar in both directions. 7 | * 8 | * @param box the box to scale 9 | * @param scalar the value by which to multiply all of the box's components 10 | * @param out 11 | */ 12 | export function boxScale(box: Box, scalar: number, out = boxAlloc()) { 13 | return boxReset(box.minX * scalar, box.minY * scalar, box.maxX * scalar, box.maxY * scalar, out); 14 | } 15 | -------------------------------------------------------------------------------- /src/boxFunctions/boxContainsBox.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | 3 | /** 4 | * Determines whether the second box is completely enclosed in the first. 5 | * 6 | * Returns true if the second box is contained in the first. 7 | * Each box is treated as a closed area, so e.g. the two boxes may share 8 | * an edge and the containment check would still pass. 9 | * 10 | * @param a 11 | * @param b 12 | */ 13 | export function boxContainsBox(a: Box, b: Box) { 14 | return b.minX >= a.minX && b.minY >= a.minY && b.maxX <= a.maxX && b.maxY <= a.maxY; 15 | } 16 | -------------------------------------------------------------------------------- /src/vecFunctions/vecCross.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | 3 | /** 4 | * Computes the two-dimensional cross product of the two vectors. 5 | * 6 | * The two-dimensional cross product is defined to be the scalar value: 7 | * 8 | * ``` 9 | * u × v = u.x * v.y - u.y * v.x 10 | * ``` 11 | * 12 | * Note that the cross product is antisymmetric, i.e. `u × v = -v × u`. 13 | * 14 | * @param u the first vector 15 | * @param v the vector to cross with the first 16 | */ 17 | export function vecCross(u: Vec, v: Vec) { 18 | return u.x * v.y - u.y * v.x; 19 | } 20 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetSegment.ts: -------------------------------------------------------------------------------- 1 | import { segmentAlloc } from "../segmentFunctions/segmentAlloc"; 2 | import { segmentReset } from "../segmentFunctions/segmentReset"; 3 | import { Polyline } from "../types"; 4 | 5 | /** 6 | * Returns a polyline's segment by given index, starting at 0. 7 | * 8 | * @param poly 9 | * @param index 10 | * @param out 11 | */ 12 | export function polylineGetSegment(poly: Polyline, index: number, out = segmentAlloc()) { 13 | const l = 2 * index; 14 | return segmentReset(poly[l], poly[l + 1], poly[l + 2], poly[l + 3], out); 15 | } 16 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetVertex.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 3 | import { vecReset } from "../vecFunctions/vecReset"; 4 | 5 | /** 6 | * Retrieves a vertex from this polyline's geometry, starting at index 0 7 | * 8 | * @param poly 9 | * @param index 10 | * @param out 11 | */ 12 | export function polylineGetVertex(poly: Polyline, index: number, out = vecAlloc()) { 13 | const l = 2 * index; 14 | return l >= 0 && l < poly.length ? vecReset(poly[l], poly[l + 1], out) : vecReset(NaN, NaN, out); 15 | } 16 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dScale.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dScale } from "../../mat2dFunctions/mat2dScale"; 2 | import { expectMat2dEqualsApprox, _mat2dValues } from "../helpers"; 3 | 4 | describe("mat2dScale", () => { 5 | it.each` 6 | mat | s | result 7 | ${[1, 2, 3, 4, 5, 6]} | ${7} | ${[7, 14, 21, 28, 35, 42]} 8 | ${[1, 0, 0, 1, 0, 0]} | ${NaN} | ${[NaN, NaN, NaN, NaN, NaN, NaN]} 9 | `("$mat $s => $result", ({ mat, s, result }) => { 10 | expectMat2dEqualsApprox(mat2dScale(_mat2dValues(mat), s), _mat2dValues(result)); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dReset.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dAlloc } from "../../mat2dFunctions/mat2dAlloc"; 2 | import { mat2dReset } from "../../mat2dFunctions/mat2dReset"; 3 | 4 | describe("mat2dReset", () => { 5 | it("copies components", () => { 6 | expect(mat2dReset(3, 4, 5, 6, 7, 8)).toEqual({ 7 | a: 3, 8 | b: 4, 9 | c: 5, 10 | d: 6, 11 | tx: 7, 12 | ty: 8, 13 | }); 14 | }); 15 | 16 | it("returns `out` if given", () => { 17 | const out = mat2dAlloc(); 18 | expect(mat2dReset(3, 4, 5, 6, 7, 8, out)).toBe(out); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecScale.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecScale } from "../../vecFunctions/vecScale"; 2 | import { expectVecEqualsApprox, _vecValues } from "../helpers"; 3 | 4 | describe("vecScale", () => { 5 | it.each` 6 | vec | scalar | result 7 | ${[6, 2]} | ${3} | ${[18, 6]} 8 | ${[6, -2]} | ${-3} | ${[-18, 6]} 9 | ${[6, -2]} | ${NaN} | ${[NaN, NaN]} 10 | ${[4, 5]} | ${0} | ${[0, 0]} 11 | `("$vec $scalar => $result", ({ vec, scalar, result }) => { 12 | expectVecEqualsApprox(vecScale(_vecValues(vec), scalar), _vecValues(result)); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetLength.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | import { polylineGetNumSegments } from "./polylineGetNumSegments"; 3 | import { polylineGetSegmentLength } from "./polylineGetSegmentLength"; 4 | 5 | /** 6 | * Computes total length of polyline 7 | * 8 | * @param poly 9 | */ 10 | export function polylineGetLength(poly: Polyline) { 11 | const numSegments = polylineGetNumSegments(poly); 12 | let length = 0; 13 | for (let i = 0; i < numSegments; i++) { 14 | length += polylineGetSegmentLength(poly, i); 15 | } 16 | 17 | return length; 18 | } 19 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetNumSegments.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | 3 | /** 4 | * Returns the number of individual line segments in this polyline 5 | * 6 | * This function computes the number of line segments represented by this polyline, 7 | * which is always `Math.max(0, poly.length / 2 - 1)`. This function makes no effort to identify and filter 8 | * "trivial" or "empty" segments that may exist along its path. 9 | * 10 | * @param poly 11 | */ 12 | export function polylineGetNumSegments(poly: Polyline) { 13 | return Math.max(0, poly.length / 2 - 1); 14 | } 15 | -------------------------------------------------------------------------------- /src/boxFunctions/boxTranslate.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | import { boxAlloc } from "./boxAlloc"; 3 | import { boxReset } from "./boxReset"; 4 | 5 | /** 6 | * Translate a box by an offset in the x- and y- directions. 7 | * 8 | * @param box the box to translate 9 | * @param tx the amount to translate in the x direction 10 | * @param ty the amount to translate in the y direction 11 | * @param out 12 | */ 13 | export function boxTranslate(box: Box, tx: number, ty: number, out = boxAlloc()) { 14 | return boxReset(box.minX + tx, box.minY + ty, box.maxX + tx, box.maxY + ty, out); 15 | } 16 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dFromTranslation.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dFromTranslation } from "../../mat2dFunctions/mat2dFromTranslation"; 2 | import { expectMat2dEqualsApprox, _mat2dValues } from "../helpers"; 3 | 4 | describe("mat2dFromTranslation", () => { 5 | it.each` 6 | x | y | result 7 | ${0} | ${0} | ${[1, 0, 0, 1, 0, 0]} 8 | ${10} | ${-20} | ${[1, 0, 0, 1, 10, -20]} 9 | ${NaN} | ${NaN} | ${[1, 0, 0, 1, NaN, NaN]} 10 | `("$x $y => $result", ({ x, y, result }) => { 11 | expectMat2dEqualsApprox(mat2dFromTranslation(x, y), _mat2dValues(result)); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecNormalize.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecNormalize } from "../../vecFunctions/vecNormalize"; 2 | import { expectVecEqualsApprox, _vecValues } from "../helpers"; 3 | 4 | describe("vecNormalize", () => { 5 | it.each` 6 | vec | result 7 | ${[0.6, -0.8]} | ${[0.6, -0.8]} 8 | ${[0.3, -0.4]} | ${[0.6, -0.8]} 9 | ${[-20, 21]} | ${[-20 / 29, 21 / 29]} 10 | ${[0, 0]} | ${[NaN, NaN]} 11 | ${[NaN, NaN]} | ${[NaN, NaN]} 12 | `("$vec => $result", ({ vec, result }) => { 13 | expectVecEqualsApprox(vecNormalize(_vecValues(vec)), _vecValues(result)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/nearestPointResultFunctions/nearestPointResultAlloc.ts: -------------------------------------------------------------------------------- 1 | import { NearestPointResult } from "../types"; 2 | 3 | class _NearestPointResult implements NearestPointResult { 4 | public x = NaN; 5 | public y = NaN; 6 | public t = NaN; 7 | public distanceValue = NaN; 8 | } 9 | 10 | /** 11 | * Creates a new NearestPointResult object in memory, with all values initialized to `NaN`. 12 | * This is useful to hold the result of math2d function calls in performance 13 | * critical workflows. 14 | */ 15 | export function nearestPointResultAlloc(): NearestPointResult { 16 | return new _NearestPointResult(); 17 | } 18 | -------------------------------------------------------------------------------- /src/internal/_rayTransformByOrtho.ts: -------------------------------------------------------------------------------- 1 | import { rayAlloc } from "../rayFunctions/rayAlloc"; 2 | import { rayReset } from "../rayFunctions/rayReset"; 3 | import { Mat2d, Ray } from "../types"; 4 | import { vecReset } from "../vecFunctions/vecReset"; 5 | import { vecTransformBy } from "../vecFunctions/vecTransformBy"; 6 | 7 | export function _rayTransformByOrtho(ray: Ray, mat: Mat2d, out: Ray = rayAlloc()) { 8 | const initial = vecReset(ray.x0, ray.y0); 9 | vecTransformBy(initial, mat, initial); 10 | return rayReset(initial.x, initial.y, mat.a * ray.dirX + mat.c * ray.dirY, mat.b * ray.dirX + mat.d * ray.dirY, out); 11 | } 12 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dTranslate.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dTranslate } from "../../mat2dFunctions/mat2dTranslate"; 2 | import { expectMat2dEqualsApprox, _mat2dValues } from "../helpers"; 3 | 4 | describe("mat2dTranslate", () => { 5 | it.each` 6 | mat | x | y | result 7 | ${[1, 2, 3, 4, 5, 6]} | ${7} | ${4} | ${[1, 2, 3, 4, 12, 10]} 8 | ${[1, 2, 3, 4, 5, 6]} | ${NaN} | ${NaN} | ${[1, 2, 3, 4, NaN, NaN]} 9 | `("$mat $x $y => $result", ({ mat, x, y, result }) => { 10 | expectMat2dEqualsApprox(mat2dTranslate(_mat2dValues(mat), x, y), _mat2dValues(result)); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/internal/_lookAt.ts: -------------------------------------------------------------------------------- 1 | import { rayAlloc } from "../rayFunctions/rayAlloc"; 2 | import { rayReset } from "../rayFunctions/rayReset"; 3 | import { Ray } from "../types"; 4 | import { EPSILON_SQ } from "./const"; 5 | 6 | export function _lookAt(x0: number, y0: number, x1: number, y1: number, out: Ray = rayAlloc()) { 7 | const dx = x1 - x0; 8 | const dy = y1 - y0; 9 | const lenSq = dx * dx + dy * dy; 10 | if (lenSq < EPSILON_SQ) { 11 | return rayReset(x0, y0, NaN, NaN, out); 12 | } else { 13 | const lenInverse = 1 / Math.sqrt(lenSq); 14 | return rayReset(x0, y0, dx * lenInverse, dy * lenInverse, out); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/intersectionResultFunctions/intersectionResultAlloc.ts: -------------------------------------------------------------------------------- 1 | import { IntersectionResult } from "../types"; 2 | 3 | class _IntersectionResult implements IntersectionResult { 4 | public exists = false; 5 | public x = NaN; 6 | public y = NaN; 7 | public t0 = NaN; 8 | public t1 = NaN; 9 | } 10 | 11 | /** 12 | * Creates a new IntersectionResult object in memory, with all values initialized to `false` and `NaN`. 13 | * This is useful to hold the result of math2d function calls in performance 14 | * critical workflows. 15 | */ 16 | export function intersectionResultAlloc(): IntersectionResult { 17 | return new _IntersectionResult(); 18 | } 19 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecTransformBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecTransformBy } from "../../vecFunctions/vecTransformBy"; 2 | import { expectVecEqualsApprox, _mat2dValues, _vecValues } from "../helpers"; 3 | 4 | describe("vecTransformBy", () => { 5 | it.each` 6 | vec | mat | result 7 | ${[3, 4]} | ${[1, 0, 0, 1, 0, 0]} | ${[3, 4]} 8 | ${[3, 4]} | ${[2, 0, 0, 2, 10, 20]} | ${[16, 28]} 9 | ${[3, 4]} | ${[0, -1, 1, 0, 10, 20]} | ${[14, 17]} 10 | `("$vec $mat => $result", ({ vec, mat, result }) => { 11 | expectVecEqualsApprox(vecTransformBy(_vecValues(vec), _mat2dValues(mat)), _vecValues(result)); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineClose.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | import { polylineAlloc } from "./polylineAlloc"; 3 | 4 | /** 5 | * Repeats the polyline's first vertex to form a closed path. 6 | * 7 | * @param poly 8 | * @param out 9 | */ 10 | export function polylineClose(poly: Polyline, out = polylineAlloc()) { 11 | const len = poly.length; 12 | if (len === 0) { 13 | out.length = 0; 14 | return out; 15 | } 16 | 17 | if (out.length !== len + 2) { 18 | out.length = len + 2; 19 | } 20 | 21 | for (let i = 0; i < len; i++) { 22 | out[i] = poly[i]; 23 | } 24 | 25 | out[len] = poly[0]; 26 | out[len + 1] = poly[1]; 27 | return out; 28 | } 29 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxClone.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxAlloc } from "../../boxFunctions/boxAlloc"; 2 | import { boxClone } from "../../boxFunctions/boxClone"; 3 | import { boxReset } from "../../boxFunctions/boxReset"; 4 | 5 | describe("boxClone", () => { 6 | it("copies components", () => { 7 | expect(boxClone(boxReset(4, 5, 6, 7))).toEqual(boxReset(4, 5, 6, 7)); 8 | }); 9 | 10 | it("returns a new box if no `out`", () => { 11 | const box = boxReset(4, 5, 6, 7); 12 | expect(boxClone(box)).not.toBe(box); 13 | }); 14 | 15 | it("returns `out` if given", () => { 16 | const out = boxAlloc(); 17 | expect(boxClone(boxReset(4, 5, 6, 7), out)).toBe(out); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineIsClosed.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON_SQ } from "../internal/const"; 2 | import { Polyline } from "../types"; 3 | 4 | /** 5 | * Returns whether the polyline's last vertex equals its first 6 | * 7 | * Computes whether the polyline forms a closed shape, i.e. its last vertex 8 | * is the same as its first. As a special case, the empty polyline `[]` is 9 | * considered closed. 10 | * 11 | * @param poly 12 | */ 13 | export function polylineIsClosed(poly: Polyline) { 14 | if (poly.length === 0) { 15 | return true; 16 | } else { 17 | const dx = poly[poly.length - 2] - poly[0]; 18 | const dy = poly[poly.length - 1] - poly[1]; 19 | return dx * dx + dy * dy < EPSILON_SQ; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/intersectionResultFunctions/intersectionResultClone.ts: -------------------------------------------------------------------------------- 1 | import { IntersectionResult } from "../types"; 2 | import { intersectionResultAlloc } from "./intersectionResultAlloc"; 3 | import { intersectionResultReset } from "./intersectionResultReset"; 4 | 5 | /** 6 | * Copies the values from the given intersection into a new intersection object. 7 | * @param intersection 8 | * @param out 9 | */ 10 | export function intersectionResultClone( 11 | intersection: IntersectionResult, 12 | out = intersectionResultAlloc(), 13 | ) { 14 | return intersectionResultReset( 15 | intersection.exists, 16 | intersection.x, 17 | intersection.y, 18 | intersection.t0, 19 | intersection.t1, 20 | out, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentReset.ts: -------------------------------------------------------------------------------- 1 | import { segmentAlloc } from "./segmentAlloc"; 2 | 3 | /** 4 | * Construct a new line segment given an (x0, y0) starting vertex and (x1, y1) ending vertex. 5 | * The two points are allowed to be the same. 6 | * 7 | * @param x0 x-coordinate of the segment's starting vertex 8 | * @param y0 y-coordinate of the segment's starting vertex 9 | * @param x1 x-coordinate of the segment's ending vertex 10 | * @param y1 y-coordinate of the segment's ending vertex 11 | * @param out 12 | */ 13 | export function segmentReset(x0: number, y0: number, x1: number, y1: number, out = segmentAlloc()) { 14 | out.x0 = x0; 15 | out.y0 = y0; 16 | out.x1 = x1; 17 | out.y1 = y1; 18 | return out; 19 | } 20 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecClone.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecAlloc } from "../../vecFunctions/vecAlloc"; 2 | import { vecClone } from "../../vecFunctions/vecClone"; 3 | import { vecReset } from "../../vecFunctions/vecReset"; 4 | import { expectVecEqualsApprox } from "../helpers"; 5 | 6 | describe("vecClone", () => { 7 | it("copies components", () => { 8 | expectVecEqualsApprox(vecClone(vecReset(4, 5)), vecReset(4, 5)); 9 | }); 10 | 11 | it("returns a new vector if no `out`", () => { 12 | const vec = vecReset(4, 5); 13 | expect(vecClone(vec)).not.toBe(vec); 14 | }); 15 | 16 | it("returns `out` if given", () => { 17 | const out = vecAlloc(); 18 | expect(vecClone(vecReset(4, 5), out)).toBe(out); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/nearestPointResultFunctions/nearestPointResultClone.ts: -------------------------------------------------------------------------------- 1 | import { NearestPointResult } from "../types"; 2 | import { nearestPointResultAlloc } from "./nearestPointResultAlloc"; 3 | import { nearestPointResultReset } from "./nearestPointResultReset"; 4 | 5 | /** 6 | * Copies the values from the given NearestPointResult into a new NearestPointResult object. 7 | * 8 | * @param nearestPointResult 9 | * @param out 10 | */ 11 | export function nearestPointResultClone(nearestPointResult: NearestPointResult, out = nearestPointResultAlloc()) { 12 | return nearestPointResultReset( 13 | nearestPointResult.x, 14 | nearestPointResult.y, 15 | nearestPointResult.t, 16 | nearestPointResult.distanceValue, 17 | out, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/boxFunctions/boxClone.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | import { boxAlloc } from "./boxAlloc"; 3 | import { boxReset } from "./boxReset"; 4 | 5 | /** 6 | * Copies values from an existing IBox into a new box. 7 | * 8 | * @param box source to copy values from 9 | * @param out destination box to copy values to 10 | * 11 | * @example 12 | * // make a copy of a given box 13 | * const myBox = boxReset(-1, -1, 1, 1); 14 | * const myBoxCopy = boxClone(myBox); 15 | * 16 | * // copy a given box into preallocated memory 17 | * const TMP_BOX = boxAlloc(); 18 | * boxClone(myBox, TMP_BOX); 19 | */ 20 | export function boxClone(box: Box, out = boxAlloc()) { 21 | return boxReset(box.minX, box.minY, box.maxX, box.maxY, out); 22 | } 23 | -------------------------------------------------------------------------------- /src/boxFunctions/boxUnion.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | import { boxAlloc } from "./boxAlloc"; 3 | import { boxReset } from "./boxReset"; 4 | 5 | /** 6 | * Compute the smallest bounding box that contains both given boxes. 7 | * 8 | * For example, if one box contains the other, this method returns the larger box. 9 | * If the two boxes don't intersect, this method returns a bounding region that covers both boxes. 10 | * 11 | * @param a 12 | * @param b 13 | * @param out 14 | */ 15 | export function boxUnion(a: Box, b: Box, out = boxAlloc()) { 16 | return boxReset( 17 | Math.min(a.minX, b.minX), 18 | Math.min(a.minY, b.minY), 19 | Math.max(a.maxX, b.maxX), 20 | Math.max(a.maxY, b.maxY), 21 | out, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/vecFunctions/vecNormalize.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON_SQ } from "../internal/const"; 2 | import { Vec } from "../types"; 3 | import { vecAlloc } from "./vecAlloc"; 4 | import { vecReset } from "./vecReset"; 5 | 6 | /** 7 | * Normalizes the vector to be length 1. If the given vector is the zero-vector, this method 8 | * returns `(NaN, NaN)`. 9 | * 10 | * @param vec the vector to normalize 11 | * @param out 12 | */ 13 | export function vecNormalize(vec: Vec, out = vecAlloc()) { 14 | const lenSq = vec.x * vec.x + vec.y * vec.y; 15 | if (lenSq < EPSILON_SQ) { 16 | return vecReset(NaN, NaN, out); 17 | } else { 18 | const lenInverse = 1 / Math.sqrt(lenSq); 19 | return vecReset(lenInverse * vec.x, lenInverse * vec.y, out); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dFromRotation.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dFromRotation } from "../../mat2dFunctions/mat2dFromRotation"; 2 | import { expectMat2dEqualsApprox, _mat2dValues } from "../helpers"; 3 | 4 | const SQRT1_2 = Math.SQRT1_2; 5 | const PI = Math.PI; 6 | describe("mat2dFromRotation", () => { 7 | it.each` 8 | rot | result 9 | ${0} | ${[1, 0, 0, 1, 0, 0]} 10 | ${PI} | ${[-1, 0, 0, -1, 0, 0]} 11 | ${(3 * PI) / 4} | ${[-SQRT1_2, -SQRT1_2, SQRT1_2, -SQRT1_2, 0, 0]} 12 | ${Math.atan2(-8, -6)} | ${[-0.6, 0.8, -0.8, -0.6, 0, 0]} 13 | `("$rot => $result", ({ rot, result }) => { 14 | expectMat2dEqualsApprox(mat2dFromRotation(rot), _mat2dValues(result)); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/vecFunctions/vecLerp.ts: -------------------------------------------------------------------------------- 1 | import { _lerp } from "../internal/_lerp"; 2 | import { Vec } from "../types"; 3 | import { vecAlloc } from "./vecAlloc"; 4 | import { vecReset } from "./vecReset"; 5 | 6 | /** 7 | * Performs a linear interpolation between the two vectors. The `r` parameter is allowed to be outside `[0, 1]`. 8 | * 9 | * @param u the vector to start interpolation from 10 | * @param v the vector to end interpolation with 11 | * @param r the ratio to interpolate the two vectors, with _r_ = 0 returning the first vector `u` and _r_ = 1 returning 12 | * the second vector `v` 13 | * @param out 14 | */ 15 | export function vecLerp(u: Vec, v: Vec, r: number, out = vecAlloc()) { 16 | return vecReset(_lerp(u.x, v.x, r), _lerp(u.y, v.y, r), out); 17 | } 18 | -------------------------------------------------------------------------------- /src/boxFunctions/boxGrow.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | import { boxAlloc } from "./boxAlloc"; 3 | import { boxReset } from "./boxReset"; 4 | 5 | /** 6 | * Expands a box by a given amount in all directions. 7 | * 8 | * Forms a new box with bounding edges `[(minX - amount) (minY - amount) (maxX + amount) (maxY + amount)]`. 9 | * 10 | * The `amount` parameter is allowed to be negative, which effectively shrinks the box. 11 | * 12 | * @param box the box to grow 13 | * @param amount amount to expand from each edge. Is allowed to be negative. 14 | * @param out 15 | */ 16 | export function boxGrow(box: Box, amount: number, out = boxAlloc()) { 17 | return boxReset(box.minX - amount, box.minY - amount, box.maxX + amount, box.maxY + amount, out); 18 | } 19 | -------------------------------------------------------------------------------- /src/rayFunctions/rayContainsPoint.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON } from "../internal/const"; 2 | import { _dot } from "../internal/_dot"; 3 | import { _dotPerp } from "../internal/_dotPerp"; 4 | import { Ray, Vec } from "../types"; 5 | 6 | /** 7 | * Determines if the point is on the ray 8 | * 9 | * This function tests whether the point lies along the ray's geometry, 10 | * using allowed error _ε_ = 1e-8. The point must lie on the positive side 11 | * of the ray (_t_ >= 0). 12 | * 13 | * @param ray the ray to inspect 14 | * @param vec point to be checked 15 | */ 16 | export function rayContainsPoint(ray: Ray, vec: Vec) { 17 | const t = _dot(ray, vec); 18 | if (t < -EPSILON) { 19 | return false; 20 | } 21 | 22 | return Math.abs(_dotPerp(ray, vec)) < EPSILON; 23 | } 24 | -------------------------------------------------------------------------------- /src/__test__/pointIntersectionResultFunctions/intersectionResultReset.spec.ts: -------------------------------------------------------------------------------- 1 | import { intersectionResultAlloc } from "../../intersectionResultFunctions/intersectionResultAlloc"; 2 | import { intersectionResultReset } from "../../intersectionResultFunctions/intersectionResultReset"; 3 | 4 | describe("intersectionResultReset", () => { 5 | it("copies components", () => { 6 | const res = intersectionResultReset(true, 4, 5, 6, 7); 7 | expect(res.exists).toBe(true); 8 | expect(res.x).toBe(4); 9 | expect(res.y).toBe(5); 10 | expect(res.t0).toBe(6); 11 | expect(res.t1).toBe(7); 12 | }); 13 | 14 | it("returns `out` if given", () => { 15 | const out = intersectionResultAlloc(); 16 | expect(intersectionResultReset(true, 4, 5, 6, 7, out)).toBe(out); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/rayFunctions/rayProjectPoint.ts: -------------------------------------------------------------------------------- 1 | import { _dot } from "../internal/_dot"; 2 | import { Ray, Vec } from "../types"; 3 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 4 | import { vecReset } from "../vecFunctions/vecReset"; 5 | 6 | /** 7 | * Projects a point onto the given line, returning the distance _t_ along the line where it falls. 8 | * 9 | * This function basically computes the dot product of the relative vector from the line's initial 10 | * point to the given point. 11 | * 12 | * @param ray line to inspect 13 | * @param point point to project onto the line 14 | * @param out 15 | */ 16 | export function rayProjectPoint(ray: Ray, point: Vec, out = vecAlloc()) { 17 | const t = _dot(ray, point); 18 | return vecReset(ray.x0 + t * ray.dirX, ray.y0 + t * ray.dirY, out); 19 | } 20 | -------------------------------------------------------------------------------- /src/boxFunctions/boxEncapsulate.ts: -------------------------------------------------------------------------------- 1 | import { Box, Vec } from "../types"; 2 | import { boxAlloc } from "./boxAlloc"; 3 | import { boxReset } from "./boxReset"; 4 | 5 | /** 6 | * Grows the box to include a given point. 7 | * 8 | * Extends the box's bounding edges, if needed, to encapsulate the given point. 9 | * If the point is already inside the box, this function does nothing. 10 | * 11 | * @param box the box to potentially grow 12 | * @param point the point that the box should grow to include 13 | * @param out 14 | */ 15 | export function boxEncapsulate(box: Box, point: Vec, out = boxAlloc()) { 16 | return boxReset( 17 | Math.min(box.minX, point.x), 18 | Math.min(box.minY, point.y), 19 | Math.max(box.maxX, point.x), 20 | Math.max(box.maxY, point.y), 21 | out, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dIsOrthogonal.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dIsOrthogonal } from "../../mat2dFunctions/mat2dIsOrthogonal"; 2 | import { _mat2dValues } from "../helpers"; 3 | 4 | describe("mat2dIsOrthogonal", () => { 5 | it.each` 6 | mat | result 7 | ${[1, 0, 0, 1, 0, 0]} | ${true} 8 | ${[1, 0, 0, 1, 6, -8]} | ${true} 9 | ${[1, 0, 0, -1, 0, 0]} | ${true} 10 | ${[0.6, -0.8, 0.8, 0.6, 0, 0]} | ${true} 11 | ${[2, 0, 0, 2, 0, 0]} | ${false} 12 | ${[2, 0, 0, 0.5, 0, 0]} | ${false} 13 | ${[1, 0, 1, 0, 0, 0]} | ${false} 14 | ${[NaN, NaN, NaN, NaN, 0, 0]} | ${false} 15 | `("$mat => $result", ({ mat, result }) => { 16 | expect(mat2dIsOrthogonal(_mat2dValues(mat))).toBe(result); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dMulMat2d.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dMulMat2d } from "../../mat2dFunctions/mat2dMulMat2d"; 2 | import { expectMat2dEqualsApprox, _mat2dValues } from "../helpers"; 3 | 4 | describe("mat2dMulMat2d", () => { 5 | it.each` 6 | a | b | result 7 | ${[1, 0, 0, 1, 0, 0]} | ${[1, 0, 0, 1, 0, 0]} | ${[1, 0, 0, 1, 0, 0]} 8 | ${[3, 4, 5, 6, 7, 8]} | ${[1, 0, 0, 1, 0, 0]} | ${[3, 4, 5, 6, 7, 8]} 9 | ${[1, -1, 1, 1, 0, 0]} | ${[1, 1, -1, 1, 0, 0]} | ${[2, 0, 0, 2, 0, 0]} 10 | ${[3, 4, 5, 6, 7, 8]} | ${[1, -2, 3, -4, 5, -6]} | ${[-7, -8, -11, -12, -8, -2]} 11 | `("$a $b => $result", ({ a, b, result }) => { 12 | expectMat2dEqualsApprox(mat2dMulMat2d(_mat2dValues(a), _mat2dValues(b)), _mat2dValues(result)); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "importHelpers": true, 6 | "lib": [ 7 | "es2015" 8 | ], 9 | "module": "CommonJS", 10 | "moduleResolution": "node", 11 | "noEmit": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "plugins": [{ "name": "typescript-tslint-plugin" }], 16 | "pretty": true, 17 | "removeComments": false, 18 | "rootDir": "src", 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "stripInternal": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "target": "ES5" 24 | }, 25 | "exclude": [ 26 | "benchmarks", 27 | "esm", 28 | "lib", 29 | "node_modules" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dClone.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dAlloc } from "../../mat2dFunctions/mat2dAlloc"; 2 | import { mat2dClone } from "../../mat2dFunctions/mat2dClone"; 3 | import { mat2dReset } from "../../mat2dFunctions/mat2dReset"; 4 | import { expectMat2dEqualsApprox } from "../helpers"; 5 | 6 | describe("mat2dClone", () => { 7 | it("copies components", () => { 8 | expectMat2dEqualsApprox(mat2dClone(mat2dReset(3, 4, 5, 6, 7, 8)), mat2dReset(3, 4, 5, 6, 7, 8)); 9 | }); 10 | 11 | it("returns a new mat2d if no `out`", () => { 12 | const mat = mat2dReset(3, 4, 5, 6, 7, 8); 13 | expect(mat2dClone(mat)).not.toBe(mat); 14 | }); 15 | 16 | it("returns `out` if given", () => { 17 | const out = mat2dAlloc(); 18 | expect(mat2dClone(mat2dReset(3, 4, 5, 6, 7, 8), out)).toBe(out); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/boxFunctions/boxIsEmpty.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | 3 | /** 4 | * Determines whether this box represents an empty area. 5 | * 6 | * A box is considered empty if its `maxX` is less than or equal to its `minX` or 7 | * its `maxY` is less than or equal to its `minY`. 8 | * 9 | * This function handles `Infinity`, `-Infinity`, and `NaN` values: 10 | * - Any box that contains a NaN edge is considered empty 11 | * - Edges with non-finite values are compared according to normal mathematical rules, so e.g. the 12 | * [-∞, +∞]×[-∞, +∞] box is NOT empty, but the [+∞, -∞]×[-1, 1] box IS empty. 13 | * 14 | * @param box 15 | */ 16 | export function boxIsEmpty(box: Box) { 17 | // Remark: prefer this comparison over distributing the ! to handle NaNs. 18 | return !(box.maxX > box.minX && box.maxY > box.minY); 19 | } 20 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetBounds.ts: -------------------------------------------------------------------------------- 1 | import { boxAlloc } from "../boxFunctions/boxAlloc"; 2 | import { boxEncapsulate } from "../boxFunctions/boxEncapsulate"; 3 | import { boxReset } from "../boxFunctions/boxReset"; 4 | import { Polyline } from "../types"; 5 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 6 | import { vecReset } from "../vecFunctions/vecReset"; 7 | 8 | /** 9 | * Computes bounding box of polyline's geometry 10 | * 11 | * @param poly 12 | * @param out 13 | */ 14 | export function polylineGetBounds(poly: Polyline, out = boxAlloc()) { 15 | const tmp0 = vecAlloc(); 16 | boxReset(Infinity, Infinity, -Infinity, -Infinity, out); 17 | for (let i = 0; i < poly.length; i += 2) { 18 | const v0 = vecReset(poly[i], poly[i + 1], tmp0); 19 | boxEncapsulate(out, v0, out); 20 | } 21 | 22 | return out; 23 | } 24 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dFromRotation.ts: -------------------------------------------------------------------------------- 1 | import { mat2dAlloc } from "./mat2dAlloc"; 2 | import { mat2dReset } from "./mat2dReset"; 3 | 4 | /** 5 | * Computes the affine transform corresponding to a given rotation, in radians 6 | * 7 | * Calculates the matrix that would yield a rotation of `theta` radians around the origin. 8 | * The rotation is from the `x+` to `y+` direction, which is _counter-clockwise_ in the 9 | * standard Cartesian coordinate system or _clockwise_ in most standard graphics 10 | * coordinate systems, as in Canvas and the DOM. 11 | * 12 | * @param theta angle in radians to create rotation for 13 | * @param out 14 | */ 15 | export function mat2dFromRotation(theta: number, out = mat2dAlloc()) { 16 | const sin = Math.sin(theta); 17 | const cos = Math.cos(theta); 18 | return mat2dReset(cos, -sin, sin, cos, 0, 0, out); 19 | } 20 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxEncapsulate.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxEncapsulate } from "../../boxFunctions/boxEncapsulate"; 2 | import { expectBoxEqualsApprox, _boxValues, _vecValues } from "../helpers"; 3 | 4 | describe("boxEncapsulate", () => { 5 | it.each` 6 | box | point | result 7 | ${[-1, -1, 1, 1]} | ${[0, 0]} | ${[-1, -1, 1, 1]} 8 | ${[-1, -1, 1, 1]} | ${[1, 0]} | ${[-1, -1, 1, 1]} 9 | ${[-1, -1, 1, 1]} | ${[0, -2]} | ${[-1, -2, 1, 1]} 10 | ${[-1, -1, 1, 1]} | ${[-2, -3]} | ${[-2, -3, 1, 1]} 11 | ${[-1, -1, 1, 1]} | ${[-2, 4]} | ${[-2, -1, 1, 4]} 12 | ${[-1, -1, 1, 1]} | ${[NaN, NaN]} | ${[NaN, NaN, NaN, NaN]} 13 | `("$box $point => $result", ({ box, point, result }) => { 14 | expectBoxEqualsApprox(boxEncapsulate(_boxValues(box), _vecValues(point)), _boxValues(result)); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dTranslate.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d } from "../types"; 2 | import { mat2dAlloc } from "./mat2dAlloc"; 3 | import { mat2dReset } from "./mat2dReset"; 4 | 5 | /** 6 | * Applies a translation on top of the given matrix, returning the result. 7 | * 8 | * This is equivalent to _left_-multiplying the matrix by a translation transform; that is, 9 | * the result of this function is equivalent to a transform that first applies the matrix _mat_ 10 | * and then translates according to (+tx, +ty). 11 | * 12 | * @param mat the matrix to transform 13 | * @param tx x-component of the translation to apply 14 | * @param ty y-component of the translation to apply 15 | * @param out 16 | */ 17 | export function mat2dTranslate(mat: Mat2d, tx: number, ty: number, out = mat2dAlloc()) { 18 | return mat2dReset(mat.a, mat.b, mat.c, mat.d, mat.tx + tx, mat.ty + ty, out); 19 | } 20 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dIsTranslationOnly.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dIsTranslationOnly } from "../../mat2dFunctions/mat2dIsTranslationOnly"; 2 | import { _mat2dValues } from "../helpers"; 3 | 4 | describe("mat2dIsTranslationOnly", () => { 5 | it.each` 6 | mat | result 7 | ${[1, 0, 0, 1, 0, 0]} | ${true} 8 | ${[1, 0, 0, 1, 6, -8]} | ${true} 9 | ${[1, 0, 0, -1, 0, 0]} | ${false} 10 | ${[0.6, -0.8, 0.8, 0.6, 0, 0]} | ${false} 11 | ${[2, 0, 0, 2, 0, 0]} | ${false} 12 | ${[2, 0, 0, 0.5, 0, 0]} | ${false} 13 | ${[1, 0, 1, 0, 0, 0]} | ${false} 14 | ${[NaN, NaN, NaN, NaN, 0, 0]} | ${false} 15 | ${[1, 0, 0, 1, NaN, NaN]} | ${true} 16 | `("$mat => $result", ({ mat, result }) => { 17 | expect(mat2dIsTranslationOnly(_mat2dValues(mat))).toBe(result); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/rayFunctions/rayGetPointAtT.ts: -------------------------------------------------------------------------------- 1 | import { Ray } from "../types"; 2 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 3 | import { vecReset } from "../vecFunctions/vecReset"; 4 | 5 | /** 6 | * Gets a point along the ray, parameterized according to distance along its 7 | * direction vector. 8 | * 9 | * Retrieves a point on the ray's geometry according to moving distance _t_ 10 | * along the direction vector from its initial point. This function does allow 11 | * negative values of _t_, returning points in the direction "behind" the ray. 12 | * 13 | * Synonymous to {@link lineGetPointAt}. 14 | * 15 | * @param ray the ray to inspect 16 | * @param t distance along the ray at which to compute point 17 | * @param out 18 | */ 19 | export function rayGetPointAtT(ray: Ray, t: number, out = vecAlloc()) { 20 | return vecReset(ray.x0 + ray.dirX * t, ray.y0 + ray.dirY * t, out); 21 | } 22 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dInvert.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dInvert } from "../../mat2dFunctions/mat2dInvert"; 2 | import { expectMat2dEqualsApprox, _mat2dValues } from "../helpers"; 3 | 4 | describe("mat2dInvert", () => { 5 | it.each` 6 | mat | result 7 | ${[1, 0, 0, 1, 0, 0]} | ${[1, 0, 0, 1, 0, 0]} 8 | ${[1, 0, 0, 1, -10, 20]} | ${[1, 0, 0, 1, 10, -20]} 9 | ${[0.5, 0, 0, -0.25, 0, 0]} | ${[2, 0, 0, -4, 0, 0]} 10 | ${[0, 0.5, -0.5, 0, 6, -8]} | ${[0, -2, 2, 0, 16, 12]} 11 | ${[4, 4, 4, 4, 6, 8]} | ${[NaN, NaN, NaN, NaN, NaN, NaN]} 12 | ${[1, 0, 1, 0, 6, 8]} | ${[NaN, NaN, NaN, NaN, NaN, NaN]} 13 | ${[3, 12, -4, -16, 0, 0]} | ${[NaN, NaN, NaN, NaN, NaN, NaN]} 14 | `("$mat => $result", ({ mat, result }) => { 15 | expectMat2dEqualsApprox(mat2dInvert(_mat2dValues(mat)), _mat2dValues(result)); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__test__/mat2dFunctions/mat2dRotate.spec.ts: -------------------------------------------------------------------------------- 1 | import { mat2dRotate } from "../../mat2dFunctions/mat2dRotate"; 2 | import { expectMat2dEqualsApprox, _mat2dValues } from "../helpers"; 3 | 4 | const PI = Math.PI; 5 | const SQRT3 = Math.sqrt(3); 6 | 7 | describe("mat2dRotate", () => { 8 | it.each` 9 | mat | rot | result 10 | ${[1, 0, 0, 1, 0, 0]} | ${PI / 3} | ${[0.5, -0.5 * SQRT3, 0.5 * SQRT3, 0.5, 0, 0]} 11 | ${[1, 0, 0, 1, 10, 20]} | ${PI / 3} | ${[0.5, -0.5 * SQRT3, 0.5 * SQRT3, 0.5, 10, 20]} 12 | ${[0.5, -0.5 * SQRT3, 0.5 * SQRT3, 0.5, 10, 20]} | ${(2 * PI) / 3} | ${[-1, 0, 0, -1, 10, 20]} 13 | `("$mat $rot => $result", ({ mat, rot, result }) => { 14 | expectMat2dEqualsApprox(mat2dRotate(_mat2dValues(mat), rot), _mat2dValues(result)); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxIsEmpty.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxIsEmpty } from "../../boxFunctions/boxIsEmpty"; 2 | import { _boxValues } from "../helpers"; 3 | 4 | describe("boxIsEmpty", () => { 5 | it.each` 6 | box | result 7 | ${[-1, -1, 1, 1]} | ${false} 8 | ${[3, -1, 5, 1]} | ${false} 9 | ${[0, 0, 0, 0]} | ${true} 10 | ${[-Infinity, -Infinity, Infinity, Infinity]} | ${false} 11 | ${[Infinity, Infinity, -Infinity, -Infinity]} | ${true} 12 | ${[0, 0, -1, -1]} | ${true} 13 | ${[Infinity, Infinity, 0, 0]} | ${true} 14 | ${[NaN, NaN, NaN, NaN]} | ${true} 15 | `("$box => $result", ({ box, result }) => { 16 | expect(boxIsEmpty(_boxValues(box))).toBe(result); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/__test__/vecFunctions/vecLerp.spec.ts: -------------------------------------------------------------------------------- 1 | import { vecLerp } from "../../vecFunctions/vecLerp"; 2 | import { expectVecEqualsApprox, _vecValues } from "../helpers"; 3 | 4 | describe("vecLerp", () => { 5 | it.each` 6 | v0 | v1 | scalar | result 7 | ${[0, 0]} | ${[1, 0]} | ${0} | ${[0, 0]} 8 | ${[0, 0]} | ${[1, 0]} | ${0.5} | ${[0.5, 0]} 9 | ${[0, 0]} | ${[1, 0]} | ${1} | ${[1, 0]} 10 | ${[4, 4]} | ${[4, 4]} | ${0} | ${[4, 4]} 11 | ${[4, 4]} | ${[4, 4]} | ${2} | ${[4, 4]} 12 | ${[10, 10]} | ${[20, 30]} | ${0.75} | ${[17.5, 25]} 13 | ${[0, 0]} | ${[1, 1]} | ${NaN} | ${[NaN, NaN]} 14 | ${[0, 0]} | ${[0, 0]} | ${NaN} | ${[NaN, NaN]} 15 | `("$v0 $v1 $scalar => $result", ({ v0, v1, scalar, result }) => { 16 | expectVecEqualsApprox(vecLerp(_vecValues(v0), _vecValues(v1), scalar), _vecValues(result)); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/vecFunctions/vecTransformBy.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d, Vec } from "../types"; 2 | import { vecAlloc } from "./vecAlloc"; 3 | import { vecReset } from "./vecReset"; 4 | 5 | /** 6 | * Multiplies the vector by an affine matrix. 7 | * 8 | * This computes a left multiplication of the vector by a matrix, i.e. _M_ × _v_. 9 | * 10 | * Per usual linear algebra rules, multiplying the vector `(x, y)` according to an affine matrix 11 | * `[a b c d e f]` is defined by: 12 | * 13 | * ``` 14 | * ⎡a c tx⎤ ⎛x⎞ ⎛ax + cy + tx⎞ 15 | * ⎢b d ty⎥ ⎜y⎟ = ⎜bx + dy + ty⎟ 16 | * ⎣0 0 1⎦ ⎝1⎠ ⎝ 1 ⎠ 17 | * ``` 18 | * 19 | * @param v the vector to transform 20 | * @param mat the matrix to multiply this vector by 21 | * @param out 22 | */ 23 | export function vecTransformBy(v: Vec, mat: Mat2d, out = vecAlloc()) { 24 | return vecReset(mat.a * v.x + mat.c * v.y + mat.tx, mat.b * v.x + mat.d * v.y + mat.ty, out); 25 | } 26 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineContainsPointInside.ts: -------------------------------------------------------------------------------- 1 | import { Polyline, Vec } from "../types"; 2 | 3 | /** 4 | * Determines whether the point is inside the given polygon, using the even-odd fill rule. 5 | * 6 | * @param poly the polygon to inspect 7 | * @param point the point to check for containment 8 | */ 9 | export function polylineContainsPointInside(poly: Polyline, point: Vec) { 10 | const x = point.x; 11 | const y = point.y; 12 | 13 | let inside = false; 14 | for (let i = 0; i < poly.length; i += 2) { 15 | const x0 = poly[i]; 16 | const y0 = poly[i + 1]; 17 | const x1 = i !== poly.length - 2 ? poly[i + 2] : poly[0]; 18 | const y1 = i !== poly.length - 2 ? poly[i + 3] : poly[1]; 19 | const intersects = y0 > y !== y1 > y && x - x0 < ((x1 - x0) * (y - y0)) / (y1 - y0); 20 | 21 | if (intersects) { 22 | inside = !inside; 23 | } 24 | } 25 | 26 | return inside; 27 | } 28 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dInvert.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON } from "../internal/const"; 2 | import { Mat2d } from "../types"; 3 | import { mat2dAlloc } from "./mat2dAlloc"; 4 | import { mat2dReset } from "./mat2dReset"; 5 | 6 | /** 7 | * Computes the inverse of the given 2d affine matrix 8 | * 9 | * @param mat the matrix to invert 10 | * @param out 11 | */ 12 | export function mat2dInvert(mat: Mat2d, out = mat2dAlloc()) { 13 | const det = mat.a * mat.d - mat.b * mat.c; 14 | if (det > -EPSILON && det < EPSILON) { 15 | return mat2dReset(NaN, NaN, NaN, NaN, NaN, NaN, out); 16 | } else { 17 | const detInverse = 1 / det; 18 | return mat2dReset( 19 | detInverse * mat.d, 20 | -detInverse * mat.b, 21 | -detInverse * mat.c, 22 | detInverse * mat.a, 23 | detInverse * (mat.c * mat.ty - mat.d * mat.tx), 24 | detInverse * (mat.b * mat.tx - mat.a * mat.ty), 25 | out, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/rayFunctions/rayLookAt.ts: -------------------------------------------------------------------------------- 1 | import { _lookAt } from "../internal/_lookAt"; 2 | import { Vec } from "../types"; 3 | import { rayAlloc } from "./rayAlloc"; 4 | 5 | /** 6 | * Constructs a ray from an initial point, pointing in the direction of a target point. 7 | * 8 | * This function initializes a ray with a given `from` initial point, and with a direction vector 9 | * that goes through the target `to` point. The direction vector will be normalized even if `from` 10 | * and `to` are not one unit apart. 11 | * 12 | * If `from` and `to` are the same point, the returned vector will still have the `from` initial 13 | * point but its direction vector will be (NaN, NaN). 14 | * 15 | * @param from initial point of the ray 16 | * @param to point that the ray should go through 17 | * @param out 18 | */ 19 | export function rayLookAt(from: Vec, to: Vec, out = rayAlloc()) { 20 | return _lookAt(from.x, from.y, to.x, to.y, out); 21 | } 22 | -------------------------------------------------------------------------------- /src/boxFunctions/boxContainsPoint.ts: -------------------------------------------------------------------------------- 1 | import { IntervalMode } from "../const"; 2 | import { Box, Vec } from "../types"; 3 | 4 | /** 5 | * Determines whether the box contains a given point. 6 | * 7 | * Checks whether the point is inside the box's enclosed region. 8 | * The box is treated as a closed area, 9 | * so points on the boundary of the box will return true. 10 | * 11 | * @param box 12 | * @param point 13 | */ 14 | export function boxContainsPoint(box: Box, point: Vec, mode: IntervalMode) { 15 | switch (mode) { 16 | case IntervalMode.OPEN: 17 | return point.x > box.minX && point.y > box.minY && point.x < box.maxX && point.y < box.maxY; 18 | case IntervalMode.CLOSED: 19 | return point.x >= box.minX && point.y >= box.minY && point.x <= box.maxX && point.y <= box.maxY; 20 | case IntervalMode.OPEN_ABOVE: 21 | return point.x >= box.minX && point.y >= box.minY && point.x < box.maxX && point.y < box.maxY; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dIsOrthogonal.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON, EPSILON_SQ } from "../internal/const"; 2 | import { Mat2d } from "../types"; 3 | 4 | /** 5 | * Returns whether the matrix is an orthogonal matrix. 6 | * 7 | * An orthogonal matrix is defined as one whose rows and columns are orthogonal unit vectors. 8 | * (For a 2d affine matrix, this is constrained to its first 2x2 submatrix, i.e. excluding the translation.) 9 | * 10 | * This is a useful property for a matrix because it means the transform preserves lengths and angles, 11 | * so in particular it preserves normals. 12 | * 13 | * @param mat the matrix to check for orthogonality 14 | */ 15 | export function mat2dIsOrthogonal(mat: Mat2d) { 16 | const d1Sq = mat.a * mat.a + mat.b * mat.b; 17 | const d2Sq = mat.c * mat.c + mat.d * mat.d; 18 | const dot = mat.a * mat.c + mat.b * mat.d; 19 | return Math.abs(d1Sq - 1) < EPSILON_SQ && Math.abs(d2Sq - 1) < EPSILON_SQ && Math.abs(dot) < EPSILON; 20 | } 21 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dScale.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d } from "../types"; 2 | import { mat2dAlloc } from "./mat2dAlloc"; 3 | import { mat2dReset } from "./mat2dReset"; 4 | 5 | /** 6 | * Applies a scaling transform on top of the given affine matrix, returning the result. 7 | * 8 | * Multiplies all components of the matrix by a given scalar value. 9 | * 10 | * This is equivalent to _left_-multiplying the matrix by a scaling transform; that is, 11 | * the result of this function is equivalent to a transform that first applies the matrix _mat_ 12 | * and then scales in both the x- and y-directions according to _scale_. 13 | * 14 | * @param mat the matrix to transform 15 | * @param theta rotation angle in radians to apply on top of the given matrix 16 | * @param out 17 | */ 18 | export function mat2dScale(mat: Mat2d, scale: number, out = mat2dAlloc()) { 19 | return mat2dReset(scale * mat.a, scale * mat.b, scale * mat.c, scale * mat.d, scale * mat.tx, scale * mat.ty, out); 20 | } 21 | -------------------------------------------------------------------------------- /src/rayFunctions/rayReset.ts: -------------------------------------------------------------------------------- 1 | import { rayAlloc } from "./rayAlloc"; 2 | 3 | /** 4 | * Construct a new ray given an (x0, y0) initial point and (dirX, dirY) direction vector. 5 | * 6 | * This function creates a new ray from given values. The (dirX, dirY) values given should 7 | * describe a unit vector: no checks or operations are done internally to guarantee that is so. 8 | * 9 | * @param x0 x-coordinate of the ray's initial point 10 | * @param y0 y-coordinate of the ray's initial point 11 | * @param dirX x-coordinate of the ray's direction vector, which should form a unit vector 12 | * along with the provided `dirY` 13 | * @param dirY y-coordinate of the ray's direction vector, which should form a unit vector 14 | * along with the provided `dirX` 15 | * @param out 16 | */ 17 | export function rayReset(x0: number, y0: number, dirX: number, dirY: number, out = rayAlloc()) { 18 | out.x0 = x0; 19 | out.y0 = y0; 20 | out.dirX = dirX; 21 | out.dirY = dirY; 22 | return out; 23 | } 24 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxContainsBox.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxContainsBox } from "../../boxFunctions/boxContainsBox"; 2 | import { _boxValues } from "../helpers"; 3 | 4 | describe("boxContainsBox", () => { 5 | it.each` 6 | a | b | result 7 | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 1]} | ${true} 8 | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 2]} | ${false} 9 | ${[-1, -1, 1, 1]} | ${[0, 0, 0, 0]} | ${true} 10 | ${[-1, -1, 1, 1]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${true} 11 | ${[-Infinity, -Infinity, Infinity, Infinity]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${true} 12 | ${[Infinity, Infinity, -Infinity, -Infinity]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${false} 13 | `("$a $b => $result", ({ a, b, result }) => { 14 | expect(boxContainsBox(_boxValues(a), _boxValues(b))).toBe(result); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxGetOutCode.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-bitwise 2 | import { boxGetOutCode } from "../../boxFunctions/boxGetOutCode"; 3 | import { Out } from "../../const"; 4 | import { _boxValues, _vecValues } from "../helpers"; 5 | 6 | describe(`boxGetOutCode [MIN_X=${Out.MIN_X}, MAX_X=${Out.MAX_X}, MIN_Y=${Out.MIN_Y}, MAX_Y=${Out.MAX_Y}]`, () => { 7 | it.each` 8 | box | vec | result 9 | ${[-1, -1, 1, 1]} | ${[0, 2]} | ${Out.MAX_Y} 10 | ${[-1, -1, 1, 1]} | ${[0, -2]} | ${Out.MIN_Y} 11 | ${[-1, -1, 1, 1]} | ${[-2, 0]} | ${Out.MIN_X} 12 | ${[-1, -1, 1, 1]} | ${[2, 0]} | ${Out.MAX_X} 13 | ${[-1, -1, 1, 1]} | ${[-3, 3]} | ${Out.MIN_X | Out.MAX_Y} 14 | ${[-1, -1, 1, 1]} | ${[-1, 0]} | ${0} 15 | ${[-1, -1, 1, 1]} | ${[1, 0]} | ${0} 16 | ${[-1, -1, 1, 1]} | ${[0, 0]} | ${0} 17 | `("$box $vec => $result", ({ box, vec, result }) => { 18 | expect(boxGetOutCode(_boxValues(box), _vecValues(vec))).toBe(result); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/boxFunctions/boxIntersection.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | import { boxAlloc } from "./boxAlloc"; 3 | import { boxReset } from "./boxReset"; 4 | 5 | /** 6 | * Computes the area intersection of the two box regions. 7 | * 8 | * Given two boxes, this function computes the region where they overlap. 9 | * If the two boxes do not overlap, 10 | * the returned value will be an empty box, i.e. with `maxX < minX` or `maxY < minY`. 11 | * 12 | * The similar function {@link boxIntersectsBox} will check whether two 13 | * boxes intersect, without actually computing that intersection region. 14 | * 15 | * @param a first box to compute intersection for 16 | * @param b second box to compute intersection for 17 | * @param out 18 | */ 19 | export function boxIntersection(a: Box, b: Box, out = boxAlloc()) { 20 | return boxReset( 21 | Math.max(a.minX, b.minX), 22 | Math.max(a.minY, b.minY), 23 | Math.min(a.maxX, b.maxX), 24 | Math.min(a.maxY, b.maxY), 25 | out, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxTransformBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxTransformBy } from "../../boxFunctions/boxTransformBy"; 2 | import { expectBoxEqualsApprox, _boxValues, _mat2dValues } from "../helpers"; 3 | 4 | describe("boxTransformBy", () => { 5 | const SQRT2 = Math.SQRT2; 6 | it.each` 7 | box | mat | result 8 | ${[4, 5, 6, 7]} | ${[1, 0, 0, 1, 0, 0]} | ${[4, 5, 6, 7]} 9 | ${[4, 5, 6, 7]} | ${[1, 0, 0, 1, -10, -20]} | ${[-6, -15, -4, -13]} 10 | ${[4, 5, 6, 7]} | ${[0, -1, 1, 0, -10, -20]} | ${[-5, -26, -3, -24]} 11 | ${[-2, -2, 2, 2]} | ${[-0.5 * SQRT2, -0.5 * SQRT2, 0.5 * SQRT2, -0.5 * SQRT2, 0, 0]} | ${[-2 * SQRT2, -2 * SQRT2, 2 * SQRT2, 2 * SQRT2]} 12 | `("$box $mat => $result", ({ box, mat, result }) => { 13 | expectBoxEqualsApprox(boxTransformBy(_boxValues(box), _mat2dValues(mat)), _boxValues(result)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/boxFunctions/boxIntersectsBox.ts: -------------------------------------------------------------------------------- 1 | import { IntervalMode } from "../const"; 2 | import { Box } from "../types"; 3 | 4 | /** 5 | * Determines whether two boxes overlap. 6 | * 7 | * This function checks whether the two boxes intersect, as areas. Both boxes are treated as closed 8 | * regions, so e.g. this function will return true if the boxes share only a single edge. 9 | * 10 | * The similar function {@link boxIntersection} can compute the overlap region. 11 | * 12 | * @param a first box to check for overlap 13 | * @param b second box to check for overlap 14 | * @param mode whether to include the boundaries of the boxes 15 | */ 16 | export function boxIntersectsBox(a: Box, b: Box, mode: IntervalMode) { 17 | switch (mode) { 18 | case IntervalMode.OPEN: 19 | case IntervalMode.OPEN_ABOVE: 20 | return a.minX < b.maxX && a.minY < b.maxY && a.maxX > b.minX && a.maxY > b.minY; 21 | 22 | case IntervalMode.CLOSED: 23 | return a.minX <= b.maxX && a.minY <= b.maxY && a.maxX >= b.minX && a.maxY >= b.minY; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dReset.ts: -------------------------------------------------------------------------------- 1 | import { mat2dAlloc } from "./mat2dAlloc"; 2 | 3 | /** 4 | * Construct a new matrix given component values. 5 | * 6 | * The resulting matrix will have the shape 7 | * 8 | * ``` 9 | * ⎡a c tx⎤ 10 | * ⎣b d ty⎦ 11 | * ``` 12 | * 13 | * @param a col 1, row 1 component, usually called `m11` in a 4x4 graphics matrix 14 | * @param b col 1, row 2 component, usually called `m12` in a 4x4 graphics matrix 15 | * @param c col 2, row 1 component, usually called `m21` in a 4x4 graphics matrix 16 | * @param d col 2, row 2 component, usually called `m22` in a 4x4 graphics matrix 17 | * @param tx col 3, row 1 component, usually called `m41` in a 4x4 graphics matrix 18 | * @param ty col 3, row 2 component, usually called `m42` in a 4x4 graphics matrix 19 | * @param out 20 | */ 21 | export function mat2dReset(a: number, b: number, c: number, d: number, tx: number, ty: number, out = mat2dAlloc()) { 22 | out.a = a; 23 | out.b = b; 24 | out.c = c; 25 | out.d = d; 26 | out.tx = tx; 27 | out.ty = ty; 28 | return out; 29 | } 30 | -------------------------------------------------------------------------------- /src/vecFunctions/vecAlloc.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | 3 | class _Vec implements Vec { 4 | public x = NaN; 5 | public y = NaN; 6 | } 7 | 8 | /** 9 | * Creates a new Vec object in memory, with all values initialized to `NaN`. 10 | * 11 | * Data allocation functions like `vecAlloc()` are useful to hold results of 12 | * Math2d function calls in inner loops of performance critical workflows. 13 | * 14 | * As with any optimization, you don't need it until you've profiled your 15 | * application! Getting into temp variable management and shared state can 16 | * severely hurt code readability and maintainability, so it's best to avoid 17 | * such optimization if you can. 18 | * 19 | * @example 20 | * // initialize temp memory 21 | * const TMP0 = vecAlloc(); 22 | * 23 | * ... { 24 | * // Use this temp memory to hold result of `vecNormalize()`. 25 | * // This avoids a heap allocation. 26 | * const result = vecNormalize(existingObj.velocity, TMP0); 27 | * } 28 | */ 29 | export function vecAlloc(): Vec { 30 | return new _Vec(); 31 | } 32 | -------------------------------------------------------------------------------- /src/boxFunctions/boxEnclosingPoints.ts: -------------------------------------------------------------------------------- 1 | import { Vec } from "../types"; 2 | import { boxAlloc } from "./boxAlloc"; 3 | import { boxReset } from "./boxReset"; 4 | 5 | /** 6 | * Computes the smallest bounding box that contains all of the provided points. 7 | * 8 | * If the provided array is empty, this method returns a box with `minX` and 9 | * `minY` set to `Infinity` and `maxX` and `maxY` set to `-Infinity`. 10 | * 11 | * @param points the points to contain 12 | * @param out 13 | */ 14 | export function boxEnclosingPoints(points: Vec[], out = boxAlloc()) { 15 | let minX: number = Infinity; 16 | let minY: number = Infinity; 17 | let maxX: number = -Infinity; 18 | let maxY: number = -Infinity; 19 | 20 | for (const point of points) { 21 | if (point.x < minX) { 22 | minX = point.x; 23 | } 24 | if (point.y < minY) { 25 | minY = point.y; 26 | } 27 | if (point.x > maxX) { 28 | maxX = point.x; 29 | } 30 | if (point.y > maxY) { 31 | maxY = point.y; 32 | } 33 | } 34 | 35 | return boxReset(minX, minY, maxX, maxY, out); 36 | } 37 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-bitwise 2 | 3 | /** 4 | * Sentinel values for the result of {@link boxGetOutCode}. The result of that 5 | * function may be a bitwise union (`|`) of these enum members. 6 | */ 7 | export const enum Out { 8 | /** 9 | * Sentinel value for the result of {@link boxGetOutCode} to represent that 10 | * the point was outside the min-X edge of the box. 11 | */ 12 | MIN_X = 1 << 0, 13 | 14 | /** 15 | * Sentinel value for the result of {@link boxGetOutCode} to represent that 16 | * the point was outside the max-X edge of the box. 17 | */ 18 | MIN_Y = 1 << 1, 19 | 20 | /** 21 | * Sentinel value for the result of {@link boxGetOutCode} to represent that 22 | * the point was outside the min-Y edge of the box. 23 | */ 24 | MAX_X = 1 << 2, 25 | 26 | /** 27 | * Sentinel value for the result of {@link boxGetOutCode} to represent that 28 | * the point was outside the max-Y edge of the box. 29 | */ 30 | MAX_Y = 1 << 3, 31 | } 32 | 33 | export const enum IntervalMode { 34 | OPEN = 0, 35 | CLOSED = 1, 36 | OPEN_ABOVE = 2, 37 | } 38 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineAlloc.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | 3 | /** 4 | * Creates a new Array object in memory to hold Polyline data. 5 | * Its initial length is 0. 6 | * 7 | * Data allocation functions like `polylineAlloc()` are useful to hold results of 8 | * Math2d function calls in inner loops of performance critical workflows. 9 | * 10 | * As with any optimization, you don't need it until you've profiled your 11 | * application! Getting into temp variable management and shared state can 12 | * severely hurt code readability and maintainability, so it's best to avoid 13 | * such optimization if you can. 14 | * 15 | * @example 16 | * // initialize temp memory 17 | * const TMP0 = polylineAlloc(); 18 | * 19 | * ... { 20 | * // Use this temp memory to hold result of `polylineTransformBy()`. 21 | * // This will avoid a heap allocation if the array doesn't have to be resized. 22 | * const result = polylineTransformBy(existingObj.path, existingObj.transform, TMP0); 23 | * } 24 | */ 25 | export function polylineAlloc(): Polyline { 26 | return []; 27 | } 28 | -------------------------------------------------------------------------------- /src/nearestPointResultFunctions/nearestPointResultReset.ts: -------------------------------------------------------------------------------- 1 | import { nearestPointResultAlloc } from "./nearestPointResultAlloc"; 2 | 3 | /** 4 | * Construct a new intersection given `exists`, `x`, `y`, `t0`, and `t1` values. 5 | * 6 | * @param exists whether an intersection was found. If `false`, the other passed values should be `NaN` 7 | * @param x the x-coordinate of the intersection, if an intersection point was found. 8 | * @param y the y-coordinate of the intersection, if an intersection point was found. 9 | * @param t0 the parameterization of the intersection along the first shape's geometry, 10 | * if an intersection point was found. 11 | * @param t1 the parameterization of the intersection along the second shape's geometry, 12 | * if an intersection point was found. 13 | * @param out 14 | */ 15 | export function nearestPointResultReset( 16 | x: number, 17 | y: number, 18 | t: number, 19 | distanceValue: number, 20 | out = nearestPointResultAlloc(), 21 | ) { 22 | out.x = x; 23 | out.y = y; 24 | out.t = t; 25 | out.distanceValue = distanceValue; 26 | return out; 27 | } 28 | -------------------------------------------------------------------------------- /src/intersectionResultFunctions/intersectionResultReset.ts: -------------------------------------------------------------------------------- 1 | import { intersectionResultAlloc } from "./intersectionResultAlloc"; 2 | 3 | /** 4 | * Construct a new intersection given `exists`, `x`, `y`, `t0`, and `t1` values. 5 | * 6 | * @param exists whether an intersection was found. If `false`, the other passed values should be `NaN` 7 | * @param x the x-coordinate of the intersection, if an intersection point was found. 8 | * @param y the y-coordinate of the intersection, if an intersection point was found. 9 | * @param t0 the parameterization of the intersection along the first shape's geometry, 10 | * if an intersection point was found. 11 | * @param t1 the parameterization of the intersection along the second shape's geometry, 12 | * if an intersection point was found. 13 | * @param out 14 | */ 15 | export function intersectionResultReset( 16 | exists: boolean, 17 | x: number, 18 | y: number, 19 | t0: number, 20 | t1: number, 21 | out = intersectionResultAlloc(), 22 | ) { 23 | out.exists = exists; 24 | out.x = x; 25 | out.y = y; 26 | out.t0 = t0; 27 | out.t1 = t1; 28 | return out; 29 | } 30 | -------------------------------------------------------------------------------- /src/boxFunctions/boxAlloc.ts: -------------------------------------------------------------------------------- 1 | import { Box } from "../types"; 2 | 3 | class _Box implements Box { 4 | public minX = NaN; 5 | public minY = NaN; 6 | public maxX = NaN; 7 | public maxY = NaN; 8 | } 9 | 10 | /** 11 | * Creates a new Box object in memory, with all values initialized to `NaN`. 12 | * 13 | * Data allocation functions like `boxAlloc()` are useful to hold results of 14 | * Math2d function calls in inner loops of performance critical workflows. 15 | * 16 | * As with any optimization, you don't need it until you've profiled your 17 | * application! Getting into temp variable management and shared state can 18 | * severely hurt code readability and maintainability, so it's best to avoid 19 | * such optimization if you can. 20 | * 21 | * @example 22 | * // initialize temp memory 23 | * const TMP0 = boxAlloc(); 24 | * 25 | * ... { 26 | * // Use this temp memory to hold result of `polygonGetBounds()`. 27 | * // This avoids a heap allocation. 28 | * const result = polygonGetBounds(existingObj.geometry, TMP0); 29 | * } 30 | */ 31 | export function boxAlloc(): Box { 32 | return new _Box(); 33 | } 34 | -------------------------------------------------------------------------------- /src/rayFunctions/rayAlloc.ts: -------------------------------------------------------------------------------- 1 | import { Ray } from "../types"; 2 | 3 | class _Ray implements Ray { 4 | public x0 = NaN; 5 | public y0 = NaN; 6 | public dirX = NaN; 7 | public dirY = NaN; 8 | } 9 | 10 | /** 11 | * Creates a new Ray object in memory, with all values initialized to `NaN`. 12 | * 13 | * Data allocation functions like `rayAlloc()` are useful to hold results of 14 | * Math2d function calls in inner loops of performance critical workflows. 15 | * 16 | * As with any optimization, you don't need it until you've profiled your 17 | * application! Getting into temp variable management and shared state can 18 | * severely hurt code readability and maintainability, so it's best to avoid 19 | * such optimization if you can. 20 | * 21 | * @example 22 | * // initialize temp memory 23 | * const TMP0 = rayAlloc(); 24 | * 25 | * ... { 26 | * // Use this temp memory to hold result of `rayLookAt()`. 27 | * // This avoids a heap allocation. 28 | * const result = rayLookAt(existingObj.source, existingObj.target, TMP0); 29 | * } 30 | */ 31 | export function rayAlloc(): Ray { 32 | return new _Ray(); 33 | } 34 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentGetPointAtT.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "../types"; 2 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 3 | import { vecReset } from "../vecFunctions/vecReset"; 4 | 5 | /** 6 | * Gets a point along the line segment, parameterized according to linear interpolation 7 | * between its endpoints. 8 | * 9 | * A segment is parameterized according to linear interpolation between its endpoints, 10 | * where _t_ = 0 represents its starting vertex and _t_ = 1 its ending vertex. 11 | * Smooth values of _t_ within that range will move along the segment, so for example 12 | * _t_ = 0.5 is its midpoint. 13 | * 14 | * This function does allow for _t_ values outside the range `[0, 1]`, which will return 15 | * points in the same direction as the segment but outside in either direction. 16 | * 17 | * @param segment the segment to inspect 18 | * @param t linear ratio along the segment to return 19 | * @param out 20 | */ 21 | export function segmentGetPointAtT(segment: Segment, t: number, out = vecAlloc()) { 22 | return vecReset(segment.x0 * (1 - t) + segment.x1 * t, segment.y0 * (1 - t) + segment.y1 * t, out); 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Alexander Ryan and affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/__test__/pointIntersectionResultFunctions/intersectionResultClone.spec.ts: -------------------------------------------------------------------------------- 1 | import { intersectionResultAlloc } from "../../intersectionResultFunctions/intersectionResultAlloc"; 2 | import { intersectionResultClone } from "../../intersectionResultFunctions/intersectionResultClone"; 3 | import { intersectionResultReset } from "../../intersectionResultFunctions/intersectionResultReset"; 4 | import { expectIntersectionEqualsApprox } from "../helpers"; 5 | 6 | describe("intersectionResultClone", () => { 7 | it("copies components", () => { 8 | expectIntersectionEqualsApprox( 9 | intersectionResultClone(intersectionResultReset(true, 4, 5, 6, 7)), 10 | intersectionResultReset(true, 4, 5, 6, 7), 11 | ); 12 | }); 13 | 14 | it("returns a new intersection if no `out`", () => { 15 | const intersection = intersectionResultReset(true, 4, 5, 6, 7); 16 | expect(intersectionResultClone(intersection)).not.toBe(intersection); 17 | }); 18 | 19 | it("returns `out` if given", () => { 20 | const out = intersectionResultAlloc(); 21 | expect(intersectionResultClone(intersectionResultReset(true, 4, 5, 6, 7), out)).toBe(out); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetTAtDistance.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | import { polylineGetNumSegments } from "./polylineGetNumSegments"; 3 | import { polylineGetNumVertices } from "./polylineGetNumVertices"; 4 | import { polylineGetSegmentLength } from "./polylineGetSegmentLength"; 5 | 6 | /** 7 | * Computes the parametric value _t_ along the polyline corresponding to a distance _d_. 8 | * 9 | * This function can be used along with {@link polylineGetPointAt} to find the actual 10 | * (x, y) point corresponding to a distance traveled _d_ along the polyline. 11 | * 12 | * @param poly the polyline to inspect 13 | * @param d distance along the polyline to travel 14 | */ 15 | export function polylineGetTAtDistance(poly: Polyline, d: number) { 16 | if (d < 0) { 17 | return 0; 18 | } 19 | 20 | const numSegments = polylineGetNumSegments(poly); 21 | for (let i = 0; i < numSegments; i++) { 22 | const segmentLength = polylineGetSegmentLength(poly, i); 23 | if (d <= segmentLength) { 24 | return i + d / segmentLength; 25 | } 26 | 27 | d -= segmentLength; 28 | } 29 | 30 | return polylineGetNumVertices(poly); 31 | } 32 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetPointAtT.ts: -------------------------------------------------------------------------------- 1 | import { _lerp } from "../internal/_lerp"; 2 | import { Polyline } from "../types"; 3 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 4 | import { vecReset } from "../vecFunctions/vecReset"; 5 | import { polylineGetNumSegments } from "./polylineGetNumSegments"; 6 | 7 | /** 8 | * Computes a point along the polyline, parameterized according to linear interpolation between 9 | * adjacent vertices. 10 | * 11 | * @param poly the polyline to compute a point on 12 | * @param t the parameter variable at which a point should be calculated 13 | * @param out 14 | */ 15 | export function polylineGetPointAtT(poly: Polyline, t: number, out = vecAlloc()) { 16 | const maxT = polylineGetNumSegments(poly); 17 | if (t < 0 || t > maxT) { 18 | return vecReset(NaN, NaN, out); 19 | } 20 | 21 | if (t === maxT) { 22 | return vecReset(poly[poly.length - 2], poly[poly.length - 1], out); 23 | } 24 | 25 | const floorT = Math.floor(t); 26 | return vecReset( 27 | _lerp(poly[2 * floorT], poly[2 * floorT + 2], t - floorT), 28 | _lerp(poly[2 * floorT + 1], poly[2 * floorT + 3], t - floorT), 29 | out, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineTransformBy.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d, Polyline } from "../types"; 2 | import { vecAlloc } from "../vecFunctions/vecAlloc"; 3 | import { vecReset } from "../vecFunctions/vecReset"; 4 | import { vecTransformBy } from "../vecFunctions/vecTransformBy"; 5 | import { polylineAlloc } from "./polylineAlloc"; 6 | 7 | 8 | /** 9 | * Transforms a polyline by an affine matrix. 10 | * 11 | * Simply transforms each of the polyline's vertices by the given matrix. 12 | * Affine transformations and their specifics within Math2d are described in more detail 13 | * in the {@link vecTransformBy} docs. 14 | * 15 | * @param poly polyline to transform 16 | * @param mat affine transform to apply 17 | * @param out 18 | */ 19 | export function polylineTransformBy(poly: Polyline, mat: Mat2d, out = polylineAlloc()) { 20 | const tmp0 = vecAlloc(); 21 | if (out.length !== poly.length) { 22 | out.length = poly.length; 23 | } 24 | 25 | for (let i = 0; i < poly.length; i += 2) { 26 | const v0 = vecReset(poly[i], poly[i + 1], tmp0); 27 | vecTransformBy(v0, mat, v0); 28 | out[i] = v0.x; 29 | out[i + 1] = v0.y; 30 | } 31 | 32 | return out; 33 | } 34 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dAlloc.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d } from "../types"; 2 | 3 | class _Mat2d implements Mat2d { 4 | public a = NaN; 5 | public b = NaN; 6 | public c = NaN; 7 | public d = NaN; 8 | public tx = NaN; 9 | public ty = NaN; 10 | } 11 | 12 | /** 13 | * Creates a new mat2d object in memory, with all values initialized to `NaN`. 14 | * 15 | * Data allocation functions like `mat2dAlloc()` are useful to hold results of 16 | * Math2d function calls in inner loops of performance critical workflows. 17 | * 18 | * As with any optimization, you don't need it until you've profiled your 19 | * application! Getting into temp variable management and shared state can 20 | * severely hurt code readability and maintainability, so it's best to avoid 21 | * such optimization if you can. 22 | * 23 | * @example 24 | * // initialize temp memory 25 | * const TMP0 = mat2dAlloc(); 26 | * 27 | * ... { 28 | * // Use this temp memory to hold result of `mat2dInvert()`. 29 | * // This avoids a heap allocation. 30 | * const result = mat2dInvert(existingObj.transform, TMP0); 31 | * } 32 | */ 33 | export function mat2dAlloc(): Mat2d { 34 | return new _Mat2d(); 35 | } 36 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentAlloc.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "../types"; 2 | 3 | class _Segment implements Segment { 4 | public x0 = NaN; 5 | public y0 = NaN; 6 | public x1 = NaN; 7 | public y1 = NaN; 8 | } 9 | 10 | /** 11 | * Creates a new Segment object in memory, with all values initialized to `NaN`. 12 | * 13 | * Data allocation functions like `segmentAlloc()` are useful to hold results of 14 | * Math2d function calls in inner loops of performance critical workflows. 15 | * 16 | * As with any optimization, you don't need it until you've profiled your 17 | * application! Getting into temp variable management and shared state can 18 | * severely hurt code readability and maintainability, so it's best to avoid 19 | * such optimization if you can. 20 | * 21 | * @example 22 | * // initialize temp memory 23 | * const TMP0 = segmentAlloc(); 24 | * 25 | * ... { 26 | * // Use this temp memory to hold result of `polygonGetSideSegment()`. 27 | * // This avoids a heap allocation. 28 | * const result = polygonGetSideSegment(existingObj.geometry, 0, TMP0); 29 | * } 30 | */ 31 | export function segmentAlloc(): Segment { 32 | return new _Segment(); 33 | } 34 | -------------------------------------------------------------------------------- /src/boxFunctions/boxGetOutCode.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-bitwise 2 | import { Out } from "../const"; 3 | import { Box, Vec } from "../types"; 4 | 5 | /** 6 | * Determines where the specified point lies in relation to the given box. 7 | * 8 | * The returned value is a binary OR of the possible values 9 | * {@link Out.MIN_X}, {@link Out.MAX_X}, {@link Out.MIN_Y}, and {@link Out.MAX_Y} 10 | * indicating, for each side, whether the point lies beyond that edge. If the point 11 | * is inside the box, this function returns the value 0. 12 | * 13 | * @param box 14 | * @param point 15 | * @example 16 | * const myBox = boxReset(-2, -2, 2, 2); 17 | * const outCode1 = boxGetOutCode(myBox, vecReset(-4, 4)); // returns Out.MIN_X | Out.MAX_Y 18 | * const outCode2 = boxGetOutCode(myBox, vec2Origin()); // returns 0 19 | */ 20 | export function boxGetOutCode(box: Box, point: Vec) { 21 | let out = 0; 22 | if (point.x < box.minX) { 23 | out |= Out.MIN_X; 24 | } else if (point.x > box.maxX) { 25 | out |= Out.MAX_X; 26 | } 27 | 28 | if (point.y < box.minY) { 29 | out |= Out.MIN_Y; 30 | } else if (point.y > box.maxY) { 31 | out |= Out.MAX_Y; 32 | } 33 | 34 | return out; 35 | } 36 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxUnion.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxUnion } from "../../boxFunctions/boxUnion"; 2 | import { expectBoxEqualsApprox, _boxValues } from "../helpers"; 3 | 4 | describe("boxUnion", () => { 5 | it.each` 6 | a | b | result 7 | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 1]} 8 | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 2]} | ${[-1, -1, 1, 2]} 9 | ${[-1, -1, 1, 1]} | ${[0, 0, 0, 0]} | ${[-1, -1, 1, 1]} 10 | ${[-1, -1, 1, 1]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${[-1, -1, 1, 1]} 11 | ${[-1, -1, 1, 1]} | ${[3, -1, 5, 1]} | ${[-1, -1, 5, 1]} 12 | ${[-Infinity, -Infinity, Infinity, Infinity]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${[-Infinity, -Infinity, Infinity, Infinity]} 13 | ${[Infinity, Infinity, -Infinity, -Infinity]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${[-0.5, -0.5, 0.5, 0.5]} 14 | `("$a $b => $result", ({ a, b, result }) => { 15 | expectBoxEqualsApprox(boxUnion(_boxValues(a), _boxValues(b)), _boxValues(result)); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/boxFunctions/boxReset.ts: -------------------------------------------------------------------------------- 1 | import { boxAlloc } from "./boxAlloc"; 2 | 3 | /** 4 | * Construct a new box given `minX`, `minY`, `maxX`, and `maxY` bounding values. 5 | * 6 | * @param minX 7 | * min-X boundary of the box, which is typically the _left_ edge 8 | * @param minY 9 | * min-Y boundary of the box, which could be the _top_ OR the _bottom_ edge of the box depending on how your 10 | * rendering and coordinate systems are laid out. 11 | * @param maxX 12 | * max-X boundary of the box, which is typically the _right_ edge 13 | * @param maxY 14 | * min-Y boundary of the box, which could be the _top_ OR the _bottom_ edge of the box depending on how your 15 | * rendering and coordinate systems are laid out. 16 | * @param out 17 | * @example 18 | * // initialize a new box that's [-1, 1]×[-1, 1] 19 | * const myBox = boxReset(-1, -1, 1, 1); 20 | * 21 | * // reset an existing box's values to [4, 8]×[0, 8] 22 | * const myBox2 = boxAlloc(); 23 | * boxReset(4, 0, 8, 8, myBox2); 24 | */ 25 | export function boxReset(minX: number, minY: number, maxX: number, maxY: number, out = boxAlloc()) { 26 | out.minX = minX; 27 | out.minY = minY; 28 | out.maxX = maxX; 29 | out.maxY = maxY; 30 | return out; 31 | } 32 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxIntersection.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxIntersection } from "../../boxFunctions/boxIntersection"; 2 | import { expectBoxEqualsApprox, _boxValues } from "../helpers"; 3 | 4 | describe("boxIntersection", () => { 5 | it.each` 6 | a | b | result 7 | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 1]} 8 | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 2]} | ${[-1, -1, 1, 1]} 9 | ${[-1, -1, 1, 1]} | ${[0, 0, 0, 0]} | ${[0, 0, 0, 0]} 10 | ${[-1, -1, 1, 1]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${[-0.5, -0.5, 0.5, 0.5]} 11 | ${[-1, -1, 1, 1]} | ${[3, -1, 5, 1]} | ${[3, -1, 1, 1]} 12 | ${[-Infinity, -Infinity, Infinity, Infinity]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${[-0.5, -0.5, 0.5, 0.5]} 13 | ${[Infinity, Infinity, -Infinity, -Infinity]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${[Infinity, Infinity, -Infinity, -Infinity]} 14 | `("$a $b => $result", ({ a, b, result }) => { 15 | expectBoxEqualsApprox(boxIntersection(_boxValues(a), _boxValues(b)), _boxValues(result)); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__test__/rayFunctions/rayIntersectPolyline.spec.ts: -------------------------------------------------------------------------------- 1 | import { rayIntersectPolyline } from "../../rayFunctions/rayIntersectPolyline"; 2 | import { expectArrayEqualsApprox, _rayValues } from "../helpers"; 3 | 4 | const SQRT1_2 = Math.SQRT1_2; 5 | describe("rayIntersectPolyline", () => { 6 | it.each` 7 | ray | polyline | t0s 8 | ${[0.5, 0.5, 1, 0]} | ${[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]} | ${[0.5]} 9 | ${[0.5, 0.5, -1, 0]} | ${[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]} | ${[0.5]} 10 | ${[0.5, 0.5, 0, 1]} | ${[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]} | ${[0.5]} 11 | ${[0.5, 0.5, 0, -1]} | ${[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]} | ${[0.5]} 12 | ${[0.5, 0.5, SQRT1_2, SQRT1_2]} | ${[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]} | ${[SQRT1_2]} 13 | ${[0.5, 0.5, 0.6, 0.8]} | ${[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]} | ${[0.625]} 14 | ${[0.5, 0.5, -0.8, 0.6]} | ${[0, 0, 1, 0, 1, 1, 0, 1, 0, 0]} | ${[0.625]} 15 | `("$ray $polyline => $t0s", ({ ray, polyline, t0s }) => { 16 | const actual = rayIntersectPolyline(_rayValues(ray), polyline); 17 | expectArrayEqualsApprox( 18 | actual.map((pir) => pir.t0), 19 | t0s, 20 | ); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dRotate.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d } from "../types"; 2 | import { mat2dAlloc } from "./mat2dAlloc"; 3 | import { mat2dReset } from "./mat2dReset"; 4 | 5 | /** 6 | * Applies a rotation in radians to the given matrix, returning the result. 7 | * 8 | * This is equivalent to _left_-multiplying the matrix by a rotation transform; that is, 9 | * the result of this function is equivalent to a transform that first applies the matrix _mat_ 10 | * and then rotates according to the angle _theta_. 11 | * 12 | * The rotation is from the `x+` to `y+` direction, which is _counter-clockwise_ in the 13 | * standard Cartesian coordinate system or _clockwise_ in most standard graphics 14 | * coordinate systems, as in Canvas and the DOM. 15 | * 16 | * @param mat the matrix to transform 17 | * @param theta rotation angle in radians to apply on top of the given matrix 18 | * @param out 19 | */ 20 | export function mat2dRotate(mat: Mat2d, theta: number, out = mat2dAlloc()) { 21 | const cos = Math.cos(theta); 22 | const sin = Math.sin(theta); 23 | return mat2dReset( 24 | cos * mat.a - sin * mat.c, 25 | cos * mat.b - sin * mat.d, 26 | sin * mat.a + cos * mat.c, 27 | sin * mat.b + cos * mat.d, 28 | mat.tx, 29 | mat.ty, 30 | out, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxContainsPoint.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxContainsPoint } from "../../boxFunctions/boxContainsPoint"; 2 | import { IntervalMode } from "../../const"; 3 | import { _boxValues, _vecValues } from "../helpers"; 4 | 5 | describe("boxContainsPoint", () => { 6 | it.each` 7 | box | point | imode | result 8 | ${[-1, -1, 1, 1]} | ${[0, 0]} | ${IntervalMode.CLOSED} | ${true} 9 | ${[-1, -1, 1, 1]} | ${[1, 0]} | ${IntervalMode.CLOSED} | ${true} 10 | ${[-1, -1, 1, 1]} | ${[0, -2]} | ${IntervalMode.CLOSED} | ${false} 11 | ${[-1, -1, 1, 1]} | ${[-2, -3]} | ${IntervalMode.CLOSED} | ${false} 12 | ${[-1, -1, 1, 1]} | ${[NaN, NaN]} | ${IntervalMode.CLOSED} | ${false} 13 | ${[-Infinity, -Infinity, Infinity, Infinity]} | ${[1, 1]} | ${IntervalMode.CLOSED} | ${true} 14 | ${[Infinity, Infinity, -Infinity, -Infinity]} | ${[1, 1]} | ${IntervalMode.CLOSED} | ${false} 15 | `("$box $point $imode => $result", ({ box, point, imode, result }) => { 16 | expect(boxContainsPoint(_boxValues(box), _vecValues(point), imode)).toBe(result); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineGetDistanceAtT.ts: -------------------------------------------------------------------------------- 1 | import { Polyline } from "../types"; 2 | import { polylineGetNumSegments } from "./polylineGetNumSegments"; 3 | import { polylineGetSegmentLength } from "./polylineGetSegmentLength"; 4 | 5 | /** 6 | * Computes the Euclidean distance traveled along the polyline's geometry to get to 7 | * the parametric point at _t_. 8 | * 9 | * Values of _t_ are interpreted according to the {@link IPolyline} parameterization: 10 | * _t_ should be between 0 and the polyline's vertex count minus 1, and smooth values of _t_ therein 11 | * signify linear interpolation between adjacent vertices of the polyline's geometry. 12 | * 13 | * This function is the inverse of {@link polylineGetTAtDistance}. 14 | * 15 | * @param poly the polyline to inspect 16 | * @param t the parametric value along the polyline's geometry to measure distance to 17 | */ 18 | export function polylineGetDistanceAtT(polyline: Polyline, t: number) { 19 | const numSegments = polylineGetNumSegments(polyline); 20 | let traveled = 0; 21 | for (let i = 0; i < numSegments; i++) { 22 | const segmentLength = polylineGetSegmentLength(polyline, i); 23 | if (t <= 1) { 24 | return traveled + t * segmentLength; 25 | } else { 26 | traveled += segmentLength; 27 | t -= 1; 28 | } 29 | } 30 | 31 | return traveled; 32 | } 33 | -------------------------------------------------------------------------------- /src/boxFunctions/boxTransformBy.ts: -------------------------------------------------------------------------------- 1 | import { _arrayReset } from "../internal/_arrayReset"; 2 | import { polylineGetBounds } from "../polylineFunctions/polylineGetBounds"; 3 | import { polylineTransformBy } from "../polylineFunctions/polylineTransformBy"; 4 | import { Box, Mat2d, Polyline } from "../types"; 5 | import { boxAlloc } from "./boxAlloc"; 6 | 7 | /** 8 | * Compute the bounds of the image of this box after applying a 2D affine transformation. 9 | * 10 | * This function calculates the minimum bounds that will contain the image after applying a transformation 11 | * to the given bounding box. Note that the actual geometric result of transforming a given 12 | * box may not be an axis-aligned box! For example, spinning the [-1, 1]×[-1, 1] box 45° clockwise yields a diamond 13 | * connecting the four points (√2, 0), (0, √2), (-√2, 0), (0, -√2). The bounding box of _that diamond_ 14 | * is [-√2, √2]×[-√2, √2]. 15 | * 16 | * @param box the box to transform 17 | * @param mat the affine transformation to apply to the box 18 | * @param out 19 | */ 20 | export function boxTransformBy(box: Box, mat: Mat2d, out = boxAlloc()) { 21 | const tmp = [] as Polyline; 22 | _arrayReset(tmp, box.minX, box.minY, box.minX, box.maxY, box.maxX, box.maxY, box.maxX, box.minY); 23 | polylineTransformBy(tmp, mat, tmp); 24 | return polylineGetBounds(tmp, out); 25 | } 26 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxIntersectsBox.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxIntersectsBox } from "../../boxFunctions/boxIntersectsBox"; 2 | import { IntervalMode } from "../../const"; 3 | import { _boxValues } from "../helpers"; 4 | 5 | describe("boxIntersectsBox", () => { 6 | it.each` 7 | a | b | imode | result 8 | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 1]} | ${IntervalMode.CLOSED} | ${true} 9 | ${[-1, -1, 1, 1]} | ${[-1, -1, 1, 2]} | ${IntervalMode.CLOSED} | ${true} 10 | ${[-1, -1, 1, 1]} | ${[0, 0, 0, 0]} | ${IntervalMode.CLOSED} | ${true} 11 | ${[-1, -1, 1, 1]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${IntervalMode.CLOSED} | ${true} 12 | ${[-1, -1, 1, 1]} | ${[3, -1, 5, 1]} | ${IntervalMode.CLOSED} | ${false} 13 | ${[-Infinity, -Infinity, Infinity, Infinity]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${IntervalMode.CLOSED} | ${true} 14 | ${[Infinity, Infinity, -Infinity, -Infinity]} | ${[-0.5, -0.5, 0.5, 0.5]} | ${IntervalMode.CLOSED} | ${false} 15 | `("$a $b $imode => $result", ({ a, b, imode, result }) => { 16 | expect(boxIntersectsBox(_boxValues(a), _boxValues(b), imode)).toBe(result); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineIntersectRay.ts: -------------------------------------------------------------------------------- 1 | import { _polylineIntersectHelper } from "../internal/_polylineIntersectHelper"; 2 | import { segmentIntersectRay } from "../segmentFunctions/segmentIntersectRay"; 3 | import { Polyline, Ray } from "../types"; 4 | 5 | /** 6 | * Computes all locations at which a polyline crosses a given ray. 7 | * 8 | * For each returned intersection, the intersection's _t0_ describes where the point fell on the polyline's geometry 9 | * according to the {@link IPolyline} parameterization: 10 | * values of _t0_ are between 0 and the polyline's vertex count minus 1, and smooth values of _t0_ therein 11 | * signify linear interpolation between adjacent vertices along the polyline's geometry. 12 | * 13 | * The returned points will be sorted by _t0_ increasing, i.e. they will be sorted according to the 14 | * order in which one would visit those locations if one were to travel from the polyline's start to its end 15 | * along its segments. 16 | * 17 | * Almost equivalent to {@link rayIntersectPolyline}, except the _t0_ and _t1_ values are reversed 18 | * and the returned intersections are sorted according to the polyline's geometry. 19 | * 20 | * @param poly 21 | * @param ray 22 | */ 23 | export function polylineIntersectRay(poly: Polyline, ray: Ray) { 24 | return _polylineIntersectHelper(poly, ray, segmentIntersectRay); 25 | } 26 | -------------------------------------------------------------------------------- /src/rayFunctions/rayTransformBy.ts: -------------------------------------------------------------------------------- 1 | import { _lookAt } from "../internal/_lookAt"; 2 | import { Mat2d, Ray } from "../types"; 3 | import { vecReset } from "../vecFunctions/vecReset"; 4 | import { vecTransformBy } from "../vecFunctions/vecTransformBy"; 5 | import { rayAlloc } from "./rayAlloc"; 6 | 7 | /** 8 | * Transforms a ray by an affine matrix. 9 | * 10 | * This function computes the result of applying a transform to the ray's geometry. 11 | * The resulting initial point will be the result of applying the given transform to the original 12 | * initial point, and its direction will be the result of applying any rotations or shears from 13 | * the matrix. The resulting direction vector will be correctly normalized when applicable. 14 | * 15 | * Affine transformations and their specifics within Math2d are described in more detail 16 | * in the {@link vecTransformBy} docs. 17 | * 18 | * Synonymous to {@link lineTransformBy}. 19 | * 20 | * @param ray the ray to transform 21 | * @param mat the affine transform to apply 22 | * @param out 23 | */ 24 | export function rayTransformBy(ray: Ray, mat: Mat2d, out = rayAlloc()) { 25 | const p0 = vecReset(ray.x0, ray.y0); 26 | vecTransformBy(p0, mat, p0); 27 | const p1 = vecReset(ray.x0 + ray.dirX, ray.y0 + ray.dirY); 28 | vecTransformBy(p1, mat, p1); 29 | return _lookAt(p0.x, p0.y, p1.x, p1.y, out); 30 | } 31 | -------------------------------------------------------------------------------- /src/rayFunctions/rayIntersectPolyline.ts: -------------------------------------------------------------------------------- 1 | import { _polylineIntersectHelper } from "../internal/_polylineIntersectHelper"; 2 | import { _swapAndReorderIntersections } from "../internal/_swapAndReorderIntersections"; 3 | import { segmentIntersectRay } from "../segmentFunctions/segmentIntersectRay"; 4 | import { Polyline, Ray } from "../types"; 5 | 6 | /** 7 | * Computes all locations at which a ray crosses a given polyline. 8 | * 9 | * For each returned intersection, the intersection's _t0_ describes where the point fell on the ray's geometry 10 | * according to {@link IRay} parameterization: 11 | * _t0_ ≥ 0, corresponds to travel of distance _t0_ along the ray's direction vector. (See {@link rayGetPointAt}.) 12 | * 13 | * The returned points will be sorted by _t0_ increasing, i.e. they will be sorted according to the 14 | * order in which one would hit the intersections if one were to start from the ray's initial point and 15 | * shoot along its direction vector. 16 | * 17 | * Almost equivalent to {@link polylineIntersectRay}, except the _t0_ and _t1_ values are reversed 18 | * and the returned intersections are sorted according to the ray's geometry. 19 | * 20 | * @param poly 21 | * @param ray 22 | */ 23 | export function rayIntersectPolyline(ray: Ray, poly: Polyline) { 24 | return _swapAndReorderIntersections(_polylineIntersectHelper(poly, ray, segmentIntersectRay)); 25 | } 26 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineIntersectSegment.ts: -------------------------------------------------------------------------------- 1 | import { _polylineIntersectHelper } from "../internal/_polylineIntersectHelper"; 2 | import { segmentIntersectSegment } from "../segmentFunctions/segmentIntersectSegment"; 3 | import { Polyline, Segment } from "../types"; 4 | 5 | /** 6 | * Computes all locations at which a polyline crosses a given line segment. 7 | * 8 | * For each returned intersection, the intersection's _t0_ describes where the point fell on the polyline's geometry 9 | * according to the {@link IPolyline} parameterization: 10 | * values of _t0_ are between 0 and the polyline's vertex count minus 1, and smooth values of _t0_ therein 11 | * signify linear interpolation between adjacent vertices along the polyline's geometry. 12 | * 13 | * The returned points will be sorted by _t0_ increasing, i.e. they will be sorted according to the 14 | * order in which one would visit those locations if one were to travel from the polyline's start to its end 15 | * along its segments. 16 | * 17 | * Almost equivalent to {@link segmentIntersectPolyline}, except the _t0_ and _t1_ values are reversed 18 | * and the returned intersections are sorted according to the polyline's geometry. 19 | * 20 | * @param poly 21 | * @param segment 22 | */ 23 | export function polylineIntersectSegment(poly: Polyline, segment: Segment) { 24 | return _polylineIntersectHelper(poly, segment, segmentIntersectSegment); 25 | } 26 | -------------------------------------------------------------------------------- /src/internal/_polylineIntersectHelper.ts: -------------------------------------------------------------------------------- 1 | import { intersectionResultAlloc } from "../intersectionResultFunctions/intersectionResultAlloc"; 2 | import { intersectionResultReset } from "../intersectionResultFunctions/intersectionResultReset"; 3 | import { polylineGetNumSegments } from "../polylineFunctions/polylineGetNumSegments"; 4 | import { polylineGetSegment } from "../polylineFunctions/polylineGetSegment"; 5 | import { segmentAlloc } from "../segmentFunctions/segmentAlloc"; 6 | import { IntersectionResult, Polyline, Segment } from "../types"; 7 | 8 | 9 | export function _polylineIntersectHelper( 10 | poly: Polyline, 11 | value: T, 12 | doIntersectSegment: (segment: Segment, value: T, out: IntersectionResult) => IntersectionResult, 13 | ) { 14 | const tmp0 = segmentAlloc(); 15 | const tmp1 = intersectionResultAlloc(); 16 | 17 | const allIntersections: IntersectionResult[] = []; 18 | const numSegments = polylineGetNumSegments(poly); 19 | // prevent repeated intersections at the same vertex in successive segments 20 | let lastIntersection = NaN; 21 | 22 | for (let i = 0; i < numSegments; i++) { 23 | const segment = polylineGetSegment(poly, i, tmp0); 24 | const out = doIntersectSegment(segment, value, tmp1); 25 | if (out.exists && lastIntersection !== out.t1) { 26 | lastIntersection = out.t1; 27 | allIntersections.push(intersectionResultReset(true, out.x, out.y, i + out.t0, out.t1)); 28 | } 29 | } 30 | 31 | return allIntersections; 32 | } 33 | -------------------------------------------------------------------------------- /src/__test__/boxFunctions/boxEnclosingPoints.spec.ts: -------------------------------------------------------------------------------- 1 | import { boxAlloc } from "../../boxFunctions/boxAlloc"; 2 | import { boxEnclosingPoints } from "../../boxFunctions/boxEnclosingPoints"; 3 | import { boxReset } from "../../boxFunctions/boxReset"; 4 | import { vecReset } from "../../vecFunctions/vecReset"; 5 | import { expectBoxEqualsApprox, _boxValues, _vecValues } from "../helpers"; 6 | 7 | describe("boxEnclosingPoints", () => { 8 | it.each` 9 | points | result 10 | ${[]} | ${[Infinity, Infinity, -Infinity, -Infinity]} 11 | ${[[NaN, NaN]]} | ${[Infinity, Infinity, -Infinity, -Infinity]} 12 | ${[[1, 2]]} | ${[1, 2, 1, 2]} 13 | ${[[1, 2], [-1, -2]]} | ${[-1, -2, 1, 2]} 14 | ${[[1, 2], [-1, -2], [2, -1]]} | ${[-1, -2, 2, 2]} 15 | ${[[1, 2], [-1, -2], [2, -1], [-2, 1]]} | ${[-2, -2, 2, 2]} 16 | ${[[NaN, NaN], [-1, -2], [2, -1], [-2, 1]]} | ${[-2, -2, 2, 1]} 17 | ${[[-3, NaN], [-1, -2], [2, -1], [-2, 1]]} | ${[-3, -2, 2, 1]} 18 | `("$points => $result", ({ points, result }) => { 19 | expectBoxEqualsApprox(boxEnclosingPoints(points.map(_vecValues)), _boxValues(result)); 20 | }); 21 | 22 | it("updates an `out` box if provided", () => { 23 | const out = boxAlloc(); 24 | boxEnclosingPoints([vecReset(1, 2)], out); 25 | expectBoxEqualsApprox(out, boxReset(1, 2, 1, 2)); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/mat2dFunctions/mat2dMulMat2d.ts: -------------------------------------------------------------------------------- 1 | import { Mat2d } from "../types"; 2 | import { mat2dAlloc } from "./mat2dAlloc"; 3 | import { mat2dReset } from "./mat2dReset"; 4 | 5 | /** 6 | * Computes the result of affine matrix multiplication _m1_ × _m2_. 7 | * 8 | * The resulting matrix is equivalent to a transform that first applies _m2_ and then applies 9 | * _m1_; that is, `(m1×m2)v = m1(m2×v)`. 10 | * 11 | * Affine matrix multiplication is defined by 12 | * 13 | * ``` 14 | * ⎡m1.a m1.c m1.tx⎤ ⎡m2.a m2.c m2.tx⎤ ⎡r.a r.c r.tx⎤ 15 | * ⎢m1.b m1.d m1.ty⎥ ⎢m2.b m2.d m2.ty⎥ = ⎢r.b r.d r.ty⎥ 16 | * ⎣ 0 0 1⎦ ⎣ 0 0 1⎦ ⎣ 0 0 1⎦ 17 | * ``` 18 | * 19 | * where: 20 | * - `r.a = m1.a * m2.a + m1.c * m2.b` 21 | * - `r.b = m1.b * m2.a + m1.d * m2.b` 22 | * - `r.c = m1.a * m2.c + m1.c * m2.d` 23 | * - `r.d = m1.b * m2.c + m1.d * m2.d` 24 | * - `r.tx = m1.a * m2.tx + m1.c * m2.ty + m1.tx` 25 | * - `r.ty = m1.b * m2.e + m1.c * m2.ty + m1.ty` 26 | * 27 | * @param m1 the first matrix to multiply 28 | * @param m2 the second matrix to multiply 29 | * @param out 30 | */ 31 | export function mat2dMulMat2d(m1: Mat2d, m2: Mat2d, out = mat2dAlloc()) { 32 | const a = m1.a * m2.a + m1.c * m2.b; 33 | const b = m1.b * m2.a + m1.d * m2.b; 34 | const c = m1.a * m2.c + m1.c * m2.d; 35 | const d = m1.b * m2.c + m1.d * m2.d; 36 | const tx = m1.a * m2.tx + m1.c * m2.ty + m1.tx; 37 | const ty = m1.b * m2.tx + m1.c * m2.ty + m1.ty; 38 | return mat2dReset(a, b, c, d, tx, ty, out); 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "math2d", 3 | "version": "4.0.1", 4 | "module": "./esm/index.js", 5 | "main": "./commonjs/index.js", 6 | "types": "./esm/index.d.ts", 7 | "scripts": { 8 | "build": "yarn build:commonjs && yarn build:esm", 9 | "build:commonjs": "rollup -c rollup.commonjs.js", 10 | "build:esm": "rollup -c rollup.esm.js", 11 | "check": "tsc --noEmit", 12 | "clean": "rm -rf commonjs esm", 13 | "docs": "scripts/generate-typedoc-data.sh", 14 | "lint": "tslint --project tsconfig.json", 15 | "prepack": "yarn clean && yarn build", 16 | "test": "jest" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.12.10", 20 | "@types/jest": "^26.0.19", 21 | "@wessberg/rollup-plugin-ts": "^1.3.8", 22 | "jest": "^26.6.3", 23 | "prettier": "^2.2.1", 24 | "rollup": "^2.35.1", 25 | "ts-jest": "^26.4.4", 26 | "tslint": "^6.1.3", 27 | "tslint-config-prettier": "^1.18.0", 28 | "typescript": "~4.1.3", 29 | "typescript-tslint-plugin": "1.0.1" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/crazytoucan/math2d.git" 34 | }, 35 | "keywords": [ 36 | "2d", 37 | "bounding", 38 | "box", 39 | "geometry", 40 | "linear algebra", 41 | "math", 42 | "matrix", 43 | "polygon", 44 | "polyline", 45 | "vector" 46 | ], 47 | "files": [ 48 | "esm", 49 | "commonjs" 50 | ], 51 | "homepage": "https://crazytoucan.github.io/math2d/", 52 | "bugs": "https://github.com/crazytoucan/math2d/issues", 53 | "license": "MIT" 54 | } 55 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentIntersectPolyline.ts: -------------------------------------------------------------------------------- 1 | import { _polylineIntersectHelper } from "../internal/_polylineIntersectHelper"; 2 | import { _swapAndReorderIntersections } from "../internal/_swapAndReorderIntersections"; 3 | import { Polyline, Segment } from "../types"; 4 | import { segmentIntersectSegment } from "./segmentIntersectSegment"; 5 | 6 | /** 7 | * Computes all locations at which a line segment meets a given polyline. 8 | * 9 | * For each returned intersection, the intersection's _t0_ describes where the point fell on the segment's geometry 10 | * according to {@link ISegment} parameterization: 11 | * linear interpolation between its endpoints where _t0_ = 0 represents its starting vertex 12 | * and _t0_ = 1 its ending vertex. 13 | * Smooth values of _t_ within that range will move along the segment, so for example 14 | * _t0_ = 0.5 is its midpoint. 15 | * 16 | * The returned points will be sorted by _t0_ increasing, i.e. they will be sorted according to the 17 | * order in which one would visit those locations if one were to travel from the line segment's starting 18 | * vertex to its ending vertex. 19 | * 20 | * Almost equivalent to {@link polylineIntersectSegment}, except the _t0_ and _t1_ values are reversed 21 | * and the returned intersections are sorted according to the segment's geometry. 22 | * 23 | * @param segment the segment to intersect 24 | * @param poly the polyline to find intersections with 25 | */ 26 | export function segmentIntersectPolyline(segment: Segment, poly: Polyline) { 27 | return _swapAndReorderIntersections(_polylineIntersectHelper(poly, segment, segmentIntersectSegment)); 28 | } 29 | -------------------------------------------------------------------------------- /src/rayFunctions/rayWhichSide.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON } from "../internal/const"; 2 | import { _dotPerp } from "../internal/_dotPerp"; 3 | import { Ray, Vec } from "../types"; 4 | 5 | /** 6 | * Computes on which side of the ray (as a _line_) a given point lies. 7 | * 8 | * This method returns one of four possible values: 9 | * 10 | * - `0`, if the point lies exactly along the line 11 | * - `1`, if the point lies in the halfplane defined by its direction vector sweeping in the x+ to y+ direction 12 | * ("left" side, or counter-clockwise in the standard Cartesian coordinate system) 13 | * - `-1`, if the point lies in the halfplane defined by its direction vector sweeping in the x+ to y- direction 14 | * ("right" side, or clockwise in the standard Cartesian coordinate system) 15 | * - `NaN`, if either of the point's coordinates or any of the line's components are NaN. 16 | * 17 | * This measures the perpendicular distance of the point to the line's 18 | * direction vector. A _positive_ return value means that the point lies on the side of the line 19 | * rotated from its direction vector in the x+ to y+ direction ("left" or counter-clockwise in the 20 | * standard Cartesian coordinate system), while a _negative_ return value means it lies in the other half-plane 21 | * ("right" or clockwise in the standard Cartesian coordinate system). 22 | * 23 | * If the point lies on the line, this function returns 0. 24 | * 25 | * @param ray the line to inspect 26 | * @param point the reference point to check for closest distance 27 | */ 28 | export function rayWhichSide(ray: Ray, point: Vec) { 29 | const d = _dotPerp(ray, point); 30 | return Math.abs(d) < EPSILON ? 0 : Math.sign(d); 31 | } 32 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &docker 5 | - image: circleci/openjdk:8-jdk-node-browsers 6 | 7 | - &restore_yarn_cache 8 | restore_cache: 9 | name: Restore node_modules cache 10 | keys: 11 | - v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} 12 | - v1-node-{{ arch }}-{{ .Branch }}- 13 | - v1-node-{{ arch }}- 14 | - &run_yarn 15 | run: 16 | name: Install Packages 17 | command: yarn --frozen-lockfile 18 | 19 | - &attach_workspace 20 | at: '.' 21 | 22 | jobs: 23 | setup: 24 | docker: *docker 25 | steps: 26 | - checkout 27 | - run: 28 | name: Nodejs Version 29 | command: node --version 30 | - *restore_yarn_cache 31 | - *run_yarn 32 | - save_cache: 33 | name: Save node_modules cache 34 | key: v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} 35 | paths: 36 | - ~/.cache/yarn 37 | 38 | lint: 39 | docker: *docker 40 | steps: 41 | - checkout 42 | - *restore_yarn_cache 43 | - *run_yarn 44 | - run: yarn lint 45 | 46 | test: 47 | docker: *docker 48 | steps: 49 | - checkout 50 | - *restore_yarn_cache 51 | - *run_yarn 52 | - run: yarn test --maxWorkers=2 53 | 54 | build: 55 | docker: *docker 56 | steps: 57 | - checkout 58 | - *restore_yarn_cache 59 | - *run_yarn 60 | - run: yarn run check 61 | 62 | workflows: 63 | version: 2 64 | commit: 65 | jobs: 66 | - setup 67 | - lint: 68 | requires: 69 | - setup 70 | - test: 71 | requires: 72 | - setup 73 | - build: 74 | requires: 75 | - setup 76 | -------------------------------------------------------------------------------- /src/rayFunctions/rayNearestDistanceSqToPoint.ts: -------------------------------------------------------------------------------- 1 | import { _dot } from "../internal/_dot"; 2 | import { nearestPointResultAlloc } from "../nearestPointResultFunctions/nearestPointResultAlloc"; 3 | import { nearestPointResultReset } from "../nearestPointResultFunctions/nearestPointResultReset"; 4 | import { Ray, Vec } from "../types"; 5 | import { vecDistanceSq } from "../vecFunctions/vecDistanceSq"; 6 | import { rayGetPointAtT } from "./rayGetPointAtT"; 7 | 8 | /** 9 | * Determines the closest the ray comes to a given reference point 10 | * 11 | * If the point lies on the positive side of the ray (_t_ ≥ 0), this function 12 | * finds the projection of the point onto the ray's geometry. 13 | * If the point lies on the negative side of the ray (_t_ < 0), 14 | * this function returns the ray's initial point. 15 | * 16 | * This function returns the squared distance in the result's `distanceValue`. 17 | * This is preferred over returning the (non-squared) distance because points 18 | * behind the ray need to measure distance to the ray's initial point, which would otherwise involve a square root. 19 | * If you know the point lies in front of the ray, and want to measure the distance while avoiding a square 20 | * root, prefer {@link lineNearestDistanceToPoint}. To determine which side of the ray a point lies on, 21 | * see {@link lineProjectPoint}. 22 | * 23 | * @param ray the ray to inspect 24 | * @param point the reference point to project onto the ray 25 | * @param out 26 | */ 27 | export function rayNearestDistanceSqToPoint(ray: Ray, point: Vec, out = nearestPointResultAlloc()) { 28 | const t = Math.max(0, _dot(ray, point)); 29 | const closest = rayGetPointAtT(ray, t); 30 | const distanceSq = vecDistanceSq(closest, point); 31 | return nearestPointResultReset(closest.x, closest.y, t, distanceSq, out); 32 | } 33 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentIntersectRay.ts: -------------------------------------------------------------------------------- 1 | import { _intersectionSwapTs } from "../internal/_intersectionSwapTs"; 2 | import { intersectionResultAlloc } from "../intersectionResultFunctions/intersectionResultAlloc"; 3 | import { rayIntersectSegment } from "../rayFunctions/rayIntersectSegment"; 4 | import { Ray, Segment } from "../types"; 5 | 6 | /** 7 | * Computes the intersection point between the ray and the segment, if it exists. 8 | * 9 | * Finds the location at which the ray and segment meet. If the ray "misses" the segment, 10 | * this function returns no intersection. For edge cases where the ray completely overlaps the segment, 11 | * or starts within the segment, this function returns the _first_ point that the intersection occurs, 12 | * according to the segment's parameterization. 13 | * 14 | * The returned value is an {@link IIntersectionResult} object which will have have the 15 | * `exists` flag set to `true` iff an intersection was found. It additionally 16 | * has the following fields, if the intersection exists: 17 | * 18 | * - `x` – the x-coordinate of the point of intersection 19 | * - `y` – the y-coordinate of the point of intersection 20 | * - `t0` – where along the segment's geometry the intersection was found, 21 | * according to the segment's parameterization 22 | * - `t1` – where along the ray's geometry the intersection was found, 23 | * according to the ray's parameterization 24 | * 25 | * Almost equivalent to {@link rayIntersectSegment}, except the _t0_ and _t1_ values are reversed. 26 | * 27 | * @param segment the segment to intersect 28 | * @param ray the ray to find intersection with 29 | * @param out 30 | */ 31 | export function segmentIntersectRay(segment: Segment, ray: Ray, out = intersectionResultAlloc()) { 32 | return _intersectionSwapTs(rayIntersectSegment(ray, segment, out)); 33 | } 34 | -------------------------------------------------------------------------------- /src/__test__/rayFunctions/rayIntersectSegment.spec.ts: -------------------------------------------------------------------------------- 1 | import { rayIntersectSegment } from "../../rayFunctions/rayIntersectSegment"; 2 | import { expectEqualsApprox, _rayValues, _segmentValues } from "../helpers"; 3 | 4 | const SQRT1_2 = Math.SQRT1_2; 5 | const SQRT2 = Math.SQRT2; 6 | describe("rayIntersectSegment", () => { 7 | it.each` 8 | ray | segment | t0 9 | ${[0, 0, 1, 0]} | ${[6, -5, 6, 5]} | ${6} 10 | ${[0, 0, -1, 0]} | ${[-6, -5, -6, 5]} | ${6} 11 | ${[0.5, 0.5, 0, 1]} | ${[0, 1, 1, 1]} | ${0.5} 12 | ${[0.5, 0.5, 1, 0]} | ${[1, 0, 1, 1]} | ${0.5} 13 | ${[0, 0, SQRT1_2, SQRT1_2]} | ${[0, 4, 4, 0]} | ${2 * SQRT2} 14 | ${[0, 0, -SQRT1_2, SQRT1_2]} | ${[0, 4, -4, 0]} | ${2 * SQRT2} 15 | ${[0, 0, -SQRT1_2, -SQRT1_2]} | ${[0, -4, -4, 0]} | ${2 * SQRT2} 16 | ${[0, 0, SQRT1_2, -SQRT1_2]} | ${[0, -4, 4, 0]} | ${2 * SQRT2} 17 | ${[5, 10, SQRT1_2, SQRT1_2]} | ${[5, 14, 9, 10]} | ${2 * SQRT2} 18 | ${[5, 10, -SQRT1_2, SQRT1_2]} | ${[5, 14, 1, 10]} | ${2 * SQRT2} 19 | ${[5, 10, -SQRT1_2, -SQRT1_2]} | ${[5, 6, 1, 10]} | ${2 * SQRT2} 20 | ${[5, 10, SQRT1_2, -SQRT1_2]} | ${[5, 6, 9, 10]} | ${2 * SQRT2} 21 | ${[0, 0, 1, 0]} | ${[5, 1, 1, 5]} | ${false} 22 | ${[0, 0, 0, 1]} | ${[5, 1, 1, 5]} | ${false} 23 | ${[0, 0, -1, 0]} | ${[5, 1, 1, 5]} | ${false} 24 | ${[0, 0, 0, -1]} | ${[5, 1, 1, 5]} | ${false} 25 | `("$ray $segment => $t0", ({ ray, segment, t0 }) => { 26 | const actual = rayIntersectSegment(_rayValues(ray), _segmentValues(segment)); 27 | if (t0 === false) { 28 | expect(actual.exists).toBe(false); 29 | expect(actual.t0).toBe(NaN); 30 | expect(actual.t1).toBe(NaN); 31 | expect(actual.x).toBe(NaN); 32 | expect(actual.y).toBe(NaN); 33 | } else { 34 | const _r = _rayValues(ray); 35 | expectEqualsApprox(actual.t0, t0); 36 | // TODO: test `t1` 37 | expectEqualsApprox(actual.x, _r.x0 + t0 * _r.dirX); 38 | expectEqualsApprox(actual.y, _r.y0 + t0 * _r.dirY); 39 | } 40 | }); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineNearestDistanceSqToPoint.ts: -------------------------------------------------------------------------------- 1 | import { nearestPointResultAlloc } from "../nearestPointResultFunctions/nearestPointResultAlloc"; 2 | import { nearestPointResultReset } from "../nearestPointResultFunctions/nearestPointResultReset"; 3 | import { segmentAlloc } from "../segmentFunctions/segmentAlloc"; 4 | import { segmentNearestDistanceSqToPoint } from "../segmentFunctions/segmentNearestDistanceSqToPoint"; 5 | import { Polyline, Vec } from "../types"; 6 | import { polylineGetNumSegments } from "./polylineGetNumSegments"; 7 | import { polylineGetSegment } from "./polylineGetSegment"; 8 | 9 | 10 | /** 11 | * Finds the closest the polyline comes to a given reference point. 12 | * 13 | * The returned value _t_ is defined according to the {@link IPolyline} parameterization: 14 | * values of _t0_ are between 0 and the polyline's vertex count minus 1, and smooth values of T 15 | * signify linear interpolation between the two adjacent vertices along the polyline's geometry. 16 | * 17 | * This function returns the squared euclidean distance in the `distanceValue` field of the result. 18 | * 19 | * @param poly the polyline to inspect 20 | * @param point the point to measure distance to 21 | * @param out 22 | */ 23 | export function polylineNearestDistanceSqToPoint(poly: Polyline, point: Vec, out = nearestPointResultAlloc()) { 24 | const tmp0 = segmentAlloc(); 25 | const tmp1 = nearestPointResultAlloc(); 26 | 27 | const winner = nearestPointResultReset(NaN, NaN, NaN, Infinity, out); 28 | const len = polylineGetNumSegments(poly); 29 | for (let i = 0; i < len; i++) { 30 | const segment = polylineGetSegment(poly, i, tmp0); 31 | const segmentNearest = segmentNearestDistanceSqToPoint(segment, point, tmp1); 32 | if (segmentNearest.distanceValue < winner.distanceValue) { 33 | nearestPointResultReset( 34 | segmentNearest.x, 35 | segmentNearest.y, 36 | i + segmentNearest.t, 37 | segmentNearest.distanceValue, 38 | winner, 39 | ); 40 | } 41 | } 42 | 43 | return winner; 44 | } 45 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentNearestDistanceSqToPoint.ts: -------------------------------------------------------------------------------- 1 | import { _lerp } from "../internal/_lerp"; 2 | import { EPSILON_SQ } from "../internal/const"; 3 | import { nearestPointResultAlloc } from "../nearestPointResultFunctions/nearestPointResultAlloc"; 4 | import { nearestPointResultReset } from "../nearestPointResultFunctions/nearestPointResultReset"; 5 | import { Segment, Vec } from "../types"; 6 | import { vecCross } from "../vecFunctions/vecCross"; 7 | import { vecDot } from "../vecFunctions/vecDot"; 8 | import { vecGetLengthSq } from "../vecFunctions/vecGetLengthSq"; 9 | import { vecReset } from "../vecFunctions/vecReset"; 10 | 11 | /** 12 | * Finds the closest the segment comes to a given reference point. 13 | * 14 | * This function returns the squared euclidean distance in the `distanceValue` field of the result. 15 | * 16 | * @param segment segment to inspect 17 | * @param point point to measure squared distance to 18 | * @param out 19 | */ 20 | export function segmentNearestDistanceSqToPoint(segment: Segment, point: Vec, out = nearestPointResultAlloc()) { 21 | const segVector = vecReset(segment.x1 - segment.x0, segment.y1 - segment.y0); 22 | const pointVector = vecReset(point.x - segment.x0, point.y - segment.y0); 23 | 24 | const segLengthSq = vecGetLengthSq(segVector); 25 | if (segLengthSq < EPSILON_SQ) { 26 | return nearestPointResultReset(segment.x0, segment.y0, 0, vecGetLengthSq(pointVector), out); 27 | } 28 | 29 | const dot = vecDot(pointVector, segVector); 30 | if (dot < 0) { 31 | return nearestPointResultReset(segment.x0, segment.y0, 0, vecGetLengthSq(pointVector), out); 32 | } else if (dot > segLengthSq) { 33 | const dx = point.x - segment.x1; 34 | const dy = point.y - segment.y1; 35 | return nearestPointResultReset(segment.x1, segment.y1, 1, dx * dx + dy * dy, out); 36 | } else { 37 | const perp = vecCross(segVector, pointVector); 38 | const distanceSq = (perp * perp) / segLengthSq; 39 | const t = dot / segLengthSq; 40 | 41 | return nearestPointResultReset( 42 | _lerp(segment.x0, segment.x1, t), 43 | _lerp(segment.y0, segment.y1, t), 44 | t, 45 | distanceSq, 46 | out, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/segmentFunctions/segmentIntersectSegment.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON } from "../internal/const"; 2 | import { _intersectionDNE } from "../internal/_intersectionDNE"; 3 | import { _lookAt } from "../internal/_lookAt"; 4 | import { intersectionResultAlloc } from "../intersectionResultFunctions/intersectionResultAlloc"; 5 | import { rayIntersectSegment } from "../rayFunctions/rayIntersectSegment"; 6 | import { Segment } from "../types"; 7 | import { segmentGetLength } from "./segmentGetLength"; 8 | 9 | /** 10 | * Computes the intersection point between the two line segments, if it exists. 11 | * 12 | * Finds the location at which the two segments meet. If the two segments "miss" each other, 13 | * this function returns no intersection. If the two segments overlap along an entire interval 14 | * (i.e. they are parallel and lie partly on top of each other), this function returns the first 15 | * point they have in common, according to the first segment's parameterization. 16 | * 17 | * The returned value is an {@link IIntersectionResult} object which will have have the 18 | * `exists` flag set to `true` iff an intersection was found. It additionally 19 | * has the following fields, if the intersection exists: 20 | * 21 | * - `x` – the x-coordinate of the point of intersection 22 | * - `y` – the y-coordinate of the point of intersection 23 | * - `t0` – where along the first segment's geometry the intersection was found, 24 | * according to the first segment's parameterization 25 | * - `t1` – where along the second segment's geometry the intersection was found, 26 | * according to the second segment's parameterization 27 | * 28 | * @param a the first segment to intersect 29 | * @param b the second segment to find intersections with 30 | * @param out 31 | */ 32 | export function segmentIntersectSegment(a: Segment, b: Segment, out = intersectionResultAlloc()) { 33 | const aRay = _lookAt(a.x0, a.y0, a.x1, a.y1); 34 | rayIntersectSegment(aRay, b, out); 35 | const segmentLength = segmentGetLength(a); 36 | if (out.exists && out.t0 > -EPSILON && out.t0 < segmentLength + EPSILON) { 37 | out.t0 /= segmentLength; 38 | return out; 39 | } else { 40 | return _intersectionDNE(out); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/rayFunctions/rayIntersectSegment.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON } from "../internal/const"; 2 | import { _intersectionDNE } from "../internal/_intersectionDNE"; 3 | import { _lookAt } from "../internal/_lookAt"; 4 | import { intersectionResultAlloc } from "../intersectionResultFunctions/intersectionResultAlloc"; 5 | import { segmentGetLength } from "../segmentFunctions/segmentGetLength"; 6 | import { Ray, Segment } from "../types"; 7 | import { rayIntersectRay } from "./rayIntersectRay"; 8 | 9 | /** 10 | * Computes the intersection point between the ray and the segment, if it exists. 11 | * 12 | * Finds the location at which the ray and segment meet. If the ray "misses" the segment, 13 | * this function returns no intersection. For edge cases where the ray completely overlaps the segment, 14 | * or starts within the segment, this function returns the _first_ point that the intersection occurs, 15 | * according to the ray's parameterization. 16 | * 17 | * The returned value is an {@link IIntersectionResult} object which will have have the 18 | * `exists` flag set to `true` iff an intersection was found. It additionally 19 | * has the following fields, if the intersection exists: 20 | * 21 | * - `x` – the x-coordinate of the point of intersection 22 | * - `y` – the y-coordinate of the point of intersection 23 | * - `t0` – where along the ray's geometry the intersection was found, 24 | * according to the ray's parameterization 25 | * - `t1` – where along the segment's geometry the intersection was found, 26 | * according to the segment's parameterization 27 | * 28 | * Almost equivalent to {@link segmentIntersectRay}, except the _t0_ and _t1_ values are reversed. 29 | * 30 | * @param ray the ray to intersect 31 | * @param segment the segment to intersect 32 | * @param out 33 | */ 34 | export function rayIntersectSegment(ray: Ray, segment: Segment, out = intersectionResultAlloc()) { 35 | const segmentRay = _lookAt(segment.x0, segment.y0, segment.x1, segment.y1); 36 | rayIntersectRay(ray, segmentRay, out); 37 | if (!out.exists) { 38 | return out; 39 | } 40 | 41 | const segmentLength = segmentGetLength(segment); 42 | if (out.t1 < segmentLength + EPSILON) { 43 | out.t1 /= segmentLength; 44 | return out; 45 | } 46 | 47 | return _intersectionDNE(out); 48 | } 49 | -------------------------------------------------------------------------------- /src/polylineFunctions/polylineTrim.ts: -------------------------------------------------------------------------------- 1 | import { _clamp } from "../internal/_clamp"; 2 | import { Polyline } from "../types"; 3 | import { polylineAlloc } from "./polylineAlloc"; 4 | import { polylineGetNumSegments } from "./polylineGetNumSegments"; 5 | import { polylineGetPointAtT } from "./polylineGetPointAtT"; 6 | 7 | /** 8 | * Trims a polyline to a range of its _t_ parameter. 9 | * 10 | * This function trims geometry from either or both ends of the polyline, kind of like a "substring" 11 | * operation. Note that fractional values are allowed on the bounding _t_ parameters, which may slice off 12 | * only parts of segments to form new vertices. 13 | * 14 | * A polyline's parameterization is defined according to _t_, where 15 | * values of _t_ are between 0 and the polyline's vertex count minus 1, and smooth values of _t_ therein 16 | * signify linear interpolation between adjacent vertices along the polyline's geometry. 17 | * 18 | * Both the `tStart` and `tEnd` parameters will be clamped to their allowed domains [0, _N_-1] for a 19 | * polyline of _N_ vertices. 20 | * 21 | * @param poly the polyline to trim 22 | * @param tStart minimum _t_ value to include. The resulting polyline will have its first vertex at 23 | * the point described by this value on the original polyline. Internally, this value will be clamped 24 | * to the allowed domain `[0, poly.length/2 - 1]`. 25 | * @param tStart minimum _t_ value to include. The resulting polyline will have its last vertex at 26 | * the point described by this value on the original polyline. Internally, this value will be clamped 27 | * to the allowed domain `[0, poly.length/2 - 1]`. 28 | * @param out 29 | */ 30 | export function polylineTrim(poly: Polyline, tStart: number, tEnd: number, out = polylineAlloc()) { 31 | if (poly.length === 0) { 32 | out.length = 0; 33 | return; 34 | } 35 | 36 | const maxAllowedT = polylineGetNumSegments(poly); 37 | tStart = _clamp(tStart, 0, maxAllowedT); 38 | tEnd = _clamp(tEnd, tStart, maxAllowedT); 39 | 40 | const startFloor = Math.floor(tStart); 41 | const endCeil = Math.ceil(tEnd); 42 | 43 | out.length = 2 * (endCeil - startFloor + 1); 44 | let cursor = 0; 45 | const beginPoint = polylineGetPointAtT(poly, tStart); 46 | out[cursor++] = beginPoint.x; 47 | out[cursor++] = beginPoint.y; 48 | 49 | for (let i = startFloor + 1; i < tEnd; i++) { 50 | out[cursor++] = poly[2 * i]; 51 | out[cursor++] = poly[2 * i + 1]; 52 | } 53 | 54 | const endPoint = polylineGetPointAtT(poly, tEnd); 55 | out[cursor++] = endPoint.x; 56 | out[cursor++] = endPoint.y; 57 | return out; 58 | } 59 | -------------------------------------------------------------------------------- /src/rayFunctions/rayIntersectRay.ts: -------------------------------------------------------------------------------- 1 | import { EPSILON } from "../internal/const"; 2 | import { _dot } from "../internal/_dot"; 3 | import { _intersectionDNE } from "../internal/_intersectionDNE"; 4 | import { _rayTransformByOrtho } from "../internal/_rayTransformByOrtho"; 5 | import { mat2dReset } from "../mat2dFunctions/mat2dReset"; 6 | import { intersectionResultAlloc } from "../intersectionResultFunctions/intersectionResultAlloc"; 7 | import { intersectionResultReset } from "../intersectionResultFunctions/intersectionResultReset"; 8 | import { Ray } from "../types"; 9 | import { rayGetPointAtT } from "./rayGetPointAtT"; 10 | 11 | /** 12 | * Computes the intersection point between the two rays, if it exists. 13 | * 14 | * Finds the location at which the two rays meet. If the rays point away or "miss" each other, 15 | * or they are parallel, this function returns no intersection. 16 | * 17 | * The returned value is an {@link IIntersectionResult} object which will have have the 18 | * `exists` flag set to `true` iff an intersection was found. It additionally 19 | * has the following fields, if the intersection exists: 20 | * 21 | * - `x` – the x-coordinate of the point of intersection 22 | * - `y` – the y-coordinate of the point of intersection 23 | * - `t0` – where along the first ray's geometry the intersection was found, 24 | * according to the ray's parameterization 25 | * - `t1` – where along the second ray's geometry the intersection was found, 26 | * according to the ray's parameterization 27 | * 28 | * @param a the first ray to intersect 29 | * @param b the second ray to intersect 30 | * @param out 31 | */ 32 | export function rayIntersectRay(a: Ray, b: Ray, out = intersectionResultAlloc()) { 33 | // Transform ray `b` by the same matrix that maps `a` to the x- basis. 34 | // We then compute the intersection (with the x-axis) in the transformed space. 35 | // This transform is equivalent to "translate by (-a.x0, -a.y0) then rotate by -a.angle". 36 | const transform = mat2dReset( 37 | a.dirX, 38 | -a.dirY, 39 | a.dirY, 40 | a.dirX, 41 | -a.x0 * a.dirX - a.y0 * a.dirY, 42 | a.x0 * a.dirY - a.y0 * a.dirX, 43 | ); 44 | 45 | const localB = _rayTransformByOrtho(b, transform); 46 | const isParallel = Math.abs(localB.dirY) < EPSILON; 47 | 48 | if (isParallel && Math.abs(localB.y0) >= EPSILON) { 49 | return _intersectionDNE(out); 50 | } 51 | 52 | if (isParallel) { 53 | intersectionResultReset(true, a.x0, a.y0, 0, -localB.x0 * localB.dirX, out); 54 | } else { 55 | const t0 = localB.x0 - (localB.dirX / localB.dirY) * localB.y0; 56 | const intersectionPoint = rayGetPointAtT(a, t0); 57 | const t1 = _dot(b, intersectionPoint); 58 | intersectionResultReset(true, intersectionPoint.x, intersectionPoint.y, t0, t1, out); 59 | } 60 | 61 | return out.t0 >= 0 && out.t1 >= 0 ? out : _intersectionDNE(out); 62 | } 63 | -------------------------------------------------------------------------------- /src/__test__/helpers.ts: -------------------------------------------------------------------------------- 1 | import { boxReset } from "../boxFunctions/boxReset"; 2 | import { mat2dReset } from "../mat2dFunctions/mat2dReset"; 3 | import { rayReset } from "../rayFunctions/rayReset"; 4 | import { segmentReset } from "../segmentFunctions/segmentReset"; 5 | import { Box, Mat2d, IntersectionResult, Vec } from "../types"; 6 | import { vecReset } from "../vecFunctions/vecReset"; 7 | 8 | export const TEST_PRECISION_DIGITS = 10; 9 | 10 | export function expectEqualsApprox(actual: number, expected: number) { 11 | if (isNaN(expected)) { 12 | expect(actual).toBeNaN(); 13 | } else if (isFinite(expected)) { 14 | expect(actual).toBeCloseTo(expected, TEST_PRECISION_DIGITS); 15 | } else { 16 | expect(actual).toBe(expected); 17 | } 18 | } 19 | 20 | export function expectBoxEqualsApprox(box: Box, expected: Box) { 21 | expectEqualsApprox(box.minX, expected.minX); 22 | expectEqualsApprox(box.minY, expected.minY); 23 | expectEqualsApprox(box.maxX, expected.maxX); 24 | expectEqualsApprox(box.maxY, expected.maxY); 25 | } 26 | 27 | export function expectVecEqualsApprox(actual: Vec, expected: Vec) { 28 | expectEqualsApprox(actual.x, expected.x); 29 | expectEqualsApprox(actual.y, expected.y); 30 | } 31 | 32 | export function expectIntersectionEqualsApprox(actual: IntersectionResult, expected: IntersectionResult) { 33 | expect(actual.exists).toBe(expected.exists); 34 | expectEqualsApprox(actual.x, expected.x); 35 | expectEqualsApprox(actual.y, expected.y); 36 | expectEqualsApprox(actual.t0, expected.t0); 37 | expectEqualsApprox(actual.t1, expected.t1); 38 | } 39 | 40 | export function expectIntersectionDNE(intersection: IntersectionResult) { 41 | expect(intersection).toEqual({ 42 | exists: false, 43 | x: NaN, 44 | y: NaN, 45 | t0: NaN, 46 | t1: NaN, 47 | }); 48 | } 49 | 50 | export function expectMat2dEqualsApprox(actual: Mat2d, expected: Mat2d) { 51 | expectEqualsApprox(actual.a, expected.a); 52 | expectEqualsApprox(actual.b, expected.b); 53 | expectEqualsApprox(actual.c, expected.c); 54 | expectEqualsApprox(actual.d, expected.d); 55 | expectEqualsApprox(actual.tx, expected.tx); 56 | expectEqualsApprox(actual.ty, expected.ty); 57 | } 58 | 59 | export function expectArrayEqualsApprox(actual: number[], expected: number[]) { 60 | expect(actual.length).toBe(expected.length); 61 | for (let i = 0; i < expected.length; i++) { 62 | expectEqualsApprox(actual[i], expected[i]); 63 | } 64 | } 65 | 66 | export function _mat2dValues(values: number[]) { 67 | expect(values).toHaveLength(6); 68 | const [a, b, c, d, tx, ty] = values; 69 | return mat2dReset(a, b, c, d, tx, ty); 70 | } 71 | 72 | export function _boxValues(values: number[]) { 73 | expect(values).toHaveLength(4); 74 | const [minX, minY, maxX, maxY] = values; 75 | return boxReset(minX, minY, maxX, maxY); 76 | } 77 | 78 | export function _vecValues(values: number[]) { 79 | expect(values).toHaveLength(2); 80 | const [x, y] = values; 81 | return vecReset(x, y); 82 | } 83 | 84 | export function _rayValues(values: number[]) { 85 | expect(values).toHaveLength(4); 86 | const [x0, y0, dirX, dirY] = values; 87 | expectEqualsApprox(dirX * dirX + dirY * dirY, 1); 88 | return rayReset(x0, y0, dirX, dirY); 89 | } 90 | 91 | export function _segmentValues(values: number[]) { 92 | expect(values).toHaveLength(4); 93 | const [x0, y0, x1, y1] = values; 94 | return segmentReset(x0, y0, x1, y1); 95 | } 96 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { boxAlloc } from "./boxFunctions/boxAlloc"; 2 | export { boxClone } from "./boxFunctions/boxClone"; 3 | export { boxContainsBox } from "./boxFunctions/boxContainsBox"; 4 | export { boxContainsPoint } from "./boxFunctions/boxContainsPoint"; 5 | export { boxEncapsulate } from "./boxFunctions/boxEncapsulate"; 6 | export { boxEnclosingPoints } from "./boxFunctions/boxEnclosingPoints"; 7 | export { boxGetOutCode } from "./boxFunctions/boxGetOutCode"; 8 | export { boxGrow } from "./boxFunctions/boxGrow"; 9 | export { boxIntersection } from "./boxFunctions/boxIntersection"; 10 | export { boxIntersectsBox } from "./boxFunctions/boxIntersectsBox"; 11 | export { boxIsEmpty } from "./boxFunctions/boxIsEmpty"; 12 | export { boxReset } from "./boxFunctions/boxReset"; 13 | export { boxScale } from "./boxFunctions/boxScale"; 14 | export { boxTransformBy } from "./boxFunctions/boxTransformBy"; 15 | export { boxTranslate } from "./boxFunctions/boxTranslate"; 16 | export { boxUnion } from "./boxFunctions/boxUnion"; 17 | export { _box } from "./boxFunctions/_box"; 18 | export { IntervalMode, Out } from "./const"; 19 | export { intersectionResultAlloc } from "./intersectionResultFunctions/intersectionResultAlloc"; 20 | export { intersectionResultClone } from "./intersectionResultFunctions/intersectionResultClone"; 21 | export { intersectionResultReset } from "./intersectionResultFunctions/intersectionResultReset"; 22 | export { mat2dAlloc } from "./mat2dFunctions/mat2dAlloc"; 23 | export { mat2dClone } from "./mat2dFunctions/mat2dClone"; 24 | export { mat2dDeterminant } from "./mat2dFunctions/mat2dDeterminant"; 25 | export { mat2dFromRotation } from "./mat2dFunctions/mat2dFromRotation"; 26 | export { mat2dFromTranslation } from "./mat2dFunctions/mat2dFromTranslation"; 27 | export { mat2dIdentity } from "./mat2dFunctions/mat2dIdentity"; 28 | export { mat2dInvert } from "./mat2dFunctions/mat2dInvert"; 29 | export { mat2dIsOrthogonal } from "./mat2dFunctions/mat2dIsOrthogonal"; 30 | export { mat2dIsTranslationOnly } from "./mat2dFunctions/mat2dIsTranslationOnly"; 31 | export { mat2dMulMat2d } from "./mat2dFunctions/mat2dMulMat2d"; 32 | export { mat2dReset } from "./mat2dFunctions/mat2dReset"; 33 | export { mat2dRotate } from "./mat2dFunctions/mat2dRotate"; 34 | export { mat2dScale } from "./mat2dFunctions/mat2dScale"; 35 | export { mat2dTranslate } from "./mat2dFunctions/mat2dTranslate"; 36 | export { _mat2d } from "./mat2dFunctions/_mat2d"; 37 | export { nearestPointResultAlloc } from "./nearestPointResultFunctions/nearestPointResultAlloc"; 38 | export { nearestPointResultClone } from "./nearestPointResultFunctions/nearestPointResultClone"; 39 | export { nearestPointResultReset } from "./nearestPointResultFunctions/nearestPointResultReset"; 40 | export { polylineAlloc } from "./polylineFunctions/polylineAlloc"; 41 | export { polylineClose } from "./polylineFunctions/polylineClose"; 42 | export { polylineContainsPoint } from "./polylineFunctions/polylineContainsPoint"; 43 | export { polylineContainsPointInside } from "./polylineFunctions/polylineContainsPointInside"; 44 | export { polylineGetBounds } from "./polylineFunctions/polylineGetBounds"; 45 | export { polylineGetDistanceAtT } from "./polylineFunctions/polylineGetDistanceAtT"; 46 | export { polylineGetLength } from "./polylineFunctions/polylineGetLength"; 47 | export { polylineGetNumSegments } from "./polylineFunctions/polylineGetNumSegments"; 48 | export { polylineGetNumVertices } from "./polylineFunctions/polylineGetNumVertices"; 49 | export { polylineGetPointAtT } from "./polylineFunctions/polylineGetPointAtT"; 50 | export { polylineGetSegment } from "./polylineFunctions/polylineGetSegment"; 51 | export { polylineGetSegmentLength } from "./polylineFunctions/polylineGetSegmentLength"; 52 | export { polylineGetTAtDistance } from "./polylineFunctions/polylineGetTAtDistance"; 53 | export { polylineGetVertex } from "./polylineFunctions/polylineGetVertex"; 54 | export { polylineIntersectRay } from "./polylineFunctions/polylineIntersectRay"; 55 | export { polylineIntersectSegment } from "./polylineFunctions/polylineIntersectSegment"; 56 | export { polylineIsClosed } from "./polylineFunctions/polylineIsClosed"; 57 | export { polylineNearestDistanceSqToPoint } from "./polylineFunctions/polylineNearestDistanceSqToPoint"; 58 | export { polylineTransformBy } from "./polylineFunctions/polylineTransformBy"; 59 | export { polylineTrim } from "./polylineFunctions/polylineTrim"; 60 | export { rayAlloc } from "./rayFunctions/rayAlloc"; 61 | export { rayClone } from "./rayFunctions/rayClone"; 62 | export { rayContainsPoint } from "./rayFunctions/rayContainsPoint"; 63 | export { rayGetPointAtT } from "./rayFunctions/rayGetPointAtT"; 64 | export { rayIntersectPolyline } from "./rayFunctions/rayIntersectPolyline"; 65 | export { rayIntersectRay } from "./rayFunctions/rayIntersectRay"; 66 | export { rayIntersectSegment } from "./rayFunctions/rayIntersectSegment"; 67 | export { rayLookAt } from "./rayFunctions/rayLookAt"; 68 | export { rayNearestDistanceSqToPoint } from "./rayFunctions/rayNearestDistanceSqToPoint"; 69 | export { rayProjectPoint } from "./rayFunctions/rayProjectPoint"; 70 | export { rayReset } from "./rayFunctions/rayReset"; 71 | export { rayTransformBy } from "./rayFunctions/rayTransformBy"; 72 | export { rayWhichSide } from "./rayFunctions/rayWhichSide"; 73 | export { _ray } from "./rayFunctions/_ray"; 74 | export { segmentAlloc } from "./segmentFunctions/segmentAlloc"; 75 | export { segmentGetEndpoint0 } from "./segmentFunctions/segmentGetEndpoint0"; 76 | export { segmentGetEndpoint1 } from "./segmentFunctions/segmentGetEndpoint1"; 77 | export { segmentGetLength } from "./segmentFunctions/segmentGetLength"; 78 | export { segmentGetLengthSq } from "./segmentFunctions/segmentGetLengthSq"; 79 | export { segmentGetPointAtT } from "./segmentFunctions/segmentGetPointAtT"; 80 | export { segmentIntersectPolyline } from "./segmentFunctions/segmentIntersectPolyline"; 81 | export { segmentIntersectRay } from "./segmentFunctions/segmentIntersectRay"; 82 | export { segmentIntersectSegment } from "./segmentFunctions/segmentIntersectSegment"; 83 | export { segmentNearestDistanceSqToPoint } from "./segmentFunctions/segmentNearestDistanceSqToPoint"; 84 | export { segmentReset } from "./segmentFunctions/segmentReset"; 85 | export { segmentReverse } from "./segmentFunctions/segmentReverse"; 86 | export { _segment } from "./segmentFunctions/_segment"; 87 | export { Box, IntersectionResult, Mat2d, Polyline, Ray, Segment, Vec } from "./types"; 88 | export { vecAdd } from "./vecFunctions/vecAdd"; 89 | export { vecAlloc } from "./vecFunctions/vecAlloc"; 90 | export { vecClone } from "./vecFunctions/vecClone"; 91 | export { vecCross } from "./vecFunctions/vecCross"; 92 | export { vecDistance } from "./vecFunctions/vecDistance"; 93 | export { vecDistanceSq } from "./vecFunctions/vecDistanceSq"; 94 | export { vecDot } from "./vecFunctions/vecDot"; 95 | export { vecGetLength } from "./vecFunctions/vecGetLength"; 96 | export { vecGetLengthSq } from "./vecFunctions/vecGetLengthSq"; 97 | export { vecLerp } from "./vecFunctions/vecLerp"; 98 | export { vecNormalize } from "./vecFunctions/vecNormalize"; 99 | export { vecPerp } from "./vecFunctions/vecPerp"; 100 | export { vecReset } from "./vecFunctions/vecReset"; 101 | export { vecScale } from "./vecFunctions/vecScale"; 102 | export { vecSubtract } from "./vecFunctions/vecSubtract"; 103 | export { vecTransformBy } from "./vecFunctions/vecTransformBy"; 104 | export { _vec } from "./vecFunctions/_vec"; 105 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Data type to hold an (x, y) value. 3 | * 4 | * The vector type in the Math2d package is used interchangeably to represent both points in the plane 5 | * and vectors as in linear algebra. 6 | * 7 | * Math2d chooses to lay out the (x, y) values in a JavaScript object rather than an array 8 | * for ease of use. After carefully benchmarking that difference, it's been confirmed that this 9 | * does not sacrifice performance or memory compactness. 10 | * 11 | */ 12 | export interface Vec { 13 | /** x-coordinate of the vector */ 14 | x: number; 15 | 16 | /** y-coordinate of the vector */ 17 | y: number; 18 | } 19 | 20 | /** 21 | * Data type to represent a 2D line segment. 22 | * 23 | * A segment object holds two (x, y) endpoints, representing the line segment connecting those points 24 | * in the plane. Unlike the similar {@link IRay} type, a segment has finite length. 25 | * 26 | * For a series of connected line segments, see the {@link IPolyline} data type. 27 | * 28 | * Where relevant, a segment is parameterized according to linear interpolation 29 | * between its endpoints, where _t_ = 0 represents its starting vertex and _t_ = 1 its 30 | * ending vertex. 31 | * 32 | */ 33 | export interface Segment { 34 | /** 35 | * x-coordinate of the starting vertex of the segment. 36 | */ 37 | x0: number; 38 | 39 | /** 40 | * y-coordinate of the starting vertex of the segment. 41 | */ 42 | y0: number; 43 | 44 | /** 45 | * x-coordinate of the ending vertex of the segment. 46 | */ 47 | x1: number; 48 | 49 | /** 50 | * y-coordinate of the ending vertex of the segment. 51 | */ 52 | y1: number; 53 | } 54 | 55 | /** 56 | * Data type to represent a ray in 2D space, i.e. an initial point in the plane plus a unit-length direction 57 | * vector coming from that point. 58 | * 59 | * Where relevant, a ray is parameterized according to _t_ ≥ 0 with movement of distance _t_ along its direction vector. 60 | * In this mapping, _t_ = 0 represents the initial point (x0, y0). 61 | * 62 | */ 63 | export interface Ray { 64 | /** 65 | * x-coordinate of the ray's initial point 66 | */ 67 | x0: number; 68 | 69 | /** 70 | * y-coordinate of the ray's initial point 71 | */ 72 | y0: number; 73 | 74 | /** 75 | * x-coordinate of the direction vector of the ray. 76 | * A ray's (dirX, dirY) direction vector is a unit vector emanating from its initial point. 77 | */ 78 | dirX: number; 79 | 80 | /** 81 | * y-coordinate of the direction vector of the ray. 82 | * A ray's (dirX, dirY) direction vector is a unit vector emanating from its initial point. 83 | */ 84 | dirY: number; 85 | } 86 | 87 | /** 88 | * Data type to hold a 2D affine transformation matrix. 89 | * 90 | * Two-dimensional vector graphics operations are usually represented using an affine transform matrix, 91 | * i.e. a linear 2x2 matrix plus a 2D translation. Math2d chooses to lay out this data in a 92 | * flat object structure, as opposed to an array or nested arrays, for ease of use and performance. 93 | * The field names used here match other standards, like the native DOMMatrix 94 | * specification, the Canvas reference APIs, and the open-source glMatrix library. 95 | * 96 | * ``` 97 | * ⎡a c tx⎤ 98 | * ⎣b d ty⎦ 99 | * ``` 100 | * 101 | * Per usual linear algebra, multiplying a vector `v = (x, y)` according to this affine matrix is defined by: 102 | * 103 | * ``` 104 | * ⎡a c e⎤ ⎛x⎞ ⎛ax + cy + tx⎞ 105 | * ⎢b d f⎥ ⎜y⎟ = ⎜bx + dy + ty⎟ 106 | * ⎣0 0 1⎦ ⎝1⎠ ⎝ 1 ⎠ 107 | * ``` 108 | * 109 | */ 110 | export interface Mat2d { 111 | /** 112 | * Col 1, row 1 component, usually called `m11` in a 4x4 graphics matrix. 113 | */ 114 | a: number; 115 | 116 | /** 117 | * Col 1, row 2 component, usually called `m12` in a 4x4 graphics matrix. 118 | */ 119 | b: number; 120 | 121 | /** 122 | * Col 2, row 1 component, usually called `m21` in a 4x4 graphics matrix. 123 | */ 124 | c: number; 125 | 126 | /** 127 | * Col 2, row 2 component, usually called `m22` in a 4x4 graphics matrix. 128 | */ 129 | d: number; 130 | 131 | /** 132 | * Col 3, row 1 component, usually called `m41` in a 4x4 graphics matrix. 133 | */ 134 | tx: number; 135 | 136 | /** 137 | * Col 3, row 2 component, usually called `m42` in a 4x4 graphics matrix. 138 | */ 139 | ty: number; 140 | } 141 | 142 | /** 143 | * Data type to represent an axis-aligned bounding box (AABB). 144 | * 145 | * Bounding boxes are often used for approximations of geometric shapes or for certain 146 | * categories of performance-optimized spatial calculations, such as spatial index queries 147 | * and viewbox culling. 148 | * 149 | * A box in Math2d is defined by its `minX`, `minY`, `maxX`, and `maxY` edges. Where 150 | * relevant and unless documented otherwise, boxes are interpreted as _closed_ regions, 151 | * i.e. they include those points that lie along their edges. 152 | * 153 | * ``` 154 | * ┌╴x+ 155 | * y+ minY ─┌──────┐ 156 | * maxY ─└──────┘ 157 | * │ │ 158 | * minX maxX 159 | * ``` 160 | * 161 | * Math2d chooses to lay out this data in a 162 | * flat object structure, as opposed to an array or nested arrays, for ease of use and performance. 163 | * 164 | */ 165 | export interface Box { 166 | /** 167 | * Min-X boundary of this box, typically the "left" edge. 168 | */ 169 | minX: number; 170 | 171 | /** 172 | * Min-Y boundary of this box. Note that this could be either the "top" or the "bottom" of the 173 | * box, depending on how your rendering and coordinate systems are laid out. 174 | */ 175 | minY: number; 176 | 177 | /** 178 | * Min-X boundary of this box, typically the "right" edge. 179 | */ 180 | maxX: number; 181 | 182 | /** 183 | * Max-Y boundary of this box. Note that this could be either the "top" or the "bottom" of the 184 | * box, depending on how your rendering and coordinate system are laid out. 185 | */ 186 | maxY: number; 187 | } 188 | 189 | /** 190 | * An alias for `number[]`. Data type to represent a sequence of connected line segments in the plane, 191 | * as an interleaved array of vertex coordinates. 192 | * 193 | * The `IPolyline` type is just an alias for an array of number values. For example, 194 | * a polyline connecting the points p0, p1, p2, ..., pn in space is represented in Math2d as the array: 195 | * A polyline is not necessary closed. 196 | * 197 | * ``` 198 | * [p0x, p0y, p1x, p1y, p2x, p2y, ..., pnx, pny] 199 | * ``` 200 | * 201 | * Where relevant, a polyline with _N_ points is parameterized according to _t_ with linear interpolation between 202 | * adjacent points by index. For example, _t_ = 3.5 represents the midpoint between the index 3 and index 4 point 203 | * of the polyline, _t_ = 0 is its first point, and _t_ = _N_ is its final point. Any _t_ value falling 204 | * outside of the range [0, _N_] is disallowed. 205 | * 206 | * Math2d chooses to lay out this data in a flattened (interleaved) array, as opposed to e.g. an array of 207 | * IVecs, for performance and more compact storage. 208 | */ 209 | export type Polyline = number[]; 210 | 211 | /** 212 | * Data type to hold the result of a point intersection between two pieces of geometry. 213 | * 214 | */ 215 | export interface IntersectionResult { 216 | /** 217 | * Whether an intersection was found. If the return value of a function is `false` for the `exists` field, 218 | * the other Intersection values will be set to `NaN` and should not be interpreted. 219 | */ 220 | exists: boolean; 221 | 222 | /** 223 | * The x-coordinate of the intersection, if an intersection point was found. 224 | */ 225 | x: number; 226 | 227 | /** 228 | * The y-coordinate of the intersection, if an intersection point was found. 229 | */ 230 | y: number; 231 | 232 | /** 233 | * The parameterization of the intersection along the first shape's geometry, 234 | * if an intersection point was found. 235 | */ 236 | t0: number; 237 | 238 | /** 239 | * The parameterization of the intersection along the second shape's geometry, 240 | * if an intersection point was found. 241 | */ 242 | t1: number; 243 | } 244 | 245 | /** 246 | * Data type to hold the result of find the nearest point on a piece of geometry. 247 | */ 248 | export interface NearestPointResult { 249 | /** 250 | * The x-coordinate of the nearest point. 251 | */ 252 | x: number; 253 | 254 | /** 255 | * The y-coordinate of the nearest point. 256 | */ 257 | y: number; 258 | 259 | /** 260 | * The parameterization of the nearest point along the geometry. 261 | */ 262 | t: number; 263 | 264 | /** 265 | * The value of the distance from the computed nearest point to the original point. 266 | * This may be the actual (Euclidean) distance, the signed euclidean distance, or the distance squared, 267 | * depending on the actual nearest function called. 268 | * 269 | * Consult the docs for the particular nearest function being called for specifics on what value 270 | * is returned in this field. 271 | */ 272 | distanceValue: number; 273 | } 274 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![](https://img.shields.io/npm/v/math2d)](https://www.npmjs.com/package/math2d) 4 | [![](https://img.shields.io/circleci/build/github/crazytoucan/math2d/master)](https://app.circleci.com/pipelines/github/crazytoucan/math2d?branch=master) 5 | ![](https://img.shields.io/npm/l/math2d) 6 | 7 | # math2d 8 | 9 | Two dimensional vector math library for JavaScript. 10 | Built for performance in computation-heavy real-time engines without sacrificing usability. 11 | Supports tree shaking and dead code removal to avoid bloating client-side bundles. 12 | Zero dependencies. 13 | 14 | ## Getting Started 15 | 16 | ```sh 17 | $ yarn add math2d 18 | ``` 19 | 20 | ```ts 21 | import { _vec, mat2dFromRotation, vecTransformBy } from "math2d"; 22 | 23 | const threeFour = _vec(3, 4); 24 | const spin = mat2dFromRotation(Math.PI / 4); 25 | console.log(vecTransformBy(threeFour, spin)); 26 | ``` 27 | 28 | ## API 29 | 30 | ### Box Functions 31 | 32 | * **_box**: shorthand for defining a Box from minX, minY, maxX, maxY 33 | * **boxAlloc**: Creates a new Box object in memory, with all values initialized to `NaN` 34 | * **boxClone**: Copies values from an existing IBox into a new box 35 | * **boxContainsBox**: Determines whether the second box is completely enclosed in the first 36 | * **boxContainsPoint**: Determines whether the box contains a given point 37 | * **boxEncapsulate**: Grows the box to include a given point 38 | * **boxEnclosingPoints**: Computes the smallest bounding box that contains all of the provided points 39 | * **boxGetOutCode**: Determines where the specified point lies in relation to the given box 40 | * **boxGrow**: Expands a box by a given amount in all directions 41 | * **boxIntersection**: Computes the area intersection of the two box regions 42 | * **boxIntersectsBox**: Determines whether two boxes overlap 43 | * **boxIsEmpty**: Determines whether this box represents an empty area 44 | * **boxReset**: Construct a new box given `minX`, `minY`, `maxX`, and `maxY` bounding values 45 | * **boxScale**: Scales a box by a fixed scalar in both directions 46 | * **boxTransformBy**: Compute the bounds of the image of this box after applying a 2D affine transformation 47 | * **boxTranslate**: Translate a box by an offset in the x- and y- directions 48 | * **boxUnion**: Compute the smallest bounding box that contains both given boxes 49 | 50 | ### Intersection Result Functions 51 | 52 | * **intersectionResultAlloc**: Creates a new IntersectionResult object in memory, with all values initialized to `false` and `NaN` 53 | * **intersectionResultClone**: Copies the values from the given intersection into a new intersection object 54 | * **intersectionResultReset**: Construct a new intersection given `exists`, `x`, `y`, `t0`, and `t1` values 55 | 56 | ### Mat2d Functions 57 | 58 | * **_mat2d**: shorthand for defining a Mat2d from a, b, c, d, tx, ty 59 | * **mat2dAlloc**: Creates a new mat2d object in memory, with all values initialized to `NaN` 60 | * **mat2dClone**: Copies the values from the given matrix into a new matrix 61 | * **mat2dDeterminant**: Computes the determinant of the affine matrix 62 | * **mat2dFromRotation**: Computes the affine transform corresponding to a given rotation, in radians 63 | * **mat2dFromTranslation**: Computes the affine transform corresponding to a given (tx, ty) translation 64 | * **mat2dIdentity**: Returns the identity affine matrix, `[1 0 0 1 0 0]` 65 | * **mat2dInvert**: Computes the inverse of the given 2d affine matrix 66 | * **mat2dIsOrthogonal**: Returns whether the matrix is an orthogonal matrix 67 | * **mat2dIsTranslationOnly**: Returns whether the matrix corresponds to only a translation 68 | * **mat2dMulMat2d**: Computes the result of affine matrix multiplication _m1_ × _m2_ 69 | * **mat2dReset**: Construct a new matrix given component values 70 | * **mat2dRotate**: Applies a rotation in radians to the given matrix, returning the result 71 | * **mat2dScale**: Applies a scaling transform on top of the given affine matrix, returning the result 72 | * **mat2dTranslate**: Applies a translation on top of the given matrix, returning the result 73 | 74 | ### Nearest Point Result Functions 75 | 76 | * **nearestPointResultAlloc**: Creates a new NearestPointResult object in memory, with all values initialized to `NaN` 77 | * **nearestPointResultClone**: Copies the values from the given NearestPointResult into a new NearestPointResult object 78 | * **nearestPointResultReset**: Construct a new intersection given `exists`, `x`, `y`, `t0`, and `t1` values 79 | 80 | ### Polyline Functions 81 | 82 | * **polylineAlloc**: Creates a new Array object in memory to hold Polyline data. Its initial length is 0 83 | * **polylineClose**: Repeats the polyline's first vertex to form a closed path 84 | * **polylineContainsPoint**: undefined 85 | * **polylineContainsPointInside**: Determines whether the point is inside the given polygon, using the even-odd fill rule 86 | * **polylineGetBounds**: Computes bounding box of polyline's geometry 87 | * **polylineGetDistanceAtT**: Computes the Euclidean distance traveled along the polyline's geometry to get to the parametric point at _t_ 88 | * **polylineGetLength**: Computes total length of polyline 89 | * **polylineGetNumSegments**: Returns the number of individual line segments in this polyline 90 | * **polylineGetNumVertices**: Returns the number of vertices in this polyline 91 | * **polylineGetPointAtT**: Computes a point along the polyline, parameterized according to linear interpolation between adjacent vertices 92 | * **polylineGetSegment**: Returns a polyline's segment by given index, starting at 0 93 | * **polylineGetSegmentLength**: Computes the length of one of a polyline's segments by index, starting at 0 94 | * **polylineGetTAtDistance**: Computes the parametric value _t_ along the polyline corresponding to a distance _d_ 95 | * **polylineGetVertex**: Retrieves a vertex from this polyline's geometry, starting at index 0 96 | * **polylineIntersectRay**: Computes all locations at which a polyline crosses a given ray 97 | * **polylineIntersectSegment**: Computes all locations at which a polyline crosses a given line segment 98 | * **polylineIsClosed**: Returns whether the polyline's last vertex equals its first 99 | * **polylineNearestDistanceSqToPoint**: Finds the closest the polyline comes to a given reference point 100 | * **polylineTransformBy**: Transforms a polyline by an affine matrix 101 | * **polylineTrim**: Trims a polyline to a range of its _t_ parameter 102 | 103 | ### Ray Functions 104 | 105 | * **_ray**: shorthand for defining a Ray from x0, y0, dirX, dirY 106 | * **rayAlloc**: Creates a new Ray object in memory, with all values initialized to `NaN` 107 | * **rayClone**: Copies the values from the given ray into a new ray 108 | * **rayContainsPoint**: Determines if the point is on the ray 109 | * **rayGetPointAtT**: Gets a point along the ray, parameterized according to distance along its direction vector 110 | * **rayIntersectPolyline**: Computes all locations at which a ray crosses a given polyline 111 | * **rayIntersectRay**: Computes the intersection point between the two rays, if it exists 112 | * **rayIntersectSegment**: Computes the intersection point between the ray and the segment, if it exists 113 | * **rayLookAt**: Constructs a ray from an initial point, pointing in the direction of a target point 114 | * **rayNearestDistanceSqToPoint**: Determines the closest the ray comes to a given reference point 115 | * **rayProjectPoint**: Projects a point onto the given line, returning the distance _t_ along the line where it falls 116 | * **rayReset**: Construct a new ray given an (x0, y0) initial point and (dirX, dirY) direction vector 117 | * **rayTransformBy**: Transforms a ray by an affine matrix 118 | * **rayWhichSide**: Computes on which side of the ray (as a _line_) a given point lies 119 | 120 | ### Segment Functions 121 | 122 | * **_segment**: shorthand for defining a Segment from x0, y0, x1, y1 123 | * **segmentAlloc**: Creates a new Segment object in memory, with all values initialized to `NaN` 124 | * **segmentGetEndpoint0**: Retrieves the starting endpoint (_t_ = 0) of the segment, as a vector 125 | * **segmentGetEndpoint1**: Retrives the ending endpoint (_t_ = 1) of the segment, as a vector 126 | * **segmentGetLength**: Computes the length of the line segment 127 | * **segmentGetLengthSq**: Computes the squared length of the line segment 128 | * **segmentGetPointAtT**: Gets a point along the line segment, parameterized according to linear interpolation between its endpoints 129 | * **segmentIntersectPolyline**: Computes all locations at which a line segment meets a given polyline 130 | * **segmentIntersectRay**: Computes the intersection point between the ray and the segment, if it exists 131 | * **segmentIntersectSegment**: Computes the intersection point between the two line segments, if it exists 132 | * **segmentNearestDistanceSqToPoint**: Finds the closest the segment comes to a given reference point 133 | * **segmentReset**: Construct a new line segment given an (x0, y0) starting vertex and (x1, y1) ending vertex. The two points are allowed to be the same 134 | * **segmentReverse**: Computes the reverse of the segment, i.e. swapping its starting vertex and ending vertex 135 | 136 | ### Vec Functions 137 | 138 | * **_vec**: shorthand for defining a Vec from x, y 139 | * **vecAdd**: Computes the result of adding the two given vectors 140 | * **vecAlloc**: Creates a new Vec object in memory, with all values initialized to `NaN` 141 | * **vecClone**: Copies the values from the given vector into a new vector 142 | * **vecCross**: Computes the two-dimensional cross product of the two vectors 143 | * **vecDistance**: Computes the straight-line (Euclidean) distance between the two points 144 | * **vecDistanceSq**: Computes the squared straight-line (i.e. Euclidean) distance between the two points 145 | * **vecDot**: Computes the dot product of the two vectors, i.e. `u.x * v.x + u.y * v.y` 146 | * **vecGetLength**: Computes the straight-line length (i.e. Euclidean norm) of the given vector 147 | * **vecGetLengthSq**: Computes the squared straight-line length (i.e. square of the Euclidean norm) of the given vector 148 | * **vecLerp**: Performs a linear interpolation between the two vectors. The `r` parameter is allowed to be outside `[0, 1]` 149 | * **vecNormalize**: Normalizes the vector to be length 1. If the given vector is the zero-vector, this method returns `(NaN, NaN)` 150 | * **vecPerp**: Computes the perp of the given vector, as defined by `vecPerp(a, b) = (-b, a)`. This is equivalent to a counter-clockwise rotation in the standard plane 151 | * **vecReset**: Construct a new vector given an `x` and `y` value 152 | * **vecScale**: Scales both coordinates of this vector by a given scalar 153 | * **vecSubtract**: Computes `u - v`, i.e. subtracting the second vector from the first 154 | * **vecTransformBy**: Multiplies the vector by an affine matrix 155 | --------------------------------------------------------------------------------