"""TEST_15(球体筐体実験)の発表スライド用ジオメトリ図を生成する.
作業用の幾何確認図(build_radius_geometry_figure.py 等)はグリッド・凡例・作業タイトルで
情報密度が高く,スライドには不向き.本スクリプトは同じ断面計算を流用しつつ,
発表向けに要素を絞った 3 枚を `output/_slide_panels/` に出力する.
1. test15_setup_cutaway.png : 実験構成の注釈付き軸断面(r=100・similar_radial).
評価板・球キャビティ・軸上 LED・バッフル・遮断される直接光を日本語ラベルで指す
2. test15_radius_nest.png : 半径スイープの入れ子断面(評価板は不変のまま球だけ拡大)
3. test15_junction.png : 接合のさせ方の比較(垂直立ち上げ=喉 vs 水平出し,r=250)
実行方法:
uv run python scripts/build_slide_test15_geometry.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib
import numpy as np
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parent))
from build_radius_geometry_figure import loft_profile, sphere_profile # noqa: E402
from sweep_sphere_radius import ( # noqa: E402
BAFFLE_GAP,
BOARD_HALF,
BOARD_SURFACE_Z,
baffle_radius,
led_z,
)
ROOT = Path(__file__).resolve().parents[1]
OUT_DIR = ROOT / "output" / "_slide_panels"
plt.rcParams["font.family"] = ["Yu Gothic", "MS Gothic", "DejaVu Sans"]
plt.rcParams["font.size"] = 13
BOARD_COLOR = "crimson"
WALL_COLOR = "#455a74"
LED_COLOR = "#e6a817"
BAFFLE_COLOR = "black"
VARIANT = "board_baffle"
CAVITY = "similar_radial"
def cavity_outline(radius: float, cavity: str = CAVITY) -> tuple[np.ndarray, np.ndarray]:
"""片側(+X)のリム→ロフト→球極の断面プロファイルを返す."""
lx, lz = loft_profile(radius, cavity)
sx, sz = sphere_profile(radius, cavity)
return np.concatenate([lx, sx]), np.concatenate([lz, sz])
def draw_scale_bar(ax, x: float, z: float, length: float = 100.0) -> None:
"""軸の代わりに置くスケールバーを描く."""
ax.plot([x, x + length], [z, z], color="black", lw=2.0)
ax.plot([x, x], [z - 4, z + 4], color="black", lw=2.0)
ax.plot([x + length, x + length], [z - 4, z + 4], color="black", lw=2.0)
ax.text(x + length / 2, z - 10, f"{length:g} mm", ha="center", va="top", fontsize=12)
def make_setup_cutaway(radius: float = 100.0, annotate: bool = True) -> None:
"""実験構成の軸断面を描く.
Args:
radius: 球内半径(mm).
annotate: True で日本語注釈とスケールバー付き(説明用).False で図形のみ
(スライド側で注釈を載せる前提.点線・マーカーは遠目に見えるよう太くする).
"""
fig, ax = plt.subplots(figsize=(8.2, 8.2))
x, z = cavity_outline(radius)
# 内壁(左右対称).閉じた輪郭として太線で描く
ax.plot(np.concatenate([x, -x[::-1]]), np.concatenate([z, z[::-1]]),
color=WALL_COLOR, lw=4.0 if annotate else 5.0, solid_capstyle="round")
# 評価板(上面のフタ)
ax.plot([-BOARD_HALF[0], BOARD_HALF[0]], [BOARD_SURFACE_Z] * 2,
color=BOARD_COLOR, lw=8.0 if annotate else 10.0, solid_capstyle="butt")
# LED(軸上 1 灯・壁面密着・評価板向き)と バッフル
zl = led_z(radius, VARIANT, CAVITY)
rb = baffle_radius(radius, VARIANT, CAVITY)
ax.plot(0.0, zl, marker="^", color=LED_COLOR, ms=16 if annotate else 22,
mec="black", mew=0.8, zorder=5)
ax.plot([-rb, rb], [zl + BAFFLE_GAP] * 2, color=BAFFLE_COLOR,
lw=5.0 if annotate else 7.0)
# 遮断される直接光(LED → 板の両端.バッフルで切れることを示す)
for sign in (-1.0, 1.0):
ax.plot([0.0, sign * BOARD_HALF[0]], [zl, BOARD_SURFACE_Z],
color=LED_COLOR, lw=1.4 if annotate else 5.5, ls=":", alpha=0.9)
if not annotate:
ax.set_xlim(-125, 125)
ax.set_ylim(-210, 15)
ax.set_aspect("equal")
ax.axis("off")
fig.tight_layout()
out = OUT_DIR / "test15_setup_cutaway_plain.png"
fig.savefig(out, dpi=150, bbox_inches="tight", transparent=True)
plt.close(fig)
print(f"出力: {out}")
return
# 注釈
common = {"fontsize": 14, "ha": "left", "va": "center",
"arrowprops": {"arrowstyle": "-", "color": "0.3", "lw": 1.2}}
ax.annotate("評価板 99×102 mm\n(上面の開口にフタとして嵌める)",
xy=(BOARD_HALF[0] * 0.55, BOARD_SURFACE_Z), xytext=(85, 30), **common)
ax.annotate("球キャビティ\n(内壁は拡散反射・白)",
xy=(float(np.interp(-120, z[::-1], x[::-1])), -120), xytext=(120, -95),
**common)
ax.annotate("LED(軸上に 1 灯・壁面に密着・\n評価板の方を向ける)",
xy=(3, zl), xytext=(60, -215), **common)
ax.annotate("バッフル\n(評価板への直接光を完全に遮断)",
xy=(rb * 0.8, zl + BAFFLE_GAP), xytext=(60, -175), **common)
ax.text(0, zl + 42, "直接光は届かない\n(多重反射光のみが板を照らす)",
fontsize=12, ha="center", color="0.35")
draw_scale_bar(ax, -180, -245)
ax.set_xlim(-195, 245)
ax.set_ylim(-260, 45)
ax.set_aspect("equal")
ax.axis("off")
fig.tight_layout()
out = OUT_DIR / "test15_setup_cutaway.png"
fig.savefig(out, dpi=150, bbox_inches="tight")
plt.close(fig)
print(f"出力: {out}")
def make_radius_nest(
radii: tuple[float, ...] = (88.0, 100.0, 135.0, 170.0, 250.0), annotate: bool = True
) -> None:
"""半径スイープの入れ子断面を描く(評価板は不変のまま球だけ拡大).
Args:
radii: 描く球内半径(mm).
annotate: True で説明文・スケールバー付き,ラベルは半径表記(説明用).
False でサイズラベル(直径表記・大きい文字)のみ=スライド用.
ラベルの重なりを避けるため左右交互に置く.
"""
fig, ax = plt.subplots(figsize=(8.8, 9.6))
colors = plt.cm.viridis(np.linspace(0.05, 0.85, len(radii)))
for i, (radius, color) in enumerate(zip(radii, colors, strict=True)):
x, z = cavity_outline(radius)
ax.plot(np.concatenate([x, -x[::-1]]), np.concatenate([z, z[::-1]]),
color=color, lw=2.6 if annotate else 3.4)
zl = led_z(radius, VARIANT, CAVITY)
rb = baffle_radius(radius, VARIANT, CAVITY)
ax.plot(0.0, zl, marker="^", color=color, ms=9 if annotate else 13,
mec="black", mew=0.6, zorder=5)
ax.plot([-rb, rb], [zl + BAFFLE_GAP] * 2, color="black", lw=2.2 if annotate else 3.0)
if annotate:
ax.text(float(x.max()) + 8, float(z[np.argmax(x)]), f"r={radius:g}",
color=color, fontsize=13, va="center", fontweight="bold")
else:
sign = 1.0 if i % 2 == 0 else -1.0
ax.text(sign * (float(x.max()) + 10), float(z[np.argmax(x)]),
f"φ{2 * radius:g}", color=color, fontsize=30, va="center",
ha="left" if sign > 0 else "right", fontweight="bold")
ax.plot([-BOARD_HALF[0], BOARD_HALF[0]], [BOARD_SURFACE_Z] * 2,
color=BOARD_COLOR, lw=8.0 if annotate else 10.0, solid_capstyle="butt", zorder=6)
if annotate:
ax.text(0, 16, "評価板・カメラ・LED 条件は不変", color=BOARD_COLOR,
fontsize=15, ha="center", fontweight="bold")
ax.text(0, -520, "球の内半径だけを 88〜250 mm で変える(LED・バッフルは相似に追従)",
fontsize=13, ha="center", color="0.25")
draw_scale_bar(ax, -255, -490)
ax.set_aspect("equal")
ax.axis("off")
fig.tight_layout()
out = OUT_DIR / ("test15_radius_nest.png" if annotate else "test15_radius_nest_plain.png")
fig.savefig(out, dpi=150, bbox_inches="tight", transparent=not annotate)
plt.close(fig)
print(f"出力: {out}")
def make_junction_comparison(radius: float = 250.0) -> None:
"""接合のさせ方の比較(垂直立ち上げ=喉 vs 水平出し)を描く."""
fig, ax = plt.subplots(figsize=(10.5, 5.6))
designs = [
("similar_g2", "tab:red", "板に垂直に立ち上げる(喉ができる)"),
("similar_radial", "tab:blue", "水平に出して球へ回り込む(喉が消える)"),
]
for cavity, color, label in designs:
x, z = cavity_outline(radius, cavity)
ax.plot(x, z, color=color, lw=3.2, label=label)
ax.plot(-x, z, color=color, lw=3.2)
ax.plot([-BOARD_HALF[0], BOARD_HALF[0]], [BOARD_SURFACE_Z] * 2,
color=BOARD_COLOR, lw=8.0, solid_capstyle="butt", zorder=6)
ax.text(0, 10, "評価板", color=BOARD_COLOR, fontsize=14, ha="center", fontweight="bold")
# 喉(板直下のくびれ)の注釈: 垂直版は板縁の下でほとんど広がらない
ax.annotate("喉(くびれ): 板の直下で通路が広がらず\n周辺の照度が落ちる",
xy=(54, -25), xytext=(85, -105), fontsize=13, ha="left", va="center",
arrowprops={"arrowstyle": "->", "color": "tab:red", "lw": 1.6},
color="tab:red",
bbox={"boxstyle": "round,pad=0.35", "fc": "white", "ec": "none", "alpha": 0.9})
ax.set_xlim(-270, 270)
ax.set_ylim(-140, 30)
ax.set_aspect("equal")
ax.axis("off")
ax.legend(fontsize=13, loc="lower left", frameon=False)
ax.set_title("評価板と球の接合のさせ方(r=250 の断面・上部を拡大)", fontsize=15)
fig.tight_layout()
out = OUT_DIR / "test15_junction.png"
fig.savefig(out, dpi=150, bbox_inches="tight")
plt.close(fig)
print(f"出力: {out}")
def main() -> int:
"""スライド用ジオメトリ図 3 枚を生成する."""
OUT_DIR.mkdir(parents=True, exist_ok=True)
make_setup_cutaway()
make_setup_cutaway(annotate=False)
make_radius_nest()
make_radius_nest(annotate=False)
make_junction_comparison()
return 0
if __name__ == "__main__":
sys.exit(main())