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.* class ComponentOrganiser : AbstractModel() { // 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 // If true, then the inserts are printed bottom down without feet. // You can glue feet on yourself afterwards. var withoutFeet = true 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 } meth lidPattern() : Shape2d { val outside = insertShape(1,1).offset( insertTaper - 1 ) val inside = outside.offset(-1.2) val one = (outside - inside) return one .repeatY( drawerY, unitDepth ) .repeatX( drawerX, unitWidth ) .center() } 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() } val all = drawer.color("Green").brighter() + 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) return all } 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 ) } meth insertShape( x : int, y : int ) : Shape2d { val width = unitWidth * x - insertGap var depth = unitDepth * y - insertGap return Square( width - insertTaper*2 - insertThickness * 2, depth - insertTaper*2 - insertThickness * 2 ).center().roundAllCorners(insertRadius,6) } fun insert( x : int, y : int ) : Shape3d { val height = unitHeight - insertGap val profile = insertShape( x, y ) // 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 feet16() = footProfile().extrude(shoeHeight-0.2) .tileX(4, 0.5) .tileY(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 ) @Custom var tabHeight = 6.0 /** Designed to fit 5 smaller containers inside a 1x2 insert, specifically for through-hole resistors. */ @Piece fun forReistors() = arrangeY( 0.7, forResistors( 0 ), forResistors( 1 ), forResistors( 2 ), forResistors( 3 ), forResistors( 4 ) ).centerY() + insert1x2().rotateZ(90) .centerY() .bottomTo( -insertThickness ) .previewOnly() fun forResistors( tabPosition : int ) : Shape3d { val width = unitDepth * 2 - insertThickness*2 - insertTaper * 2 - 1 val depth = (unitWidth - insertThickness*2 - insertTaper * 2) / 5 - 0.5 val height = unitHeight - insertGap - insertThickness - tabHeight - 1 val thickness = insertThickness val profile = Square( width, depth ).center().roundAllCorners(2,4) val main = ExtrusionBuilder().apply { crossSection( profile ) forward( height ) crossSection() crossSection( - thickness ) forward( -height + thickness ) crossSection() }.build() val tabWidth = (main.size.x - 4) / 5 val tab = Square( tabWidth, tabHeight ) .roundCorner(3,3) .roundCorner(2,3) .extrude( thickness ).rotateX(90) .bottomTo( main.top ) .backTo( main.back ) .leftTo(main.left + 2 + tabPosition * tabWidth ) return main + tab } @Piece fun sixth() : Shape3d { val width = (unitDepth * 2 - insertThickness*2 - insertTaper * 2) / 3 - 1.0 val depth = (unitWidth - insertThickness*2 - insertTaper * 2) / 2 - 1.0 val height = unitHeight - insertGap - insertThickness - tabHeight - 1 val thickness = insertThickness val profile = Square( width, depth ).center().roundAllCorners(2,4) val main = ExtrusionBuilder().apply { crossSection( profile ) forward( height ) crossSection() crossSection( - thickness ) forward( -height + thickness ) crossSection() }.build() val tabWidth = (main.size.x - 4) val tab = Square( tabWidth, tabHeight ) .roundCorner(3,3) .roundCorner(2,3) .extrude( thickness ).rotateX(90) .bottomTo( main.top ) .backTo( main.back ) .leftTo(main.left + 2 ) val one = main + tab val set = one.tileX(3, 1).tileY(2, 1).centerXY() return set + insert1x2().rotateZ(90) .centerY() .bottomTo( -insertThickness ) .previewOnly() } @Piece( printable = false ) 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 } }