module 與 function
February 24, 2022在〈OpenSCAD CheatSheet〉最後談到了模組與函式,在 OpenSCAD 中,模組用來繪製 2D/3D 圖形,函式用來計算,從函數式角度來看,模組有副作用,函式無副作用。
沒有魔法數字
在談到 OpenSCAD 的模組或函式前,先來看一個重要的觀念,就以〈OpenSCAD CheatSheet〉的範例來說好了:
translate([-5, -5, -5])
linear_extrude(10)
text("春", font = "標楷體");
嗯?這範例有什麼問題?10 是什麼?-5 又是什麼?最好為數字取個具意義的名稱!例如:
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);
在撰寫程式的過程中,每隔一段時間,記得整理一下程式碼,做這類為值取名稱的動作,這可以讓程式碼的可讀性提升,也容易發現可抽取出來重複使用的模組或函式。
定義模組
在發現繪製圖形的程式碼出現重複流程,或者已達階段性目標,封裝為模組就會非常方便,例如,若想將以上程式碼封裝為 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
建立模組,雖然名為模組,不過這邊的模組,不像其他語言中用來管理名稱的模組,OpenSCAD 的模組只是用來定義一個可重複使用的繪圖功能。
上面這個範例,只是使用了 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
;OpenSCAD 的模組或函式呼叫時,若給的引數個數可以多於參數個數,會發生警訊,如果少於參數個數,沒有被指定引數的參數會是 undef
,有時你會需要判斷參數是不是有指定值,以做出相對應的預設動作,這時可以使用 is_undef
來判斷參數值是不是 undef
。
模組中用了 ?:
三元運算子,?
前如果是 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 的模組會有輸出,也就是改變螢幕的狀態,或者是在控制台(console)輸出文字,就函數式的角度而言,可以說模組是有**副作用(Side effect)**的操作。
OpenSCAD 的函式,則是像數學函式的概念,就像是 f(x) = x + 1
,你給它 x
為 1,一定是傳回 2,不會有副作用。
簡單來說,如果你有一個數學運算,像是想使用畢氏定理求斜邊長好了,就可以使用 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
來定義;另外,對於 pow
函式,也可以使用 ^
運算子來取代。例如:
function length_hypotenuse(length_side1, length_side1) =
(length_side1 ^ 2 + length_side2 ^ 2) ^ 0.5;
若需要變數來代表計算結果,可以使用 let
,例如,定義計算兩點間距離的函式:
function length_between(point1, point2) =
let(
x = point1.x - point2.x,
y = point1.y - point2.y,
z = point1.z - point2.z
)
(x ^ 2 + y ^ 2 + z ^ 2) ^ 0.5;
echo(length_between([1, 2, 3], [3, 2, 1])); // ECHO: 2.82843
let
可以使用的地方很多,並不限於函式,像是 ?:
或者 list 表示式 等,若需要變數來代表計算結果,都可以適當地使用。
在 OpenSCAD 裡,[1, 2, 3, 4, 5]
可用來建立一個 list,可以透過索引來存取元素,索引由 0 開始;由於 list 也常用來表示點或向量,例如用 [10, 20, 30]
表示點座標,如果有個 p = [10, 20, 30]
,雖然可以使用 p[0]
、p[1]
、p[2]
來取得 x、y、z,不過 OpenSCAD 可以直接透過 p.x
、p.y
、p.z
這樣的 dot 表示法。
另一點要記得的是,if/else
在 OpenSCAD 是陳述句(statement),與 for
相同,使用它們意謂著有副作用;在 function
若想有 if/else
的效果,可以使用 ?:
三元運算,例如定義一個 max
函式:
function max(a, b) = a >= b ? a : b;
在 function
中要有巢狀的 if/else
效果,也是組合多個 ?:
運算。例如:
function compare(a, b) = a > b ? 1 : (a < b ? -1 : 0);
不過這樣有點難以閱讀,我個人都是排版為以下風格,比較容易閱讀:
function compare(a, b) =
a > b ? 1 :
a < b ? -1 :
0; // default value
需要重複性計算的演算怎麼辦?使用遞迴!
「遞迴只應天上有,凡人只能用迴圈」是嗎?其實只要任務單一,每次遞迴只專注當次子任務的分解,不管前後任務之狀態,遞迴並不困難!
方才的範例有看到 len
函式,可以用來計算文字的長度,假設現在沒有 len
函式可以用,可以自行定義,記得,只要看當次任務該解什麼就好了,那麼,計算文字長度的當次任務是什麼,不就是「看看有沒有字元,有就在計數上加 1」:
function length(lt, count = 0) =
is_undef(lt[count]) ? count : length(lt, count + 1);
echo(length("TEST")); // ECHO: 4
OpenSCAD 的字串可以透過索引來取得各個字元,就 length
的函式本體來說,就是將 count
當成索引,取 lt
中的字元,也就是「看看有沒有字元」,如果超出索引就是 undef
,雖然可以使用 ==
來比較 undef
,不過建議使用 is_undef
函式,比較具有可讀性,若不是 undef
就 count + 1
,也就是「有就在計數上加 1」,下次計數就是下一次呼叫 length
的事了。
echo 與 assert
在程式運行過程中,有時會想知道某些計算的值是否正確,可以使用 echo
模組來指定的值輸出,例如:
echo(1, 2, 3); // ECHO: 1, 2, 3
x = 10;
echo(x); // ECHO: 10
echo(str("x = ", x)); // ECHO: "x = 10"
echo(x = x); // ECHO: x = 10
第四個 echo
是個特別的用法,=
左邊會被當成文字顯示,=
右邊取 x
真正的值,這會比第三個 echo
先用 str
組裝字串後顯示的方式便利。
echo
也可以被安插在運算式或函式呼叫之前,這時會將指定的訊息顯示出來,運算式或函式呼叫的結果則直接傳遞,例如:
p1 = [10, 20];
p2 = [30, 40];
length = let(v = p2 - p1) echo(v) sqrt(v * v); // ECHO: v = [20, 20]
echo(length); // ECHO: 28.2843
如果 echo
後面沒有運算式或函式呼叫,那麼 echo
執行結果 undef
。
foo = echo("XD"); // ECHO: "XD"
echo(foo); // ECHO: undef
有時在程式執行過程中,你想斷言必須處於某種狀態,這時可以使用 assert
,例如:
// 經過一番運算…
foo = expr;
assert(foo != 0, "foo 必須不為 0");
assert
的第一個引數若為 true
,什麼事都不會發生,若為 false
就會發生 ERROR: Assertion 並中止程式,如果有指定第二個引數,就會在引發 ERROR: Assertion 時一併顯示。
echo
也可以被安插在運算式或函式呼叫之前,如果斷言成功,運算式或函式呼叫的結果直接傳遞,例如:
p1 = [10, 20];
p2 = [30, 40];
length = let(v = p2 - p1) assert(v != [0, 0]) sqrt(v * v);
echo(length); // ECHO: 28.2843
如果 assert
後面沒有運算式或函式呼叫,那麼 assert
執行結果 undef
。