認識 BREP

November 25, 2021

在〈基本 2D 操作〉中,談到如何透過 circlerect 等來建立 2D 幾何資訊,如果需要更複雜的 2D 幾何資訊呢?在談到怎麼做之前,得先來認識一下 CadQuery 採用的幾個幾何術語與觀念。

BREP 術語

在 CadQuery 的官方文件〈3D BREP Topology Concepts〉談到,CadQuery 是以 OpenCascade 作為核心,而 OpenCascade 採用 BREP(Boundary Representation)來表示實體模型,也就是以點、線、面等作為基礎,其相互間的關係來描述模型。

相對地,OpenSCAD 主要是採用 CSG(Constructive Solid Geometry)來表示實體模型,也就是一個實體模型,可以表示為立方體、圓柱體、球點等基本物體布林操作後的結果,這讓 OpenSCAD 相對而言,初期進入的門檻很低,只不過 OpenSCAD 偏 Functional programming 典範,後期要做複雜模型時,相對就比較不容易了。

CadQuery 對 OpenCascade 作了封裝,然而使用 CadQuery 時,若能掌握幾個 BREP 的觀念,在使用上就會很有彈性:

  • 頂點(vertex):空間中的一個點。
  • 邊(edge):頂點間的連結。
  • 線(wire):一組依序相連的邊,通常是包圍著一個面。
  • 面(face):線圍成的區域。
  • 殼(shell):彼此連結的一組面。
  • 實體(solid):可以將一個空間封閉起來的殼。
  • 複合體(compound):一組不連續的實體組合。

BREP 與 CadQuery API

來透過畫正方形並擠出為立方體,瞭解一下以上的觀念,正方形的四個頂點是 (1, 1)、(-1, 1)、(-1, -1) 與 (1, -1),先移到第一個頂點:

import cadquery as cq

box = cq.Workplane().moveTo(1, 1)

Workplane 會使用 Vector 內部型態儲存頂點資訊,接著畫一條線到 (-1, 1):

box = cq.Workplane().moveTo(1, 1).lineTo(-1, 1)

現在兩個頂點構成了一個邊,Workplane 會使用 Edge 型態儲存邊的資訊,來繼續畫線:

box = (cq.Workplane()
			.moveTo(1, 1)
			.lineTo(-1, 1)
			.lineTo(-1, -1)
			.lineTo(1, -1)
		)

這會構成了三個獨立的邊,若要將這三個邊組成一個線,可以使用 wire 方法:

box = (cq.Workplane()
			.moveTo(1, 1)
			.lineTo(-1, 1)
			.lineTo(-1, -1)
			.lineTo(1, -1)
			.wire()
		)

Workplane 會使用 Wire 內部型態儲存線的資訊,你可以使用 extrude 來擠出 Wire,這會構成一個 3D 實體,不過就以上的例子來說,擠出後會看到破面。例如:

box = (cq.Workplane()
			.moveTo(1, 1)
			.lineTo(-1, 1)
			.lineTo(-1, -1)
			.lineTo(1, -1)
			.wire()
			.extrude(1)
		)

extrude 會透過 Wire 的資訊來計算出必要的面,然而不會檢查這些面構成的殼是否能封閉一個空間,3D 模型的破面就是,無法封閉一個空間的殼:

認識 BREP

就以上的程式而言,是因為你只有三個邊,這並沒有圍成一個封閉路徑,你可以明確地再增加一個邊連回 (1, 1),以構成封閉路徑:

box = (cq.Workplane()
			.moveTo(1, 1)
			.lineTo(-1, 1)
			.lineTo(-1, -1)
			.lineTo(1, -1)
			.lineTo(1, 1)   # 明確地再增加一個邊
			.wire()
			.extrude(1)
		)

或者呼叫 close(),表示就目前給予的頂點順序構成封閉路徑,這會自動將最後一個頂點連回第一個頂點:

box = (cq.Workplane()
			.moveTo(1, 1)
			.lineTo(-1, 1)
			.lineTo(-1, -1)
			.lineTo(1, -1)
			.close()        # 封閉路徑
			.wire()
			.extrude(1)
		)

這樣擠出後就不會有破面了:

認識 BREP

多邊形函式

如果你事先已經有一組頂點資訊了,更簡單的方式是透過 polyline 來建立邊,例如,來寫個 polygon 函式,可以生成多邊形:

import cadquery as cq

def polygon(points):
	return cq.Workplane().polyline(points).close()

# 建立正方形並擠出
box = polygon([(1, 1), (-1, 1), (-1, -1), (1, -1)]).extrude(1)

在〈基本 2D 操作〉中談到的 circlerect 等,會建立封閉路徑的 Wire,可以直接進行擠出,這邊特別要提及的是,世界上其實沒有完全的圓,circle 建立的圓是虛擬的,輸出為 STL 的話,可以看出是正多邊形,若沒有調整 exporters.exporttoleranceangularTolerance 的話,預設應該是 96 個邊左右,這在一般人的眼睛就覺得是個完美的圓了:

認識 BREP

對於一些模型,有時也不用使用到 96 邊的正多邊形作為圓,這時就會想要能自行調整邊數的圓:

from math import cos, sin, radians
import cadquery as cq

def polygon(points):
	return cq.Workplane().polyline(points).close()
	
def circle(r, fn):
	a_step = radians(360 / fn)
	points = [(r * cos(a_step * i), r * sin(a_step * i)) for i in range(fn)]
	return polygon(points)

plate = circle(10, 12).extrude(1)

執行後的結果如下:

認識 BREP

實際上,Workplane 本身就有個 polygon 方法,只不過目前只用來建立正多邊形,而這邊自行定義的 polygon 函式,可以用點指定任意的形狀。

CadQuery 還有一些高階的 2D 操作方法,有些會構成邊,有些會將邊組成線,API 文件中,基本上都會說明,只要掌握以下的觀念,搭配 API 文件,就能彈性地建立各種 2D 幾何資訊。