# 새턴 V 로켓 (아폴로 계획 발사 형상) — Blender Python 코드만으로 만든 실물 크기 모델(전체 높이 약 110.6m).
# 동체는 단면 프로필(반지름, 높이)을 Z 축으로 돌린 회전체 한 개로 만들고, 면마다 재질 번호를 매겨
# 1단(S-IC)·3단(S-IVB)의 흑백 '롤 패턴' 도장을 겹친 면 없이 칠했다.
# F-1 엔진 5기는 노즐 벨 회전체 + 터보펌프·배기 매니폴드를 하나로 묶은 뒤 메시를 공유해 배치했고,
# 엔진 페어링·날개 4쌍, 단 사이 이음 링, 얼리지 모터 덮개, APS 모듈, 사령·기계선, 발사 탈출 탑(트러스)을 더했다.
# 성조기와 'USA' 글자는 평면 메시를 동체 곡면에 감아(원통 좌표 변환) 붙였다.
# 로켓은 이동식 발사대(Mobile Launcher) 갑판의 배기 구멍 위에 고정 팔 4개로 얹혀 있고, 갑판에는 꼬리 서비스 마스트 3기를 세웠다.
from atelier import *

SHOT = {"azimuth": -50, "elevation": 7, "lens": 70, "light": "day", "background": "#2a3340"}

SEG = 96  # 동체 둘레 분할 (45° = 12칸)

WHITE = mat('Paint_White', '#e6e4dc', 0.45)
BLACK = mat('Paint_Black', '#1f2124', 0.5)
IU = mat('Instrument_Unit_Grey', '#c9c8c1', 0.5)
SLA = mat('SLA_Panel_Silver', '#d2d3cf', 0.4, 0.35)
SILVER = mat('ServiceModule_Aluminium', '#b9bcbf', 0.3, 0.9)
HEAT = mat('HeatShield_Dark', '#3b3a38', 0.8)
NOZZLE = mat('F1_Nozzle_Extension_Black', '#2c2b2a', 0.55, 0.5)
PUMP = mat('F1_Engine_Steel', '#7d7a73', 0.4, 0.85)
LES_RED = mat('LES_Tower_Orange', '#b2402c', 0.5)
RED = mat('Flag_Red', '#a8262f', 0.5)
BLUE = mat('Flag_Blue', '#2a3570', 0.5)
ML_STEEL = mat('Launcher_Steel_Grey', '#6e7275', 0.7, 0.3)

R1 = 5.03   # S-IC / S-II 반지름 (지름 10.06m)
R3 = 3.30   # S-IVB 반지름 (지름 6.6m)
RSM = 1.96  # 사령·기계선 반지름 (지름 3.9m)


# ---------------------------------------------------------------- 동체 회전체(면별 도장)
def painted_lathe(name, profile, seg, mats, paint, cap_mat=None):
    """profile=[(r,z),...] 아래→위. 구간마다 정점을 따로 만들어 단 경계가 각지게 남도록 한다.
    paint(theta_deg, z_mid) → mats 인덱스."""
    bm = bmesh.new()
    for j in range(len(profile) - 1):
        (ra, za), (rb, zb) = profile[j], profile[j + 1]
        zm = (za + zb) / 2
        ringa, ringb = [], []
        for i in range(seg):
            a = 2 * math.pi * i / seg
            ringa.append(bm.verts.new((ra * math.cos(a), ra * math.sin(a), za)) if ra > 0 else None)
            ringb.append(bm.verts.new((rb * math.cos(a), rb * math.sin(a), zb)) if rb > 0 else None)
        ca = bm.verts.new((0, 0, za)) if ra == 0 else None
        cb = bm.verts.new((0, 0, zb)) if rb == 0 else None
        for i in range(seg):
            k = (i + 1) % seg
            if ra == 0:
                f = bm.faces.new((ca, ringb[i], ringb[k]))
            elif rb == 0:
                f = bm.faces.new((ringa[i], ringa[k], cb))
            else:
                f = bm.faces.new((ringa[i], ringa[k], ringb[k], ringb[i]))
            th = math.degrees(2 * math.pi * (i + 0.5) / seg)
            f.material_index = paint(th, zm)
            f.smooth = abs(rb - ra) < 1e-6 or (ra > 0 and rb > 0)
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    for m in mats:
        me.materials.append(m)
    ob = bpy.data.objects.new(name, me)
    link(ob)
    return ob


Z_BASE = 5.0
HULL = [
    (0, Z_BASE), (R1, Z_BASE),                       # 1단 바닥 열차폐판
    (R1, 22.0), (R1, 26.5), (R1, 39.0), (R1, 42.0),  # S-IC: 연료탱크 / 탱크 사이 / 산화제탱크 / 전방 스커트
    (R1, 47.6),                                      # S-IC/S-II 단간부
    (R1, 66.4),                                      # S-II
    (R3, 72.0),                                      # S-II/S-IVB 원뿔 단간부
    (R3, 75.0), (R3, 85.0),                          # S-IVB
    (R3, 85.9),                                      # 계기부(IU)
    (RSM, 94.4),                                     # 달착륙선 어댑터(SLA)
    (RSM, 98.2),                                     # 기계선(SM)
    (0.42, 101.3), (0.30, 101.45), (0, 101.45),      # 사령선 + 보호 덮개(BPC)
]
I_W, I_B, I_IU, I_SLA, I_SM, I_HEAT = range(6)


def hull_paint(th, z):
    if z <= Z_BASE + 1e-3:
        return I_HEAT
    if z < 22.0:   # 연료탱크: 네 줄의 검은 세로 띠
        d = min(abs(((th - q) + 180) % 360 - 180) for q in (0, 90, 180, 270))
        return I_B if d < 15 else I_W
    if z < 26.5:   # 탱크 사이 구간: 8칸 체크
        return I_B if int(th // 45) % 2 == 0 else I_W
    if z < 39.0:
        return I_W
    if z < 42.0:   # 전방 스커트: 엇갈린 8칸
        return I_B if int(th // 45) % 2 == 1 else I_W
    if z < 72.0:
        return I_W
    if z < 75.0:   # S-IVB 후방: 사분면 흑백
        return I_B if int(th // 90) % 2 == 0 else I_W
    if z < 85.0:
        return I_W
    if z < 85.9:
        return I_IU
    if z < 94.4:
        return I_SLA
    if z < 98.2:
        return I_SM
    return I_W


hull = painted_lathe('SaturnV_Hull', HULL, SEG, [WHITE, BLACK, IU, SLA, SILVER, HEAT], hull_paint)


# ---------------------------------------------------------------- 단 이음 링
def seam(name, r, z, t=0.07, material=WHITE):
    return torus(name, R=r, r=t, loc=(0, 0, z), material=material, seg=SEG, rseg=8)


for nm, r, z in [('Seam_SIC_Aft', R1, 5.25), ('Seam_SIC_Intertank_Low', R1, 22.0), ('Seam_SIC_Intertank_High', R1, 26.5),
                 ('Seam_SIC_Forward', R1, 39.0), ('Seam_SIC_Top', R1, 42.0), ('Seam_SII_Bottom', R1, 47.6),
                 ('Seam_SII_Top', R1, 66.4), ('Seam_SIVB_Bottom', R3, 72.0), ('Seam_SIVB_Top', R3, 85.0),
                 ('Seam_IU_Top', R3, 85.9), ('Seam_SLA_Top', RSM, 94.4), ('Seam_SM_Top', RSM, 98.2)]:
    seam(nm, r + 0.02, z, 0.08, IU)

# 탱크 사이 구간·전방 스커트·S-IVB 후방 스커트의 세로 보강재(코루게이션) — 바탕 도장 색을 따른다
def stringers(prefix, r, z0, z1, every=1):
    pieces = {}
    for i in range(0, SEG, every):
        th = 360 * (i + 0.5) / SEG
        idx = hull_paint(th, (z0 + z1) / 2)
        m = (WHITE, BLACK)[idx == I_B]
        if m.name not in pieces:
            pieces[m.name] = box(f'{prefix}_Stringer', size=(0.10, 2 * math.pi * r / SEG * 0.42, z1 - z0), material=m, base=True)
            ob = pieces[m.name]
        else:
            ob = duplicate(pieces[m.name])
        a = math.radians(th)
        ob.location = (r * math.cos(a), r * math.sin(a), z0)
        ob.rotation_euler.z = a


stringers('Intertank', R1, 22.15, 26.35)
stringers('SIC_ForwardSkirt', R1, 39.15, 41.85)
stringers('SIVB_AftSkirt', R3, 72.15, 74.85)

# 시스템 터널(케이블 통로): 1단·2단 옆을 따라 내려가는 돌출 덮개
for nm, z0, z1 in [('SystemsTunnel_SIC', 5.6, 41.6), ('SystemsTunnel_SII', 48.0, 66.0)]:
    cylinder(nm, r=0.32, h=z1 - z0, loc=(R1 - 0.12, 0, z0), material=WHITE, seg=16)


# ---------------------------------------------------------------- F-1 엔진
def f1_engine():
    # 노즐 연장부(검은 아래쪽)와 재생 냉각 노즐·연소실(위쪽 강철)을 두 회전체로 나눈다
    ext = [(1.88, 0.0), (1.81, 0.55), (1.69, 1.2), (1.55, 1.8), (1.45, 2.15)]
    up = [(1.45, 2.15), (1.30, 2.65), (1.07, 3.25), (0.84, 3.75), (0.60, 4.15), (0.49, 4.4), (0.56, 4.7), (0.60, 5.1), (0.52, 5.5), (0, 5.6)]
    inner = [(r - 0.05, z) for r, z in reversed(ext)]
    nz = lathe('F1_Nozzle_Extension', [(0, 2.1)] + inner + [(1.88, -0.02)] + ext[1:], material=NOZZLE, seg=64)
    parts = [nz, lathe('F1_Thrust_Chamber', up, material=PUMP, seg=64)]
    parts.append(torus('F1_ExhaustManifold', R=1.47, r=0.12, loc=(0, 0, 2.15), material=NOZZLE, seg=64, rseg=10))
    parts.append(torus('F1_ChamberRing', R=0.68, r=0.08, loc=(0, 0, 4.6), material=PUMP, seg=48, rseg=8))
    # 터보펌프 + 열교환기 + 배관
    tp = cylinder('F1_Turbopump', r=0.42, h=1.5, loc=(0.95, 0, 3.9), material=PUMP, seg=24)
    parts.append(tp)
    parts.append(cylinder('F1_TurbineExhaustDuct', r=0.16, h=1.4, loc=(1.15, 0, 2.3), rot=(0, 0.25, 0), material=NOZZLE, seg=16))
    parts.append(cylinder('F1_HeatExchanger', r=0.28, h=0.7, loc=(0.95, 0, 5.4), material=PUMP, seg=20))
    for k, a in enumerate((1.6, -1.6)):
        parts.append(cylinder(f'F1_PropLine{k}', r=0.17, h=1.3, loc=(0.55 * math.cos(a), 0.55 * math.sin(a), 4.3),
                              material=PUMP, seg=14))
    parts.append(box('F1_GimbalBlock', size=(0.7, 0.7, 0.4), loc=(0, 0, 5.75), material=PUMP))
    return join(parts, 'F1_Engine')


eng = f1_engine()
ENG_R = 3.75
eng.name = 'F1_Engine_Center'
outer_engines = []
for k in range(4):
    a = math.radians(45 + 90 * k)
    e = duplicate(eng, loc=(ENG_R * math.cos(a), ENG_R * math.sin(a), 0), rot=(0, 0, a + math.pi), name=f'F1_Engine_Outer_{k + 1}')
    outer_engines.append(e)
eng.rotation_euler.z = math.radians(30)

# ---------------------------------------------------------------- 엔진 페어링과 날개
fair_prof = [(1.95, 4.45), (2.12, 4.05), (2.16, 4.3), (2.08, 5.6), (1.86, 7.3), (1.52, 9.2), (1.10, 11.2), (0.62, 13.2), (0.25, 14.6), (0, 15.0)]
fairing = lathe('Engine_Fairing', fair_prof, material=WHITE, seg=48)
fin_pts = [(4.9, 4.25), (9.6, 4.75), (9.6, 6.35), (4.9, 11.6)]
fin = extrude_profile('Fin', fin_pts, 0.42, material=WHITE)
bevel(fin, 0.08, 2)
for k in range(4):
    a = math.radians(45 + 90 * k)
    f = fairing if k == 0 else duplicate(fairing, name='Engine_Fairing')
    f.location = (ENG_R * math.cos(a), ENG_R * math.sin(a), 0)
    f.name = f'Engine_Fairing_{k + 1}'
    fn = fin if k == 0 else duplicate(fin)
    fn.rotation_euler = (0, 0, a)
    fn.name = f'Fin_{k + 1}'

# 얼리지(분리 보조) 모터 덮개: S-IC/S-II 단간부 8개
ull = lathe('Ullage_Fairing', [(0.0, 0.0), (0.34, 0.25), (0.36, 2.2), (0.0, 3.0)], material=WHITE, seg=16)
ull.scale.y = 0.7
for k in range(8):
    a = math.radians(22.5 + 45 * k)
    u = ull if k == 0 else duplicate(ull)
    u.location = ((R1 - 0.08) * math.cos(a), (R1 - 0.08) * math.sin(a), 43.0)
    u.rotation_euler.z = a
    u.name = f'Ullage_Motor_Fairing_{k + 1}'

# S-IVB 보조 추진(APS) 모듈 2개
for k, a in enumerate((math.radians(0), math.radians(180))):
    m = box(f'SIVB_APS_Module_{k + 1}', size=(0.9, 1.5, 2.6), loc=((R3 + 0.3) * math.cos(a), (R3 + 0.3) * math.sin(a), 72.4),
            rot=(0, 0, a), material=BLACK, base=True)
    bevel(m, 0.18, 3)

# SLA 패널 이음선 4줄
for k in range(4):
    a = math.radians(90 * k)
    p0 = ((R3 + 0.03) * math.cos(a), (R3 + 0.03) * math.sin(a), 85.95)
    p1 = ((RSM + 0.03) * math.cos(a), (RSM + 0.03) * math.sin(a), 94.35)
    d = Vector(p1) - Vector(p0)
    sl = cylinder(f'SLA_Panel_Seam_{k + 1}', r=0.05, h=d.length, loc=p0, material=IU, seg=8)
    sl.rotation_euler = Vector((0, 0, 1)).rotation_difference(d).to_euler()

# ---------------------------------------------------------------- 기계선 RCS 쿼드
quad = box('SM_RCS_Quad', size=(0.35, 0.7, 0.9), loc=(0, 0, 0), material=SILVER)
bevel(quad, 0.05, 2)
for k in range(4):
    a = math.radians(45 + 90 * k)
    q = quad if k == 0 else duplicate(quad)
    q.location = ((RSM + 0.12) * math.cos(a), (RSM + 0.12) * math.sin(a), 96.6)
    q.rotation_euler.z = a
    q.name = f'SM_RCS_Quad_{k + 1}'
    for s in (-1, 1):
        n = cone(f'SM_RCS_Thruster_{k + 1}', r=0.07, h=0.2, loc=((RSM + 0.2) * math.cos(a), (RSM + 0.2) * math.sin(a), 96.6 + s * 0.45),
                 rot=(0 if s > 0 else math.pi, 0, 0), material=NOZZLE, seg=10)

# ---------------------------------------------------------------- 발사 탈출 시스템(LES)
def strut(name, p0, p1, r, material):
    p0, p1 = Vector(p0), Vector(p1)
    d = p1 - p0
    ob = cylinder(name, r=r, h=d.length, loc=p0, material=material, seg=8)
    ob.rotation_euler = Vector((0, 0, 1)).rotation_difference(d).to_euler()
    return ob


T0, T1 = 101.0, 104.8
legs = []
w0, w1 = 0.62, 0.36
corners = [(1, 1), (-1, 1), (-1, -1), (1, -1)]
lev = [T0, T0 + 1.27, T0 + 2.53, T1]
def cp(c, z):
    w = w0 + (w1 - w0) * (z - T0) / (T1 - T0)
    return (c[0] * w, c[1] * w, z)
tower = []
for c in corners:
    tower.append(strut('LES_Tower_Leg', cp(c, T0 - 0.3), cp(c, T1), 0.06, LES_RED))
for i in range(4):
    c0, c1 = corners[i], corners[(i + 1) % 4]
    for b in range(3):
        za, zb = lev[b], lev[b + 1]
        tower.append(strut('LES_Tower_Brace', cp(c0, za), cp(c1, zb), 0.035, LES_RED))
        tower.append(strut('LES_Tower_Brace', cp(c1, za), cp(c0, zb), 0.035, LES_RED))
    for b in range(4):
        tower.append(strut('LES_Tower_Ring', cp(c0, lev[b]), cp(c1, lev[b]), 0.035, LES_RED))
join(tower, 'LES_Tower_Truss')
lathe('LES_Escape_Motor', [(0, T1 - 0.05), (0.40, T1 - 0.05), (0.40, T1 + 0.25), (0.33, T1 + 0.6), (0.33, 108.6), (0.30, 109.0),
                           (0.20, 109.0), (0.20, 109.6), (0.15, 110.25), (0.06, 110.5), (0, 110.6)], material=WHITE, seg=32)
lathe('LES_Nose_Cone', [(0, 108.95), (0.31, 108.95), (0.31, 109.25), (0.21, 109.9), (0.11, 110.35), (0.0, 110.42)], material=BLACK, seg=32)
for k in range(4):
    a = math.radians(45 + 90 * k)
    n = cylinder(f'LES_Motor_Nozzle_{k + 1}', r=0.13, r2=0.07, h=0.45, loc=(0.30 * math.cos(a), 0.30 * math.sin(a), T1 + 0.35),
                 material=NOZZLE, seg=12, base=False)
    # 노즐 축(로컬 +Z, 좁은 쪽)이 위·안쪽을 향하도록 기울인다 → 넓은 출구가 바깥 아래로
    n.rotation_euler = Vector((0, 0, 1)).rotation_difference(Vector((-math.cos(a) * 0.55, -math.sin(a) * 0.55, 1)).normalized()).to_euler()


# ---------------------------------------------------------------- 성조기와 USA 글자 (곡면에 감기)
def wrap_mesh(me, R, theta_c, z_c, lift=0.012):
    for v in me.vertices:
        u, w, d = v.co
        ang = theta_c + u / R
        rr = R + lift + d
        v.co = (rr * math.cos(ang), rr * math.sin(ang), z_c + w)
    me.update()


def wrapped_text(name, body, size, R, theta_c, z_c, depth, material):
    t = text3d(name + '_src', body, size=size, depth=depth)
    t.data.align_y = 'CENTER'
    t.data.offset = size * 0.035  # 글자 획을 굵게
    dg = bpy.context.evaluated_depsgraph_get()
    me = bpy.data.meshes.new_from_object(t.evaluated_get(dg))
    bpy.data.objects.remove(t, do_unlink=True)
    for v in me.vertices:
        v.co.z += depth  # 뒷면을 0에 맞춘다
    wrap_mesh(me, R, theta_c, z_c)
    me.materials.append(material)
    ob = bpy.data.objects.new(name, me)
    link(ob)
    return ob


FRONT = math.radians(-90)
for k, ch in enumerate('USA'):
    wrapped_text(f'Lettering_{ch}', ch, 3.3, R1, FRONT, 34.6 - k * 3.1, 0.03, BLACK)


def flag(name, R, theta_c, z_c, wid, hgt):
    bm = bmesh.new()
    cols, rows = 24, 13
    vs = [[bm.verts.new((-wid / 2 + wid * i / cols, hgt / 2 - hgt * j / rows, 0)) for i in range(cols + 1)] for j in range(rows + 1)]
    for j in range(rows):
        for i in range(cols):
            f = bm.faces.new((vs[j + 1][i], vs[j + 1][i + 1], vs[j][i + 1], vs[j][i]))
            if j < 7 and i < 10:
                f.material_index = 2
            else:
                f.material_index = 0 if j % 2 == 0 else 1
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    wrap_mesh(me, R, theta_c, z_c, lift=0.02)
    for m in (RED, WHITE, BLUE):
        me.materials.append(m)
    ob = bpy.data.objects.new(name, me)
    link(ob)
    return ob


flag('US_Flag_SIC', R1, FRONT, 37.55, 4.4, 2.4)

# 발사탑 연결(엄빌리컬) 접속판 — 한쪽 옆면을 따라 단마다 하나씩
UMB = math.radians(150)
for nm, r, z, w, h in [('Umbilical_Plate_SIC', R1, 40.4, 1.0, 0.8), ('Umbilical_Plate_SII_Aft', R1, 49.5, 1.0, 0.8),
                       ('Umbilical_Plate_SII_Fwd', R1, 64.6, 1.0, 0.8), ('Umbilical_Plate_SIVB', R3, 79.0, 0.8, 0.7),
                       ('Umbilical_Plate_IU', R3, 85.45, 0.7, 0.5), ('Umbilical_Plate_SM', RSM, 95.6, 0.5, 0.5)]:
    p = box(nm, size=(0.24, w, h), loc=(r * math.cos(UMB), r * math.sin(UMB), z), rot=(0, 0, UMB), material=HEAT)
    bevel(p, 0.03, 1)


# ---------------------------------------------------------------- 이동식 발사대(ML) 갑판과 고정 팔
# 실제 갑판은 약 49m x 41m, 두께 7.6m 이고 가운데 13.7m 정사각 배기 구멍 위에 로켓이 선다. 엔진은 구멍 안으로 내려간다.
ML_TOP, ML_H, ML_X, ML_Y, HOLE = 2.0, 7.6, 49.0, 41.0, 13.7


def launcher_deck(name):
    bm = bmesh.new()
    ho, hx, hy = HOLE / 2, ML_X / 2, ML_Y / 2
    outer = [(-hx, -hy), (hx, -hy), (hx, hy), (-hx, hy)]
    inner = [(-ho, -ho), (ho, -ho), (ho, ho), (-ho, ho)]
    z0, z1 = ML_TOP - ML_H, ML_TOP
    ot = [bm.verts.new((x, y, z1)) for x, y in outer]
    ob_ = [bm.verts.new((x, y, z0)) for x, y in outer]
    it = [bm.verts.new((x, y, z1)) for x, y in inner]
    ib = [bm.verts.new((x, y, z0)) for x, y in inner]
    for i in range(4):
        k = (i + 1) % 4
        bm.faces.new((ot[i], ot[k], it[k], it[i]))      # 윗면
        bm.faces.new((ob_[k], ob_[i], ib[i], ib[k]))    # 밑면
        bm.faces.new((ob_[i], ob_[k], ot[k], ot[i]))    # 바깥 옆면
        bm.faces.new((it[i], it[k], ib[k], ib[i]))      # 구멍 안쪽 면
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    me.materials.append(ML_STEEL)
    ob = bpy.data.objects.new(name, me)
    link(ob)
    return ob


launcher_deck('Mobile_Launcher_Deck')

# 고정(홀드다운) 팔: 갑판 윗면에서 1단 바닥(Z_BASE)까지 받친다
arm = box('HoldDown_Arm', size=(2.2, 1.4, Z_BASE - ML_TOP), loc=(0, 0, ML_TOP), material=ML_STEEL, base=True)
bevel(arm, 0.08, 1)
for k in range(4):
    a = math.radians(90 * k)
    h = arm if k == 0 else duplicate(arm)
    h.location = ((R1 + 0.2) * math.cos(a), (R1 + 0.2) * math.sin(a), ML_TOP)
    h.rotation_euler.z = a
    h.name = f'HoldDown_Arm_{k + 1}'

# 꼬리 서비스 마스트 3기: 갑판 위에 선 철탑 상자(연료·전원 연결용)
tsm = box('Tail_Service_Mast', size=(2.4, 2.4, 6.5), loc=(0, 0, ML_TOP), material=LES_RED, base=True)
bevel(tsm, 0.1, 1)
for k, a in enumerate((math.radians(110), math.radians(160), math.radians(-160))):
    t = tsm if k == 0 else duplicate(tsm)
    t.location = (11.5 * math.cos(a), 11.5 * math.sin(a), ML_TOP)
    t.rotation_euler.z = a
    t.name = f'Tail_Service_Mast_{k + 1}'
