import static uk.co.nickthecoder.foocad.arrange.v1.Arrange.* import uk.co.nickthecoder.foocad.extras.v1.* import static uk.co.nickthecoder.foocad.extras.v1.Extras.* import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.* import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.* import uk.co.nickthecoder.foocad.smartextrusion.v1.* import static uk.co.nickthecoder.foocad.smartextrusion.v1.SmartExtrusion.* class Plicks : Model { @Custom( about="The nominal size of a 1x1x1 block" ) var unitSize = 10 @Custom( about="The holes' deviation from stright. Large for a tight fit." ) var bumpSize = 0.7 @Custom( about="Diameter of the hole. Larger than the filament diameter!" ) var holeSize = 1.75 + 0.7 @Custom( about="How much to shrink in X and Y so that piece pack together well" ) var clearance = 0.1 @Custom( about="How much to shrink in Z so that pieces pack together well" ) var clearanceZ = 0.1 @Custom( about="Scale horizontal hole in Z. Adjust if vertical hole are looser/tighter than horizontal holes" ) var holeZScaling = 1.2 @Custom( about="Only cosmetic" ) var chamfer = 1.0 // Cross-section of the holes. vertical holes are circular, but // horizontal holes use Extra's "safeOverhang" for better printing of the overhangs. meth holeShape( horizontal : bool ) : Shape2d { val circle = Circle( holeSize/2 ).hole() if (horizontal) { return circle.safeOverhang().mirrorY().scaleY(holeZScaling) } else { return circle } } // 2D shape of a brick (as seen from above) meth shape( width : int, length : int ) : Shape2d { return Square( unitSize * width, unitSize * length ) .roundAllCorners( chamfer, 1 ) .center().offset(-clearance/2) } // A 3D brick without any holes meth plainBrick( width : int, length : int, height : int ) : Shape3d { val shape = shape( width, length ) return shape.smartExtrude( height * unitSize - clearanceZ ) .edges( Chamfer( chamfer ) ) } // The path a hole makes. This is the "clever" part of the system which // ensures that the filament is held firmly in the holes despite the fact that // the diameter of the holes is LARGER than the filament. // The holes wiggle, which forces the filament to wiggle, and therefore // it presses outwards (due to the springy nature of the filament). meth wigglePath( length : double, bump : double ) : Path2d { val pre = 1 val halfLength = length/2 - pre return PolygonBuilder().apply { moveTo( 0, -length/2 ) lineBy( 0, pre ) bezierBy( Vector2( 0, halfLength/2 ), Vector2( 0, halfLength/2 ), Vector2( bump, halfLength ) ) bezierBy( Vector2( 0, halfLength/2 ), Vector2( 0, halfLength/2 ), Vector2( -bump, halfLength ) ) lineBy( 0, pre ) }.buildPath() } // A single hole. Length is in "base" units, i.e. will be 1 for the smallest (unit) cube. // The holes are different when horizontal, to aid printing the overhangs. meth hole( length : int, horizontal : bool ) : Shape3d { val path = wigglePath( unitSize, bumpSize ) val holeShape = holeShape( horizontal ) val one = holeShape.extrude(path) if (length == 1) { return one } else { val middle = holeShape.extrude( (length - 2)* unitSize ) .rotateX(-90).centerZ() .frontTo( one.back ) return (middle + one.frontTo( middle.back ).also()) .frontTo( -unitSize * length/2 ) } } // Holes in all 3 directions. // width, length and height are in "base" units. i.e. will be 1 for the smallest // (unit) cube. // `blind` Are the vertical holes blind? i.e. do they stop before exiting the bottom. // Used for "base plates" meth holes( width : int, length : int, height : int, blind : bool ) : Shape3d { val small = 0.33 val large = 1-small val holesX = hole( width, true ).rotateZ(90) .repeatY( length, unitSize ) .repeatZ( height, unitSize ) .translateY( unitSize * (large - length/2) ) .translateZ( unitSize * small -clearanceZ/2 ) val holesY = hole( length, true ) .repeatX( width, unitSize ) .repeatZ( height, unitSize ) .translateX( unitSize * (large - width/2) ) .translateZ( unitSize * large -clearanceZ/2 ) val holesZ = hole( height, false ) .rotateX(90).bottomTo(0).rotateZ(-90-45) .repeatX( width, unitSize ) .repeatY( length, unitSize ) .translateX( unitSize * (small - width/2) ) .translateY( unitSize * (small - length/2) ) val holes = holesX + holesY + holesZ return if (blind) { holes - holes.boundingCube().topTo( 0.6 ) } else { holes } } // A complete brick of any size. meth brick( width : int, length : int, height : int, blind : bool ) : Shape3d { return plainBrick( width, length, height ) - holes( width, length, height, blind ).color("Red") } meth brick( width : int, length : int, height : int ) = brick( width, length, height, false ) // Standard size bricks... // === n x 1 x 1 === @Piece meth brick1x1x1() = brick( 1,1,1 ) @Piece meth brick2x1x1() = brick( 2,1,1 ) @Piece meth brick3x1x1() = brick( 3,1,1 ) @Piece meth brick4x1x1() = brick( 4,1,1 ) @Piece meth brick5x1x1() = brick( 5,1,1 ) @Piece meth brick6x1x1() = brick( 6,1,1 ) // === n x 2 x 1 === @Piece meth brick2x2x1() = brick( 2,2,1 ) @Piece meth brick3x2x1() = brick( 3,2,1 ) @Piece meth brick4x2x1() = brick( 4,2,1 ) @Piece meth brick5x2x1() = brick( 5,2,1 ) @Piece meth brick6x2x1() = brick( 6,2,1 ) // === n x 2 x 2 === @Piece meth brick2x2x2() = brick( 2,2,2 ) @Piece meth brick3x2x2() = brick( 3,2,2 ) @Piece meth brick4x2x2() = brick( 4,2,2 ) @Piece meth brick5x2x2() = brick( 5,2,2 ) @Piece meth brick6x2x2() = brick( 6,2,2 ) // Base plates (with blind holes) @Piece meth base4x4() = brick( 4,4,1, true ) @Piece meth base8x4() = brick( 8,4,1, true ) @Piece meth base8x8() = brick( 8,8,1, true ) override fun build() : Shape3d { // Use this to inspect/debug the holes. // return holes( 3, 2, 1, true ) return arrangeX( arrangeY( brick1x1x1(), brick2x1x1(), brick3x1x1(), brick4x1x1(), brick5x1x1(), brick6x1x1() ), arrangeY( brick2x2x1(), brick3x2x1(), brick4x2x1(), brick5x2x1(), brick6x2x1() ), arrangeY( brick2x2x2(), brick3x2x2(), brick4x2x2(), brick5x2x2(), brick6x2x2() ) ) } }