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) } }