Exit Full View
Up

/Torch/CobTwist.foocad

CobTwist
FooCAD Source Code
/*
A torch with a wide COD LED strip which can lay flat on flat surface
The light can be directed up/down by simply twisting the body.

Powered by a 18650 battery, through a boost circuit to raise to 12V needed
for the LED cob. Includes a USB battery charge port at the other end to the
on/off switch.

The feet can be taken on/off if required.

The feet include impressions for magnets to glued into.

The battery is held in place using a friction fit using tape to pad
it out slightly. MI used masking tape because it has high friction.
Ensure that you cover the terminals well. It could slide about!
I also taped the end of the charger module to be extra safe, and the
boost module is 100% protected by the "boostHolder" pieces.

Build parts
-----------

Build in several parts. Use the custom "piece" field :
    foot (x2)
    tube
    chargerEnd
    switchEnd
    boost (x2) (to secure the boost circuit within the tube) Required M3 bolts

Bought parts
------------

A 18650 battery. I use 2nd hand ones from old laptops. Usually only one or
two cells have died when the battery as a whole is dead. I don't tend to
use torches for long periods, so a high capacity battery is a waste!

Search ebay (or other shop) for :
    usb battery charge module
    dc boost module mounting holes (3.x V to 12V no usb socket

FYI, The boost modules with mouting holes are becoming rarer.
If you can't find any, the "boost" piece won't be of use.

Requirements
------------

    At least 17cm build height!

Possible Improvements
---------------------

    Add posts to the "buttonEnd", so that it can never rotate (twisting the wires)
    
    Cut away the tube to allow space for the wires from the LED cob strip.

*/

import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.*
import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.*

class CobTwist : Model {
  

    // Size of the LED module
    @Custom
    var length = 173
    val width = 15.5
    var thickness = 2.7

    val buttonD = 13 // Old one was 16.5

    val endT=8
    val endBase=2
    val endChamfer=1
    val endExtra = 3
    val endSkin=0.8

    // Thickness of the tube walls. On a small extrusion this seems a little 
    // flimsy, but the final result feels very rigid to me.
    val tubeT = 0.6
    val radius=13 // external radius of the tube

    // The boost module is 22 mm, so we need an internal radius of at least 11 mm
    fun internalRadius() = radius - tubeT

    val footT = 5 // Diff between internal and external radius of the foot.
    val magnetD = 11 // Diameter of the magnets on the feet (include some slack?)
    val magnetT = 1.0 // Thickness of the magnets (include some slack?)

    // For an interference fit between the tube and the ends.
    // I recommend gluing the ends on. You don't want anybody
    // shorting out the battery (lithium batteries are dangerous!)
    val slack = 0.3

    // I didn't include any slack at first. (I ussumed that the split nature
    // of the foot would suffice. However, the feet tend to turn the ends,
    // so a higher value may be better (0.4 maybe??? Testing for the first time...)
    val footSlack = 0.4

    //@Custom
    var text = ""
  
    @Piece
    fun others() : Shape3d {
        return (
            buttonEnd() +
            chargerEnd().translateX(30) +
            boostHolder().translateY(-3).rotateZ(180).also().translateX(60) +
            foot().tileX(2,5).translateY(-35)
            ).center()
    }
    

    // ==== FUNCTIONS BEGIN ====
    override fun build() : Shape3d {
    
        return buttonEnd() +
            chargerEnd().rotateX(180).translateZ(length+30) +
            tube().translateZ( endBase ) +
            led().translate( 0,10,endBase ) +
            battery().translateZ(30).translateY(-2) +
            foot() +
            foot().mirrorZ().translateZ(length)
      
    }

    // For debugging only.
    fun battery() = Cylinder( 65, 9 ).color("GhostWhite")

    // Used by the "chargerEnd" and "buttonEnd" pieces.
    // Goes on both ends of the tube. Each end is modified in different ways.
    fun end() : Shape3d {
        val outside = Circle( radius + slack + endSkin )
        val inside = Circle( radius + slack )
       
        return ExtrusionBuilder().apply {
            crossSection(Circle( radius + slack - endChamfer))
            forward(endChamfer)
            crossSection(outside)
            forward(endT-endChamfer)
            crossSection(outside)
            crossSection(inside)
            forward(-endT + endBase )
            crossSection(inside)
        }.build().color( "LightGreen" )
    }

    // The "chargerEnd" piece. Includes a hole and mounting post
    // for a usb charger module. Ensure you get one with no component on
    // one side, as you will be gluing it to a plastic post.
    @Piece
    fun chargerEnd() : Shape3d {
        val hole = Cube( 8.5, 3.5, endT*3 ).center()
        val floor = Cube( 18, 6, 10 ).centerXY().translateZ(0.9)
        val entry = Cube( 11, 5, 0.5).centerXY().translateZ(-0.1)

        // If you glue the charger module to the post with epoxy,
        // then these holes will give extra support.
        // If using super-glue, then don't include these holes, as they
        // will only reduce the surface area, and give no mechanical advantage.
        val holes = Cube(2,10,2)
            .tileX(2,5)
            .tileZ(2,5)
            .centerXY()
            .translateZ(3)

        val post = Cube( 20, 0.6, 13 ).centerX().translateY(-3.5) +
            Cube( 0.6, 5, 13 ).translateY(-8) -
            holes


        val indentText = Text( text, BOLD, 7 )
            .center()
            .mirrorX()
            .translateY(6.5)
            .extrude(0.6).translateZ( -0.01 )
            .color("Orange")

        return end() - hole - floor - entry - indentText + post 
    }

    // The "buttonEnd" piece. Includes a hole for a button.
    // Note, my button's retaining ring fouled again the cob LED strip,
    // so I glued it in place.
    // Consider using a smaller diameter button!
    // NOTE. I should have added post(s) to prevent the end from rotating.
    // (to prevent wires getting twisted). I'll have to glue mine in place.
    fun buttonEnd() : Shape3d {
        val post = Cube(0.6, 8, 8).centerXY().translateX(10)
        return end() -
            Cylinder( 30, buttonD / 2).center() +
            post.mirrorX().also()
    }
        

    // Used for debugging only. Shows the size of the boost module, as well
    // as the mounting holes.
    fun boostModule() : Shape3d {
        val board = Cube( 43.5, 21.5, 1.5 ).color("Green")
        val components = Cube(43.5, 21.5, 10).translateZ(1.5).color("WhiteSmoke")

        val holes = Cylinder.hole( 20, 1.5 ).translate(15,-7.5,-1)
            .translate(-30,15,-1).also()
        return (board + components).centerXY() - holes
    }

    @Piece
    fun boosts() = boostHolder().translateY(-3).rotateZ(180).also()

    // The "boost" piece. Print two of them to secure and protect the boost
    // module. Requires two short M3 bolts (no nuts).
    @Piece
    fun boostHolder() : Shape3d {

        val thick = 0.6
        val height = 9
        val r = internalRadius() - slack 

        val oversize = Cube( r * 3, r * 3, height ).centerXY()
        val snug = Circle( r ) - Square( radius * 2 ).centerX().translateY(2)
        val inside = Circle( r-1 )

        val block = Cube( 6,4,10 ).translate(-10,-2,0)
        val hole = Cylinder.hole(30,1.5).center().rotateZ(-90).rotateX(90).translate(-7.5,0,7.5)

        // val debug = boostModule().rotateZ(90).rotateX(90).translate(0,4,21.75+thick)

        // We make a "fake" chamfer at the bottom to prevent squishage of the
        // first layer making it too big to fit in the tube.
        val solid = (oversize - inside.extrude(height).translateZ(thick)) /
            (snug.extrude(height).translateZ(0.4) + snug.offset(-0.4).extrude(height))

        return block - hole + solid 
    }

    // The "tube" piece.
    // I had no trouble printing it, despite being tall and narrow, it didn't
    // get knocked over during the print (and I did not use a brim).
    // Consider printing a small version (using @Custum length), and
    // check that your LED strip fit within.
    @Piece
    fun tube() : Shape3d {
        val up = 7.5
        val boardT = 2
        val apeture = 11
        
        val profile = (
            Square( radius*2+1 ).center() -    // Overly large square
            Circle( radius - tubeT ) +         // Internal circle
            Square( radius*2, 20 ).centerX().translateY( up-tubeT ) - // Fill in the top
            Square( width, boardT ).centerX().translateY( up ) - // Slot for LED
            Square( apeture, 10 ).centerX().translateY( up ) // Apeture for light
        ) / Circle( radius ).convexity(3)

        val removeForSolder = Cube( width, 10, 5 ).centerX()
            .hull( Cube( 2, 10, 6 ).centerX().translateZ(8) )
            .translateY(up)
            
        val removeForSolders = removeForSolder + removeForSolder.mirrorZ().translateZ(length)

        val removeForButton = Cylinder(21,10).translateZ(length-20)
        return profile.extrude( length ) - removeForSolders - removeForButton
    }

    // The "foot" piece. Print two identical copies.
    // These have a split design, so that they can be taken on/off easily
    // after the body of the torch is built.
    // I say "easily" - it takes a bit of brute force and coordination,
    // but there's no unsoldering involved (unlike the first design!)
    @Piece
    fun foot() : Shape3d {
        val internalR  = radius + slack + endSkin + footSlack
        val externalR = internalR + footT

        val extra = 8
        val foo = 47 // The width at floor level
        val thickness = 9 // Thickness at the top
        val thickness2 = 17 // Thickness at the bottom (Badly named!)

        val base = Square( foo, 10 ).roundAllCorners(3).centerX().translateY(-internalR-extra)
        val shape = (Circle( externalR ).hull(base)) // - Circle( internalR )

        val profile = PolygonBuilder().apply {
            moveTo(-externalR,0)
            lineTo(externalR+extra,0)
            lineTo(externalR+extra,thickness2)
            bezierTo(
                Vector2(externalR+extra-20,thickness2),
                Vector2(10,thickness),
                Vector2(0,thickness)
            )
            lineTo(-externalR,thickness)
        }.build()

        val center = ExtrusionBuilder().apply {
            forward(-0.1)
            crossSection( Circle( internalR - 1 ) )
            forward(1)
            crossSection( Circle( internalR ) )
            forward( thickness-2 )
            crossSection( Circle( internalR ) )
            forward(1)
            crossSection( Circle( internalR - 1 ) )
            forward(30)
            crossSection( Circle( internalR - 1 ) )
        }.build().color("Red")

        val inter = profile.extrude( foo ).centerZ().rotateX(90).rotateZ(-90)

        val split = (
                Square(1,extra+2).centerX() +
                Square(8).center().rotate(45).translateY(extra+3.5)
            )
            .extrude( thickness2+2 )
            .centerX()
            .translate(0,-internalR-extra-1,-1)

        val magnetHoles = Cylinder.hole( magnetT+0.1, magnetD/2 )
            .rotateX(90)
            .translateZ(thickness2/2)
            .translateY(-internalR-extra+magnetT)
            .translateX(10).mirrorX().also()

        return shape.extrude( thickness2 ).color( "Violet" ) / inter -
            center - 
            magnetHoles -
            split 
    }

    fun led() = Cube( width,thickness,length )
        .centerX().mirrorY()
        .color("WhiteSmoke")

    fun line( fromX:  double, fromY : double, toX : double, toY : double ) =
        line( Vector2(fromX, fromY), Vector2(toX, toY) )

    fun line( from : Vector2, to : Vector2 ) : Shape2d {
        val across = (to-from).normal().unit()*2
        return PolygonBuilder().apply {
            moveTo(from)
            lineTo(from+across)
            lineTo(to+across)
            lineTo(to)
        }.build()
    }

    fun box( name : String ) : Shape2d{
        return Square( 60, 40 ).center().color("DarkGreen") +
            Text( name ).center().color("MintCream")
    }

    // A simple circuit diagram
    fun circuit() : Shape2d {
        val switch = box("Switch").translateX(-270)
        val led = box("LED").translateX(-180)
        val boost = box("Boost").translateX(-90)
        val battery = box("Battery")
        val charger = box("Charger").translateX(90)


        val plusToSwitch = line( -30,0, -60, -30 ) +
            line( -60,-30, -210,-30 ) +
            line( -210, -30, -240, -5 )

        val switchToCharger = line( -240,5, -210, 30 ) +
            line( -210,30, 30,30 ) +
            line( 30,30, 60,5 )

        val chargerToBoost = line( 60, 20, 25,50 ) +
            line( 25, 50, -25, 50 ) +
            line( -25, 50, -60, 5 )

        val boostToLED = line( -120, 10, -150, 10 )

        val batteryToCharger = line( 30,0, 60, -10 )

        val posLines = plusToSwitch + switchToCharger + 
            chargerToBoost +
            boostToLED

        val negLines = batteryToCharger +
            chargerToBoost.mirrorY() +
            boostToLED.mirrorY()

        val text1 = Text( """On Charger :
    'B+' goes to the switch
    'B-' goes to battery's negative
    'out+' goes to boost's 'in+'
    'out-' goes to boost's 'in-'
   
""" ).translate(120,40)
    
        val text2 = Text( """On Boost :
    'out' connects to the LED
    'in' connects to the charger's "out"
""").translate(-180,-70)

        return led + switch + boost + battery + charger +
            posLines.color("Red") +
            negLines.color("Black") +
            text1 + text2
    }

}