FooCAD

Platforms : JVM
Language : Kotlin, Feather (scripting language)
Source Code (GitLab)

About

FooCAD is a 3D modelling program using a scripting language to define the models. It is inspired by OpenSCAD.

The scripting language is an object oriented language called Feather.

Why do I want to use a scripting language to define 3D models? Why not use a graphical user interface?

The example below creates a 3D model of 3 bolts, how would you do this using a graphical program? Make a rounded hexagon, extrude it to make the head. Make a cylinder for the shaft. Making the screw thread is a little trickier though right! Because the path that the thread takes is inherently a mathematical path, not something you want to draw by hand. Let's ignore that for now, we'll assume the GUI has a helix path generator, and an extrude along a path feature. So we have the final product. But now I say I want a longer bolt, with a wider head. You have to start from scratch.

I want a 3D modelling program where I never have to re-do steps. If I want a longer bolt, I change one number, and the bolt is longer. If I want a wider head, I change another number.

I am an accomplished programmer; using a scripting language is as natural to me as paint is to an artist.

Example Script

Here's a typical script, which creates three bolts, and some text with mitred edges :

class Bolts : MultiTargetModel {

    var boltRadius = 10
    var boltLength = 100
   
    fun build(layout: String) : Shape3d {

        // A Hexagon with rounded corners
        val roundedHex = Circle(boltRadius * 2.5).sides(6).roundAllCorners(5)
        val head = roundedHex.extrude( boltRadius * 1.5 )

        // A Cylinder with a bevel at one end.
        val shaftProfile = Circle( boltRadius )
        val shaft = ExtrusionBuilder()
            .crossSection( shaftProfile )
            .forward( boltLength - 2 )
            .crossSection( shaftProfile )
            .forward( 2 )
            .crossSection( shaftProfile.scale(0.85) )
            .build()


        // A simple triangle
        val threadProfile = Shape2dBuilder()
            .moveTo(0,-4)
            .lineTo(4,0)
            .lineTo(0,4)
            .build()

        val thread = ExtrusionBuilder.thread(
            threadProfile.translateX(boltRadius-1),
            boltLength - 10,
            10,
            30
        ).translateZ(5)


        val bolt = head + shaft + thread

        val edgeProfile = Circle(2).sides(4)
        val plainText = Text( "Bolts", 40 ).toPolygon()
        val textOutline = ExtrusionBuilder.followShape( plainText, edgeProfile )
        val fancyText = plainText.extrude(4).centerZ().color( "MediumSlateBlue" ) + textOutline.color("DarkBlue")

        val bolts = bolt.repeatX( 3, 60 ).color("Silver")

        return bolts + fancyText.translateY(-70)
    }

    fun targets() = CommonTargets.scadAndImage()

}

I'll now describe each part of the script :

class Example : MultiTargetModel {

This isn't terribly interesting, all scripts start like this, it's just boiler plate code.

var boltRadius = 10
var boltLength = 100

Declare two variables. Note that semi-colons aren't needed, but you can include them if you want. We didn't declare the types, but were deduced to be integers.

fun build(layout: String) : Shape3d {

All scripts have a function called build which returns a 3D shape. In this case, it will return a combination of the bolts and the text. We'll ignore 'layout' for now - it is only useful for more complex models.

Circle(boltRadius * 2.5).sides(6)

Create a circle with 6 sides (aka a hexagon!). 'sides' is a method of the Circle class. Notice that we need a "." to call methods as well as the brackets.

.roundAllCorners(5)

roundAllCorners is a method of all 2D Shapes (not just Circle). I chose a radius of 5 for the rounded corners.

val roundedHex = Circle(boltRadius * 2.5).sides(6).roundAllCorners(5)

'val' declares a variable (simliar to 'var'), but a 'val' cannot be re-assigned a new value.

val head = roundedHex.extrude( boltRadius * 1.5 )

Extrude is another method of all 2D shapes. It creates a 3D shape by extruding (stretching) the 2D shape up the Z axis. Extrudes the rounded hexagon up the Z axis to form the head of the bolt.

Next we will create the shaft of the bolt. We could use a simple cylinder like so :

Cylinder( boltLength, boltRadius )

But I wanted to add a chamfer on the end (and also to intruduce you to ExtrusionBuilder)...

val shaftProfile = Circle( boltRadius )

The shaft is made from a simple circle.

val shaft = ExtrusionBuilder()

We create an ExtrusionBuilder object. I like to think of it as a Play-Doh fun factory

val shaft = ExtrusionBuilder()
    .crossSection( shaftProfile )
    .forward( boltLength - 2 )
    .crossSection( shaftProfile )

This defines a cylinder (a circle of the same size at both ends). I stopped 2 units shy of the end, for the chamfer...

.forward( 2 )
    .crossSection( shaftProfile.scale(0.85) )

Move forward the last 2 units, but this time use a SMALLER circle as the cross section.

.build()

.forward(...) and .crossSection(...) returned the ExtrusionBuilder each time. The build() method creates a Shape3D and we no longer need the ExtrusionBuilder.

Note ExtrusionBuilder can create much more complex shapes. You can move in any direction you like (not only up the Z axis as we did here), and you can change to completly different cross sections after each segment.

Next we will define a thread for the outside of the shaft.

val threadProfile = Shape2dBuilder()
   .moveTo(0,-4)
   .lineTo(4,0)
   .lineTo(0,4)
   .build()

This is a long winded way to create a triangle. I chose to do it this way to introduce Shape2dBuilder, which can build much more complicated 2D shapes. As well as moveTo and lineTo, there is also arcTo, circularArcTo, bezierTo and quadBezierTo. There is also a method radius(n), which will cause all subsequent lineTo's to have rounded corners.

Another way to create 2D shapes is to use Inkscape, and import the shapes using SVGParser.

Back to the example...

val thread = ExtrusionBuilder.thread(
    threadProfile.translateX(boltRadius-1),
    boltLength - 10,
    10,
    30
)

We use the built in 'thread' function of ExtrusionBuilder. The parameters are :

  • A 2d profile (we are using a triangle),
  • Length (we are 10 units shorter of the bolt's length)
  • Pitch (the distance between the threads
  • Sides (the number of segments for one revolution around the thread).
val bolt = head + shaft + thread

We can define a bolt by adding the parts together. This is called a "Union" in OpenSCAD. We could return just a single bolt, but I wasnted to show you a few more things...

val edgeProfile = Circle(2).sides(4)

Create a diamond shape (a square rotated 45 degrees).

val plainText = Text( "Bolts", 40 ).toPolygon()

Create a 2D object with the word "Bolts" in font size 40.
The call to .toPolygon() converts the text into a more general polygon, which let's us play around with it a little...
val textOutline = ExtrusionBuilder.followShape( plainText, edgeProfile )

We take the text and trace round the outside with the diamond shape edgeProfile. This has the effect of adding a double chamfer to the text (the inside is empty).

val fancyText = plainText.extrude(4).centerZ().color( "MediumSlateBlue" ) + textOutline.color("DarkBlue")

Combine the plain text and the outline to form a fancy bit of text.

val bolts = bolt.repeatX( 3, 60 ).color("Silver")

There are many built-in layout methods, 'repeatX' being one of the simplest. In this case, we make 3 copies spaced 60 units apart.

return bolts + fancyText.translateY(-70)

At last, we have reached the end. Return the 3 bolts, and the fancy text below them.