import static uk.co.nickthecoder.foocad.along.v2.Along.* import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.* import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.* import static uk.co.nickthecoder.foocad.extras.v1.Extras.* include Hinge.feather class SeedsContainer : Model { // What is the size of the packet of seeds? Make the the maximum of all your packets, // plus a little extra. // A standard index card is 6x4 inches, but I'm setting the height a little less. val packetWidth = 6*25.4 // 6 inches val packetHeight = 90 // The "Y" direction. This is arbitrary. Make it bigger to fit in more packets. // Note. 120 gives a total depth of 302mm, which is slightly bigger than my7 printer's // stated size, but it still printed ok! val depth = 120 // 80 // The angle of the slope, so that the base is narrower than the opening. // An angle makes the packets naturally fan out, so you can see the packets easier. val angle = 10 // Height of the non-sloped part where the two halves meet. var flat = 10 // The thicknesses of the flat part. Thicker than most of the box for // extra rigidity. var flatT = 2.2 // The thickness of the sides (and bottom) of the box. var thickness = 1.2 // How much extra space is there above the divider's tab. var extraHeight = 2.0 // The height of the lip, which helps align the two halves. val lipHeight = 3 // The thickness of the lip, which helps align the two halves. val lipThickness = 1.0 // The slack between the lip, and the opening that it mates with. // Note, this should NOT be an interference fit. var slack = 0.4 var dividerThickness = 0.8 var tabHeight = 10 var chamfer = 2 var cornerRadius = 5 // The size of the pin at the bottom of each divider var pinDiameter = 5 // Note, the length of the pin is actually smaller than this. // I didn't leave enough "gap", so the dividers ended up too tight. // So as a bodge, I shrank the pins, but the rest of the code uses the // existing value. var pinLength = 5 val claspBuilder = ClaspBuilder( 40, 2.4 ) fun baseWidth() = packetWidth + thickness*2 + pinLength*2 + 1 fun baseHeight() = (packetHeight + tabHeight + thickness + extraHeight)*0.5 fun baseDepth() = depth + depth*Degrees.sin(angle) @Piece fun divederA() = divider(true,0) @Piece fun dividerB() = divider(true,1) fun divider(otherHalf : bool, tab: int) : Shape3d { var profile : Shape2d = Square( packetWidth, packetHeight ) .roundCorners( listOf(3,2), cornerRadius ) .centerX() if (tab >= 0) { val tabWidth = (packetWidth - 10)/4 profile += Square( tabWidth, tabHeight ) .translateY(packetHeight) .roundCorners( listOf(3,2), 5 ) .leftTo( -packetWidth/2 + 5 + tab * tabWidth ) } val sheet = profile .extrude( dividerThickness ) val pin = Sector( pinDiameter/2, 90, 270) .extrude( packetWidth + pinLength*1.5 ).alongX() .centerX() return if (otherHalf) { val otherHalfPin = ( Sector( pinDiameter/2, 90, 270).translateX(dividerThickness) / Square( pinDiameter/2, pinLength*1.5 ).centerY().rightTo(0) ).extrude( packetWidth + pinLength*1.5 ).alongX().centerX() sheet + pin + otherHalfPin.translateY(-pinDiameter-2) } else { sheet + pin } } fun topProfile() = Square( baseWidth(), depth + depth * Degrees.sin(angle) ) .center().roundAllCorners(cornerRadius, 6) fun half() : Shape3d { val width = baseWidth() val bottom = Square( width, depth ) .center().roundAllCorners(cornerRadius, 6) val top = topProfile() val height = baseHeight() val box = ExtrusionBuilder().apply { joinStrategy = OneToOneJoinStrategy() crossSection( bottom.offset(-chamfer) ) forward( chamfer ) crossSection( bottom ) forward( height -flat -chamfer ) crossSection( top ) forward(flat) crossSection() crossSection( -flatT ) forward(-flat + flatT-thickness) crossSection() forward( -flatT + thickness ) crossSection( flatT - thickness ) forward( flat-height + thickness + chamfer ) crossSection( bottom.offset( -thickness ) ) forward( -chamfer ) crossSection( -chamfer ) }.build() return box } fun base() : Shape3d { val half : Shape3d = half() val pinRetainers = Square( pinLength, depth+thickness ) .roundCorners(listOf(3,0),5) .extrude(pinDiameter) .centerY() .bottomTo( thickness + pinDiameter + 2 ) .leftTo( half.left + thickness ) .mirrorX().also() .color("Orange") return if ( depth > 80 ) { val post = Cube( pinLength, pinLength, pinRetainers.bottom - thickness ) .bottomTo( thickness ) .centerY() .leftTo( pinRetainers.left ) .mirrorX().also() .color("Orange") half + pinRetainers + post } else { half + pinRetainers } } fun lid() : Shape3d { val half : Shape3d = half() val profile = topProfile().offset(-thickness) val lip = ExtrusionBuilder().apply { crossSection( profile ) crossSection( -lipThickness-slack ) forward( lipHeight ) crossSection() crossSection( -lipThickness ) forward( -lipHeight - lipThickness/2 ) crossSection() forward( -lipThickness - slack ) crossSection( profile ) }.buildClosed().translateZ( baseHeight() ) return lip + half } /** My first couple of prints didn't include a lip (or was wrong), so I created this to print just a lip, which I can glue on. */ fun lip() : Shape3d { val profile = topProfile().offset(-flatT-0.2) val rim = ExtrusionBuilder().apply { crossSection( profile ) forward( lipHeight ) crossSection() crossSection( -slack ) forward( lipHeight ) crossSection() crossSection( -lipThickness ) forward( - lipHeight -lipThickness/2 ) crossSection() forward( -lipHeight + lipThickness/2 ) crossSection( profile.offset( -lipThickness ) ) }.buildClosed() return rim } @Piece fun box() : Shape3d { // NOTE, These have "slack" parameters, but we are using the defaults. // If you attempt to print fast and/or with large layer heights, you may // need to increase the "slack" values. val hinge = Hinge( 10, 5, baseWidth()-20, 2 ).rotateZ(90) val base : Shape3d = base().backTo(hinge.front) val lid : Shape3d = lid().frontTo(hinge.back) val lug = claspBuilder.buildLug() .rotateZ(-90) .backTo(base.front) .translateZ(base.top) .color("Red") // The 0.2 is "slop", without which the two halves may not want to fully close. val result = base + lid + hinge.translateZ( base.size.z + 0.2 ) + lug.rotateZ(180).also() println( "Box size : ${result.size}" ) return result } @Piece fun testHinge() : Shape3d { val width = 40 val hinge = Hinge( 10, 5, width, 1 ) return hinge.toOriginZ() + Cube( 2, width, hinge.outerD ) .centerY() .leftTo( hinge.right ) .mirrorX().also() } @Piece fun testClasp() : Shape3d { val lug = claspBuilder.buildLug() return lug.rotateX(180).also() + claspBuilder.buildClasp() } @Piece fun test() : Shape3d { val width = claspBuilder.length val hinge = Hinge( 10, 5, width, 2 ) val depth = 10 val height = hinge.outerD + 1 val t = 0.8 val body = (Cube( width, depth, height ) .centerX()- Cube( width-t*2, depth-t*2, height ) .centerX() .translate(0,t,t) ).frontTo( hinge.right ) val lug = claspBuilder.buildLug() .rotateZ(90) .frontTo(body.back) .translateZ(height) return hinge.rotateZ(90).translateZ(height) + body.mirrorY().also() + lug.rotateZ(180).also() } override fun build() : Shape3d { return box() } } /** A general purpose class for creating a clasp, which consists of two lugs, and a clasp which slides over one lug, and locks over the second lug. The clasp needs to be man-handled onto the first lug, and therefore I suggest not using PLA (as this is very stiff). I used PETG for the clasp parts (and PLA for the lugs on the main body). */ class ClaspBuilder( val length : double, val thickness : double ) { // The radius of corners, to compensate for internal corners being rounded. // i.e. Too much plastic in internal corners, so we remove some plastic from // external corners var radius = 0.5 // How much smaller is the lugs compared to the clasp in which they fit? var slack = 0.5 // was 0.4, then 0.6 // Extra space, so that the clasp doesn't have to go all the way home for it // to clear the 2nd lug. var gap = 1.0 // The radius of the curve on the front (decorative only) var frontR = 3 fun lugProfile() = PolygonBuilder().apply { radius( radius ) moveTo( thickness, -thickness ) lineTo( thickness, 0 ) lineTo( thickness * 2, 0 ) lineTo( thickness * 2, -thickness*1.5 ) radius(0) lineTo( 0, -thickness*3.5 ) lineTo( 0, -thickness ) }.build() fun claspHalfProfile() = PolygonBuilder().apply { moveTo( -slack, thickness*1.5 ) radius(radius+slack) lineTo( thickness, thickness*1.5 ) radius(0) lineTo( thickness, thickness*0.5 ) lineTo( thickness*2, thickness*0.5 ) lineTo( thickness*2, thickness*2 ) lineTo( -slack, thickness* 4+slack ) lineTo( -slack, thickness* 5.7+slack ) radius( frontR +slack ) lineTo( thickness*3, thickness*2.7 ) radius( 0 ) lineTo( thickness*3, -slack ) lineTo( -slack, -slack ) }.build().offset(-slack) fun buildLug() = lugProfile() .translateY(-thickness/2) .extrude( length/2 -thickness-gap) .translateZ(thickness) .rotateX(90) fun buildClasp() : Shape3d { val halfP = claspHalfProfile() val wholeP = halfP.mirrorY().also() val solidP = Hull2d(wholeP) val main = wholeP .extrude( length ) .centerZ() val solidEnd = solidP.extrude(thickness) .topTo( -length/2 ) val halfEnd = (Hull2d( halfP ) + wholeP).extrude(thickness) .bottomTo( length/2 ) + // And a very thin part, than we can cut away, but this allows a // brim to be added, which doesn't interfere with the inside. // A brim may help if you have problems with the long thin part lifting. Cube(0.4, wholeP.size.y - thickness, 0.4).topTo( length/2 + thickness ).centerXY() val result = (main + solidEnd + halfEnd) .rotateX(90) .color("Green") // Blunt the corners a little val roundedCorners = Square( wholeP.size.y - slack*3, length + thickness*2 ).center() .roundAllCorners(3) .extrude(result.size.x) .rotateY(90) return result / roundedCorners } }