# 우주왕복선 발사 형상(STS 스택) — 오비터 + 외부연료탱크(ET) + 고체로켓부스터(SRB) 2기.
# 오비터 동체는 단면표(폭·바닥·지붕 높이)를 보간해 수십 개의 슈퍼타원 단면을 이어 붙인 로프트 메시이고,
# 이중 삼각익과 수직꼬리날개는 평면형 위에 익형 두께 분포를 얹어 bmesh 로 직접 짰다.
# 단열 타일의 흑백 경계, 창문, 앞전 RCC 는 면 위치에 따라 재질 슬롯을 나눠 칠했다.
# ET·SRB·엔진 노즐은 회전체(lathe), 인터탱크 주름과 SRB 이음 링은 루프로 만들었다. 단위 m, 실제 크기.
import math
from atelier import *

SHOT = {"azimuth": -70, "elevation": 8, "lens": 60, "light": "day", "background": "#1d2533"}

# ---------------------------------------------------------------- 재질
M_WHITE = mat('Orbiter_White_Tiles', '#e6e4dd', 0.62)
M_BLACK = mat('Orbiter_Black_Tiles', '#222326', 0.75)
M_RCC = mat('RCC_Leading_Edge_Grey', '#5c5e61', 0.6)
M_GLASS = mat('Cockpit_Window', '#1a2530', 0.08, 0.3)
M_BASE = mat('Aft_Heat_Shield', '#3b3936', 0.8)
M_SSME = mat('SSME_Nozzle_Metal', '#a39a8a', 0.35, 0.85)
M_ET = mat('ET_Foam_Orange', '#ad5b2b', 0.85)
M_IT = mat('ET_Intertank_Foam', '#9b4f28', 0.85)
M_SRB = mat('SRB_White_Paint', '#e2e0d8', 0.5)
M_JOINT = mat('SRB_Joint_Band', '#9c9b96', 0.55, 0.2)
M_SRBN = mat('SRB_Nozzle_Ablative', '#4a4643', 0.8)
M_STRUT = mat('Attach_Strut_Metal', '#8d9095', 0.4, 0.7)
M_SEAM = mat('Panel_Seam_Grey', '#6f6e6a', 0.7)


def build_mesh(name, verts, faces, mats=None, mat_idx=None, smooth_all=True):
    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.validate()
    ob = bpy.data.objects.new(name, me)
    link(ob)
    for m in (mats or []):
        me.materials.append(m)
    if mat_idx is not None:
        me.polygons.foreach_set('material_index', mat_idx)
    if smooth_all:
        for p in me.polygons:
            p.use_smooth = True
    return ob


def pchip(xs, ys):
    """단조 3차 보간 함수(과도한 출렁임 없이 단면표를 매끄럽게)."""
    n = len(xs)
    h = [xs[i + 1] - xs[i] for i in range(n - 1)]
    d = [(ys[i + 1] - ys[i]) / h[i] for i in range(n - 1)]
    m = [0.0] * n
    m[0], m[-1] = d[0], d[-1]
    for i in range(1, n - 1):
        if d[i - 1] * d[i] <= 0:
            m[i] = 0.0
        else:
            w1, w2 = 2 * h[i] + h[i - 1], h[i] + 2 * h[i - 1]
            m[i] = (w1 + w2) / (w1 / d[i - 1] + w2 / d[i])

    def f(x):
        if x <= xs[0]:
            return ys[0]
        if x >= xs[-1]:
            return ys[-1]
        i = max(k for k in range(n - 1) if xs[k] <= x)
        t = (x - xs[i]) / h[i]
        h00, h10 = 2 * t ** 3 - 3 * t ** 2 + 1, t ** 3 - 2 * t ** 2 + t
        h01, h11 = -2 * t ** 3 + 3 * t ** 2, t ** 3 - t ** 2
        return h00 * ys[i] + h10 * h[i] * m[i] + h01 * ys[i + 1] + h11 * h[i] * m[i + 1]
    return f


def spow(x, p):
    return math.copysign(abs(x) ** p, x)


# ================================================================ 오비터 (로컬 좌표: x=기수 방향 u, y=우익 v, z=지붕 w)
orbiter = bpy.data.objects.new('Orbiter', None)
link(orbiter)
ORB_PARTS = []

# 단면표: u, 반폭, 바닥높이, 지붕높이
ST = [
    (0.0, 2.65, 0.00, 5.30),
    (21.0, 2.65, 0.00, 5.30),
    (23.0, 2.65, 0.00, 5.45),
    (25.0, 2.65, 0.03, 5.95),
    (26.3, 2.62, 0.07, 6.05),
    (27.5, 2.52, 0.16, 5.62),
    (28.6, 2.30, 0.30, 4.72),
    (29.8, 1.95, 0.52, 4.05),
    (30.9, 1.50, 0.82, 3.45),
    (31.8, 1.00, 1.12, 2.95),
    (32.4, 0.58, 1.42, 2.55),
    (32.8, 0.12, 1.80, 2.15),
]
f_hw = pchip([s[0] for s in ST], [s[1] for s in ST])
f_wb = pchip([s[0] for s in ST], [s[2] for s in ST])
f_wt = pchip([s[0] for s in ST], [s[3] for s in ST])
NOSE = 32.9


def black_line(u):
    """옆면 흑백 타일 경계 높이(기수 쪽으로 올라간다)."""
    if u < 22:
        return 1.05
    t = min(1.0, (u - 22) / (NOSE - 22))
    return 1.05 + 1.25 * t * t


def sec(u):
    hw, wb, wt = f_hw(u), f_wb(u), f_wt(u)
    return hw, wb, wt, wb + 0.36 * (wt - wb)


def fus_point(u, a, grow=0.0):
    """동체 단면(u, 각도 a) 위의 점. 각도 0=우측, 90°=지붕. grow 만큼 바깥으로."""
    hw, wb, wt, wc = sec(u)
    hw, wb, wt = hw + grow, wb - grow, wt + grow
    c, s_ = math.cos(a), math.sin(a)
    y = hw * spow(c, 0.30 if s_ < 0 else 0.55)
    z = wc + (wt - wc) * spow(s_, 0.62) if s_ >= 0 else wc + (wc - wb) * spow(s_, 0.25)
    return (u, y, z)


def boundary_angle(u):
    hw, wb, wt, wc = sec(u)
    zb = min(black_line(u), wb + 0.8 * (wt - wb))
    if zb < wc:
        return -math.asin(min(1.0, ((wc - zb) / (wc - wb)) ** 4))
    return math.asin(min(1.0, ((zb - wc) / (wt - wc)) ** (1 / 0.62)))


NB, NT = 14, 18           # 반쪽 링의 아래(검은 타일)/위(흰 타일) 분할 수


def ring_angles(u):
    ab = boundary_angle(u)
    out = [-math.pi / 2 + (ab + math.pi / 2) * k / NB for k in range(NB)]
    out += [ab + (math.pi / 2 - ab) * k / NT for k in range(NT)]
    return out + [math.pi / 2] + [math.pi - a for a in reversed(out[1:])]


def fuselage():
    us = []
    u = 0.0
    while u < NOSE - 0.05:
        us.append(u)
        u += 0.9 if u < 20 else (0.35 if u < 26 else 0.16)
    us.append(NOSE - 0.04)
    N = 2 * (NB + NT)
    verts, faces, mi = [], [], []
    for u in us:
        for a in ring_angles(u):
            verts.append(fus_point(u, a))
    R = len(us)
    for i in range(R - 1):
        for k in range(N):
            a0, a1 = i * N + k, i * N + (k + 1) % N
            faces.append((a0, a1, a1 + N, a0 + N))
            cu = (us[i] + us[i + 1]) / 2
            bottom = k < NB or k >= N - NB
            m = 1 if bottom else 0
            if cu > NOSE - 0.75:
                m = 2                                    # 기수 RCC
            mi.append(m)
    tip = len(verts)
    verts.append((NOSE, 0.0, f_wb(NOSE) * 0.5 + f_wt(NOSE) * 0.5))
    for k in range(N):
        faces.append(((R - 1) * N + k, (R - 1) * N + (k + 1) % N, tip))
        mi.append(2)
    faces.append(tuple(reversed(range(N))))
    mi.append(4)
    return build_mesh('Orbiter_Fuselage', verts, faces, [M_WHITE, M_BLACK, M_RCC, M_GLASS, M_BASE], mi)


def surface_patch(name, u0, u1, a0, a1, grow, material, nu=8, na=10):
    """동체 표면을 grow 만큼 띄워 따라가는 얇은 패널(창문·검은 마스크)."""
    verts, faces = [], []
    for i in range(nu + 1):
        u = u0 + (u1 - u0) * i / nu
        for j in range(na + 1):
            verts.append(fus_point(u, a0 + (a1 - a0) * j / na, grow))
    for i in range(nu):
        for j in range(na):
            a = i * (na + 1) + j
            faces.append((a, a + na + 1, a + na + 2, a + 1))
    return build_mesh(name, verts, faces, [material])


ORB_PARTS.append(fuselage())

D = math.radians
ORB_PARTS.append(surface_patch('Cockpit_Black_Mask', 27.25, 28.95, D(34), D(146), 0.008, M_BLACK, 12, 28))
for k in range(3):
    lo = 50 + k * 13.5
    for a0, a1 in ((lo, lo + 12), (180 - lo - 12, 180 - lo)):
        ORB_PARTS.append(surface_patch('Forward_Windshield', 27.62, 28.62, D(a0), D(a1), 0.018, M_GLASS, 6, 4))
for a0, a1 in ((74, 87), (93, 106)):
    ORB_PARTS.append(surface_patch('Overhead_Window', 26.95, 27.45, D(a0), D(a1), 0.016, M_GLASS, 4, 4))
for a0, a1 in ((20, 36), (144, 160)):
    ORB_PARTS.append(surface_patch('Side_Hatch_Window', 26.35, 26.95, D(a0), D(a1), 0.016, M_GLASS, 4, 4))



def airfoil_t(xa):
    """앞전 0 → 뒷전 1 의 두께 분포(NACA 4자리형)."""
    x = xa
    return 1.0 * (0.2969 * math.sqrt(x) - 0.1260 * x - 0.3516 * x * x + 0.2843 * x ** 3 - 0.1036 * x ** 4) / 0.5 + 0.012


def hinge_xs(K1, K2, xh, g=0.006):
    """앞전 0→뒷전 1 시위 분할. 인덱스 K1~K1+1 사이가 힌지선 띠가 되도록 고정."""
    a = [(xh - g) * (1 - math.cos(math.pi / 2 * k / K1)) for k in range(K1 + 1)]
    b = [xh + g + (1 - xh - g) * k / K2 for k in range(K2 + 1)]
    return a + b


def wings():
    # 평면형: 스트레이크(81°) + 주익(45°), 반스팬 11.9 m
    def le(v):
        return 24.5 - (v - 2.6) * math.tan(math.radians(81)) if v <= 4.7 else 11.2 - (v - 4.7)

    def te(v):
        return 0.25 + 0.05 * (v - 2.6)

    vs = [2.25, 2.6, 3.1, 3.6, 4.2, 4.7, 5.3, 6.0, 6.6, 7.22, 7.32, 8.0, 9.0, 10.0, 10.8, 11.4, 11.9]
    K1, K2 = 20, 5
    K = K1 + K2 + 1
    verts, faces, mi = [], [], []
    loop_n = 2 * K
    for side in (1, -1):
        base = len(verts)
        for v in vs:
            u_le, u_te = le(v), te(v)
            chord = u_le - u_te
            u_lem = 4.0 + (11.9 - v)                  # 주익 앞전 연장선
            T = 0.12 * (u_lem - u_te)
            w0 = 0.32 + (v - 2.6) * 0.058
            xs = hinge_xs(K1, K2, 1 - 2.3 / chord)
            ring = []
            for k in range(loop_n):
                # 0..K 윗면(앞전→뒷전), K+1..2K-1 아랫면(뒷전→앞전)
                xa = xs[k] if k <= K else xs[2 * K - k]
                u = u_le - xa * chord
                xm = (u_lem - u) / (u_lem - u_te)
                if xm < 0:                             # 스트레이크 구간은 얇게
                    th = T * airfoil_t(min(1, xa * chord / 3.0)) * max(0.22, 0.55 + 0.04 * xm)
                else:
                    th = T * airfoil_t(min(1.0, max(0.0, xm))) if xa * chord > 0.6 else T * airfoil_t(xm if xm > 0 else 0) * math.sqrt(xa * chord / 0.6)
                z = w0 + 0.8 * th if k <= K else w0 - 0.2 * th
                ring.append((u, side * v, z))
            verts.extend(ring)
        S = len(vs)
        for i in range(S - 1):
            for k in range(loop_n):
                a0 = base + i * loop_n + k
                a1 = base + i * loop_n + (k + 1) % loop_n
                f = (a0, a1, a1 + loop_n, a0 + loop_n)
                faces.append(f if side == 1 else tuple(reversed(f)))
                kk = k + 0.5
                vm = (vs[i] + vs[i + 1]) / 2
                if kk < K:
                    xa = xs[k]
                    m = 2 if xa < 0.035 else (1 if xa < 0.09 else 0)
                    if vm > 3.3 and (k == K1 or (k > K1 and 7.22 < vm < 7.32)):
                        m = 3                          # 엘레본 힌지·분할선
                    mi.append(m)
                else:
                    xa = xs[2 * K - k - 1] if k < 2 * K - 1 else 0
                    mi.append(2 if xa < 0.02 else 1)
        tipring = list(range(base + (S - 1) * loop_n, base + S * loop_n))
        rootring = list(range(base, base + loop_n))
        faces.append(tuple(tipring) if side == -1 else tuple(reversed(tipring)))
        mi.append(2)
        faces.append(tuple(rootring) if side == 1 else tuple(reversed(rootring)))
        mi.append(1)
    ob = build_mesh('Orbiter_Wings', verts, faces, [M_WHITE, M_BLACK, M_RCC, M_SEAM], mi)
    return ob


ORB_PARTS.append(wings())


def vertical_tail():
    ws = [4.9, 5.4, 6.5, 7.6, 8.8, 10.0, 11.2, 12.4, 13.4, 14.3]
    K1, K2 = 13, 6
    K = K1 + K2 + 1
    xs = hinge_xs(K1, K2, 0.64, 0.004)
    verts, faces, mi = [], [], []
    loop_n = 2 * K
    for w in ws:
        u_le = 8.8 - (w - 5.4) * 1.0
        u_te = -1.0 - (w - 5.4) * 0.19
        chord = u_le - u_te
        T = 0.075 * chord
        for k in range(loop_n):
            xa = xs[k] if k <= K else xs[2 * K - k]
            side = 1 if k <= K else -1
            th = T * airfoil_t(xa) * 0.5
            verts.append((u_le - xa * chord, side * th, w))
    S = len(ws)
    for i in range(S - 1):
        for k in range(loop_n):
            a0 = i * loop_n + k
            a1 = i * loop_n + (k + 1) % loop_n
            faces.append((a0, a0 + loop_n, a1 + loop_n, a1))
            j = k if k < K else 2 * K - k - 1
            m = 2 if xs[j] < 0.03 else 0
            if j == K1 and ws[i] >= 5.4:
                m = 3                                    # 러더/스피드브레이크 힌지선
            mi.append(m)
    faces.append(tuple(range((S - 1) * loop_n, S * loop_n)))
    mi.append(2)
    faces.append(tuple(reversed(range(loop_n))))
    mi.append(0)
    return build_mesh('Orbiter_Vertical_Tail', verts, faces, [M_WHITE, M_BLACK, M_RCC, M_SEAM], mi)


ORB_PARTS.append(vertical_tail())


def oms_pod(side):
    us = [-0.9, -0.75, -0.5, 0.0, 2.0, 4.0, 5.5, 7.0, 8.5, 9.8, 10.8]
    sc = [0.45, 0.82, 0.95, 1.0, 1.0, 0.98, 0.9, 0.74, 0.52, 0.3, 0.06]
    N = 32
    verts, faces = [], []
    for u, s in zip(us, sc):
        for k in range(N):
            a = 2 * math.pi * k / N
            verts.append((u, side * 1.95 + 1.15 * s * math.cos(a), 4.8 + 1.15 * s * math.sin(a) - (1 - s) * 0.45))
    R = len(us)
    for i in range(R - 1):
        for k in range(N):
            a0, a1 = i * N + k, i * N + (k + 1) % N
            f = (a0, a1, a1 + N, a0 + N)
            faces.append(f if side > 0 else tuple(reversed(f)))
    faces.append(tuple(reversed(range(N))) if side > 0 else tuple(range(N)))
    faces.append(tuple(range((R - 1) * N, R * N)) if side > 0 else tuple(reversed(range((R - 1) * N, R * N))))
    ob = build_mesh('Orbiter_OMS_Pod_' + ('R' if side > 0 else 'L'), verts, faces, [M_WHITE])
    return ob


for sd in (1, -1):
    ORB_PARTS.append(oms_pod(sd))


def bell(name, r0, r1, L, material, seg=40):
    prof = []
    n = 14
    for i in range(n + 1):
        t = i / n
        prof.append((r0 + (r1 - r0) * (t ** 0.62), -L * t))
    ob = lathe(name, prof, material=material, seg=seg)
    solidify(ob, 0.04)
    return ob


# 주 엔진(SSME) 3기: 위 1, 아래 2 — 노즐은 -u 방향
ssme_proto = bell('SSME_Nozzle', 0.52, 1.15, 3.0, M_SSME)
ssme_proto.rotation_euler = (0, math.radians(-90), 0)
ENG_POS = [(0.0, 4.1), (1.38, 1.85), (-1.38, 1.85)]
for i, (v, w) in enumerate(ENG_POS):
    ob = ssme_proto if i == 0 else duplicate(ssme_proto, name='SSME_Nozzle')
    ob.location = (-0.55, v, w)
    mount = cylinder('SSME_Powerhead_Mount', r=0.62, h=0.6, loc=(-0.6, v, w), rot=(0, math.radians(90), 0), material=M_BASE, seg=24)
    ORB_PARTS += [ob, mount]

# OMS 엔진 노즐
oms_n = bell('OMS_Nozzle', 0.28, 0.55, 1.3, M_SSME, seg=28)
oms_n.rotation_euler = (0, math.radians(-90), 0)
oms_n.location = (-0.85, 1.95, 4.85)
ORB_PARTS += [oms_n, duplicate(oms_n, loc=(-0.85, -1.95, 4.85))]

# 보디 플랩
flap = prism('Orbiter_Body_Flap', [(-2.55, -3.0), (0.1, -3.05), (0.1, 3.05), (-2.55, 3.0)], 0.5, loc=(0, 0, 0.06), material=M_BLACK)
ORB_PARTS.append(flap)




# 화물칸 도어: 가로 이음선 5줄, 힌지선 2줄, 중앙선 1줄
A_HINGE = math.radians(28)
for i, u in enumerate((4.2, 8.75, 13.3, 17.85, 22.4)):
    pts = [fus_point(u, A_HINGE + (math.pi - 2 * A_HINGE) * k / 24, 0.012) for k in range(25)]
    ORB_PARTS.append(curve_tube('Payload_Bay_Door_Seam', pts, radius=0.03, material=M_SEAM, bevel_res=1))
for a in (A_HINGE, math.pi - A_HINGE, math.pi / 2):
    pts = [fus_point(4.2 + 18.2 * k / 20, a, 0.012) for k in range(21)]
    ORB_PARTS.append(curve_tube('Payload_Bay_Hinge_Line', pts, radius=0.03, material=M_SEAM, bevel_res=1))

parent_to(ORB_PARTS, orbiter)

# ================================================================ 스택 배치(월드)
ET_R = 4.2
ET_Z0 = 8.0
ORB_X = ET_R + 1.35          # 오비터 배면(w=0)의 X
ORB_Z = 6.6                  # 오비터 후미(u=0)의 Z
orbiter.matrix_world = Matrix(((0, 0, 1, ORB_X), (0, -1, 0, 0), (1, 0, 0, ORB_Z), (0, 0, 0, 1)))

# ================================================================ 외부연료탱크(ET)
lh2 = [(0.0, ET_Z0)]
for i in range(1, 9):
    t = i / 8 * math.pi / 2
    lh2.append((ET_R * math.sin(t), ET_Z0 + 1.7 * (1 - math.cos(t))))
lh2 += [(ET_R, 33.5), (0.0, 33.5)]
et_lh2 = lathe('ET_LH2_Tank', lh2, material=M_ET, seg=96)

lox = [(0.0, 40.2), (ET_R, 40.2), (ET_R, 42.0)]
L, R = 12.3, ET_R
rho = (R * R + L * L) / (2 * R)
for i in range(1, 25):
    zz = L * i / 24
    r = math.sqrt(max(0.0, rho * rho - zz * zz)) + R - rho
    if r < 0.62:
        break
    lox.append((r, 42.0 + zz))
ztop = lox[-1][1]
lox += [(0.55, ztop + 0.05), (0.22, ztop + 0.9), (0.06, ztop + 1.25), (0.0, ztop + 1.3)]
et_lox = lathe('ET_LOX_Tank', lox, material=M_ET, seg=96)

# 인터탱크: 주름(스트링거) 단면
def intertank():
    N = 216
    z0, z1 = 33.35, 40.35
    verts, faces = [], []
    for z in (z0, z1):
        for k in range(N):
            a = 2 * math.pi * k / N
            r = ET_R + (0.07 if (k // 2) % 3 == 0 else 0.03)
            verts.append((r * math.cos(a), r * math.sin(a), z))
    for k in range(N):
        faces.append((k, (k + 1) % N, N + (k + 1) % N, N + k))
    return build_mesh('ET_Intertank', verts, faces, [M_IT], smooth_all=False)


et_it = intertank()
for zz in (33.4, 40.3):
    torus('ET_Flange_Ring', R=ET_R + 0.06, r=0.09, loc=(0, 0, zz), material=M_IT, seg=96, rseg=8)

# ================================================================ 고체로켓부스터(SRB)
SRB_R = 1.855
SRB_Y = ET_R + SRB_R + 0.95
srb_prof = [(0.0, 3.0), (1.72, 3.0), (2.5, 0.0), (2.66, 0.0), (2.62, 0.3), (1.87, 3.4), (SRB_R, 3.45), (SRB_R, 39.0)]
for i in range(1, 7):
    t = i / 6
    srb_prof.append((SRB_R - 0.33 * t, 39.0 + 1.9 * t))
zc = 40.9
for i in range(1, 13):
    t = i / 12
    r = 1.525 * (1 - t) ** 1.25 + 0.1 * (1 - t)
    srb_prof.append((max(r, 0.12), zc + 4.35 * t))
srb_prof += [(0.06, 45.36), (0.0, 45.42)]
srb_body = lathe('SRB_Case', srb_prof, material=M_SRB, seg=64)
srb_noz = bell('SRB_Nozzle', 0.7, 1.75, 2.8, M_SRBN, seg=48)
srb_noz.location = (0, 0, 3.0)
SRB_PARTS = [srb_body, srb_noz]
for zz in (3.45, 11.4, 19.4, 27.4, 35.4, 38.9):
    SRB_PARTS.append(cylinder('SRB_Field_Joint', r=SRB_R + 0.035, h=0.38, loc=(0, 0, zz - 0.19), material=M_JOINT, seg=64, cap=False))
SRB_PARTS.append(cylinder('SRB_Aft_Attach_Ring', r=SRB_R + 0.07, h=0.55, loc=(0, 0, 9.2), material=M_JOINT, seg=64, cap=False))
srb_L = bpy.data.objects.new('SRB_Left', None)
link(srb_L)
parent_to(SRB_PARTS, srb_L)
srb_L.location = (0, -SRB_Y, 0)
srb_R = bpy.data.objects.new('SRB_Right', None)
link(srb_R)
srb_R.location = (0, SRB_Y, 0)
for p in SRB_PARTS:
    c = duplicate(p, name=p.name)
    c.parent = srb_R

srb_R.rotation_euler = (0, 0, math.pi)

# SRB 부속: 시스템 터널, 홀드다운 포스트(로컬 좌표, 좌우 공유)
SRB_EXTRA = []
ta = math.atan2(-0.87, 0.5)
tun = box('SRB_Systems_Tunnel', (0.32, 0.26, 35.4), loc=((SRB_R + 0.1) * math.cos(ta), (SRB_R + 0.1) * math.sin(ta), 3.55), rot=(0, 0, ta), material=M_SRB, base=True)
SRB_EXTRA.append(tun)
for k in range(4):
    a = math.pi / 4 + k * math.pi / 2
    SRB_EXTRA.append(box('SRB_Holddown_Post', (0.55, 0.45, 0.7), loc=(2.62 * math.cos(a), 2.62 * math.sin(a), 0.0), rot=(0, 0, a), material=M_JOINT, base=True))
for p in SRB_EXTRA:
    p.parent = srb_L
    c = duplicate(p, name=p.name)
    c.parent = srb_R

# ================================================================ 결합 구조(월드)
def strut(name, a, b, r=0.12, material=M_STRUT):
    return curve_tube(name, [a, b], radius=r, material=material, bevel_res=2)


# 오비터 앞 바이포드 / 뒤 결합부
strut('ET_Orbiter_Forward_Bipod', (4.05, 1.1, 29.6), (ORB_X + 0.05, 0.0, ORB_Z + 24.3), 0.13)
strut('ET_Orbiter_Forward_Bipod', (4.05, -1.1, 29.6), (ORB_X + 0.05, 0.0, ORB_Z + 24.3), 0.13)
box('Forward_Bipod_Fitting', (0.6, 0.5, 0.5), loc=(ORB_X - 0.05, 0, ORB_Z + 24.3), material=M_STRUT)
torus('ET_Aft_Attach_Ring', R=ET_R + 0.05, r=0.12, loc=(0, 0, 10.4), material=M_STRUT, seg=96, rseg=8)
for sy in (1, -1):
    strut('ET_Orbiter_Aft_Strut', (4.0, sy * 1.3, 10.4), (ORB_X + 0.05, sy * 1.9, ORB_Z + 2.4), 0.16)
    strut('ET_Orbiter_Aft_Diagonal', (4.0, sy * 1.3, 10.4), (ORB_X + 0.05, sy * 0.4, ORB_Z + 3.4), 0.11)
box('ET_Aft_Crossbeam', (0.5, 3.2, 0.45), loc=(4.05, 0, 10.4), material=M_STRUT)

# LOX 피드라인(오비터 쪽 +Y) 과 LH2 엄빌리컬(-Y), 케이블 트레이
fa = math.radians(22)
fx, fy = (ET_R + 0.32) * math.cos(fa), (ET_R + 0.32) * math.sin(fa)
curve_tube('ET_LOX_Feedline', [(fx, fy, 36.8), (fx, fy, 11.6), (fx + 0.35, fy, 10.6), (ORB_X - 0.3, fy, ORB_Z + 1.5)], radius=0.22, material=M_ET, bevel_res=3)
for zz in range(13, 36, 3):
    box('ET_Feedline_Bracket', (0.5, 0.18, 0.18), loc=(fx * 0.965, fy * 0.965, zz), rot=(0, 0, fa), material=M_ET)
curve_tube('ET_LH2_Umbilical_Line', [(3.35, -1.7, 9.1), (4.4, -1.7, 8.5), (ORB_X - 0.3, -1.7, ORB_Z + 1.5)], radius=0.2, material=M_ET, bevel_res=3)
for sy in (1, -1):
    box('Orbiter_ET_Umbilical_Plate', (0.45, 1.1, 1.3), loc=(ORB_X - 0.15, sy * 1.7, ORB_Z + 1.5), material=M_BASE)
ca = math.radians(-24)
cx, cy = (ET_R + 0.1) * math.cos(ca), (ET_R + 0.1) * math.sin(ca)
box('ET_Cable_Tray', (0.24, 0.42, 29.5), loc=(cx, cy, 10.8), rot=(0, 0, ca), material=M_ET, base=True)
# LOX 탱크 오자이브를 타고 오르는 가압 라인
pa = math.radians(-24)
pts = [(r + 0.16, z) for r, z in lox[2:] if z <= ztop]
curve_tube('ET_GOX_Pressurization_Line', [(r * math.cos(pa), r * math.sin(pa), z) for r, z in pts], radius=0.09, material=M_ET, bevel_res=2)

# SRB-ET 결합: 앞(인터탱크) 볼트 박스, 뒤 스트럿 3개
for sy in (1, -1):
    box('SRB_Forward_Attach', (0.7, SRB_Y - SRB_R - ET_R + 0.5, 0.6), loc=(0, sy * (ET_R + SRB_Y - SRB_R) / 2, 37.6), material=M_STRUT)
    for x0, z0 in ((-1.1, 9.5), (0.0, 9.8), (1.1, 9.5)):
        strut('SRB_Aft_Attach_Strut', (x0, sy * (SRB_Y - math.sqrt(1.95 ** 2 - x0 ** 2) + 0.05), z0), (x0 * 0.85, sy * (math.sqrt(ET_R ** 2 - (x0 * 0.85) ** 2) - 0.08), 10.4), 0.13)
