Source: Solid.js

// noinspection JSUnusedGlobalSymbols

import {ceiling, sqrt, sin, cos, pi} from '@backstrap/math';
import {Shape} from './Shape';
import {Surface} from './Surface';

/**
 * Class representing a factory for various simple solid objects.
 * @extends Coords
 */
export class Solid extends Shape {
    /**
     * Generate a block (a cuboid.)
     *
     * @param {number} [w] - width
     * @param {number} [h] - height
     * @param {number} [d] - depth
     * @returns {Geometry[]}
     */
    block(w = 1, h = w, d = h) {
        return [
            (u, v) => [-w/2, h*(u - 0.5), -d*(v - 0.5)],
            (u, v) => [w/2, h*(u - 0.5), d*(v - 0.5)],
            (u, v) => [w*(u - 0.5), -h/2, d*(v - 0.5)],
            (u, v) => [w*(u - 0.5), h/2, -d*(v - 0.5)],
            (u, v) => [w*(u - 0.5), -h*(v - 0.5), -d/2],
            (u, v) => [w*(u - 0.5), h*(v - 0.5), d/2],
        ].map(f => this.surface(f, [0, 1, 1], [0, 1, 1])).flat();
    }

    /**
     * Generate a rod (a solid cylinder, with ends.)
     *
     * @param {number} [r] - radius
     * @param {number} [h] - height
     * @returns {Geometry[]}
     */
    rod(r = 1, h = 1) {
        const surface = new Surface(this);
        return surface.cylinder(r, h)
            .concat(surface.dupe().translate(0, 0, h*0.5).disc(r))
            .concat(surface.dupe().translate(0, 0, -h*0.5).rotate(pi).disc(r));
    }

    /**
     * Generate a solid cone.
     *
     * @param {number} [r] - radius
     * @param {number} [h] - height
     * @returns {Geometry[]}
     */
    cone(r = 1, h = 1) {
        const surface = new Surface(this);
        return surface.conic(0, r, h)
            .concat(surface.dupe().translate(0, 0, -h*0.5).rotate(pi).disc(r));
    }

    /**
     * Generate a sphere with cubic tesselation.
     *
     * @param {number} [r] - radius
     * @returns {Geometry[]}
     */
    sphere(r = 1) {
        const dv = ceiling(this.dv/4);
        const uvRange = [-0.5, 0.5, dv];
        const dist = (u, v) => r/sqrt(u*u + v*v + 0.25);

        return [
            (u, v) => (p => [ u*p,  v*p,  p/2])(dist(u, v)),
            (u, v) => (p => [ u*p, -v*p, -p/2])(dist(u, v)),
            (u, v) => (p => [ p/2,  u*p,  v*p])(dist(u, v)),
            (u, v) => (p => [ u*p, -p/2,  v*p])(dist(u, v)),
            (u, v) => (p => [-p/2, -u*p,  v*p])(dist(u, v)),
            (u, v) => (p => [-u*p,  p/2,  v*p])(dist(u, v)),
        ].map(f => this.surface(f,  uvRange, uvRange)).flat();
    }

    /**
     * Generate a regular torus.
     *
     * @param {number} [r1] - primary radius
     * @param {number} [r2] - secondary radius
     * @returns {Geometry[]}
     */
    torus(r1 = 2, r2 = 1) {
        const uvRange = [0, 2*pi, this.dv];

        return this.surface((u, v) => [
            (r1 + r2*cos(v))*cos(u),
            (r1 + r2*cos(v))*sin(u),
            r2*sin(v)
        ], uvRange, uvRange);
    }

    /**
     * Generate the surface of a general parametric volume.
     * The default arguments produce a properly oriented unit cube.
     * If drawing "single-sided", note that
     * the function f naturally parameterizes the interior
     * of the volume, oriented with right-hand-rule,
     * so the "upward-facing" normals to the surface g(u, v) = f(u, v, k)
     * are outward-facing when k = w[1] (not k = w[0]).
     *
     * @param {function(number, number, number): number[]} [f] - parameterization of the volume
     * @param {number[]} [u] - [start, end, step] for u parameter (default [0, 1, 1])
     * @param {number[]} [v] - [start, end, step] for v parameter (default [0, 1, 1])
     * @param {number[]} [w] - [start, end, step] for w parameter (default [0, 1, 1])
     * @returns {Geometry[]}
     */
    volume(f = (...v) => v, u = [0, 1, 1], v = [0, 1, 1], w = [0, 1, 1]) {
        const backward = ([start, end, steps]) => [end, start, steps];

        return [
            this.surface((s, t) => f(s, t, w[0]), u, backward(v)),
            this.surface((s, t) => f(s, t, w[1]), u, v),
            this.surface((s, t) => f(s, v[0], t), u, w),
            this.surface((s, t) => f(s, v[1], t), backward(u), w),
            this.surface((s, t) => f(u[0], s, t), v, backward(w)),
            this.surface((s, t) => f(u[1], s, t), v, w),
        ].flat();
    }

    /**
     * Generate the surface of an extruded convex polygon.
     * The polygon is drawn in the x-y plane, and extruded along the z-axis.
     * The default parameters draw a unit cube.
     *
     * @param {function(number): number[]} [f] - parameterized cross-section f(t) = [x, y]
     * @param {number[]} [t] - [start, end, step] for the cross-section parameterization
     * @param {number} [h] - extrusion height
     * @returns {Geometry[]}
     */
    extrusion(f = t => [cos(t), sin(t)], t = [0, 2*pi], h = 1) {
        return [
            this.surface((u, v) => v ? [...f(u), h/2] : [0, 0, h/2], t, [0, 1, 1]),
            this.surface((u, v) => v ? [...f(u), -h/2] : [0, 0, -h/2], t, [1, 0, 1]),
            this.surface((u, v) => [...f(u), v], t, [-h/2, h/2, 1]),
        ].flat(Infinity);
    }

    /**
     * Generate the surface of an extruded convex polygon.
     * The polygon is drawn in the x-y plane, and extruded along the z-axis.
     * The default parameters draw a unit cube.
     *
     * @param {number[][]} [pts] - 2D polygon vertices (must enclose the origin "convex-ly")
     * @param {number} [h] - extrusion height
     * @returns {Geometry[]}
     */
    polygonExtrusion(pts = [[0.5, 0.5], [-0.5, 0.5], [-0.5, -0.5], [0.5, -0.5]], h = 1) {
        const vertices = [pts.at(-1), ...pts];
        const segments = pts.length - 2;
        const f = d => (u, v) => [pts[v ? 0 : 1 + u|0][0], pts[v ? 0 : 1 + u|0][1], d/2];

        return [
            this.surface(f(h), [segments, 0, segments], [0, 1, 1]),
            this.surface(f(-h), [0, segments, segments], [0, 1, 1]),
            pts.map(
                (pt, n) => this.surface(
                    (u, v) => [
                        (u ? pt : vertices[n])[0],
                        (u ? pt : vertices[n])[1],
                        h / (v ? 2 : -2),
                    ],
                    [0, 1, 1], [0, 1, 1]
                )
            ),
        ].flat(Infinity);
    }
}