Source: models/profiles/frame-rect.js

"use strict"

/**
 * Builds rectangular frames with varying corner details
 * @memberof models.profiles
 * @namespace frameRect
 */

const rectangularFrameInit = ({ lib, swLib }) => {
    const { circle, rectangle } = lib.primitives
    const { rotate, align, mirror } = lib.transforms
    const { subtract, union } = lib.booleans
    const { measureDimensions } = lib.measurements
    const { TAU } = lib.maths.constants

    const { position } = swLib.core
    const { profiles } = swLib.models

    // Defining corner styles and available tag options
    const cornerStyleTypes = ['round', 'tri', 'rect', 'ellipse', 'cornerBez']

    const cStyleDefault = { inv: false, inset: true, offset: false }
    const cStRound = { inv: true, offset: true }
    const cStRect = { inv: true, offset: true }
    const cStEllipse = { ...cStRect }
    const cStCornerBez = { inv: true }

    const cRoundStyles = {
        round: {
            id: 'round',
            ...cStyleDefault,
            ...cStRound,
            func: ({ radius }) => {
                return circle({ radius })
            }
        },
    }

    const cTriStyles = {
        tri45deg: {
            id: 'tri45deg',
            ...cStyleDefault,
            func: profiles.triangle.right45
        },
        tri30deg: {
            id: 'tri30deg',
            ...cStyleDefault,
            func: profiles.triangle.right30
        },
        triGolden: {
            id: 'triGolden',
            ...cStyleDefault,
            func: profiles.triangle.rightGolden
        },
        triSilver: {
            id: 'triSilver',
            ...cStyleDefault,
            func: profiles.triangle.rightSilver
        },
        triBronze: {
            id: 'triBronze',
            ...cStyleDefault,
            func: profiles.triangle.rightBronze
        },
        triCopper: {
            id: 'triCopper',
            ...cStyleDefault,
            func: profiles.triangle.rightCopper
        },
    }

    const cStDefRect = {
        ...cStyleDefault,
        ...cStRect,
    }
    const cRectStyles = {
        rectGolden: {
            id: 'rectGolden',
            ...cStDefRect,
            func: profiles.rectangle.golden
        },
        rectSixtyThirty: {
            id: 'rectSixtyThirty',
            ...cStDefRect,
            func: profiles.rectangle.sixtyThirty
        },
        rectSilver: {
            id: 'rectSilver',
            ...cStDefRect,
            func: profiles.rectangle.silver
        },
        rectBronze: {
            id: 'rectBronze',
            ...cStDefRect,
            func: profiles.rectangle.bronze
        },
        rectCopper: {
            id: 'rectCopper',
            ...cStDefRect,
            func: profiles.rectangle.copper
        },
        rectSuperGolden: {
            id: 'rectSuperGolden',
            ...cStDefRect,
            func: profiles.rectangle.superGolden
        },
        rectPlastic: {
            id: 'rectPlastic',
            ...cStDefRect,
            func: profiles.rectangle.plastic
        },
    }

    const cStDefEllipse = {
        ...cStyleDefault,
        ...cStEllipse
    }
    const cEllipseStyles = {
        ellipseGolden: {
            id: 'ellipseGolden',
            ...cStDefEllipse,
            func: profiles.ellipse.golden
        },
        ellipseSixtyThirty: {
            id: 'ellipseSixtyThirty',
            ...cStDefEllipse,
            func: profiles.ellipse.sixtyThirty
        },
        ellipseSilver: {
            id: 'ellipseSilver',
            ...cStDefEllipse,
            func: profiles.ellipse.silver
        },
        ellipseBronze: {
            id: 'ellipseBronze',
            ...cStDefEllipse,
            func: profiles.ellipse.bronze
        },
        ellipseCopper: {
            id: 'ellipseCopper',
            ...cStDefEllipse,
            func: profiles.ellipse.copper
        },
        ellipseSuperGolden: {
            id: 'ellipseSuperGolden',
            ...cStDefEllipse,
            func: profiles.ellipse.superGolden
        },
        ellipsePlastic: {
            id: 'ellipsePlastic',
            ...cStDefEllipse,
            func: profiles.ellipse.plastic
        },
    }

    const cStDefCornBez = {
        ...cStyleDefault,
        ...cStCornerBez,
    }
    const cCornerBezStyles = {
        cornerBezGolden: {
            id: 'cornerBezGolden',
            ...cStDefCornBez,
            func: profiles.curves.rightCorner.golden
        },
        cornerBezSixtyThirty: {
            id: 'cornerBezSixtyThirty',
            ...cStDefCornBez,
            func: profiles.curves.rightCorner.sixtyThirty
        },
        cornerBezSilver: {
            id: 'cornerBezSilver',
            ...cStDefCornBez,
            func: profiles.curves.rightCorner.silver
        },
        cornerBezBronze: {
            id: 'cornerBezBronze',
            ...cStDefCornBez,
            func: profiles.curves.rightCorner.bronze
        },
        cornerBezCopper: {
            id: 'cornerBezCopper',
            ...cStDefCornBez,
            func: profiles.curves.rightCorner.copper
        },
        cornerBezSuperGolden: {
            id: 'cornerBezSuperGolden',
            ...cStDefCornBez,
            func: profiles.curves.rightCorner.superGolden
        },
        cornerBezPlastic: {
            id: 'cornerBezPlastic',
            ...cStDefCornBez,
            func: profiles.curves.rightCorner.plastic
        },
    }

    const cornerStyles = {
        ...cRoundStyles,
        ...cTriStyles,
        ...cRectStyles,
        ...cEllipseStyles,
        ...cCornerBezStyles,
    }

    const rectangularFrameDefOpts = {
        direction: 'in',
        frameWidth: 5,
        cornerOpts: {
            style: 'round',
            radius: 2.5,
        },
        cornerStyle: cRoundStyles.round,
    }
    rectangularFrameDefOpts.outCornerOpts = rectangularFrameDefOpts.cornerOpts

    /**
     * Builds a shaped rectangle with the given corner options
     * @memberof models.profiles.frameRect
     * @param {object} opts 
     * @param {number[]} opts.size
     * @param {object} opts.cornerOpts - options for interior side
     * @param {string} opts.cornerOpts.style - profile type, like "tri45deg", "rectSixtyThirty", "ellipseGolden", "cornerBezSilver"
     * @param {number} opts.cornerOpts.size
     * @param {number} opts.cornerOpts.length
     * @param {number} opts.cornerOpts.width
     * @param {number} opts.cornerOpts.radius
     * @param {string} opts.cornerOpts.longAxis
     * @param {string} opts.cornerOpts.opt - Option for each corner. Choose between "inv", "inset", "offset"
     * @returns ...
     */
    const shapedRectangle = ({
        size,
        cornerOpts = rectangularFrameDefOpts.cornerOpts,
    }) => {
        let outRect = rectangle({ size })
        const outRectCoords = position.getGeomCoords(outRect)
        const outRectCorners = position.rectangle.getRectangleCorners(outRect)
        const cornerStyle = cornerStyles[cornerOpts.style]

        const cornerPieces = []
        const notches = []

        if (cornerStyle) {
            const styleType = cornerStyleTypes.find(cStyleType => cornerStyle.id.startsWith(cStyleType))
            const cornerPiece = cornerStyle.func(cornerOpts)
            const cornerPieceDims = measureDimensions(cornerPiece)
            let baseNotch = rectangle({ size: [cornerPieceDims[0] / 2, cornerPieceDims[1] / 2] })
            if (['tri', 'cornerBez'].includes(styleType)) {
                baseNotch = rectangle({ size: [cornerPieceDims[0] - 0.01, cornerPieceDims[1] - 0.01] })
            }

            Object.entries(outRectCorners).forEach(([cName, cPt]) => {
                let inPieceRotation = null
                let inPieceAlign = ['center', 'center', 'center']
                let inPieceMirror = null

                if (cornerOpts.longAxis == 'y') {
                    if (styleType == 'tri') {
                        inPieceRotation = [0, 0, 0]
                    } else {
                        inPieceRotation = [0, 0, TAU / 4]
                    }
                } else {
                    if (styleType == 'tri') {
                        inPieceRotation = [0, 0, TAU / 4]
                    } else {
                        inPieceRotation = [0, 0, 0]
                    }
                }

                if (cName == 'c4') {
                    // (-X, -Y)
                    inPieceAlign = ['min', 'min', 'center']
                    if (styleType == 'cornerBez') {
                        inPieceRotation = [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2]]
                        inPieceMirror = cornerOpts.longAxis == 'y' ? null : [0, 1, 0]
                    }
                    if (styleType == 'tri') {
                        inPieceRotation = [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2]]
                        inPieceMirror = cornerOpts.longAxis == 'y' ? [0, 1, 0] : null
                    }
                } else if (cName == 'c3') {
                    // (-X, +Y)
                    inPieceAlign = ['min', 'max', 'center']
                    if (styleType == 'cornerBez') {
                        inPieceRotation = cornerOpts.longAxis == 'y' ?
                            [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2] + TAU / 2] :
                            [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2]]
                        inPieceMirror = cornerOpts.longAxis == 'y' ? [1, 0, 0] : null
                    }
                    if (styleType == 'tri') {
                        inPieceRotation = cornerOpts.longAxis == 'y' ?
                            [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2]] :
                            [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2] + TAU / 2]
                        inPieceMirror = cornerOpts.longAxis == 'y' ? null : [1, 0, 0]
                    }
                } else if (cName == 'c2') {
                    // (+X, +Y)
                    inPieceAlign = ['max', 'max', 'center']
                    if (styleType == 'cornerBez') {
                        inPieceRotation = [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2] + TAU / 2]
                        inPieceMirror = cornerOpts.longAxis == 'y' ? null : [0, 1, 0]
                    }
                    if (styleType == 'tri') {
                        inPieceRotation = [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2] + TAU / 2]
                        inPieceMirror = cornerOpts.longAxis == 'y' ? [0, 1, 0] : null
                    }
                } else {
                    // defaults to c1 (+X, -Y)
                    inPieceAlign = ['max', 'min', 'center']
                    if (styleType == 'cornerBez') {
                        inPieceRotation = cornerOpts.longAxis == 'y' ?
                            [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2]] :
                            [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2] + TAU / 2]
                        inPieceMirror = cornerOpts.longAxis == 'y' ? [1, 0, 0] : null
                    }
                    if (styleType == 'tri') {
                        inPieceRotation = cornerOpts.longAxis == 'y' ?
                            [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2] + TAU / 2] :
                            [inPieceRotation[0], inPieceRotation[1], inPieceRotation[2]]
                        inPieceMirror = cornerOpts.longAxis == 'y' ? null : [1, 0, 0]
                    }
                }

                const rotatedNotch = rotate(inPieceRotation, baseNotch)
                const newNotch = align(
                    { modes: inPieceAlign, relativeTo: cPt },
                    rotatedNotch
                )
                notches.push(newNotch)

                let rotatedCornerPiece = rotate(inPieceRotation, cornerPiece)
                if (inPieceMirror) {
                    rotatedCornerPiece = mirror({ normal: inPieceMirror }, rotatedCornerPiece)
                }
                const newCornerPiece = align(
                    { modes: inPieceAlign, relativeTo: cPt },
                    rotatedCornerPiece
                )
                cornerPieces.push(newCornerPiece)
            })

            outRect = subtract(outRect, ...notches)
            outRect = union(outRect, ...cornerPieces)
        }
        return outRect
    }

    /**
     * Builds a rectangular frame with the given corner options (interior and exterior)
     * @memberof models.profiles.frameRect
     * @param {object} opts 
     * @param {number[]} opts.size - Size of interior space
     * @param {string} opts.direction - Where ornaments are applied. Choose between "in" (default), "out", or "both"
     * @param {number} opts.frameWidth - Width of frame around interior space
     * @param {object} opts.cornerOpts - options for interior side
     * @param {string} opts.cornerOpts.style - profile type, like "tri45deg", "rectSixtyThirty", "ellipseGolden", "cornerBezSilver"
     * @param {number} opts.cornerOpts.size
     * @param {number} opts.cornerOpts.length
     * @param {number} opts.cornerOpts.width
     * @param {number} opts.cornerOpts.radius
     * @param {string} opts.cornerOpts.longAxis
     * @param {string} opts.cornerOpts.opt - Option for each corner. Choose between "inv", "inset", "offset"
     * @param {object} opts.outCornerOpts - options for exterior side
     * @returns ...
     */
    const rectangularFrame = ({
        size,
        direction = rectangularFrameDefOpts.direction,
        frameWidth = rectangularFrameDefOpts.frameWidth,
        cornerOpts = rectangularFrameDefOpts.cornerOpts,
        outCornerOpts = rectangularFrameDefOpts.outCornerOpts,
    }) => {
        const specs = {
            totalSize: [
                frameWidth * 2 + size[0],
                frameWidth * 2 + size[1],
            ],
            longAxis: cornerOpts.longAxis || position.findLongAxis(size)
        }

        const iCornOpts = { ...cornerOpts, longAxis: specs.longAxis }
        const oCornOpts = { ...outCornerOpts, longAxis: specs.longAxis }

        const inRectBase = rectangle({ size })
        const inCornerStyle = cornerStyles[iCornOpts.style]

        const outRectBase = rectangle({ size: specs.totalSize })
        const outCornerStyle = cornerStyles[oCornOpts.style]

        let inRect = inRectBase
        let outRect = outRectBase

        if (direction != 'out' && inCornerStyle) {
            inRect = shapedRectangle({
                size,
                cornerOpts: iCornOpts,
            })
        }

        if (direction != 'in' && outCornerStyle) {
            outRect = shapedRectangle({
                size: specs.totalSize,
                cornerOpts: oCornOpts,
            })
        }

        return subtract(outRect, inRect);
    }

    return {
        rectangularFrame,
    }
}

module.exports = { init: rectangularFrameInit };