Source: swcad-js-components/src/trim-family-frame/index.js

"use strict"

const trimFamilyFrameInit = ({ jscad, swcadJs }) => {
    const {
        cylinder,
        cuboid,
        roundedCuboid,
        triangle,
    } = jscad.primitives

    const {
        align,
        translate,
        rotate,
    } = jscad.transforms

    const {
        intersect,
        subtract,
        union,
    } = jscad.booleans

    const {
        extrudeLinear,
        project
    } = jscad.extrusions

    const {
        measureBoundingBox,
    } = jscad.measurements

    const {
        math,
        position,
    } = swcadJs.calcs

    const {
        standards,
    } = swcadJs.data

    const {
        trim,
    } = swcadJs.profiles


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


    /**
     * Builds default values and opts for the model
     * @param {*} opts 
     * @returns default values and opts
     * @memberof components.trimFamilyFrame
     * @access private
     */
    const modelDefaults = () => {
        /** Specific value declarations */
        const cornerThickness = math.inchesToMm(1 / 4)

        const defaultValues = {
            opts: {
                trimType: 'plain',
                frameForm: 'half',
                trimJointType: 'plain',
                cornerType: 'plain',
                backingType: 'none',
                ornaments: {
                    trimLevels: 1,
                },
            },
            dims: {
                size: [math.inchesToMm(2.5), math.inchesToMm(3)],
                interfaceThickness: standards.INTERFACE_THICKNESS,
                fitGap: standards.FIT_GAP,
                backingThickness: standards.INTERFACE_THICKNESS,
                trimWidth: math.inchesToMm(3 / 8),
                trimThickness: math.inchesToMm(3 / 16),
                trimJointWidth: math.inchesToMm(1 / 2),
                trimJointThickness: math.inchesToMm(7 / 32),
                trimJointLength: math.inchesToMm(1 / 4),
                cornerWidth: math.inchesToMm(5 / 8),
                cornerThickness,
                cornerRadius: cornerThickness / 2,
                rabbetWidth: math.inchesToMm(1 / 8),
                rabbetDepth: math.inchesToMm(1 / 8),
            },
            points: {
                centre: [0, 0, 0]
            },
            types: {
                default: standards.types.TYPE_DEFAULT,
                alt: standards.types.TYPE_ALT,
            },
        }

        /** 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,
            trimType: defaultValues.opts.trimType,
            frameForm: defaultValues.opts.frameForm,
            trimJointType: defaultValues.opts.trimJointType,
            cornerType: defaultValues.opts.cornerType,
            backingType: defaultValues.opts.backingType,
            backingThickness: defaultValues.dims.backingThickness,
            ornaments: defaultValues.opts.ornaments,
            trimWidth: defaultValues.dims.trimWidth,
            trimThickness: defaultValues.dims.trimThickness,
            cornerWidth: defaultValues.dims.cornerWidth,
            cornerThickness: defaultValues.dims.cornerThickness,
            cornerRadius: defaultValues.dims.cornerRadius,
            trimJointWidth: defaultValues.dims.trimJointWidth,
            trimJointThickness: defaultValues.dims.trimJointThickness,
            trimJointLength: defaultValues.dims.trimJointLength,
            rabbetWidth: defaultValues.dims.rabbetWidth,
            rabbetDepth: defaultValues.dims.rabbetDepth,
        }

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


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


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

        // User options
        const {
            type = defaults.opts.type,
            scale = defaults.opts.scale,
            interfaceThickness = defaults.opts.interfaceThickness,
            fitGap = defaults.opts.fitGap,
            size = defaults.opts.size,
            trimType = defaults.opts.trimType,
            frameForm = defaults.opts.frameForm,
            trimJointType = defaults.opts.trimJointType,
            cornerType = defaults.opts.cornerType,
            backingType = defaults.opts.backingType,
            backingThickness = defaults.opts.backingThickness,
            ornaments = defaults.opts.ornaments,
            trimWidth = defaults.opts.trimWidth,
            trimThickness = defaults.opts.trimThickness,
            cornerWidth = defaults.opts.cornerWidth,
            cornerThickness = defaults.opts.cornerThickness,
            cornerRadius = defaults.opts.cornerRadius,
            trimJointWidth = defaults.opts.trimJointWidth,
            trimJointThickness = defaults.opts.trimJointThickness,
            trimJointLength = defaults.opts.trimJointLength,
            rabbetWidth = defaults.opts.rabbetWidth,
            rabbetDepth = defaults.opts.rabbetDepth,
        } = opts

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

        const initOpts = {
            size,
            trimType,
            frameForm,
            trimJointType,
            cornerType,
            backingType,
            backingThickness,
            ornaments,
            trimWidth,
            trimThickness,
            cornerWidth,
            cornerThickness,
            cornerRadius,
            trimJointWidth,
            trimJointThickness,
            trimJointLength,
            rabbetWidth,
            rabbetDepth,
            ...stdOpts,
        }


        return initOpts
    }

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


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

        const {
            size,
            type,
            scale,
            interfaceThickness,
            fitGap,
            trimType,
            frameForm,
            trimJointType,
            cornerType,
            backingType,
            backingThickness,
            ornaments,
            trimWidth,
            trimThickness,
            cornerWidth,
            cornerThickness,
            cornerRadius,
            trimJointWidth,
            trimJointThickness,
            trimJointLength,
            rabbetWidth,
            rabbetDepth,
        } = opts

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

        const width = size[0]
        const depth = size[1]

        const hasCorner = cornerType && cornerType != 'none'
        const hasCornerJoint = trimJointType && trimJointType != 'none'

        const cWidth = hasCorner ? cornerWidth : 0
        const cThickness = hasCorner ? cornerThickness : 0
        const cRadius = hasCorner ? cornerRadius : 0
        const cornerDiff = cWidth - trimWidth
        const tJointWidth = hasCorner && hasCornerJoint ? trimJointWidth : 0
        const tJointThickness = hasCorner && hasCornerJoint ? trimJointThickness : 0
        const tJointLength = hasCorner && hasCornerJoint ? trimJointLength : 0

        const cornerWidthOffset = hasCorner ? cWidth * 2 : 0
        const trimJointOffset = hasCorner && hasCornerJoint ? trimJointLength * 2 : 0

        let trimOffset = 0
        let trimCentreOffset = (trimWidth / 2)

        if (cornerDiff > 0) {
            trimOffset = cornerDiff / 2
            trimCentreOffset = trimOffset + (trimWidth / 2)
        }

        const trimLengthX = width - (trimOffset * 2)
        const trimLengthY = depth - (trimOffset * 2)

        const backingWidth = width - (trimCentreOffset * 2)
        const backingHeight = depth - (trimCentreOffset * 2)

        const frameInnerWidth = trimLengthX - (trimWidth * 2)
        const frameInnerHeight = trimLengthY - (trimWidth * 2)

        const cornerPoints = [
            [0, 0, 0],
            [width, 0, 0],
            [0, depth, 0],
            [width, depth, 0],
        ]

        const centrePoint = [width / 2, depth / 2, 0]

        const trimPoints = [
            [centrePoint[0], trimOffset, 0],
            [centrePoint[0], depth - trimOffset, 0],
            [trimOffset, centrePoint[1], 0],
            [width - trimOffset, centrePoint[1], 0],
        ]

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

        /** Constant values for model */
        const modelConstants = {
        }

        /** Derived user options for the model */
        const modelOpts = {
            type,
            scale,
            trimType,
            frameForm,
            trimJointType,
            cornerType,
            backingType,
            ornaments,
        }

        /** Various dimensions for model */
        const modelDims = {
            size,
            width,
            depth,
            interfaceThickness,
            fitGap,
            trimWidth,
            trimThickness,
            frameInnerWidth,
            frameInnerHeight,
            trimJointWidth: tJointWidth,
            trimJointThickness: tJointThickness,
            trimJointLength: tJointLength,
            cornerWidth: cWidth,
            cornerThickness: cThickness,
            cornerRadius: cRadius,
            backingThickness,
            cornerWidthOffset,
            trimJointOffset,
            trimLengthX,
            trimLengthY,
            trimOffset,
            trimCentreOffset,
            backingWidth,
            backingHeight,
            rabbetWidth,
            rabbetDepth,
        }

        /** Various key points for model */
        const modelPoints = {
            centre: centrePoint,
            corners: cornerPoints,
            trimEdges: trimPoints,
        }

        /** 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
    }


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


    /**
     * Rectangular frame using the system trim families
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof components
     * @author R. J. Salvador
     */
    const trimFamilyFrame = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        /* ----------------------------------------
         * Modelling, Component/Assembly Modules
         * ------------------------------------- */

        /**
     * Frame corner piece
     * @param {*} modelProps
     * @returns geom3 object. `null` if given invalid params or with `cornerType` = `'none'`
     */
        const frameCorner = (modelProps) => {
            const {
                opts,
                dims,
                points,
            } = modelProps

            let fCorner = null
            if (!opts.cornerType || opts.cornerType == 'none') {
                return fCorner
            } else {
                switch (opts.cornerType) {
                    case 'rounded':
                        fCorner = cylinder({
                            height: dims.cornerThickness,
                            radius: dims.cornerRadius || 0.5
                        })
                        break;
                    case 'plain':
                    default:
                        // default to cuboid
                        fCorner = cuboid({
                            size: [dims.cornerWidth, dims.cornerWidth, dims.cornerThickness]
                        })
                        break;
                }
            }

            return fCorner
        }


        /**
         * Frame corner joint piece
         * @param {*} modelProps
         * @returns geom3
         */
        const frameCornerJoint = (modelProps) => {
            const {
                opts,
                dims,
                points,
            } = modelProps

            let fCornerJoint = null
            if (!opts.trimJointType || opts.trimJointType == 'none') {
                return fCornerJoint
            } else {
                switch (opts.trimJointType) {
                    case 'rounded':
                        fCornerJoint = roundedCuboid({
                            size: [dims.trimJointWidth, dims.trimJointLength, dims.trimJointThickness],
                            radius: radius || 0.5
                        })
                        break;
                    case 'plain':
                    default:
                        // default to cuboid
                        fCornerJoint = cuboid({
                            size: [dims.trimJointWidth, dims.trimJointLength, dims.trimJointThickness]
                        })
                        break;
                }
            }

            return fCornerJoint
        }


        /**
         * Frame corner assembly
         * @param {*} modelProps
         * @returns geom3
         */
        const cornerFrameAssembly = (modelProps) => {
            const {
                opts,
                dims,
                points,
            } = modelProps

            if ((!opts.cornerType || opts.cornerType == 'none') && (!opts.trimJointType || opts.trimJointType == 'none')) {
                return null
            }

            let corner = frameCorner(modelProps)
            const cornerBbox = measureBoundingBox(corner)
            const cornerJoint = frameCornerJoint(modelProps)

            const jointX = align({
                modes: ['min', 'center', 'min'],
                relativeTo: [
                    cornerBbox[1][0],
                    0,
                    cornerBbox[0][2],
                ]
            }, rotate([0, 0, Math.PI / 2], cornerJoint))

            const jointY = align({
                modes: ['center', 'min', 'min'],
                relativeTo: [
                    0,
                    cornerBbox[1][1],
                    cornerBbox[0][2],
                ]
            }, cornerJoint)

            let cornerAssembly = corner

            if (jointX) {
                cornerAssembly = union(cornerAssembly, jointX)
            }
            if (jointY) {
                cornerAssembly = union(cornerAssembly, jointY)
            }

            return cornerAssembly
        }


        /**
         * Frame trim length
         * @param {*} modelProps
         * @returns geom3
         */
        const frameTrim = (modelProps, length) => {
            const {
                opts,
                dims,
                points,
            } = modelProps

            let fTrim = null
            if (!opts.trimType || opts.trimType == 'none') {
                return fTrim
            } else {
                let mitredTrimProfile = triangle({
                    type: 'ASA', values: [
                        Math.PI / 4,
                        length,
                        Math.PI / 4,
                    ]
                })
                mitredTrimProfile = rotate([0, 0, Math.PI / 2], mitredTrimProfile)
                let mitredTrimArea = extrudeLinear({ height: dims.trimThickness * 2 }, mitredTrimProfile)
                mitredTrimArea = translate([dims.trimWidth / 2, 0, -dims.trimThickness], mitredTrimArea)

                const unitHeight = dims.trimWidth / opts.ornaments.trimLevels
                const unitDepth = dims.trimThickness / opts.ornaments.trimLevels
                const trimSizeDims = [unitHeight, unitDepth]
                const trimStyle = 'dado'

                let trimSize = 'small'
                switch (opts.trimType) {
                    case 'aranea':
                        switch (opts.ornaments.trimLevels) {
                            case 3:
                                trimSize = 'largeOrn1'
                                break;
                            case 2:
                                trimSize = 'mediumOrn1'
                                break;
                            case 1:
                            default:
                                trimSize = 'smallOrn1'
                                break;
                        }

                        const tFamilyAranea = trim.aranea({
                            size: trimSizeDims,
                        });
                        let frameProfileAranea = tFamilyAranea[trimStyle][trimSize]
                        let araneaTrim = extrudeLinear({ height: length }, frameProfileAranea)
                        araneaTrim = rotate([Math.PI / -2, Math.PI / -2, 0], araneaTrim)

                        fTrim = extrudeLinear({ height: length }, araneaTrim)
                        break;
                    case 'bibliopoli':
                        switch (opts.ornaments.trimLevels) {
                            case 3:
                                trimSize = 'largeOrn1'
                                break;
                            case 2:
                                trimSize = 'mediumOrn1'
                                break;
                            case 1:
                            default:
                                trimSize = 'smallOrn1'
                                break;
                        }

                        const tFamilyBibliopoli = trim.bibliopoli({
                            size: trimSizeDims,
                        });
                        let frameProfileBibliopoli = tFamilyBibliopoli[trimStyle][trimSize]
                        let bibliopoliTrim = extrudeLinear({ height: length }, frameProfileBibliopoli)
                        bibliopoliTrim = rotate([Math.PI / -2, Math.PI / -2, 0], bibliopoliTrim)

                        fTrim = extrudeLinear({ height: length }, bibliopoliTrim)
                        break;
                    case 'catonis':
                        switch (opts.ornaments.trimLevels) {
                            case 3:
                                trimSize = 'largeOrn1'
                                break;
                            case 2:
                                trimSize = 'mediumOrn1'
                                break;
                            case 1:
                            default:
                                trimSize = 'smallOrn1'
                                break;
                        }

                        const tFamilyCatonis = trim.catonis({
                            size: trimSizeDims,
                        });
                        let frameProfileCatonis = tFamilyCatonis[trimStyle][trimSize]
                        let catonisTrim = extrudeLinear({ height: length }, frameProfileCatonis)
                        catonisTrim = rotate([Math.PI / -2, Math.PI / -2, 0], catonisTrim)

                        fTrim = extrudeLinear({ height: length }, catonisTrim)
                        break;
                    case 'rounded':
                        fTrim = roundedCuboid({
                            size: [dims.trimWidth, length, dims.trimThickness],
                            radius: dims.interfaceThickness
                        })
                        break;
                    case 'plain':
                    default:
                        // default to cuboid
                        fTrim = cuboid({
                            size: [dims.trimWidth, length, dims.trimThickness]
                        })
                        break;
                }

                fTrim = intersect(
                    fTrim,
                    mitredTrimArea,
                )
            }

            return fTrim
        }


        /**
         * Assembled trim
         * @param {*} modelProps
         * @returns geom3
         */
        const assembledTrim = (modelProps) => {
            const {
                opts,
                dims,
                points,
            } = modelProps

            const trimX = frameTrim(modelProps, dims.trimLengthX)
            const trimY = frameTrim(modelProps, dims.trimLengthY)

            const trimLengths = [
                align({
                    modes: ['center', 'min', 'min'],
                    relativeTo: points.trimEdges[0],
                }, rotate([0, 0, Math.PI / -2], trimX)),
                align({
                    modes: ['center', 'max', 'min'],
                    relativeTo: points.trimEdges[1],
                }, rotate([0, 0, Math.PI / 2], trimX)),
                align({
                    modes: ['min', 'center', 'min'],
                    relativeTo: points.trimEdges[2],
                }, rotate([0, 0, Math.PI], trimY)),
                align({
                    modes: ['max', 'center', 'min'],
                    relativeTo: points.trimEdges[3],
                }, trimY),
            ]

            let aTrim = union(...trimLengths)
            return aTrim
        }


        /**
         * Assembled frame
         * @param {*} modelProps
         * @returns geom3
         */
        const assembledFrame = (modelProps) => {
            const {
                opts,
                dims,
                points,
            } = modelProps

            let aFrame = assembledTrim(modelProps)

            let corners = null
            if (!opts.cornerType || opts.cornerType == 'none') {
                corners = null
            } else {
                const cornerOffset = dims.trimThickness / 2
                const cornerPts = [
                    {
                        point: [
                            points.corners[0][0],
                            points.corners[0][1],
                            points.corners[0][2],
                        ],
                        rotation: [0, 0, 0],
                        alignmentMode: ['min', 'min', 'min'],
                    },
                    {
                        point: [
                            points.corners[1][0],
                            points.corners[1][1],
                            points.corners[1][2],
                        ],
                        rotation: [0, 0, Math.PI / 2],
                        alignmentMode: ['max', 'min', 'min'],
                    },
                    {
                        point: [
                            points.corners[2][0],
                            points.corners[2][1],
                            points.corners[2][2],
                        ],
                        rotation: [0, 0, Math.PI / -2],
                        alignmentMode: ['min', 'max', 'min'],
                    },
                    {
                        point: [
                            points.corners[3][0],
                            points.corners[3][1],
                            points.corners[3][2],
                        ],
                        rotation: [0, 0, Math.PI],
                        alignmentMode: ['max', 'max', 'min'],
                    },
                ]

                const cornerAssemblies = cornerPts.map((cornerPt, idx) => {
                    const corner = cornerFrameAssembly(modelProps)
                    return align({
                        modes: cornerPt.alignmentMode,
                        relativeTo: cornerPt.point,
                    }, rotate(cornerPt.rotation, corner))
                })

                corners = union(...cornerAssemblies)
                aFrame = union(aFrame, corners)
            }

            let backing = null
            aFrame = align({ modes: ['center', 'center', 'min'] }, aFrame)

            if (opts.backingType && opts.backingType != 'none') {
                backing = cuboid({
                    size: [
                        dims.backingWidth,
                        dims.backingHeight,
                        dims.backingThickness
                    ]
                })
                backing = align({ modes: ['center', 'center', 'min'], }, backing)

                aFrame = union(aFrame, backing)
            }

            return aFrame
        }

        /**
        * Back side assembly for full- size frames
        * @param {*} modelProps
        * @returns geom3
        */
        const fullFrameBack = (modelProps, backOutline) => {
            const {
                opts,
                dims,
                points,
            } = modelProps

            const extBox = extrudeLinear({ height: dims.rabbetDepth }, backOutline)
            const intBox = cuboid({
                size: [
                    dims.frameInnerWidth + (dims.rabbetWidth * 2),
                    dims.frameInnerHeight + (dims.rabbetWidth * 2),
                    dims.rabbetDepth
                ]
            })

            return subtract(
                position.ctr(extBox),
                position.ctr(intBox)
            );
        }

        /* ----------------------------------------
         * Complete Assembly
         * ------------------------------------- */

        /** Final Assembly */
        const finalAssembly = (modelProps) => {
            const aFrame = assembledFrame(modelProps)
            let mainPart = aFrame

            if (modelProps.opts.frameForm == 'full') {
                const aFrameBackOutline = project(
                    {},
                    align({ modes: ['center', 'center', 'min'] }, aFrame)
                )
                const fBack = fullFrameBack(modelProps, aFrameBackOutline)
                // add frame back to main thing
                mainPart = union(
                    align({ modes: ['center', 'center', 'min'] }, mainPart),
                    align({ modes: ['center', 'center', 'max'] }, fBack),
                )
            }

            return mainPart
        }

        /* ----------------------------------------
         * Outputs
         * ------------------------------------- */

        const frameCornerInst = frameCorner(modelProperties)
        const frameCornerJointInst = frameCornerJoint(modelProperties)
        const cornerFrameAssemblyInst = cornerFrameAssembly(modelProperties)
        const frameTrimInst = frameTrim(modelProperties, 25.4)
        const assembledTrimInst = assembledTrim(modelProperties)
        const assembledFrameInst = assembledFrame(modelProperties)

        let mainModel = finalAssembly(modelProperties)

        let modelParts = {
            frameCorner: frameCornerInst,
            frameCornerJoint: frameCornerJointInst,
            cornerFrameAssembly: cornerFrameAssemblyInst,
            frameTrim: frameTrimInst,
            assembledTrim: assembledTrimInst,
            assembledFrame: assembledFrameInst,
        }

        return [mainModel, modelParts, modelProperties]
    }

    return trimFamilyFrame
}

module.exports = {
    init: trimFamilyFrameInit
}