Source: details/foils.js

"use strict"

/**
 * Builds "foil" shapes such as trefoils, quatrefoils, cinquefoils, etc. Input 2D profiles must be centred at (0, 0, 0)
 * @namespace details.foils
 */

const foilBuilder = ({ lib }) => {
    const { union, subtract, scission } = lib.booleans
    const { rotate, align, translate, mirror } = lib.transforms
    const { circle, cuboid, rectangle } = lib.primitives
    const { measureBoundingBox } = lib.measurements
    const { extrudeRotate } = lib.extrusions

    /**
     * Builds a 2D n-foil opening
     * @memberof details.foils
     * @instance
     * @param {Object} opts
     * @param {number} opts.numLobes - number of lobes
     * @param {number} opts.radius - radius of container circle
     * @param {string} opts.lobeRadiusType - "inSlice", "halfRadius", "mean"
     * @access private
     */
    const buildFoil2d = (opts) => {
        const centralAngle = Math.PI * 2 / opts.numLobes;
        const sinHalfCentral = Math.sin(centralAngle / 2);

        // this radius has zero overlap between lobe circles
        const lobeRadiusInSlice = sinHalfCentral / (1 + sinHalfCentral) * opts.radius;
        const lobeRadiusDiff = opts.radius / 2 - lobeRadiusInSlice;
        const lobeRadiusMean = lobeRadiusInSlice + (lobeRadiusDiff / 2);

        const lobeRadType = opts.lobeRadiusType || 'mean'
        let lobeRadius = lobeRadiusMean;
        if (lobeRadType === 'inSlice') {
            lobeRadius = lobeRadiusInSlice
        } else if (lobeRadType === 'halfRadius') {
            lobeRadius = opts.radius / 2
        }

        const lobeCircle = circle({ radius: lobeRadius });
        const alignedLobeCircle = align({ modes: ['none', 'min'], relativeTo: [0, -opts.radius] }, lobeCircle);
        let centreCircle = lobeCircle;
        if (opts.numLobes === 3) {
            // special case for trefoils
            if (lobeRadType === 'mean') {
                centreCircle = circle({ radius: opts.radius * 0.435 });
            }
            else if (lobeRadType === 'inSlice') {
                centreCircle = circle({ radius: opts.radius * 0.3 });
            }
        }

        const rotationAngles = [];
        for (let index = 1; index < opts.numLobes; index++) {
            rotationAngles.push(centralAngle * index);
        }
        // console.log(rotationAngles);

        const rotatedLobes = rotationAngles.map(angle => {
            return rotate([0, 0, angle], alignedLobeCircle);
        });

        return union(centreCircle, alignedLobeCircle, ...rotatedLobes);
    }

    /**
     * Builds a 3D n-foil opening using a given 2D cross-section profile
     * @memberof details.foils
     * @instance
     * @param {Object} opts
     * @param {number} opts.numLobes - number of lobes
     * @param {number} opts.radius - radius of container circle
     * @param {string} opts.lobeRadiusType - "inSlice", "halfRadius", "mean"
     * @param {boolean} opts.cutCentre - if true, cuts a circular hole in centre of opening
     * @param {geom2.Geom2} geomProfile - 2D cross-section profile
     * @access private
     */
    const buildFoil3d = (opts, geomProfile) => {
        const centralAngle = Math.PI * 2 / opts.numLobes;
        const sinHalfCentral = Math.sin(centralAngle / 2);
        const isCentreCut = opts.cutCentre || true;

        // this radius has zero overlap between lobe circles
        const lobeRadiusInSlice = sinHalfCentral / (1 + sinHalfCentral) * opts.radius;
        const lobeRadiusDiff = opts.radius / 2 - lobeRadiusInSlice;
        const lobeRadiusMean = lobeRadiusInSlice + (lobeRadiusDiff / 2);

        const lobeRadType = opts.lobeRadiusType || 'mean'
        let lobeRadius = lobeRadiusMean;
        if (lobeRadType === 'inSlice') {
            lobeRadius = lobeRadiusInSlice
        } else if (lobeRadType === 'halfRadius') {
            lobeRadius = opts.radius / 2
        }

        const translatedProfile = translate([lobeRadius, 0, 0], geomProfile);
        const lobeCircle = extrudeRotate({ segments: 48 }, translatedProfile);
        const alignedLobeCircle = translate([0, -(opts.radius - lobeRadius), 0], lobeCircle);

        const lobeCircleBbox = measureBoundingBox(alignedLobeCircle);
        const cutBlockThickness = (lobeCircleBbox[1][2] - lobeCircleBbox[0][2]) * 2;
        const cutBlock1 = rotate([0, 0, centralAngle / 2], align({ modes: ['min', 'center', 'none'] }, cuboid({ size: [lobeRadius, opts.radius * 2, cutBlockThickness] })));
        const cutBlock2 = mirror({ normal: [1, 0, 0] }, cutBlock1);
        const cutBlock = union(cutBlock1, cutBlock2);
        let cutLobe = subtract(alignedLobeCircle, cutBlock);

        const profileBbox = measureBoundingBox(geomProfile);
        console.log(profileBbox);
        const profileSize = [profileBbox[1][0] - profileBbox[0][0], profileBbox[1][1] - profileBbox[0][1]];
        console.log(profileSize);
        const negProfile = subtract(rectangle({ size: [profileSize[0] + 1, profileSize[1] + 1] }), geomProfile);
        const negProfileCut = subtract(negProfile, translate([(profileSize[0] + 2) / 2, 0, 0], rectangle({ size: [profileSize[0] + 2, profileSize[1] + 2] })));
        const negProfileAdj = translate([profileSize[0] / 2, 0, 0], negProfileCut);

        let centreCircle = extrudeRotate({ segments: 48 }, translate([lobeRadius, 0, 0], negProfileAdj));
        if (opts.numLobes === 3) {
            // special case for trefoils
            if (lobeRadType === 'mean') {
                centreCircle = extrudeRotate({ segments: 48 }, translate([opts.radius * 0.435, 0, 0], negProfileCut));
            }
            else if (lobeRadType === 'inSlice') {
                centreCircle = extrudeRotate({ segments: 48 }, translate([opts.radius * 0.3, 0, 0], negProfileCut));
            }
            else if (lobeRadType === 'halfRadius') {
                centreCircle = extrudeRotate({ segments: 48 }, translate([opts.radius * 0.5, 0, 0], negProfileCut));
            }
        }

        if (isCentreCut) {
            cutLobe = scission(subtract(cutLobe, centreCircle))[0];
        }

        const rotationAngles = [];
        for (let index = 1; index < opts.numLobes; index++) {
            rotationAngles.push(centralAngle * index);
        }

        const rotatedLobes = rotationAngles.map(angle => {
            return rotate([0, 0, angle], cutLobe);
        });

        return union(cutLobe, ...rotatedLobes)
    }

    return {
        buildFoil2d,
        buildFoil3d,
        /**
         * Builds a trefoil opening using a given 2d cross-section profile
         * @memberof details.foils
         * @instance
         * @param {Object} opts
         * @param {number} opts.radius - radius of container circle
         * @param {string} opts.lobeRadiusType - "inSlice", "halfRadius", "mean"
         * @param {boolean} opts.cutCentre - if true, cuts a circular hole in centre of opening (only for 3D)
         * @param {geom2.Geom2} geomProfile - 2D cross-section profile
         */
        trefoil: (opts, geomProfile) => {
            if (geomProfile) {
                return buildFoil3d({ ...opts, numLobes: 3 }, geomProfile);
            } else {
                return buildFoil2d({ ...opts, numLobes: 3 });
            }
        },
        /**
         * Builds a quatrefoil opening using a given 2d cross-section profile
         * @memberof details.foils
         * @instance
         * @param {Object} opts
         * @param {number} opts.radius - radius of container circle
         * @param {string} opts.lobeRadiusType - "inSlice", "halfRadius", "mean"
         * @param {boolean} opts.cutCentre - if true, cuts a circular hole in centre of opening (only for 3D)
         * @param {geom2.Geom2} geomProfile - 2D cross-section profile
         */
        quatrefoil: (opts, geomProfile) => {
            if (geomProfile) {
                return buildFoil3d({ ...opts, numLobes: 4 }, geomProfile);
            } else {
                return buildFoil2d({ ...opts, numLobes: 4 });
            }
        },
        /**
         * Builds a cinquefoil opening using a given 2d cross-section profile
         * @memberof details.foils
         * @instance
         * @param {Object} opts
         * @param {number} opts.radius - radius of container circle
         * @param {string} opts.lobeRadiusType - "inSlice", "halfRadius", "mean"
         * @param {boolean} opts.cutCentre - if true, cuts a circular hole in centre of opening (only for 3D)
         * @param {geom2.Geom2} geomProfile - 2D cross-section profile
         */
        cinquefoil: (opts, geomProfile) => {
            if (geomProfile) {
                return buildFoil3d({ ...opts, numLobes: 5 }, geomProfile);
            } else {
                return buildFoil2d({ ...opts, numLobes: 5 });
            }
        },
        /**
         * Builds a sexfoil opening using a given 2d cross-section profile
         * @memberof details.foils
         * @instance
         * @param {Object} opts
         * @param {number} opts.radius - radius of container circle
         * @param {string} opts.lobeRadiusType - "inSlice", "halfRadius", "mean"
         * @param {boolean} opts.cutCentre - if true, cuts a circular hole in centre of opening (only for 3D)
         * @param {geom2.Geom2} geomProfile - 2D cross-section profile
         */
        sexfoil: (opts, geomProfile) => {
            if (geomProfile) {
                return buildFoil3d({ ...opts, numLobes: 6 }, geomProfile);
            } else {
                return buildFoil2d({ ...opts, numLobes: 6 });
            }
        },
        /**
         * Builds an octofoil opening using a given 2d cross-section profile
         * @memberof details.foils
         * @instance
         * @param {Object} opts
         * @param {number} opts.radius - radius of container circle
         * @param {string} opts.lobeRadiusType - "inSlice", "halfRadius", "mean"
         * @param {boolean} opts.cutCentre - if true, cuts a circular hole in centre of opening (only for 3D)
         * @param {geom2.Geom2} geomProfile - 2D cross-section profile
         */
        octofoil: (opts, geomProfile) => {
            if (geomProfile) {
                return buildFoil3d({ ...opts, numLobes: 8 }, geomProfile);
            } else {
                return buildFoil2d({ ...opts, numLobes: 8 });
            }
        },
    }
}

module.exports = { init: foilBuilder };