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.* /** Cool down your drinks with chilled stainless steel balls. You can buy them in bulk but you'll need a container to put them in! The simplest way to stack them is to print 2 or more sets of 3x2 or 4x3 or 5x4 etc. They then stack by rotating by 90 degrees. */ class WhiskeyBallTray2 : Model { @Custom var lid = true @Custom var across = 2 @Custom var down = 2 @Custom var ballSize = 21 @Custom var thickness = 2 @Custom var gap = 1.0 @Custom var pegHeight = 3.0 @Custom var pegSize = 9.0 @Custom var earSize = 15 fun arrange( shape : Shape3d ) : Shape3d { val list = listOf() val dx = (ballSize + gap) for ( y in 0 until down ) { for ( x in 0 until across ) { list.add( shape.translate( x * dx , y * dx, 0)) } } return Union3d( list ) } fun pegProfile() : Shape2d { return PolygonBuilder().apply { moveTo( pegSize, 0 ) circularArcTo( 0, pegSize, pegSize, false, false ) circularArcTo(-pegSize, 0, pegSize, false, false ) circularArcTo( 0,-pegSize, pegSize, false, false ) circularArcTo( pegSize, 0, pegSize, false, false ) }.build() / Square( pegSize * 1.2 ).center() } fun pegs() : Shape3d { return pegProfile() .chamferedExtrude( pegHeight, 0, 0.6 ) .repeatX( across - 1, ballSize + gap) .repeatY( down - 1, ballSize + gap ) .centerXY() .translateZ( ballSize/2 + thickness - 0.3 ) } fun holes() : Shape3d { val slack = 0.2 val zExcess = 1.6 return pegProfile().offset(slack) .chamferedExtrude( pegHeight + zExcess + 0.01, 0.6, 0 ) .repeatX( across - 1, ballSize + gap) .repeatY( down - 1, ballSize + gap ) .centerXY() .translateZ( ballSize/2 + thickness - pegHeight -zExcess ) } // These are an attempt to prevent the piece lifting off the bed, by // removing long lengths of plastic, and therefore reducing the force from // shrinkage when the part cools. fun notches() : Shape3d { return Cylinder( ballSize + thickness + 1, 4 ).sides(4) .repeatX( across - 1, ballSize + gap ).centerX() .translateY( down / 2 * (ballSize + gap) + thickness + 0 ) .mirrorY().also() + Cylinder( ballSize + thickness + 1, 4 ).sides(4) .repeatY( down - 1, ballSize + gap ).centerY() .translateX( across / 2 * (ballSize + gap) + thickness + 1 ) .mirrorX().also() } fun ears() : Shape3d { return Circle( earSize ).translate( (ballSize + gap)/2 * across, (ballSize + gap)/2 * down ) .mirrorX().also() .mirrorY().also() .extrude(0.2) } override fun build() : Shape3d { val profile = Square( (ballSize+gap) * across + thickness * 2, (ballSize + gap) * down + thickness * 2 ).roundAllCorners( ballSize/2+thickness - 4 ).center() val solid = profile.chamferedExtrude( ballSize/2 + thickness, 3, 1 ) val holes = arrange( Sphere(ballSize/2).translateZ(ballSize/2 + thickness) ).centerXY() val result = solid - holes val withoutEars = if (lid) { result - holes() - notches() } else { result + pegs() - notches() } return if (earSize > 0) { withoutEars + ears() } else { withoutEars } } }