Exit Full View
Up

/Garden/SeedsContainer.foocad

SeedsContainer

I want to keep my seeds in a box, so that I can rummage through them easily. The seeds are in paper envelopes about 15cm x 8cm, but some are wider than 8cm.

I highly recommend printing "testClasp" or "testHinge" before printing the main box, to check that the hinge's "slack" and "gap" settings are suitable for your printer.

Colors

  • Fruit and Veg : Red
  • Flowers : Orange / Yellow
  • Leafy Greens & Herbs : Green
FooCAD Source Code
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<int>(3,2), cornerRadius )
            .centerX()

        if (tab >= 0) {
            val tabWidth = (packetWidth - 10)/4
            profile += Square( tabWidth, tabHeight )
                .translateY(packetHeight)
                .roundCorners( listOf<int>(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<int>(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

    }

}