莫比烏斯帶

November 27, 2021

在〈多面體版本的 sweep〉中,曾經實現了莫比烏斯帶,它需要能建構多面體的 polyhedron 函式、位移與轉動矩陣,以及基於切面建立多面體的 sweep,這些 cqMore 都有提供。

其實〈多面體版本的 sweep〉中實現的莫比烏斯帶有點小問題,只是當時我故意不提,因為最後一個切面會旋轉 180 度,這時與第一個切面在索引順序上是對不上的,只不過因為切面是長方形,看不出來罷了,如果不是長方形會如何呢?

五角星莫比烏斯帶

cqmore.polyhedron 提供有 sweep 函式,cqmore.matrix 提供了矩陣處理相關函式,而 cqmore.polygon 提供了一些建立簡單多邊形的函式,例如 star,如果用它來做莫比烏斯帶會如何呢?

from cqmore import Workplane
from cqmore.matrix import translation, rotationX, rotationZ
from cqmore.polyhedron import sweep
from cqmore.polygon import star

def mobius_strip(radius, frags):
    star_r = radius / 2
    profile = [(p[0] * star_r, p[1] * star_r, 0) for p in star()]

    translationX20 = translation((radius, 0, 0))
    rotationX90 = rotationX(90)

    angle_step = 360 / frags
    profiles = []
    for i in range(frags + 1): # 這邊要加一
        m = rotationZ(i * angle_step) @ translationX20 @ rotationX90 @ rotationZ(i * angle_step / 2)
        profiles.append(m.transformAll(profile))

    return Workplane().polyhedron(*sweep(profiles))

radius = 20
frags = 24

strip = mobius_strip(radius, frags)

這會看到以下的結果:

莫比烏斯帶

很顯然地,第一個面與最後一個面沒有接合在一起,實際上,cqmore.polyhedronsweep 函式有個 closeIdx 參數,預設為 -1,表示不會將第一個切面與最後一個切面接合,若設為 0 的話就會接合,而且第一個切面與最後一個切面的頂點索引 0 是對應的:

from cqmore import Workplane
from cqmore.matrix import translation, rotationX, rotationZ
from cqmore.polyhedron import sweep
from cqmore.polygon import star

def mobius_strip(radius, frags):
    star_r = radius / 2
    # 轉為 3D 的點
    profile = [(p[0] * star_r, p[1] * star_r, 0) for p in star()]

    translationX20 = translation((radius, 0, 0))
    rotationX90 = rotationX(90)

    angle_step = 360 / frags
    profiles = []
    for i in range(frags): # 這邊不用加一了
        m = rotationZ(i * angle_step) @ translationX20 @ rotationX90 @ rotationZ(i * angle_step / 2)
        profiles.append(m.transformAll(profile))

    # closeIdx 設為 0 以上的值就會自動接合切面
    return Workplane().polyhedron(*sweep(profiles, closeIdx = 0))

radius = 20
frags = 24

strip = mobius_strip(radius, frags)

不過,對於正好旋轉 180 度的莫比烏斯帶,這就會出現扭轉的結果:

莫比烏斯帶

考慮到 sweep 時切面可能旋轉而接合,為此在接合第一個切面與最後一個切面時,可以調整 closeIdx 來位移第一個切面的索引 0,要與最後一個切面的第幾個頂點索引對應:

from cqmore import Workplane
from cqmore.matrix import translation, rotationX, rotationZ
from cqmore.polyhedron import sweep
from cqmore.polygon import star

def mobius_strip(radius, frags):
    star_r = radius / 2
    profile = [(p[0] * star_r, p[1] * star_r, 0) for p in star()]

    translationX20 = translation((radius, 0, 0))
    rotationX90 = rotationX(90)

    angle_step = 360 / frags
    profiles = []
    for i in range(frags):
        m = rotationZ(i * angle_step) @ translationX20 @ rotationX90 @ rotationZ(i * angle_step / 2)
        profiles.append(m.transformAll(profile))

    return Workplane().polyhedron(*sweep(profiles, closeIdx = 5))

radius = 20
frags = 24

strip = mobius_strip(radius, frags)

這樣就不會有扭轉的現象了:

莫比烏斯帶

六角星莫比烏斯帶

不過看來有不平順?這是因為以上使用了五角星,這一開始是想強調出轉動 180 度會有什麼問題,如果要平順的結果,比較好處理的是偶數的星芒:

from cqmore import Workplane
from cqmore.matrix import translation, rotationX, rotationZ
from cqmore.polyhedron import sweep
from cqmore.polygon import star

def mobius_strip(radius, frags):
    star_r = radius / 2
    # 六角星
    profile = [(p[0] * star_r, p[1] * star_r, 0) for p in star(n = 6)]

    translationX20 = translation((radius, 0, 0))
    rotationX90 = rotationX(90)

    angle_step = 360 / frags
    profiles = []
    for i in range(frags):
        m = rotationZ(i * angle_step) @ translationX20 @ rotationX90 @ rotationZ(i * angle_step / 2)
        profiles.append(m.transformAll(profile))

    return Workplane().polyhedron(*sweep(profiles, closeIdx = 6))

radius = 20
frags = 24

strip = mobius_strip(radius, frags)

這樣就沒有問題了:

莫比烏斯帶