Exit Full View
Up

/Games/GiftTube.foocad

GiftTube

Twist th two pieces to open. Inside there is a tiny compartment. Roll up a note for a money gift idea?

The outer piece has a nub of plastic, which fits into a groove in the inner piece. This groove is a maze. Twist and slide the outer case to navigate the maze. The tricky part is that the maze is hidden from view.

FooCAD Source Code
class GiftTube : Model {
    
    val outerD = 40.0

    val chamfer = 2.0

    // The thickness of the inner tube (the part with the nub, not the maze)
    var thickness = 1.0

    // The outer tube is thicker, because it has the maze cut into it.
    // This is also the thickness of the top and bottom surfaces.
    var outerThickness = 2.0

    val mainHeight = 60.0

    val endHeight = 5.0

    var slack = 0.4

    var grooveSlack = 0.5

    var sides = 40 // Must be a multiple of resXY

    var nubSize = 2.0
    
    // The size of the maze in the "x/y" circular direction
    var resXY = 10

    // The size of the maze in the z direction.
    var resZ = 10

    var grooveP : Shape2d = null // Created at the start of build()

    fun createGrooveP() {
        grooveP = Circle( nubSize ).sides(4)
    }

    var pathBuilder : Path3dBuilder

    // Converts a maze x,y coordinate into a 3d coordinate of the outer tube.
    fun to3d( x : int, y : int ) : Vector3 {
        val degrees = 360 / resXY * x
        val radius = outerD / 2 - outerThickness - grooveSlack
        return Vector3(
            Degrees.cos( degrees ) * radius,
            Degrees.sin( degrees ) * radius,
            mainHeight - mainHeight / resXY * y
        )
    }

    var currentX = 0
    var currentY = 0

    fun moveTo( x : double, y : double ) {
        currentX = x
        currentY = y
        pathBuilder = Path3dBuilder().apply {
            moveTo( to3d( x, y ) )
        }
    }

    fun vertical( n : double ) {
        currentY += n
        pathBuilder.lineTo( to3d( currentX, currentY ) )
    }

    fun horizontal( n : double ) {
        val start = to3d( currentX, currentY )
        currentX += n
        var steps = sides * n / resXY
        var delta = 360 / sides

        if (steps < 0) {
            steps = -steps
            delta = -delta
        }

        println( "H Steps $steps" )
        for ( i in 1 .. steps ) {
            val m = Matrix3d.rotateZ( delta * i / 180 * Math.PI )
            val to =  m * start
            println( "H $i : $to" )
            pathBuilder.lineTo( to )
        }

    }

    
    fun makeGroove() : Shape3d {
        return grooveP.extrude( pathBuilder.build(), true )
    }

    fun outer() : Shape3d {
        
        val main = ExtrusionBuilder().apply {
            crossSection( Circle( outerD/2 - chamfer ).sides(sides) )

            forward(chamfer)
            crossSection( Circle( outerD/2 ).sides(sides) )

            forward( mainHeight - chamfer )
            crossSection()
            crossSection( -outerThickness )

            forward( -mainHeight + outerThickness + chamfer )
            crossSection()

            forward(-chamfer)
            crossSection( -chamfer )
            
        }.build()
    
        return  main.color( "Yellow" )
    }

    fun inner() : Shape3d {
        val h = mainHeight - outerThickness - chamfer - slack

        val nubP = Circle( nubSize ).sides(4) / Square( nubSize*3 ).centerY()
        val nub = nubP.revolve(180)
            .rotateZ(-90)
            .translateZ(  endHeight + h +slack )
            .translateX( outerD/2 - outerThickness - slack*2 )



        val main = ExtrusionBuilder().apply {
            crossSection( Circle( outerD/2 - chamfer ).sides(sides) )

            forward(chamfer)
            crossSection( Circle( outerD/2 ).sides(sides) )

            forward( endHeight )
            crossSection()
            crossSection( Circle( outerD/2 - outerThickness - slack ).sides(sides) )
            forward( h )
            crossSection()
            crossSection( -thickness )
            forward( -h - endHeight + outerThickness )
            crossSection()
            forward( -chamfer )
            crossSection( -chamfer )

        }.build()

        return nub + main.color( "Orange" )
    }

    fun maze() : Shape3d {
        moveTo( 0,-1 ) // Start "outside"
        vertical( 3 )
        horizontal( 5 )
        vertical( 1 )
        horizontal( 2 )
        vertical( 2 )
        horizontal( -3 )
        vertical( 1 )
        horizontal( -6 )
        vertical( 2 )
        horizontal( 1 )
        vertical( 1 )
        horizontal( 9 )

        // Follow the yellow brick road !
        val solution = makeGroove().color( "Yellow" )


        moveTo( 0, 1  )
        horizontal( -2 )
        vertical(1)
        horizontal( -2 )
        val end1a = makeGroove().color( "Red" )

        moveTo( -3, 2 )
        vertical( -1 )
        val end1b = makeGroove().color( "Red" ).darker()

        val end1 = end1a + end1b

        moveTo( 0, 2 )
        vertical( 1 )
        horizontal( 2 )
        vertical( 1 )
        horizontal( -3 )
        vertical( -2 )
        horizontal( 1 )
        val end2 = makeGroove().color( "Green" )

        moveTo( 3, 2 )
        vertical( 3 )
        horizontal( -5 )
        vertical( -2 )
        val end3 = makeGroove().color( "Orange" )


        moveTo( -2, 8 )
        horizontal( -2 )
        vertical( -1 )
        horizontal( 1 )
        val end4 = makeGroove().color( "Blue" )

        moveTo( -2, 6 )
        horizontal( -3 )
        vertical( 2 )
        horizontal( -1 )
        val end5a = makeGroove().color( "Indigo" )
        moveTo( -5, 7 )
        horizontal( -2 )
        vertical( 1 )
        horizontal( -1 )
        val end5b = makeGroove().color( "Indigo" ).darker()
        val end5 = end5a + end5b

        moveTo( 2, 6 )
        vertical( 1 )
        horizontal( -4 )
        val end6a = makeGroove().color( "Violet" )
        moveTo( 0, 7 )
        vertical( 1 )
        horizontal( 1 )
        val end6b = makeGroove().color( "Violet" ).darker()

        val end6 = end6a + end6b 

        moveTo( 3, 4 )
        horizontal( 1 )
        vertical( -1 )
        val end7 = makeGroove().color( "Salmon" )


        moveTo( 5, 3 )
        vertical( 1 )
        horizontal( 1 )
        val end8 = makeGroove().color( "DarkKhaki" )

        moveTo( 2, 2 )
        vertical( -1 )
        horizontal( 1 )
        val end9a = makeGroove().color( "Silver" )
        moveTo( 4, 2 )
        vertical( -1 )
        horizontal( 1 )
        val end9b = makeGroove().color( "Silver" )
        val end9 = end9a + end9b

        return solution + end1 + end2 + end3 + end4 + end5 + end6 + end7 + end8 + end9

    }

    override fun build() : Shape3d {
        createGrooveP()

        val maze : Shape3d = maze()
        val inner : Shape3d = inner()
        val outer : Shape3d = outer() - maze

        val both = outer + inner
            .mirrorZ()
            .translateZ(mainHeight + endHeight + thickness + slack + 1)

        return outer +
            inner.translateX( 60 ) +
            (both / Cube( 200,1,200).center()).translateX( 120 ) +
            maze.translateX( 180 )
            
    }
}


class Maze( val seed : double, val width : int, val depth : int ) {

    // The even parts of the grid denote if this location has been visited,
    // The odd parts denote if there is a path from one location to a neighbour.
    // Note that "diagonals" (both indicies are odd) will never be used.
    val grid = listOf<bool>()
    var completed = 0

    val random = Random( seed )

    init {
        
        for (y in 0 until depth*2 ) {
            for (x in 0 until width *2 ) {
                grid.add( false )
            }
        }
    }


    fun get( i : int, j : int ) : bool {
        return if ( j < 0 || j >= depth * 2) {
            true
        } else if ( i < 0 ) {
            get( width * 2, j )
        } else if ( i >= width * 2 ) {
            get( i - width * 2, j )
        } else {
            grid.get( j * width + i )
        }
    }

    fun set( i : int, j : int ) {
        if ( i < 0 ) {
            set( width * 2, j )
        } else if ( i >= width * 2 ) {
            set( i - width * 2, j )
        } else {
            grid.set( j * width + i, true  )
        }
    }


    fun isOccupied( x : int, y : int ) : bool {
        return get( x * 2, y * 2 )
    }

    fun visit( x : int, y : int, dx : int, dy : int ) {
        set( x * 2, y * 2 )
        set( x * 2 + dx, y * 2 + dy )
    }

    fun grow() {
        set( 0, 0 )
        growFrom( 0, 0 )
    }

    fun growFrom( x : int, y : int ) {
        
    }
}