# 球体筐体実験(TEST_11)用の球殻メッシュ生成スクリプト.
#
# 現行筐体(前側/後側)を理想化した「完全閉球」の白色キャビティに置き換え,
# 幾何レバーの理想極限(漏れゼロの積分球)で照明均一性の上限を測る実験に使う.
# 評価板(照明ムラ補正ボード)を内包できる中心・半径を引数で指定する.
"""球殻(integrating sphere 状の閉じた白キャビティ)STL を生成する.
内半径 r の icosphere(法線を内向きに反転)と外半径 r+t の icosphere を
連結した watertight な厚み付き殻を作り,バイナリ STL として出力する.
単位は mm(プロジェクトのシーン YAML 規約に合わせる).
使用例:
uv run python scripts/generate_sphere_housing.py \
--radius 100 --thickness 3 --center 0 0 -40 \
--output configs/meshes/球殻_r100.stl
"""
from __future__ import annotations
import argparse
from pathlib import Path
import numpy as np
import trimesh
def build_sphere_shell(
radius: float,
thickness: float,
center: tuple[float, float, float],
subdivisions: int,
) -> trimesh.Trimesh:
"""厚み付き球殻メッシュを生成する.
内面(半径 radius)は法線をキャビティ側(中心向き)に反転させ,
外面(半径 radius + thickness)は外向きのまま連結する.
2 つの閉曲面から成るため全体として watertight な実体殻になる.
Args:
radius: 内半径(mm).キャビティの半径.
thickness: 殻厚(mm).
center: 球中心座標 (x, y, z)(mm).
subdivisions: icosphere の分割回数.6 で辺長 ≈ radius * 0.03 程度.
Returns:
球殻メッシュ(mm 単位).
Raises:
ValueError: radius または thickness が正でない場合.
"""
if radius <= 0:
raise ValueError(f"内半径は正の値を指定してください: {radius}")
if thickness <= 0:
raise ValueError(f"殻厚は正の値を指定してください: {thickness}")
inner = trimesh.creation.icosphere(subdivisions=subdivisions, radius=radius)
# 内面の法線は実体(殻)から見て外側=キャビティ中心向きが正しいため反転する
inner.invert()
outer = trimesh.creation.icosphere(subdivisions=subdivisions, radius=radius + thickness)
shell = trimesh.util.concatenate([outer, inner])
shell.apply_translation(np.asarray(center, dtype=np.float64))
return shell
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""コマンドライン引数をパースして返す."""
parser = argparse.ArgumentParser(
prog="generate_sphere_housing",
description="球体筐体実験用の閉じた球殻 STL(mm 単位)を生成する.",
)
parser.add_argument("--radius", type=float, default=100.0, help="内半径(mm).既定 100")
parser.add_argument("--thickness", type=float, default=3.0, help="殻厚(mm).既定 3")
parser.add_argument(
"--center",
type=float,
nargs=3,
default=(0.0, 0.0, -40.0),
metavar=("X", "Y", "Z"),
help="球中心座標(mm).既定 0 0 -40(評価板を内包する位置)",
)
parser.add_argument(
"--subdivisions",
type=int,
default=6,
help="icosphere 分割回数.既定 6(面数 81920 x 2,辺長 ≈ 3mm)",
)
parser.add_argument(
"--output",
type=Path,
default=Path("configs/meshes/球殻_r100.stl"),
help="出力 STL パス.既定 configs/meshes/球殻_r100.stl",
)
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
"""球殻を生成して STL 出力し,検証情報を表示する."""
args = _parse_args(argv)
shell = build_sphere_shell(
radius=args.radius,
thickness=args.thickness,
center=tuple(args.center),
subdivisions=args.subdivisions,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
shell.export(args.output)
# 期待体積 = (4/3)π((r+t)^3 - r^3).メッシュ体積との一致で内面反転の正しさを確認できる
expected_volume = (
4.0 / 3.0 * np.pi * ((args.radius + args.thickness) ** 3 - args.radius**3)
)
print(f"出力: {args.output}")
print(f"faces: {len(shell.faces)}, watertight: {shell.is_watertight}")
print(f"bounds (mm): {shell.bounds.round(2).tolist()}")
print(f"体積 (mm^3): {shell.volume:.0f}(理論値 {expected_volume:.0f})")
return 0
if __name__ == "__main__":
raise SystemExit(main())