在我的作品中有 Zentangle bracelet 以及 Generalized zentangle bracelet:
這兩個作品上的花紋,其實是使用 Symmetrical triangle generator 產生的:
要怎麼完成這樣的作品?手環是圓柱形,而原本的花紋設計是 2D 平面,基本上,是可以設計直接將線條畫在圓柱上,例如 Random maze cylinder generator 就是這麼做的,不過,Random maze cylinder generator 只需要垂直與水平兩種線就可以完成,這樣的設計方式還算不複雜,然而,上面的情況,線條必須有各個方向,雖然是可以設計的出來,不過數學與實作上都會複雜一些。
圓的組成
先別管上面的作品好了,我們來朝另一個方向想,先來看看,怎麼將字畫在圓柱上?最簡單的方式,就是將字投影到圓柱上。例如:
radius = 10;
height = 20;
thickness = 1;
$fn = 24;
render() intersection() {
difference() {
cylinder(
r1 = radius + thickness,
r2 = radius + thickness,
h = height, center = true);
cylinder(
r1 = radius,
r2 = radius,
h = height, center = true);
}
rotate([90, 0, 0]) linear_extrude(radius + thickness)
text("A", size = height, valign = "center", halign = "center");
}
這是一種方法沒錯,完成的結果如下:
不過,這個方式沒辦法完成以下的效果:
因為用投影的方式,文字只能投到半圓柱上,沒辦法做出跨 180 度以上的字繞圓柱效果。
回想一下 圓的組成 中談到的,圓實際上可以看成是由數個三角形圍繞而成,那麼圓柱呢?可以看成是數個三角形拉高後圍繞組成,如果將這些三角形攤平,並與 2D 圖案拉高後取交集:
這就會取得許多圓弧(其實是梯形),接著將這些圓弧繞起來成為一個圓:
那麼就又可以得到一個圓柱了,而圓柱上就會有原本 2D 平面的圖案,這樣是不是比要使用 3D 座標簡單呢!
程式的實作
在實作時,首先要決定的是,2D 圖案的範圍,也就是長與寬,為了方便,圖案定位方式如下:
因此,如果要將文字 A 繞成圓柱,我們把字這樣放:
linear_extrude(thickness)
translate([font_size / 2, font_size / 2, 0])
rotate(90)
text("A", size = font_size, valign = "center", halign = "center");
接著,我們必須有個模組,建立組成圓時必須使用的三角形,每個三角形是等腰三角形,兩個等腰的長其實就是圓半徑。
OpenSCAD 中,polygon
模組可以指定座標來建立多邊形,因此可以使用 polygon
模組來建立三角形;如果圓由 fn
個三角形組成,那麼兩個等腰的夾角就是 a = 360 / fn
,若兩個等腰形成的頂點為原點,那麼另兩個頂點的座標就是 [radius * cos(a / 2), radius * sin(a / 2)]
與 [radius * cos(a / 2), -radius * sin(a / 2)]
,因此,我們建立以下的 one_over_fn_for_circle
模組:
radius = 10;
$fn = 24;
module one_over_fn_for_circle(radius, fn) {
a = 360 / fn;
x = radius * cos(a / 2);
y = radius * sin(a / 2);
polygon(points=[[0, 0], [x, y],[x, -y]]);
}
one_over_fn_for_circle(radius, $fn);
接著,我們必須將指定的文字立起來:
然後用攤開的三角形拉高去取交集…
那麼,被攤開的圓,圓周長必須等於 2D 圖案的長 length
,因此,2 * PI * radius = length
,被攤開的圓,半徑就是 radius = length / (2 * PI)
,接著就只要跑迴圈,建立每個三角形,移至正確的位置,與文字取交集,再移回一開始建立三角形時的位置,轉個適當角度,將可以組合回原來的圓,因此,如果只是針對文字轉 2D 圖案,我們可以這麼實作:
tx = "A";
font_size = 20;
thickness = 1;
$fn = 24;
module one_over_fn_for_circle(radius, fn) {
a = 360 / fn;
x = radius * cos(a / 2);
y = radius * sin(a / 2);
polygon(points=[[0, 0], [x, y],[x, -y]]);
}
module text_to_cylinder(tx, font_size, thickness, fn) {
r = font_size / 6.28318;
a = 360 / fn;
y = r * sin(a / 2);
for(i = [0 : fn - 1]) {
// 將交集結果移動、轉回圓的一部份
rotate(a * i) translate([0, -(2 * y * i + y), 0])
// 取交集
intersection() {
// 將三角形攤開
translate([0, 2 * y * i + y, 0])
linear_extrude(font_size)
one_over_fn_for_circle(r, fn);
// 文字立起
translate([r - thickness, 0, font_size])
rotate([0, 90, 0])
// 文字
linear_extrude(thickness)
translate([font_size / 2, font_size / 2, 0])
rotate(90)
text(tx, size = font_size, valign = "center", halign = "center");
}
}
}
text_to_cylinder(tx, font_size, thickness, $fn);
這樣就可以得到以下的結果了:
從這張圖可以看到,圖案如果有較陡的斜線會有明顯的鋸齒,這可以加大 $fn
來改善,加大 $fn
就表示組成圓的三角形越多,而與圖案的交集就越多,就會需要比較長的運算時間,這是這個方法的缺點。
實際上,這個演算可適用於各種四邊形的 2D 圖案,而在 操作 children() 中看過,可以使用 children
來代表要操作的對象,因此,可以將上例修改為更通用,例如:
tx = "A";
font_size = 20;
thickness = 1;
fn = 24;
module one_over_fn_for_circle(radius, fn) {
a = 360 / fn;
x = radius * cos(a / 2);
y = radius * sin(a / 2);
polygon(points=[[0, 0], [x, y],[x, -y]]);
}
module square_to_cylinder(length, width, square_thickness, fn) {
r = length / 6.28318;
a = 360 / fn;
y = r * sin(a / 2);
for(i = [0 : fn - 1]) {
// 將交集結果移動、轉回圓的一部份
rotate(a * i) translate([0, -(2 * y * i + y), 0])
// 取交集
intersection() {
// 將三角形攤開
translate([0, 2 * y * i + y, 0])
linear_extrude(width)
one_over_fn_for_circle(r, fn);
// 圖案立起
translate([r - square_thickness, 0, width])
rotate([0, 90, 0])
children(0);
}
}
}
square_to_cylinder(font_size, font_size, thickness, fn)
linear_extrude(thickness)
translate([font_size / 2, font_size / 2, 0])
rotate(90)
text(tx, size = font_size, valign = "center", halign = "center");
上面的 square_to_cylinder
模組是通用的版本,只要指定一個 2D 圖案,並告知其長、寬、厚度與圓由幾個三角形組成,就會為建立一個圓柱,表面為指定的圖案了,因此,也可以使用 surface
讀取圖片來建立 2D 圖案,像 PNG to pen holder 就是這樣來的囉!