PythonSCADコミュニティ・開発者に感謝申し上げます。ありがとうございます。
以下、OpenSCAD、Pov-Ray内のシーン記述、曲線の記述を参考にPythonSCADへ変換してみました。
OpenSCADコミュニティ、Pov-Rayコミュニティ、記事執筆者に感謝申し上げます。ありがとうございます。
- Pov-Rayの紹介
Pov-Rayを変換したサンプル例
01. 連結リング(半トーラスとリンク)
// ==========================================
// 連結リング(半トーラスとリンク)
// 作成日: 2024/06/xx
// ==========================================
$fn = 60; // 円滑度(滑らかさ)を高めるために設定します(今回は12が元なので変更可)
// --- モジュール定義 ---
// 1. 半トーラス形状(輪の半分)
// rotate_extrude で円を回転させてトーラスの断面を作り、cubeで半分に切り取る
module half_torus() {
intersection() {
// 半分に切り取るための立方体(8mm立方)を少しずらして配置
translate([0, -4, -4]) cube([8, 8, 8], center=false);
// トーラスの断面の円をX=2.5にオフセットし、Z軸回りに回転させて生成
rotate_extrude(convexity=10, $fn=12)
translate([2.5, 0, 0])
circle(r=0.9);
}
}
// 2. 半リンク形状(半トーラス+軸部分の円柱)
// 半トーラスをX方向に2mm移動し、Y方向に2.5mm移動した場所にZ軸方向に立つ円柱を配置
module half_link() {
translate([2, 0, 0]) half_torus();
translate([0, 2.5, 0])
rotate([0, 90, 0])
cylinder(r=0.9, h=4, center=true, $fn=12);
}
// 3. リンク全体(左右対称の半リンク2つを180度回転して組み合わせる)
module link() {
half_link();
rotate([0, 0, 180])
half_link();
}
// --- メイン処理 ---
link();
OpenSCADを変換してください。
1. 完成コード(PythonSCAD)
# ==========================================
# 連結リング(半トーラスとリンク)
# 作成日: 2026/08/19
# ==========================================
from pythonscad import *
# --- パラメータ設定 (単位: mm) ---
fn_global = 60 # 元の $fn=60 相当(今回は形状ごとに fn を指定)
fn_local = 12 # rotate_extrude / cylinder の分割数(元コードに合わせる)
cube_size = 8.0
torus_R = 2.5 # トーラス中心半径(断面円の中心Xオフセット)
tube_r = 0.9 # チューブ半径(断面円の半径)
half_shift_x = 2.0 # 半トーラスをX方向へ移動
pin_y = 2.5 # 軸円柱のY位置
pin_len = 4.0 # 軸円柱の長さ(X方向)
convexity_val = 10 # OpenSCADの指定に合わせて保持(環境により無視される場合あり)
# --- 関数(モジュール)定義 ---
def half_torus(fn_rot=12, fn_circle=12):
"""
半トーラス形状(輪の半分)
rotate_extrude でトーラスを作り、x>=0 側だけ残すために cube と intersection する
"""
cutter_cube = translate(
cube([cube_size, cube_size, cube_size], center=False),
[0.0, -cube_size / 2.0, -cube_size / 2.0]
)
# 2D断面円(circle は常に原点中心。2Dのtranslateは [x, y] の2要素)
section_2d = translate(circle(r=tube_r, fn=fn_circle), [torus_R, 0.0])
# 2D断面をZ軸周りに回してトーラス化
torus_3d = rotate_extrude(section_2d, convexity=convexity_val, fn=fn_rot)
return intersection(cutter_cube, torus_3d)
def half_link(fn=12):
"""
半リンク形状(半トーラス+軸部分の円柱)
"""
part1 = translate(half_torus(fn_rot=fn, fn_circle=fn), [half_shift_x, 0.0, 0.0])
pin = cylinder(r=tube_r, h=pin_len, center=True, fn=fn)
pin_x = rotate(pin, [0.0, 90.0, 0.0]) # 円柱をX軸方向へ
part2 = translate(pin_x, [0.0, pin_y, 0.0])
return part1 + part2
def link(fn=12):
"""
リンク全体(左右対称の半リンク2つを180度回転して結合)
"""
a = half_link(fn=fn)
b = rotate(half_link(fn=fn), [0.0, 0.0, 180.0])
return a + b
# --- メイン処理 ---
result = link(fn=fn_local)
show(result)
変換時の重要ポイント
- OpenSCAD の
$fn=60;は Python では書けないので、fn_global/fn_localのように変数で管理し、circle(..., fn=...)やcylinder(..., fn=...)の引数として渡します - 2D図形(
circle)の移動はtranslate(2d_obj, [x, y])(2要素)です。OpenSCADのtranslate([2.5,0,0]) circle(...)をそのまま3要素にするとエラーになります
02. チェーンリンク(リングを円周上に並べる)
// ==========================================
// チェーンリンク(リングを円周上に並べる)
// 作成日: 2026/07/25
// ==========================================
$fn = 60; // 円の滑らかさ(全体の基本)
// --- パラメータ設定 (単位: mm) ---
// リンク(1個の輪っか)寸法
w = 3; // リンクの直線部の長さ(X方向に伸びる円柱の長さ)
r1 = 5; // リンクの「円環部」の直径(中心線の直径イメージ)
r2 = 0.9; // リンクの断面半径(太さ)
// 配置(円周上に並べる)設定
N = 18; // リング(リンク)の数
ring_R = 15; // 並べる円の半径(リンク中心が乗る円)
// 計算用(よく使う値をまとめる)
link_major_R = r1 / 2; // 円環の中心線半径
torus_span = r1 + 2 * r2; // だいたいの外形寸法(直径方向の最大)
// --- モジュール定義 ---
// 0) トーラス(ドーナツ)本体
// rotate_extrude() は「2D図形を回転させて3Dにする」命令
module torus(r1_d, r2_r) {
rotate_extrude(convexity = 10)
translate([r1_d/2, 0, 0])
circle(r = r2_r);
}
// 1) 半トーラス(トーラスの半分だけ)
// intersection() = 共通部分だけ残す(= ドーナツを箱で半分に切るイメージ)
module half_torus(r1_d, r2_r) {
intersection() {
// (A) 半分に切るための「箱」(X<0側を消して、X>=0側だけ残す)
// cubeはcenter=trueを使い、箱の中心をずらして「Xが0~+L」になるようにする
translate([torus_span/2, 0, 0])
cube([torus_span, torus_span, torus_span], center = true);
// (B) 元のトーラス
torus(r1_d, r2_r);
}
}
// 2) リンクの半分(片側)
// 半トーラス + 直線円柱 を union() で足し算
module half_link(w_len, r1_d, r2_r) {
union() {
// (A) 片側の半円部分(半トーラス)を、直線部の端に配置
translate([w_len/2, 0, 0])
half_torus(r1_d, r2_r);
// (B) 直線部(円柱):Z方向の円柱を回転してX方向に向ける
translate([0, r1_d/2, 0])
rotate([0, 90, 0])
cylinder(h = w_len, r = r2_r, center = true);
}
}
// 3) リンク1個(左右対称に2つの half_link を配置)
module link(w_len, r1_d, r2_r) {
union() {
half_link(w_len, r1_d, r2_r);
// 反対側(180度回転して反転)
rotate([0, 0, 180])
half_link(w_len, r1_d, r2_r);
}
}
// --- メイン処理 ---
// 円周上にリンクを並べる
for (i = [0 : N - 1]) {
angle = 360 / N * i; // i番目の角度(度)
dx = ring_R * cos(angle); // 円周上のX座標
dy = ring_R * sin(angle); // 円周上のY座標
// 接線方向に向けるために +90度
// さらに「鎖っぽく」するために、1個おきに90度ひねる(i%2 で 0/1 を交互に)
translate([dx, dy, 0])
rotate([0, 0, angle + 90])
rotate([(i % 2) * 90, 0, 0])
link(w, r1, r2);
}
OpenSCADを変換してください。
1. 完成コード(PythonSCAD)
# ==========================================
# チェーンリンク(リングを円周上に並べる)
# 作成日: 2026/08/19
# ==========================================
from pythonscad import *
from math import pi, sin, cos
# --- パラメータ設定 (単位: mm) ---
fn_val = 60 # 円・回転体の滑らかさ
eps = 0.05 # チラつき防止(今回は主に予備)
# リンク(1個の輪っか)寸法
w = 3.0 # リンクの直線部の長さ(X方向に伸びる円柱の長さ)
r1 = 5.0 # リンクの「円環部」の直径(中心線の直径イメージ)
r2 = 0.9 # リンクの断面半径(太さ)
# 配置(円周上に並べる)設定
N = 18 # リング(リンク)の数
ring_R = 15.0 # 並べる円の半径(リンク中心が乗る円)
# 計算用
link_major_R = r1 / 2.0 # 円環の中心線半径(参考)
torus_span = r1 + 2.0 * r2 # だいたいの外形寸法(直径方向の最大)
# --- 関数(モジュール)定義 ---
def torus(r1_d, r2_r, fn):
"""
トーラス(ドーナツ)本体
rotate_extrude は「2D断面をZ軸周りに回して3Dにする」
"""
profile = translate(circle(r=r2_r, fn=fn), [r1_d / 2.0, 0.0]) # 2D移動は [x, y]
return rotate_extrude(profile, convexity=10)
def half_torus(r1_d, r2_r, span, fn):
"""
半トーラス:intersection(共通部分)で、ドーナツを箱で半分に切る
X<0側を消して X>=0側だけ残すイメージ
"""
box = cube([span, span, span], center=True)
box = translate(box, [span / 2.0, 0.0, 0.0]) # 3D移動は [x, y, z]
t = torus(r1_d, r2_r, fn)
return intersection(box, t)
def half_link(w_len, r1_d, r2_r, span, fn):
"""
リンクの半分(片側)= 半トーラス + 直線円柱
"""
# (A) 片側の半円部分(半トーラス)を、直線部の端に配置
arc = half_torus(r1_d, r2_r, span, fn)
arc = translate(arc, [w_len / 2.0, 0.0, 0.0])
# (B) 直線部(円柱):Z方向の円柱を回転してX方向に向ける
bar = cylinder(h=w_len, r=r2_r, center=True, fn=fn)
bar = rotate(bar, [0.0, 90.0, 0.0]) # X方向へ倒す
bar = translate(bar, [0.0, r1_d / 2.0, 0.0]) # 端の位置へ
return arc + bar
def link(w_len, r1_d, r2_r, span, fn):
"""
リンク1個(左右対称に2つの half_link を配置)
"""
a = half_link(w_len, r1_d, r2_r, span, fn)
b = rotate(half_link(w_len, r1_d, r2_r, span, fn), [0.0, 0.0, 180.0])
return a + b
# --- メイン処理 ---
# 円周上にリンクを並べる(接線方向に向け、1個おきに90度ひねる)
result = None
for i in range(N):
angle_deg = 360.0 / N * i
theta = angle_deg * pi / 180.0
dx = ring_R * cos(theta)
dy = ring_R * sin(theta)
one = link(w, r1, r2, torus_span, fn_val)
one = rotate(one, [(i % 2) * 90.0, 0.0, 0.0]) # 1個おきにひねり
one = rotate(one, [0.0, 0.0, angle_deg + 90.0]) # 接線方向へ
one = translate(one, [dx, dy, 0.0])
result = one if result is None else (result + one)
show(result)
2. 変換のポイント(超重要だけ)
- OpenSCAD の
$fn=60;は PythonSCAD では書けないので、fn_val=60を作って 各circle/cylinderにfn=fn_valで渡しています -
union(){...}はa + bに置き換えています(PythonSCADでは演算子で足す) -
intersection(){...}はルールに合わせてintersection(a, b)を使っています - 2D形状(
circle)の移動はtranslate(2d, [x, y])(2要素)、3D形状はtranslate(3d, [x, y, z])(3要素) です
04. バックミンスター・フラーのジオデシック・ドーム風
「計算機言語で形を造る: OpenSCAD Scripting Manual
平沢 岳人 (著) 形式: Kindle版」を参考にしました。建築愛を感じるとてもいい本です。おすすめ本です。
// ==========================================
// 球面三角形の再帰分割(geodesic風ワイヤー)
// 作成日: 2026/07/25
// ==========================================
$fn = 36; // 円の滑らかさ(必要なら上げる)
// --- パラメータ設定 (単位: mm として扱うなら、全体スケールに注意) ---
edge_r = 0.04; // 辺(円柱)の半径
node_r = 0.04; // 頂点(球)の半径
level = 3; // 再帰分割レベル(0,1,2,3...)
// --- ヘルパー関数 ---
// ベクトル長さ(OpenSCADの norm() を使わず自前で定義)
function vlen(v) = sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
// 点pを「半径Rの球面」へ投影(原点中心の球を想定)
function project_to_sphere(p, R) = p * (R / vlen(p));
// 2点の中点
function midpoint(a, b) = (a + b) / 2;
// --- 2点間に円柱を生成するモジュール(center=true準拠) ---
module genCylinder(pA, pB, r) {
v = pB - pA;
h = vlen(v);
mid = (pA + pB) / 2;
// Z軸([0,0,1]) を v 方向へ回す回転を作る
axis = cross([0, 0, 1], v);
axis_len = vlen(axis);
// acosに入る値の誤差対策で clamp
cosang = (h == 0) ? 1 : v[2] / h;
cosang2 = min(1, max(-1, cosang));
ang = acos(cosang2);
translate(mid)
// vがZ軸と平行に近いとき axis がほぼ0になるので分岐
(axis_len < 1e-9)
? cylinder(h = h, r = r, center = true)
: rotate(a = ang, v = axis)
cylinder(h = h, r = r, center = true);
}
// --- メイン(再帰分割して球面上の三角形を細分化し、ワイヤー表示する) ---
module backy_ball(p0, p1, p2, lv) {
// p0が球面上にある前提で、その半径Rを採用(p1,p2も同じ半径が望ましい)
R = vlen(p0);
// 3辺の中点を作る
m01 = midpoint(p0, p1);
m12 = midpoint(p1, p2);
m20 = midpoint(p2, p0);
// 中点は球の内側に落ちるので、球面へ投影して戻す(外接球面に投影)
p01 = project_to_sphere(m01, R);
p12 = project_to_sphere(m12, R);
p20 = project_to_sphere(m20, R);
if (lv > 0) {
// できた3点(p01,p12,p20)で中央の三角形+周辺3つ=合計4つに分割
backy_ball(p01, p12, p20, lv - 1);
backy_ball(p0, p01, p20, lv - 1);
backy_ball(p1, p01, p12, lv - 1);
backy_ball(p2, p12, p20, lv - 1);
} else {
// lv==0: この三角形の「辺」と「頂点」を描画(ワイヤーフレーム)
union() {
// 辺(円柱)
genCylinder(p0, p1, edge_r);
genCylinder(p1, p2, edge_r);
genCylinder(p2, p0, edge_r);
// 頂点(球)
translate(p0) sphere(r = node_r);
translate(p1) sphere(r = node_r);
translate(p2) sphere(r = node_r);
}
}
}
// --- 実行例 ---
// 半径1の球面上にある正四面体の3点を例として三角形1枚を細分化して描画
R0 = 20; // 球の半径(mm)
p0 = project_to_sphere([ 1, 1, 1], R0);
p1 = project_to_sphere([-1, -1, 1], R0);
p2 = project_to_sphere([-1, 1, -1], R0);
color("SteelBlue")
backy_ball(p0, p1, p2, level);
OpenSCADを変換してください。
1. 完成コード(PythonSCAD)
# ==========================================
# 球面三角形の再帰分割(geodesic風ワイヤー)
# 作成日: 2026/08/19
# ==========================================
from pythonscad import *
from math import pi, sin, cos, sqrt, acos, atan2
# --- パラメータ設定 (単位: mm) ---
edge_r = 0.25 # 辺(円柱)の半径
node_r = 0.25 # 頂点(球)の半径
level = 3 # 再帰分割レベル(0,1,2,3...)
R0 = 20.0 # 球の半径(mm)
fn_val = 24 # 円・球の滑らかさ
eps = 1e-9 # ゼロ割り等の保護
# --- ヘルパー関数(ベクトル) ---
def vadd(a, b):
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
def vsub(a, b):
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
def vscale(v, s):
return [v[0] * s, v[1] * s, v[2] * s]
def vlen(v):
return sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
def midpoint(a, b):
return vscale(vadd(a, b), 0.5)
def clamp(x, lo, hi):
return max(lo, min(hi, x))
def project_to_sphere(p, R):
"""点pを原点中心・半径Rの球面へ投影"""
L = vlen(p)
if L < eps:
return [0.0, 0.0, 0.0]
return vscale(p, R / L)
# --- 2点間に円柱を生成(center=True準拠、Z軸円柱を回して合わせる) ---
def gen_cylinder(pA, pB, r, fn):
v = vsub(pB, pA)
h = vlen(v)
mid = midpoint(pA, pB)
# 2点がほぼ同じなら、円柱の代わりにノード球を返す(安全策)
if h < eps:
return translate(sphere(r=r, fn=fn), mid)
# OpenSCADの rotate(a=ang, v=axis)(軸-角回転)がPythonSCADで使えない前提なので、
# 「yaw(方位角) + pitch(傾き)」の2段回転でZ軸をv方向に合わせる。
vx, vy, vz = v[0] / h, v[1] / h, v[2] / h
pitch = acos(clamp(vz, -1.0, 1.0)) # 0..pi
yaw = 0.0 if (abs(vx) < eps and abs(vy) < eps) else atan2(vy, vx)
pitch_deg = pitch * 180.0 / pi
yaw_deg = yaw * 180.0 / pi
cyl0 = cylinder(h=h, r=r, center=True, fn=fn)
cyl1 = rotate(cyl0, [0.0, pitch_deg, 0.0]) # まず傾ける
cyl2 = rotate(cyl1, [0.0, 0.0, yaw_deg]) # 次に方位を合わせる
return translate(cyl2, mid)
# --- メイン(再帰分割して球面上の三角形を細分化し、ワイヤー表示) ---
def backy_ball(p0, p1, p2, lv, edge_r, node_r, fn):
# p0が球面上にある前提で、その半径Rを採用
R = vlen(p0)
# 3辺の中点 -> 球面に投影
p01 = project_to_sphere(midpoint(p0, p1), R)
p12 = project_to_sphere(midpoint(p1, p2), R)
p20 = project_to_sphere(midpoint(p2, p0), R)
if lv > 0:
# 4枚に分割
return (
backy_ball(p01, p12, p20, lv - 1, edge_r, node_r, fn)
+ backy_ball(p0, p01, p20, lv - 1, edge_r, node_r, fn)
+ backy_ball(p1, p01, p12, lv - 1, edge_r, node_r, fn)
+ backy_ball(p2, p12, p20, lv - 1, edge_r, node_r, fn)
)
# lv == 0: この三角形の辺(円柱)+頂点(球)
edges = (
gen_cylinder(p0, p1, edge_r, fn)
+ gen_cylinder(p1, p2, edge_r, fn)
+ gen_cylinder(p2, p0, edge_r, fn)
)
nodes = (
translate(sphere(r=node_r, fn=fn), p0)
+ translate(sphere(r=node_r, fn=fn), p1)
+ translate(sphere(r=node_r, fn=fn), p2)
)
return edges + nodes
# --- 実行例(正四面体の3点で三角形1枚を細分化) ---
p0 = project_to_sphere([ 1.0, 1.0, 1.0], R0)
p1 = project_to_sphere([-1.0, -1.0, 1.0], R0)
p2 = project_to_sphere([-1.0, 1.0, -1.0], R0)
result = backy_ball(p0, p1, p2, level, edge_r, node_r, fn_val)
result = color(result, "SteelBlue")
show(result)
2. 変換時の注意
- OpenSCADの **
rotate(a=ang, v=axis)(軸-角回転)**は、PythonSCADでは同じ書き方ができない環境が多いので、ここでは yaw(Z回転)+ pitch(Y回転) の2段回転で「Z軸の円柱」を目的方向に向けています。 -
$fn = 36のような独立行は Python では SyntaxError になるので、sphere(..., fn=fn_val)/cylinder(..., fn=fn_val)のように 各形状の引数で渡す形に変換しています
05. 半球全体を生成
1. 完成コード(PythonSCAD)※半球(+Z側)をワイヤー生成
# ==========================================
# 球面三角形の再帰分割(geodesic風ワイヤー)- 半球版(+Z)
# 作成日: 2026/08/20
# ==========================================
from pythonscad import *
from math import pi, sin, cos, sqrt, acos, atan2
# --- パラメータ設定 (単位: mm) ---
R0 = 20.0 # 球の半径
edge_r = 0.25 # 辺(円柱)の半径
node_r = 0.35 # 頂点(球)の半径
level = 3 # 再帰分割レベル
fn_val = 20 # 円・球の滑らかさ
eps = 1e-9 # 数値保護
q_digit = 6 # 重複辺除去用の丸め桁(大きいほど厳密)
# --- ベクトルヘルパー ---
def vadd(a, b): return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
def vsub(a, b): return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
def vscale(v, s): return [v[0] * s, v[1] * s, v[2] * s]
def vlen(v): return sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
def midpoint(a, b):
return vscale(vadd(a, b), 0.5)
def clamp(x, lo, hi):
return max(lo, min(hi, x))
def project_to_sphere(p, R):
L = vlen(p)
if L < eps:
return [0.0, 0.0, 0.0]
return vscale(p, R / L)
def pkey(p):
# float誤差で同一点が別物扱いされないよう、丸めてキー化
return (round(p[0], q_digit), round(p[1], q_digit), round(p[2], q_digit))
def edgekey(a, b):
ka, kb = pkey(a), pkey(b)
return (ka, kb) if ka <= kb else (kb, ka)
# --- 2点間の円柱(Z軸円柱を yaw+pitch で方向合わせ) ---
def gen_cylinder(pA, pB, r, fn):
v = vsub(pB, pA)
h = vlen(v)
mid = midpoint(pA, pB)
if h < eps:
return translate(sphere(r=r, fn=fn), mid)
vx, vy, vz = v[0] / h, v[1] / h, v[2] / h
pitch = acos(clamp(vz, -1.0, 1.0)) # Z軸からの傾き
yaw = 0.0 if (abs(vx) < eps and abs(vy) < eps) else atan2(vy, vx)
pitch_deg = pitch * 180.0 / pi
yaw_deg = yaw * 180.0 / pi
cyl0 = cylinder(h=h, r=r, center=True, fn=fn)
cyl1 = rotate(cyl0, [0.0, pitch_deg, 0.0])
cyl2 = rotate(cyl1, [0.0, 0.0, yaw_deg])
return translate(cyl2, mid)
# --- 再帰分割:形状を直接作らず、重複しない「辺」と「点」を収集 ---
def collect_wire(p0, p1, p2, lv, segs_dict, nodes_dict):
R = vlen(p0)
p01 = project_to_sphere(midpoint(p0, p1), R)
p12 = project_to_sphere(midpoint(p1, p2), R)
p20 = project_to_sphere(midpoint(p2, p0), R)
if lv > 0:
collect_wire(p01, p12, p20, lv - 1, segs_dict, nodes_dict)
collect_wire(p0, p01, p20, lv - 1, segs_dict, nodes_dict)
collect_wire(p1, p01, p12, lv - 1, segs_dict, nodes_dict)
collect_wire(p2, p12, p20, lv - 1, segs_dict, nodes_dict)
return
# lv==0: 辺3本+頂点3点を登録(キーで重複除去)
for a, b in [(p0, p1), (p1, p2), (p2, p0)]:
ek = edgekey(a, b)
if ek not in segs_dict:
segs_dict[ek] = (a, b)
for p in [p0, p1, p2]:
pk = pkey(p)
if pk not in nodes_dict:
nodes_dict[pk] = p
# --- メイン:半球(+Z側)を「正八面体の上半分」4三角形から開始 ---
# 正八面体の頂点(すでに球面上)
p_top = [0.0, 0.0, R0]
p_x = [R0, 0.0, 0.0]
p_y = [0.0, R0, 0.0]
p_nx = [-R0, 0.0, 0.0]
p_ny = [0.0, -R0, 0.0]
# 上半球を覆う4枚(三角形の向きはどれでもOK)
base_tris = [
(p_top, p_x, p_y),
(p_top, p_y, p_nx),
(p_top, p_nx, p_ny),
(p_top, p_ny, p_x),
]
segments = {}
nodes = {}
for (a, b, c) in base_tris:
collect_wire(a, b, c, level, segments, nodes)
wire = None
for (a, b) in segments.values():
obj = gen_cylinder(a, b, edge_r, fn_val)
wire = obj if wire is None else (wire + obj)
dots = None
for p in nodes.values():
s = translate(sphere(r=node_r, fn=fn_val), p)
dots = s if dots is None else (dots + s)
result = wire if dots is None else (wire + dots)
result = color(result, "SteelBlue")
show(result)
2. どうやって「半球全体」になっている?
- **正八面体(オクタヘドラル)**の「上半分」は、頂点
p_topと赤道上の4点(±X, ±Y)でできる 4枚の球面三角形でちょうど覆えます - その4枚をそれぞれ同じ再帰分割にかけ、最後に 辺と頂点を辞書で重複除去して、半球のワイヤーになります
06. 半球に“縁(赤道リング)だけ太さを変える
1. 完成コード(PythonSCAD)※赤道リングだけ太くする版
# ==========================================
# 半球ワイヤー(geodesic風)- 赤道リングだけ太さ変更
# 作成日: 2026/08/20
# ==========================================
from pythonscad import *
from math import pi, sin, cos, sqrt, acos, atan2
# --- パラメータ設定 (単位: mm) ---
R0 = 20.0 # 球の半径
edge_r = 0.3 # 通常の辺(円柱)半径
ring_edge_r = 0.8 # 赤道リングの辺(円柱)半径 ←ここだけ太くする
node_r = 0.3 # 通常の頂点(球)半径
ring_node_r = 1.0 # 赤道リング上の頂点(球)半径(任意)
level = 3 # 再帰分割レベル
fn_val = 24 # 円・球の滑らかさ
eps = 1e-9 # 数値保護
# 「赤道判定」用の許容値(Z=0 からどのくらいまでを赤道扱いにするか)
equator_tol = 0.02 # mm(大きすぎると赤道以外も太くなります)
q_digit = 6 # 重複除去用の丸め桁
# --- ベクトルヘルパー ---
def vadd(a, b): return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
def vsub(a, b): return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
def vscale(v, s): return [v[0] * s, v[1] * s, v[2] * s]
def vlen(v): return sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
def midpoint(a, b):
return vscale(vadd(a, b), 0.5)
def clamp(x, lo, hi):
return max(lo, min(hi, x))
def project_to_sphere(p, R):
L = vlen(p)
if L < eps:
return [0.0, 0.0, 0.0]
return vscale(p, R / L)
def pkey(p):
return (round(p[0], q_digit), round(p[1], q_digit), round(p[2], q_digit))
def edgekey(a, b):
ka, kb = pkey(a), pkey(b)
return (ka, kb) if ka <= kb else (kb, ka)
def is_on_equator(p, tol):
return abs(p[2]) <= tol
# --- 2点間の円柱(Z軸円柱を yaw+pitch で方向合わせ) ---
def gen_cylinder(pA, pB, r, fn):
v = vsub(pB, pA)
h = vlen(v)
mid = midpoint(pA, pB)
if h < eps:
return translate(sphere(r=r, fn=fn), mid)
vx, vy, vz = v[0] / h, v[1] / h, v[2] / h
pitch = acos(clamp(vz, -1.0, 1.0))
yaw = 0.0 if (abs(vx) < eps and abs(vy) < eps) else atan2(vy, vx)
pitch_deg = pitch * 180.0 / pi
yaw_deg = yaw * 180.0 / pi
cyl0 = cylinder(h=h, r=r, center=True, fn=fn)
cyl1 = rotate(cyl0, [0.0, pitch_deg, 0.0])
cyl2 = rotate(cyl1, [0.0, 0.0, yaw_deg])
return translate(cyl2, mid)
# --- 再帰分割:重複しない「辺」と「点」を収集 ---
def collect_wire(p0, p1, p2, lv, segs_dict, nodes_dict):
R = vlen(p0)
p01 = project_to_sphere(midpoint(p0, p1), R)
p12 = project_to_sphere(midpoint(p1, p2), R)
p20 = project_to_sphere(midpoint(p2, p0), R)
if lv > 0:
collect_wire(p01, p12, p20, lv - 1, segs_dict, nodes_dict)
collect_wire(p0, p01, p20, lv - 1, segs_dict, nodes_dict)
collect_wire(p1, p01, p12, lv - 1, segs_dict, nodes_dict)
collect_wire(p2, p12, p20, lv - 1, segs_dict, nodes_dict)
return
for a, b in [(p0, p1), (p1, p2), (p2, p0)]:
ek = edgekey(a, b)
if ek not in segs_dict:
segs_dict[ek] = (a, b)
for p in [p0, p1, p2]:
pk = pkey(p)
if pk not in nodes_dict:
nodes_dict[pk] = p
# --- メイン:上半球(+Z側)を正八面体の上半分4三角形から開始 ---
p_top = [0.0, 0.0, R0]
p_x = [R0, 0.0, 0.0]
p_y = [0.0, R0, 0.0]
p_nx = [-R0, 0.0, 0.0]
p_ny = [0.0, -R0, 0.0]
base_tris = [
(p_top, p_x, p_y),
(p_top, p_y, p_nx),
(p_top, p_nx, p_ny),
(p_top, p_ny, p_x),
]
segments = {}
nodes = {}
for (a, b, c) in base_tris:
collect_wire(a, b, c, level, segments, nodes)
# 辺(赤道だけ太く)
wire = None
for (a, b) in segments.values():
r_seg = ring_edge_r if (is_on_equator(a, equator_tol) and is_on_equator(b, equator_tol)) else edge_r
obj = gen_cylinder(a, b, r_seg, fn_val)
wire = obj if wire is None else (wire + obj)
# 頂点(赤道だけ少し大きくする:任意)
dots = None
for p in nodes.values():
r_node = ring_node_r if is_on_equator(p, equator_tol) else node_r
s = translate(sphere(r=r_node, fn=fn_val), p)
dots = s if dots is None else (dots + s)
result = wire if dots is None else (wire + dots)
result = color(result, "SteelBlue")
show(result)
2. 変更点(何をして“赤道だけ太く”している?)
- 辺を作る直前に、両端点が どちらも
Z≈0(赤道上) ならring_edge_rを使う、そうでなければedge_rを使う、という分岐にしています - 赤道判定の精度は
equator_tolで調整します(分割レベルを上げると点が増えるので、基本は小さめが安全)
07. 赤道リングだけ色を変える(青:通常、"forestgreen":赤道)
1. 完成コード(PythonSCAD)※赤道リングだけ forestgreen、それ以外は青
# ==========================================
# 半球ワイヤー(geodesic風)- 赤道リングだけ色&太さ変更
# 作成日: 2026/08/20
# ==========================================
from pythonscad import *
from math import pi, sin, cos, sqrt, acos, atan2
# --- パラメータ設定 (単位: mm) ---
R0 = 20.0 # 球の半径
edge_r = 0.3 # 通常の辺(円柱)半径
ring_edge_r = 0.8 # 赤道リングの辺(円柱)半径
node_r = 0.4 # 通常の頂点(球)半径
ring_node_r = 0.6 # 赤道リング上の頂点(球)半径(任意)
level = 3 # 再帰分割レベル
fn_val = 24 # 円・球の滑らかさ
eps = 1e-9 # 数値保護
equator_tol = 0.02 # 赤道判定: |z| <= tol を赤道扱い
q_digit = 6 # 重複除去用の丸め桁
# --- ベクトルヘルパー ---
def vadd(a, b): return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
def vsub(a, b): return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
def vscale(v, s): return [v[0] * s, v[1] * s, v[2] * s]
def vlen(v): return sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
def midpoint(a, b):
return vscale(vadd(a, b), 0.5)
def clamp(x, lo, hi):
return max(lo, min(hi, x))
def project_to_sphere(p, R):
L = vlen(p)
if L < eps:
return [0.0, 0.0, 0.0]
return vscale(p, R / L)
def pkey(p):
return (round(p[0], q_digit), round(p[1], q_digit), round(p[2], q_digit))
def edgekey(a, b):
ka, kb = pkey(a), pkey(b)
return (ka, kb) if ka <= kb else (kb, ka)
def is_on_equator(p, tol):
return abs(p[2]) <= tol
# --- 2点間の円柱(Z軸円柱を yaw+pitch で方向合わせ) ---
def gen_cylinder(pA, pB, r, fn):
v = vsub(pB, pA)
h = vlen(v)
mid = midpoint(pA, pB)
if h < eps:
return translate(sphere(r=r, fn=fn), mid)
vx, vy, vz = v[0] / h, v[1] / h, v[2] / h
pitch = acos(clamp(vz, -1.0, 1.0))
yaw = 0.0 if (abs(vx) < eps and abs(vy) < eps) else atan2(vy, vx)
pitch_deg = pitch * 180.0 / pi
yaw_deg = yaw * 180.0 / pi
cyl0 = cylinder(h=h, r=r, center=True, fn=fn)
cyl1 = rotate(cyl0, [0.0, pitch_deg, 0.0])
cyl2 = rotate(cyl1, [0.0, 0.0, yaw_deg])
return translate(cyl2, mid)
# --- 再帰分割:重複しない「辺」と「点」を収集 ---
def collect_wire(p0, p1, p2, lv, segs_dict, nodes_dict):
R = vlen(p0)
p01 = project_to_sphere(midpoint(p0, p1), R)
p12 = project_to_sphere(midpoint(p1, p2), R)
p20 = project_to_sphere(midpoint(p2, p0), R)
if lv > 0:
collect_wire(p01, p12, p20, lv - 1, segs_dict, nodes_dict)
collect_wire(p0, p01, p20, lv - 1, segs_dict, nodes_dict)
collect_wire(p1, p01, p12, lv - 1, segs_dict, nodes_dict)
collect_wire(p2, p12, p20, lv - 1, segs_dict, nodes_dict)
return
for a, b in [(p0, p1), (p1, p2), (p2, p0)]:
ek = edgekey(a, b)
if ek not in segs_dict:
segs_dict[ek] = (a, b)
for p in [p0, p1, p2]:
pk = pkey(p)
if pk not in nodes_dict:
nodes_dict[pk] = p
# --- メイン:上半球(+Z側)を正八面体の上半分4三角形から開始 ---
p_top = [0.0, 0.0, R0]
p_x = [R0, 0.0, 0.0]
p_y = [0.0, R0, 0.0]
p_nx = [-R0, 0.0, 0.0]
p_ny = [0.0, -R0, 0.0]
base_tris = [
(p_top, p_x, p_y),
(p_top, p_y, p_nx),
(p_top, p_nx, p_ny),
(p_top, p_ny, p_x),
]
segments = {}
nodes = {}
for (a, b, c) in base_tris:
collect_wire(a, b, c, level, segments, nodes)
# ---- 色分けして組み立て(通常=青、赤道=forestgreen) ----
normal_obj = None
ring_obj = None
# 辺(円柱)
for (a, b) in segments.values():
is_ring = is_on_equator(a, equator_tol) and is_on_equator(b, equator_tol)
r_seg = ring_edge_r if is_ring else edge_r
seg = gen_cylinder(a, b, r_seg, fn_val)
if is_ring:
ring_obj = seg if ring_obj is None else (ring_obj + seg)
else:
normal_obj = seg if normal_obj is None else (normal_obj + seg)
# 頂点(球)
for p in nodes.values():
is_ring = is_on_equator(p, equator_tol)
r_n = ring_node_r if is_ring else node_r
nd = translate(sphere(r=r_n, fn=fn_val), p)
if is_ring:
ring_obj = nd if ring_obj is None else (ring_obj + nd)
else:
normal_obj = nd if normal_obj is None else (normal_obj + nd)
# 色を付けて合成
colored_normal = color(normal_obj, "SteelBlue") if normal_obj is not None else None
colored_ring = color(ring_obj, "forestgreen") if ring_obj is not None else None
if colored_normal is None:
result = colored_ring
elif colored_ring is None:
result = colored_normal
else:
result = colored_normal + colored_ring
show(result)
Geodesic 1V 2/3 Icosahedron Dome(フレームモデル)
資料を基に、geodisic domeを作成してください。
The Icosahedron
Uniform Polyhedron: U22
Platonic Solid
Platonic Element: Water
Vertices: 12
Edges: 30
Faces: 20
Wythoff symbol: 5|2 3
Symmetry Group: icosahedral
Vertex Configuration: {3, 3, 3, 3, 3}
Dual: dodecahedron
V: s3 * 5/12 * (3 + √5)
A: s2 * 20 / 4 * √3
rinner: s / 12 * (3 * √3 + √15)
router: s / 4 * √(10 + 2 * √5)
s = 1.00
, V = 2.18
, A = 8.66
, rinner = 0.76
, router = 0.95
, ravg = 0.86
1V/L1 2/3 Icosahedron Dome
Geodesic 1V 2/3 Icosahedron Dome (front view)
Geodesic 1V 2/3 Icosahedron Dome (bird view)
vertices/connectors: 11
5 x 4-way
6 x 5-way
1V 2/3 Icosahedron Dome Construction Map
edges/struts & bending angles (αstrut):
A x 25: 1.05146 (31.72°)
faces: 15 (3-sided)
A-A-A x 15 (60.00°, 60.00°, 60.00°)
diameter: 2.000, radius: 1.000
height: 1.447 or 72.36% of diameter
1V 2/3 Icosahedron Dome Calculator
1. 完成コード(PythonSCAD)
# ==========================================
# Geodesic 1V 2/3 Icosahedron Dome(フレームモデル)
# 作成日: 2026/08/20
# ==========================================
from pythonscad import *
from math import pi, sin, cos, sqrt
# --- パラメータ設定 (単位: mm) ---
dome_diameter = 200.0 # ドームの「底面外接円」直径(資料の diameter=2.0 を mm にスケール)
strut_r = 2.2 # ストラット(棒)の半径
node_r = 3.2 # 接続ノード(球)の半径(見た目用)
fn_val = 60 # 円・球の分割数
eps = 0.1 # チラつき防止用微小値
# --- 幾何ユーティリティ ---
def v_add(a, b):
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
def v_sub(a, b):
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
def v_mul(a, k):
return [a[0] * k, a[1] * k, a[2] * k]
def v_dot(a, b):
return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
def v_cross(a, b):
return [
a[1]*b[2] - a[2]*b[1],
a[2]*b[0] - a[0]*b[2],
a[0]*b[1] - a[1]*b[0],
]
def v_len(a):
return sqrt(v_dot(a, a))
def v_norm(a):
L = v_len(a)
if L < 1e-12:
return [0.0, 0.0, 0.0]
return [a[0]/L, a[1]/L, a[2]/L]
def clamp(x, lo, hi):
return max(lo, min(hi, x))
def rodrigues_rotate(p, axis_unit, angle_rad):
"""
軸(axis_unit)周りに angle_rad 回転(ロドリゲスの回転公式)
"""
k = axis_unit
c = cos(angle_rad)
s = sin(angle_rad)
term1 = v_mul(p, c)
term2 = v_mul(v_cross(k, p), s)
term3 = v_mul(k, v_dot(k, p) * (1.0 - c))
return v_add(v_add(term1, term2), term3)
def rotate_from_vec_to_z(points, v_from):
"""
v_from の向きを +Z に揃える回転を、全点に適用する。
"""
z_axis = [0.0, 0.0, 1.0]
a = v_norm(v_from)
b = z_axis
# a -> b の回転軸と角度
axis = v_cross(a, b)
axis_len = v_len(axis)
# ほぼ同じ向きなら回転なし
if axis_len < 1e-10:
# 逆向き(180度)の場合だけ特別処理
if v_dot(a, b) < 0.0:
# 任意の直交軸(X軸)で180度回転
axis_unit = [1.0, 0.0, 0.0]
ang = pi
return [rodrigues_rotate(p, axis_unit, ang) for p in points]
return points
axis_unit = v_mul(axis, 1.0 / axis_len)
ang = acos_safe(v_dot(a, b))
return [rodrigues_rotate(p, axis_unit, ang) for p in points]
def acos_safe(x):
# 数値誤差で 1.00000002 とかになる事故を防ぐ
return acos_approx(clamp(x, -1.0, 1.0))
def acos_approx(x):
# math.acos を使いたいが、import 条件を増やさずに安全にするため近似ではなく acos を追加インポートするのが本来。
# ただし本教材では「必要なものを正しく import する」が大事なので、ここで math.acos を明示的に使う。
from math import acos
return acos(x)
# --- 形状生成 ---
def icosahedron_vertices():
"""
標準的な正二十面体の頂点(黄金比)を返す。
※この時点では「頂点がZ軸上に1つだけ来る向き」ではないので、後で回転します。
"""
phi = (1.0 + sqrt(5.0)) / 2.0
pts = [
[0.0, 1.0, phi], # 0
[0.0, -1.0, phi], # 1
[0.0, 1.0, -phi], # 2
[0.0, -1.0, -phi], # 3
[1.0, phi, 0.0], # 4
[-1.0, phi, 0.0], # 5
[1.0, -phi, 0.0], # 6
[-1.0, -phi, 0.0], # 7
[phi, 0.0, 1.0], # 8
[-phi, 0.0, 1.0], # 9
[phi, 0.0, -1.0], # 10
[-phi, 0.0, -1.0], # 11
]
return pts
def icosahedron_faces():
"""
上の頂点順に対応した三角形面(20枚)
"""
return [
[0, 1, 8], [0, 8, 4], [0, 4, 5], [0, 5, 9], [0, 9, 1],
[1, 9, 7], [1, 7, 6], [1, 6, 8],
[2, 3, 11], [2, 10, 3], [2, 5, 4], [2, 11, 5], [2, 4, 10],
[3, 6, 7], [3, 7, 11], [3, 10, 6],
[4, 8, 10], [5, 11, 9], [6, 10, 8], [7, 9, 11],
]
def dome_faces_2_3(pts, faces):
"""
2/3ドーム(=底の5面を除いた15面)
→ 最下点(bottom vertex)を含む面を取り除く
"""
z_list = [p[2] for p in pts]
bottom_i = z_list.index(min(z_list))
keep = []
removed = []
for f in faces:
if bottom_i in f:
removed.append(f)
else:
keep.append(f)
return keep, removed, bottom_i
def base_ring_indices(removed_faces, bottom_i):
"""
開口部(底の輪)を作る5頂点のインデックスを取得
removed_faces は bottom_i を含む5面のはず
"""
ring = set()
for f in removed_faces:
for idx in f:
if idx != bottom_i:
ring.add(idx)
return sorted(list(ring))
def scale_to_base_diameter(pts, ring_idx, target_base_diameter):
"""
底の外接円(XY半径の最大)を target_base_diameter に合わせて全体をスケール
"""
r0 = 0.0
for i in ring_idx:
x, y, _z = pts[i]
r0 = max(r0, sqrt(x*x + y*y))
if r0 < 1e-12:
return pts, 1.0
target_r = target_base_diameter / 2.0
s = target_r / r0
return [v_mul(p, s) for p in pts], s
def make_node(p, r, fn):
return translate(sphere(r=r, fn=fn), p)
def make_strut(p1, p2, r, fn):
"""
任意方向の棒:
cylinder の回転合わせは高校生には重いので、
2つの球を hull でつないで「カプセル形状」にする(頑丈&簡単)
"""
a = make_node(p1, r, fn)
b = make_node(p2, r, fn)
return hull(a, b)
def unique_edges_from_faces(faces):
edges = set()
for f in faces:
a, b, c = f[0], f[1], f[2]
for i, j in [(a, b), (b, c), (c, a)]:
if i > j:
i, j = j, i
edges.add((i, j))
return sorted(list(edges))
def build_dome_frame(points, faces, strut_radius, node_radius, fn):
# ストラット
edges = unique_edges_from_faces(faces)
frame = None
for (i, j) in edges:
s = make_strut(points[i], points[j], strut_radius, fn)
frame = s if frame is None else (frame + s)
# ノード(見た目+強度)
# 2/3ドーム仕様の「4-way, 5-way」に相当する頂点が見える
nodes = None
used = set()
for f in faces:
for idx in f:
used.add(idx)
for idx in sorted(list(used)):
n = make_node(points[idx], node_radius, fn)
nodes = n if nodes is None else (nodes + n)
return frame + nodes
# --- メイン処理 ---
# 1) 正二十面体の頂点・面
pts = icosahedron_vertices()
faces = icosahedron_faces()
# 2) 「1頂点が天頂になる向き」へ回転(資料の“ドーム”らしい向きにする)
# ここでは頂点0を天頂に選んで +Z に合わせる
pts = rotate_from_vec_to_z(pts, pts[0])
# 3) 2/3ドーム:底の5面を除去(→ 15面)
keep_faces, removed_faces, bottom_i = dome_faces_2_3(pts, faces)
# 4) 底の輪(5頂点)を取り出し、底面直径を dome_diameter に合わせてスケール
ring_idx = base_ring_indices(removed_faces, bottom_i)
pts_mm, scale_val = scale_to_base_diameter(pts, ring_idx, dome_diameter)
# 5) フレーム生成(ストラット+ノード)
result = build_dome_frame(pts_mm, keep_faces, strut_r, node_r, fn_val)
# 6) 色(任意)
result = color(result, "skyblue")
show(result)
2. どう「資料」を使っているか
- **正二十面体(Icosahedron)**を作り、底側の5面だけ除去して 15面にしています
→ 資料の「faces: 15 (3-sided)」に一致 - 「1V 2/3 dome」なので、分割(Frequency 2,3…)はしていません
→ すべて同一長のストラットになるのが1Vの特徴 - 資料の「diameter: 2.000, radius: 1.000」を底面外接円直径の基準とみなし、
dome_diameterに合わせて自動スケールしています
参考資料









