Exit Full View
Up

/Boxes/ComponentOrganiser.foocad

ComponentOrganiser
FooCAD Source Code
/**
    A storage container designed for my electronics components (but will
    be suitable for other uses too).

    Each drawer contains a number of tubs (inserts).
    The inserts can be different sizes, but are all integer multiples of the
    smallest insert.

    I have chosen to use the same size inserts as used by
    Raaco's "A Series", because I already have some of these,
    as well as other brands which use the same "standard" size.

    Unlike the Raaco system, I don't want lids on each drawer.
    I would like to open the drawer fully, so that I can get to all of the tubs
    without the drawer falling out.

    I want a mechanism to prevent the drawer being pulled out too far.

    Each drawer front should have a place for a label.

    The inserts have small feet, which fit into circles in the drawers.
    The feet ensure the tubs line up correctly, even when some tubs are missing.
    To print the feet, we probably need to print the tubs upside down;
    the base is printed using bridging - you will need good part cooling!

    Many cabinets can be glued together to form a large array.

    See Racco's solutions :
        https://www.raacostorage.co.uk/PBSCCatalog.asp?CatID=2279874
        https://www.raacostorage.co.uk/a-range-inserts-c102x2279884
    FYI. The "55" refers to the depth (y-axis) of the smallest insert.
    This is also referred to as the "A" range.

    You could fix the cabinets to the wall using screws, but the existing model
    doesn't help you out! Consider tweaking the code to add "keyholes" on
    the cabinet and adjust the "extra" space so that the drawers still fit.

Print Notes

    I use a "coarse" profile, with 0.3mm layers and high speed moves.
    (except for the 1st layer which I like to take slow).

    Consider using a 0.8mm nozzle with maybe 0.6mm layers???
   
*/

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 : Model {
    
    // 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

    var earDiameter = 30

    // If true, then the inserts are printed bottom down without feet.
    // You can glue feet on yourself afterwards.
    var withoutFeet = true

/*
    var piece = ""
    override fun pieceNames() = listOf<String>(
        "drawer", "drawerBox", "cabinet",
        "feet", "64feet",
        "insert1x1", "insert1x2", "insert1x3", "insert1x4", 
        "insert2x1", "insert2x2", "insert2x4", "insert2x4",
        "insert3x1", "insert3x2", "insert3x3",
        "insert4x1", "insert4x2"
    )
    override fun setPieceName( name : String ) { piece = name }
*/

    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
    }

    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()
        }
    
        // TODO Feather Bug. If the type declaration is removed, we get a cryptic error. 
        // at RUNTIME
        val ears : Shape3d = if (earDiameter <= 0 ) {
            Cube(0)
        } else {
            val foo : Shape3d =
                Circle( earDiameter/2 )
                    .translateX(width/2).mirrorX().also()
                    .extrude( 0.3 ) +
                Circle( earDiameter/2 )
                    .translate(width/2-2, depth-2 ).mirrorX().also()
                    .extrude( 0.3 )
            foo
        }
        

        val all = drawer.color("Green").brighter() +
            ears +
            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)

         val ears = if (earDiameter <= 0 ) {
            Cube(0)
        } else {
            Circle( earDiameter/2 )
                .translateX(all.size.x/2).mirrorX().also()
                .translateY(all.size.y).also()
                .extrude( 0.3 )
                .color("Green")
        }

        return all + ears
    }


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

    fun insert( x : int, y : int ) : Shape3d {
        
        val width = unitWidth * x - insertGap
        var depth = unitDepth * y - insertGap
        val height = unitHeight - insertGap
    
        val profile = Square(
            width - insertTaper*2 - insertThickness * 2,
            depth - insertTaper*2 - insertThickness * 2
        ).center().roundAllCorners(insertRadius,6)
       
        // 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 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 )

    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
    }

}