Exit Full View
ZipUp

/Furniture/LightShadeFlower.foocad

LightShadeFlower

A ceiling light shade.

Inspired by : Jesper Makes

FooCAD Source Code
import uk.co.nickthecoder.foocad.smartextrusion.v1.*
import static uk.co.nickthecoder.foocad.smartextrusion.v1.SmartExtrusion.*
import static uk.co.nickthecoder.foocad.layout.v1.Layout2d.*
import static uk.co.nickthecoder.foocad.layout.v1.Layout3d.*
import uk.co.nickthecoder.foocad.extras.v1.*
import static uk.co.nickthecoder.foocad.extras.v1.Extras.*
import static LightShadeFlower.*

/**
TODO
    Correct value for fixtureDiameter
    Slots in petals.
    Petals do not overlap the ribs by an even amount (now broken - not lining up!).
    The bulb should be in the middle of the flower.
    Model a typical light fitting and bulb.
    Allow the sphere to be squashed
*/
class LightShadeFlower : Model {

    // Each line below represents a ring of 12 petals.
    // The three numbers are :
    // 1. Position of the ring as an angle in degrees (like lines of latitude)
    // 2. The tilt of the petal (0 = horizontal, 90 = straight down)
    // 3. The size of the petal (in bizare units that aren't important - adjust the values by eye!)
    //    The size isn't in millimeters to make it easy to change the overall size of the
    //    shade without changing the data.
    val petalData = listOf<Petal>(
        Petal(  -27, -60, 46 ),
        Petal(  -15, -50, 54 ),
        Petal(  5, -45, 60 ),
        Petal(  27, -30, 54 ),
        Petal(  46, -20, 46 ),
        Petal(  57, -3, 34 )
    )

    // The actual shade will be large than this.
    @Custom( about="The diameter of the sphere which forms the core of the lamp" )
    var diameter = 225

    @Custom
    var topAngle = -50

    @Custom
    var bottomAngle = 60

    @Custom
    var ribSize = Vector2( 20, 2 )

    @Custom
    var petalThickness = 1.2

    @Custom
    var ribCount = 24

    @Custom
    var fixtureDiameter = 35

    @Custom( about="A small chamfer on the structual elements (avoids elephant foot issues)" )
    var ribChamfer = 0.5

    @Custom( about="Clearances for the wider slots (length,width)" )
    var slotClearance = Vector2( 0.2, 0.2 )

    // It is likely that these values will be identical to `slotClearance`.
    @Custom( about="Clearances of the narrow slots into which the petals are placed (length,width)" )
    var petalSlotClearance = Vector2( 0.2, 0.2 )


    meth extrudeRib( shape : Shape2d ) = shape.smartExtrude( ribSize.y )
        .edges( ProfileEdge.chamfer( ribChamfer ) )

    meth topPoint() = Vector2( diameter/2, 0 ).rotateDegrees( topAngle )
    meth bottomPoint() = Vector2( diameter/2, 0 ).rotateDegrees( bottomAngle )


    meth mainSlot() : Shape2d = Square( ribSize.x + slotClearance.x, ribSize.y + slotClearance.y)
        .centerY()
        .margin( -slotClearance.x, 0 )

    meth petalSlot() : Shape2d = Square( ribSize.x + petalSlotClearance.x, petalThickness + petalSlotClearance.y)
        .centerY()
        .margin( -petalSlotClearance.x, 0 )


    // The pieces should slot together easily, with a slight friction fit.
    // If the fit is too tight increase slotClearance/petalSlotClear Y value.
    // When pushed all the way in, the edges should align.
    // If they don't, adjust slotClearance/petalSlotClear X value.
    // Use the same print settings and filament that you will use for the actual pieces.
    @Piece( about="Test pieces to help adjust slotClearance and petalClearance" )
    meth testFitRib() : Shape3d {
        return (
            Square( ribSize.x ).centerY() -
            mainSlot().rightTo(ribSize.x/2) -
            petalSlot().centerYTo( ribSize.x * 0.25 ).leftTo(ribSize.x/2)
        ).extrude( ribSize.y ).tileX(2, 1)
    }

    @Piece
    meth testFitPetal() : Shape3d {
        return extrudeRib(
            Square( ribSize.x ).centerY().roundAllCorners(2) -
            mainSlot().rightTo(ribSize.x/2)
        )
    }

    @Piece
    meth lowerTop() : Shape3d {

        val topPoint = topPoint()
        val shape = Circle( topPoint.x + ribSize.x/2 ) - Circle(topPoint.x - ribSize.x/2 )

        val slots = mainSlot()
            .leftTo( topPoint.x )
            .repeatAround( ribCount )

        return extrudeRib(shape - slots)
    }

    @Piece
    meth top() : Shape3d {
        val outer = lowerTop()

        val topPoint = topPoint()
        
        val inner = extrudeRib(
            Circle( fixtureDiameter/2 + ribSize.x ) - Circle( fixtureDiameter/2 )
        )

        val ribs = Square( topPoint.x - fixtureDiameter + ribSize.x*0.75, ribSize.x/2 - 0.2 )
            .roundCorner(3,10)
            .roundCorner(2,10)
            .mirrorX()
            .leftTo( fixtureDiameter/2 + ribSize.x/2 )
            .extrude( ribSize.y )
            .rotateX(90)
            .centerY()
            .rotateZ(360/2/ribCount)
            .repeatAroundZ( 6 )
            
        return outer + inner + ribs
    }

    @Piece
    meth bottom() : Shape3d {

        val bottomPoint = bottomPoint()
        val shape = Circle( bottomPoint.x + ribSize.x/2 ) - Circle(bottomPoint.x - ribSize.x/2 )

        return shape.extrude( ribSize.y )
    }

    @Piece
    meth ribEven() = rib( true )

    @Piece
    meth ribOdd() = rib( false )

    @Piece
    meth ribEven6() = ribEven().rightTo(ribSize.x + 5)
        .repeatX(3, ribSize.x*2 )
        .rotateZ(180).also()

    @Piece
    meth ribOdd6() = ribOdd().rightTo(ribSize.x + 5)
        .repeatX(3, ribSize.x*2 )
        .rotateZ(180).also()

    meth rib( even : bool ) : Shape3d {
        val topPoint = topPoint()
        val bottomPoint = bottomPoint()

        val shape = PolygonBuilder().apply {
            moveTo( topPoint + Vector2( 0, ribSize.x ) )
            lineTo( topPoint )
            circularArcTo( bottomPoint, diameter/2, false, false )
        }.buildPath().thickness( ribSize.x )

        val slot = mainSlot()
        val petalSlot = petalSlot()

        var slots : Shape2d = slot
            .rightTo( topPoint.x )
            .centerYTo( topPoint.y + ribSize.x/4 )
            .translateY( ribSize.x/2 ).also()

        var isEven = false
        for ( petal in petalData ) {
            isEven = ! isEven
            if (isEven != even) continue
            slots += petalSlot
                //.leftTo( ribSize.x/2 )
                .rotate( - petal.tilt + petal.around )
                .translateX( diameter/2 + ribSize.x/4 )
                .rotate( -petal.around )
        }

        return extrudeRib(shape - slots)
    }

    meth petalShape( nominalSize : double ) : Shape2d {
        val size = diameter/2 * nominalSize/100

        val foo = size*0.1
        val bar = size*0.4
        val baz = size*0.4
        val x = size * 1.3

        return PolygonBuilder().apply {
            moveTo( size/2, -size/2 )
            circularArcTo( size/2, size/2, size/2, true, false )
            bezierByTo(
                Vector2( baz, 0 ),
                Vector2( bar, -foo ),
                Vector2( x, 0 )
            )
            bezierByTo(
                Vector2( -bar, -foo ),
                Vector2( -baz, 0 ),
                Vector2( size/2, -size/2 )
            )
        }.build().leftTo( -ribSize.x/2 )
    }

    @Piece
    @Slice( topFillPattern="concentric", bottomFillPattern="concentric" )
    meth petal0() = petal( petalData[0].size ).rotateZ(90)

    @Piece
    @Slice( topFillPattern="concentric", bottomFillPattern="concentric" )
    meth petal1() = petal( petalData[1].size ).rotateZ(90)

    @Piece
    @Slice( topFillPattern="concentric", bottomFillPattern="concentric" )
    meth petal2() = petal( petalData[2].size )

    @Piece
    @Slice( topFillPattern="concentric", bottomFillPattern="concentric" )
    meth petal3() = petal( petalData[3].size )

    @Piece
    @Slice( topFillPattern="concentric", bottomFillPattern="concentric" )
    meth petal4() = petal( petalData[4].size )

    meth petal( size : double ) = petalShape( size ).extrude( petalThickness )

    @Piece
    meth middle() : Shape3d {
        val bottomPoint = bottomPoint()
        return Circle( bottomPoint.x ).extrude( petalThickness )
    }

    @Piece( printable = false )
    meth assembled() : Shape3d {
        val topPoint = topPoint()
        val bottomPoint = bottomPoint()

        val ribsEven = ribEven()
            .centerZ()
            .rotateX(90)
            .repeatAroundZ( ribCount / 2 )

        val ribsOdd = ribOdd()
            .centerZ()
            .rotateX(90)
            .rotateZ( 360 / ribCount )
            .repeatAroundZ( ribCount / 2 )

        val ribs = ribsOdd + ribsEven

        val top = top()
            .mirrorZ()
            .topTo( topPoint.y + ribSize.x * 0.75 + ribSize.y/2 )

        val lowerTop = lowerTop()
            .bottomTo( top.top - ribSize.x/2 - ribSize.y )

        val bottom = bottom()
            .bottomTo( bottomPoint.y + ribSize.x/2 )

        val middle = middle()
            .bottomTo( bottom.top + 0.1 )
            .previewOnly()

        var petals : Shape3d = Union3d()
        var extraRotation = 0

        var maxX = 0

        for ( petal in petalData ) {
            val newPetal = petal( petal.size )
                .centerZ()
                .leftTo( 0 )
                // Tilt the petal
                .rotateY( petal.tilt - petal.around )
                // Move to the equator
                .translateX( diameter/2 ) //+ ribSize.x/2 )
                // Move to the correct latitude
                .rotateY( petal.around )
            maxX = Math.max( maxX, newPetal.right )

            petals += newPetal
                // Make copies all the way round
                // Comment out this line to see only 1 petal of each size.
                .repeatAroundZ( ribCount/2 )
                // alternate between odd and even ribs
                .rotateZ( extraRotation )
                .color( "GhostWhite")

            // alternate between odd and even ribs
            extraRotation = 360/ribCount - extraRotation
        }

        val diameterText = Text("Total Diameter = ${maxX*2}mm")
            .extrude(1)
            .rotateY(-90).rotateZ(90)
            .topTo(topPoint.y + ribSize.x).leftTo( maxX )
            .previewOnly()
        val holeDiameter = (middle.size.x - ribSize.x).toInt()
        val holeDiameterText = Text( "Hole = ${holeDiameter}mm" )
            .extrude(1)
            .mirrorX().rotateZ(180).centerY()
            .topTo( middle.bottom )
            .centerX()//.leftTo( -holeDiameter/2 )
            .previewOnly()

        val shade = ribs + top + lowerTop + middle + bottom + petals

        return shade + diameterText + holeDiameterText
    }

    // Rotate the result so that the preview image gives a good impression.
    // Without the rotation, the image is as if your eyes were near the ceiling!
    @Piece( printable = false )
    override fun build() = assembled().rotateX(-130).rotateZ(15)
}

class Petal(
    val around : double,
    val tilt : double,
    val size : double
) {
}