2026年最初のFablab Miyazaki勉強会(FLMxFRIDAY)は、impossible passthroughというちょっと不思議な3DモデルをDesignSpark Mechanical(Windows専用)というCADソフトで作ってみようという内容でした。MacBookで(しかも遅刻して)参加した私は、お菓子を食べながら隣の人の画面を覗かせてもらうだけでしたが、普段使っているTinkercadや以前少し習ったFusionとはまた異なる操作感が新鮮でした。
勉強会が終わった後に、ひょっとして、、と思って、Geminiに「impossible passthrough作れる?」と聞いてみたら、OpenSCADのコードを生成してくれました。ただ、これもMacのセキュリティに引っかかってインストールができませんでしたので(頑張れば出来ますが、怖かったので諦めて)、Pythonで生成するようお願いしてみたら、バッチリ動くコードを書いてくれました。STLのプレビューも良さそうなので、あとは実際にプリントして動くか試してみようと思います。
import numpy as np
from stl import mesh
# ==========================================
# 1. パラメータ設定
# ==========================================
height = 60.0 # 高さ(mm)
layers = 100 # 縦方向の滑らかさ(層の数)
resolution = 120 # 円周方向の滑らかさ
twist_angle = 2 * np.pi # 360度のねじれ
# 断面形状のパラメータ(四つ葉型: r = R + A*cos(4θ))
R_base = 10.0
A = 4.0 # 凹凸の深さ
clearance = 0.6 # パーツ間の隙間
R_outer = 18.0 # 外側の筒の半径
offset_dist = 22.0 # 左右に並べるための距離
# Z座標と角度の配列
z_vals = np.linspace(0, height, layers)
theta_vals = np.linspace(0, 2 * np.pi, resolution, endpoint=False)
vertices = []
faces = []
def add_face(v1, v2, v3):
faces.append([v1, v2, v3])
# ==========================================
# 2. 内側のパーツ(すり抜ける側)の生成
# ==========================================
v_start_inner = len(vertices)
# 頂点の計算
for z in z_vals:
twist = twist_angle * (z / height)
for t in theta_vals:
# 隙間の分だけ細くする (- clearance / 2.0)
r = R_base + A * np.cos(4 * t) - clearance / 2.0
x = r * np.cos(t + twist) - offset_dist
y = r * np.sin(t + twist)
vertices.append([x, y, z])
# 上下のフタ(中心点)
cap_bottom_in = len(vertices); vertices.append([-offset_dist, 0, 0])
cap_top_in = len(vertices); vertices.append([-offset_dist, 0, height])
def v_in(l, t): return v_start_inner + l * resolution + (t % resolution)
# 側面のポリゴン化(法線は外側)
for l in range(layers - 1):
for t in range(resolution):
add_face(v_in(l, t), v_in(l, t+1), v_in(l+1, t))
add_face(v_in(l, t+1), v_in(l+1, t+1), v_in(l+1, t))
# 上下のフタのポリゴン化
for t in range(resolution):
add_face(v_in(0, t), cap_bottom_in, v_in(0, t+1))
add_face(v_in(layers-1, t), v_in(layers-1, t+1), cap_top_in)
# ==========================================
# 3. 外側のパーツ(土台側)の生成
# ==========================================
v_start_hole = len(vertices)
# 内側の穴の頂点(ねじれる)
for z in z_vals:
twist = twist_angle * (z / height)
for t in theta_vals:
# 隙間の分だけ穴を広げる (+ clearance / 2.0)
r = R_base + A * np.cos(4 * t) + clearance / 2.0
x = r * np.cos(t + twist) + offset_dist
y = r * np.sin(t + twist)
vertices.append([x, y, z])
v_start_outer = len(vertices)
# 外側の筒の頂点(対応する頂点同士を結ぶため、同じねじれを加える)
for z in z_vals:
twist = twist_angle * (z / height)
for t in theta_vals:
x = R_outer * np.cos(t + twist) + offset_dist
y = R_outer * np.sin(t + twist)
vertices.append([x, y, z])
def v_hole(l, t): return v_start_hole + l * resolution + (t % resolution)
def v_out(l, t): return v_start_outer + l * resolution + (t % resolution)
# 側面のポリゴン化
for l in range(layers - 1):
for t in range(resolution):
# 穴の側面(法線は内側=パーツの中心に向かう)
add_face(v_hole(l, t), v_hole(l+1, t), v_hole(l, t+1))
add_face(v_hole(l, t+1), v_hole(l+1, t), v_hole(l+1, t+1))
# 外側の筒の側面(法線は外側)
add_face(v_out(l, t), v_out(l, t+1), v_out(l+1, t))
add_face(v_out(l, t+1), v_out(l+1, t+1), v_out(l+1, t))
# 上下のフタのポリゴン化(穴のフチと外側のフチをドーナツ状に結ぶ)
for t in range(resolution):
# 底面
add_face(v_hole(0, t), v_hole(0, t+1), v_out(0, t))
add_face(v_hole(0, t+1), v_out(0, t+1), v_out(0, t))
# 天面
add_face(v_hole(layers-1, t), v_out(layers-1, t), v_hole(layers-1, t+1))
add_face(v_hole(layers-1, t+1), v_out(layers-1, t), v_out(layers-1, t+1))
# ==========================================
# 4. STLファイルとしての書き出し
# ==========================================
vertices = np.array(vertices)
faces = np.array(faces)
# numpy-stlのメッシュオブジェクトを作成
result_mesh = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces):
for j in range(3):
result_mesh.vectors[i][j] = vertices[f[j], :]
# 保存
filename = 'impossible_passthrough_python.stl'
result_mesh.save(filename)
print(f"✅ STLデータを {filename} として保存しました!")