import static uk.co.nickthecoder.foocad.along.v2.Along.* import static uk.co.nickthecoder.foocad.changefilament.v1.ChangeFilament.* import static uk.co.nickthecoder.foocad.chamferedextrude.v1.ChamferedExtrude.* import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.* import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.* include ThickWalledBox.foocad class RoundedBox : ThickWalledBox { override fun profile() : Shape2d = Square( width, depth ).center().roundAllCorners( 1, 4 ) override fun inset( amount : double ) : Shape2d { return Square( width-amount*2, depth-amount*2 ).center().roundAllCorners( 1-amount,4 ) } override fun build() : Shape3d { return super.build() } } class WordGames : Model { // The final two blanks. Use the Customiser to reprint and failed/lost letters. @Custom( required = false ) var letters = "??" @Custom var pieceSize = Vector2(20, 4) @Custom var radius = 2 @Custom var chamfer = 0.4 @Custom var style = TextStyle( "Lato BLACK", 10 ) @Custom( about="Negative for inset, positive for raised text" ) var letterHeight = -0.6 @Custom( about="Should we include the tile's points at the bottom right of each tile?" ) var includePoints = true @Custom var pointsStyle = TextStyle( "Lato Black", 4 ) @Custom var romanNumerals = true var boardWallWidth = 2.0 var boardSlack = 0.5 var boardThickness = 2.0 var boardWallHeight = 1.0 var bonusThickness = 0.8 // For Raid Word's "fields" - where you build your words. var raidMargin = 3.0 fun numberAsString( value : int ) : String { if ( romanNumerals ) { return listOf( "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X" )[value] } else { return "$value" } } fun points( letter : char ) = if ( "ZQ".contains("$letter") ) 10 else if ( "JX".contains( "$letter") ) 8 else if ( "K".contains("$letter") ) 5 else if ( "FHVWY".contains( "$letter") ) 4 else if ( "BCMP".contains( "$letter") ) 3 else if ( "DG".contains( "$letter") ) 2 else if ( ! letter.isLetter() ) 0 else 1 fun square() = Square( pieceSize.x ).roundAllCorners( radius ).center() fun tab() = Square(pieceSize.x / 3, pieceSize.x - 5).roundAllCorners(1).center() fun piece( letter : char ) : Shape3d { val main = square().chamferedExtrude( pieceSize.y, chamfer ) var text : Shape3d = Text( "$letter", style ).toPolygon().center() .extrude( Math.abs(letterHeight) ) .translateY(1.5) .color( "Grey" ) val points : int = points( letter ) if ( includePoints ) { val pointsText = Text ( numberAsString( points ), pointsStyle ) .toPolygon() .rightTo( main.right-1.8) .frontTo( main.front+1.8) .extrude( Math.abs(letterHeight) ) .color( "Grey" ) text = text + pointsText } val tile = if ( letterHeight < 0 ) { main - text.translateZ( pieceSize.y + letterHeight + 0.01 ) } else { main + text.translateZ( pieceSize.y ) } return tile } fun pieces ( str : String ) = pieces( str, 1.0 ) fun pieces ( str : String, spacing : double ) : Shape3d { val pieces = listOf() for ( i in 0 until str.length() ) { val c = str.charAt(i) pieces.add( piece(c).translateX( (pieceSize.x+spacing) * i ) ) } return Union3d( pieces ) } @Piece fun tiles1() : Shape3d { // These are the distribution of letters in english scrabble. val all = layoutY( 1, pieces( "AAAAAAA" ), pieces( "AABBCCD" ), pieces( "DDDEEEE" ), pieces( "EEEEEEE" ), pieces( "EFFGGGH" ), pieces( "HIIIIII" ), pieces( "IIIJKLL" ) ) val firstLayerSheet = Square( all.size.x, all.size.y ) .roundAllCorners( radius ) .offset( -chamfer ) .extrude( 0.2 ) .leftTo( all.left + chamfer ).frontTo( all.front + chamfer ) return all + firstLayerSheet } @Piece fun tiles2() : Shape3d { // These are the distribution of letters in english scrabble. val all = layoutY( 1, pieces( "LLMMNNN" ), pieces( "NNNOOOO" ), pieces( "OOOOPPQ" ), pieces( "RRRRRRS" ), pieces( "SSSTTTT" ), pieces( "TTUUUUV" ), pieces( "VWWXYYZ" ) ) val firstLayerSheet = Square( all.size.x, all.size.y ) .offset( -chamfer ) .roundAllCorners( radius ).extrude( 0.2 ) .leftTo( all.left ).frontTo( all.front ) return all + firstLayerSheet } @Piece fun tiles() : Shape3d { val all = pieces( letters ) val firstLayerSheet = Square( all.size.x, all.size.y ) .roundAllCorners( radius ) .offset( -chamfer ) .extrude( 0.2 ) .leftTo( all.left + chamfer ).frontTo( all.front + chamfer ) return all + firstLayerSheet } fun scrabbleBox( isTop : bool ) : Shape3d { val x = 188 val y = 145 val z = 60 val boxy = RoundedBox().apply { thickness = 3.0 width = x depth = y bottomHeight = z topHeight = 15 } val box = boxy.build().frontTo(-boxy.thickness).leftTo(-boxy.thickness) if (isTop) return box val boards = Cube( 115, 115, 5 ) .bottomTo(boxy.thickness) .leftTo( slack ) .frontTo( slack ) .tileZ( 9, 0.4 ) .previewOnly() val divHeight = 55 val divider = Cube( boards.size.x + boxy.thickness + slack *2, boxy.thickness, divHeight ) .frontTo( boards.back + slack) .bottomTo(boxy.thickness) .color("Orange") val divider2 = Square( divHeight/2, divider.size.z ).roundCorner(3, 20) .extrude( divider.size.y ).alongX() .backTo( divider.back ) .bottomTo(boxy.thickness) .rightTo( divider.right ) .color("Orange") val racks = Cube( 185, 20, 50 ) .bottomTo(boxy.thickness) .leftTo( slack ) .frontTo( divider2.back + slack ) .previewOnly() // We could add the game's name to the front and back of the box // But I prefer to print some spare letters, and glue them on. val title = Text( "Scrabble", 30 ).extrude(0.4).center().rotateX(90) .backTo(box.front) .translateZ( z / 2 ) val title2 = title.rotateZ(180) .frontTo(box.back) return box + divider + divider2 + boards + racks // + title + title2 } @Piece fun scrabbleBox() = scrabbleBox( false ) @Piece fun scrabbleBoxLid() = scrabbleBox( true ) @Custom var slack = 1.0 var dividerHeight = 35 fun raidBox( isTop : bool ) : Shape3d { val x = 175 val y = 175 val z = 35 val boxy = RoundedBox().apply { thickness = 3.0 width = x depth = y bottomHeight = z topHeight = 15 } val box = boxy.build().frontTo(-boxy.thickness).leftTo(-boxy.thickness) if (isTop) return box val dividerT = boxy.thickness val four = raid4() .leftTo( slack ) .frontTo( slack ) val repeatY = four.size.y + boxy.thickness + slack*2 val fours = four.repeatY( 3, repeatY ) val three = raid3() .leftTo( fours.right + dividerT + slack*2 ) .frontTo( slack ) val threes = three.repeatY( 3, repeatY ) val fives = raid5() .leftTo( slack ) .frontTo( fours.back + dividerT + slack*2 ) val twos = raid(2) .bottomTo(boxy.thickness) .leftTo( fives.right + dividerT + slack*2 ) .frontTo( fives.front ) // We could add the game's name to the front and back of the box // But I prefer to print some spare letters, and glue them on. val title = Text( "Scrabble", 30 ).extrude(0.4).center().rotateX(90) .backTo(box.front) .translateZ( z / 2 ) val title2 = title.rotateZ(180) .frontTo(box.back) val pieces = (twos + threes + fours + fives) .bottomTo(boxy.thickness) .previewOnly() val dividersX = raidBoxDividerX( boxy, four ).repeatY( 3, repeatY ) + raidBoxDividerX( boxy, three ).repeatY( 2, repeatY ) + raidBoxDividerX( boxy, fives ) + raidBoxDividerX( boxy, twos ).repeatY( 2, -repeatY ) + Cube( 25, boxy.thickness, dividerHeight ) .leftTo( three.left - boxy.thickness - slack ) .frontTo( threes.back + slack ) .bottomTo( boxy.thickness ) val dividersY = raidBoxDividerY( boxy, fives ) + raidBoxDividerY( boxy, four ).repeatY( 3, repeatY ) return box + pieces + dividersX + dividersY } fun raidBoxDividerY( boxy : RoundedBox, pieces : Shape3d ) : Shape3d { return Cube( boxy.thickness, pieces.size.y + slack * 2 + boxy.thickness, dividerHeight ) .leftTo( pieces.right + slack ) .bottomTo( boxy.thickness ) .frontTo( pieces.front - slack - boxy.thickness/2 ) } fun raidBoxDividerX( boxy : RoundedBox, pieces : Shape3d ) : Shape3d { val a = Square( pieces.size.x / 2 - 15, dividerHeight ) .roundCorner( 1, 7 ) .extrude( boxy.thickness ) .alongY2() .frontTo( pieces.back + slack ) .bottomTo( boxy.thickness ) return a.leftTo( pieces.left - boxy.thickness ) + a.mirrorX().rightTo( pieces.right + boxy.thickness ) } @Piece fun raidBox() = raidBox( false ) @Piece fun raidBoxLid() = raidBox( true ) @Custom var addFirstLayerSheet = true /* Glue these to the front of the box. Consider using a batton clamped in place to help line the pieces up. If leaving gaps between the letters, use a spacer (such as another tile). */ fun tilesTitle( letters : String ) : Shape3d { chamfer = 0 pieceSize = Vector2(pieceSize.x, 0.8 ) letterHeight = -0.4 val all = pieces( letters ) return if (addFirstLayerSheet) { val firstLayerSheet = Square( all.size.x, all.size.y ) .roundAllCorners( radius ) .offset( -chamfer ) .extrude( 0.2 ) .leftTo( all.left + chamfer ).frontTo( all.front + chamfer ) .color( "Orange" ) all + firstLayerSheet } else { all } } @Piece fun tilesSCRABBLE() = tilesTitle( "SCRABBLE" ) @Piece fun tilesRAID() = tilesTitle( "RAID" ) /* One ninth of a scrabble board! This is a 5x5 grid (the whole board is 15x15) */ @Piece fun scrabbleBoard() : Shape3d { return scrabblePlace(boardThickness).tileX(5).tileY(5) } fun scrabblePlace( thickness : double ) : Shape3d { val inside = pieceSize.x + boardSlack val outside = inside + boardWallWidth val place = Cube( outside, outside, thickness ).centerXY().color( "LightGrey" ) val wallP = Square( inside + boardWallWidth).center() - Square( inside ).center() val wall = wallP.extrude( thickness + boardWallHeight ) .color( "Green" ) return (place + wall) } @Piece fun tws() = scrabbleBonus(true, true).tileX(4,3).tileY(2,3).color( "Red" ) @Piece fun dws() = scrabbleBonus(false, true).tileX(6,1).tileY(3,1).color( "Pink" ) // One spare! @Piece fun tls() = scrabbleBonus(true, false).tileX(4,1).tileY(3,1).color("Blue" ) @Piece fun dls() = scrabbleBonus(false, false).tileX(6,1).tileY(4,1).color("LightBlue") fun scrabbleBonus( isTripple : bool, isWord : bool ) : Shape3d { //val lug = Cube( pieceSize.x, pieceSize.x, boardWallHeight - 0.2 ).topTo(0).centerXY() val piece = scrabblePlace(bonusThickness) val text = Text( numberAsString( if (isTripple) 3 else 2 ), style ) .scale (if (isWord) 1.5 else 1.0 ) .extrude(pieceSize.y).center() return piece - text.bottomTo(Math.max(bonusThickness/2, 0.4)) } @Piece fun scrabbleBonusPadding() : Shape3d { val lug = Square( pieceSize.x, pieceSize.x ) .roundAllCorners(1) // Without this, I had to manually shave the corners. .center() .extrude( boardWallHeight - 0.2 ) return lug } /** I can't print the "scrabbleBonus" pieces as I'd like, so I print them in two parts. This "spacer" is glued to the board, and then the bonus is glued ontop of this. In hind slight, maybe I should have made the board solid where bonuses appear, and then glued directly onto that. */ fun scrabbleBonusSpacer() : Shape3d { val piece = scrabblePlace(bonusThickness) val lug = Cube( pieceSize.x, pieceSize.x, piece.size.z + boardWallHeight - 0.2 ).centerXY() return piece + lug } /* A place to put completed words in the Raid Words game. */ fun raid( n : int ) : Shape3d { val holes = Square( pieceSize.x + boardSlack ) //.roundAllCorners( radius ) .tileX( n, boardSlack + boardWallWidth ) .center() .extrude( boardWallHeight+1 ) .translateZ( boardThickness ) .color("Red") val main = Square( (pieceSize.x + boardSlack*2) * n + boardWallWidth * (n-1) + raidMargin*2 - boardSlack, pieceSize.x + boardSlack + raidMargin*2 ).center() .roundAllCorners( radius + raidMargin/2) .extrude( boardThickness + boardWallHeight ) .color("Orange") val result = main - holes val pieces = piece('A').tileX( n, boardWallWidth + boardSlack*2 ).centerXY().translateZ(boardThickness).previewOnly() return result //+ pieces } @Piece fun raid2() = raid(2) @Piece fun raid3() = raid(3) @Piece fun raid4() = raid(4) @Piece fun raid5() = raid(5) @Piece fun rack() : Shape3d { val profile = SVGParser().parseFile( "wordGamesRack.svg" ).shapes["rack"] return profile.extrude( (pieceSize.x + 3)* 8 ).alongX().toOrigin() } @Piece fun bonusSpacer() = scrabbleBonusSpacer().tileX(2,1).tileY(2,1) @Piece fun bonusPadding() = scrabbleBonusPadding().tileX( 1, 1 ).tileY( 1, 1 ) override fun build() : Shape3d { val board = scrabbleBoard().translateY(50) val tws = scrabbleBonus(true, true).color("Red").translate(0,50,boardThickness + boardWallHeight ) val dws = scrabbleBonus(false, true).color("Pink").translateZ( boardThickness + boardWallHeight ) .leftTo(tws.right).frontTo(tws.back) val tls = scrabbleBonus(true,false).color("Blue").translateZ( boardThickness + boardWallHeight ) .leftTo(dws.right).frontTo(dws.back) val dls = scrabbleBonus(true,false).color("LightBlue").translateZ( boardThickness + boardWallHeight ) .leftTo(tls.right).frontTo(tls.back) return raid(2) + raid(3).translateX( pieceSize.x * 3.5 ) + board + tws + dws + tls + dls } /* Change filament colour when printing the tiles. */ /* override fun postProcess(gcode: GCode) { if ( piece.startsWith("tiles" ) ) { val height = if (letterHeight < 0) { pieceSize.y + letterHeight } else { pieceSize.y } pauseAtHeight( gcode, height, "Change Filament" ) } } */ }