/** A storage container designed for my electronics components (but will be suitable for other uses too). Each drawer contains a number of tubs (inserts). The inserts can be different sizes, but are all integer multiples of the smallest insert. I have chosen to use the same size inserts as used by Raaco's "A Series", because I already have some of these, as well as other brands which use the same "standard" size. Unlike the Raaco system, I don't want lids on each drawer. I would like to open the drawer fully, so that I can get to all of the tubs without the drawer falling out. I want a mechanism to prevent the drawer being pulled out too far. Each drawer front should have a place for a label. The inserts have small feet, which fit into circles in the drawers. The feet ensure the tubs line up correctly, even when some tubs are missing. To print the feet, we probably need to print the tubs upside down; the base is printed using bridging - you will need good part cooling! Many cabinets can be glued together to form a large array. See Racco's solutions : https://www.raacostorage.co.uk/PBSCCatalog.asp?CatID=2279874 https://www.raacostorage.co.uk/a-range-inserts-c102x2279884 FYI. The "55" refers to the depth (y-axis) of the smallest insert. This is also referred to as the "A" range. You could fix the cabinets to the wall using screws, but the existing model doesn't help you out! Consider tweaking the code to add "keyholes" on the cabinet and adjust the "extra" space so that the drawers still fit. Print Notes I use a "coarse" profile, with 0.3mm layers and high speed moves. (except for the 1st layer which I like to take slow). Consider using a 0.8mm nozzle with maybe 0.6mm layers??? */ import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.* import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.* import static uk.co.nickthecoder.foocad.chamferedextrude.v1.ChamferedExtrude.* class ComponentOrganiser : Model { // The gap between the inserts. var insertGap = 0.5 // The size of the inserts. Note "insertGap" is subtracted from the // x and y sizes of the actual inserts. // Note Racco states that the smallest A insert is 39x55x47 // Swap width and depth for inserts oriented the other way. var unitWidth = 55 var unitDepth = 40 // NOTE this isn't 39! var unitHeight = 46.5 // Does NOT include the feet. var shoeHeight = 2.0 // Note Racco states that the smallest B insert is 55x79x69, // So I suppose the unit is (80x55x69) i.e. twice as wide as the A series. // The difference between the size of the cabinet's void, and the drawer // that fits in it. var drawerGap = 1.0 var chamfer = 1.0 var drawerThickness = 1.8 var cabinetThickness = 1.8 var insertThickness = 1.0 var insertTaper = 1.0 var insertRadius = 2.0 // The radius of the circles in the drawers that the feet fit into. var shoeRadius = 9 // Depth of the "extra" bit at the back of the drawer, so that the drawer // can be fully open, and yet still support itself within the cabinet. var extraDepth = 50 // The number of 1x1 inserts that can be fitted in a single drawer. var drawerX = 4 var drawerY = 4 var drawerCount = 4 // Width of the stubs at the back of the drawers var extraStub = 10 // Size of the clips which prevent the drawer from coming out of the cabinet. var clipHeight = 10 var clipWidth = 2 var clipDepth = 10 var earDiameter = 30 // If true, then the inserts are printed bottom down without feet. // You can glue feet on yourself afterwards. var withoutFeet = true /* var piece = "" override fun pieceNames() = listOf( "drawer", "drawerBox", "cabinet", "feet", "64feet", "insert1x1", "insert1x2", "insert1x3", "insert1x4", "insert2x1", "insert2x2", "insert2x4", "insert2x4", "insert3x1", "insert3x2", "insert3x3", "insert4x1", "insert4x2" ) override fun setPieceName( name : String ) { piece = name } */ fun handleProfile() : Shape2d { val width = unitWidth * drawerX val height = drawerHeight()-chamfer val out = 15 val foo = 5 val bar = 5 val flat = 8 val profile = PolygonBuilder().apply { moveTo( 0,0 ) lineTo( out, height*0.5-flat) lineTo( out, height*0.5 + flat ) bezierTo( Vector2(out, height*0.5+flat+foo), Vector2(bar*2, height-bar), Vector2(drawerThickness,height) ) lineTo(0,height) }.build() return profile } fun handle() : Shape3d { val width = unitWidth * drawerX var depth = unitDepth * drawerY var handleWidth = width + drawerThickness*2 val hp = handleProfile() val solid = hp .extrude( handleWidth ) val hole = hp.offset( -drawerThickness ) .extrude( handleWidth-drawerThickness*2 ) .translateZ(drawerThickness) val opening = Cube( solid.size.x, solid.size.y, solid.size.z - drawerThickness*2 ) .translate(drawerThickness,-solid.size.y/2-8,drawerThickness) val result :Shape3d = if (handleWidth > 90) { val supports = hp.extrude( drawerThickness ) .centerZ() .translateZ(handleWidth/2-40).translateZ(80).also() solid - hole - opening + supports } else { solid - hole - opening } return result.rotateX(90).rotateZ(-90).centerX() .translateY(drawerThickness) .translateZ( chamfer ) } fun drawerHeight() = unitHeight + shoeHeight + insertGap + drawerThickness fun freeWidth() = unitWidth * drawerX fun freeDepth() = unitDepth * drawerY fun drawerWidth() = unitWidth * drawerX + drawerThickness*2 fun drawerDepth() = unitDepth * drawerY + drawerThickness*2 + extraDepth fun footwells() : Shape2d { return Circle( shoeRadius ) .repeatX( drawerX+1, unitWidth ) .repeatY( drawerY+1, unitDepth ) .center() } fun drawerBottom() : Shape3d { val freeWidth : double = freeWidth() var freeDepth : double = freeDepth() val shoes = ( ( Circle( shoeRadius + drawerThickness ) - Circle( shoeRadius ) ) .repeatX( drawerX+1, unitWidth ) .repeatY( drawerY+1, unitDepth ) .center() / Square( freeWidth, freeDepth ).center() ) .extrude( shoeHeight - 0.3 ) .translateZ( drawerThickness ) .translateY( freeDepth / 2 + drawerThickness) // Lines connecting the shoes. // These aren't *needed*, but they do add extra strength to the floor, // for little extra time and plastic. They look nice too ;-) val verticals = Cube( drawerThickness, unitDepth - shoeRadius*2, shoeHeight ) .repeatY( drawerY, unitDepth ) .repeatX( drawerX-1, unitWidth ) .centerXY() .translate( 0, drawerThickness + freeDepth/2, drawerThickness ) val horizontals = Cube( unitWidth - shoeRadius*2, drawerThickness, shoeHeight ) .repeatX( drawerX, unitWidth ) .repeatY( drawerY-1, unitDepth ) .centerXY() .translate( 0, drawerThickness + freeDepth/2, drawerThickness ) return shoes + verticals + horizontals } fun drawer(multiPart : bool) : Shape3d { val freeWidth : double = freeWidth() var freeDepth : double = freeDepth() val height = drawerHeight() val width = drawerWidth() val depth = drawerDepth() var handleWidth = width val profile : Shape2d = if ( multiPart==true ) { Square( width, depth - extraDepth ).centerX() } else { val foo : Shape2d = Square( width, depth ).centerX() .roundCorner(3,10) .roundCorner(2,10) foo } var drawer : Shape3d = ExtrusionBuilder().apply { crossSection( profile.offset( -chamfer ) ) forward( chamfer ) crossSection( profile ) forward( height - chamfer ) crossSection() crossSection( - drawerThickness ) forward( -height + drawerThickness +chamfer ) crossSection() forward( -chamfer ) crossSection( -chamfer ) }.build() if ( multiPart) { drawer = drawer - Cube( width, drawerThickness, drawerThickness*2 ) .centerX() .topTo( height ) } else { // The clip uses two tricks to print easily. // Angled at the bottom by 45 degrees // the "gap" has a break in it which must be cut manually. This allows // the bottom of the clip to print via bridging val clipP = PolygonBuilder().apply { moveTo(0,0) lineTo(0,clipDepth) lineTo(clipWidth,2) lineTo(clipWidth,0) }.build() val clip = clipP.extrude(clipHeight) - Cube( clipP.size.y ).rotateY(45) val clips = clip.centerY().centerZ() .translate( width/2, // Need to allow space for the drawer fronts. // This also lets us unlock the drawer without the side of cabinet // being available. freeDepth + drawerThickness*2 + 22, height/2 ) .mirrorX().also() val clipGapP = Square( 1, extraDepth*0.8 ) .translateX(clipHeight+1).also() + Square( clipHeight+1-0.6, 1 ) val clipGap = clipGapP.extrude( drawerThickness+2 ) .rotateY(90).center() .translate( width/2-drawerThickness/2, freeDepth + drawerThickness*2 + extraDepth*0.4, height/2 ) .mirrorX().also() val back = Cube( freeWidth, drawerThickness, height ) .translate( -freeWidth/2, freeDepth+drawerThickness, 0 ) val extraTop = ( profile / Square( width, extraDepth+drawerThickness ) .translateY(freeDepth+drawerThickness) .centerX() ) .extrude( drawerThickness ) .topTo(height) drawer = drawer - clipGap + clips + back + extraTop + handle() } // TODO Feather Bug. If the type declaration is removed, we get a cryptic error. // at RUNTIME val ears : Shape3d = if (earDiameter <= 0 ) { Cube(0) } else { val foo : Shape3d = Circle( earDiameter/2 ) .translateX(width/2).mirrorX().also() .extrude( 0.3 ) + Circle( earDiameter/2 ) .translate(width/2-2, depth-2 ).mirrorX().also() .extrude( 0.3 ) foo } val all = drawer.color("Green").brighter() + ears + drawerBottom() //println( "Drawer Size ${all.size}" ) return all } @Piece fun cabinet() : Shape3d { val width = drawerWidth() + drawerGap + cabinetThickness*2 var depth = drawerDepth() + drawerGap + cabinetThickness*2 val singleHeight = drawerHeight() + drawerGap + cabinetThickness*2 val storeyHeight = singleHeight - cabinetThickness val singleP = Square( width, singleHeight ) .roundAllCorners(cabinetThickness/2) val single = ExtrusionBuilder().apply { crossSection( singleP.offset(-chamfer) ) forward( chamfer ) crossSection( singleP ) forward( depth - chamfer ) crossSection() crossSection( -cabinetThickness ) forward( -depth + cabinetThickness ) crossSection() }.build().centerX().color("Yellow").darker() val clipHole = Cube( cabinetThickness+0.02, clipHeight+2, clipDepth+2 ).center() .translate(width/2-cabinetThickness/2, singleHeight/2-drawerGap/2, depth-10) .color("Red") // Make holes at the back too, so that the clips aren't permanently squashed. // PLA has a tendency to conform to stresses, and therefore would become loose. val clipHole2 = clipHole.translate( -0.4 ,0,-depth + extraDepth - clipDepth -1 ) .color("Orange") val clipHole2b = clipHole2 + Cube( cabinetThickness+0.02, clipHeight+2, 60 ).centerY() .rotateY(-2) .translate(width/2-cabinetThickness-0.4, singleHeight/2-drawerGap/2, 0) .bottomTo(clipHole2.bottom+ clipDepth) .color("Orange") val single2 = single - clipHole.mirrorX().also() - clipHole2b.mirrorX().also() val all = single2 .repeatY(drawerCount, storeyHeight) val ears = if (earDiameter <= 0 ) { Cube(0) } else { Circle( earDiameter/2 ) .translateX(all.size.x/2).mirrorX().also() .translateY(all.size.y).also() .extrude( 0.3 ) .color("Green") } return all + ears } fun footProfile() : Shape2d { val width = unitWidth - insertGap var depth = unitDepth - insertGap val profile = Square( width - insertTaper*2 - insertThickness * 2, depth - insertTaper*2 - insertThickness * 2 ).center().roundAllCorners(insertRadius,6) return profile.offset( insertThickness ) / Circle( shoeRadius-1 ).translate( 1/2*unitWidth, 1/2*unitDepth ) } fun insert( x : int, y : int ) : Shape3d { val width = unitWidth * x - insertGap var depth = unitDepth * y - insertGap val height = unitHeight - insertGap val profile = Square( width - insertTaper*2 - insertThickness * 2, depth - insertTaper*2 - insertThickness * 2 ).center().roundAllCorners(insertRadius,6) // We build them upside down, starting at the inside of the base. val main = ExtrusionBuilder().apply { forward( height - insertThickness ) crossSection( profile ) forward( -height + insertThickness ) crossSection( insertTaper ) crossSection( insertThickness ) forward( height ) crossSection( -insertTaper ) }.build() val all = if (withoutFeet) { main.mirrorZ().toOriginZ() } else { val footProfile = profile.offset( insertThickness ) / Circle( shoeRadius-1 ).translate( x/2*unitWidth, y/2*unitDepth ) val feet = footProfile.extrude( shoeHeight-0.2 ) .mirrorX().also() .mirrorY().also() .color("Green") main + feet.bottomTo(main.top) } return all } @Piece fun drawer() = drawer(false) @Piece fun drawerBox() = drawer(true) @Piece fun feet() = footProfile().extrude(shoeHeight-0.2) .tileX(4, 0.5) .center() @Piece fun feet64() = footProfile().extrude(shoeHeight-0.2) .tileX(8, 0.5) .tileY(8,0.5) .center() @Piece fun insert1x1() = insert( 1, 1 ) @Piece fun insert2x1() = insert( 2, 1 ) @Piece fun insert1x2() = insert( 1, 2 ) @Piece fun insert2x2() = insert( 2, 2 ) override fun build() : Shape3d { val storeyHeight = drawerHeight() + drawerGap + cabinetThickness val drawer : Shape3d = drawer(false) val result = cabinet().rotateX(90) + drawer(false) .backTo(-cabinetThickness) .translateY( -216 ) // At the "stop" .translateZ( cabinetThickness ) //.repeatZ( drawerCount, storeyHeight ) //.translate( 0, 216, storeyHeight ).also() return result } }