Pythonで3次元CADを扱う
マウスを使ってクリックして、形状作成するのではなく、コードを書いて、形状を作成していくCADです。プレビューしながら、修正していくことができます。動かしてどうなるか検証するのが楽しいところです。
今回は、Qiitaの6年前のCadQueryの紹介記事を参考に、PythonSCADに変換してみました。
簡単に変換できるかと、試してみたところ、CadQueryのメジャーバージョンアップ(仕様変更)による影響もあるようで、調整しました。
さまざまなサンプルがありますが、CadQueryのドキュメントのExamplesも、大変参考になります。build123dのExamplesもほぼ同じ内容ですので、参考にしてみてください。
以下は、Windows11のPythonSCADと、CQ-editor.exeを使って確認しています。パソコン教室での利用検証は今後になりますが、さまざまな環境でためしてみたいところです。
開発コミュニティ、開発者、紹介記事の皆様、感謝申し上げます。ありがとうございます。
CadQueryからPythonSCADへの変換ルール
以下を変換規則として、コードを記述しました。
物体の位置や向きが同じように調整しましたが、まだまだ検証しないといけないかもしれません。
また、PythonSCADのピリオドでつなぐ「メソッドチェーン」を使って、書き直してみても良いと思います。
図形を作る時、「メソッドチェーン」という書き方を使うと、処理の手順が左から右へスラスラ読めて、手順がとても分かりやすくなります。いろいろ検証してみてください。
### CadQueryからPythonSCADへの座標系・Workplane変換ルール
CadQueryはZ-up座標系ですが、Workplaneごとに2Dスケッチのローカル座標(X, Y)と、押し出し方向(法線)が異なります。
PythonSCADに変換する際は、まずXY平面で `linear_extrude` (標準は+Z方向へ押し出し) を行った後、以下のルールに沿って `rotate` を適用し、3D空間に正しく配置すること。
1. **Workplane("front") または "XY" の場合:**
- 回転: なし (`rotate(..., [0, 0, 0])`)
- 結果: 2Dの(X,Y)はグローバルの(X,Y)となり、押し出しは グローバル +Z 方向となる。
2. **Workplane("top") または "XZ" の場合:**
- 回転: X軸回りに -90度 (`rotate(..., [-90, 0, 0])`)
- 結果: 2Dの(X,Y)はグローバルの(X,Z)となり、押し出しは グローバル -Y 方向となる。
3. **Workplane("right") または "YZ" の場合:**
- 回転: Y軸回りに 90度、 (`rotate(..., [0, 90, 0])`)
- 結果: 2Dの(X,Y)はグローバルの(Y,Z)となり、押し出しは グローバル +X 方向となる。
※ 押し出し: CadQueryの `extrude(d)` は法線方向への押し出しである。`extrude(d, both=True)` の場合は、PythonSCAD側で `center=True` の設定や、厚みの半分を移動させる `translate` を併用して再現すること。
Ex01. x:20, y:20, z:5mmのブロックを作成
import cadquery as cq
result = cq.Workplane("front").box(20, 20, 5)
show_object(result)
PythonSCADコード
変換方針
CadQuery:
result = cq.Workplane("front").box(20, 20, 5)
- 2D→3Dの押し出し方向は グローバル +Z
- 回転は なし(
rotate(..., [0, 0, 0])相当) -
box(20, 20, 5)はデフォルトで中心配置(centered=True相当)なので、PythonSCADでもcenter=Trueにします
完成コード(PythonSCAD)
# ==========================================
# CadQuery -> PythonSCAD 変換: front.box(20,20,5)
# 作成日: 2026/08/11
# ==========================================
from pythonscad import *
# --- パラメータ設定 (単位: mm) ---
width_x = 20.0 # X方向サイズ
depth_y = 20.0 # Y方向サイズ
height_z = 5.0 # Z方向サイズ
# --- メイン処理 ---
result = cube([width_x, depth_y, height_z], center=True)
show(result)
Ex02. ブロックを移動する
import cadquery as cq
result = cq.Workplane("front").box(20, 20, 5)
result = result.translate((20, 30, 10))
show_object(result)
完成コード(PythonSCAD)
# ==========================================
# CadQuery -> PythonSCAD 変換例
# 作成日: 2026/08/11
# ==========================================
from pythonscad import *
# --- パラメータ設定 (単位: mm) ---
box_x = 20.0
box_y = 20.0
box_z = 5.0
tx = 20
ty = 30
tz = 10
# --- CadQuery ---
# result = cq.Workplane("front").box(20, 20, 5)
# result = result.translate((20, 30, 10))
# --- PythonSCAD 変換 ---
# Workplane("front") はルールにより「回転なし」(=XYで作って+Zへ押し出し/配置と整合) として扱う
body = cube([box_x, box_y, box_z], center=True)
# CadQuery の translate((x, y, z)) -> PythonSCAD の translate(obj, [x, y, z])
result = translate(body, [tx, ty, tz])
show(result)
Ex03. ブロックに穴をあける
import cadquery as cq
length = 80.0
height = 60.0
thickness = 10.0
center_hole_dia = 22.0
result = (cq.Workplane("XY").box(length, height, thickness)
.faces(">Z").workplane().hole(center_hole_dia))
show_object(result)
完成コード(PythonSCAD)
# ==========================================
# CadQuery変換: 直方体 + 上面からの貫通穴
# 作成日: 2026/08/11
# ==========================================
from pythonscad import *
# --- パラメータ設定 (単位: mm) ---
length = 80.0 # X方向
height = 60.0 # Y方向
thickness = 10.0 # Z方向
center_hole_dia = 22.0
fn_val = 80 # 円の滑らかさ
eps = 0.1 # チラつき防止用微小値
# --- ヘルパー ---
def r_from_dia(d):
return d / 2.0
# --- 関数(モジュール)定義 ---
def create_body(lx, ly, lz):
"""CadQuery: Workplane('XY').box(lx, ly, lz)
-> PythonSCAD: 原点中心の直方体(center=True)
"""
return cube([lx, ly, lz], center=True)
def create_cutter(lz, hole_d, fn):
"""CadQuery: faces('>Z').workplane().hole(d)
-> 上面からの穴だが、結果はZ方向の貫通穴なので center=True 円柱で一括再現
"""
hole = cylinder(h=lz + 2 * eps, r=r_from_dia(hole_d), center=True, fn=fn)
return hole
# --- メイン処理 ---
body = create_body(length, height, thickness)
cutter = create_cutter(thickness, center_hole_dia, fn_val)
result = body - cutter
show(result)
変換メモ(座標系・Workplane)
-
hole(center_hole_dia)は、上面(>Z)から作る操作ですが、最終的には「Z方向の貫通穴」になるため、PythonSCADでは Z軸の円柱カッターをbody - cutterで一括くり抜きにしています
Ex04. ブロックに穴をあける(座標指定)
中心に対し(10, 20)ずらした、直径16の穴を直方体にあける。
import cadquery as cq
length = 80.0
height = 60.0
thickness = 10.0
center_hole_dia = 16.0
result = cq.Workplane("XY").box(length, height, thickness)
result = result.faces(">Z").workplane().center(10, 20).hole(center_hole_dia)
show_object(result)
完成コード(PythonSCAD)
# ==========================================
# CadQuery -> PythonSCAD 変換例:Box + 上面基準の穴あけ
# 作成日: 2026/08/11
# ==========================================
from pythonscad import *
# --- パラメータ設定 (単位: mm) ---
length = 80.0 # X方向
height = 60.0 # Y方向
thickness = 10.0 # Z方向
center_hole_dia = 16.0
hole_offset_x = 10.0 # CadQuery: center(10, 20)
hole_offset_y = 20.0
fn_val = 60
eps = 0.1
# --- 関数(モジュール)定義 ---
def create_body(l, h, t):
"""本体(足す形状): CadQuery box(length, height, thickness) を center=True で再現"""
return cube([l, h, t], center=True)
def create_cutter(t, hole_d, ox, oy, fn):
"""カッター(引く形状): 上面workplaneの center(ox, oy) に穴を配置し、貫通させる"""
hole_r = hole_d / 2.0
hole = cylinder(h=t + 2 * eps, r=hole_r, center=True, fn=fn)
hole = translate(hole, [ox, oy, 0.0])
return hole
# --- メイン処理 ---
body = create_body(length, height, thickness)
cutter = create_cutter(thickness, center_hole_dia, hole_offset_x, hole_offset_y, fn_val)
result = body - cutter
show(result)
変換の対応関係
-
box(length, height, thickness)→cube([length, height, thickness], center=True) -
faces(">Z").workplane().center(10, 20).hole(d)
→ 穴軸はZ方向のまま、穴中心を グローバルXYで (10, 20) に平行移動して、body - cutterで一括くり抜き
Ex05. ブロックにネジ穴をあける
import cadquery as cq
length = 80.0
height = 60.0
thickness = 10.0
result = (cq.Workplane(cq.Plane.XY()).box(length, height, thickness))
result = result.faces(">Z").workplane().rect(70.0, 50.0, forConstruction=True).vertices().cboreHole(3.0, 4.0, 2.0, depth=None)
show_object(result)
完成コード(PythonSCAD)
# ==========================================
# CadQuery変換: 80x60x10 ブロック + 四隅の座ぐり付き貫通穴
# 作成日: 2026/08/11
# ==========================================
from pythonscad import *
# --- パラメータ設定 (単位: mm) ---
length = 80.0 # X方向
width = 60.0 # Y方向(CadQueryの height に相当)
thickness = 10.0 # Z方向(CadQueryの thickness)
rect_x = 70.0 # 70x50 の矩形頂点位置(穴位置決め)
rect_y = 50.0
hole_d = 3.0 # 貫通穴 直径
cbore_d = 4.0 # 座ぐり 直径
cbore_dep = 2.0 # 座ぐり 深さ(上面から下向き)
fn_val = 60
eps = 0.1 # チラつき防止&貫通保証
# --- 関数定義 ---
def create_body(lx, ly, lz):
"""本体(足す形状)"""
return cube([lx, ly, lz], center=True)
def create_cutter(lx, ly, lz, rx, ry, hole_d, cbore_d, cbore_dep, fn):
"""カッター(引く形状): 4箇所の貫通穴 + 上面からの座ぐり"""
x = rx / 2.0
y = ry / 2.0
hole_r = hole_d / 2.0
cbore_r = cbore_d / 2.0
# 4つの頂点(±x, ±y)
points = [
[+x, +y],
[+x, -y],
[-x, +y],
[-x, -y],
]
cutter = None
for px, py in points:
# 貫通穴(Z方向に貫通): center=True でブロック中心に合わせる
thru = cylinder(h=lz + 2 * eps, r=hole_r, center=True, fn=fn)
thru = translate(thru, [px, py, 0.0])
# 座ぐり(上面 >Z から深さ cbore_dep だけ掘る)
# 上面Z = +lz/2 なので、座ぐり円柱は
# z = (lz/2 - cbore_dep - eps) から (lz/2 + eps) まで届くようにする
cbore = cylinder(h=cbore_dep + 2 * eps, r=cbore_r, center=False, fn=fn)
cbore = translate(cbore, [px, py, (lz / 2.0) - cbore_dep - eps])
one = thru + cbore
cutter = one if cutter is None else (cutter + one)
return cutter
# --- メイン処理 ---
body = create_body(length, width, thickness)
cutter = create_cutter(
length, width, thickness,
rect_x, rect_y,
hole_d, cbore_d, cbore_dep,
fn_val
)
result = body - cutter
show(result)
変換のポイント(CadQuery → PythonSCAD)
-
faces(">Z").workplane()は「上面を基準に加工する」意味なので、PythonSCADでは 上面Z座標+thickness/2を使って座ぐり位置を決める、という置き換えになります -
cboreHole(3.0, 4.0, 2.0, depth=None)は
Ex06. ブロックをunionする
import cadquery as cq
r1 = cq.Workplane("front").box(20, 20, 5)
r1 = r1.translate((10, 10, 0))
r2 = cq.Workplane("front").box(20, 20, 5)
r2 = r2.translate((0, 0, 0))
r1 = r1.union(r2)
show_object(r1)
完成コード(PythonSCAD)
# ==========================================
# CadQuery -> PythonSCAD 変換例(2つの箱を結合)
# 作成日: 2026/08/11
# ==========================================
from pythonscad import *
# --- パラメータ設定 (単位: mm) ---
box_x = 20.0 # X方向サイズ
box_y = 20.0 # Y方向サイズ
box_z = 5.0 # Z方向サイズ
r1_tx = 10.0 # r1 のX移動
r1_ty = 10.0 # r1 のY移動
r1_tz = 0.0 # r1 のZ移動
r2_tx = 0.0 # r2 のX移動
r2_ty = 0.0 # r2 のY移動
r2_tz = 0.0 # r2 のZ移動
# --- CadQuery Workplane("front") / "XY" ルール ---
# 回転なし:2D(X,Y)がグローバル(X,Y)、押し出し相当もグローバル+Z
# ※今回の box は最初から3Dなので rotate は不要
# --- メイン処理 ---
box_shape = cube([box_x, box_y, box_z], center=True)
r1 = translate(box_shape, [r1_tx, r1_ty, r1_tz])
r2 = translate(box_shape, [r2_tx, r2_ty, r2_tz])
# CadQuery の union -> PythonSCAD は + 演算子で結合
result = r1 + r2
show(result)
変換の対応関係
-
box(20, 20, 5):cube([20, 20, 5], center=True) -
translate((10, 10, 0)):translate(obj, [10, 10, 0])(※PythonSCADはリストで指定) -
union:r1 + r2(※関数union()は使わない)
Ex07. ブロックををカット(difference)する
import cadquery as cq
r1 = cq.Workplane("front").box(20, 20, 5)
r1 = r1.translate((10, 10, 0))
r2 = cq.Workplane("front").box(20, 20, 5)
r2 = r2.translate((0, 0, 0))
r1 = r1.cut(r2)
show_object(r1)
完成コード(PythonSCAD)
# ==========================================
# CadQuery r1.cut(r2) 変換(front=XY扱い)
# 作成日: 2026/08/11
# ==========================================
from pythonscad import *
# --- パラメータ設定 (単位: mm) ---
box_x = 20.0
box_y = 20.0
box_z = 5.0
r1_tx = 10.0
r1_ty = 10.0
r1_tz = 0.0
eps = 0.1 # 今回は面が完全一致しないので必須ではないが、習慣として置く
# --- メイン処理 ---
# CadQuery:
# r1 = Workplane("front").box(20, 20, 5).translate((10, 10, 0))
# r2 = Workplane("front").box(20, 20, 5).translate((0, 0, 0))
#
# 変換ルールより Workplane("front") は PythonSCAD のXY基準とみなし回転なし。
r1 = cube([box_x, box_y, box_z], center=True)
r1 = translate(r1, [r1_tx, r1_ty, r1_tz])
r2 = cube([box_x, box_y, box_z], center=True)
r2 = translate(r2, [0.0, 0.0, 0.0])
result = r1 - r2
show(result)
変換のポイント
- CadQuery の
cut()は PythonSCAD ではr1 - r2(差集合, difference)で表現します -
box(20,20,5)は PythonSCAD のcube([20,20,5], center=True)に対応させるのが自然です(中心基準が一致)
おまけ08. CadQueryとbuild123dを試す
冒頭の紹介記事のtest.pyを現行バージョンで動くように変更しました。形状はちょっと違います。ありがとうございます。
タッキング穴付きフックの作成
1. CadQuery版
import cadquery as cq
# パラメータ
X1, X2, X3 = 40.0, 2.0, 2.0
Y1, Y2, Y3 = 40.0, 2.0, 10.0
Z1 = 40.0
HOLE = 5.0
# 1. フック本体
hook = (
cq.Workplane("XY")
.moveTo(0,0)
.lineTo(X1,0)
.lineTo(X1,Y1)
.lineTo(X1-X2,Y1)
.lineTo(X1-X2,Y2)
.lineTo(X3,Y2)
.lineTo(X3,Y3)
.lineTo(0,Y3)
.close()
.extrude(Z1)
)
# 2. X面とY面に連続して穴を開ける(メソッドチェーン)
result = (
# --- まず右側面(>X)に穴を開ける ---
hook.faces(">X")
.workplane()
.center(18, 20)
.rarray(8, 10, 4, 4)
.hole(HOLE)
# --- 続けて天面(>Y)を選び直し、穴を開ける ---
.faces(">Y")
.workplane()
.center(20, 0)
.rarray(8, 10, 4, 4)
.hole(HOLE)
)
result = result.edges("|Z").fillet(0.25)
show_object(result)
### 2D平面のグリッド全体の「位置をズラす」方法
CadQueryの「自動的に中心を計算する仕様」と、「面のサイズ(面積)の違い」が組み合わさって起きた現象
-
rarray(10, 10, 4, 4)のサイズ
10mm間隔で4つ並ぶので、穴の集団の全体のサイズは 「30mm × 30mm」 になります。 -
faces(">Y")(天面)のサイズ
L字フックの上側の面は、幅(厚み)が 2mm、奥行きが 40mm しかない細長い面です。 -
CadQueryの自動センタリング
.workplane()を実行すると、CadQueryは「その面のど真ん中」を新しい原点(0,0)に設定し、そこに 30x30mm の穴の集団を配置しようとします。
幅2mmしかない細いフチのど真ん中に、30x30mmの巨大な陣形を無理やり置いたため、穴の大部分が面からはみ出してしまい、フックの内側(下側の空っぽの空間)に空振りしてドリルを打っている状態になっていました。
rarray は常に「面のど真ん中」に配置されますが、デザインの都合で「全体をもう少し上(または右)にズラしたい」という場合は、rarray の直前に .center(Xズレ量, Yズレ量) を挟みます。
2. build123d版 (AIからの回答抜粋)
CadQueryとbuild123dでは「面から作った作業平面」の原点と軸の決め方が別物なので、.center(18, 20)のような数値をそのまま移すと必ずずれます。
CadQueryのfaces(">X").workplane()は、デフォルト(centerOption="ProjectedOrigin")で直前の原点をその面に投影した点を新しい原点にします。面の中心ではありません。さらにcenter()の移動はチェーン内で累積します。このコードを実際に追うと:
- 1つ目: 原点(0,0,0)をx=40面に投影→(40,0,0)、
center(18,20)で**(40,18,20)**。ローカル軸はCadQuery独自の規則(グローバルZ×法線)でx=+Y、y=+Z。よって穴の中心は (40, y, z)、y∈{6,14,22,30}、z∈{5,15,25,35} - 2つ目: 直前の原点(40,18,20)がy=40面に投影されて(40,40,20)、法線+Yに対するローカルxは**(-1,0,0)なので
center(20,0)で(20,40,20)**。穴は底板を上から貫通し、x∈{8,16,24,32}、z∈{5,15,25,35}
つまりcenter(20,0)の「20」は、1つ目の穴あけの原点移動が持ち越された上での相対値です。一方build123dのPlane(face)は原点を面の中心に置き、x_dirは面の内部パラメータ(OCCT任せ)から取るため、同じ数値を渡しても基準も向きも一致しません。ずれるのは当然なのです。
対処: Planeをグローバル座標で明示する
変換時は面から平面を導出せず、上で計算した実座標でPlane(origin=…, x_dir=…, z_dir=…)を自作するのが確実です。
from build123d import *
X1, X2, X3 = 40.0, 2.0, 2.0
Y1, Y2, Y3 = 40.0, 2.0, 10.0
Z1 = 40.0
HOLE = 5.0
with BuildPart() as bp:
# 1. フック本体
with BuildSketch(Plane.XY):
with BuildLine():
Polyline(
(0, 0), (X1, 0), (X1, Y1), (X1 - X2, Y1),
(X1 - X2, Y2), (X3, Y2), (X3, Y3), (0, Y3),
close=True,
)
make_face()
extrude(amount=Z1)
# 2a. x=40 面の 4x4 穴(CadQuery実座標: 原点(40,18,20)、面内x=+Y、y=+Z)
with Locations(Plane(origin=(X1, 18, 20), x_dir=(0, 1, 0), z_dir=(1, 0, 0))):
with GridLocations(8, 10, 4, 4):
Hole(radius=HOLE / 2)
# 2b. 底板への貫通穴(CadQuery実座標: 原点(20,40,20)、-Y方向に貫通)
with Locations(Plane(origin=(20, Y1, 20), x_dir=(1, 0, 0), z_dir=(0, 1, 0))):
with GridLocations(8, 10, 4, 4):
Hole(radius=HOLE / 2)
# 3. Z平行エッジのフィレット
fillet(bp.edges().filter_by(Axis.Z), radius=0.25)
show_object(bp.part)
CQ-Editor 0.30-dev-8を使いました。CQ-editor-Windows.zip(384MB)
解凍後のフォルダ容量は、約1.15 GBになります。
preview画面をマウスを使って、まわしにくいです。
previewが出てくるのに、多少時間がかかります。
GridLocations(8, 10, 4, 4)はrarrayと同じく原点中心の対称配置、Holeはdepth省略で貫通、edges("|Z")はedges().filter_by(Axis.Z)が対応します。座標が合っているかは、CadQuery側とbuild123d側の両方でSTLを出して重ねるか、上記の穴中心リスト(y∈{6,14,22,30}など)と照合すれば検証できます。
教材作成に向けて
今回の「ずれ」は、実は教材設計の指針として貴重な発見だと思います。CadQueryのProjectedOrigin+center()累積は、書いた本人でも後から座標を追いにくい仕組みです。高校生向けサンプルでは、CadQuery側でもworkplane(centerOption="CenterOfBoundBox")を明示するか、いっそ最初からグローバル座標で穴位置を計算して見せる書き方に統一しておくと、生徒が座標を追え、将来build123dへ翻訳する際も機械的に対応が付きます。「暗黙の基準に頼らず、座標系を自分で宣言する」——これ自体がCADプログラミングの本質的な学習項目です。














