FLM Friday

2026-02-14 python ai /posts/2026/2026-02-14-impossible-passthrough.png

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} として保存しました!")
2月の週末