thi-ng/umbrella

View on GitHub
packages/geom-closest-point/src/line.ts

Summary

Maintainability
A
1 hr
Test Coverage
import type { FnU3, Maybe } from "@thi.ng/api";
import type { ReadonlyVec, Vec } from "@thi.ng/vectors";
import { dist } from "@thi.ng/vectors/dist";
import { distSq } from "@thi.ng/vectors/distsq";
import { dot } from "@thi.ng/vectors/dot";
import { empty } from "@thi.ng/vectors/empty";
import { magSq } from "@thi.ng/vectors/magsq";
import { mixN } from "@thi.ng/vectors/mixn";
import { set } from "@thi.ng/vectors/set";
import { sub } from "@thi.ng/vectors/sub";

/**
 * Computes the parametric distance `t` of point `p` projected onto line
 * `a` -> `b`, relative to `a`. I.e. the projection of `p` can then be
 * computed like so:
 *
 * @example
 * ```ts
 * import { closestT } from "@thi.ng/geom-closest-point";
 * import { mixN } from "@thi.ng/vectors";
 *
 * mixN([], a, b, closestT(p, a, b))
 * ```
 *
 * If the return value is outside the closed [0,1] interval, the
 * projected point lies outside the line segment. Returns `undefined` if
 * `a` and `b` are coincident.
 *
 * - {@link closestPointLine}
 * - {@link closestPointSegment}
 *
 * @param p - query point
 * @param a - line point A
 * @param b - line point B
 */
export const closestT: FnU3<ReadonlyVec, Maybe<number>> = (p, a, b) => {
    const d = sub([], b, a);
    const l = magSq(d);
    return l > 1e-6 ? dot(sub([], p, a), d) / l : undefined;
};

/**
 * Returns closest point to `p` on infinite line defined by points `a`
 * and `b`. Use {@link closestPointSegment} to only consider the actual line
 * segment between these two points.
 *
 * {@link closestPointSegment}
 *
 * @param p - query point
 * @param a - line point A
 * @param b - line point B
 */
export const closestPointLine: FnU3<ReadonlyVec, Vec> = (p, a, b) =>
    mixN([], a, b, closestT(p, a, b) || 0);

/**
 * Returns distance from `p` to closest point to infinite line `a` ->
 * `b`. Use {@link distToSegment} to only consider the actual line segment
 * between these two points.
 *
 * {@link distToSegment}
 *
 * @param p - query point
 * @param a - line point A
 * @param b - line point B
 */
export const distToLine: FnU3<ReadonlyVec, number> = (p, a, b) =>
    dist(p, closestPointLine(p, a, b) || a);

/**
 * Returns closest point to `p` on line segment `a` -> `b`. By default,
 * if the result point lies outside the segment, returns a copy of the
 * closest end point. The result is written to the optional `out` vector
 * (or if omitted, a new one is created).
 *
 * If `insideOnly` is true, only returns the closest point iff it
 * actually is inside the segment. The behavior of this configurable via
 * the optional `eps` arg and by default includes both end points. This
 * function uses {@link closestT} to compute the parametric position of the
 * result point and determine if it lies within the line segment. If
 * `eps > 0`, the end points `a` and `b` will be excluded from the
 * match, effectively shortening the valid line segment from both ends,
 * i.e. the valid interval of the parametric position will be
 * [eps,1-eps]. If the result lies outside this interval, the function
 * returns `undefined`. Likewise, if `a` and `b` are coincident.
 *
 * @param p - query point
 * @param a - line point A
 * @param b - line point B
 * @param out - result
 * @param eps - epsilon value
 */
export const closestPointSegment = (
    p: ReadonlyVec,
    a: ReadonlyVec,
    b: ReadonlyVec,
    out?: Vec,
    insideOnly = false,
    eps = 0
) => {
    const t = closestT(p, a, b);
    if (t !== undefined && (!insideOnly || (t >= eps && t <= 1 - eps))) {
        out = out || empty(p);
        return t <= 0 ? set(out, a) : t >= 1 ? set(out, b) : mixN(out, a, b, t);
    }
};

/**
 * Returns distance from `p` to closest point on line segment `a` ->
 * `b`.
 *
 * @param p - query point
 * @param a - line point A
 * @param b - line point B
 */
export const distToSegment: FnU3<ReadonlyVec, number> = (p, a, b) =>
    dist(p, closestPointSegment(p, a, b) || a);

export const closestPointPolyline = (
    p: ReadonlyVec,
    pts: ReadonlyArray<Vec>,
    closed = false,
    out: Vec = []
) => {
    if (!pts.length) return;
    const tmp: Vec = [];
    const n = pts.length - 1;
    let minD = Infinity,
        i,
        j;
    if (closed) {
        i = n;
        j = 0;
    } else {
        i = 0;
        j = 1;
    }
    for (; j <= n; i = j, j++) {
        if (closestPointSegment(p, pts[i], pts[j], tmp)) {
            const d = distSq(p, tmp);
            if (d < minD) {
                minD = d;
                set(out, tmp);
            }
        }
    }
    return minD < Infinity ? out : undefined;
};

/**
 * Returns the index of the start point containing the segment in the
 * polyline array `points` farthest away from `p` with regards to the
 * line segment `a` to `b`. `points` is only checked between indices
 * `from` and `to` (not including the latter).
 *
 * @param a - line point A
 * @param b - line point B
 * @param points - points
 * @param from - start search index
 * @param to - end search index
 */
export const farthestPointSegment = (
    a: ReadonlyVec,
    b: ReadonlyVec,
    points: ReadonlyVec[],
    from = 0,
    to = points.length
) => {
    let maxD = -1;
    let maxIdx: number = -1;
    const tmp = empty(a);
    for (let i = from; i < to; i++) {
        const p = points[i];
        const d = distSq(p, closestPointSegment(p, a, b, tmp) || a);
        if (d > maxD) {
            maxD = d;
            maxIdx = i;
        }
    }
    return [maxIdx, Math.sqrt(maxD)];
};