module 與 function


在〈使用 OpenSCAD CheatSheet〉中最後談到了自訂模組,嗯!學程式語言最討厭大概就是名詞定義了,模組這玩意兒不是你在一 些主流語言中看過的那玩意兒,實際上函式在 OpenSCAD 也不是!

沒有魔法數字

不過要談到 OpenSCAD 的模組或函式之前,先來看一個重要的觀念,就以〈使 用 OpenSCAD CheatSheet〉中的一個範例來說好了:

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

嗯?這範例有什麼問題?10 是什麼?-5 又是什麼?理想上,在一個 OpenSCAD 中,呼叫模組或函式時,最好不要使用數字!最好是給予數字一個具意義的變數名稱!例如:

height = 10;
offset_for_center = -height / 2;

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

這樣在呼叫函式或模組時,看起來會清楚的多。不過也不是只有用數字,最好是將值都給予一個具意義的變數名稱,像是 "春""標楷體" 也可以給予一個變數名稱:

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);

這樣程式碼就是最理想的情況,在撰寫 OpenSCAD 程式的過程中,每一段時間,就得記得回顧一下程式碼,做這類給予值一個名稱的動作。

建立模組

給予值一個名稱的動作,除了可讀性的考量之外,另一個目的就是,當你發現模型已達一個階段性目標時,要將之封裝為模組就會非常方便,例如,若上 頭你想要建立一個 chinese_word 模組,第一步可以這麼寫:

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();

可以看到,在 OpenSCAD 中使用 module 來建立模組,而上面這個範例,只是使用了 module 區域來包住先前的程式碼而已,這當然不是最後的成果,目的只是要你在下一步,將 translatelinear_extrudetext 中使用到的變數,放到 module 的參數列而已,可以的話,順便在參數的順序安排上花一點心思:

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);

單看 chinese_word(word, font, height, offset_for_center) 這句,是不是清楚多了?現在想了一下 offset_for_center 這個,好像可以更有彈性一些,讓使用者能自行決定要不要置中,這可以這麼修改:

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);

這邊看到了 center = true,這是預設參數,如果沒有提供 center 參數,那麼它就是 true。模組中用了 ?: 三元運算子,? 前如果是 true,就傳回 : 前的值,否則傳回 : 後的值。那麼,接下來如果想將 chinese_word,繞 X 軸旋轉 90 度,只要寫…

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

這樣的程式碼,是不是比層層縮排要來得容易閱讀,具有彈性的多了?!現在來回顧一下,〈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]);
} 

相信你應該看得懂程式碼在做什麼了,自行試著將之封裝為模組吧!

一個複雜的模型,一定是由許多基礎的子模型建構而來,就像其他類型的程式中,一個複雜的任務演算,一定是由許多子任務演算來完成;每當你覺得模 型達到一個子目標(也就是那種…嗯…接下來可以做下一步了的時候),就可以將之建立為模組,久而久之,你就會累積起自己的常用模組。

建立函式

在 OpenSCAD 中,模組很具體,就是一個可見的模型,也就是,你的程式會在 OpenSCAD 的模型預覽上作出改變,套句 Functional programming 中的術語,你可以說模組是有副作用(Side effect)的 操作。

其實在命令式語言中,被稱之為函式或方法的東西,就相當於 OpenSCAD 中的模組,而 OpenSCAD 中的函式,真的就是…數學函式的概念,就像是 f(x) = x + 1,你給它 x 為 1,一定是傳回 2,不會有副作用,絕對的引用透明(Referential Transparency)。

好啦!我並不想賣弄術語,簡單來說,如果你有一個數學運算,像是想使用畢氏定理求斜邊長好了,就可以使用 OpenSCAD 的函式:

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

可以看到,建立函式使用 function,當然不只有能建數學公式,只要是給一些引數,傳回一個值的演算,都可 以使用 function 來定義。

不過,比較麻煩的是,你不能用 if...else,這只能在模組中用,也不能用 for, 這也只能在模組用,if...else 比較好解決,可以改用 ?: 三元運算,沒有迴圈比較麻煩,需要重複性計算的演算怎麼辦?記得嗎?OpenSCAD 是 Functional programming,重複性的計算就是…使用遞迴!

「遞迴只應天上有,凡人只能用迴圈」是嗎?其實只要任務單一,每次遞迴只專注當次子任務的分解,不管前後任務之狀態,遞迴並不困難的!

在前面的範例有看到 len 函式,可以用來計算文字的長度,假設現在沒有 len 函式可以用了,我們自己來定義一個,記得,只要看當次任務該解什麼就好了,那麼,計算文字長度的當次任務是什麼,不就是「看看有沒有字元,有就在 計數上加 1」:

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

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

my_len 的函式本體來說,就是將 count 當成索引,取 text 中的字元,也就是「看看有沒有字元」,如果超出索引就是 undef,不是 undefcount + 1,也就是「有就在計數上加 1」,下次計數就是下一次呼叫 my_len 的事了…XD

順便一提的是,以 Functional programming 的術語來說,OpenSCAD 的模組就像是 Functional programming 中非純綷、有副作用的部份,而 OpenSCAD 的函式,就像是 Functional programming 中純綷、無副作用的部份。

use 與 include

一些可重用的模組或函式,你可以分門別類地放在不同的 .scad 檔案中,為檔案取個適當的名稱,之後若需要檔案中的某些函式或模組,可以使用 use 來指定檔案。例如,你有個 2d.scad,當中有一些 2D 繪圖的模組或函式,在另一個檔案中想使用時,可以如下:

use <2d.scad>;

use 很單純,只會使用指定檔案中的模組或函式定義,也就是說,如果有個 a.scad 被 b.scad 使用了(use <a.scad>;),而現在你有個 c.scad 需要 a.scad 與 b.scad 中的定義,那麼就必須同時在 c.scad 中:

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

只是在 c.scad 中使用一個 use <b.scad>;,並不會自動執行 b.scad 中的 use <a.scad>

use 只會使用指定檔案中的定義,不會執行指定檔案中的程式碼;另一個 include, 會將被 include 的檔案,直接合併至目前檔案中執行,也就是說,被 include 的檔案中之程式碼會被執行,因此,被 include 的檔案中設定的變數也會生效,這是使用 include 的一個情境。

use 不同的是,如果有個 a.scad 被 b.scad include 了,而現在有個 c.scad 又 include 了 b.scad,那麼,就是將 a.scad、b.scad、c.scad 合併然而一起執行。

順便一提的是,我這陣子玩的 3D 模型原始碼,一些可重用的部份,收集在兩個 repoitory 中,有興趣的可以看看: