<3 OpenSCAD

I know that I had the piece a few days ago about the OpenSCAD tutorial, but I kinda gotta come back and talk about this more. This is bonkers and I love it so much.

As I’ve alluded to in the past, I’m in the middle of making a [REDACTED] for [REDACTED] this year, which calls for a 3D model for printing. I’ve tried so many different tools, open source and proprietary, and OpenSCAD is the first one that I’ve worked with that worked for me in a way that I understand. I know that other tools are likely far more capable, but so far I haven’t run into any limits of OpenSCAD. I parameterized every size so that I could tweak it and keep up with the math a bit more easily, and the OpenSCAD syntax makes this super easy. This particular model is meant to look like a hard hat, so let’s talk about the model and the code (yes, it’s code), and the process I went through to get there.

Notes really quick

Quick set of notes as we dig in:

  • OpenSCAD is read right-to-left by the interpreter, so if I have sphere(1) and I want to resize it to being an pill shape, I would use resize([100,50,50]) sphere(1). The sphere(1) is evaluated first, and resize([]) is evaluated second, with the output of sphere(1) as its input.
  • I’ll past the whole source file at the end of the article, so if you see a variable and you wonder what its value is check there.
  • The appearance of the OpenSCAD app itself isn’t really important here, though it is very yellow.
  • In a difference() transform, the first child element is the starting point and everything that follows is subtracted from it.
  • In OpenSCAD you can use any unit of measure so long as that unit of measure is millimeters or degrees (for angles). Inches, radians, fathoms, these are not supported units in OpenSCAD from what I understand. In OpenSCAD a dimension of 1 means a dimension of 1mm.

Getting started

The primatives of the model which resemble a lemondrop candy.

So when I started on this model, I used resized sphere primitives for the dome of the hat and the brim. I took a cylinder primitive and rotated it 90 degrees to make the ridge that’s typical across the top middle of many hard hat types. You can see above how these three primitives kinda fit together, but this most certainly does not look like a hat.

union() {
    // Top shell
    resize([
        hat_width,
        hat_length,
        hat_depth*2
    ])
        sphere(1);
    // Vertical ridge
    resize([
        hat_ridge_width,
        hat_length+hat_ridge_thickness,
        hat_depth*2+hat_ridge_thickness
    ])
        rotate([0,90,0])
        cylinder(h=1, d=1,center=true);
    // Brim of hat
    resize([
        hat_width+brim_width,
        hat_length+brim_width,
        brim_height*2
    ])
        sphere(1);
}Code language: OpenSCAD (openscad)

You can see I have the top shell, which is a sphere. I start the sphere with a radius of 1 because it doesn’t matter, I’m immediately resizing it. After that I use the cylinder to make the ridge and then another sphere that I flatten considerably becomes the brim. I union all of those items, and next it’s time to start removing stuff to make it more hat-shaped and less lemon drop shaped.

It's looking more hat-shaped! You can see inside the hat (where a hypothetical head would go) and you can see the brim is offset inward a little.

So here we are, the hat looks a lot more like a hat. Here’s the code:

difference() {
    union() {
        // Top shell
        resize([
            hat_width,
            hat_length,
            hat_depth*2
        ])
            sphere(1);
        // Vertical ridge
        resize([
            hat_ridge_width,
            hat_length+hat_ridge_thickness,
            hat_depth*2+hat_ridge_thickness
        ])
            rotate([0,90,0])
            cylinder(h=1, d=1,center=true);
        // Brim of hat
        resize([
            hat_width+brim_width,
            hat_length+brim_width,
            brim_height*2
        ])
            sphere(1);
    }
    // Remove an offset for the bottom
    color("red") translate([0,0,0])
        resize([
            hat_width+brim_width-lip_width,
            hat_length+brim_width-lip_width,
            2//lip_thickness
        ])
        cylinder(h=1,d=1);
    
    // Remove lower portion of brim
    translate([0,0,-hat_depth-hat_ridge_thickness+0.5])
        cylinder(
            h=hat_depth+hat_ridge_thickness,
            d=hat_length+brim_width
        );

    // Remove interior sphere
    translate([0,0,0])
        resize([
            hat_width-hat_thickness,
            hat_length-hat_thickness,
            hat_depth*2-hat_thickness
        ])
        sphere(1);
}Code language: OpenSCAD (openscad)

The union() statement is just what we have before, everything else is a Boolean transform on that. You can see that from that lemon drop shape I have first removed a portion of the under-side of the brim. This creates a lip that the cover will set into. After that I remove the lower half of the lemon drop using a cylinder primitive. After that, I remove the inside of the hat where a hypothetical head would go using a sphere primitive. This can be a little tricky as you develop your model, but OpenSCAD syntax allows you to prefix a percent symbol to the beginning of any statement and it’ll render a translucent variant so you can see it. Here I am viewing the sphere I’m removing from the interior of the hat.

The hard hat, but with the sphere being deleted from the inside portion of the hat where the head would go rendered in a semi-translucent white.

Before:

    // Remove interior sphere
    translate([0,0,0])
        resize([
            hat_width-hat_thickness,
            hat_length-hat_thickness,
            hat_depth*2-hat_thickness
        ])
        sphere(1);Code language: OpenSCAD (openscad)

After:

    // Remove interior sphere
    %translate([0,0,0])
        resize([
            hat_width-hat_thickness,
            hat_length-hat_thickness,
            hat_depth*2-hat_thickness
        ])
        sphere(1);Code language: OpenSCAD (openscad)

This is super helpful as you’re first developing your model, and it allows you to see the full extent of what you’re working with.

Screw holes!

This model needs to have a cover that screws on, so now I need screw holes. I iterated over the screw holes a lot, and I essentially came up with cylinder primitives with smaller cylinder primitives subtracted from them in their middle. After I worked on these a bit, I realized that there’s no need to repeat myself, so I took a page from DRY and made a module!

module screw_hole_post(
    hole_size=screw_hole_size,
    hole_post_size=screw_hole_post_size,
    hole_post_length=screw_hole_post_length,
    offset_x=screw_hole_offset_x,
    offset_y=screw_hole_offset_y,
    offset_z=screw_hole_offset_z
) {
    difference() {
        translate([
            offset_x,
            offset_y,
            offset_z
        ])
            cylinder(
                h=hole_post_length,
                d=hole_post_size
            );
        translate([
            offset_x,
            offset_y,
            offset_z-1
        ])
            cylinder(
                h=hole_post_length,
                d=hole_size
            );
    }
}Code language: OpenSCAD (openscad)

In OpenSCAD, functions are closer to their mathematical understanding, and modules are for reusable pieces. You can see this module simple creates one cylinder and then subtracts a smaller cylinder from its middle. Here, I’ll add it to the hat now.

union() {
    for (dims = [
            [1,1,1],
            [-1,-1,1],
            [-1,1,1],
            [1,-1,1]
        ]) {
            screw_hole_post(
                hole_size=screw_hole_size,
                hole_post_size=screw_hole_post_size,
                hole_post_length=screw_hole_post_length,
                offset_x=dims.x*screw_hole_offset_x,
                offset_y=dims.y*screw_hole_offset_y,
                offset_z=dims.z*screw_hole_offset_z+lip_thickness
            );
    }
}Code language: OpenSCAD (openscad)

You can see here that I’ve used my screw_hole_post module inside of a for loop. This allows me to create four different screw hole posts based on those four vectors in the dims matrix (a matrix is a vector of vectors). So I make my four screw holes and hit F5 to render.

The hard hat with screw hole posts, but they're protruding through the top of the hat.

This is very close to what I want, but not quite what I want. I don’t want the screw hole posts sticking out through the top of the hat, so it’s time to use another Boolean transform: intersection()!

union() {
    intersection() {
        translate([0,0,0])
            resize([
                hat_width-hat_thickness,
                hat_length-hat_thickness,
                hat_depth*2-hat_thickness
            ])
            sphere(1);
        for (dims = [
                [1,1,1],
                [-1,-1,1],
                [-1,1,1],
                [1,-1,1]
            ]) {
            screw_hole_post(
                hole_size=screw_hole_size,
                hole_post_size=screw_hole_post_size,
                hole_post_length=screw_hole_post_length,
                offset_x=dims.x*screw_hole_offset_x,
                offset_y=dims.y*screw_hole_offset_y,
                offset_z=dims.z*screw_hole_offset_z+lip_thickness
            );
        }
    }
}Code language: OpenSCAD (openscad)

This snippet is doing a lot, so here it is step-by-step (ordered!):

  1. The four screw hole posts are created inside the for loop based on the dims matrix, just as before.
  2. Then it calculates a sphere which matches the dimensions of the hat, less its thickness.
  3. It’s then calculating the intersection (area of overlap) between that sphere and the screw hole posts, discarding any portions which do not have any intersections.
  4. Then it groups all of those in a union()

When it’s done, you have something that looks like this:

This is much more like it! Here we have a lip in which the cover can rest, we have screw hole posts which are only coming out the bottom, and there’s a nice cavity for electronics.

Now for the cover

The cover is pretty straight-forward: it’s a thin slice of material matching the dimensions of the inside of the lip with small enough tolerance that we can fit it into position but not too loose of a fit that it just falls out. It’ll need holes that line up with the screw holes, and its thickness should be such that when it’s in position it’s flush with the lip on the brim. We want something like this…

Here’s the code:


// Bottom cover
difference() {
    // This is the actual cover itself.
    translate([
        cover_offset_x,
        cover_offset_y,
        cover_offset_z
    ])
        resize([
            hat_width+brim_width-lip_width-cover_dimensional_offset,
            hat_length+brim_width-lip_width-cover_dimensional_offset,
            cover_thickness
        ])
        cylinder(
            h=1,
            d=1
        );

    // Screw holes in the cover
    for (dims = [
            [1,1,1],
            [-1,-1,1],
            [-1,1,1],
            [1,-1,1]
        ]) {
        translate([
            dims.x*screw_hole_offset_x+cover_offset_x,
            dims.y*screw_hole_offset_y+cover_offset_y,
            dims.z*cover_offset_z-1
        ])
            cylinder(
                h=cover_thickness*2,
                d=screw_hole_size
            );
    }
}Code language: OpenSCAD (openscad)

You’ll see some patterns here that are familiar, let’s go over it. First things first, this is going to be a difference() Boolean operation, so the first item is the subject and everything afterward exists only to subtract from that subject. We start with a cylinder that we’re setting to dimensions to fit tightly in the lip, less our 0.5mm tolerance. We set the thickness to 2mm. From there we’re removing material to allow the screws in. We’re using the same dims matrix that we used before, and we’re creating cylinders that are twice the thickness of the cover and with a diameter of the screw hole size in the screw hole posts.

That’s pretty much it! That’s the model. I’ll post the code below, but one thing to note: in OpenSCAD it’s important to allow room for overlap. If two objects are butted up against one another, they may or may not be connected. It’s sometimes important to make them overlap by a teensy amount, say 0.0001mm, just to make sure that there’s no ambiguity when you render.

That’s all, folks

Well, that’s my first real model that I designed myself. I hope that this gets you curious about what you could accomplish in a model of your own. As promised, here’s the full code for the model.

$fa=1;
$fs=0.4;
$fn=56;

connect_overlap=0.0001;
void_overlap_margin=5;

// Screw hole sizes
screw_hole_size=6;
screw_hole_post_size=12;
screw_hole_post_length=30;
// Screw hole offsets
screw_hole_offset_y=29;
screw_hole_offset_x=29;
screw_hole_offset_z=0;

// Hat dimensions
hat_width=90; // Width of the top dome of the hat
hat_length=100;
hat_depth=38;
hat_thickness=2;
hat_ridge_width=12;
hat_ridge_thickness=2;
brim_width=20;
brim_height=10;

// Lip dimensions, for where the cover fits
lip_width=5;
// The thickness of the lip; should be same as the cover thickness.
lip_thickness=2;

// Cover dimensions
cover_dimensional_offset=0.5;
cover_thickness=2;
cover_offset_x=120;
cover_offset_y=0;
cover_offset_z=0;


module screw_hole_post(
    hole_size=screw_hole_size,
    hole_post_size=screw_hole_post_size,
    hole_post_length=screw_hole_post_length,
    offset_x=screw_hole_offset_x,
    offset_y=screw_hole_offset_y,
    offset_z=screw_hole_offset_z
) {
    difference() {
        translate([
            offset_x,
            offset_y,
            offset_z
        ])
            cylinder(
                h=hole_post_length,
                d=hole_post_size
            );
        translate([
            offset_x,
            offset_y,
            offset_z-1
        ])
            cylinder(
                h=hole_post_length,
                d=hole_size
            );
    }
}

// Overall top shell
difference() {
    union() {
        // Top shell
        resize([
            hat_width,
            hat_length,
            hat_depth*2
        ])
            sphere(1);
        // Vertical ridge
        resize([
            hat_ridge_width,
            hat_length+hat_ridge_thickness,
            hat_depth*2+hat_ridge_thickness
        ])
            rotate([0,90,0])
            cylinder(h=1, d=1,center=true);
        // Brim of hat
        resize([
            hat_width+brim_width,
            hat_length+brim_width,
            brim_height*2
        ])
            sphere(1);
    }
    
    // Remove an offset for the bottom
    color("red") translate([0,0,0])
        resize([
            hat_width+brim_width-lip_width,
            hat_length+brim_width-lip_width,
            2//lip_thickness
        ])
        cylinder(h=1,d=1);
    
    // Remove lower portion of brim
    translate([0,0,-hat_depth-hat_ridge_thickness+0.5])
        cylinder(
            h=hat_depth+hat_ridge_thickness,
            d=hat_length+brim_width
        );

    // Remove interior sphere
    translate([0,0,0])
        resize([
            hat_width-hat_thickness,
            hat_length-hat_thickness,
            hat_depth*2-hat_thickness
        ])
        sphere(1);
}

// Screw holes
union() {
    intersection() {
        translate([0,0,0])
            resize([
                hat_width-hat_thickness,
                hat_length-hat_thickness,
                hat_depth*2-hat_thickness
            ])
            sphere(1);
        for (dims = [
                [1,1,1],
                [-1,-1,1],
                [-1,1,1],
                [1,-1,1]
            ]) {
            screw_hole_post(
                hole_size=screw_hole_size,
                hole_post_size=screw_hole_post_size,
                hole_post_length=screw_hole_post_length,
                offset_x=dims.x*screw_hole_offset_x,
                offset_y=dims.y*screw_hole_offset_y,
                offset_z=dims.z*screw_hole_offset_z+lip_thickness
            );
        }
    }
}

// Bottom cover
difference() {
    // This is the actual cover itself.
    translate([
        cover_offset_x,
        cover_offset_y,
        cover_offset_z
    ])
        resize([
            hat_width+brim_width-lip_width-cover_dimensional_offset,
            hat_length+brim_width-lip_width-cover_dimensional_offset,
            cover_thickness
        ])
        cylinder(
            h=1,
            d=1
        );

    // Screw holes in the cover
    for (dims = [
            [1,1,1],
            [-1,-1,1],
            [-1,1,1],
            [1,-1,1]
        ]) {
        translate([
            dims.x*screw_hole_offset_x+cover_offset_x,
            dims.y*screw_hole_offset_y+cover_offset_y,
            dims.z*cover_offset_z-1
        ])
            cylinder(
                h=cover_thickness*2,
                d=screw_hole_size
            );
    }
}Code language: OpenSCAD (openscad)