/Boxes/ComponentOrganiserCabinet.foocad

A storage container for my electronics components.
Each drawer contains a number of tubs (inserts). The inserts can be different sizes, but are all integer multiples of the smallest insert.
The cabinet is deeper than the "useful" size of the drawers (see extraDepth),
allowing easy access to all inserts.
The drawers are prevented from being pulled out completely by springy clips. This can be overridden by pushing in the clips.
The reinforment patterns on four sides interlock with neightbouring cabinets.
Print Notes
PLA is NOT suitable for the drawers, because the springy clip will permanently deform over time.
Supports are NOT required (and should be turned off).
Consider bumping up perimeters, so that the rails and panels are solid.
If you have a large enough print bed consider using a brim or ears to aid bed-adhesion.
import uk.co.nickthecoder.foocad.smartextrusion.v1.*
import static uk.co.nickthecoder.foocad.smartextrusion.v1.SmartExtrusion.*
import uk.co.nickthecoder.foocad.extras.v1.*
import static uk.co.nickthecoder.foocad.extras.v1.Extras.*
import uk.co.nickthecoder.foocad.cup.v1.*
import static uk.co.nickthecoder.foocad.cup.v1.Cup.*
import static uk.co.nickthecoder.foocad.arrange.v1.Arrange.*
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.*
include ComponentOrganiser.foocad
include Panelling.feather
class ComponentOrganiserCabinet : Model {
@Custom( about="The number of 1x1 inserts that can be fitted in a single drawer" )
var drawerX = 4
@Custom( about="The number of 1x1 inserts that can be fitted in a single drawer" )
var drawerY = 4
@Custom( about="The number of drawers" )
var drawerCount = 4
@Custom( about="Use the other orientation for the inserts?" )
var otherOrientation = false
@Custom( about="" )
var cabinetThickness = 1.8
@Custom( about="" )
var drawerThickness = 1.8
@Custom( about="Width and thickness of ribs around the cabinet which gives extra stiffness" )
var cabinetPanelling = Vector2( 14, 2 )
@Custom( about="Extra space at the back, which lets the usuable part of the drawer being fully accessible" )
var extraDepth = 50
// If the cabinet lifts of the bed slightly, make drawers shorter than normal
@Custom( about="Drawers are less deep than the cabinet" )
var drawerExtraClearance = 0
@Custom
var railSize = Vector2( 7, 5 )
@Custom
var drawerPanelling = Vector2( 8, 2 )
@Custom( about="Clearnace for the drawer to fit into the cabinet" )
var drawerClearance = 0.5
@Custom( about="Width, depth and height of the main part of the clip (prevents drawers pulled out too far)" )
var clipSize = Vector3( cabinetThickness + drawerClearance + 0.5, 10, 16 )
@Custom( about="Thickness of the springy part of the clip" )
var springThickness = 2.0
// PLA can print longer bridges than PETG.
@Custom( about="Maximum length of bridging allowed (for the drawer's handle)" )
var maxBridging = 70
// End of attributes
meth inserts() = ComponentOrganiser().apply {
if ( otherOrientation ) {
val oldUnitWidth = unitWidth
unitWidth = unitDepth
unitDepth = oldUnitWidth
}
}
meth drawerHeight() = inserts().unitHeight + inserts().shoeHeight + inserts().insertGap + drawerThickness
meth freeWidth() = inserts().unitWidth * drawerX
meth freeDepth() = inserts().unitDepth * drawerY
meth plainDrawerWidth() = inserts().unitWidth * drawerX + drawerThickness*2
meth totalDrawerWidth() = plainDrawerWidth() + drawerPanelling.y*2
meth plainDrawerDepth() = inserts().unitDepth * drawerY + drawerThickness*2
meth totalDrawerDepth() = inserts().unitDepth * drawerY + drawerThickness*2 + extraDepth
meth drawerPanelling() = Panelling( drawerPanelling )
.dividerRatio(1.5)
.radius( 2 )
meth curvedHandleProfile() : Shape2d {
val width = inserts().unitWidth * drawerX
val height = (drawerHeight() + railSize.y)*0.8
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
}
meth curvedHandle( width : double ) : Shape3d {
// This is built pointing upwards. i.e. the final result needs to be
// rotated about the Y axis.
var depth = inserts().unitDepth * drawerY
val hp = curvedHandleProfile()
val solid = hp
.extrude( width )
val hole = hp.offset( -drawerThickness )
.extrude( width-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)
var supportCount = width ~/ maxBridging
val result :Shape3d = if (width > maxBridging) {
// Ensure we have an even number, so the middle is free of supports.
if (supportCount % 2 == 1) supportCount ++
val supports = hp.extrude( drawerThickness )
.spreadZ( supportCount, width, 1 )
.centerZTo( width/2 )
solid - hole - opening + supports
} else {
solid - hole - opening
}
return result.rotateX(90).rotateZ(-90).centerX()
.translateY(drawerThickness)
}
// Deprecated in favour of drawerPanelled.
// The handle gets in the way of removing the inserts at the rear of the lower drawer.
// TODO BEWARE. Not perfect! Needs work before printing!!
@Piece( about="A drawer with a built-in handle across the whole width" )
meth drawerCurvedHandle() : Shape3d {
val plainDrawer = drawerBox()
val drawerExtra = drawerExtra()
// Extra height at the front to hide the gap between drawers
val hideGap = Square( totalDrawerWidth(), railSize.y )
.roundCorner(3,1)
.roundCorner(2,1)
.centerX()
.extrude( drawerThickness )
.rotateX(90)
.frontTo( plainDrawer.front )
.bottomTo( plainDrawer.top )
.label( "hideGap" )
// Gives extra rigidity
val cuboidForSidePanels = plainDrawer.marginBack( drawerPanelling.y*2 )
val sidePanels = drawerPanelling()
.maxPanelSize( cuboidForSidePanels, 100 )
.sidePanels( cuboidForSidePanels )
// A handle to pull the drawers out, and also adds rigidity.
val handle = curvedHandle( sidePanels.size.x )
.backTo( plainDrawer.front + drawerThickness )
.topTo( hideGap.top )
return (plainDrawer + sidePanels + drawerFloorPattern()) and drawerExtra + handle
}
// The box for the drawer (without the extra part at the back, or any decorations)
meth drawerBox() : Shape3d {
// A plain tray where the inserts are placed.
val box = Square( plainDrawerWidth(), plainDrawerDepth() )
.center()
.cup( drawerHeight(), drawerThickness )
.label( "box" )
return box
}
// Pattern to hold the inserts in the correct position.
// I accidentally printed a drawer without this pattern, so I printed it separately,
// and trimmed off the permineter before glueing.
@Piece
meth drawerFloorPattern() = inserts().floorPattern( drawerX, drawerY )
.bottomTo( drawerThickness )
meth drawerExtra() : Shape3d {
val drawerHeight = drawerHeight()
val drawerWidth = plainDrawerWidth()
val drawerDepth = plainDrawerDepth()
// Blocks at the back, which stay on the rails when the drawer is fully extended.
val extra = Square( drawerHeight, extraDepth - drawerExtraClearance )
.roundCorner(3,2)
//.roundCorner(2,10,1)
.roundCorner(2,2,1)
.smartExtrude( railSize.x )
.bottom( Chamfer( drawerPanelling.y ) )
.rotateY(90)
.bottomTo(0)
.leftTo( -totalDrawerWidth()/2 )
.frontTo( plainDrawerDepth()/2 )
.mirrorX().also()
// For strength (and also prevents items falling down the back)
val triSize = 7
val backTriangle = Square( triSize + 0.4, triSize )
.roundCorner(3, triSize, 1)
.extrude( plainDrawerWidth() )
.rotateY(-90)
.centerX()
.frontTo( plainDrawerDepth()/2 )
.topTo( drawerHeight() )
// 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 clipShape = PolygonBuilder().apply {
moveTo(0,0)
lineTo(0,clipSize.y)
lineTo(clipSize.x,2)
lineTo(clipSize.x,0)
}.build()
// The clip cannot start at the very back of the plain drawer, because we need a
// run-up for the bridging.
val safeStart = 0.5
val clip = (
clipShape.extrude(clipSize.z-safeStart).translateZ(safeStart) -
Cube( clipShape.size.y ).rotateY(45).translateZ(safeStart) +
Cube( springThickness, extraDepth*0.8, clipSize.z ).mirrorX().frontTo(-clipSize.y) +
Cube( springThickness, 1, 1 ).backTo(-clipSize.y).mirrorX().color("Red")
)
.leftTo( extra.right - springThickness )
.centerZTo( drawerHeight/2 )
.frontTo( drawerDepth/2 +1 )
val clips = clip
.mirrorX().also()
.marginX( -clipSize.x )
val clipClearance = 3
val clipGaps = Cube( railSize.x + 2, extraDepth*0.8, clipSize.z + clipClearance )
.rightTo( extra.right + 1 )
.frontTo( clip.front )
.centerZTo( clip.middle.z )
.mirrorX().also()
return (extra + backTriangle).remove( clipGaps ).insert( clips )
}
// Ear radius 13 fits my print bed.
@Piece( about="A drawer without handles, affix whatever handles you want." )
meth drawerPanelled() : Shape3d {
val plainDrawer = drawerBox()
val cuboidForSidePanels = plainDrawer.boundingCube()
.marginFront( drawerPanelling.y )
.marginBack( drawerPanelling.y*2 )
// Gives extra rigidity
val sidePanels = drawerPanelling()
.maxPanelSize( cuboidForSidePanels, 100 )
.cornerRadius(2)
.sidePanels( cuboidForSidePanels )
val drawerExtra = drawerExtra()
val hideGap = Square( totalDrawerWidth(), railSize.y + drawerPanelling.y )
.roundCorner(3,2)
.roundCorner(2,2)
.centerX()
.extrude( drawerThickness )
.rotateX(90)
.frontTo( plainDrawer.front )
.bottomTo( plainDrawer.top - drawerPanelling.y )
.label( "hideGap" )
val partiallyDecorated = plainDrawer + drawerFloorPattern() + hideGap
val frontPanel = drawerPanelling()
.radius(3)
.cornerRadius(2)
.frontPanel( partiallyDecorated )
return ( partiallyDecorated + frontPanel + sidePanels) and drawerExtra
}
@Piece( about="A simple handle which can be glued onto the front of `drawerPanelled`" )
meth handleGlued() : Shape3d {
val length = 50
val depth = 10
val overlap = 2
val base = Square( length + overlap*2, depth )
.center()
.roundAllCorners( overlap )
.extrude(1)
val main = Square( length, depth )
.roundCorner(3,depth/2)
.roundCorner(2,depth/2)
.centerX()
.extrude( 1 )
.rotateX(90)
.backTo( base.back - overlap )
val tri = Triangle( depth/4, depth/4 )
.mirrorY()
.extrude( length )
.rotateY(-90)
.centerX()
.backTo( main.front )
.bottomTo( base.top )
val chamferTop = Cube( length*2 )
.centerZ().centerX()
.rotateX(-45)
.translateY( base.back - base.top )
return main + base + tri - chamferTop
}
@Piece
meth cabinet() : Shape3d {
val width = totalDrawerWidth() + drawerClearance*2 + cabinetThickness*2
var depth = totalDrawerDepth() + drawerClearance + cabinetThickness
val storeyHeight = drawerHeight() + drawerClearance*2 + railSize.y
val totalHeight = storeyHeight * drawerCount + cabinetThickness * 2
val box = Square( width, totalHeight )
.centerX()
.cup( depth, cabinetThickness )
val panels = Panelling( cabinetPanelling )
.maxPanelSize( box, 150 )
.interlock()
.frontBackAndSidePanels( box )
// Holes for the clips to lock into
val clipHoles = Cube( cabinetThickness+1, clipSize.z+1+drawerClearance*2, clipSize.y-0.5 )
.center()
.translateX(width/2-cabinetThickness/2)
.centerYTo( cabinetThickness + drawerClearance + drawerHeight()/2 )
.topTo( box.top - 4 )
.repeatY( drawerCount, storeyHeight )
.mirrorX().also()
// Rails for the drawers to run on
val rails = Square( railSize )
.roundCorner(3,0.5,1)
.roundCorner(0,0.5,1)
.rightTo( box.right - cabinetThickness )
.backTo( cabinetThickness + storeyHeight )
.mirrorX().also()
.smartExtrude( depth - drawerThickness - drawerClearance )
.repeatY( drawerCount, storeyHeight )
// Follows the line of `rails`, but is for strength only.
val stiffenerHeight = 4
val backHorizontalStiffeners = Square( width, railSize.y )
.centerX()
.backTo( cabinetThickness + storeyHeight )
.smartExtrude( stiffenerHeight )
.top( Chamfer(0.5) )
.bottomTo( cabinetThickness )
.repeatY( drawerCount, storeyHeight )
val backVerticalStiffsers = Square( railSize.y, totalHeight )
.smartExtrude( stiffenerHeight )
.top( Chamfer(0.5) )
.bottomTo( cabinetThickness )
.leftTo( -width*0.4 + railSize.x )
.mirrorX().also()
return box + panels - clipHoles + rails + backHorizontalStiffeners + backVerticalStiffsers
}
@Piece( printable = false )
override meth build() : Shape3d {
val storeyHeight = drawerHeight() + drawerClearance*2 + railSize.y
val cabinet = cabinet().rotateX(90)
//val drawer = drawerCurvedHandle()
val drawer = drawerPanelled()
.color("Orange")
.backTo( cabinet.back - cabinetThickness - drawerClearance )
.bottomTo( cabinetThickness + drawerClearance )
val fullyOpen = -169
val drawers = drawer
.translate( 0, 1*fullyOpen, storeyHeight ).also()
return cabinet + drawers
}
}
