Sunday, May 8, 2016

OpenSCAD Rendering Tricks, Part 2: Laser Cutting

This is my fifth post in a series about the open source split-flap display I’ve been designing in my free time. Check out a video of the prototype.

Posts in the series:
Scripting KiCad Pcbnew exports
Automated KiCad, OpenSCAD rendering using Travis CI
Using UI automation to export KiCad schematics
OpenSCAD Rendering Tricks, Part 2: Laser Cutting
OpenSCAD Rendering Tricks, Part 3: Web viewer

In addition to creating a nice animated rendering, I wanted to make sure I could consistently export the final vector design to be laser cut. There were three main challenges to this:
  1. Layout - All of the pieces that make up the 3D design need to be laid out flat so they can be cut out of a single sheet of wood.
  2. Kerf - When laser cutting, the beam burns away material, leaving a gap where cuts were made (referred to as kerf). This means that shapes will all be slightly smaller than desired if cut exactly to dimension, so the dimensions need to be adjusted to compensate.
  3. Generating output - Laser cutters typically operate using a vector image such as SVG, and expect a strict set of encoded properties, e.g. cut lines in blue, vector engraving in black, etc, so we need to transform OpenSCAD’s SVG output to conform.


Layout

For a little background, in the 3d model I designed each distinct piece (e.g. gear, front enclosure face, etc) as a planar shape (to be cut out of thin MDF wood board) laying flat on the XY plane. Here’s a simple example:

thickness = 4;
module a() {
    color("red") {
        linear_extrude(thickness, center=true) {
            difference() {
                square([40,80]);
                translate([10, 10]) {
                    square([20, 60]);
                }
            }
        }
    }
}

module b() {
    linear_extrude(thickness, center=true) {
        difference() {
            square([40, 40]);
            translate([20, 20]) {
                circle(r=15);
            }
        }
    }
}





Because each piece is a separate module, they can be moved and rotated (using the translate and rotate operators) to be assembled into a 3d model, or laid out flat next to each other in the plane for laser cutting:

module 3d() {
    translate([-2,0,0])
        rotate([0,-90,0])
            a();
    translate([0, 82, 0])
        rotate([90, 0, 0])
            b();
}

module flat() {
    projection() {
        a();
        translate([0, 90, 0]) {
            b();
        }
    }
}




The splitflap design uses this technique to reuse the same components in the 3d model and 2d flattened layout. The only thing you have to remember is to include all the pieces from the 3d model into the flattened module as well!


Kerf

While laser cutters enable small, intricate designs, it’s important to remember that just like a table saw blade, the laser beam doing the cutting is not infinitesimally small. This means that if the center of the laser follows the edges/lines of your design exactly, you will actually lose a small amount of material on either side of that line. This is referred to as “kerf,” which has a width that varies depending on the laser cutter, power/speed settings, and material being cut.

To illustrate, here’s an exaggerated example: you can see the desired design on the left, and in the middle I’ve superimposed a particularly wide “laser beam” path in blue as if the center of the laser followed the contours of the design to cut it out.



Notice how much less of the teal part is exposed in the middle image? On the right, you can see the material that would be left if a wood panel was cut using the blue “laser beam” path — the shape that we wanted came out way too small and thin!

To correct for this kerf, we need to adjust the design so that all edges are shifted outward by half the laser beam width. This can be done by applying the offset operator:

offset(delta=kerf/2) {
    projection() {
        a();
    }
}


Note that before the offset is applied a projection() is used, which flattens a 3d shape by removing the Z-axis. This is necessary because the offset operator only works on 2d geometry.

Below you can see the design after applying the kerf-adjustment offset on the left (it’s fatter and the hole is smaller than the original), along with an updated “laser beam” overlay in the middle image that follows those adjusted edges. If you look at what material would remain after cutting, in the rightmost image, you can see that the remaining shape is actually the size that we wanted from our original design (compare it to the original in the left image above)!



On a real design, the impact of kerf won’t be quite so visually obvious as in this example (it’s something small like 0.2mm for the wood I used), but that small difference can be pretty important if you want a clean, tight fit.

Generating output

The last piece of the puzzle is taking the flattened 3d design that’s been kerf-corrected and shipping it off to be laser cut. I ordered my laser cut parts from Ponoko, which provides a template SVG file and expects certain image properties for different types of laser cuts:




One common technique to save money when laser cutting is to make multiple pieces share a common cut line since you’re generally charged for the total length of all cuts.

This presents a problem though if you use a simple export of a single SVG image — sometimes OpenSCAD will merge shapes if their edges perfectly overlap:

The bottom piece is actually two separate components that accidentally got merged together!


Another issue with exporting the entire design as a single SVG is that you can’t render overlapping components with 2d shapes. In the splitflap design, the text to be engraved is aligned directly on top of the bottom panel:




But when flattened into a 2d shape, the overlapping text is merged into the bottom panel shape, and since the bottom panel is larger than the engraved text, the text is lost completely in the exported design.

With a bit of scripting it’s not too difficult to export each component to its own SVG before merging them to avoid both of these problems. To start with, we can create a wrapper module that lets us render a single child element at a time (and we can also use this to apply the kerf correction discussed above):

module projection_renderer(render_index = 0, kerf_width = 0) {
    echo(num_components=$children);
    offset(delta=kerf_width/2) {
        projection() {
            // Only include a single child, the one at index "render_index"
            children(render_index);
        }
    }
}



To use it, we just wrap the list of laid out elements with it:

render_index = 0;
projection_renderer(render_index=render_index, kerf_width=0.1) {
    a();
    translate([0, 90, 0]) {
        b();
    }
}


Then from a python script, we can first run OpenSCAD to identify the number of individual components to render (determined by looking for the output of the echo(num_components=$children) statement from the projection_renderer), and then invoke OpenSCAD that many times, using the -D render_index=<value> command line argument to increment the render_index variable each time.


Once all the components have been exported as separate SVGs, it’s easy to combine the <path> elements from each SVG into a single file.

There are a few other tricks I used so that the python script can distinguish between components that should be cut out vs. engraved and apply the appropriate stroke and fill styles in the final SVG.

You can find those tricks and more details in the source code:
/3d/generate_2d.py
/3d/projection_renderer.scad
/3d/projection_renderer.py
/3d/svg_processor.py
/3d/openscad.py

In a past blog post, I discussed how I run this script using Travis CI to automatically render the flattened 2d design (shown at the top of this post) and more every time the source code changes. You should check it out if you haven’t already: Automated KiCad, OpenSCAD rendering using Travis CI.

OpenSCAD Rendering Tricks, Part 1: Animated GIF

This is my fourth post in a series about the open source split-flap display I’ve been designing in my free time. Check out a video of the prototype.

Posts in the series:
Scripting KiCad Pcbnew exports
Automated KiCad, OpenSCAD rendering using Travis CI
Using UI automation to export KiCad schematics
OpenSCAD Rendering Tricks, Part 1: Animated GIF

Early when designing the split flap 3D model using OpenSCAD I wanted to include a visualization in the project’s README so others could see what it looked like. It’s possible to capture an image manually (File→Export→Export as Image), but that’s an extra thing to remember to do after every change and it’s also not very consistent. The image that’s exported is basically a snapshot of the current preview window, so the image dimensions and camera angle would be different each time. Plus, a single static image doesn’t fully convey the 3D model, so I wanted something more dynamic.

The final product: a 360° animation that cycles through three views of the model.

I was inspired by Bryan Duxbury’s blog post on creating an animated gif from an OpenSCAD model. He used OpenSCAD’s built-in animation feature, which lets you parameterize your model using a special animation time variable, $t. To make a spinning animation, you can just wrap your model in a rotate transformation proportional to $t. This works well, but still requires some manual export steps from the GUI.

To fully automate this, I used OpenSCAD’s command-line interface which lets you specify options like --imgsize=width,height and --camera=translatex,y,z,rotx,y,z,dist to control the exported image. This makes it easy to write a script that exports snapshots from 360 degrees:

num_frames = 50
start_angle = 135
for i in range(num_frames):
    angle = start_angle + (i * 360 / num_frames)
    openscad.run(
        'splitflap.scad',
        'frame_%05d.png' % i,
        output_size = [320, 240],
        camera_translation = [0, 0, 0],
        camera_rotation = [60, 0, angle],
        camera_distance = 600,
    )


(This uses a simple Python wrapper to invoke OpenSCAD’s command line interface)

 In addition to a simple rotation, I wanted to showcase different parts of the model in the animation. At the top of splitflap.scad , I defined a few variables that control the visibility/opacity of the enclosure and flaps (this was also useful while designing the model):

render_enclosure = 1; // 2=opaque color; 1=translucent; 0=invisible
render_flaps = true;


Then from a script, I can invoke OpenSCAD using arguments like -D render_enclosure=0 -D render_flaps=false which override the variable definitions in the file. I use this so that over the course of three animated revolutions you can see all the different parts of the design.

Three different views of the model by changing the render_enclosure and render_flaps variables.
Unfortunately, by invoking openscad once per frame, the 3D model’s geometry needs to be recompiled for every camera angle rendered, which takes a nontrivial amount of time. With a desired 50 frames per revolution * 3 rendering options, that’s 150 total invocations of OpenSCAD! As far as I can tell there’s no easy way around this, but we can still speed it up by using multiple cores.

Using a threadpool (multiprocessing.dummy.Pool in Python) we can enqueue each of the OpenSCAD frame-rendering tasks to be run in parallel across a specified number of workers. Since each OpenSCAD process uses up to a single core, we can choose a pool size to match the number of cores available.

from multiprocessing.dummy import Pool
num_frames = 50
start_angle = 135
def render_frame(i):
    angle = start_angle + (i * 360 / num_frames)
    openscad.run(
        'splitflap.scad',
        'frame_%05d.png' % i,
        output_size = [320, 240],
        camera_translation = [0, 0, 0],
        camera_rotation = [60, 0, angle],
        camera_distance = 600,
    )
pool = Pool() # By default, Pool uses one thread per available CPU
for _ in pool.imap_unordered(render_frame, range(num_frames)):
    # Consume results as they occur so any task exceptions are rethrown asap
    pass
pool.close()
pool.join()


As a minor aside, it’s not really necessary to use separate threads, since each task is already launching a separate subprocess, but a threadpool provides a convenient abstraction for bounded parallel execution.

On my machine, rendering with a 4-thread Pool reduced the rendering time from 6 minutes 41 seconds down to just under 3 minutes!

The last step is to put all those frames together as an animated gif, which is fairly straightforward using ImageMagick:
convert 'frame_*.png' -set delay 1x15 animation.gif

The full script implementation can be found in the following files:
/3d/generate_gif.py
/3d/openscad.py

In a past blog post, I discussed how I run this script using Travis CI to automatically re-render the 3d animation every time I make a change to the source code. You should check it out if you haven’t already: Automated KiCad, OpenSCAD rendering using Travis CI.

Thanks for reading! In part 2 I’ll cover some more OpenSCAD tricks with similar command line scripting techniques to easily export a design for laser cutting.