import static uk.co.nickthecoder.foocad.extras.v1.Extras.*
import static uk.co.nickthecoder.foocad.extras.v1.Extras.*
import uk.co.nickthecoder.foocad.smartextrusion.v1.SmartExtrusion
import static uk.co.nickthecoder.foocad.smartextrusion.v1.SmartExtrusion.*
import uk.co.nickthecoder.foocad.threaded.v2.*
import static uk.co.nickthecoder.foocad.threaded.v2.Thread.*
import static uk.co.nickthecoder.foocad.chamferedextrude.v1.ChamferedExtrude.*
class Avagojo : AbstractModel(), ModelWithSetup, PostProcessor {
@Custom
var ballDiameter = 6
@Custom
var gridSize = 10
@Custom
var maze = "avagojo-final.svg"
@Custom
var solutionHeight = 0.4
@Custom
var bottomLettering = Text( "" ) // or rules!
.hAlign(HAlignment.CENTER)
@Custom
var doubleHoles = false
var floorMargin = 4
val glassThickness = 2.0
var floorThickness = 2.0
// As a proportion of gridSize
val holeRadius = 0.38
val wallThickness = 0.15
val wallHeight = 4
val roomHeight = 7
val solutionRatio = 0.95
val wallChamferTop = gridSize * wallThickness * 0.3
val wallChamferBottom = gridSize * wallThickness * 0.3
val postDiameter = 6
val columnDiameter = postDiameter // For postThreaded only
var across = 10
var down = 10
var offset = Vector2(0,0)
@Custom
var topBottomThickness = 1.6
@Custom( about="For the side pieces" )
var grooveChamfer = 0.6
val slack = 0.3
var svgDocument : SVGDocument = null
override fun setup() {
svgDocument = SVGDocument( maze )
if (! svgDocument.shapes.containsKey( "playingArea" ) ) {
throw Exception( "ERROR. SVG document $maze must contain a shape with ID 'playingArea'" )
return
}
val playingArea = svgDocument["playingArea"]
//println( "Playing area size = ${playingArea.size}" )
across = playingArea.size.x / 10
down = playingArea.size.y / 10
offset = playingArea.corner
}
fun svgToWorld( svg : Vector2 ) : Vector2 = (svg - offset)/10 * gridSize
fun svgToGrid( svg : Vector2 ) : Vector2 = (svg - offset)/10
var mazeFloor : Shape3d
var mazeWalls : Shape3d
// The maze and walls printed as 1 piece
@Piece
fun maze() : Shape3d {
generate()
return mazeFloor + mazeWalls
}
// Just the maze walls. Also print `mazeFloor` and glue them together.
@Piece
fun mazeWalls() : Shape3d {
generate()
return mazeWalls.bottomTo(0)
}
// Just the maze floor, printed upside down, so that the play surface is
// smooth (against the printer's bed).
@Piece
fun mazeFloor() : Shape3d {
generate()
return mazeFloor.rotateX(180).bottomTo(0)
}
// Prints an SVG document to the log. Copy/Paste into a file to print it on a 2D printer.
@Piece( printable = false )
fun holes() : Shape3d {
var floor : Shape2d = Square( across * gridSize, down * gridSize )
var result : Shape3d = floor.extrude(0.1).color("White")
val width = floor.size.x
val height = floor.size.y
Log.println( "" )
Log.println( "" )
return result
}
fun generate() {
val margin = floorMargin + gridSize * wallThickness/2
var floor : Shape2d = Square( across * gridSize + margin*2, down * gridSize + margin*2 )
.roundAllCorners(5,1)
.translate(-margin, -margin)
var walls : Shape3d = Union3d()
val playingArea = svgDocument["playingArea"].paths[0]
var outerWalls : Shape3d = createPlainWall( Path2d( listOf(playingArea[0], playingArea[1]), false ) )
outerWalls += createPlainWall( Path2d( listOf(playingArea[1], playingArea[2]), false ) )
outerWalls += createPlainWall( Path2d( listOf(playingArea[2], playingArea[3]), false ) )
outerWalls += createPlainWall( Path2d( listOf(playingArea[3], playingArea[0]), false ) )
if (svgDocument.shapes.containsKey("ramp")) {
val ramp = svgDocument["ramp"]
val path = ramp.paths[0]
val rect = if (ramp.size.x > ramp.size.y ) {
Square(
gridSize * 4 - wallThickness*gridSize,
gridSize - wallThickness*gridSize
)
} else {
Square(
gridSize - wallThickness*gridSize,
gridSize * 4 - wallThickness*gridSize
)
}
.center()
.translate( svgToWorld(ramp.middle) )
floor -= rect
}
val post = extrudeWall(
Square( gridSize + gridSize*wallThickness )
.center()
.roundAllCorners( gridSize * wallThickness/2 )
)
.bottomTo( floorThickness )
.color( "lightBlue" ) -
Cylinder( 100, postDiameter/2 + slack ).center()
for ( key in svgDocument.shapes.keySet() ) {
val position = svgToWorld( svgDocument[key].middle )
// Check that the object is within the bounds of the playing area.
// This will filter out text/graphics for edges or bottom of the case.
if (position.x < floor.left || position.x > floor.right ||
position.y < floor.front || position.y > floor.back ) {
continue
}
if (key.startsWith( "hole" ) ) {
// Punch a hole in the floor
floor -= Circle( gridSize*holeRadius ).translate( position )
} else if (key.startsWith( "post" ) ) {
// Add a post to support the playing surface and the glass
walls += post.translate( position.x, position.y, 0 )
floor -= Circle( postDiameter / 2 + slack ).translate( position.x, position.y )
} else if (key.startsWith("path") ) {
val path = svgDocument[key].paths[0]
val wall = createWall( path )
walls += wall
} else if (key.startsWith("plain") ) {
val path = svgDocument[key].paths[0]
val wall = createPlainWall( path )
walls += wall
} else if (key.startsWith( "center" ) ) {
val center2d : Shape2d = Square( gridSize*3 )
.center() -
Circle( gridSize*3/2 - gridSize*wallThickness ) -
Square( gridSize ).center().translate( 0, gridSize*1.5 )
val rotated = if (key == "centerS") {
center2d.rotate(180)
} else if (key == "centerE") {
center2d.rotate(-90)
} else if (key == "centerW") {
center2d.rotate(90)
} else {
center2d
}
val center3d = rotated
.translate( position )
.chamferedExtrude( wallHeight, -wallChamferBottom, wallChamferTop )
.bottomTo( floorThickness )
.color( "lightBlue" )
walls += center3d
}
}
walls += outerWalls
val perimeter = Square( gridSize * (across + wallThickness), gridSize * (down + wallThickness) )
.roundAllCorners(gridSize * wallThickness/2)
.translate( -wallThickness*gridSize/2, -wallThickness*gridSize/2 )
.extrude( roomHeight )
mazeFloor = floor.chamferedExtrude( floorThickness, 0.5, 0 )
mazeWalls = walls.color( "lightBlue" )
}
fun endCap( svgPoint : Vector2 ) : Shape2d {
val grid = svgToGrid( svgPoint )
// Is it touching the perimeter?
return if (grid.x < 0.1 || grid.y < 0.1 || grid.x > across - 0.1 || grid.y > down - 0.1 ) {
Square(0)
} else {
// Is it touching the ramp?
if ( grid.y < 1.1 && grid.x >= across - 5.1) {
Square(0)
} else {
Circle( gridSize * wallThickness/2 ).sides(16)
.translate( svgToWorld(svgPoint) )
}
}
}
// Extrudes like this :
// / \ } wallTopChamfer
// | | \
// | | } wallHeight - wallTopChamfer - wallBottomChamfer * 2
// | | /
// / \ } wallBottomChamfer
// |___| } wallBottomChamfer
fun extrudeWall( wall : Shape2d ) : Shape3d {
return ExtrusionBuilder().apply {
crossSection( wall.offset( wallChamferBottom ) )
forward(wallChamferBottom)
crossSection()
forward(wallChamferBottom)
crossSection( - wallChamferBottom )
forward( wallHeight - wallChamferBottom*2 - wallChamferTop )
crossSection()
forward( wallChamferTop )
crossSection( -wallChamferTop )
}.build()
.bottomTo( floorThickness )
}
fun createWall( path : Path2d ) : Shape3d {
val endCaps = endCap( path[0] ) + endCap(path[-1] )
val wall2d = path.thickness( gridSize * wallThickness )
.translate(-offset)
.scale( gridSize / 10 )
return extrudeWall( wall2d + endCaps )
}
// Used for the outside, and the walls around the ramp.
// Has no bottom chamfer.
fun createPlainWall( path : Path2d ) : Shape3d {
val endCaps = Circle( gridSize * wallThickness/2 ).sides(16)
.translate( svgToWorld(path[0]) )
.translate( svgToWorld(path[-1]) - svgToWorld(path[0]) ).also()
val wall2d = path.thickness( gridSize * wallThickness )
.translate(-offset)
.scale( gridSize / 10 )
return (wall2d + endCaps)
.chamferedExtrude( wallHeight, 0, wallChamferTop )
.bottomTo( floorThickness )
}
// Optional. The path through the maze printed upside-down (thin).
// Can be glued onto piece `maze` to give a smooth playing suface.
// Not needed when using piece `mazeFloor` and `mazeWalls`, as these will already
// give a smooth 2 colour finish.
@Piece
fun solution() : Shape3d {
if (! svgDocument.shapes.containsKey( "solution" )) {
return Text( "No solution in SVG document" ).extrude(0.2)
}
val solution = svgDocument["solution"]
val path = solution
.scale( gridSize/10 )
.toOrigin()
.translate( svgToWorld( solution.corner ) )
.paths[0]
// TODO New Feather feature. Specify rounded corners, and end caps.
.thickness( (gridSize - gridSize*wallThickness - wallChamferBottom*2) * solutionRatio )
.roundAllCorners( 1 )
return path.extrude( solutionHeight )
.rotateY(180).bottomTo(0)
}
// A Single (simple) post.
// Consider this deprecated, as I prefer pairs of `postThreaded` and `postBase`.
@Piece
fun post() : Shape3d {
return ExtrusionBuilder().apply {
crossSection( Square(gridSize*0.9).center().roundAllCorners(2) )
forward( roomHeight )
crossSection()
crossSection( Circle( postDiameter/2 ) )
forward( floorThickness + roomHeight + slack )
crossSection()
}.build().color( "lightGrey")
}
@Piece
fun postPair() = postThreaded().backTo(-1) +
postBase().frontTo(1)
// The top part of a post, which support the glass.
// Screws into `postBase`.
@Piece
fun postThreaded() : Shape3d {
val thread = Thread( postDiameter )
val extra = 1
val base = 1
val thinner = postDiameter/2 + extra - columnDiameter/2
val post = ExtrusionBuilder().apply {
crossSection( Circle( columnDiameter/2 ) )
forward( roomHeight - wallHeight - extra )
crossSection( extra )
forward(base)
crossSection()
}.build()
val threaded = thread.threadedRod( roomHeight + wallHeight + floorThickness - 0.5 )
.chamferBottom( false )
.bottomTo( post.top )
return post + threaded
}
// The bottom half of `postThreaded`. This supports the playing area, and
// `postThreaded` supports the glass.
@Piece
fun postBase() : Shape3d {
val thread = Thread( postDiameter )
return Square(gridSize*0.9).center().roundAllCorners(2)
.extrude( roomHeight ) -
thread.threadedHole( roomHeight )
}
// A 2D shape for the edges of the game.
// Contains 3 grooves for the glass, the mazeFloor and the base.
fun edgeProfile() : Shape2d {
val basic = Square( 10, topBottomThickness*2 + floorThickness*2 + roomHeight*2 + glassThickness + slack )
.roundCorner(3,4)
.roundCorner(0,4)
.frontTo( -floorThickness - topBottomThickness - slack )
val baseCut = Square( 10, floorThickness + slack )
.leftTo( basic.right - floorMargin - slack )
.frontTo( basic.front + topBottomThickness )
val glassCut = Square ( 10, glassThickness + slack )
.leftTo( basic.right - floorMargin - slack )
.backTo( basic.back - topBottomThickness )
val floorCut = Square( 10, floorThickness + slack )
.leftTo( basic.right - floorMargin-slack )
.centerYTo( (glassCut.front + baseCut.back)/2 )
val chamfer = Triangle( grooveChamfer, grooveChamfer ).mirrorX().rightTo( basic.right + 0.01 )
val chamfers : Shape2d = if (grooveChamfer > 0) {
chamfer.frontTo( baseCut.back ).mirrorY().backTo( baseCut.front ).also(2) +
chamfer.frontTo( glassCut.back ).mirrorY().backTo( glassCut.front ).also(2) +
chamfer.frontTo( floorCut.back ).mirrorY().backTo( floorCut.front ).also(2)
} else {
Square(0)
}
return (basic - baseCut - floorCut - glassCut - chamfers)
}
@Piece
fun edgesFrontAndBack() = edgeFront().backTo(-1) +
edgeBack().frontTo(1)
// All four edge pieces. `edgeFront`, `edgeBack`, 2x edgeSide.
@Piece
fun edges() : Shape3d {
val sides = edgeSide().frontTo(0)
.backTo(-2).also()
val front = edgeFront()
.backTo(sides.front-1)
val back = edgeBack()
.backTo(front.front-2)
return sides + front.backTo( sides.front -2 ) + back
}
// Decorates `edgeFrontOrBack` with a protruding pattern svg ID `front`.
@Piece
fun edgeFront() : Shape3d {
val edge = edgeFrontOrBack().centerXY()
val text = if ( svgDocument.shapes.containsKey( "front" ) ) {
svgDocument["front"]
.center()
.extrude(0.6)
//.topTo(edge.top+ 0.1)
.bottomTo(edge.top)
} else {
null
}
return edge + text
}
fun withEars( edge : Shape3d ) : Shape3d {
val ears = Square( edge.size.y, edge.size.y*1.5 )
.roundAllCorners(5)
.leftTo( edge.right - 4 )
.centerYTo( edge.middle.y )
.margin( -edge.size.y )
.extrude(0.2)
.mirrorX().also()
.color( "White" ).opacity(0.3)
return edge + ears
}
fun edgeBackWithEars() = withEars( edgeBack() )
// Decorates `edgeFrontOrBack` with a protruding pattern svg ID `back`.
@Piece
fun edgeBack() : Shape3d {
val edge = edgeFrontOrBack().centerXY()
val text = if ( svgDocument.shapes.containsKey( "back" ) ) {
svgDocument["back"]
.center()
.extrude(0.6)
.bottomTo(edge.top)
} else {
null
}
return edge + text
}
// The front or back edges without text or other decoration.
fun edgeFrontOrBack() : Shape3d {
val length = gridSize * across + wallThickness*gridSize + slack
val solid = edgeProfile()
.extrude( length )
.rotateY(90)
.centerX()
.bottomTo(0)
var screwHoles : Shape3d = Cylinder(10,2.3).sides(8)
.rotateY(90)
.translateY( roomHeight/2 + roomHeight + floorThickness + slack )
.rightTo( length/2+1 )
.translateZ( solid.size.z * 0.5 )
.mirrorX().also()
if (doubleHoles) {
screwHoles = screwHoles.translateY( - roomHeight - floorThickness - slack ).also()
}
return (solid - screwHoles ).color("Orange").brighter()
}
@Piece
fun edgeSizeWithEars() = withEars( edgeSide() )
@Piece
fun edgeSide() : Shape3d {
val length = gridSize * down + wallThickness*gridSize + slack
val profile = edgeProfile().rightTo(0)
val solid = ExtrusionBuilder().apply {
crossSection( profile )
for (i in 0 until 9) {
turnY(10)
crossSection()
}
forward(length/2)
crossSection()
}.build().rightTo(0).mirrorX().also()
var screwHoles : Shape3d = (
Cylinder( 30, 1.8 ).translateZ(-1) +
Cylinder( 30, 3.5 ).translateZ(solid.size.z * 0.45)
)
.translateX( length / 2 + solid.size.z*0.5 )
.translateY( roomHeight/2 + floorThickness + slack + roomHeight)
.mirrorX().also()
if ( doubleHoles ) {
screwHoles = screwHoles.translateY( -floorThickness - slack - roomHeight ).also()
}
return (solid - screwHoles).color("Orange").darker()
}
// The flat bottom of the game.
@Piece
fun base() : Shape3d {
val margin = floorMargin + gridSize * wallThickness/2
var floor = Square( across * gridSize + margin*2, down * gridSize + margin*2 )
.roundAllCorners(5,1)
.center()
.chamferedExtrude( floorThickness, 0.2 )
val extra = Square(
across * gridSize + wallThickness*gridSize -2,
down * gridSize + wallThickness*gridSize -2
)
.center()
.extrude( topBottomThickness )
.bottomTo( floorThickness )
val text = bottomLettering
.center()
.mirrorX()
.extrude(0.6).topTo( extra.top )
return (floor + extra - text).color("darkGrey").darker()
}
// An extra piece to move the ball from the basement to the playing surface.
// Glue to the bottom of `maze` (or `mazeFloor`).
@Piece
fun ramp() : Shape3d {
val ramp = Triangle( gridSize * 4, roomHeight + floorThickness + slack)
.extrude( gridSize - wallThickness*gridSize - slack*2 )
.rotateX(90)
val side = Cube( ramp.size.x, wallThickness*gridSize + slack, roomHeight )
.backTo( ramp.front )
val sides = side.frontTo( ramp.back ).also()
val guide = Triangle( gridSize, side.size.y )
.extrude( side.size.z )
.leftTo( side.right )
.frontTo( side.front )
// Prevents the ball being "caught" in the right angled corner.
val angle = Triangle( sides.size.y*1.5, sides.size.y )
.mirrorX()
.extrude( roomHeight )
.rightTo( ramp.left )
.backTo( sides.back )
return (ramp + sides + guide + angle).color("yellow").darker()
}
override fun postProcess( gcode : GCode ) {
}
@Piece( printable = false )
override fun build() : Shape3d {
val maze = maze()
.centerXY()
.bottomTo( topBottomThickness + floorThickness + roomHeight + slack/2 )
val sides = edgeSide()
.rotateX(90).rotateZ(-90)
.bottomTo(0)
.rightTo( maze.left + floorMargin - slack/2 )
.mirrorX().also()
val front = edgeFront()
.rotateX(90)
.bottomTo( 0 )
.frontTo( sides.front -0.6)
val back = edgeBack()
.rotateX(90).rotateZ(180)
.bottomTo( 0 )
.backTo( sides.back +0.6 )
val base = base().mirrorZ().bottomTo( slack/2 )
val glass = Square( maze.size.x, maze.size.y )
.center()
.roundAllCorners(3)
.extrude( glassThickness )
.topTo( front.top - topBottomThickness - slack/2 )
.previewOnly()
val ramp = ramp()
.centerYTo( maze.front + floorMargin + wallThickness*gridSize/2 + gridSize/2 )
.rightTo( maze.right - floorMargin )
.bottomTo( base.top )
val solution = solution()
.rotateY(180).bottomTo( maze.bottom + floorThickness )
.translate( -gridSize*across/2, -gridSize*down/2, 0 )
.color("purple")
val ball = Sphere( ballDiameter/2 )
.bottomTo( solution.top )
.translate( -2.5*gridSize, 0*gridSize, 0 )
.color( "Silver" )
.previewOnly()
val ballBasement = ball.bottomTo( base.top )
val postThreaded = postThreaded()
.rotateY(180)
.center().topTo(glass.bottom)
val postBase = postBase()
.bottomTo( base.top )
return Cube(0) +
maze +
//ball +
ballBasement +
sides +
front +
back +
//postThreaded + //.translateX(50) +
//postBase + //.translateX(40) +
base +
ramp +
//solution +
//glass +
Cube(0)
}
}