ここからはじまり、PythonSCADコミュニティ・開発者に感謝申し上げます。
以下、改訂・追記しました。ありがとうございます。
はじめに
PythonSCAD(OpenSCAD の Python 対応版)に同梱されている Python に build123d を pip で導入し、build123d の形状を PythonSCAD の GUI 上でそのまま表示できるようにしました。
build123d 用の表示環境としては cq-editor や VS Code の OCP CAD Viewer がありますが、どちらも起動に時間がかかります。PythonSCAD は起動が速く、F5 プレビューも軽いので、build123d のコードを書いては見て直す、という学習サイクルに向いています。
本記事では、導入時に踏んだ Windows のセキュリティブロック、名前空間の衝突、そして最終的に落ち着いた橋渡しモジュール pybuild123d.py をまとめます。
検証環境:
- Windows 11(個人端末)
- PythonSCAD 1.1.2(Windows x86-64、ポータブル版)
- 同梱 Python 3.14.6
- build123d(pip で導入)
1. 同梱 Python に build123d を入れる
PythonSCAD の展開フォルダにある python.exe には pip が入っていないので、まず get-pip.py で pip を入れ、その Python 経由で build123d を入れます。
cd C:\Users\<user>\Documents\PythonSCAD-1.1.2-windows-x86-64
# https://bootstrap.pypa.io/get-pip.py をこのフォルダにダウンロードしておく
.\python.exe get-pip.py
.\python.exe -m pip install build123d
Lib\site-packages 配下に build123d と、その依存である OCP(OpenCASCADE の Python バインディング)が入ります。OCP の wheel は 100MB を超えるので、回線によっては時間がかかります。
pip の実行は pip.exe ではなく python.exe -m pip の形にしておくと、Scripts フォルダを PATH に通さなくても確実に同梱 Python の site-packages に入ります。「Scripts が PATH にありません」という警告が出ても無害です。生徒用 PC で環境変数を触らずに済むので、教室展開ではこちらを推奨します。
なお、python -m ensurepip は embeddable package ベースの配布では ensurepip モジュール自体が同梱されていないことがあります。.\python.exe -m ensurepip --version がエラーになる場合は get-pip.py を使ってください。
2. 「アプリケーション制御ポリシーによってブロック」される
いざ import すると、こう落ちました。
>>> from build123d import *
...
File "...\Lib\site-packages\OCP\__init__.py", line 12, in <module>
from OCP.OCP import *
ImportError: DLL load failed while importing OCP:
アプリケーション制御ポリシーによってこのファイルがブロックされました。
Unblock-File をフォルダ全体に再帰でかけても効きません。
原因は Zone.Identifier ではなく Smart App Control
Unblock-File が消すのは「インターネットからダウンロードした」印(Zone.Identifier)だけです。今回のメッセージは Windows のコード整合性ポリシー(Smart App Control / WDAC / AppLocker)が、署名のない DLL の読み込みを拒否したときに出るものです。OCP.cp314-win_amd64.pyd は実体が DLL で、pip の wheel に入っているものにはコード署名がありません。
python.exe や PythonSCAD 本体が動くのは、署名済みか、Microsoft の評判サービスで既知と判定されているためと考えられます(Get-AuthenticodeSignature .\pythonscad.exe で署名の有無を確認できます)。なお、初回起動時に出る「Windows によって PC が保護されました」のマゼンタ色の画面は SmartScreen で、「詳細情報 → 実行」で突破できます。Smart App Control のブロックには突破ボタンがなく、別の仕組みです。
どのポリシーかを確認する
PowerShell(管理者)で次を実行します。
Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy" | Select VerifiedAndReputablePolicyState
Get-AppLockerPolicy -Effective | Select -Expand RuleCollections
-
VerifiedAndReputablePolicyStateが1→ Smart App Control が有効(2は評価モード、0は無効) - AppLocker に Dll ルールが並ぶ → 組織のポリシー
私の環境では 1 でした。ブロックされたファイルの実名は、イベントビューアーの「アプリケーションとサービス ログ > Microsoft > Windows > CodeIntegrity > Operational」(イベント ID 3077)で確認できます。
対処
個人端末なら、Windows セキュリティ → アプリとブラウザー コントロール → スマート アプリ コントロール → オフ で解決します。一度オフにすると Windows を再インストールするまで再有効化できませんが、ユーザー側から個別の DLL を許可する手段はなく、自己署名も通らないため、開発機ではこれしかありません。
学校などの管理端末で AppLocker / WDAC が入っている場合、ユーザー側では回避できません。管理者にパス規則かハッシュ規則の追加を依頼することになります。相談する際は、上記イベントログに残るファイルパスとポリシー名を添えると話が早いです。
なお、Documents 配下にポータブル環境を置く方式は「管理者権限なし」や Zone.Identifier は回避できますが、DLL ルール付きのポリシーの前では、pip で入れた未署名のネイティブ拡張(OCP に限らず numpy なども)が同じ理由で落ちます。展開先の PC で先に上記コマンドを走らせておくと、事前に判定できます。
3. from pythonscad import * が build123d とぶつかる
ブロックが解けた後、build123d のコードで謎のエラーが出ました。
sel = edges().filter_by(Axis.Z, reverse=True)...
~~~~~^^
TypeError: error during parsing
error during parsing は PythonSCAD 側の C++ バインディングが引数を解釈できなかったときのメッセージです。つまり呼ばれた edges() は build123d のものではなく PythonSCAD のものでした。
両モジュールに共通する名前を確認すると:
import build123d, pythonscad
print(sorted(set(dir(build123d)) & set(dir(pythonscad))))
['edge', 'edges', 'faces', 'fillet', 'mirror', 'offset', 'scale', 'text', 'version']
edges faces fillet mirror offset scale text と、build123d でよく使う名前が軒並み重なっています。from ... import * は後に書いたほうが勝つので、from pythonscad import * を build123d の後に置くと、これらが全部 PythonSCAD 側に上書きされます。
結論: PythonSCAD 側は * で読み込まず、必要な名前だけ個別に import する。 実際に必要なのは polyhedron と show くらいです。あるいは、次節の同梱ライブラリのように「build123d を後に import して勝たせる」という順序で回避する方法もあります。
4. 同梱の pybuild123d.py
ここまで進めたところで、PythonSCAD の開発者から「PythonSCAD には pybuild123d ライブラリが同梱されている」とコメントをもらいました。libraries\python\pybuild123d.py にあり、中身は次のとおりです。
from pythonscad import *
from build123d import *
from functools import wraps
def build123d(obj):
@wraps(obj)
def wrapTheFunction(*args, **kwargs):
result = obj(*args, **kwargs)
tsl = result.part.tessellate(tolerance=.001)
points = [list(vector) for vector in tsl[0]]
faces = [list(face[::-1]) for face in tsl[1]]
return polyhedron(points, faces)
return wrapTheFunction
使い方は、モデルを作る関数に @build123d を付けます。
from pythonscad import *
from pybuild123d import * # ← 必ず最後に import する
@build123d
def b123dpart():
with BuildPart() as p:
with BuildSketch() as sketch:
rect = Rectangle(width=100, height=50)
offset(rect, -2, mode=Mode.SUBTRACT)
extrude(amount=10)
return p
p1 = b123dpart() # build123d 製 → PythonSCAD オブジェクトになっている
p2 = sphere(3) # PythonSCAD 製
show(p1 | p2) # 両者をそのまま合成できる
同梱版から分かること
import の順序が衝突対策になっている
from pythonscad import * の後に from build123d import * を書いているので、モジュール内では build123d の名前が勝ち、from pybuild123d import * で外に出るのも build123d 版の edges や offset です。生徒側でも「pybuild123d を一番最後に import する」を守れば前節の衝突は起きません。順番を変えると壊れます。
変換後は PythonSCAD の演算が使える
デコレータを通った p1 は PythonSCAD のオブジェクトなので、sphere(3) と | で合成できます。build123d で作った部品を OpenSCAD 流の CSG に持ち込める、というのは教材として面白い性質です。
制約
-
result.part決め打ちなので、関数がBuildPartの Builder を返す前提です。代数モードのSolidやBuildSketchを返すとAttributeErrorになります。2D や線は表示できません -
tolerance=.001固定でangular_toleranceの指定がなく、大きな曲面では三角形数が増えます -
デコレータ方式なので、公式サンプルをコピーしたあと本体全体を関数の中に1段インデントし直す必要があります。このとき
extrude(amount=10)がBuildSketchブロックの中に紛れ込むと、RuntimeError: (extrude doesn't apply to ['BuildPart'])というエラーになります。分かりにくい文面ですが、意味は「extrude は BuildPart 用だが、今は別の Builder の中にいる」です。build123d の Builder モードは
withの入れ子で文脈を判定するので、インデントのずれがそのままエラーになります。
5. 拡張モジュール b123dview.py
同梱版を上位互換する形で、次の点を足したモジュールを別名で libraries\python\ に置きました。同梱版と同じファイル名にすると PythonSCAD の更新で元に戻されるので、別名にしています。
-
show()に build123d のオブジェクトを直接渡せる(インデントし直し不要) - Builder(
p)と結果(p.part)のどちらでも受ける - 2D スケッチと線も表示できる
- 分割精度を引数で調整できる
- 同梱版互換の
@build123dデコレータも残す
build123d の Shape.tessellate() は三角形メッシュ(頂点リスト、三角形インデックス)を返すので、それを PythonSCAD の polyhedron() に渡せば表示できます。
面の向きに注意が必要で、OCCT のテッセレーションは外向き法線に対して反時計回り、OpenSCAD の polyhedron は外から見て時計回りを要求します。インデックスを [::-1] で反転しないと、F5 プレビューでは見えても F6 レンダーで裏返り判定になります。
以下が最終形です。
"""build123d の形状を PythonSCAD GUI で表示する橋渡しモジュール(同梱 pybuild123d の拡張版)"""
from functools import wraps
from build123d import * # 利用側への再エクスポート用
from build123d import (Shape, Solid, Compound, Face, Sketch, # 内部の型判定用
Edge, Wire, Curve, Builder, GeomType, extrude)
from pythonscad import polyhedron, sphere, hull, union, show as _ps_show
_OPTS = {"tolerance", "angular_tolerance", "sheet", "wire_r", "segs"}
def _mesh(shape, tol, ang):
v, t = shape.tessellate(tolerance=tol, angular_tolerance=ang)
return polyhedron([list(p) for p in v], [list(f[::-1]) for f in t])
def to_polyhedron(obj, tolerance=0.01, angular_tolerance=0.2,
sheet=0.2, wire_r=0.3, segs=16):
"""build123d のオブジェクトを PythonSCAD のオブジェクトに変換する
3D (Solid/Compound) → polyhedron
2D (Face/Sketch) → 厚さ sheet の薄い板
線 (Edge/Wire/Curve)→ 半径 wire_r のカプセルをつないだ管
"""
if isinstance(obj, Builder): # BuildPart / BuildSketch / BuildLine を直接受ける
obj = obj._obj
if isinstance(obj, (Face, Sketch)):
return _mesh(extrude(obj, amount=sheet), tolerance, angular_tolerance)
if isinstance(obj, (Edge, Wire, Curve)):
parts = []
for e in obj.edges():
n = 1 if e.geom_type == GeomType.LINE else segs
pts = [e @ (i / n) for i in range(n + 1)]
for p, q in zip(pts, pts[1:]):
parts.append(hull(sphere(wire_r, fn=8).translate(list(p)),
sphere(wire_r, fn=8).translate(list(q))))
return union(parts)
if isinstance(obj, (Solid, Compound)):
return _mesh(obj, tolerance, angular_tolerance)
raise TypeError(f"show できない型です: {type(obj).__name__}")
def show(*objs, **kw):
"""build123d / PythonSCAD どちらのオブジェクトも受け付ける show"""
opts = {k: v for k, v in kw.items() if k in _OPTS} # ocp_vscode 用の未知の引数は無視
for o in objs:
_ps_show(to_polyhedron(o, **opts) if isinstance(o, (Shape, Builder)) else o)
show_object = show
def build123d(func):
"""同梱版互換: モデル作成関数を包んで PythonSCAD オブジェクトを返すデコレータ"""
@wraps(func)
def wrapper(*args, **kwargs):
return to_polyhedron(func(*args, **kwargs))
return wrapper
設計上のポイント
show を上書きする
show(to_polyhedron(x)) と毎回書かせるのではなく、show 自体が build123d の型を判定して変換します。利用側は build123d 公式ドキュメントの show(ex13) や show_object(...) をそのまま貼れます。
Builder も直接受ける
with BuildPart() as p: の結果は p.part、BuildSketch は .sketch、BuildLine は .line ですが、Builder._obj で共通に取り出せるので show(p) でも show(p.part) でも通ります。
2D と線も見せる
Sketch や Face をそのまま tessellate すると厚みゼロで F6 が壊れるので、薄く押し出して板にします。Plane * sketch で 3D 空間に置いた後でも、面の法線方向に押し出されるので向きは気にしなくて済みます。線は各区間を小さな球の hull でつないだカプセルにします。曲線を細かく分けるほど重くなるので segs で調整します。
build123d を再エクスポートする
先頭の from build123d import * により、利用側は from b123dview import * の 1 行で build123d の全名前空間が使えます。ただし Shape などの基底クラスは build123d の __all__ に含まれないため、内部で型判定に使う分は別途明示的に import しています。
同梱版のデコレータも残す
@build123d は to_polyhedron を呼ぶだけの薄い包みにしたので、return p でも return p.part でも代数モードの Solid でも通ります。なお、デコレータに完成品を渡す show(build123d(final_part)) は関数オブジェクトが返ってきて Invalid type for Object in show になります。デコレータは「モデルを作る関数」に付けるものです。
分割精度
tessellate(tolerance=0.001) は三角形数がかなり増えます。学習用途なら tolerance=0.01, angular_tolerance=0.2 あたりからが、見た目と速度のバランスが取りやすいです。
6. 使い方
代数モード
from b123dview import *
a, b = 40, 4
ex13 = Cylinder(radius=50, height=10)
plane = Plane(ex13.faces().sort_by().last)
ex13 -= (
plane
* PolarLocations(radius=a, count=4)
* CounterSinkHole(radius=b, counter_sink_radius=2 * b, depth=10)
)
ex13 -= (
plane
* PolarLocations(radius=a, count=4, start_angle=45, angular_range=360)
* CounterBoreHole(radius=b, counter_bore_radius=2 * b, depth=10, counter_bore_depth=b)
)
show(ex13)
Builder モード
from b123dview import *
with BuildPart() as p:
with BuildSketch() as sketch:
rect = Rectangle(width=100, height=50)
offset(rect, -2, mode=Mode.SUBTRACT)
extrude(amount=10)
show(p) # p.part でも可
# show(sketch) # 2D スケッチだけ確認したいとき
# show(sketch, sheet=3) # 板厚を変える

2D スケッチだけ show(sketch)

板厚を変える show(sketch, sheet=3)

PythonSCAD のオブジェクトと合成する
from pythonscad import sphere
from b123dview import *
box = Box(20, 20, 20)
show(to_polyhedron(box) | sphere(13)) # build123d 製と PythonSCAD 製を CSG で合成
7. 注意点
-
変換後は build123d のメソッドが使えない:
to_polyhedronを通した瞬間に PythonSCAD のオブジェクトになるので、その後.faces()やfilletは効きません。「build123d で作り切ってから変換し、そのあとは PythonSCAD の演算で扱う」という順序を、教材の冒頭で明示する必要があります。PythonSCAD のcube()と build123d のBox()を変換なしに直接ブール演算することはできません -
Builder モードのインデント:
withの入れ子が文脈そのものなので、extrudeやfilletの位置を1段間違えるとdoesn't apply to [...]で落ちます。デコレータ方式でサンプルをずらすときが特に要注意です -
浮動小数の一致判定:
filter_by(lambda e: e.length == 5)のような完全一致は、OCCT の計算結果によっては取りこぼします。abs(e.length - 5) < 1e-6か、filter_by(GeomType.LINE)のような幾何種別の絞り込みと組み合わせるほうが「なぜ選ばれない」を減らせます -
Z-fighting: 2D の板は XY 面のスケッチなら Z が 0〜
sheetに出ます。3D 部品と重ねて見るときに面がちらつく場合はsheetを変えるか、both=Trueで押し出す形に直します - 座標系と単位: 両者とも Z 上・右手系・mm なので、変換で気にする点はありません
-
PythonSCAD 側 API の版差:
union(parts)がリストを受けない版ではunion(*parts)に変えてください
おわりに
Windows のセキュリティブロックから始まりましたが、結果として「PythonSCAD の GUI を build123d の即席ビューアとして使う」という、起動待ちのない学習環境ができました。
OpenSCAD 系の CSG と B-Rep の build123d を同じ画面で扱えるので、両者の書き味の違いを比べながら学ぶ教材にも展開できそうです。build123d の記述を PythonSCAD より手軽に書けるところまで持っていけるか、引き続き検証していきます。ありがとうございます。
- PythonSCADのGUIでbuild123dを動かす
