/Games/Avagojo.foocad
SVG Rules
You can create mazes by drawing an SVG diagram of the maze, using Inkscape.
I strongly recommend you copy
an existing maze, rather than starting from scratch,
because it has sensible document properties, such as a grid.
All objects must be ungrouped, and in the "default" layer.
Every object must have an ID which specifies what type of object it is. Use Ctrl+Shift+O in Inkscape to edit an object's ID. There are two pieces which MUST be present :
playingArea
ramp
: A 4x1 rectangle.
Optional items with fixed IDs :
solution
: A path, showing the solution of the maze.center
: A 3x3 circle for the center of the maze. This may have a suffix of "N", "S", "E" or "W" to indicate the direction of the exit.
The remaing IDs must be prefixed as follows :
hole
: The text afterhole
may be used to print the hole numbers on the floor???path
A wall (where X is anypost
A post to support the playing area and the glass.
All wall nodes must be at a grid intersection. (Enable Snapping -> Nodes -> Cusp nodes)
All other objects must have their bounding box corners at a grid intersection. (Use
Alt
while dragging, and enable Snapping -> Bounding Boxes -> Corners).Walls may be a path containing many line segments. However, I find it easier to make each wall a single line (either horizontal or vertical). Warning, multi-line paths can cause issues when using a large wall chamfer.
All other attributes, such as color, line thickness etc. are ignored.
Printing the Maze Piece
The playing surface should be as smooth as possible. I have two ideas :
Print the maze and the walls separately, with the floor of the maze on the print bed. Then glue them afterwards.
Pause the print just before the walls, and use a clothes iron with a PTFE sheet. Practive on a piece of scrap first??? PTFE sheets can be found in kitchenware. Search for non-stick oven liner/baking sheet.
A thin line, which is the solution though the maze Printed upside down, and glued. This will affect the game play, as the ball will roll off the strip, and into the hole.
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 : ModelWithSetup, PostProcessor { @Custom var ballDiameter = 6 @Custom var gridSize = 10 // TODO Make this 11? @Custom var maze = "avagojo-final.svg" @Custom var solutionHeight = 0.4 @Custom var bottomLettering = Text( "" ) // or rules! .hAlign(HAlignment.CENTER) 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) val topBottomThickness = 1 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) } 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<Vector2>(playingArea[0], playingArea[1]), false ) ) outerWalls += createPlainWall( Path2d( listOf<Vector2>(playingArea[1], playingArea[2]), false ) ) outerWalls += createPlainWall( Path2d( listOf<Vector2>(playingArea[2], playingArea[3]), false ) ) outerWalls += createPlainWall( Path2d( listOf<Vector2>(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) ) walls += createPlainWall(Path2d(listOf<Vector2>(path[2], path[3]), false)) walls += createPlainWall(Path2d(listOf<Vector2>(path[3], path[0]), false)) 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( "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" ) //(walls intersection( perimeter )).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") } // 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 ) return (basic - baseCut - floorCut - glassCut) } // All four edge pieces. `edgeFront`, `edgeBack`, 2x edgeSide. @Piece fun edges() : Shape3d { val sides = edgeSide().frontTo(0) .backTo(-2).also() val front = edgeFront() .frontTo(0) val back = edgeBack() .frontTo(0) .backTo(-2).also() 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) .bottomTo(edge.top) } else { null } return edge + text } // 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) val screwHoles = Cylinder(10,2).sides(8) .rotateY(90) .translateY( roomHeight/2 ) .translateY( roomHeight + floorThickness + slack ).also() .rightTo( length/2+1 ) .translateZ( solid.size.z * 0.5 ) .mirrorX().also() return (solid - screwHoles).color("Orange").brighter() } @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() val screwHoles = ( Cylinder( 30, 1.5 ).sides(8).translateZ(-1) + Cylinder( 30, 3 ).translateZ(solid.size.z * 0.25) ) .translateX( length / 2 + solid.size.z*0.5 ) .translateY( roomHeight/2 ) .translateY( floorThickness + slack + roomHeight ).also() .mirrorX().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 ) return (ramp + sides + guide).color("yellow").darker() } override fun postProcess( gcode : GCode ) { } 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) } }