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