0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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)

スクリーンショット 2026-08-11 165432.png

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)

スクリーンショット 2026-08-11 161412.png

Ex02. ブロックを移動する

import cadquery as cq

result = cq.Workplane("front").box(20, 20, 5)
result = result.translate((20, 30, 10))
show_object(result)

スクリーンショット 2026-08-11 170125.png

完成コード(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)

スクリーンショット 2026-08-11 170432.png

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)

スクリーンショット 2026-08-11 172324.png

完成コード(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 で一括くり抜きにしています

スクリーンショット 2026-08-11 172623.png

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)

スクリーンショット 2026-08-11 172931.png

完成コード(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 で一括くり抜き
    スクリーンショット 2026-08-11 173429.png

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)

スクリーンショット 2026-08-11 174252.png

完成コード(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)
    • 直径3の貫通穴(depth=None → 貫通)
    • 上面から直径4を深さ2で座ぐり
      の2形状を「同じ中心」で削る、と解釈してカッターを作っています
      スクリーンショット 2026-08-11 174546.png

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)

スクリーンショット 2026-08-11 175323.png

完成コード(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)

スクリーンショット 2026-08-11 182013.png

変換の対応関係

  • 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)

スクリーンショット 2026-08-11 181902.png

完成コード(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) に対応させるのが自然です(中心基準が一致)

スクリーンショット 2026-08-11 192004.png

おまけ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) 

スクリーンショット 2026-08-11 183300.png

### 2D平面のグリッド全体の「位置をズラす」方法
CadQueryの「自動的に中心を計算する仕様」と、「面のサイズ(面積)の違い」が組み合わさって起きた現象

  1. rarray(10, 10, 4, 4) のサイズ
    10mm間隔で4つ並ぶので、穴の集団の全体のサイズは 「30mm × 30mm」 になります。
  2. faces(">Y") (天面)のサイズ
    L字フックの上側の面は、幅(厚み)が 2mm、奥行きが 40mm しかない細長い面です。
  3. 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が出てくるのに、多少時間がかかります。

スクリーンショット 2026-08-11 190858.png

GridLocations(8, 10, 4, 4)rarrayと同じく原点中心の対称配置、Holedepth省略で貫通、edges("|Z")edges().filter_by(Axis.Z)が対応します。座標が合っているかは、CadQuery側とbuild123d側の両方でSTLを出して重ねるか、上記の穴中心リスト(y∈{6,14,22,30}など)と照合すれば検証できます。

教材作成に向けて

今回の「ずれ」は、実は教材設計の指針として貴重な発見だと思います。CadQueryのProjectedOrigin+center()累積は、書いた本人でも後から座標を追いにくい仕組みです。高校生向けサンプルでは、CadQuery側でもworkplane(centerOption="CenterOfBoundBox")を明示するか、いっそ最初からグローバル座標で穴位置を計算して見せる書き方に統一しておくと、生徒が座標を追え、将来build123dへ翻訳する際も機械的に対応が付きます。「暗黙の基準に頼らず、座標系を自分で宣言する」——これ自体がCADプログラミングの本質的な学習項目です。

0
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?