Exit Full View
ZipUp

/Games/StayJoined.foocad

StayJoined

Clearances

The blocks are NOT exact multiples of the unit-size; they are slightly smaller. This is to account for innacuracies in printing. Imagine a BIG back block, say 20x20x1. Now imaging placing a row of brick horizontally. If the blocks cannot squeeze close enough, then the holes won't match up with the 20x20 base.

There are two clearances clearance and clearanceZ. The former is for the X and Y dimensions.

FooCAD Source Code
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() )
        )
    }

}