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コミュニティ・開発者に感謝申し上げます。ありがとうございます。

本プログラムのシーン構成および曲線の方程式は、以下の素晴らしい作品・解説を
参考に(インスパイアされて)PythonSCADコードとして新たに再構築したものです。
素敵な知見を共有してくださった作者様に深く感謝いたします。
ありがとうございます。

01. 鉢の断面を、r=R+D×sin(mθ) の式で波打たせて。mが波の数、Dが波の深さ。今回はm=8で、縁に近づくほど波が強くなるようにして。壁は3mm、底は平ら、原点は底面の中心。単位はmmで、閉じた一体の形にして

スクリーンショット 2026-09-11 100605.png

1. 完成コード(PythonSCAD)

# ==========================================
# 波打ち断面の鉢
# 作成日: 2026/08/27
# ==========================================

from pythonscad import *
from math import pi, sin, cos

# --- パラメータ設定 (単位: mm) ---
pot_height       = 70.0    # 鉢の高さ Z方向
base_radius      = 28.0    # 底付近の基本半径 R
rim_radius       = 48.0    # 縁付近の基本半径 R
wall_thickness   = 3.0     # 壁厚
bottom_thickness = 3.0     # 底の厚み

wave_count       = 8       # m:波の数
wave_depth       = 6.0     # D:縁での最大波深さ
wave_power       = 1.8     # 大きいほど、縁に近い部分だけ波が強くなる

theta_segments   = 192     # 円周方向の分割数
height_segments  = 36      # 高さ方向の分割数

eps              = 0.1     # 安全用の微小値


# --- 関数(モジュール)定義 ---

def lerp(a, b, t):
    """線形補間"""
    return a + (b - a) * t


def base_profile_radius(z):
    """
    高さ z における基本半径 R(z)
    底から縁に向かって少し広がる鉢形状にする
    """
    t = z / pot_height
    return lerp(base_radius, rim_radius, t)


def wave_amplitude(z):
    """
    高さ z における波の強さ D(z)
    縁に近づくほど波が強くなる
    """
    t = z / pot_height
    return wave_depth * (t ** wave_power)


def outer_radius(theta, z):
    """
    外側表面の半径
    r = R + D * sin(mθ)
    ただし R と D は高さ z によって変化する
    """
    r_base = base_profile_radius(z)
    d_wave = wave_amplitude(z)
    return r_base + d_wave * sin(wave_count * theta)


def inner_radius(theta, z):
    """
    内側表面の半径
    外側から壁厚 3mm 分だけ内側に入れる
    """
    return outer_radius(theta, z) - wall_thickness


def create_wavy_pot():
    """
    波打つ鉢を、閉じた一体の polyhedron として生成する
    原点は底面中心、底面は Z=0
    """

    points = []
    faces = []

    outer_idx = []
    inner_idx = []

    # --- 外側リングを作成 ---
    for iz in range(height_segments + 1):
        z = pot_height * iz / height_segments
        row = []

        for it in range(theta_segments):
            theta = 2 * pi * it / theta_segments
            r = outer_radius(theta, z)

            x = r * cos(theta)
            y = r * sin(theta)

            points.append([x, y, z])
            row.append(len(points) - 1)

        outer_idx.append(row)

    # --- 内側リングを作成 ---
    # 内側は底板の上面 bottom_thickness から始める
    for iz in range(height_segments + 1):
        z = bottom_thickness + (pot_height - bottom_thickness) * iz / height_segments
        row = []

        for it in range(theta_segments):
            theta = 2 * pi * it / theta_segments
            r = inner_radius(theta, z)

            x = r * cos(theta)
            y = r * sin(theta)

            points.append([x, y, z])
            row.append(len(points) - 1)

        inner_idx.append(row)

    # --- 中心点 ---
    bottom_center_idx = len(points)
    points.append([0, 0, 0])

    inner_floor_center_idx = len(points)
    points.append([0, 0, bottom_thickness])

    # --- 外側側面 ---
    for iz in range(height_segments):
        for it in range(theta_segments):
            nt = (it + 1) % theta_segments

            faces.append([
                outer_idx[iz][it],
                outer_idx[iz][nt],
                outer_idx[iz + 1][nt],
                outer_idx[iz + 1][it]
            ])

    # --- 内側側面 ---
    for iz in range(height_segments):
        for it in range(theta_segments):
            nt = (it + 1) % theta_segments

            faces.append([
                inner_idx[iz][it],
                inner_idx[iz + 1][it],
                inner_idx[iz + 1][nt],
                inner_idx[iz][nt]
            ])

    # --- 上端のリム部分 ---
    outer_top = outer_idx[-1]
    inner_top = inner_idx[-1]

    for it in range(theta_segments):
        nt = (it + 1) % theta_segments

        faces.append([
            outer_top[it],
            outer_top[nt],
            inner_top[nt],
            inner_top[it]
        ])

    # --- 底面の外側、Z=0の平らな面 ---
    outer_bottom = outer_idx[0]

    for it in range(theta_segments):
        nt = (it + 1) % theta_segments

        faces.append([
            bottom_center_idx,
            outer_bottom[nt],
            outer_bottom[it]
        ])

    # --- 鉢の内側底面、Z=bottom_thickness の平らな面 ---
    inner_floor = inner_idx[0]

    for it in range(theta_segments):
        nt = (it + 1) % theta_segments

        faces.append([
            inner_floor_center_idx,
            inner_floor[it],
            inner_floor[nt]
        ])

    return polyhedron(points=points, faces=faces)


# --- メイン処理 ---

pot = create_wavy_pot()

# 色付け
result = color(pot, "peru")

# 3Dプレビュー表示
show(result)

2. 形状のポイント

この鉢は、円周方向の半径を次の式で変化させています。

$$
r = R + D \times \sin(m\theta)
$$

今回は、

wave_count = 8

なので、鉢の周囲に 8個の波 ができます。

また、波の深さ D は高さによって変化させています。

D(z) = wave_depth * (z / pot_height) ** wave_power

つまり、底ではほぼ波がなく、上の縁に近づくほど波が強くなります。


3. 設計上の注意

  • 原点は 底面中心 [0, 0, 0]
  • 底面は Z=0 に配置
  • 壁厚は 3mm
  • 底厚も 3mm
  • ブーリアン演算ではなく、最初から閉じた一体の polyhedron として生成
  • 上は鉢として開口していますが、リム・内壁・底面がつながった一体形状です

3Dプリントする場合は、波の深さ wave_depth を大きくしすぎると薄い部分ができやすいので、まずは 4.0〜6.0mm くらいが安全です。


02. 浅めの広口ボウルを作って。口径120mm、高さ70mm。側面を12回くり返す波でリブにして。リブははっきり尖らせて、テラコッタ色のマットな質感で

1. 完成コード(PythonSCAD)

# ==========================================
# 浅めの広口リブ付きボウル
# 作成日: 2026/09/10
# ==========================================

from pythonscad import *
from math import pi, sin, cos

# --- パラメータ設定 (単位: mm) ---
mouth_diameter      = 120.0   # 口径:リブの先端を含む最大直径
height              = 70.0    # ボウル高さ
wall_thickness      = 3.0     # 側面の厚み
bottom_thickness    = 4.0     # 底の厚み

rib_count           = 12      # 側面の波・リブの数
rib_amplitude       = 4.0     # リブの出っ張り量:大きいほど尖りが目立つ
z_steps             = 28      # 高さ方向の分割数
theta_steps         = rib_count * 2  # 谷と山を交互に作り、尖ったリブにする

top_outer_r_tip     = mouth_diameter / 2.0
top_outer_r_mean    = top_outer_r_tip - rib_amplitude
bottom_outer_r_mean = 32.0    # 底側の平均半径

terracotta_color    = "#C65A3A"  # テラコッタ系の色


# --- 関数(モジュール)定義 ---

def triangular_wave(angle, ribs):
    """
    角度方向に三角波を作る。
    sin波ではなく三角波にすることで、リブの山がはっきり尖る。
    戻り値は -1.0 ~ +1.0。
    """
    cycle = (angle / (2.0 * pi) * ribs) % 1.0
    return 1.0 - 4.0 * abs(cycle - 0.5)


def outer_radius_at(z, angle):
    """
    外側半径を計算する。
    高さ方向に広がるボウル形状 + 角度方向の12本リブを合成する。
    """
    t = z / height

    # 底から口元に向かって広がるカーブ
    flare = t ** 0.72
    smooth_r = bottom_outer_r_mean + (top_outer_r_mean - bottom_outer_r_mean) * flare

    # リブは底で少し弱く、口元で強くする
    rib_strength = 0.35 + 0.65 * t
    rib = rib_amplitude * rib_strength * triangular_wave(angle, rib_count)

    return smooth_r + rib


def create_ribbed_bowl():
    """
    polyhedronで、外面・内面・底面・口元リムを持つ
    中空の広口ボウルを作成する。
    """
    points = []
    faces = []

    # --- 外側リングの点 ---
    outer_start = 0
    for j in range(z_steps + 1):
        z = height * j / z_steps
        for i in range(theta_steps):
            angle = 2.0 * pi * i / theta_steps
            r = outer_radius_at(z, angle)
            x = r * cos(angle)
            y = r * sin(angle)
            points.append([x, y, z])

    # --- 内側リングの点 ---
    inner_start = len(points)
    for j in range(z_steps + 1):
        z = bottom_thickness + (height - bottom_thickness) * j / z_steps
        for i in range(theta_steps):
            angle = 2.0 * pi * i / theta_steps
            r = outer_radius_at(z, angle) - wall_thickness
            x = r * cos(angle)
            y = r * sin(angle)
            points.append([x, y, z])

    bottom_center_index = len(points)
    points.append([0.0, 0.0, 0.0])

    inner_bottom_center_index = len(points)
    points.append([0.0, 0.0, bottom_thickness])

    def outer_idx(j, i):
        return outer_start + j * theta_steps + (i % theta_steps)

    def inner_idx(j, i):
        return inner_start + j * theta_steps + (i % theta_steps)

    # --- 外側の側面 ---
    for j in range(z_steps):
        for i in range(theta_steps):
            faces.append([
                outer_idx(j, i),
                outer_idx(j, i + 1),
                outer_idx(j + 1, i + 1),
                outer_idx(j + 1, i)
            ])

    # --- 内側の側面 ---
    for j in range(z_steps):
        for i in range(theta_steps):
            faces.append([
                inner_idx(j, i),
                inner_idx(j + 1, i),
                inner_idx(j + 1, i + 1),
                inner_idx(j, i + 1)
            ])

    # --- 口元のリム:外側と内側をつなぐ ---
    top_j = z_steps
    for i in range(theta_steps):
        faces.append([
            outer_idx(top_j, i),
            outer_idx(top_j, i + 1),
            inner_idx(top_j, i + 1),
            inner_idx(top_j, i)
        ])

    # --- 底の外側下面 ---
    for i in range(theta_steps):
        faces.append([
            bottom_center_index,
            outer_idx(0, i + 1),
            outer_idx(0, i)
        ])

    # --- ボウル内側の底面 ---
    for i in range(theta_steps):
        faces.append([
            inner_bottom_center_index,
            inner_idx(0, i),
            inner_idx(0, i + 1)
        ])

    return polyhedron(points, faces)


# --- メイン処理 ---

# 1. ボウル形状の生成
bowl = create_ribbed_bowl()

# 2. テラコッタ色を付ける
#    マット感は3Dプリント時のフィラメントやレンダリング設定で調整するとよいです。
result = color(bowl, terracotta_color)

# 3. 3Dプレビュー表示
show(result)

スクリーンショット 2026-09-11 100735.png

ポイント

  • rib_count = 12 で、側面に12回くり返す波を作っています。
  • 通常の sin 波ではなく、三角波を使っているので、リブの山がはっきり尖ります。
  • mouth_diameter = 120.0 は、リブの出っ張り先端を含めた最大口径として設定しています。
  • テラコッタ色は "#C65A3A" で指定しています。実際に「マット」に見せたい場合は、3Dプリントではマット系フィラメント、レンダリングでは粗さの高い材質設定を使うと雰囲気が出ます。

03. 直径90mm・高さ8mmのコースターを作って。外周に幅3mmのふちを立てて、床は厚さ3mm。床の上に、数式で決めた曲線に沿って高さ5mm・幅2.4mm(ハーモノグラフだけは2.2mm)のリブを立てて。曲線はリサージュ・バラ曲線・トロコイド・ハーモノグラフの4種類。単位はmm、原点は底面中心。ブーリアンは使わないで、面を直接張って作って

1. 完成コード(PythonSCAD)

# ==========================================
# 4種の数式曲線コースター(交差エラー対策版)
# 作成日: 2026/08/28
# ==========================================

from pythonscad import *
from math import pi, sin, cos, sqrt

# --- パラメータ設定 (単位: mm) ---
coaster_r = 45        # コースターの半径 (直径90mm)
total_h   = 8         # 全体の高さ
floor_h   = 3         # 床の厚み
rim_w     = 3         # ふちの幅
rib_h     = 5         # リブ(曲線)の高さ

fn_val    = 120       # 円や曲線の滑らかさ(分割数)

# --- 関数(モジュール)定義 ---

# 1. コースターの土台(回転押し出し)
def make_coaster_base():
    profile = [
        [0, 0],
        [coaster_r, 0],
        [coaster_r, total_h],
        [coaster_r - rim_w, total_h],
        [coaster_r - rim_w, floor_h],
        [0, floor_h]
    ]
    return rotate_extrude(polygon(profile), fn=fn_val)

# 2. 曲線の座標生成
def generate_curve_points(curve_name, steps=180):
    points = []
    for i in range(steps):
        t = i * 2 * pi / steps
        
        if curve_name == "lissajous":
            # 【修正】はみ出さないよう振幅を 35 -> 30 に縮小
            x = 30 * sin(3 * t + pi / 2)
            y = 30 * sin(4 * t)
            
        elif curve_name == "rose":
            r_val = 38 * cos(5 * t)
            x = r_val * cos(t)
            y = r_val * sin(t)
            
        elif curve_name == "trochoid":
            x = 16 * cos(t) + 16 * cos(2 * t)
            y = 16 * sin(t) - 16 * sin(2 * t)
            
        elif curve_name == "harmonograph":
            x = 25 * sin(3 * t) + 10 * sin(10 * t)
            y = 25 * cos(3 * t) + 10 * cos(10 * t)
            
        points.append([x, y])
    return points

# 3. 【新方式】自己交差に強い、四角形の連続によるリボン生成
def make_robust_ribbon(points, w):
    n = len(points)
    segments = []
    
    # 2点間を結ぶ「小さな四角形のタイル」を180枚作る
    for i in range(n):
        p1 = points[i]
        p2 = points[(i + 1) % n]  # 最後の点は最初の点に繋ぐ
        
        dx = p2[0] - p1[0]
        dy = p2[1] - p1[1]
        
        length = sqrt(dx * dx + dy * dy)
        if length == 0:
            continue
            
        # 進行方向に対して垂直なベクトル(法線)
        nx = -dy / length
        ny = dx / length
        
        # 線の幅(w)を持たせた4つの角の座標を計算
        vx1 = p1[0] + nx * w / 2
        vy1 = p1[1] + ny * w / 2
        vx2 = p1[0] - nx * w / 2
        vy2 = p1[1] - ny * w / 2
        
        vx3 = p2[0] - nx * w / 2
        vy3 = p2[1] - ny * w / 2
        vx4 = p2[0] + nx * w / 2
        vy4 = p2[1] + ny * w / 2
        
        # 1区間分の四角形ポリゴンを作成し、リストに保存
        seg = polygon([[vx1, vy1], [vx2, vy2], [vx3, vy3], [vx4, vy4]])
        segments.append(seg)
        
    # 全ての小さな四角形を + 演算子で結合し、1つの2D形状にする
    result = segments[0]
    for s in segments[1:]:
        result = result + s
        
    return result

# 4. 指定した曲線のコースターを組み立てる
def make_coaster(curve_name):
    base = make_coaster_base()
    pts = generate_curve_points(curve_name, steps=180)
    width = 2.2 if curve_name == "harmonograph" else 2.4
    
    # 新しいリボン生成関数で2D図形を作り、指定の高さへ押し出し
    rib_2d = make_robust_ribbon(pts, width)
    rib_3d = linear_extrude(rib_2d, height=rib_h)
    
    # リブをZ方向(上)へ持ち上げて床の上に乗せ、結合
    rib_placed = translate(rib_3d, [0, 0, floor_h])
    
    return base + rib_placed

# --- メイン処理 ---

# 4つのコースターを生成
c1 = make_coaster("lissajous")
c2 = make_coaster("rose")
c3 = make_coaster("trochoid")
c4 = make_coaster("harmonograph")

# X・Y方向にズラして配置
pos1 = translate(c1, [-50,  50, 0])
pos2 = translate(c2, [ 50,  50, 0])
pos3 = translate(c3, [-50, -50, 0])
pos4 = translate(c4, [ 50, -50, 0])

# 全てを結合してプレビュー
result = pos1 + pos2 + pos3 + pos4
show(result)

スクリーンショット 2026-09-11 102308.png

ポイント

  • 原点は 底面中心 [0, 0, 0] です
  • Z方向は
    • 床:0〜3mm
    • リブ:床上から 5mm
    • 外周ふち:0〜8mm
  • difference()union() は使わず、polyhedron(points=..., faces=...) で面を直接張っています
  • 4種類すべてを入れていますが、1種類だけにしたい場合は次の部分を書き換えます
patterns_to_use = ["lissajous"]

使える名前は以下です。

"lissajous"
"rose"
"trochoid"
"harmonograph"

04. 六角断面の花瓶(ねじり 0°)

花瓶

断面 六角
ねじり 0°

形
ふくらみ 1.00
ふくらみの位置 0.42
口のすぼまり 0.78

寸法
高さ 150mm
半径 45mm
肉厚 2.4mm
幅 90mm

1. 完成コード(PythonSCAD)

# ==========================================
# 六角断面の花瓶(ねじり 0°)
# 作成日: 2026/08/28
# ==========================================

from pythonscad import *
from math import pi, cos

# --- パラメータ設定 (単位: mm) ---
height              = 150.0   # 花瓶の高さ(Z方向)
outer_radius        = 45.0    # 最大外半径(六角形の中心から頂点まで)
outer_width         = 90.0    # 最大幅(頂点から頂点までの目安)
wall_thickness      = 2.4     # 肉厚
bottom_thickness    = 2.4     # 底の厚み

hex_sides           = 6       # 六角断面
twist_angle_deg     = 0.0     # ねじり角度
bulge_amount        = 1.00    # ふくらみ量
bulge_position      = 0.42    # ふくらみ位置(高さに対する割合)
mouth_ratio         = 0.78    # 口のすぼまり比率

slice_count         = 48      # 高さ方向の分割数
eps                 = 0.1     # くり抜き用の微小マージン

# --- 派生パラメータ ---
mouth_radius = outer_radius * mouth_ratio

# 底側は少しだけすぼめると花瓶らしく安定した形になります
base_radius_ratio = 0.86
base_radius = outer_radius * base_radius_ratio

# 六角形の「面に垂直な肉厚」を 2.4mm に近づけるための補正
# 六角形の内側半径は、単純に r - 肉厚 ではなく、
# 面からの距離を考えて wall_thickness / cos(30°) だけ小さくします。
hex_wall_offset = wall_thickness / cos(pi / 6)


# --- 関数(モジュール)定義 ---

def clamp01(x):
    """0〜1の範囲に収める"""
    if x < 0:
        return 0
    if x > 1:
        return 1
    return x


def smoothstep(x):
    """なめらかな変化を作る関数"""
    u = clamp01(x)
    return u * u * (3 - 2 * u)


def outer_profile_radius(z):
    """
    高さ z における外側半径を計算する。
    下から bulge_position までは最大半径へふくらみ、
    そこから上は口の半径へすぼまる。
    """
    t = clamp01(z / height)

    if t <= bulge_position:
        u = smoothstep(t / bulge_position)
        r = base_radius + (outer_radius - base_radius) * u * bulge_amount
    else:
        u = smoothstep((t - bulge_position) / (1.0 - bulge_position))
        r = outer_radius + (mouth_radius - outer_radius) * u

    return r


def inner_profile_radius(z):
    """
    高さ z における内側半径を計算する。
    六角形の面に対する肉厚が指定値に近くなるよう補正する。
    """
    r = outer_profile_radius(z) - hex_wall_offset

    # 安全対策:半径が小さくなりすぎないようにする
    if r < 1.0:
        r = 1.0

    return r


def create_hex_frustum(z1, z2, r1, r2):
    """
    六角形断面の円すい台を作る。
    cylinder の fn=6 にすることで六角柱・六角すい台になる。
    """
    seg_h = z2 - z1
    part = cylinder(h=seg_h, r1=r1, r2=r2, center=False, fn=hex_sides)

    # ねじり角度 0°なので、ここでは回転なし。
    # 将来 twist_angle_deg を使う場合は、分割ごとに少しずつ回転させる。
    part = rotate(part, [0, 0, 0])
    part = translate(part, [0, 0, z1])

    return part


def create_outer_body():
    """外側の花瓶形状(足す形状)を作る"""
    dz = height / slice_count

    body = None

    for i in range(slice_count):
        z1 = i * dz
        z2 = (i + 1) * dz

        r1 = outer_profile_radius(z1)
        r2 = outer_profile_radius(z2)

        part = create_hex_frustum(z1, z2, r1, r2)

        if body is None:
            body = part
        else:
            body = body + part

    return body


def create_inner_cutter():
    """
    内側をくり抜くカッターを作る。
    底を残すため、bottom_thickness より上だけ削る。
    上面は eps だけ突き抜けさせて、口を確実に開ける。
    """
    usable_height = height - bottom_thickness
    dz = usable_height / slice_count

    cutter = None

    for i in range(slice_count):
        z1 = bottom_thickness + i * dz
        z2 = bottom_thickness + (i + 1) * dz

        # 最上段は少し上へ突き抜けさせる
        if i == slice_count - 1:
            z2 = height + eps

        r1 = inner_profile_radius(z1)
        r2 = inner_profile_radius(min(z2, height))

        part = create_hex_frustum(z1, z2, r1, r2)

        if cutter is None:
            cutter = part
        else:
            cutter = cutter + part

    return cutter


# --- メイン処理 ---

# 1. 本体とカッターを作成
outer_body = create_outer_body()
inner_cutter = create_inner_cutter()

# 2. 一括くり抜き
vase = outer_body - inner_cutter

# 3. 色付け
result = color(vase, "ivory")

# 4. 3Dプレビュー表示
show(result)

スクリーンショット 2026-09-11 102748.png


2. ポイント解説

  • fn=6 の円柱を使うことで、断面が六角形の花瓶になります。
  • slice_count = 48 で高さ方向に細かく分割し、半径を少しずつ変えて「ふくらみ」と「口のすぼまり」を表現しています
  • 肉厚 2.4mm は、六角形の面に対してほぼ一定になるように補正しています
  • 底は bottom_thickness = 2.4 として残してあるので、花瓶として底付きになります
  • ねじりは twist_angle_deg = 0.0 なので、今回はまっすぐな六角花瓶です

05. 六角形の植木鉢

植木鉢
六角

形
上の広がり
60mm

下の狭まり
45mm

水抜き穴(半径)
6.0mm

寸法
高さ
130mm

肉厚
3.0mm

1. 完成コード(PythonSCAD)

# ==========================================
# 六角形の植木鉢
# 作成日: 2026/08/28
# ==========================================

from pythonscad import *

# --- パラメータ設定 (単位: mm) ---
top_width       = 60.0    # 上側の外寸(広がり)
bottom_width    = 45.0    # 下側の外寸(狭まり)
height          = 130.0   # 植木鉢の高さ
wall_thickness  = 3.0     # 肉厚
bottom_thickness = 3.0    # 底の厚み
drain_hole_r    = 6.0     # 水抜き穴の半径

fn_hex          = 6       # 六角形
eps             = 0.1     # ブーリアン演算のチラつき防止

# --- 半径計算 ---
# cylinder(fn=6) は六角柱・六角錐台として使えます。
# ここでは「幅」を外接円直径として扱い、半径 = 幅 / 2 とします。
top_r_outer    = top_width / 2
bottom_r_outer = bottom_width / 2

top_r_inner    = top_r_outer - wall_thickness
bottom_r_inner = bottom_r_outer - wall_thickness


# --- 関数定義 ---

def create_outer_pot():
    """外側の六角形テーパー形状"""
    outer = cylinder(
        h=height,
        r1=bottom_r_outer,
        r2=top_r_outer,
        center=False,
        fn=fn_hex
    )
    return outer


def create_cutters():
    """内側のくり抜きと水抜き穴をまとめたカッター"""

    # 内側をくり抜くカッター
    # 底板を bottom_thickness 分だけ残すため、Z方向に少し上げる
    inner_h = height - bottom_thickness + eps

    inner_cavity = cylinder(
        h=inner_h,
        r1=bottom_r_inner,
        r2=top_r_inner,
        center=False,
        fn=fn_hex
    )

    inner_cavity = translate(
        inner_cavity,
        [0, 0, bottom_thickness]
    )

    # 水抜き穴カッター
    # 底板を確実に貫通するように eps 分だけ上下に余裕を持たせる
    drain_hole = cylinder(
        h=bottom_thickness + 2 * eps,
        r=drain_hole_r,
        center=False,
        fn=60
    )

    drain_hole = translate(
        drain_hole,
        [0, 0, -eps]
    )

    # カッターは + でまとめ、最後に一括で引き算する
    cutters = inner_cavity + drain_hole

    return cutters


# --- メイン処理 ---

outer_pot = create_outer_pot()
cutters = create_cutters()

result = outer_pot - cutters

# 色をつける
result = color(result, "skyblue")

show(result)

スクリーンショット 2026-09-11 103419.png

2. ポイント

  • fn=6 の円柱を使うことで、六角形の植木鉢にしています
  • r1 が下側半径、r2 が上側半径なので、上に広がるテーパー形状になります
  • 内側は少し小さい六角形でくり抜き、肉厚 3.0mm を作っています
  • 底は bottom_thickness = 3.0 として残し、中央に半径 6.0mm の水抜き穴を開けています
  • ブーリアン演算は安全のため、くり抜きカッターをまとめてから outer_pot - cutters で一括処理しています

スクリーンショット 2026-09-11 103449.png


06. ルーローの三角形を作って。正三角形の各頂点を中心に、対辺を通る円弧で。幅60mmで、厚みのある“ころ”にして

1. 完成コード(PythonSCAD)

# ==========================================
# ルーローの三角形ころ
# 作成日: 2026/08/28
# ==========================================

from pythonscad import *
from math import sqrt

# --- パラメータ設定 (単位: mm) ---
width      = 60.0   # ルーロー三角形の幅(定幅)
thickness  = 20.0   # ころの厚み(Z方向)
fn_val     = 120    # 円弧のなめらかさ
eps        = 0.1    # 安全用

# --- 関数定義 ---

def create_reuleaux_2d(w, fn):
    """
    正三角形の各頂点を中心に、
    対辺の2頂点を通る円を3つ作り、
    その共通部分としてルーロー三角形を作る。
    """
    side = w
    tri_h = side * sqrt(3) / 2

    # 正三角形の3頂点を、XY平面で原点中心になるように配置
    p1 = [0,        2 * tri_h / 3]
    p2 = [-side/2, -tri_h / 3]
    p3 = [ side/2, -tri_h / 3]

    # 各頂点を中心に、半径 side の円を作る
    c1 = translate(circle(r=side, fn=fn), p1)
    c2 = translate(circle(r=side, fn=fn), p2)
    c3 = translate(circle(r=side, fn=fn), p3)

    # 3つの円の共通部分がルーローの三角形
    return intersection(c1, c2, c3)

def create_roller(w, t, fn):
    """厚みのあるルーロー三角形ころを生成"""
    profile_2d = create_reuleaux_2d(w, fn)
    roller = linear_extrude(profile_2d, height=t)
    return roller

# --- メイン処理 ---

body = create_roller(width, thickness, fn_val)

# 色を付けて表示
result = color(body, "skyblue")

show(result)

スクリーンショット 2026-09-11 103804.png

ポイント

  • ルーローの三角形は、正三角形の3つの頂点を中心にした同じ半径の円の共通部分で作れます
  • 今回は幅 60mm なので、正三角形の一辺も 60mm にしています
  • linear_extrude(..., height=thickness) で2D形状に厚みを付けて、“ころ”のような3D形状にしています
  • thickness = 20.0 を変えると、ころの厚みを変更できます

07. 高さ170mmの花瓶を作って。輪郭は底34mm・腰46mm・いちばん太いところ52mm・首33mm・口40mmの半径で、なめらかにつないで。壁は3mm。表面にボロノイの透かしを入れる。セルは90個、骨の幅は2.6mm。下から22mmと上から12mmは穴なしにして。ブーリアンは使わず、面を直接張って作って。単位はmm、原点は底面中心

1. 完成コード(PythonSCAD)

※ ブーリアン演算(+, -, union, difference)は使わず、polyhedron で面を直接張って作っています。
※ ボロノイ模様は、花瓶の側面をいったん「展開図」として考え、90個の点からできる境界線だけを骨として残す方式です。

# ==========================================
# ボロノイ透かし花瓶
# 作成日: 2026/08/28
# 単位: mm
# 原点: 底面中心
# ==========================================

from pythonscad import *
from math import pi, sin, cos, sqrt
import random

# --- パラメータ設定 (単位: mm) ---

vase_height = 170.0       # 花瓶の高さ
wall_thick  = 3.0         # 壁厚

# 輪郭半径
r_bottom = 34.0           # 底の半径
r_waist  = 46.0           # 腰の半径
r_belly  = 52.0           # いちばん太いところの半径
r_neck   = 33.0           # 首の半径
r_mouth  = 40.0           # 口の半径

# 輪郭の高さ位置
# 必要ならここを調整すると、花瓶のシルエットを変えられます
z_bottom = 0.0
z_waist  = 42.0
z_belly  = 82.0
z_neck   = 132.0
z_mouth  = vase_height

# ボロノイ透かし設定
voronoi_cell_count = 90   # セル数
bone_width = 2.6          # 骨の幅
bottom_solid_h = 22.0     # 下から穴なし
top_solid_h    = 12.0     # 上から穴なし

# メッシュ解像度
# 大きくすると滑らかになるが、処理は重くなります
theta_segments = 192      # 円周方向分割
z_segments     = 160      # 高さ方向分割

seed_value = 20260828     # ボロノイ模様を固定する乱数シード
body_color = "ivory"


# --- 関数(モジュール)定義 ---

def hermite_radius(z, profile):
    """
    指定した高さ z における外側半径を返す。
    Hermite補間で、底・腰・最大径・首・口をなめらかにつなぐ。
    """
    if z <= profile[0][0]:
        return profile[0][1]
    if z >= profile[-1][0]:
        return profile[-1][1]

    # 各制御点での傾き dr/dz を計算
    slopes = []
    for i in range(len(profile)):
        if i == 0:
            dz = profile[1][0] - profile[0][0]
            dr = profile[1][1] - profile[0][1]
        elif i == len(profile) - 1:
            dz = profile[-1][0] - profile[-2][0]
            dr = profile[-1][1] - profile[-2][1]
        else:
            dz = profile[i + 1][0] - profile[i - 1][0]
            dr = profile[i + 1][1] - profile[i - 1][1]
        slopes.append(dr / dz)

    # z が含まれる区間を探す
    for i in range(len(profile) - 1):
        z0 = profile[i][0]
        z1 = profile[i + 1][0]

        if z0 <= z <= z1:
            r0 = profile[i][1]
            r1 = profile[i + 1][1]
            m0 = slopes[i]
            m1 = slopes[i + 1]

            dz = z1 - z0
            t = (z - z0) / dz

            h00 = 2.0 * t**3 - 3.0 * t**2 + 1.0
            h10 = t**3 - 2.0 * t**2 + t
            h01 = -2.0 * t**3 + 3.0 * t**2
            h11 = t**3 - t**2

            r = h00 * r0 + h10 * dz * m0 + h01 * r1 + h11 * dz * m1

            # 内側半径が負にならないように安全確保
            return max(r, wall_thick + 1.0)

    return profile[-1][1]


def periodic_distance_x(x0, x1, period):
    """
    展開図の左右端がつながっているものとして、X方向の周期距離を返す。
    花瓶の円周方向は 0度 と 360度 が同じ位置なので、この処理が必要。
    """
    dx = abs(x0 - x1)
    return min(dx, period - dx)


def create_voronoi_seeds(count, width, z_min, z_max, seed_value):
    """
    ボロノイセルの元になる点を作る。
    width は展開図の横幅、z_min〜z_max が穴あけ可能エリア。
    """
    rng = random.Random(seed_value)
    seeds = []

    for _ in range(count):
        sx = rng.random() * width
        sz = z_min + rng.random() * (z_max - z_min)
        seeds.append([sx, sz])

    return seeds


def is_voronoi_bone(theta, z, seeds, unwrap_width, ref_radius):
    """
    その位置がボロノイ模様の「骨」に入るかを判定する。

    近い2つのシード点からの距離がほぼ同じ場所がボロノイ境界。
    その境界から bone_width/2 以内を骨として残す。
    """
    x = theta * ref_radius

    nearest_1 = None
    nearest_2 = None

    for seed in seeds:
        sx = seed[0]
        sz = seed[1]

        dx = periodic_distance_x(x, sx, unwrap_width)
        dz = z - sz
        d2 = dx * dx + dz * dz

        if nearest_1 is None or d2 < nearest_1[0]:
            nearest_2 = nearest_1
            nearest_1 = [d2, sx, sz]
        elif nearest_2 is None or d2 < nearest_2[0]:
            nearest_2 = [d2, sx, sz]

    if nearest_1 is None or nearest_2 is None:
        return False

    d1_sq = nearest_1[0]
    d2_sq = nearest_2[0]

    sx1 = nearest_1[1]
    sz1 = nearest_1[2]
    sx2 = nearest_2[1]
    sz2 = nearest_2[2]

    seed_dx = periodic_distance_x(sx1, sx2, unwrap_width)
    seed_dz = sz2 - sz1
    seed_dist = sqrt(seed_dx * seed_dx + seed_dz * seed_dz)

    if seed_dist <= 0.001:
        return False

    boundary_dist = abs(d2_sq - d1_sq) / (2.0 * seed_dist)

    return boundary_dist <= bone_width / 2.0


def should_keep_wall(theta, z, seeds, unwrap_width, ref_radius):
    """
    側面メッシュの小さな面を残すかどうかを判定する。
    下22mm、上12mmは穴なしの完全な壁にする。
    """
    if z <= bottom_solid_h:
        return True

    if z >= vase_height - top_solid_h:
        return True

    return is_voronoi_bone(theta, z, seeds, unwrap_width, ref_radius)


def add_quad(faces, a, b, c, d):
    """
    四角形面を追加する。
    polyhedron は四角形面も扱えるので、ここでは三角形分割せずに張る。
    """
    faces.append([a, b, c, d])


def create_vase_mesh():
    """
    花瓶を polyhedron 用の points と faces で直接作る。
    ブーリアン演算は使わない。
    """
    profile = [
        [z_bottom, r_bottom],
        [z_waist,  r_waist],
        [z_belly,  r_belly],
        [z_neck,   r_neck],
        [z_mouth,  r_mouth],
    ]

    # 展開図の基準半径
    # ボロノイの骨幅をmm感覚に近づけるため、平均的な半径を使う
    ref_radius = (r_bottom + r_waist + r_belly + r_neck + r_mouth) / 5.0
    unwrap_width = 2.0 * pi * ref_radius

    z_open_min = bottom_solid_h
    z_open_max = vase_height - top_solid_h

    seeds = create_voronoi_seeds(
        voronoi_cell_count,
        unwrap_width,
        z_open_min,
        z_open_max,
        seed_value
    )

    points = []
    faces = []

    outer_index = []
    inner_index = []

    # 頂点を作る
    for j in range(z_segments + 1):
        z = vase_height * j / z_segments
        r_outer = hermite_radius(z, profile)
        r_inner = max(r_outer - wall_thick, 0.1)

        outer_row = []
        inner_row = []

        for i in range(theta_segments):
            theta = 2.0 * pi * i / theta_segments

            x_outer = r_outer * cos(theta)
            y_outer = r_outer * sin(theta)

            x_inner = r_inner * cos(theta)
            y_inner = r_inner * sin(theta)

            outer_row.append(len(points))
            points.append([x_outer, y_outer, z])

            inner_row.append(len(points))
            points.append([x_inner, y_inner, z])

        outer_index.append(outer_row)
        inner_index.append(inner_row)

    # どの小面を残すかを先に判定
    keep = []

    for j in range(z_segments):
        row = []

        z_mid = vase_height * (j + 0.5) / z_segments

        for i in range(theta_segments):
            theta_mid = 2.0 * pi * (i + 0.5) / theta_segments
            row.append(should_keep_wall(theta_mid, z_mid, seeds, unwrap_width, ref_radius))

        keep.append(row)

    # 面を張る
    for j in range(z_segments):
        for i in range(theta_segments):
            if not keep[j][i]:
                continue

            i0 = i
            i1 = (i + 1) % theta_segments
            j0 = j
            j1 = j + 1

            o00 = outer_index[j0][i0]
            o10 = outer_index[j0][i1]
            o11 = outer_index[j1][i1]
            o01 = outer_index[j1][i0]

            in00 = inner_index[j0][i0]
            in10 = inner_index[j0][i1]
            in11 = inner_index[j1][i1]
            in01 = inner_index[j1][i0]

            # 外側面
            add_quad(faces, o00, o10, o11, o01)

            # 内側面(法線が内側を向くように逆順)
            add_quad(faces, in00, in01, in11, in10)

            # 左側の厚み面
            left_i = (i - 1) % theta_segments
            if not keep[j][left_i]:
                add_quad(faces, in00, o00, o01, in01)

            # 右側の厚み面
            right_i = (i + 1) % theta_segments
            if not keep[j][right_i]:
                add_quad(faces, o10, in10, in11, o11)

            # 下側の厚み面
            if j == 0 or not keep[j - 1][i]:
                add_quad(faces, o00, in00, in10, o10)

            # 上側の厚み面
            if j == z_segments - 1 or not keep[j + 1][i]:
                add_quad(faces, o11, in11, in01, o01)

    # 底を閉じる面
    # 花瓶として底が抜けないように、内側半径部分も面でふさぐ
    bottom_center_index = len(points)
    points.append([0.0, 0.0, 0.0])

    for i in range(theta_segments):
        i0 = i
        i1 = (i + 1) % theta_segments

        in0 = inner_index[0][i0]
        in1 = inner_index[0][i1]

        # 下向きの面
        faces.append([bottom_center_index, in1, in0])

    return polyhedron(points=points, faces=faces)


# --- メイン処理 ---

vase = create_vase_mesh()

result = color(vase, body_color)

show(result)

スクリーンショット 2026-09-11 104709.png


2. 使い方のポイント

  • theta_segmentsz_segments を大きくすると、輪郭とボロノイ模様がより滑らかになります。
  • ただし、数値を大きくしすぎるとプレビューが重くなります。
  • ボロノイ模様を変えたい場合は、seed_value の数値を変更してください。
  • 輪郭の雰囲気を変えたい場合は、以下の高さ位置を調整します。
z_waist  = 42.0
z_belly  = 82.0
z_neck   = 132.0

3. 設計上の注意

このモデルはブーリアンで穴を開けているのではなく、
「残す面」と「抜く面」を最初から判定して、直接ポリゴン面を張っています。

そのため、

  • ボロノイ透かしあり
  • 壁厚 3mm
  • 下から22mmは穴なし
  • 上から12mmは穴なし
  • 原点は底面中心
  • 単位はmm

という条件を満たす、3Dプリント向けのメッシュ構造になっています。


08. ボロノイ透かし花瓶(voronoi_seed_vase)

以下は、提示された Blender 用スクリプトを PythonSCAD の polyhedron(points, faces) で再構成する形に変換したものです。
ボロノイ模様はブーリアンで穴を開けるのではなく、元コードと同じ考え方で「残す面/抜く面」を判定し、メッシュとして花瓶を直接作っています。


1. 完成コード(PythonSCAD)

# ==========================================
# ボロノイ透かし花瓶(voronoi_seed_vase)
# Blender版からPythonSCADへ変換
# 作成日: 2026/09/11
# ==========================================

from pythonscad import *
from math import pi, sin, cos, hypot
import random

# --- パラメータ設定 (単位: mm) ---

# --- いちばん大事な数字:種(シード)---
SEEDS = [1]

# --- 花瓶のかたち ---
HEIGHT = 170.0
PROFILE = [
    [0.00, 34.0],   # 底
    [0.18, 46.0],   # 腰
    [0.55, 52.0],   # いちばん太いところ
    [0.78, 33.0],   # 首
    [1.00, 40.0],   # 口
]

WALL = 3.0
FLOOR_T = 5.0
SOLID_BOTTOM = 22.0
SOLID_TOP = 12.0

# --- 透かし模様(ボロノイ)---
CELL_COUNT = 90
WEB_WIDTH = 2.6

# --- きめの細かさ ---
# 重い場合は、まず NTHETA=160, NZ=170 くらいで試してください
NTHETA = 320
NZ = 340

# 色(R, G, B)
COLORS = [
    [0.86, 0.83, 0.76],  # オフホワイト
    [0.20, 0.26, 0.34],  # インクブルー
    [0.68, 0.34, 0.24],  # テラコッタ
    [0.33, 0.40, 0.30],  # モスグリーン
]

# STLを書き出す場合だけフォルダを指定
# 例: EXPORT_STL_DIR = "C:/Users/yourname/Desktop/vases"
EXPORT_STL_DIR = ""

PREFIX = "vase_"
TAU = pi * 2.0

# R_MIDは、展開図上で角度方向の距離をmm換算するために使う
R_MID = 1.0


# --- 関数(モジュール)定義 ---

def radius_at(t):
    """
    高さの割合 t(0〜1)から、その高さでの外側半径を求める。
    点と点の間は Catmull-Rom 風の補間で滑らかにつなぐ。
    """
    pts = PROFILE

    if t <= pts[0][0]:
        return pts[0][1]

    if t >= pts[-1][0]:
        return pts[-1][1]

    for i in range(len(pts) - 1):
        t0, r0 = pts[i]
        t1, r1 = pts[i + 1]

        if t0 <= t <= t1:
            u = (t - t0) / (t1 - t0)

            if i - 1 >= 0:
                p0 = pts[i - 1][1]
            else:
                p0 = r0

            if i + 2 < len(pts):
                p3 = pts[i + 2][1]
            else:
                p3 = r1

            return 0.5 * (
                (2.0 * r0)
                + (-p0 + r1) * u
                + (2.0 * p0 - 5.0 * r0 + 4.0 * r1 - p3) * u * u
                + (-p0 + 3.0 * r0 - 3.0 * r1 + p3) * u * u * u
            )

    return pts[-1][1]


def make_seeds(count, seed, z0, z1, candidates=12):
    """
    ボロノイ模様の種点を作る。
    同じ seed なら毎回同じ配置になる。
    候補点をいくつか出し、一番すいている場所を選ぶ。
    """
    rng = random.Random(seed)
    pts = []

    def dist2(a, b):
        dth = abs(a[0] - b[0])
        dth = min(dth, TAU - dth)
        return (dth * R_MID) ** 2 + (a[1] - b[1]) ** 2

    for _ in range(count):
        best = None
        best_d = -1.0

        trial_count = candidates if pts else 1

        for _c in range(trial_count):
            cand = [
                rng.uniform(0.0, TAU),
                rng.uniform(z0, z1)
            ]

            d = min((dist2(cand, p) for p in pts), default=1e18)

            if d > best_d:
                best_d = d
                best = cand

        pts.append(best)

    return pts


def field(th, z, seeds):
    """
    ボロノイ境界からの距離判定。
    負なら材料を残す場所、正なら穴にする場所。
    """
    d1 = 1e18
    d2 = 1e18

    for sth, sz in seeds:
        dth = abs(th - sth)

        if dth > pi:
            dth = TAU - dth

        d = hypot(dth * R_MID, z - sz)

        if d < d1:
            d2 = d1
            d1 = d
        elif d < d2:
            d2 = d

    return (d2 - d1) - WEB_WIDTH


def create_vase_mesh(seed):
    """
    1本分の花瓶メッシュを作る。
    戻り値は points, faces。
    """
    nt = NTHETA
    nz = NZ

    z0 = SOLID_BOTTOM
    z1 = HEIGHT - SOLID_TOP

    seeds = make_seeds(CELL_COUNT, seed, z0, z1)
    zs = [HEIGHT * j / nz for j in range(nz + 1)]

    # 1) 各マス目を「残す/抜く」に分類
    keep = [[True] * nt for _ in range(nz)]

    for j in range(nz):
        zc = (zs[j] + zs[j + 1]) * 0.5

        if zc < z0 or zc > z1:
            continue

        for i in range(nt):
            th = TAU * (i + 0.5) / nt
            keep[j][i] = field(th, zc, seeds) < 0.0

    # 2) 角だけでつながる場所を補修
    for _pass in range(4):
        fixed = 0

        for j in range(nz - 1):
            for i in range(nt):
                b = (i + 1) % nt

                a00 = keep[j][i]
                a01 = keep[j][b]
                a10 = keep[j + 1][i]
                a11 = keep[j + 1][b]

                if a00 and a11 and not a01 and not a10:
                    keep[j][b] = True
                    fixed += 1
                elif a01 and a10 and not a00 and not a11:
                    keep[j][i] = True
                    fixed += 1

        if fixed == 0:
            break

    # 3) 穴のフチを境界線に近づけて、カクつきを減らす
    P = [[[TAU * i / nt, zs[j]] for i in range(nt)] for j in range(nz + 1)]

    lim_th = 0.45 * (TAU / nt) * R_MID
    lim_z = 0.45 * (HEIGHT / nz)
    h = 0.3

    for j in range(1, nz):
        for i in range(nt):
            corners = [
                keep[j - 1][(i - 1) % nt],
                keep[j - 1][i],
                keep[j][(i - 1) % nt],
                keep[j][i]
            ]

            if all(corners) or not any(corners):
                continue

            th, z = P[j][i]

            if z < z0 - 2.0 or z > z1 + 2.0:
                continue

            f = field(th, z, seeds)

            gth = (
                field(th + h / R_MID, z, seeds)
                - field(th - h / R_MID, z, seeds)
            ) / (2.0 * h)

            gz = (
                field(th, z + h, seeds)
                - field(th, z - h, seeds)
            ) / (2.0 * h)

            g2 = gth * gth + gz * gz

            if g2 < 1e-9:
                continue

            mx = max(-lim_th, min(lim_th, -f * gth / g2))
            my = max(-lim_z, min(lim_z, -f * gz / g2))

            P[j][i][0] = th + mx / R_MID
            P[j][i][1] = z + my

    # 4) 頂点と面を作る
    verts = []
    faces = []

    jf = max(1, int(round(FLOOR_T / HEIGHT * nz)))

    OUT = [[None] * nt for _ in range(nz + 1)]
    INN = [[None] * nt for _ in range(nz + 1)]

    def out_vertex(j, i):
        if OUT[j][i] is None:
            th, z = P[j][i]
            t = min(max(z, 0.0), HEIGHT) / HEIGHT
            r = radius_at(t)

            OUT[j][i] = len(verts)
            verts.append([
                r * cos(th),
                r * sin(th),
                z
            ])

        return OUT[j][i]

    def inn_vertex(j, i):
        if INN[j][i] is None:
            th, z = P[j][i]
            t = min(max(z, 0.0), HEIGHT) / HEIGHT
            r = radius_at(t) - WALL

            INN[j][i] = len(verts)
            verts.append([
                r * cos(th),
                r * sin(th),
                z
            ])

        return INN[j][i]

    # 外壁・内壁
    for j in range(nz):
        for i in range(nt):
            if not keep[j][i]:
                continue

            a = i
            b = (i + 1) % nt

            # 外側面
            faces.append([
                out_vertex(j, a),
                out_vertex(j, b),
                out_vertex(j + 1, b),
                out_vertex(j + 1, a)
            ])

            # 内側面:底板より上だけ作る
            if j >= jf:
                faces.append([
                    inn_vertex(j, a),
                    inn_vertex(j + 1, a),
                    inn_vertex(j + 1, b),
                    inn_vertex(j, b)
                ])

    # 底面
    c_bottom = len(verts)
    verts.append([0.0, 0.0, 0.0])

    for i in range(nt):
        faces.append([
            c_bottom,
            out_vertex(0, (i + 1) % nt),
            out_vertex(0, i)
        ])

    # 底板の上面
    c_floor = len(verts)
    verts.append([0.0, 0.0, zs[jf]])

    for i in range(nt):
        faces.append([
            c_floor,
            inn_vertex(jf, i),
            inn_vertex(jf, (i + 1) % nt)
        ])

    # 口のフチ
    for i in range(nt):
        a = i
        b = (i + 1) % nt

        faces.append([
            out_vertex(nz, a),
            out_vertex(nz, b),
            inn_vertex(nz, b),
            inn_vertex(nz, a)
        ])

    # 穴の側面
    for j in range(jf, nz):
        for i in range(nt):
            if not keep[j][i]:
                continue

            a = i
            b = (i + 1) % nt

            # 左右方向の穴フチ
            if not keep[j][(i - 1) % nt]:
                faces.append([
                    out_vertex(j, a),
                    out_vertex(j + 1, a),
                    inn_vertex(j + 1, a),
                    inn_vertex(j, a)
                ])

            if not keep[j][(i + 1) % nt]:
                faces.append([
                    out_vertex(j + 1, b),
                    out_vertex(j, b),
                    inn_vertex(j, b),
                    inn_vertex(j + 1, b)
                ])

            # 上下方向の穴フチ
            if j - 1 >= jf and not keep[j - 1][i]:
                faces.append([
                    out_vertex(j, b),
                    out_vertex(j, a),
                    inn_vertex(j, a),
                    inn_vertex(j, b)
                ])

            if j + 1 < nz and not keep[j + 1][i]:
                faces.append([
                    out_vertex(j + 1, a),
                    out_vertex(j + 1, b),
                    inn_vertex(j + 1, b),
                    inn_vertex(j + 1, a)
                ])

    return verts, faces


def create_vase(seed, location, color_rgb):
    """
    polyhedronで花瓶を作り、位置と色を設定する。
    """
    verts, faces = create_vase_mesh(seed)

    vase = polyhedron(points=verts, faces=faces)
    vase = translate(vase, location)
    vase = color(vase, [color_rgb[0], color_rgb[1], color_rgb[2], 1.0])

    return vase


def combine_parts(parts):
    """
    複数の花瓶を + 演算子で結合する。
    """
    result_obj = parts[0]

    for part in parts[1:]:
        result_obj = result_obj + part

    return result_obj


# --- メイン処理 ---

# 1. 展開図上での基準半径を決める
R_MID = radius_at(0.5)

# 2. 複数シードの場合に横に並べる間隔
max_radius = max(r for _, r in PROFILE)
pitch = max_radius * 2.0 + 30.0

# 3. 花瓶を生成
made_parts = []

for k, seed in enumerate(SEEDS):
    x_pos = (k - (len(SEEDS) - 1) * 0.5) * pitch
    location = [x_pos, 0.0, 0.0]

    color_rgb = COLORS[k % len(COLORS)]

    vase_obj = create_vase(seed, location, color_rgb)
    made_parts.append(vase_obj)

# 4. 1つの表示用オブジェクトにまとめる
result = combine_parts(made_parts)

# 5. 必要ならSTLを書き出す
if EXPORT_STL_DIR:
    for k, part in enumerate(made_parts):
        seed = SEEDS[k]
        filename = EXPORT_STL_DIR.rstrip("\\/") + "/" + PREFIX + "seed%02d.stl" % seed
        export(part, filename)

# 6. 3Dプレビュー表示
show(result)

スクリーンショット 2026-09-11 171150.png


変換のポイント

  • Blender の bpy.data.meshes.new()from_pydata() に相当する部分は、PythonSCAD では polyhedron(points=verts, faces=faces) に置き換えています
  • 形状の生成ロジックは、元コードと同じく
    頂点リスト verts と面リスト faces を作ってメッシュ化する方式です
  • PythonSCAD では $fn = 60 のような書き方は使わず、必要な場合は各形状の fn 引数で指定します。今回は polyhedron なので fn は使っていません
  • color(obj, [r, g, b, a]) の形式で色を付けています
  • 複数の花瓶を作る場合は、SEEDS = [1, 2, 3] のように変更できます

最初はかなり重くなる可能性があるので、動作確認用に次のように下げるのがおすすめです。

NTHETA = 160
NZ = 170

きれいに出ることを確認してから、元の NTHETA = 320, NZ = 340 に戻すとよいです。

📄 ライセンスと「オープンソース」の文化について

本記事のソースコードは、すべて MIT ライセンス で公開しています。

  • 高校生のみなさんへ 🚀
    プログラミングの世界には、「自分が作った便利な仕組みをみんなに共有し、お互いに助け合って技術を発展させる(オープンソース)」という素晴らしい文化があります。このコードも、作者の名前(クレジット)さえ残してもらえれば、改造して学校の課題に使ったり、自分のアプリに組み込んだりして自由に無料で使ってOKです!
    ぜひこのコードをベースに、自分だけの新しいプログラムを作って挑戦してみてください。
  • 免責事項 ⚠️
    本記事およびコードは個人の研究・検証に基づくものであり、所属する組織の公式見解ではありません。自由に使っていただけますが、利用に伴ういかなる損害についても執筆者は責任を負いかねますので、すべて「自己責任(無保証)」の範囲内で楽しく学んでくださいね。

参考資料

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?