Source: swcad-js-components/src/routed-shapes/index.js

"use strict"

const routedShapesInit = ({ jscad, swcadJs }) => {
    const {
        cylinderElliptic,
        cuboid,
    } = jscad.primitives

    const {
        translate,
        rotate,
    } = jscad.transforms

    const {
        subtract,
        union,
    } = jscad.booleans

    const {
        extrudeLinear,
        project
    } = jscad.extrusions

    const { toOutlines } = jscad.geometries.geom2
    const { TAU } = jscad.maths.constants

    const {
        math,
        geometry,
        position,
    } = swcadJs.calcs

    const {
        standards,
    } = swcadJs.data

    const {
        beadsBits: beadsBits2d,
    } = swcadJs.profiles

    const {
        beadsBits,
    } = swcadJs.components


    //==============================================================================


    /**
     * Builds default values and opts for the model
     * @param {*} opts 
     * @returns default values and opts
     * @memberof components.routedShapes
     * @access private
     */
    const modelDefaults = () => {
        /** Specific value declarations */
        const defaultValues = {
            dims: {
                size: [
                    math.inchesToMm(3),
                    math.inchesToMm(6),
                    math.inchesToMm(2),
                ],
                height: math.inchesToMm(2),
                radius: [math.inchesToMm(1.5), math.inchesToMm(3)],
            },
            points: {
                centre: [0, 0, 0]
            },
            types: {
                default: standards.types.TYPE_DEFAULT,
                alt: standards.types.TYPE_ALT,
            },
            topBit: {
                type: 'chamfer',
                radius1: math.inchesToMm(1 / 2),
                radius2: math.inchesToMm(3 / 4),
                offset1: 0,
                offset2: 0,
                offset3: 0,
                offset4: 0,
            },
            bottomBit: {
                type: 'cove',
                radius1: math.inchesToMm(1 / 2),
                radius2: math.inchesToMm(3 / 4),
                offset1: 0,
                offset2: 0,
                offset3: 0,
                offset4: 0,
            }
        }

        /** Options used by SW models */
        const standardOpts = {
            type: defaultValues.types.default.id,
            scale: 1,
            interfaceThickness: standards.INTERFACE_THICKNESS,
            fitGap: standards.FIT_GAP,
        }

        /** Computed values for option defaults */
        const defaultOpts = {
            ...standardOpts,
            size: defaultValues.dims.size,
            height: defaultValues.dims.height,
            radius: defaultValues.dims.radius,
            topBit: defaultValues.topBit.type,
            topBitOpts: defaultValues.topBit,
            bottomBit: defaultValues.bottomBit.type,
            bottomBitOpts: defaultValues.bottomBit,
        }

        return {
            opts: defaultOpts,
            vals: defaultValues,
        }
    }


    //------------------------------------------------------------------------------


    /**
     * Initializes cuboid options with user input
     * @param {*} opts 
     * @returns model properties
     * @memberof components.routedShapes
     * @access private
     */
    const modelCuboidOpts = (opts) => {
        const defaults = modelDefaults()

        // User options
        const {
            size = defaults.opts.size,
            topBit = defaults.opts.topBit,
            topBitOpts = defaults.opts.topBitOpts,
            bottomBit = defaults.opts.bottomBit,
            bottomBitOpts = defaults.opts.bottomBitOpts,
            type = defaults.opts.type,
            scale = defaults.opts.scale,
            interfaceThickness = defaults.opts.interfaceThickness,
            fitGap = defaults.opts.fitGap,
        } = opts

        const stdOpts = {
            type,
            scale,
            interfaceThickness,
            fitGap,
        }

        const initOpts = {
            size,
            topBit,
            topBitOpts,
            bottomBit,
            bottomBitOpts,
            ...stdOpts,
        }


        return initOpts
    }


    /**
     * Initializes cylinder elliptic options with user input
     * @param {*} opts 
     * @returns model properties
     * @memberof components.routedShapes
     * @access private
     */
    const modelCylinderEllipticOpts = (opts) => {
        const defaults = modelDefaults()

        // User options
        const {
            height = defaults.opts.height,
            radius = defaults.opts.radius,
            topBit = defaults.opts.topBit,
            topBitOpts = defaults.opts.topBitOpts,
            bottomBit = defaults.opts.bottomBit,
            bottomBitOpts = defaults.opts.bottomBitOpts,
            type = defaults.opts.type,
            scale = defaults.opts.scale,
            interfaceThickness = defaults.opts.interfaceThickness,
            fitGap = defaults.opts.fitGap,
        } = opts

        const stdOpts = {
            type,
            scale,
            interfaceThickness,
            fitGap,
        }

        const initOpts = {
            height,
            radius,
            topBit,
            topBitOpts,
            bottomBit,
            bottomBitOpts,
            ...stdOpts,
        }


        return initOpts
    }


    //------------------------------------------------------------------------------


    /**
     * Builds model properties from the given opts
     * @param {*} opts 
     * @returns model properties
     * @memberof components.routedShapes
     * @access private
     */
    const modelProps = (opts) => {
        const defaults = modelDefaults()

        const {
            size,
            height,
            radius,
            topBit,
            topBitOpts,
            bottomBit,
            bottomBitOpts,
            type,
            scale,
            interfaceThickness,
            fitGap,
        } = opts

        /* ----------------------------------------
        * Prop calculations
        * ------------------------------------- */

        // ...

        /* ----------------------------------------
        * Preparing Model Properties, Dimensions
        * ------------------------------------- */

        /** Constant values for model */
        const modelConstants = {
            type,
            scale,
        }

        /** Derived user options for the model */
        const modelOpts = {
            type,
            scale,
            topBit,
            bottomBit,
        }

        /** Various dimensions for model */
        const modelDims = {
            size,
            height,
            radius,
            topBit: topBitOpts,
            bottomBit: bottomBitOpts,
            interfaceThickness,
            fitGap,
        }

        /** Various key points for model */
        const modelPoints = {
            centre: defaults.vals.points.centre,
        }

        /** Components used by model */
        const modelComponents = {
        }

        /* ---------------------------------------------
        *  Model Properties
        * ----------------------------------------------
        * Properties accessible to all model functions.
        * --------------------------------------------- */

        const modelProperties = {
            metadata: {
                id: '9999',
                name: 'New Model',
                project: 'New Project',
                author: 'Somebody Somewhere',
                organization: 'Salvador Workshop',
                client: null,
            },
            constants: modelConstants,
            opts: modelOpts,
            dims: modelDims,
            points: modelPoints,
            components: modelComponents,
        }


        return modelProperties
    }


    /**
     * Generates cut path for a given router bit
     * @param {*} outlinePts 
     * @param {*} bitProfile 
     * @param {*} bitModel 
     * @returns geom3
     * @memberof components.routedShapes
     * @access private
     */
    const generateRouterCutPath = (outlinePts, bitProfile, bitModel) => {
        const closedOutlinePts = [
            ...outlinePts,
            outlinePts[0]
        ]
        const cutPoints = outlinePts.map(outlinePt => {
            return translate(
                [
                    outlinePt[0],
                    outlinePt[1],
                    0,
                ],
                bitModel
            )
        })

        const lineData = geometry.getLineDataFromOutlinePoints(closedOutlinePts)

        const lineModels = lineData.map(lData => {
            let lineModel = extrudeLinear({ height: lData.length }, bitProfile)
            lineModel = rotate(
                [TAU / 4, 0, TAU / -4],
                lineModel
            )
            lineModel = position.ctr(lineModel)

            let adjLineModel = rotate([0, 0, lData.angle], lineModel)
            adjLineModel = translate([
                lData.mid[0],
                lData.mid[1],
                0,
            ], adjLineModel)

            return adjLineModel
        })

        const cutLines = lineModels

        const cutPath = union(
            ...cutPoints,
            ...cutLines,
        )

        return cutPath
    }


    /**
     * Builds the router cut path from the given details
     * @param {*} outlinePts 
     * @param {*} bitType 
     * @param {*} bitOpts 
     * @returns geom3
     * @memberof components.routedShapes
     * @access private
     */
    const routerCut = (outlinePts, bitType, bitOpts) => {
        let baseRouterBitProfile = null
        let baseRouterBit = null
        /**
         * A 'naive' `cutPathGenMode` allows this function to use a simple `hullChain()`.
         * The complex variant takes a bit more work to build.
         * */
        let cutPathGenMode = 'complex'

        switch (bitType) {
            case 'cove':
                baseRouterBitProfile = beadsBits2d.corner.cove(bitOpts)[0]
                baseRouterBit = beadsBits.corner.cove(bitOpts)[0]
                break;
            case 'roundOver':
                baseRouterBitProfile = beadsBits2d.corner.roundOver(bitOpts)[0]
                baseRouterBit = beadsBits.corner.roundOver(bitOpts)[0]
                break;
            case 'rabbet':
                baseRouterBitProfile = beadsBits2d.corner.rabbet(bitOpts)[0]
                baseRouterBit = beadsBits.corner.rabbet(bitOpts)[0]
                break;
            case 'chamfer':
            default:
                baseRouterBitProfile = beadsBits2d.corner.chamfer(bitOpts)[0]
                baseRouterBit = beadsBits.corner.chamfer(bitOpts)[0]
                cutPathGenMode = 'naive'
                break;
        }

        const cutPath = generateRouterCutPath(outlinePts, baseRouterBitProfile, baseRouterBit)

        return cutPath
    }


    //------------------------------------------------------------------------------


    /**
     * Routed cuboid
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof components.routedShapes
     */
    const routedCuboid = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelCuboidOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            topBit: topBitType,
            bottomBit: bottomBitType,
        } = modelProperties.opts

        const {
            size,
            topBit,
            bottomBit,
        } = modelProperties.dims

        const baseShape = cuboid({
            size,
        })
        const baseShapeProfile = project({}, baseShape)
        const baseShapeProfileOutlinePts = toOutlines(baseShapeProfile)[0]

        const cutPathTop = routerCut(baseShapeProfileOutlinePts, topBitType, topBit)
        const cutPathBottom = routerCut(baseShapeProfileOutlinePts, bottomBitType, bottomBit)

        let cutShape = baseShape
        const hasTop = !!topBitType && topBitType != 'none'
        const hasBottom = !!bottomBitType && bottomBitType != 'none'

        if (hasTop) {
            cutShape = subtract(
                cutShape,
                translate([0, 0, size[2] / 2], cutPathTop),
            )
        }
        if (hasBottom) {
            cutShape = subtract(
                cutShape,
                translate([0, 0, size[2] / -2], cutPathBottom),
            )
        }

        const mainModel = cutShape
        const modelParts = {
            mainModel,
            cutPathTop,
            cutPathBottom,
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * Routed elliptical cylinder
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof components.routedShapes
     */
    const routedCylinderElliptic = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelCylinderEllipticOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            topBit: topBitType,
            bottomBit: bottomBitType,
        } = modelProperties.opts

        const {
            height,
            radius,
            topBit,
            bottomBit,
        } = modelProperties.dims

        const baseShape = cylinderElliptic({
            height,
            startRadius: radius,
            endRadius: radius,
        })
        const baseShapeProfile = project({}, baseShape)
        const baseShapeProfileOutlinePts = toOutlines(baseShapeProfile)[0]

        const cutPathTop = routerCut(baseShapeProfileOutlinePts, topBitType, topBit)
        const cutPathBottom = routerCut(baseShapeProfileOutlinePts, bottomBitType, bottomBit)

        let cutShape = baseShape
        const hasTop = !!topBitType && topBitType != 'none'
        const hasBottom = !!bottomBitType && bottomBitType != 'none'

        if (hasTop) {
            cutShape = subtract(
                cutShape,
                translate([0, 0, height / 2], cutPathTop),
            )
        }
        if (hasBottom) {
            cutShape = subtract(
                cutShape,
                translate([0, 0, height / -2], cutPathBottom),
            )
        }

        const mainModel = cutShape
        const modelParts = {
            mainModel,
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * @namespace routedShapes
     * @memberof components
     */
    const routedShapes = {
        routedCuboid,
        routedCylinderElliptic,
    }

    return routedShapes
}

module.exports = {
    init: routedShapesInit
}