# 허블 우주망원경 (Hubble Space Telescope) — 2009년 마지막 정비(SM4) 이후의 궤도 위 모습.
# 망원경 축을 X 축(+X 가 개구부)으로 눕혀 실제 크기(길이 약 13.2m, 후방 지름 4.2m)로 만들었다.
# 몸통은 회전체(lathe)로 후방 덮개·장비 구획(10각 베이)·전방 차광통을 쌓고, 차광통 안쪽은 어두운 내벽과
# 주경·부경 스파이더까지 넣었다. 태양전지판(SA3)은 셀 격자를 bmesh 루프로 한 장 만든 뒤
# 링크 복제로 네 장을 붙였고, 열린 개구부 덮개·고이득 안테나(포물면 회전체)·노란 손잡이(커브 튜브)를 더했다.
# 은박 단열재(MLI)와 흰 NOBL 덮개는 Principled 상수 재질로 구분했다. 전시용 받침대 위에 올려 둔다.
from atelier import *
import random

random.seed(1990)

SHOT = {"azimuth": -38, "elevation": 22, "lens": 50, "light": "studio", "background": "#1c2433"}

H = 5.6            # 망원경 축 높이(받침대 위)
R_AFT = 2.10       # 후방 덮개 반지름
R_ES = 2.24        # 장비 구획(10각) 외접 반지름
R_LS = 1.53        # 전방 셸·차광통 반지름
X_ES0, X_ES1 = 3.50, 5.00
X_FRONT = 13.05

M_MLI = mat('MLI_Silver_Foil', '#c8ccd2', rough=0.28, metal=1.0)
M_AFT = mat('Aft_Shroud_Aluminized', '#aeb3ba', rough=0.38, metal=0.9)
M_NOBL = mat('NOBL_White_Blanket', '#dedcd4', rough=0.62)
M_DARK = mat('Baffle_Black', '#16171a', rough=0.85)
M_MECH = mat('Mechanism_Gray', '#4a4e55', rough=0.45, metal=0.6)
M_CELL = mat('Solar_Cell_Blue', '#1a2f66', rough=0.2, metal=0.3)
M_FRAME = mat('Solar_Frame_Gold', '#b88d43', rough=0.35, metal=0.85)
M_RAIL = mat('Handrail_Yellow', '#d8a823', rough=0.5)
M_DISH = mat('HGA_Dish_White', '#d6d3ca', rough=0.55)
M_DOOR = mat('Aperture_Door_Coating', '#c3c6cb', rough=0.45, metal=0.35)
M_MIRROR = mat('Primary_Mirror', '#8fa3b8', rough=0.08, metal=1.0)
M_STAND = mat('Display_Stand_Graphite', '#2b2e34', rough=0.4, metal=0.5)

AX = (0, math.pi / 2, 0)   # lathe 의 Z 축 → 월드 +X


def P(x, r, th):
    """망원경 축 좌표(x, 반지름, 각도θ) → 월드 좌표. θ=0 은 +Y, 90° 는 위(+Z)."""
    return Vector((x, r * math.cos(th), H + r * math.sin(th)))


def axial(name, profile, x0, material, seg=64, sm=True):
    return lathe(name, profile, loc=(x0, 0, H), rot=AX, material=material, seg=seg, shade_smooth=sm)


def shell_patch(name, r, t, a0, a1, x0, x1, material, n=10):
    """원통 표면에 붙는 곡면 패널(두께 t)."""
    bm = bmesh.new()
    vs = []
    for rr in (r, r + t):
        ring = []
        for i in range(n + 1):
            a = a0 + (a1 - a0) * i / n
            ring.append((bm.verts.new(P(x0, rr, a)), bm.verts.new(P(x1, rr, a))))
        vs.append(ring)
    inn, out = vs
    for i in range(n):
        bm.faces.new((out[i][0], out[i + 1][0], out[i + 1][1], out[i][1]))
        bm.faces.new((inn[i][1], inn[i + 1][1], inn[i + 1][0], inn[i][0]))
        bm.faces.new((inn[i][0], inn[i + 1][0], out[i + 1][0], out[i][0]))
        bm.faces.new((out[i][1], out[i + 1][1], inn[i + 1][1], inn[i][1]))
    bm.faces.new((inn[0][0], out[0][0], out[0][1], inn[0][1]))
    bm.faces.new((inn[n][1], out[n][1], out[n][0], inn[n][0]))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new(name, me)
    link(ob)
    smooth(ob)
    return assign(ob, material)


def rail(name, x0, x1, r, th, h=0.13, rad=0.022):
    """축 방향 손잡이: 양 끝 받침이 있는 ㄷ자 관."""
    pts = [P(x0, r - 0.01, th), P(x0, r + h, th), P(x1, r + h, th), P(x1, r - 0.01, th)]
    return curve_tube(name, [tuple(p) for p in pts], radius=rad, material=M_RAIL, resolution=2, bevel_res=2)


def ring_band(name, x, r, w, t, material, seg=64):
    return axial(name, [(r - 0.01, -w / 2), (r + t, -w / 2), (r + t, w / 2), (r - 0.01, w / 2)], x, material, seg)


def crinkled_tube(name, r, x0, x1, material, rows, seg=96, amp=0.008, lip=None):
    """은박 단열재의 구김: 반지름을 고정 시드 난수로 조금씩 흔든 원통."""
    prof = []
    for i in range(rows + 1):
        x = x0 + (x1 - x0) * i / rows
        prof.append((r, x))
    if lip:
        prof.append((r - lip, x1))
    ob = axial(name, prof, 0.0, material, seg=seg)
    for v in ob.data.vertices:
        rr = math.hypot(v.co.x, v.co.y)
        if rr > r - 0.001 and x0 + 0.05 < v.co.z < x1 - 0.05:
            k = 1 + random.uniform(-amp, amp) / r
            v.co.x *= k
            v.co.y *= k
    return ob


# ---------------------------------------------------------------- 1. 몸통
# 후방 덮개(Aft Shroud) + 후방 격벽
axial('Aft_Shroud_Bulkhead_Edge', [(0.0, 0.0), (1.70, 0.0), (1.95, 0.05), (R_AFT, 0.20), (R_AFT, 0.30)], 0.0, M_AFT, seg=96)
crinkled_tube('Aft_Shroud', R_AFT, 0.30, X_ES0, M_AFT, rows=30, amp=0.005)
axial('Aft_Bulkhead_Plate', [(0.0, -0.03), (1.60, -0.03), (1.60, 0.01), (0.0, 0.01)], 0.0, M_MECH, sm=False)
for k in range(8):
    th = math.radians(22.5 + 45 * k)
    rib = box(f'Aft_Bulkhead_Rib_{k}', (0.06, 1.15, 0.10), loc=P(-0.06, 0.95, th), material=M_AFT)
    rib.rotation_euler = (th, 0, 0)
axial('Aft_Bulkhead_Center_Port', [(0.0, -0.10), (0.38, -0.10), (0.42, -0.03), (0.42, 0.0)], 0.0, M_AFT, seg=32)
ring_band('Aft_Shroud_Flange', 0.22, R_AFT, 0.08, 0.035, M_MECH)
ring_band('Aft_Shroud_Ring_Mid', 1.80, R_AFT, 0.05, 0.02, M_MECH)
ring_band('Aft_Shroud_Forward_Ring', X_ES0 - 0.04, R_AFT, 0.08, 0.06, M_MECH)

# 장비 구획(SSM Equipment Section): 10개 베이의 다각형 링
axial('Equipment_Section', [(R_AFT - 0.05, 0.0), (R_ES, 0.0), (R_ES, X_ES1 - X_ES0), (R_LS - 0.02, X_ES1 - X_ES0)],
      X_ES0, M_MLI, seg=10, sm=False)

# 전방 셸 + 차광통(Light Shield): 바깥 은박 + 앞 테두리
crinkled_tube('Forward_Shell_Light_Shield', R_LS, X_ES1 - 0.01, X_FRONT, M_MLI, rows=80, lip=0.07)
axial('Light_Shield_Inner_Baffle', [(R_LS - 0.07, X_FRONT), (R_LS - 0.07, 5.6), (0.0, 5.6)], 0.0, M_DARK)
ring_band('Forward_Shell_Joint_Ring', 9.0, R_LS, 0.10, 0.03, M_MECH)
for i, x in enumerate((6.0, 7.0, 8.0, 10.0, 11.0, 12.0)):
    ring_band(f'Light_Shield_Seam_{i}', x, R_LS, 0.035, 0.012, M_AFT)
ring_band('Light_Shield_Front_Lip', X_FRONT - 0.06, R_LS, 0.12, 0.04, M_MECH)

# 차광통 안: 주경(가운데 구멍), 내부 차광 링, 부경과 4갈래 스파이더
axial('Primary_Mirror', [(0.32, 5.62), (1.22, 5.62), (1.22, 5.70), (0.32, 5.70)], 0.0, M_MIRROR, sm=False)
for i, x in enumerate((7.4, 9.2, 11.0)):
    axial(f'Internal_Baffle_Ring_{i}', [(R_LS - 0.07, x), (R_LS - 0.22, x + 0.02), (R_LS - 0.22, x + 0.05), (R_LS - 0.07, x + 0.05)],
          0.0, M_DARK, sm=False)
axial('Secondary_Mirror_Housing', [(0.0, 10.15), (0.20, 10.15), (0.20, 10.55), (0.05, 10.62), (0.0, 10.62)], 0.0, M_MECH)
for k in range(4):
    th = math.radians(45 + 90 * k)
    a, b = P(10.38, 0.18, th), P(10.38, R_LS - 0.08, th)
    mid, d = (a + b) / 2, (b - a)
    vane = box(f'Spider_Vane_{k}', (0.30, d.length, 0.018), loc=mid, material=M_DARK)
    vane.rotation_euler = (th, 0, 0)

# ---------------------------------------------------------------- 2. 장비 베이 문(10개)
bay_w = 2 * R_ES * math.sin(math.radians(18)) * 0.86
apo = R_ES * math.cos(math.radians(18))
bay_mats = [None, M_NOBL, M_NOBL, M_MLI, M_NOBL, None, M_NOBL, M_MLI, M_NOBL, M_NOBL]
for k in range(10):
    th = math.radians(36 * k)
    c = P((X_ES0 + X_ES1) / 2, apo + 0.025, th)
    if bay_mats[k] is None:
        # 태양전지판 뿌리 부분: 구동 장치 상자
        d = box(f'SA_Drive_Housing_{k}', (1.25, 0.55, 0.10), loc=c, material=M_MECH)
    else:
        d = box(f'Equipment_Bay_Door_{k}', (1.30, bay_w, 0.05), loc=c, material=bay_mats[k])
        rail(f'Bay_Handrail_{k}', X_ES0 + 0.25, X_ES1 - 0.25, apo + 0.05, th + math.radians(10.5), h=0.10)
    d.rotation_euler = (th - math.pi / 2, 0, 0)

# ---------------------------------------------------------------- 3. 후방 덮개 문·손잡이
for k in range(4):
    th0 = math.radians(45 + 90 * k)
    shell_patch(f'Aft_Shroud_Door_{k}', R_AFT, 0.03, th0 - math.radians(28), th0 + math.radians(28), 0.55, 3.15, M_MLI)
    for s in (-1, 1):
        rail(f'Aft_Door_Handrail_{k}_{"L" if s < 0 else "R"}', 0.75, 2.95, R_AFT + 0.03, th0 + s * math.radians(33))
for k in range(4):
    th = math.radians(90 * k)
    rail(f'Aft_Shroud_Handrail_{k}', 0.40, 3.30, R_AFT, th)

# 후방 격벽 저이득 안테나
cylinder('Aft_LGA_Mast', 0.05, 0.55, loc=(0.0, 0, H + 1.1), rot=(0, -math.pi / 2, 0), material=M_MECH, seg=12)
cone('Aft_Low_Gain_Antenna', 0.16, 0.30, loc=(-0.55, 0, H + 1.1), rot=(0, -math.pi / 2, 0), material=M_DISH, seg=20)

# ---------------------------------------------------------------- 4. 전방 셸·차광통 외부 부품
# 자기 토커 막대 4개(NOBL 덮개)
for k in range(4):
    th = math.radians(45 + 90 * k)
    c = P(10.9, R_LS + 0.13, th)
    bar = cylinder(f'Magnetic_Torquer_{k}', 0.07, 3.2, loc=c, rot=(0, math.pi / 2, 0), material=M_NOBL, seg=16, base=False)
    for j, x in enumerate((9.5, 12.3)):
        st = box(f'Magnetic_Torquer_{k}_Standoff_{j}', (0.10, 0.08, 0.16), loc=P(x, R_LS + 0.06, th), material=M_MECH)
        st.rotation_euler = (th - math.pi / 2, 0, 0)
# 차광통 손잡이
for k, th in enumerate((0.0, math.pi)):
    rail(f'Light_Shield_Handrail_{k}', 6.2, 12.6, R_LS, th)
    rail(f'Forward_Shell_Handrail_{k}', 5.3, 8.6, R_LS, th + math.radians(25))
# 태양 센서·고정 별추적기 상자
for k, (x, th) in enumerate(((12.4, math.radians(-30)), (12.4, math.radians(210)), (5.6, math.radians(-125)))):
    s = box(f'Sun_Sensor_{k}', (0.35, 0.28, 0.18), loc=P(x, R_LS + 0.08, th), material=M_MECH)
    s.rotation_euler = (th - math.pi / 2, 0, 0)
# 전방 저이득 안테나
cylinder('Forward_LGA_Mast', 0.04, 0.9, loc=P(11.6, R_LS - 0.02, math.radians(-90)), rot=(math.pi, 0, 0), material=M_MECH, seg=10)
cone('Forward_Low_Gain_Antenna', 0.14, 0.26, loc=P(11.6, R_LS + 0.86, math.radians(-90)), rot=(math.pi, 0, 0), material=M_DISH, seg=20)

# ---------------------------------------------------------------- 5. 개구부 덮개(열린 상태)
HINGE = Vector((X_FRONT + 0.05, 0, H + R_LS + 0.10))
R_DOOR = 1.62
OPEN = math.radians(100)
door_m = Matrix.Translation(HINGE) @ Matrix.Rotation(-OPEN, 4, 'Y') @ Matrix.Translation((0, 0, -R_DOOR - 0.0)) @ Matrix.Rotation(math.pi / 2, 4, 'Y')
door = cylinder('Aperture_Door', R_DOOR, 0.10, material=M_DOOR, seg=64, base=False)
door.matrix_world = door_m @ Matrix.Translation((0, 0, 0.03))
door_in = cylinder('Aperture_Door_Inner_Black', 1.50, 0.02, material=M_DARK, seg=64, base=False)
door_in.matrix_world = door_m @ Matrix.Translation((0, 0, -0.03))
door_rib = lathe('Aperture_Door_Rim', [(R_DOOR - 0.06, 0.07), (R_DOOR + 0.02, 0.07), (R_DOOR + 0.02, 0.11), (R_DOOR - 0.06, 0.11)],
                 material=M_MECH, seg=64, shade_smooth=False)
door_rib.matrix_world = door_m
for k in range(3):
    st = box(f'Aperture_Door_Stiffener_{k}', (0.06, 2 * R_DOOR * 0.92 * math.cos(math.radians(30 * (k - 1))) * 0.9, 0.04), material=M_MECH)
    st.matrix_world = door_m @ Matrix.Translation((R_DOOR * 0.92 * math.sin(math.radians(30 * (k - 1))) * 0.9, 0, 0.10))
cylinder('Aperture_Door_Hinge', 0.08, 1.1, loc=HINGE, rot=(math.pi / 2, 0, 0), material=M_MECH, seg=16, base=False)
for s in (-1, 1):
    box(f'Aperture_Door_Hinge_Bracket_{"L" if s < 0 else "R"}', (0.55, 0.10, 0.16),
        loc=(HINGE.x - 0.22, s * 0.42, H + R_LS + 0.03), material=M_MECH)
# 덮개 개폐 구동기
curve_tube('Aperture_Door_Actuator', [tuple(P(12.2, R_LS + 0.02, math.radians(90))), tuple(P(12.2, R_LS + 0.18, math.radians(90))),
                                      tuple(HINGE + Vector((-0.05, 0, 0.02)))], radius=0.035, material=M_MECH, resolution=2)

# ---------------------------------------------------------------- 6. 태양전지판 (SA3 강체형) 2날개
SA_X = (X_ES0 + X_ES1) / 2
SA_TILT = math.radians(50)
SA_L, SA_W = 7.1, 1.24
SA_ROOT = 2.75
NCOL, NROW, GAP = 5, 22, 0.014

bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(SA_W, SA_L, 0.035), verts=bm.verts)
bmesh.ops.translate(bm, vec=(SA_W / 2, SA_L / 2, 0), verts=bm.verts)
cw = (SA_W - GAP * (NCOL + 1)) / NCOL
ch = (SA_L - GAP * (NROW + 1)) / NROW
frame_me = bpy.data.meshes.new('SA_Panel_Frame')
bm.to_mesh(frame_me)
bm.free()
bm = bmesh.new()
for i in range(NCOL):
    for j in range(NROW):
        g = bmesh.ops.create_cube(bm, size=1.0)
        bmesh.ops.scale(bm, vec=(cw, ch, 0.012), verts=g['verts'])
        bmesh.ops.translate(bm, vec=(GAP + cw / 2 + i * (cw + GAP), GAP + ch / 2 + j * (ch + GAP), 0.0175 + 0.004), verts=g['verts'])
cell_me = bpy.data.meshes.new('SA_Panel_Cells')
bm.to_mesh(cell_me)
bm.free()
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=(SA_W - 0.04, SA_L - 0.04, 0.008), verts=bm.verts)
bmesh.ops.translate(bm, vec=(SA_W / 2, SA_L / 2, -0.0175 - 0.002 - 0.004), verts=bm.verts)
back_me = bpy.data.meshes.new('SA_Panel_Back')
bm.to_mesh(back_me)
bm.free()
back_ob = assign(link(bpy.data.objects.new('SA_Panel_Back', back_me)), M_AFT)
frame_ob = assign(link(bpy.data.objects.new('SA_Panel_Frame', frame_me)), M_FRAME)
cell_ob = assign(link(bpy.data.objects.new('SA_Panel_Cells', cell_me)), M_CELL)

first = True
for side, name in ((1, 'Plus'), (-1, 'Minus')):
    W = Matrix.Translation((SA_X, 0, H)) @ Matrix.Rotation(SA_TILT, 4, 'Y')
    if side < 0:
        W = W @ Matrix.Rotation(math.pi, 4, 'Z')
    for h, off in (('A', 0.07), ('B', -0.07 - SA_W)):
        L = Matrix.Translation((off, SA_ROOT, 0))
        if first:
            f, c, b = frame_ob, cell_ob, back_ob
            f.name, c.name, b.name = f'SA_{name}_Panel_{h}_Frame', f'SA_{name}_Panel_{h}_Cells', f'SA_{name}_Panel_{h}_Back'
            first = False
        else:
            f = duplicate(frame_ob, name=f'SA_{name}_Panel_{h}_Frame')
            c = duplicate(cell_ob, name=f'SA_{name}_Panel_{h}_Cells')
            b = duplicate(back_ob, name=f'SA_{name}_Panel_{h}_Back')
        f.matrix_world = W @ L
        c.matrix_world = W @ L
        b.matrix_world = W @ L
    mast = cylinder(f'SA_{name}_Mast', 0.06, SA_ROOT + SA_L + 0.15 - 2.05, material=M_MECH, seg=12)
    mast.matrix_world = W @ Matrix.Translation((0, 2.05, 0)) @ Matrix.Rotation(-math.pi / 2, 4, 'X')
    for j, u in enumerate((SA_ROOT, SA_ROOT + SA_L)):
        beam = box(f'SA_{name}_Cross_Beam_{j}', (2 * SA_W + 0.18, 0.08, 0.07), material=M_FRAME)
        beam.matrix_world = W @ Matrix.Translation((0, u, 0))
    hub = cylinder(f'SA_{name}_Deploy_Mechanism', 0.16, 0.45, material=M_MECH, seg=16)
    hub.matrix_world = W @ Matrix.Translation((0, 2.15, 0)) @ Matrix.Rotation(-math.pi / 2, 4, 'X')

# ---------------------------------------------------------------- 7. 고이득 안테나(HGA) 2기
HGA_X = 7.7
BOOM = 2.5
f_len, r_dish = 0.48, 0.66
prof = [(r_dish * i / 10, (r_dish * i / 10) ** 2 / (4 * f_len)) for i in range(11)]
dish_src = lathe('HGA_Dish', prof, material=M_DISH, seg=40)
solidify(dish_src, 0.025)
apply_all(dish_src)
for s, name in ((1, 'Upper'), (-1, 'Lower')):
    th = s * math.pi / 2
    base = P(HGA_X, R_LS - 0.02, th)
    tip = P(HGA_X, R_LS + BOOM, th)
    boom = cylinder(f'HGA_{name}_Boom', 0.09, BOOM + 0.02, loc=base, rot=(0 if s > 0 else math.pi, 0, 0), material=M_AFT, seg=14)
    box(f'HGA_{name}_Boom_Hinge', (0.22, 0.22, 0.22), loc=P(HGA_X, R_LS + 0.10, th), material=M_MECH)
    box(f'HGA_{name}_Gimbal', (0.26, 0.26, 0.24), loc=tip, material=M_MECH)
    n = Vector((0.55, -0.80, 0.22 * s)).normalized()
    q = n.to_track_quat('Z', 'Y')
    D = Matrix.Translation(tip + n * 0.22) @ q.to_matrix().to_4x4()
    dish = dish_src if s > 0 else duplicate(dish_src)
    dish.name = f'HGA_{name}_Dish'
    dish.matrix_world = D
    yoke = cylinder(f'HGA_{name}_Yoke', 0.05, 0.24, material=M_MECH, seg=10)
    yoke.matrix_world = Matrix.Translation(tip) @ q.to_matrix().to_4x4()
    feed = cylinder(f'HGA_{name}_Feed', 0.06, 0.12, material=M_MECH, seg=12, base=False)
    feed.matrix_world = D @ Matrix.Translation((0, 0, f_len))
    for k in range(3):
        a = 2 * math.pi * k / 3
        rim = D @ Vector((0.6 * math.cos(a), 0.6 * math.sin(a), 0.6 ** 2 / (4 * f_len)))
        curve_tube(f'HGA_{name}_Feed_Strut_{k}', [tuple(rim), tuple(D @ Vector((0, 0, f_len - 0.05)))],
                   radius=0.012, material=M_MECH, resolution=1, bevel_res=1)

# ---------------------------------------------------------------- 8. 전시 받침대
STAND_X = 1.75
cylinder('Display_Stand_Base', 1.5, 0.14, loc=(STAND_X, 0, 0), material=M_STAND, seg=48)
cylinder('Display_Stand_Plinth', 0.45, 0.20, loc=(STAND_X, 0, 0.14), material=M_STAND, seg=32)
cylinder('Display_Stand_Post', 0.14, H - R_AFT - 0.34 + 0.05, loc=(STAND_X, 0, 0.34), material=M_STAND, seg=20)
shell_patch('Display_Stand_Cradle', R_AFT + 0.0, 0.06, math.radians(-106), math.radians(-74), STAND_X - 0.35, STAND_X + 0.35, M_STAND)
