在〈使用 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
區域來包住先前的程式碼而已,這當然不是最後的成果,目的只是要你在下一步,將 translate
、linear_extrude
與 text
中使用到的變數,放到 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
,不是 undef
就 count + 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 中,有興趣的可以看看: