thi-ng/umbrella

View on GitHub
packages/geom/src/vertices.ts

Summary

Maintainability
A
35 mins
Test Coverage
import type { Maybe } from "@thi.ng/api";
import { peek } from "@thi.ng/arrays/peek";
import { isArray } from "@thi.ng/checks/is-array";
import { isNumber } from "@thi.ng/checks/is-number";
import type { MultiFn1O } from "@thi.ng/defmulti";
import { defmulti } from "@thi.ng/defmulti/defmulti";
import { sample as _arcVertices } from "@thi.ng/geom-arc/sample";
import { DEFAULT_SAMPLES } from "@thi.ng/geom-resample/api";
import { resample } from "@thi.ng/geom-resample/resample";
import { sampleCubic } from "@thi.ng/geom-splines/cubic-sample";
import { sampleQuadratic } from "@thi.ng/geom-splines/quadratic-sample";
import { cossin } from "@thi.ng/math/angle";
import { TAU } from "@thi.ng/math/api";
import type { Vec } from "@thi.ng/vectors";
import { add2, add3 } from "@thi.ng/vectors/add";
import { cartesian2 } from "@thi.ng/vectors/cartesian";
import { madd2 } from "@thi.ng/vectors/madd";
import { set2 } from "@thi.ng/vectors/set";
import type { IShape, PathSegment, SamplingOpts } from "./api.js";
import type { AABB } from "./api/aabb.js";
import type { Arc } from "./api/arc.js";
import type { Circle } from "./api/circle.js";
import { ComplexPolygon } from "./api/complex-polygon.js";
import type { Cubic } from "./api/cubic.js";
import type { Ellipse } from "./api/ellipse.js";
import type { Group } from "./api/group.js";
import type { Path } from "./api/path.js";
import type { Points } from "./api/points.js";
import { Polygon } from "./api/polygon.js";
import type { Polyline } from "./api/polyline.js";
import type { Quadratic } from "./api/quadratic.js";
import type { Rect } from "./api/rect.js";
import { __dispatch } from "./internal/dispatch.js";
import { __circleOpts, __sampleAttribs } from "./internal/vertices.js";

/**
 * Extracts/samples vertices from given shape's boundary and returns them as
 * array. Some shapes also support
 * [`SamplingOpts`](https://docs.thi.ng/umbrella/geom-resample/interfaces/SamplingOpts.html).
 *
 * @remarks
 * The given sampling options (if any) can also be overridden per shape using
 * the special `__samples` attribute. If specified, these will be merged with
 * the options.
 *
 * Currently implemented for:
 *
 * - {@link AABB}
 * - {@link Arc}
 * - {@link BPatch}
 * - {@link Circle}
 * - {@link ComplexPolygon}
 * - {@link Cubic}
 * - {@link Ellipse}
 * - {@link Extra}
 * - {@link Group}
 * - {@link Line}
 * - {@link Path}
 * - {@link Points}
 * - {@link Points3}
 * - {@link Quad}
 * - {@link Quadratic}
 * - {@link Rect}
 * - {@link Triangle}
 *
 * @example
 * ```ts
 * import { circle, vertices } from "@thi.ng/geom";
 *
 * // using default
 * vertices(circle(100))
 *
 * // specify resolution only
 * vertices(circle(100), 6)
 *
 * // specify more advanced options
 * vertices(circle(100), { dist: 10 })
 *
 * // using shape attribs
 * vertices(circle(100, { __samples: { dist: 10 } }))
 * ```
 *
 * @param shape
 * @param opts
 */
export const vertices: MultiFn1O<
    IShape,
    number | Partial<SamplingOpts>,
    Vec[]
> = defmulti<any, Maybe<number | Partial<SamplingOpts>>, Vec[]>(
    __dispatch,
    {
        bpatch: "points",
        cubic3: "cubic",
        group3: "group",
        line: "polyline",
        line3: "polyline",
        path3: "path",
        points3: "points",
        poly3: "poly",
        polyline3: "polyline",
        quad: "poly",
        quad3: "poly",
        quadratic3: "quadratic",
        tri: "poly",
        tri3: "poly",
    },
    {
        // e +----+ h
        //   |\   :\
        //   |f+----+ g
        //   | |  : |
        // a +-|--+d|
        //    \|   \|
        //   b +----+ c
        //
        aabb: ({ pos, size }: AABB) => {
            const [px, py, pz] = pos;
            const [qx, qy, qz] = add3([], pos, size);
            return [
                [px, py, pz], // a
                [px, py, qz], // b
                [qx, py, qz], // c
                [qx, py, pz], // d
                [px, qy, pz], // e
                [px, qy, qz], // f
                [qx, qy, qz], // g
                [qx, qy, pz], // h
            ];
        },

        arc: ($: Arc, opts?): Vec[] =>
            _arcVertices(
                $.pos,
                $.r,
                $.axis,
                $.start,
                $.end,
                __sampleAttribs(opts, $.attribs)
            ),

        circle: ($: Circle, opts = DEFAULT_SAMPLES) => {
            opts = __sampleAttribs(opts, $.attribs)!;
            const pos = $.pos;
            const r = $.r;
            let [num, start, last] = __circleOpts(opts, r);
            const delta = TAU / num;
            last && num++;
            const buf: Vec[] = new Array(num);
            for (let i = 0; i < num; i++) {
                buf[i] = cartesian2(null, [r, start + i * delta], pos);
            }
            return buf;
        },

        complexpoly: ($: ComplexPolygon, opts) => {
            const pts = vertices($.boundary, opts);
            for (let child of $.children) pts.push(...vertices(child, opts));
            return pts;
        },

        cubic: ($: Cubic, opts?) =>
            sampleCubic($.points, __sampleAttribs(opts, $.attribs)),

        ellipse: ($: Ellipse, opts = DEFAULT_SAMPLES) => {
            opts = __sampleAttribs(opts, $.attribs)!;
            const buf: Vec[] = [];
            const pos = $.pos;
            const r = $.r;
            let [num, start, last] = __circleOpts(
                opts,
                Math.max($.r[0], $.r[1])
            );
            const delta = TAU / num;
            last && num++;
            for (let i = 0; i < num; i++) {
                buf[i] = madd2([], cossin(start + i * delta), r, pos);
            }
            return buf;
        },

        extra: () => [],

        group: ($: Group, opts?) => {
            opts = __sampleAttribs(opts, $.attribs);
            return $.children.reduce(
                (acc, $) => acc.concat(vertices($, opts)),
                <Vec[]>[]
            );
        },

        path: ($: Path, opts?) => {
            opts = __sampleAttribs(opts, $.attribs);
            const _opts = isNumber(opts) ? { num: opts } : opts;
            const verts: Vec[] = [];
            const $segmentVerts = (segments: PathSegment[]) => {
                const closed = peek(segments)?.type === "z";
                for (let n = segments.length - 1, i = 0; i <= n; i++) {
                    const s = segments[i];
                    if (!s.geo) continue;
                    verts.push(
                        ...vertices(s.geo, {
                            ..._opts,
                            last: !closed && i === n,
                        })
                    );
                }
            };
            $segmentVerts($.segments);
            for (let sub of $.subPaths) $segmentVerts(sub);
            return verts;
        },

        points: ($: Points) => $.points.slice(),

        poly: ($: Polygon, opts?) =>
            resample($.points, __sampleAttribs(opts, $.attribs), true, true),

        polyline: ($: Polyline, opts?) =>
            resample($.points, __sampleAttribs(opts, $.attribs), false, true),

        quadratic: ($: Quadratic, opts?) =>
            sampleQuadratic($.points, __sampleAttribs(opts, $.attribs)),

        rect: ($: Rect, opts?) => {
            opts = __sampleAttribs(opts, $.attribs);
            const p = $.pos;
            const q = add2([], p, $.size);
            const verts = [set2([], p), [q[0], p[1]], q, [p[0], q[1]]];
            return opts != null ? vertices(new Polygon(verts), opts) : verts;
        },
    }
);

/**
 * Takes an array of vertices or an `IShape`. If the latter, calls
 * {@link vertices} with default options and returns result, else returns
 * original array.
 *
 * @param shape -
 */
export const ensureVertices = (shape: IShape | Vec[]) =>
    isArray(shape) ? shape : vertices(shape);