Exit Full View
Up

/Christmas/SpringoTree.foocad

SpringoTree

A Springy Christmas Tree decoration. The trunk has a slot for a coin cell (CR2032). No on/off switch. Just take the battery out!

Print Notes

Don't use PLA, because it isn't springy. Rather than buy green PETG, I used transparent, and a green LED to light it ;-)

Assembly

Either bend the LED pins in a contorted manner so that it just sits in place. Or...

Drill holes for wires in the battery connections. Thread bare wires through the holes. Drill 2 holes in the top of the trunk for a regular LED. Solder the LED to the wires. The battery's internal resistance may be ok, but you could add extra resistance if you want.

If the LED is too focused, scratch/sand the lens to spread the light.

When the wiring is done and tested, glue all the pieces together.

FooCAD Source Code
import static uk.co.nickthecoder.foocad.circular.v1.Circular.*
import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.*
import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.*
import static uk.co.nickthecoder.foocad.changefilament.v1.ChangeFilament.*
import static uk.co.nickthecoder.foocad.chamferedextrude.v1.ChamferedExtrude.*
import uk.co.nickthecoder.foocad.threaded.v1.Threaded

class SpringoTree : Model, PostProcessor {
    
   // If the layers fuse together too much, adjust the gap.
    // I believe there is "aliasing" between the gap and the layer height.
    // The gap should be slightly less than a multiple of your layer height.
    // Look at the print, and you may see some parts have a clear gap, whereas
    // other places look fused. You want a very small "fused" section.
    // NOTE. Each slicer may give different results. I use slic3r.
    @Custom
    var gap = 0.39

    // The height of one piece of the slinky spring (including the gap).
    // If this is an exact multiple of the layer height, then the alias
    // artifacts described above line up.
    // I haven't investigated adjusing this.
    @Custom
    var pitch = 2.0

    var thickness = 3
    var baseSize = Vector2( 60, 1.2 )

    var r1 = 30
    var r2 = 20
    var rounded = 4
    var n = 5
    var height = 80

    var trunkSize = Vector2(18, 24)
    var trunkThickness = 1.2

    // Text on the base

    @Custom
    var textStyle = TextStyle(BOLD)
    @Custom
    var text1 = ""
    @Custom
    var text2 = ""
    
    @Custom
    var twist = 0.0

    fun profile( n:int, r1:double, r2:double, rounded:int ) = PolygonBuilder().apply {
        for ( i in 0 until n*2 ) {
            val angle = i * 360 / n / 2
            val r = if ( i % 2 == 0 ) r1 else r2
            val x = r * Degrees.cos(angle)
            val y = r * Degrees.sin(angle)
            if (i == 0) {
                moveTo( x, y )
            } else {
                lineTo( x, y )
            }
        }
    }.build().roundAllCorners( rounded )


    @Piece
    fun springoTree() : Shape3d {
        return tree() -
            Threaded.springo( r1*2, height-10,  pitch, gap ).bottomTo(baseSize.y)
    }

    @Piece
    fun star() : Shape3d {
        val star = profile( 5, 10, 4, 0 ).extrude( 1 )
        var attachment = (Cylinder( 10, 3, 0.5 ) - tree().topTo( 10-3 ) )
            .rotateY(90).translateX(-10) -
            Cube( 100 ).centerXY().topTo(0)

        return star + attachment
    }

    fun extrudeTwist( shape : Shape2d, height : double ) : Shape3d {
        return ExtrusionBuilder().apply {
            for ( i in 0 .. height ) {
                crossSection( shape.scale( (height - i)/height ) )
                turnZ(twist)
                forward( 1 )
            }
        }.build()
    }

    fun tree() : Shape3d {
        val profile : Shape2d = profile( n, r1, r2, rounded )
        val tree = extrudeTwist( profile, height ).color("Green")
        val remove = extrudeTwist( profile.offset( -thickness ), height-thickness*height/r1 ).translateZ(-0.01)
        val hollowTree = tree - remove

        val base = (profile - Circle( trunkSize.x/2 + 0.3 )).extrude( baseSize.y )

        return (hollowTree + base ).color("Green")
    }

    @Piece
    fun trunk() : Shape3d {
    
        val flat = (profile(n,r1-7,r2-5,rounded-2)-Circle(3)).extrude( baseSize.y )
        val trunk =
            Cylinder( trunkSize.y, trunkSize.x/2 ) -
            Cylinder( trunkSize.y*3, trunkSize.x/2 - trunkThickness ).center()
        val batterySlot = Cube( 4, 21, 21 ).centerX().bottomTo( trunkThickness )
      
        return (flat + trunk - batterySlot).color("Brown")
    }

    @Piece
    fun connectors() : Shape3d {
        val thickness = 1
        val h = 10
        val r = trunkSize.x/2 - trunkThickness - 0.3
        val ring = (Circle( r ) - Circle(r - thickness)).leftTo(1.3)
        val outsideP = Circle( r ) - Circle( r - thickness )
        val profile = ring / Circle( r ) + outsideP - Square( r*1.2 ).center().frontTo(r*0.5)

        val connector = profile.extrude( h )

        val connectors = connector.mirrorX().also()

        return connectors
    }


    @Piece
    fun base() : Shape3d {
        val base = profile( 10, baseSize.x/2, baseSize.x/2 * 0.8, 6 ).extrude( baseSize.y )
        val circle = Circle( trunkSize.x/2 - trunkThickness - 0.3 )
        val trunk = (circle - circle.offset( -trunkThickness*1.5 )).extrude(10)
        val batterySlot = Cube( 4, 21, 21 ).centerX().bottomTo( baseSize.y )

        val merry = Text( text1, textStyle ).center()
            .aroundCircle( baseSize.x * 0.3 )
        val xmas = Text( text2, textStyle ).center()
            .aroundCircle( -baseSize.x * 0.3 )
            
        val text = (merry + xmas).extrude( baseSize.y ).bottomTo( baseSize.y )
            .color( "GhostWhite" )

        val main = (base + trunk - batterySlot).color( "Green" )

        return main + text
    }

    override fun build() : Shape3d {
        val base : Shape3d = base()
        val trunk : Shape3d = trunk().mirrorZ().bottomTo(baseSize.y)
        val tree : Shape3d = tree().bottomTo(trunk.top)

        val connectors : Shape3d = connectors().bottomTo(base.top)

        val battery = Circle(10).chamferedExtrude( 3.2, 0, 0.7 ).rotateY(90).previewOnly()
            .bottomTo( baseSize.y+1 ).centerX().frontTo(-5)

        val star : Shape3d = star().rotateY(-90).bottomTo( tree.top - 4 )

        return tree + trunk + base + connectors + battery + star
    }

    override fun postProcess( gcode: GCode ) {
    }

    override fun postProcess( piece : String, gcode: GCode ) {
        if ( (text1!="" || text2 !="") && piece.startsWith( "base" ) ) {
            pauseAtHeight( gcode, baseSize.y, "Change Filament" )
        }
    }
}