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

const connectionsInit = ({ jscad, swcadJs }) => {
    const { circle, rectangle, triangle, cuboid, cylinder } = jscad.primitives
    const { union, subtract, scission } = jscad.booleans
    const { align, translate, mirror } = jscad.transforms
    const { hull, hullChain } = jscad.hulls
    const { degToRad } = jscad.utils
    const { project } = jscad.extrusions


    const { position, math } = swcadJs.utils

    //---------------
    //  CONNECTIONS
    //---------------

    const connectionDefaults = {
        interfaceThickness: 1.333333,
        tolerance: math.inchesToMm(1 / 128),
        fit: math.inchesToMm(1 / 128),
        cornerRadius: math.inchesToMm(1 / 4),
        dovetailAngle: degToRad(8),
        dovetailMargin: 1,
        tabAngle: degToRad(8),
        tabMargin: 1,
    }

    const createConnTrapezoid = ({ 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
    }

    const interlockingProfiles = (
        unitWidth,
        unitLength,
        fitGap,
    ) => {
        console.log('interlockingProfiles()', unitWidth, unitLength, fitGap)
        const sampleThickness = 1 // scission only works with 3D objects. Need a filler thickness for now
        const widthUnit = unitWidth / 3
        const lengthUnit = unitLength / 3

        const dovetailEndSize = [
            lengthUnit / 6,
            lengthUnit,
        ]

        const widthCoords = [
            widthUnit,
            widthUnit * 2,
        ]
        const lengthCoords = [
            lengthUnit,
            lengthUnit * 2,
        ]

        const dovetailPanelMidpoint = [
            unitWidth / 2,
            unitLength / 2,
        ]

        const baseProfilePanel = cuboid({
            size: [unitWidth, unitLength, sampleThickness],
            center: [dovetailPanelMidpoint[0], dovetailPanelMidpoint[1], 0]
        })

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

        const upperPts = [
            [widthCoords[0] - dovetailEndSize[0], lengthCoords[1]],
            [widthCoords[1] + dovetailEndSize[0], 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],
            })
        })

        let 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]),
        ]

        return [
            project({}, dTailProfiles[0]),
            project({}, dTailProfiles[1]),
        ]
    }

    /**
     * Connection profiles
     * @memberof profiles
     * @namespace connections
     */
    const connections = {
        /**
         * ...
         * @memberof profiles.connections
         * @param {object} opts 
         * @returns ...
         */
        pegboard: ({
            spacing,
            radius,
            margin,
            cornerRadius = connectionDefaults.cornerRadius,
            fit = connectionDefaults.fit,
            tolerance = connectionDefaults.tolerance
        }) => {
            const diametre = radius * 2
            const totalGap = fit / 4 + (tolerance / 4)
            const specs = {
                ...connectionDefaults,
                diametre,
                fitDowelRadius: -totalGap + radius,
                fitHoleRadius: totalGap + radius,
                margin: margin || radius,
            }
            specs.totalWidth = specs.margin * 2 + (radius * 2 + spacing)
            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 = spacing / 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.fitDowelRadius }))
            })
            const dowelDies = specs.dowelPoints.map(dPt => {
                return translate(dPt, circle({ radius: specs.fitHoleRadius }))
            })

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

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

            return {
                male,
                female,
                size: [specs.totalWidth, specs.totalWidth],
            }
        },
        /**
         * ...
         * @memberof profiles.connections
         * @param {object} opts
         * @returns ...
         */
        polygon: ({
            radius,
            segments,
            margin,
            cornerRadius = connectionDefaults.cornerRadius,
            fit = connectionDefaults.fit,
            tolerance = connectionDefaults.tolerance
        }) => {
            const diametre = radius * 2
            const totalGap = fit / 4 + (tolerance / 4)
            const specs = {
                ...connectionDefaults,
                diametre,
                fitDowelRadius: -totalGap + radius,
                fitHoleRadius: totalGap + radius,
                margin: margin || radius,
            }
            specs.totalWidth = specs.margin * 2 + specs.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.fitDowelRadius, segments })
            const dowelDie = circle({ radius: specs.fitHoleRadius, 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)

            return {
                male,
                female,
                size: [specs.totalWidth, specs.totalWidth],
            }
        },
        /**
         * Tab and Dovetail profiles are designed to fit right against the male edge.
         * With a margin (1mm default) extending into the female edge to ensure one shape that holds both dovetail ends.
         * @memberof profiles.connections
         * @param {*} opts 
         * @returns ...
         */
        tab: ({
            width,
            depth,
            margin = connectionDefaults.tabMargin,
            angle = connectionDefaults.tabAngle,
            fit = connectionDefaults.fit,
            tolerance = connectionDefaults.tolerance
        }) => {
            const specs = {
                ...connectionDefaults,
                totalWidth: margin * 2 + width,
                totalTabDepth: margin + depth,
            }

            const baseRect = rectangle({ size: [specs.totalTabDepth, specs.totalWidth] })
            const baseRectCoords = position.getGeomCoords(baseRect)

            const male = align(
                { modes: ['max', 'center', 'center'], relativeTo: [baseRectCoords.right, 0, 0] },
                createConnTrapezoid({ depth, width, angle })
            )
            const female = align(
                { modes: ['min', 'center', 'center'], relativeTo: [baseRectCoords.left, 0, 0] },
                subtract(baseRect, male)
            )

            return {
                male,
                female,
                size: [specs.totalTabDepth, specs.totalWidth],
            }
        },
        /**
         * Tab and Dovetail profiles are designed to fit right against the male edge.
         * @memberof profiles.connections
         * @param {*} opts 
         * @returns ...
         */
        dovetail: ({
            width,
            depth,
            fitGap = connectionDefaults.fit,
        }) => {
            console.log('dovetail()', width, depth, fitGap)
            const dovetailProfiles = interlockingProfiles(width, depth, fitGap)

            return {
                male: dovetailProfiles[1],
                female: dovetailProfiles[0],
                size: [width, depth],
            }
        },
    }

    return connections;
}

module.exports = { init: connectionsInit }