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(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) ) walls += createPlainWall(Path2d(listOf(path[2], path[3]), false)) walls += createPlainWall(Path2d(listOf(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) } }