Source: models/prefab/mesh-3d.js

"use strict"

/**
 * ...
 * @memberof models.prefab
 * @namespace mesh3d
 */

//-----------
// TO-DO
//---------------------
// - Cylinders with rounded corners
//---------------------

const superPrimsMeshInit = ({ lib, swLib }) => {
    const { cuboid, cylinder, triangle, rectangle } = lib.primitives
    const { translate, rotate, align, mirror } = lib.transforms
    const { subtract, union } = lib.booleans
    const { measureBoundingBox } = lib.measurements
    const { extrudeRotate, extrudeLinear } = lib.extrusions
    const { TAU } = lib.maths.constants

    const { maths, geometry, position } = swLib.core

    const getPunchPoints = (pattern, length, width, radius) => {
        let punchPoints = geometry.getTriangularPtsInArea(length, width, radius)
        if (pattern === 'square') {
            punchPoints = geometry.getSquarePtsInArea(length, width, radius)
        }
        return punchPoints
    }

    /**
       * Generates an edge flange profile
       * @param {string} type - "inset" or "offset"
       * @param {number} width 
       * @param {number} thickness 
       * @param {string[]} flipOpts - array of options for flipping ("vertical" or "horizontal")
       */
    const edgeFlange = (type = 'inset', width, thickness, flipOpts = []) => {
        let triangleAlignOpts = {}
        let triangleMirrorOpts = null
        let bearingSurfaceAlignOpts = {}
        let mirrorOpts = null

        const height = width * 2;

        if (type === 'inset') {
            triangleAlignOpts = { modes: ['max', 'min', 'center'], relativeTo: [0, 0, 0] }
            bearingSurfaceAlignOpts = { modes: ['max', 'max', 'center'], relativeTo: [0, 0, 0] }

            if (flipOpts.includes('vertical')) {
                triangleAlignOpts.modes = ['max', 'max', 'center']
                bearingSurfaceAlignOpts.modes = ['max', 'min', 'center']
                triangleMirrorOpts = { normal: [0, 1, 0], origin: [0, -height - thickness, 0] }
            }
        } else if (type === 'offset') {
            triangleAlignOpts = { modes: ['min', 'min', 'center'], relativeTo: [0, 0, 0] }
            bearingSurfaceAlignOpts = { modes: ['min', 'max', 'center'], relativeTo: [0, 0, 0] }
            mirrorOpts = { normal: [1, 0, 0] }

            if (flipOpts.includes('vertical')) {
                triangleAlignOpts.modes = ['max', 'max', 'center']
                bearingSurfaceAlignOpts.modes = ['max', 'min', 'center']
                triangleMirrorOpts = { normal: [0, 1, 0], origin: [0, -height - thickness, 0] }
            }
        } else {
            return null;
        }

        let triangleProfile = triangle({ type: 'SAS', values: [width, TAU / 4, height] });
        if (triangleMirrorOpts != null) {
            triangleProfile = mirror(triangleMirrorOpts, triangleProfile)
        }

        const triangleSection = align(
            triangleAlignOpts,
            triangleProfile
        )

        const bearingSurface = align(
            bearingSurfaceAlignOpts,
            rectangle({ size: [width, thickness] })
        )

        let finalShape = union(bearingSurface, triangleSection);
        if (mirrorOpts != null) {
            finalShape = mirror(mirrorOpts, finalShape)
        }

        return align({ modes: ['center', 'center', 'center'] }, finalShape)
    }

    /**
     * Builds a flat mesh panel model. Mesh thickness is determined by `size[2]`
     * @memberof models.prefab.mesh3d.mesh
     * @param {*} opts 
     * @param {number[]} opts.size
     * @param {Number} opts.radius - radius
     * @param {Number} opts.segments - # of segments in mesh holes
     * @param {Number} opts.edgeMargin - distance between edges and mesh holes
     * @param {String} opts.pattern - 'tri' (default) or 'square'
     * @param {String} opts.patternMode - 'contain' (default) or 'fill'
     * @returns ...
     */
    const meshPanel = ({
        size,
        radius,
        segments = 16,
        edgeMargin,
        pattern = 'tri',
        patternMode = 'contain',
        edgeInsets = [0, 0],
        edgeOffsets = [0, 0],
    }) => {
        const punchSpecs = {
            radius: radius,
            height: size[2] * 2,
            segments,
        }

        const meshSpecs = {
            radius: radius + (size[2] / 2),
            edgeMargin: edgeMargin || radius + size[2],
        }
        meshSpecs.length = size[0] - (meshSpecs.edgeMargin * 2)
        meshSpecs.width = size[1] - (meshSpecs.edgeMargin * 2)
        meshSpecs.height = size[2]

        let outputPanel = null
        const basePlate = cuboid({ size });
        const basePlateCoords = position.cuboid.getCuboidCoords(basePlate)
        const basePlateCtrlPoints = position.cuboid.getCuboidCtrlPoints(basePlate)
        let baseShape = basePlate

        const hasInset = edgeInsets.some(insetVal => insetVal > 0)
        const hasOffset = edgeOffsets.some(offsetVal => offsetVal > 0)

        if (hasInset) {
            edgeInsets.forEach((insetWidth, idx) => {
                if (insetWidth === 0) {
                    return
                }
                const isTop = idx === 0;
                let insetSectionAlignOpts = {};
                let insetFlangeAlignOpts = {};
                const flipOpts = []
                if (isTop) {
                    insetSectionAlignOpts = { modes: ['min', 'min', 'max'] }
                    insetFlangeAlignOpts = { modes: ['center', 'max', 'min'], relativeTo: [0, basePlateCoords.back, basePlateCoords.top], }
                    flipOpts.push('vertical')
                } else {
                    insetSectionAlignOpts = { modes: ['min', 'min', 'min'] };
                    insetFlangeAlignOpts = { modes: ['center', 'min', 'min'], relativeTo: [0, basePlateCoords.front, basePlateCoords.top], };
                }
                const insetSection = align(
                    insetSectionAlignOpts,
                    edgeFlange('inset', insetWidth, 0.5, flipOpts)
                )
                const insetReinforcement = align(
                    insetFlangeAlignOpts,
                    rotate([0, TAU / 4, 0], extrudeLinear({ height: size[0] }, insetSection))
                )

                baseShape = union(insetReinforcement, baseShape)
            })
        }

        if (hasOffset) {
            edgeOffsets.forEach((offsetWidth, idx) => {
                if (offsetWidth === 0) {
                    return
                }
                const isTop = idx === 0;
                let offsetSectionAlignOpts = {};
                let offsetFlangeAlignOpts = {};
                const flipOpts = []
                if (isTop) {
                    offsetSectionAlignOpts = { modes: ['min', 'min', 'max'] }
                    offsetFlangeAlignOpts = { modes: ['center', 'max', 'max'], relativeTo: [0, basePlateCoords.back, basePlateCoords.bottom], }
                    flipOpts.push('vertical')
                } else {
                    offsetSectionAlignOpts = { modes: ['min', 'min', 'min'] };
                    offsetFlangeAlignOpts = { modes: ['center', 'min', 'max'], relativeTo: [0, basePlateCoords.front, basePlateCoords.bottom], };
                }
                const offsetSection = align(
                    offsetSectionAlignOpts,
                    edgeFlange('offset', offsetWidth, 0.5, flipOpts)
                )
                const offsetReinforcement = align(
                    offsetFlangeAlignOpts,
                    rotate([0, TAU / 4, 0], extrudeLinear({ height: size[0] }, offsetSection))
                )

                baseShape = union(offsetReinforcement, baseShape)
            })
        }

        const parts = [baseShape]
        const punch = cylinder(punchSpecs);

        if (patternMode === 'contain') {
            // pattern is neatly contained in the bounding rectangle
            const punchPoints = getPunchPoints(pattern, meshSpecs.length, meshSpecs.width, meshSpecs.radius)
            punchPoints.forEach(punchPt => {
                parts.push(translate([punchPt.x, punchPt.y, 0], punch))
            });

            outputPanel = subtract(...parts)
        }

        else if (patternMode === 'fill') {
            // pattern extends outside the bounding rectangle, and gets cut off
            const punchPoints = getPunchPoints(
                pattern,
                size[0] + (meshSpecs.radius * 2),
                size[1] + (meshSpecs.radius * 2),
                meshSpecs.radius
            )
            punchPoints.forEach(punchPt => {
                parts.push(translate([punchPt.x, punchPt.y, 0], punch))
            });

            const punchedPanel = subtract(...parts)
            const panelEdge = subtract(
                baseShape,
                cuboid({
                    size: [
                        size[0] - (meshSpecs.edgeMargin * 2),
                        size[1] - (meshSpecs.edgeMargin * 2),
                        size[2] * 1.5,
                    ]
                })
            );

            outputPanel = union(punchedPanel, panelEdge)
        }

        return outputPanel
    }

    /**
     * Builds a flat mesh panel model. Mesh thickness is determined by `size[2]`
     * @memberof models.prefab.mesh3d.mesh
     * @param {*} param0 
     * @returns ...
     */
    const meshCuboid = ({
        size,
        meshPanelThickness,
        radius,
        segments = 16,
        edgeMargin,
        edgeInsets = [0, 0],
        edgeOffsets = [0, 0],
        pattern = 'tri',
        openTop = false,
        openBottom = false,
    }) => {
        const specs = {
            meshPanelThickness: meshPanelThickness || maths.inchesToMm(3 / 32),
            edgeMargin: edgeMargin || radius * 2,
        }

        const baseCuboid = cuboid({ size })
        const baseCuboidBb = measureBoundingBox(baseCuboid);
        const baseCuboidCoords = position.cuboid.getCuboidCoords(baseCuboid)
        const baseCuboidCtrlPoints = position.cuboid.getCuboidCtrlPoints(baseCuboid)

        // [x,y,z (default)]
        const mPanelSpecs = [
            {
                size: [size[1], size[2], specs.meshPanelThickness],
                rotation: [TAU / 4, 0, TAU / 4],
                scaleFactors: [size[0] / specs.meshPanelThickness * 3, 1, 1],
            },
            {
                size: [size[0], size[2], specs.meshPanelThickness],
                rotation: [TAU / -4, 0, 0],
                scaleFactors: [1, size[1] / specs.meshPanelThickness * 3, 1],
            },
            {
                size: [size[0], size[1], specs.meshPanelThickness],
                rotation: [0, 0, 0],
                scaleFactors: [1, 1, size[2] / specs.meshPanelThickness * 3],
            },
        ]

        const parts = []
        mPanelSpecs.forEach((mPanelSpec, idx) => {
            const rotatedPanel = rotate(mPanelSpec.rotation, meshPanel({
                size: mPanelSpec.size,
                radius,
                segments,
                edgeMargin: specs.edgeMargin,
                pattern,
                edgeInsets,
                edgeOffsets
            }));

            const hasInset = edgeInsets.some(insetVal => insetVal > 0)
            const hasOffset = edgeOffsets.some(offsetVal => offsetVal > 0)

            let maxInset = 0;
            if (hasInset) {
                maxInset = Math.max(...edgeInsets)
            }
            let maxOffset = 0;
            if (hasOffset) {
                maxOffset = Math.max(...edgeOffsets)
            }

            let flipNormal = [0, 0, 0]
            let aligmentModeA = ['center', 'center', 'min']
            let aligmentModeB = ['center', 'center', 'max']
            let relPointA = baseCuboidCtrlPoints.f6
            let relPointB = baseCuboidCtrlPoints.f5

            // [left, front, bottom]
            // [right, back, top]
            if (idx === 0) {
                flipNormal = [1, 0, 0]
                aligmentModeA = ['min', 'center', 'center']
                aligmentModeB = ['max', 'center', 'center']
                relPointA = baseCuboidCtrlPoints.f2
                relPointA[0] = relPointA[0] - maxOffset
                relPointB = baseCuboidCtrlPoints.f1
                relPointB[0] = relPointB[0] + maxOffset
            } else if (idx === 1) {
                flipNormal = [0, 1, 0]
                aligmentModeA = ['center', 'min', 'center']
                aligmentModeB = ['center', 'max', 'center']
                relPointA = baseCuboidCtrlPoints.f4
                relPointA[1] = relPointA[1] - maxOffset
                relPointB = baseCuboidCtrlPoints.f3
                relPointB[1] = relPointB[1] + maxOffset
            }

            const flippedPanel = mirror({ normal: flipNormal }, rotatedPanel)

            // [left, front, bottom]
            const skipBottom = openBottom && idx == 2;
            if (!skipBottom) {
                parts.push(align({ modes: aligmentModeA, relativeTo: relPointA }, rotatedPanel))
            }

            // [right, back, top]
            const skipTop = openTop && idx == 2;
            if (!skipTop) {
                parts.push(align({ modes: aligmentModeB, relativeTo: relPointB }, flippedPanel))
            }
        });

        let mainShape = union(...parts)

        return mainShape;
    }

    /**
     * ...
     * @memberof models.prefab.mesh3d.mesh
     * @param {*} opts 
     * @returns ...
     */
    const meshCylinder = ({
        radius,
        height,
        segments = 16,
        thickness = 2,
        edgeMargin,
        edgeInsets = [0, 0],
        edgeOffsets = [0, 0],
        meshRadius,
        meshMinWidth,
        meshSegments = 16,
    }) => {
        const specs = {
            edgeMargin: edgeMargin || meshMinWidth
        }

        const baseCylinder = cylinder({ radius, height, segments });
        const cutCylinder = cylinder({ radius: radius - thickness, height: height + radius, segments });
        const baseShape = align(
            { modes: ['center', 'center', 'min'] },
            subtract(baseCylinder, cutCylinder)
        )
        const circumference = TAU * radius;

        let numPunches = 1;
        let circCtr = numPunches * meshRadius;
        while (circCtr < circumference) {
            circCtr += meshRadius * 2 + meshMinWidth;
            if (circCtr < circumference) {
                numPunches += 1
            }
        }

        const punches = []
        for (let idx = 0; idx < numPunches; idx++) {
            const currAngle = idx / numPunches * TAU
            punches.push(rotate([0, 0, currAngle], align(
                { modes: ['min', 'center', 'center'] },
                rotate(
                    [0, Math.PI / 2, 0],
                    cylinder({ radius: meshRadius, height: radius * 2, segments: meshSegments })
                )
            )))
        }

        let numPunchDiscs = 1;
        let htCtr = 0
        let remainingHt = height
        let discHeightInterval = (meshRadius * 2 + meshMinWidth) * 0.86603
        while (htCtr < height) {
            htCtr += discHeightInterval;
            if (htCtr < height) {
                numPunchDiscs += 1
                remainingHt -= discHeightInterval;
            }
        }

        const completePunch = align(
            { modes: ['center', 'center', 'min'], relativeTo: [0, 0, (specs.edgeMargin + remainingHt) / 2 * 0.86603] },
            union(...punches)
        )

        let reinforcedTube = baseShape;

        const hasInset = edgeInsets.some(insetVal => insetVal > 0)
        const hasOffset = edgeOffsets.some(offsetVal => offsetVal > 0)

        if (hasInset) {
            edgeInsets.forEach((insetWidth, idx) => {
                if (insetWidth === 0) {
                    return
                }
                const isTop = idx === 0;
                let sectionAlignOpts = {}
                let ringAlignOpts = {}
                const flipOpts = []
                if (isTop) {
                    sectionAlignOpts = { modes: ['min', 'min', 'max'], relativeTo: [0, 0, height], }
                    ringAlignOpts = { modes: ['center', 'center', 'max'], relativeTo: [0, 0, height], }
                    flipOpts.push('vertical')
                } else {
                    sectionAlignOpts = { modes: ['min', 'min', 'min'], relativeTo: [0, 0, 0], }
                    ringAlignOpts = { modes: ['center', 'center', 'min'], relativeTo: [0, 0, 0], }
                }
                const insetSection = align(
                    sectionAlignOpts,
                    edgeFlange('inset', insetWidth, 0.5, flipOpts)
                )
                const insetRing = align(
                    ringAlignOpts,
                    extrudeRotate({ segments }, translate([radius - thickness - insetWidth, 0, 0], insetSection))
                );

                reinforcedTube = union(reinforcedTube, insetRing)
            })
        }

        if (hasOffset) {
            edgeOffsets.forEach((offsetWidth, idx) => {
                if (offsetWidth === 0) {
                    return
                }
                const isTop = idx === 0;
                let sectionAlignOpts = {};
                let ringAlignOpts = {};
                const flipOpts = []
                if (isTop) {
                    sectionAlignOpts = { modes: ['min', 'min', 'max'], relativeTo: [0, 0, height], }
                    ringAlignOpts = { modes: ['center', 'center', 'max'], relativeTo: [0, 0, height], }
                    flipOpts.push('vertical')
                } else {
                    sectionAlignOpts = { modes: ['min', 'min', 'min'], relativeTo: [0, 0, 0], };
                    ringAlignOpts = { modes: ['center', 'center', 'min'], relativeTo: [0, 0, 0], };
                }
                const offsetSection = align(
                    sectionAlignOpts,
                    edgeFlange('offset', offsetWidth, 0.5, flipOpts)
                )
                const offsetRing = align(
                    ringAlignOpts,
                    extrudeRotate({ segments }, translate([radius, 0, 0], offsetSection))
                );

                reinforcedTube = union(reinforcedTube, offsetRing)
            })
        }

        let punchedTube = subtract(reinforcedTube, completePunch)
        for (let idx = 0; idx < numPunchDiscs - 1; idx++) {
            const zOffset = discHeightInterval * idx;
            let discRotation = [0, 0, 0]
            if (maths.isOdd(idx)) {
                discRotation = [0, 0, TAU / (numPunches * 2)]
            }
            punchedTube = subtract(punchedTube, translate(
                [0, 0, zOffset],
                rotate(discRotation, completePunch))
            )
        }

        return punchedTube;
    }

    return {
        meshPanel,
        meshCuboid,
        meshCylinder,
    }
}

module.exports = { init: superPrimsMeshInit };