hull 折線

November 27, 2021

線本身是沒有寬度的,如果只是要在螢幕上畫線,這不成問題,只要看得出是一條線就可以了;然而,如果要 3D 列印的話,線就得有寬度,或者嚴格的說法是,圍著線建立起多邊形。

要圍著線建立起多邊形,其實是個稍微複雜點的問題,因為得處理轉角的問題,例如在我的 dotSCAD 程式庫,就實現了 polyline2d 函式,其中可以看到有三種轉角可以設定:

hull 折線

有興趣的話,你可以試著挑戰看看實現這種折線,然而就 3D 建模而言,大部份的情況下,並不會在意轉角的樣式,單純就只是要有個具寬度的線罷了,這時可以用另一種簡單的方式,也就是〈建立 Convex hull〉中談到的,在點與點間設定一個形狀,然後計算它們的凸包構成一條直線,將每個直線聯集後,就可以得到一條折線。

2D 折線

以下是個簡單的實作,polylineJoin2Djoin 接受一個內含 WireWorkplane,用來指定折點:

from scipy.spatial import ConvexHull
from cadquery import Workplane

# 用 Wire 來建立凸包
def hull2D(points):
    hull = ConvexHull(points)
    return (cq.Workplane()
              .polyline([points[i] for i in hull.vertices])
              .close()
              .val()
           )

# 將 Workplane 中的 Wire 頂點轉為 (x, y)
def toPoints(workplane):
    return [(v.X, v.Y) for v in workplane.vertices().vals()]

# 指定折點的 Wire 來建立折線
def polylineJoin2D(points, join): 
    # 在每個點放上一個 join
    joins = [join.translate(p) for p in points]
    
    # 建立點與點間的凸包
    lines = [
        hull2D(toPoints(joins[i]) + toPoints(joins[i + 1]))
        for i in range(len(joins) - 1)
    ]
    
    # 將全部的凸包擠出,目的是為了利用 `union` 方法來交集
    polyline = Workplane(lines[0]).toPending().extrude(1)
    for i in range(1, len(lines)):
        polyline = polyline.union(Workplane(lines[i]).toPending().extrude(1))
    
    # 取 XY 平面上的 Wire 就可以了
    return polyline.faces('<Z').wires().val()


points = [(0, 0), (10, 10), (0, 15), (-10, 10), (-10, 0)]

polyline = polylineJoin2D(points, Workplane().polygon(6, 1))
show_object(polyline)

這會建立以下的結果:

hull 折線

3D 折線

這個做法可以推廣,建立 3D 版本的折線:

from scipy.spatial import ConvexHull
from cadquery import Vector, Edge, Wire, Solid, Shell, Face, Workplane

# 〈實作 polyhedron〉的 polyhedron 函式
def polyhedron(points, faces):
    def _edges(vectors, face_indices):
        leng_vertices = len(face_indices)   
        return (
            Edge.makeLine(
                vectors[face_indices[i]], 
                vectors[face_indices[(i + 1) % leng_vertices]]
            ) 
            for i in range(leng_vertices)
        )

    vectors = [Vector(*p) for p in points]

    return Solid.makeSolid(
        Shell.makeShell(
            Face.makeFromWires(
                Wire.assembleEdges(
                    _edges(vectors, face_indices)
                )
            )
            for face_indices in faces
        )
    )
    
# 將 Workplane 中的 Solid 頂點轉為 (x, y, z)
def toPoints(workplane):
    return [(v.X, v.Y, v.Z) for v in workplane.vertices().vals()]

# 建立 3D 版本的凸包
def hull3D(points):
    hull = ConvexHull(points)
    # 凸包上的頂點
    vertices = [points[i] for i in hull.vertices]

    # 用來查詢頂點的索引值
    v_i_lookup = {v: i for i, v in enumerate(vertices)}

    # 建立面索引
    faces = [
        [v_i_lookup[points[i]] for i in face]
        for face in hull.simplices
    ]

    return polyhedron(vertices, faces)

def polylineJoin3D(points, join):    
    # 在每個點放上一個 join
    joins = [join.translate(p) for p in points]
    
    # 建立點與點間的凸包
    lines = [
        hull3D(toPoints(joins[i]) + toPoints(joins[i + 1]))
        for i in range(len(joins) - 1)
    ]
    
    # 將全部的凸包交集
    polyline = Workplane(lines[0])
    for i in range(1, len(lines)):
        polyline = polyline.union(Workplane(lines[i]))
    
    return polyline


points = [(0, 0, 0), (10, 0, 0), (10, 0, 10), (10, 10, 10)]
polyline = polylineJoin3D(points, Workplane().box(1, 1, 1))
show_object(polyline)

這會建立以下的結果:

hull 折線

分享到 LinkedIn 分享到 Facebook 分享到 Twitter