Source: swcad-js-profiles/src/connections/index.js

"use strict"

/**
 * Connection Profiles
 * @author R. J. Salvador
 * @namespace connections
 * @memberof profiles
 * @since 0.12.5
 */

const connectionProfilesInit = ({ jscad, swcadJs }) => {
    const {
        cylinder,
        circle,
        ellipse: ellipseShape,
        cuboid,
        rectangle,
        triangle,
    } = jscad.primitives

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

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

    const {
        project
    } = jscad.extrusions

    const {
        hull,
        hullChain
    } = jscad.hulls

    const { toOutlines } = jscad.geometries.geom2

    const {
        math,
        position,
    } = swcadJs.calcs

    const {
        standards,
    } = swcadJs.data


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


    /**
     * Builds default values and opts for the model
     * @param {*} opts 
     * @returns default values and opts
     * @access private
     * @memberof profiles.connections
     */
    const modelDefaults = () => {
        /** Specific value declarations */
        const defaultValues = {
            constants: {
                sampleThickness: 1, // scission only works with 3D objects. Need a filler thickness for now
            },
            opts: {
                segments: 6,
                unitSegments: 24,
                numConnectors: 3,
            },
            dims: {
                size: [
                    math.inchesToMm(2.5),
                    math.inchesToMm(1.625),
                    math.inchesToMm(1),
                ],
                unitSpacing: math.inchesToMm(1),
                unitRadius: 6.35,
                radius: 12.7,
                interfaceMargin: math.inchesToMm(3 / 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,
            radius: defaultValues.dims.radius,
            unitSpacing: defaultValues.dims.unitSpacing,
            unitRadius: defaultValues.dims.unitRadius,
            segments: defaultValues.opts.segments,
            unitSegments: defaultValues.opts.unitSegments,
            numConnectors: defaultValues.opts.numConnectors,
            interfaceMargin: defaultValues.dims.interfaceMargin,
        }

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


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


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

        // User options
        const {
            size = defaults.opts.size,
            radius = defaults.opts.radius,
            unitSpacing = defaults.opts.unitSpacing,
            unitRadius = defaults.opts.unitRadius,
            unitSegments = defaults.opts.unitSegments,
            numConnectors = defaults.opts.numConnectors,
            segments = defaults.opts.segments,
            interfaceMargin = defaults.opts.interfaceMargin,
            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 = {
            ...stdOpts,
            size,
            radius,
            unitSpacing,
            unitRadius,
            segments,
            unitSegments,
            numConnectors,
            interfaceMargin,
        }


        return initOpts
    }


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


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

        const {
            size,
            radius,
            unitSpacing,
            unitRadius,
            segments,
            unitSegments,
            numConnectors,
            interfaceMargin,
            type,
            scale,
            interfaceThickness,
            fitGap,
        } = opts

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

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

        const diametre = radius * 2
        const unitDiametre = unitRadius * 2
        let interMargin = interfaceMargin
        if (typeof interfaceMargin == 'number') {
            interMargin = [interfaceMargin, interfaceMargin]
        }

        const midPoint = [
            width / 2,
            depth / 2,
        ]

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

        /** Constant values for model */
        const modelConstants = {
            sampleThickness: defaults.vals.constants.sampleThickness,
        }

        /** Derived user options for the model */
        const modelOpts = {
            type,
            scale,
            segments,
            unitSegments,
            numConnectors,
        }

        /** Various dimensions for model */
        const modelDims = {
            size,
            interfaceThickness,
            fitGap,
            width,
            depth,
            radius,
            unitSpacing,
            unitRadius,
            diametre,
            unitDiametre,
            interfaceMargin: interMargin,
        }

        /** Various key points for model */
        const modelPoints = {
            centrePt: midPoint,
        }

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

    /**
     * old function for dovetail/tab connections
     * @param {*} param0
     * @access private
     * @returns ...
     * @deprecated
     */
    const connectionTrapezoid = ({ depth, width, angle, reverse = false }) => {
        const baseRect = rectangle({ size: [depth, width] })
        const baseRectCoords = position.getGeomCoords(baseRect)
        const triOpts = position.triangle.rightTriangleOpts({ long: depth, longAngle: angle })
        const triangleEnds = [
            align(
                { modes: ['center', 'max', 'center'], relativeTo: [0, baseRectCoords.back, 0] },
                mirror({ normal: [0, 1, 0] }, triangle(triOpts))
            ),
            align(
                { modes: ['center', 'min', 'center'], relativeTo: [0, baseRectCoords.front, 0] },
                triangle(triOpts)
            ),
        ]

        const outShape = subtract(baseRect, triangleEnds)

        if (reverse) {
            return mirror({ normal: [1, 0, 0] }, outShape)
        }
        return outShape
    }


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

    /* ----------------------------------------
    * MODELLING FUNCTIONS
    * ------------------------------------- */

    /**
     * Generate dovetail connectors
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof profiles.connections
     * @since 0.12.5
     */
    const dovetail = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            width,
            depth,
            fitGap,
            interfaceMargin,
        } = modelProperties.dims

        const {
            centrePt
        } = modelProperties.points

        const {
            sampleThickness
        } = modelProperties.constants

        const dovetailWidth = width - (interfaceMargin[0] * 2)
        const dovetailLength = depth - (interfaceMargin[1] * 2)

        const dovetailEndSize = [
            dovetailLength / 6,
            dovetailLength,
        ]

        const widthCoords = [
            interfaceMargin[0],
            interfaceMargin[0] + dovetailWidth,
        ]
        const lengthCoords = [
            interfaceMargin[1],
            interfaceMargin[1] + dovetailLength,
        ]

        const baseProfilePanel = cuboid({
            size: [width, depth, sampleThickness],
            center: [centrePt[0], centrePt[1], 0]
        })

        const lowerPts = [
            [0, lengthCoords[0]],
            [widthCoords[0] + dovetailEndSize[0], lengthCoords[0]],
            [widthCoords[1] - dovetailEndSize[0], lengthCoords[0]],
            [width, lengthCoords[0]],
        ]

        const upperPts = [
            [widthCoords[0], lengthCoords[1]],
            [widthCoords[1], lengthCoords[1]],
        ]

        const allPts = [
            lowerPts[0],
            lowerPts[1],
            ...upperPts,
            lowerPts[2],
            lowerPts[3],
        ]

        let dovetailCutPoints = allPts.map(dtPt => {
            return cylinder({
                radius: fitGap / 2,
                height: sampleThickness * 10,
                center: [dtPt[0], dtPt[1], 0],
            })
        })

        const dovetailCut = hullChain(dovetailCutPoints)

        const cutPanel = subtract(
            baseProfilePanel,
            dovetailCut
        )

        const cutParts = scission(cutPanel)
        const dTailProfiles = [
            align({ modes: ['center', 'center', 'center'] }, cutParts[1]),
            align({ modes: ['center', 'center', 'center'] }, cutParts[0]),
        ]

        const dovetailProfiles = [
            project({}, dTailProfiles[0]),
            project({}, dTailProfiles[1]),
        ]

        const mainModel = [
            dovetailProfiles[1],
            dovetailProfiles[0],
        ]

        const modelParts = {
            male: dovetailProfiles[1],
            female: dovetailProfiles[0],
            cut: dovetailCut,
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * Generate tab connectors
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof profiles.connections
     * @since 0.12.5
     */
    const tab = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            width,
            depth,
            fitGap,
            interfaceMargin,
        } = modelProperties.dims

        const {
            sampleThickness
        } = modelProperties.constants

        const {
            centrePt
        } = modelProperties.points

        const tabWidth = width - (interfaceMargin[0] * 2)
        const tabLength = depth - (interfaceMargin[1] * 2)

        const tabEndSize = [
            tabLength / 6,
            tabLength,
        ]

        const widthCoords = [
            interfaceMargin[0],
            interfaceMargin[0] + tabWidth,
        ]
        const lengthCoords = [
            interfaceMargin[1],
            interfaceMargin[1] + tabLength,
        ]

        const baseProfilePanel = cuboid({
            size: [width, depth, sampleThickness],
            center: [centrePt[0], centrePt[1], 0]
        })

        const lowerPts = [
            [0, lengthCoords[0]],
            [widthCoords[0], lengthCoords[0]],
            [widthCoords[1], lengthCoords[0]],
            [width, lengthCoords[0]],
        ]

        const upperPts = [
            [widthCoords[0] + tabEndSize[0], lengthCoords[1]],
            [widthCoords[1] - tabEndSize[0], lengthCoords[1]],
        ]

        const allPts = [
            lowerPts[0],
            lowerPts[1],
            ...upperPts,
            lowerPts[2],
            lowerPts[3],
        ]

        let tabCutPoints = allPts.map(dtPt => {
            return cylinder({
                radius: fitGap / 2,
                height: sampleThickness * 10,
                center: [dtPt[0], dtPt[1], 0],
            })
        })

        const tabCut = hullChain(tabCutPoints)

        const cutPanel = subtract(
            baseProfilePanel,
            tabCut
        )

        const cutParts = scission(cutPanel)
        const dTailProfiles = [
            align({ modes: ['center', 'center', 'center'] }, cutParts[1]),
            align({ modes: ['center', 'center', 'center'] }, cutParts[0]),
        ]

        const tabProfiles = [
            project({}, dTailProfiles[0]),
            project({}, dTailProfiles[1]),
        ]

        const mainModel = [
            tabProfiles[1],
            tabProfiles[0],
        ]

        const modelParts = {
            male: tabProfiles[1],
            female: tabProfiles[0],
            cut: tabCut,
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * Generate polygon connectors
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof profiles.connections
     * @since 0.12.5
     */
    const polygon = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            segments,
        } = modelProperties.opts

        const {
            width,
            depth,
            radius,
            fitGap,
            diametre,
            interfaceMargin,
        } = modelProperties.dims

        const {
            sampleThickness
        } = modelProperties.constants

        const halfGap = fitGap / 2
        const cornerRadius = math.inchesToMm(1 / 4)

        const specs = {
            dowelRadius: -halfGap + radius,
            holeRadius: halfGap + radius,
        }

        specs.totalWidth = interfaceMargin[0] * 2 + diametre
        const halfWidth = specs.totalWidth / 2
        specs.cornerPoints = [
            [halfWidth - cornerRadius, halfWidth - cornerRadius, 0],
            [-halfWidth + cornerRadius, halfWidth - cornerRadius, 0],
            [-halfWidth + cornerRadius, -halfWidth + cornerRadius, 0],
            [halfWidth - cornerRadius, -halfWidth + cornerRadius, 0],
        ]

        const dowel = circle({ radius: specs.dowelRadius, segments })
        const dowelDie = circle({ radius: specs.holeRadius, segments })

        const corners = specs.cornerPoints.map(cPt => {
            return translate(cPt, circle({ radius: cornerRadius }))
        })
        const basePlate = hull(corners)

        const male = dowel
        const female = subtract(basePlate, dowelDie)

        const polygonProfiles = [
            female,
            male,
        ]

        const mainModel = [
            polygonProfiles[1],
            polygonProfiles[0],
        ]

        const modelParts = {
            male: polygonProfiles[1],
            female: polygonProfiles[0],
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * Generate ellipse connectors
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof profiles.connections
     * @since 0.12.6
     */
    const ellipse = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            segments,
        } = modelProperties.opts

        const {
            width,
            depth,
            fitGap,
            interfaceMargin,
        } = modelProperties.dims

        const {
            sampleThickness
        } = modelProperties.constants

        const halfGap = fitGap / 2
        const cornerRadius = math.inchesToMm(1 / 4)

        const diam = [
            width - (interfaceMargin[0] * 2),
            (depth - (interfaceMargin[1] * 2)) * 2,
        ]
        const radius = [
            diam[0] / 2,
            diam[1] / 2,
        ]
        const holeRadius = [
            radius[0] + (fitGap / 2),
            radius[1] + (fitGap / 2),
        ]

        const dowelCtr = [0, depth / -2 + interfaceMargin[1], 0]
        const dowel = ellipseShape({ radius, segments, center: dowelCtr })
        const dowelDie = ellipseShape({ radius: holeRadius, segments, center: dowelCtr })

        const mPlate = rectangle({
            size: [
                width,
                interfaceMargin[1],
                0,
            ]
        })
        let malePlate = align({
            modes: ['center', 'min', 'center'],
            relativeTo: [0, depth / -2, 0],
        }, mPlate)

        let cutDowel = subtract(
            align({
                modes: ['center', 'center', 'center'],
            }, dowel),
            align({
                modes: ['center', 'max', 'center'],
            }, rectangle({ size: [holeRadius[0] * 2, holeRadius[1] * 2] }))
        )
        cutDowel = align({
            modes: ['center', 'min', 'center'],
            relativeTo: dowelCtr,
        }, cutDowel)

        const fPlate = rectangle({
            size: [
                width,
                depth - interfaceMargin[1] - fitGap,
                0,
            ]
        })
        let femalePlate = align({
            modes: ['center', 'max', 'center'],
            relativeTo: [0, depth / 2, 0],
        }, fPlate)

        const male = union(cutDowel, malePlate)

        const female = subtract(femalePlate, dowelDie)

        const ellipseProfiles = [
            female,
            male,
        ]

        const mainModel = [
            ellipseProfiles[1],
            ellipseProfiles[0],
        ]

        const modelParts = {
            male: ellipseProfiles[1],
            female: ellipseProfiles[0],
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * Generate pegboard connectors
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof profiles.connections
     * @since 0.12.5
     */
    const pegboard = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            unitSegments,
        } = modelProperties.opts

        const {
            width,
            depth,
            unitRadius,
            unitSpacing,
            fitGap,
            unitDiametre,
            interfaceMargin,
        } = modelProperties.dims

        const {
            sampleThickness
        } = modelProperties.constants

        const halfGap = fitGap / 2
        const cornerRadius = unitDiametre * 0.75

        const specs = {
            dowelRadius: -halfGap + unitRadius,
            holeRadius: halfGap + unitRadius,
        }

        specs.totalWidth = interfaceMargin[0] * 2 + (unitRadius * 2 + unitSpacing)
        const halfWidth = specs.totalWidth / 2
        specs.cornerPoints = [
            [halfWidth - cornerRadius, halfWidth - cornerRadius, 0],
            [-halfWidth + cornerRadius, halfWidth - cornerRadius, 0],
            [-halfWidth + cornerRadius, -halfWidth + cornerRadius, 0],
            [halfWidth - cornerRadius, -halfWidth + cornerRadius, 0],
        ]
        const halfUnit = unitSpacing / 2
        specs.dowelPoints = [
            [halfUnit, halfUnit, 0],
            [-halfUnit, halfUnit, 0],
            [-halfUnit, -halfUnit, 0],
            [halfUnit, -halfUnit, 0],
        ]

        const corners = specs.cornerPoints.map(cPt => {
            return translate(cPt, circle({ radius: cornerRadius }))
        })
        const dowels = specs.dowelPoints.map(dPt => {
            return translate(dPt, circle({ radius: specs.dowelRadius, segments: unitSegments }))
        })
        const dowelDies = specs.dowelPoints.map(dPt => {
            return translate(dPt, circle({ radius: specs.holeRadius, segments: unitSegments }))
        })

        const basePlate = hull(corners)
        const dowelAssembly = union(dowels)

        const male = dowelAssembly
        const female = subtract(basePlate, dowelDies)

        const pegboardProfiles = [
            female,
            male,
        ]

        const mainModel = [
            pegboardProfiles[1],
            pegboardProfiles[0],
        ]

        const modelParts = {
            male: pegboardProfiles[1],
            female: pegboardProfiles[0],
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * Generate bolt circle connectors
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof profiles.connections
     * @since 0.12.6
     */
    const boltCircle = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            segments,
            unitSegments,
        } = modelProperties.opts

        const {
            radius,
            diametre,
            unitRadius,
            unitSpacing,
            fitGap,
            unitDiametre,
            interfaceMargin,
        } = modelProperties.dims

        const {
            sampleThickness
        } = modelProperties.constants

        const halfGap = fitGap / 2
        const holeRadius = unitRadius + halfGap
        const inCircle = circle({ radius, segments })
        const holePoints = toOutlines(inCircle)[0]
        const basePunch = circle({ radius: unitRadius })

        const dowels = holePoints.map(dPt => {
            return translate(dPt, circle({ radius: unitRadius, segments: unitSegments }))
        })
        const dowelDies = holePoints.map(dPt => {
            return translate(dPt, circle({ radius: holeRadius, segments: unitSegments }))
        })

        const basePlateRadius = radius + unitRadius + interfaceMargin[0]
        const basePlate = circle({ radius: basePlateRadius })
        const dowelAssembly = union(dowels)

        const male = dowelAssembly
        const female = subtract(basePlate, dowelDies)

        const pegboardProfiles = [
            female,
            male,
        ]

        const mainModel = [
            pegboardProfiles[1],
            pegboardProfiles[0],
        ]

        const modelParts = {
            male: pegboardProfiles[1],
            female: pegboardProfiles[0],
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * Generate dovetail row connectors
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof profiles.connections
     * @since 0.13.4
     */
    const dovetailRow = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            numConnectors
        } = modelProperties.opts

        const {
            width,
            depth,
            fitGap,
            interfaceMargin,
        } = modelProperties.dims

        const {
            centrePt
        } = modelProperties.points

        const {
            sampleThickness
        } = modelProperties.constants

        ////////

        const numMargins = numConnectors + 1

        const totalConnectionWidths = width - (interfaceMargin[0] * numMargins)
        const connectionWidth = totalConnectionWidths / numConnectors
        const connectionUnitWidth = 2 * interfaceMargin[0] + connectionWidth

        const dovetailCutOpts = modelOpts({
            ...opts,
            size: [connectionUnitWidth, depth]
        })

        const dovetailCutData = dovetail(dovetailCutOpts)
        const dovetailCutBase = dovetailCutData[1].cut

        const baseProfilePanel = cuboid({
            size: [width, depth, sampleThickness],
            center: [centrePt[0], centrePt[1], 0]
        })
        let dovetailRowCut = dovetailCutBase

        const translateDistBase = interfaceMargin[0] + connectionWidth
        for (let idx = 1; idx < numConnectors; idx++) {
            const translateDist = translateDistBase * idx
            dovetailRowCut = union(
                dovetailRowCut,
                translate([translateDist, 0, 0], dovetailCutBase)
            )
        }

        const cutPanel = subtract(
            baseProfilePanel,
            dovetailRowCut
        )
        const cutParts = scission(cutPanel)
        const dTailProfiles = [
            align({ modes: ['center', 'center', 'center'] }, cutParts[1]),
            align({ modes: ['center', 'center', 'center'] }, cutParts[0]),
        ]
        const dovetailProfiles = [
            project({}, dTailProfiles[0]),
            project({}, dTailProfiles[1]),
        ]

        ////////

        const mainModel = [
            dovetailProfiles[0],
            dovetailProfiles[1],
        ]

        const modelParts = {
            male: dovetailProfiles[0],
            female: dovetailProfiles[1],
            cut: dovetailRowCut,
        }

        return [mainModel, modelParts, modelProperties]
    }

    /**
     * Generate tab row connectors
     * @param {*} opts 
     * @returns Array with model, parts, and properties: [`geom3`, `Object.<string, geom3>`, `Object.<string, any>`]
     * @memberof profiles.connections
     * @since 0.13.4
     */
    const tabRow = (opts) => {
        const defaults = modelDefaults()
        const initOpts = modelOpts(opts)
        const modelProperties = modelProps(initOpts)

        const {
            numConnectors
        } = modelProperties.opts

        const {
            width,
            depth,
            fitGap,
            interfaceMargin,
        } = modelProperties.dims

        const {
            centrePt
        } = modelProperties.points

        const {
            sampleThickness
        } = modelProperties.constants

        ////////

        const numMargins = numConnectors + 1

        const totalConnectionWidths = width - (interfaceMargin[0] * numMargins)
        const connectionWidth = totalConnectionWidths / numConnectors
        const connectionUnitWidth = 2 * interfaceMargin[0] + connectionWidth

        const tabCutOpts = modelOpts({
            ...opts,
            size: [connectionUnitWidth, depth]
        })

        const tabCutData = tab(tabCutOpts)
        const tabCutBase = tabCutData[1].cut

        const baseProfilePanel = cuboid({
            size: [width, depth, sampleThickness],
            center: [centrePt[0], centrePt[1], 0]
        })
        let tabRowCut = tabCutBase

        const translateDistBase = interfaceMargin[0] + connectionWidth
        for (let idx = 1; idx < numConnectors; idx++) {
            const translateDist = translateDistBase * idx
            tabRowCut = union(
                tabRowCut,
                translate([translateDist, 0, 0], tabCutBase)
            )
        }

        const cutPanel = subtract(
            baseProfilePanel,
            tabRowCut
        )
        const cutParts = scission(cutPanel)
        const tProfiles = [
            align({ modes: ['center', 'center', 'center'] }, cutParts[1]),
            align({ modes: ['center', 'center', 'center'] }, cutParts[0]),
        ]
        const tabProfiles = [
            project({}, tProfiles[0]),
            project({}, tProfiles[1]),
        ]

        ////////

        const mainModel = [
            tabProfiles[0],
            tabProfiles[1],
        ]

        const modelParts = {
            male: tabProfiles[0],
            female: tabProfiles[1],
            cut: tabRowCut,
        }

        return [mainModel, modelParts, modelProperties]
    }

    return {
        defaults: modelDefaults,
        props: modelProps,
        dovetail,
        tab,
        polygon,
        ellipse,
        pegboard,
        boltCircle,
        dovetailRow,
        tabRow,
    }
}

module.exports = {
    init: connectionProfilesInit
}