1
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?

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)

スクリーンショット 2026-08-26 104824.png

変換時の重要ポイント

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

スクリーンショット 2026-08-26 105315.png

2. 変換のポイント(超重要だけ)

  • OpenSCAD の $fn=60; は PythonSCAD では書けないので、fn_val=60 を作って circle/cylinderfn=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)

スクリーンショット 2026-08-26 110808.png

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)

スクリーンショット 2026-08-26 111404.png

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)

スクリーンショット 2026-08-26 112033.png

2. 変更点(何をして“赤道だけ太く”している?)

  • 辺を作る直前に、両端点が どちらも Z≈0(赤道上) なら ring_edge_r を使う、そうでなければ edge_r を使う、という分岐にしています
  • 赤道判定の精度は equator_tol で調整します(分割レベルを上げると点が増えるので、基本は小さめが安全)
    スクリーンショット 2026-08-26 112015.png

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)

スクリーンショット 2026-08-26 112511.png


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)

スクリーンショット 2026-08-26 113454.png


2. どう「資料」を使っているか

  • **正二十面体(Icosahedron)**を作り、底側の5面だけ除去して 15面にしています
    → 資料の「faces: 15 (3-sided)」に一致
  • 「1V 2/3 dome」なので、分割(Frequency 2,3…)はしていません
    → すべて同一長のストラットになるのが1Vの特徴
  • 資料の「diameter: 2.000, radius: 1.000」を底面外接円直径の基準とみなし、dome_diameter に合わせて自動スケールしています

スクリーンショット 2026-08-26 113538.png

スクリーンショット 2026-08-26 113554.png

スクリーンショット 2026-08-26 113613.png

参考資料

1
0
0

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
1
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?