Chapter 10: Modules - Modules that change its children

Modules can be used in different ways. So far, we’ve only used it to represent objects and assemblies. However, we can use it to it modify objects, much like translate() and rotate() do.

That way, we can not only build object assembly abstractions, but also object transformation abstractions to help the maintability and clarity of our code.

Using resize for tolerance

We’re going to refactor tolerance into a module that modifies objects. When we look inside of a8_3_engine(), we see that tolerance adjustments aren’t inherent to the concept of a a8_3_engine() module. Hence, we want to separate those two concerns into two different modules.

First, we’ll introduce resize()

1 module a8_3_engine() {
2   resize([engine_radius * 2, engine_radius * 2, engine_height] + tol * [1, 1, 1]\
3 ) {
4     cylinder(engine_height, engine_radius, engine_radius);
5   }
6 }

resize resizes the child objects (in this case, the cylinder) to dimensions specified. What gets passed into resize is an array for the dimensions in each of the x, y, and z directions. As a result, notice that the elements of the resize array is in a different order than the arguments for cylinder.

What we passed into resize is a bit of array math. It’s simple to follow.

1 [engine_radius * 2 + tol, engine_radius * 2 + tol, engine_height + tol]
2  = [engine_radius * 2, engine_radius * 2, engine_height] + [tol, tol, tol]
3  = [engine_radius * 2, engine_radius * 2, engine_height] + tol * [1, 1, 1]

We originally had the top array, and we can reduce it step by step to the last one. So by passing it in, we’re increasing every dimension of the cylinder by an extra tol

Making the with_tol() module

We’re going to abstract the tolerance adjustment into a module. Because OpenSCAD doesn’t let us query the size of an object, we’ll need to pass it in ourselves.

Let’s create a module called with_tol().

1 module with_tol(size) {
2   resize(size + tol * [1, 1, 1]) {
3     children();
4   }
5 }

Notice this is similar to what we had before, but now, size is passed into with_tol(), and we resize children(), which are the child modules under with_tol()

To use it, we now rewrite a8_3_engine() as:

1 module a8_3_engine() {
2   with_tol([engine_radius * 2, engine_radius * 2, engine_height]) {
3     cylinder(engine_height, engine_radius, engine_radius);
4   }
5 }

Notice that the array we pass into with_tol() is what’s referred to as size inside of with_tol(). The cylinder() (and everything inside the curly braces of with_tol) that’s used as a child module under with_tol() is referred to as children() inside of with_tol().

Making the chop() module

Now that we know how to make modules that operate on children, we’re going make other abstractions. This time, we want a module that chops off an entire side of a plane of a model, which is what we did to make the parabolic cone.

 1 module chop(r, plane) {
 2   difference() {
 3     children();
 4 
 5     theta = plane[0] == 0 ? 0 : atan(plane[1] / plane[0]);
 6     phi_abs = atan(sqrt(pow(plane[0], 2) + pow(plane[1], 2)) / plane[2]);
 7     phi_sign = plane[0] > 0 ? phi_abs : -phi_abs;
 8     phi = plane[2] >= 0 ? phi_sign : phi_sign + 180;
 9     
10     // cut off below plane
11     rotate([0, 0, theta])
12       rotate([0, phi, 0])
13         translate(-r * [0, 0, 1])
14           cube((2 * r) * [1, 1, 1], true);
15   }
16 }

Once again, since OpenSCAD doesn’t allow you to query the size of the children, we’ll need to pass it into chop(). That’s what the ‘r’ parameter is, where ‘r’ is the largest dimension of the children modules. plane is the normal vector to the plane that you want to chop the module.

The calculations for the spherical coordinate angles, theta and phi, where theta is the angle between X and Y dimensions, and phi is the angle between the hypotenuse (of X and Y) and the Z dimension. Because arctangent is only valid between 90 and -90, we need to change phi depending on the signs of the X dimension of plane and the Z dimension of plane.

In order to do so, we need to use the tertiary form of an if statement. It takes the form:

1 [_condition_] ? [_if condition is true_] : [_if condition is false_]

Because OpenSCAD is a declarative functional language, you cannot assign a variable more than once. In fact, it’s much better to think of variables in OpenSCAD as overridable constants. Hence, we’d need to use a teriary statement to decide what the value should be, and only assign it to a variable once.

1 // this is wrong
2 if (plane[0] == 0) {
3   theta = 0;
4 } else {
5   theta = atan(plane[1] / plane[0]);
6 }
1 // this is right
2 theta = plane[0] == 0 ? 0 : atan(plane[1] / plane[0]);

And now, we can use chop() like so:

1 module parabolic_cone(height, radius) {
2   chop(10, [0, 0, 1]) {
3     scale([1, 1, height / radius]) {
4       sphere(radius);
5     }
6   }
7 }

That greatly simplifies parabolic_cone(), and we can reuse chop() in other places of the code.

Summary

We learned

  • Creating modules that modify its children
  • How to use resize()
  • Tertiary operator for if statements
  • How OpenSCAD cannot assign a variable more than once

Full Source

 1 tol = 0.4;
 2 
 3 engine_radius = 17.5 / 2;
 4 engine_height = 70;
 5 
 6 nose_assembly_height = 26.25;
 7 nose_assembly_thickness = 1.2;
 8 nose_assembly_slot_height = 5;
 9 nose_assembly_radius = engine_radius + nose_assembly_thickness;
10 
11 module with_tol(size) {
12   resize(size + tol * [1, 1, 1]) {
13     children();
14   }
15 }
16 
17 // chops off model on entire side of plane
18 module chop(r, plane) {
19   difference() {
20     children();
21 
22     theta = plane[0] == 0 ? 0 : atan(plane[1] / plane[0]);
23     phi_abs = atan(sqrt(pow(plane[0], 2) + pow(plane[1], 2)) / plane[2]);
24     phi_sign = plane[0] > 0 ? phi_abs : -phi_abs;
25     phi = plane[2] >= 0 ? phi_sign : phi_sign + 180;
26     
27     // cut off below plane
28     rotate([0, 0, theta])
29       rotate([0, phi, 0])
30         translate(-r * [0, 0, 1])
31           //with_tol((2 * r) * [1, 1, 1])
32             cube((2 * r) * [1, 1, 1], true);
33   }
34 }
35 
36 module a8_3_engine() {
37   with_tol([engine_radius * 2, engine_radius * 2, engine_height]) {
38     cylinder(engine_height, engine_radius, engine_radius);
39   }
40 }
41 
42 module parabolic_cone(height, radius) {
43   chop(10, [0, 0, 1]) {
44     scale([1, 1, height / radius]) {
45       sphere(radius);
46     }
47   }
48 }
49 
50 difference() {
51   // the nose cone
52   parabolic_cone(nose_assembly_height, nose_assembly_radius);
53 
54   translate([0, 0, nose_assembly_slot_height]) {
55     mirror([0, 0, 1]) {
56       // the engine
57       // remove the '#' that was in front of the cylinder()
58       a8_3_engine()
59 
60       // the inner dome
61       sphere(engine_radius + tol);
62     }
63   }
64 }