module and function


I mentioned module at the end of Introduction to “OpenSCAD CheatSheet”. You probably don't like definitions or terms in programming languages, such as the difference between function and method. But the fact is, module and function in OpenSCAD are different from those similar things in other languages.

No magic number

Before diving into module and function, there's one thing you should know first. To illustrate, let's see the code below.

translate([-5, -5, -5])
    linear_extrude(10) 
        text("春", font = "標楷體");

Hmm? Anything wrong? What does the number 10 mean? Why is -5 used in translate? Ideally, it's better to replace a magic number with a meaningful name when you write OpenSCAD. For example.

height = 10;
offset_for_center = -height / 2;

translate([offset_for_center, offset_for_center, offset_for_center])
    linear_extrude(height) 
        text("春", font = "標楷體");

Now, it's clearer when invoking the module or doing the transformation. Not only numbers, but you may give every value a name. For instance, "春" or 標楷體 could be assigned to a variable, too.

height = 10;
offset_for_center = -height / 2;
word = "春";
font = "標楷體";

translate([offset_for_center, offset_for_center, offset_for_center])
    linear_extrude(height) 
        text(word, font = font);

The code above is the ideal. While writing OpenSCAD, remember to review your code occasionally and give every magic number a meaningful name.

Defining modules

If every value has a suitable name, the readability will be improved. It's also convenient if you want to encapsulate code into a module. The following example illustrates how to encapsulate the previous operations into the chinese_word module.

height = 10;
offset_for_center = -height / 2;
word = "春";
font = "標楷體";

module chinese_word() {
    translate([offset_for_center, offset_for_center, offset_for_center])
        linear_extrude(height) 
            text(word, font = font);
}

chinese_word();

As you see, the module keyword is used to define a module. I just put those actions into the body of the module. The code is of course not what I want. The next step is putting the variables used by translate, linear_extrude and text into the parameter list of the chinese_word module. It's better to arrange an suitable order of the variables.

height = 10;
offset_for_center = -height / 2;
word = "春";
font = "標楷體";

module chinese_word(word, font, height, offset_for_center) {
    translate([offset_for_center, offset_for_center, offset_for_center])
        linear_extrude(height) 
            text(word, font = font);
}

chinese_word(word, font, height, offset_for_center);

It's clearer if you look only at chinese_word(word, font, height, offset_for_center), isn't it? Let's think further. Can we make offset_for_center more flexible? How about providing an option for users to determine whether they want to align center the model or not?

height = 10;
word = "春";
font = "標楷體";

module chinese_word(word, font, height, center = true) {
    offset_for_center = center ? -height / 2 : 0;
    translate([offset_for_center, offset_for_center, offset_for_center])
        linear_extrude(height) 
            text(word, font = font);
}

chinese_word(word, font, height, center = false);

In the parameter list, center = true means that true is the default value if you don't provide an argument for the center parameter.

In the module, the ?: is a ternary operator. Take expression test ? expr1 : expr2 for example. If test is true, it returns the evaluated value of expr1, otherwise, returns the expr2's value.

Writing the code below to rotate a chinese_word model 90 degrees around the x-axis.

rotate([90, 0, 0]) 
    chinese_word(word, font, height, center = false);

Compared with the beginning code, it's easier to write and read, right? Now, let's recap the example of Hello, OpenSCAD!.

my_text = "Hello, OpenSCAD!";
step_angle = 30;
radius = 30;
height = 5;

len_of_my_text = len(my_text);

for(i = [0:len_of_my_text]) {
    rotate(step_angle * i) 
        translate([radius, 0, i * 5]) 
            linear_extrude(height) 
                text(my_text[len_of_my_text - i]);
} 

I believe that you can easily understand what the code does now. As an exercise for you, try to encapsulate it into a module.

Just like a task in other programming domains, you can break down a problem into sub-problems. No matter how complex a model is, you can break down it into sub-models. Every time you make progress with your model, try to extract a sub-module from it. In time, you'll have a wealth of your commonly-used modules.

Defining functions

Modules are concrete things in OpenSCAD. They have visible results. If you make changes to a module, the model's preview changes immediately. Regarding Functional programming, invoking a module has side effects.

Methods and functions in other languages may have side effects, too. In this case, they behave like modules in OpenSCAD. As for function in OpenSCAD, they act more like functions in mathematics. Take function(x) = x + 1 for example. If the argument is 1, then the returned value is 2. Just like the function f(x) = x + 1 in mathematics. A function has no side effect. That's to say that they are referentially transparent because you can replace its argument without changing the function's behavior.

All right, no more jargon. Simply put, if you want to perform a mathematical calculation, such as getting the length of the hypotenuse from the other two sides, you may define a function in OpenSCAD. For example.

length_side1 = 10;
length_side2 = 20;

function length_hypotenuse(length_side1, length_side1) = 
    sqrt(pow(length_side1, 2) + pow(length_side2, 2));

echo(length_hypotenuse(length_side1, length_side1));  // ECHO: 22.3607

We use the function keyword to define a function. Of course, you may use function to describe any calculation which takes several arguments and then returns a result.

There are problems here. Yon cannot use if...else or for in functions. Both are only available in modules. A workaround for if...else is the ?: operator. It's troublesome, however, to do a repeating task without a loop. Well, remember that OpenSCAD is Functional programming? The way to solve this issue is using recursions.

“To iterate is human, to recurse, divine.” Is this true? No! If you can divide a problem into subproblems small enough, finding the task of the same type will be easier, and recursively solving it is NOT that hard.

There's a len function which returns the length of the text in the previous code. We can define our own len function. Remember, try to find the task of the same type first. So, what's the same job when you calculate the length of the text? That is “if the counter can be used to get a character from the text, increment the counter and do it again.”

function my_len(text, counter = 0) = 
    text[counter] == undef ? counter : my_len(text, counter + 1);

echo(my_len("TEST"));  // ECHO: 4

In the body of my_len, we use the counter variable as an index to retrieve the element of text. If the value of counter is out of index, text[counter] is undef. That's the situation that you can't get a character from the text. If it's not undef, we use counter + 1 to invoke my_len again. That is, do it again.

By the way, regarding Functional programming, modules in OpenSCAD are impure, and side-effecting and functions in OpenSCAD are pure and side-effect free.

use and include

You can categorize modules or functions into different “.scad” files, and give each file a suitable name. If you need modules and functions, you can use use <filename> to import them. For example. You have modules and functions for a 2D graph in “2d.scad”. In the main code, use the following statement to import them.

use <2d.scad>;

The statement does not execute any commands other than those definitions. So, if “b.scad” uses “a.scad”, and if “c.scad” needs all definitions of both, you still have to write the statements in “c.scad”.

use <a.scad>;
use <b.scad>;

Writing only use <b.scad> in “c.scad” will not execute use <a.scad> in “b.scad” automatically.

use <filename> imports modules and functions, but does not execute any commands other than those definitions. If you want to acts as if the contents of the included file were in the including file, use include <filename>.

When using include <filename>, the top-level variables of the included file will be active, too. It allows you specify default variables in the library. These defaults can be overridden in the main code if necessary. Remember I've said in Introduction to “OpenSCAD CheatSheet”, if you assign values to a variable repeatedly, OpenSCAD will not throw an error. In the same variable scope, the variable will keep the last assigned value.

If “b.scad” includes “a.scad”, “c.scad” includes “b.scad” and you run “c.scad”, all contents in these three files will be merged and executed.