/Christmas/SpringoTree.foocad

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.
import uk.co.nickthecoder.foocad.threaded.v2.*
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.*
class SpringoTree : AbstractModel(), 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() -
Thread.springo( r1*2, 5, 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
}
@Piece( printable = false )
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" )
}
}
}

