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