Exit Full View
Up

/Furniture/Bed.foocad

Bed

This is a demonstration of the "Construction" module, as well as the more general components in FooCAD. It is a design for a bed that I built (I didn't build the headboard)

Each part of the design starts off by defining the lumber (including the width and thickness), then we cut the lumber to the required size, and optionally cut joints (such as a Lap joint, Mitre joint etc). Next we give the part a label (using Shape3d.label(name : String). All labelled parts are arranged into a "plan", which shows each part type, and how many are needed. This is a useful guide when cutting the parts. Labelled parts are also collected together by the "BOM" (bill of materials), which is handy when buying the wood.

We then position the parts, often duplicating them (as the design is symetrical).

Finally we combine all the parts together.

NOTE, FooCAD has improved since I created this model, so this script doesn't make use of the newer features. (But I still use this as an example of the Construction module).

FooCAD Source Code
import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.*
import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.*
import static uk.co.nickthecoder.foocad.along.v2.Along.*

class Bed : Model {

    @Custom var isExploded = false

    // The size of a standard double bed. Change these for a king/queen sized bed.
    var width =1350
    var length=1900
    var height=400
    var trimOverhang = 8

    override fun build() : Shape3d {

        // This model can produce multiple "targets", including an exploded view,
        // where the pieces are assembled, but moved apart from one another
        // so that it is easier to see how everything fits together.
        // By changing the value of "exploded" to either 0 or 100, we can use the
        // same code to produce both layouts.
        var exploded = if (isExploded) 100 else 0
       
        // Choose the lumber that will be used to build the bed.
        // The numbers after the lumber's name are the width and thickness
        // The "stockLength" is used by the Bill of Materials to work out how
        // many lengths of lumber are needed.
        // I like giving each type of lumber a different color, but you can ommit the
        // color if you prefer.
        val main = Lumber( "Main", 94, 27, "ForestGreen" ).stockLength( 2400 )
        val legLumber = Lumber( "Leg", 70, 70, "DarkBlue" ).stockLength( 2400 )
        val supportLumber = Lumber( "Support", 44, 27, "DarkRed" ).stockLength( 2400 )
        val ply = SheetLumber( "Ply", 12, "Purple", 0.5 ) // 0.5 = Semi transparent

        // Load 2D shapes from an SVG file
        val svg = SVGParser().parseFile("bed-profiles.svg").shapes
        // Use one of the SVG shapes to define a moulding.
        // The moulding hides the edges of the plywood top.
        val trim = Lumber("Trim", svg.get("trim").toOrigin().convexity(4))
            .stockLength( 2400 )
        

        // A few calculations up front which will simplify the script later on.

        val baseLength = length - trimOverhang * 2
        val baseWidth = width - trimOverhang*2
        // One more lumber definition which needed the baseLength, so had to come later than the others.

        val baseHeight = height - ply.thickness()
        val insideLength = baseLength - main.thickness()
        val insideWidth = baseWidth - main.thickness() * 2

        // Now lets cut the lumber to the correct lengths, and add joints where needed.
        // By using "label" the PlanTarget can find all of the parts of the model that
        // it will include include in the plan.
        // When we cut the lumber, it is always pointing up the Z axis.
    
        // Lots of pieces are duplicated (mirrored in the x or y axis), so we create the
        // model, centered, and then apply .mirrorX().also() which creates the mirror AND
        // the original copy.


        // A few interesting tricks :
        // ".also()" with no arguments take the current shape, and combines it with what it was made from.
        // For example : foo.translate( 100,0,0 ).also()
        // will create a union of foo and a translated version of foo.
        // If the thing we want to copy is further back, then we can use ".name(...)" and ".also(...)"
        // For example : bar.name("bar").mirrorX().translate( 100,0,0 ).also("bar")
        // will create a union of "bar" and the mirrored and translated version.
        // Note that the legs use this trick twice (X and Y), and the names must be different!

        val sides = main.cut( baseLength )
            .label( "leg" )
            .alongX()
            .translate( -baseLength/2, -baseWidth/2-exploded, 0)
            .mirrorY().also()

        // NOTE, the color of the 3D preview are taken from the lumber definition.
        // I use darker() or lighter() to make alternate shades so that it is easier
        // to see how pieces of the same lumber type fit together.
        val ends = main.cut( insideWidth )
            .darker()
            .label( "end" )
            .rotateZ(90).rotateX(90).toOrigin()
            .translate( -baseLength/2-exploded, -baseWidth/2 + main.thickness(), 0)
            .mirrorX().also()
            
        val legs = legLumber.cut ( baseHeight )
            // Two lap joints allow for the side and end pieces to fit into.
            .cutZ( LapJoint( legLumber, 0 ).depth(main.thickness()/2), 0 )
            .cutZ( LapJoint( legLumber, 1 ).depth(main.thickness()/2), 0 )
            .label( "leg" )
            .translate( -baseLength/2 + main.thickness() / 2, -baseWidth/2 + main.thickness()/2, 0)
            .mirrorX().also()
            .mirrorY().also()

        // Give rigidity to the corners, and help ensure the frame is square.
        val diagonalLength = 220
        val diagonalMitre = MitreJoint( main, 0 ) // side=0
        val diagonals = main.cut( diagonalLength ).color("Orange")
            .cutZRatio( diagonalMitre, 0 )
            .cutZRatio( diagonalMitre.otherEnd(), 1 )
            .label( "diagonal" )
            .alongX().rotate(0,0,-45)
            .translate(-baseLength/2, -baseWidth/2 + diagonalLength / 1.4,0)
                .mirrorX().also()
                .mirrorY().also()

        // Simple supports for the plywood.
        val support = supportLumber.cut(insideWidth)
            .label( "support" )
            .rotateZ(90).rotateX(90).toOrigin().centerX().centerY()
            .translate( 0, 0, -exploded )

        val shelves = supportLumber.cut( 100 )
            .cutZRatio( LapJoint( supportLumber, 2 ), 0.5 )
            .label("shelf" )
            .alongX().centerX()
            .translate(0,-baseWidth/2 + main.thickness(), supportLumber.width()/2)
            .mirrorY().also()

        // I want it strong enough to stand on without the plywood sagging or breaking
        // Each 12mm board has about 450mm unsuported. Strong enough for me, YMMV.
        val supportCount = 3
        val supports = (support + shelves)
            .translate(-baseLength/2 + (length-trim.thickness()) / (supportCount+1), 0, 0)
            .repeatX( supportCount, (length-trim.thickness()) / (supportCount+1) )
              

        // The plywood top. I designed this in four parts, so that I could get it in
        // my car (cut in the shop), but lockdown happened (no wood cutting service)!
        val top = ply.cut( width-trim.thickness(), (length-trim.thickness()) / (supportCount+1) - 5 )
            .label( "top" )
            .rotateX(90).rotateZ(90).toOrigin()
            .translate(-baseLength/2 + trim.width()-trimOverhang , -baseWidth/2 + trim.width()-trimOverhang, -ply.thickness() - exploded * 4).tileX((supportCount+1), 5 + exploded /2)

        val trimMitre = MitreJoint( trim, 3 ) // The 3 indicates which direction the mitre is cut (0..3)
        val sideTrim = trim.cut( length )
            .cutZRatio( trimMitre, 0 ) // The 0 indicates it is at the end of the wood
            .cutZRatio( trimMitre.otherEnd(), 1 ) // The 1 indicates it is at the other end of the wood.
            .label( "edgeTrim" )
            .rotateZ(90).rotateY(90).mirrorZ().toOrigin().centerX() // Wrangle it the right directions, and the right way up !
            .translate( 0, -baseWidth/2 -trimOverhang - exploded*2 ,-ply.thickness())
            .mirrorY().also()

        val endTrim = trim.cut( width ).darker()
            .cutZRatio( trimMitre, 0 )
            .cutZRatio( trimMitre.otherEnd(), 1 )
            .label( "endTrim" )
            .alongY2().centerY()
            .translate( -baseLength/2 -trimOverhang - exploded*2, 0, -ply.thickness() )
            .mirrorX().also()

        // The rail was only at the head end of the bed (but now is at both ends).
        // It is there to attach a head board, and should be flush with the end.
        // The head board posts must have spacers to avoid the trim (or cut the trim or the posts).
        // In hindsight, I should have cut into the *LEG* rather than att lap joints to the rails,
        // as the rails may be climbed on, and there isn't anywhere near enough support for that in this design.
        val railLap = LapJoint( supportLumber, 0 ).depth(supportLumber.width()-main.thickness()/2).length( legLumber.width()-main.thickness()/2 )
        val rails = supportLumber.cut( baseWidth - main.thickness() * 2 - 0*supportLumber.thickness() )
            .cutZ( railLap, 0 )
            .cutZRatio( railLap, 1 )
            .label( "rail" )
            .alongY2()
            .centerY()
            .translate( baseLength/2 -supportLumber.width() + exploded * 2, 0, height * 0.75)
            .mirrorX().also()

        // A diagonal part to give extra stength to the legs at the head of the bed.
        // When a head board is added, it can apply a lot of torque, and so the legs must be prevented
        // from rotating inwards. (My old shop-bought bed had this exact problem!)
        val endBraceMitre = MitreJoint( supportLumber, 1 )
        val endBraces = supportLumber.cut( 300 )
            .cutZRatio( endBraceMitre.otherEnd(), 2 )
            .label( "endBrace" )
            .rotateY( 45 )
            .translate( baseLength/2 -300, -baseWidth/2 + main.thickness(), 40)
            .mirrorY().also()
       

        // Combine  all of the pieces!
        
        val bed = legs + sides + ends + diagonals + supports + endTrim + sideTrim + rails + endBraces + top

        // Everything was arranged upside down, so flip everything over.
        val all = bed.rotate( 180,0,0 ) +
            headboard().translate( -length/2-exploded*4,0,130)
             
        return all

    }

    /*
        An adjustable headboard.
        The bottom of "headEdge" are hinged with "headBottom", so that the
        cushionAssembly can be either upright, or slightly angled.
        The amount of angle is governed by the size of "spacer".

        Use vecro to attach the cushion to the board, so that it can be
        taken off for washing.
    */
    fun headboard() : Shape3d {
        var exploded = if (isExploded) 100 else 0

        val main = Lumber( "HeadMain", 34, 34, "ForestGreen" ).stockLength( 2400 )
        val thinLumber = Lumber( "HeadThin", 15, 25, "LightGreen" ).stockLength( 2400 )
        val ply = SheetLumber( "Ply", 9, "Purple", 1.0 ) // 0.5 = Semi transparent

        val cushionWidth = width / 2 - 30
        val cushionHeight = width / 2 - 30
        val cushion = Cube( 80, cushionWidth, cushionHeight )
            .centerY().centerZ()
            .translate( ply.thickness() + thinLumber.thickness(), 0,0 )

        val boardWidth = cushionWidth - 80
        val boardHeight = cushionHeight - 80
       
        val board = ply.cut (boardWidth, boardHeight )
            .label( "headboard" )
            .rotate( 0,0, 90 )
            .centerY()
            .centerZ()
            .translate( thinLumber.thickness() , 0, 0 )

        val ends = thinLumber.cut( boardWidth )
            .label( "headEnd" )
            .alongY2().centerY()
            .translate( 0, 0, -boardHeight / 2 )
            .mirrorZ().also()
        
        val edges = thinLumber.cut( boardHeight - thinLumber.thickness() * 2 )
            .darker()
            .label( "headEdge" )
            .centerZ()
            .translate( 0, -boardWidth/2, 0 )
            .mirrorY().also()
        
        val cushionAssembly = (cushion + board + ends + edges)
            .toOrigin()
            .translate( 0, 10, 0 )
            .mirrorY().also()

        val space = 60
        val uprightSpacing = width * 0.8
        val upHeight = 1100

        val uprights = main.cut( upHeight )
            .label( "headUpright" )
            .translate( 0, uprightSpacing / 2, 0 )
            .mirrorY().also()
            
        val top = main.cut( uprightSpacing )
            .brighter()
            .label( "headTop" )
            .alongY().centerY()
            .translate( 0,0, upHeight - main.thickness() )

        val bottom = main.cut( uprightSpacing + main.thickness() * 2 )
            .brighter()
            .label( "headBottom" )
            .alongY().centerY()


        val spacer = main.cut( space - main.thickness() )
            .darker()
            .label( "headSpacer" )
            .alongX().mirrorX()
            .translate( 0, uprightSpacing / 2, 0 )
            .mirrorY().also()

        val bottomAssembly = (bottom + spacer)
            .translate( space, 0, upHeight - boardHeight - main.width() + thinLumber.thickness() )

        val frame = (uprights + top + bottomAssembly)
            .translate( -space - main.thickness(), 0, 0 )

        return cushionAssembly + 
            frame.translate( 0, 0, -upHeight + cushionHeight - (cushionHeight - boardHeight)/2)
    }

}