# 아폴로 달 착륙선(Apollo Lunar Module, LM) — 1969년 아폴로 11호 '이글'형.
# 하강단: 모서리를 깎은 팔각기둥에 bmesh 격자를 만들고 꼭짓점을 법선 방향으로 무작위(시드 고정)로 밀어
#        구겨진 금박 단열재(캡톤 포일)를 형상으로 표현했다. 착륙 다리 4개는 막대 헬퍼로 만든 주·보조 스트럿,
#        회전체(lathe) 접시형 발판으로 만들고, 한 개를 만든 뒤 메시를 공유하는 복제로 90°씩 돌려 배치했다.
# 상승단: 육각 단면 네 개를 Y축으로 이어 붙인(loft) 각진 승무원실에 삼각 창, 정면 해치, 좌우 비대칭 추진제 탱크,
#        네 모서리 RCS 추력기 묶음(노즐 4개씩), 도킹 터널, 랑데부 레이더·S밴드 접시 안테나를 더했다.
# 단위 m, 실제 치수 근사: 전체 높이 약 7m, 발판 대각 폭 약 9.4m. 정면(해치·사다리)은 -Y 쪽.
from atelier import *
import random

random.seed(1969)

SHOT = {"azimuth": -58, "elevation": 15, "lens": 50, "light": "day", "background": "#1a1f2c"}

# ---------------------------------------------------------------- 재질
M_GOLD = mat('Foil_Kapton_Amber', '#c08a2e', rough=0.32, metal=0.85)
M_GOLD2 = mat('Foil_Kapton_Light', '#d6b04e', rough=0.28, metal=0.85)
M_BLACK = mat('Thermal_Blanket_Black', '#262628', rough=0.65, metal=0.0)
M_SKIN = mat('Ascent_Skin_Aluminum', '#8e908c', rough=0.5, metal=0.55)
M_DARK = mat('Ascent_Panel_Dark', '#3b3c3f', rough=0.55, metal=0.2)
M_SILVER = mat('Strut_Silver', '#b8bbbe', rough=0.3, metal=0.9)
M_NOZZLE = mat('Engine_Nozzle', '#4a443d', rough=0.5, metal=0.6)
M_GLASS = mat('Window_Glass', '#2b4a5c', rough=0.05, metal=0.6)
M_WHITE = mat('Antenna_Paint', '#d8d4c8', rough=0.45, metal=0.0)
M_MYLAR = mat('Foil_Mylar_Silver', '#c4c3bc', rough=0.25, metal=0.9)
M_RED = mat('Flag_Red', '#a3232e', rough=0.6)
M_FWHITE = mat('Flag_White', '#e4e1d8', rough=0.6)
M_BLUE = mat('Flag_Blue', '#24305e', rough=0.6)


# ---------------------------------------------------------------- 메시 빌더(여러 재질 슬롯을 가진 한 오브젝트)
class MB:
    def __init__(self):
        self.bm = bmesh.new()
        self.mats = []

    def mi(self, m):
        if m not in self.mats:
            self.mats.append(m)
        return self.mats.index(m)

    def _paint(self, verts, m):
        idx = self.mi(m)
        fs = set()
        for v in verts:
            fs.update(v.link_faces)
        for f in fs:
            f.material_index = idx

    def rod(self, p0, p1, r, m, seg=12, r1=None):
        p0, p1 = Vector(p0), Vector(p1)
        d = p1 - p0
        L = d.length
        rot = Vector((0, 0, 1)).rotation_difference(d.normalized()).to_matrix().to_4x4()
        M = Matrix.Translation(p0) @ rot @ Matrix.Translation((0, 0, L / 2))
        res = bmesh.ops.create_cone(self.bm, cap_ends=True, segments=seg, radius1=r,
                                    radius2=r if r1 is None else r1, depth=L, matrix=M)
        self._paint(res['verts'], m)

    def box(self, center, size, m, rotz=0.0):
        M = Matrix.Translation(Vector(center)) @ Matrix.Rotation(rotz, 4, 'Z') @ Matrix.Diagonal((*size, 1))
        res = bmesh.ops.create_cube(self.bm, size=1.0, matrix=M)
        self._paint(res['verts'], m)

    def ball(self, c, r, m, seg=12, rings=8, scale=(1, 1, 1)):
        M = Matrix.Translation(Vector(c)) @ Matrix.Diagonal((*scale, 1))
        res = bmesh.ops.create_uvsphere(self.bm, u_segments=seg, v_segments=rings, radius=r, matrix=M)
        self._paint(res['verts'], m)

    def lathe(self, profile, m, M=Matrix(), seg=32):
        rings = []
        for i in range(seg):
            a = 2 * math.pi * i / seg
            rings.append([self.bm.verts.new(M @ Vector((r * math.cos(a), r * math.sin(a), z))) for r, z in profile])
        new = []
        for i in range(seg):
            r0, r1 = rings[i], rings[(i + 1) % seg]
            for j in range(len(profile) - 1):
                new.append(self.bm.faces.new((r0[j], r1[j], r1[j + 1], r0[j + 1])))
        idx = self.mi(m)
        for f in new:
            f.material_index = idx

    def poly(self, pts, m):
        f = self.bm.faces.new([self.bm.verts.new(Vector(p)) for p in pts])
        f.material_index = self.mi(m)
        return f

    def build(self, name, smooth_shade=False):
        bmesh.ops.remove_doubles(self.bm, verts=self.bm.verts, dist=1e-6)
        bmesh.ops.recalc_face_normals(self.bm, faces=self.bm.faces)
        me = bpy.data.meshes.new(name)
        self.bm.to_mesh(me)
        self.bm.free()
        for m in self.mats:
            me.materials.append(m)
        if smooth_shade:
            for p in me.polygons:
                p.use_smooth = True
        ob = bpy.data.objects.new(name, me)
        link(ob)
        return ob


def lerp(a, b, t):
    return Vector(a).lerp(Vector(b), t)


# ================================================================ 하강단(Descent Stage)
Z0, Z1 = 1.72, 3.48          # 하강단 몸체 바닥/윗면 높이
H, C = 2.11, 1.0             # 팔각형 반폭, 모서리 깎은 길이
OCT = [(H - C, -H), (H, -H + C), (H, H - C), (H - C, H), (-H + C, H), (-H, H - C), (-H, -H + C), (-H + C, -H)]


def foil_body():
    mb = MB()
    bm = mb.bm
    n = len(OCT)
    for i in range(n):
        a, b = Vector((*OCT[i], 0)), Vector((*OCT[(i + 1) % n], 0))
        e = b - a
        nrm = Vector((e.y, -e.x, 0)).normalized()
        main = (i % 2 == 1)
        nu = 40 if main else 26
        nv = 30
        grid = []
        for jv in range(nv + 1):
            row = []
            for ju in range(nu + 1):
                u, v = ju / nu, jv / nv
                p = a.lerp(b, u)
                p.z = Z0 + (Z1 - Z0) * v
                if 0 < ju < nu and 0 < jv < nv:
                    p += nrm * random.uniform(-0.006, 0.026)
                row.append(bm.verts.new(p))
            grid.append(row)
        for jv in range(nv):
            v = (jv + 0.5) / nv
            if main:
                m = M_BLACK if v < 0.16 else M_GOLD
            else:
                m = M_BLACK if v > 0.58 else (M_BLACK if v < 0.16 else M_GOLD2)
            idx = mb.mi(m)
            for ju in range(nu):
                f = bm.faces.new((grid[jv][ju], grid[jv][ju + 1], grid[jv + 1][ju + 1], grid[jv + 1][ju]))
                f.material_index = idx
    # 윗면(갑판)과 바닥면
    mb.poly([(x, y, Z1) for x, y in OCT], M_DARK)
    mb.poly([(x, y, Z0) for x, y in reversed(OCT)], M_BLACK)
    ob = mb.build('DescentStage_FoilBody')
    return ob


foil_body()

# 하강단 정면의 성조기와 'UNITED STATES' 표기
fl = MB()
FY = -H - 0.04
fx, fz, fw, fh = -0.72, 2.95, 0.58, 0.36
fl.box((fx, FY + 0.01, fz), (fw, 0.02, fh), M_FWHITE)
for k in range(7):
    h = fh / 13
    fl.box((fx, FY, fz - fh / 2 + h * (2 * k + 0.5)), (fw, 0.01, h), M_RED)
fl.box((fx - fw * 0.3, FY - 0.006, fz + fh * 0.23), (fw * 0.4, 0.01, fh * 7 / 13), M_BLUE)
fl.build('Descent_Flag')
txt = text3d('Descent_Label_UNITED_STATES', 'UNITED STATES', size=0.11, depth=0.004,
             loc=(fx, FY + 0.005, 2.62), rot=(math.pi / 2, 0, 0), material=M_BLACK)

# 하강 엔진 노즐(회전체, 안쪽 목까지 보이게 두께 있는 종 모양)
eng = MB()
eng.lathe([(0.0, 1.47), (0.30, 1.47), (0.55, 1.18), (0.76, 0.97), (0.80, 0.95),
           (0.82, 0.97), (0.64, 1.24), (0.50, 1.56)], M_NOZZLE, seg=48)
eng.build('DescentEngine_Nozzle', smooth_shade=True)

# 하강단 바닥 열차폐 링(노즐 주위)
hs = MB()
hs.lathe([(0.52, Z0 - 0.06), (1.25, Z0 - 0.06), (1.35, Z0 + 0.01)], M_MYLAR, seg=32)
hs.build('DescentStage_HeatShield')

# ---------------------------------------------------------------- 착륙 다리(+X 방향 하나를 만들고 회전 복제)
RF, ZF = 4.55, 0.25          # 발판 위 관절
RT, ZT = 2.25, Z1 - 0.2          # 주 스트럿 위 고정점
F = Vector((RF, 0, ZF))
T = Vector((RT, 0, ZT))

legmb = MB()
# 발판: 얕은 접시
legmb.lathe([(0.0, 0.0), (0.34, 0.0), (0.45, 0.06), (0.47, 0.15), (0.43, 0.18), (0.14, 0.23), (0.0, 0.25)],
            M_GOLD2, M=Matrix.Translation((RF, 0, 0)), seg=32)
legmb.ball(F + Vector((0, 0, 0.03)), 0.09, M_SILVER)
# 주 스트럿: 아래 피스톤(은색) + 위 실린더(금박)
legmb.rod(F, lerp(F, T, 0.46), 0.065, M_SILVER)
legmb.rod(lerp(F, T, 0.42), T, 0.125, M_GOLD, seg=14)
legmb.rod(lerp(F, T, 0.40), lerp(F, T, 0.43), 0.14, M_BLACK, seg=14)
# 아웃리거(몸체 위쪽 받침)
legmb.box((2.25, 0, Z1 - 0.17), (0.38, 0.34, 0.34), M_GOLD)
# 보조 스트럿 2개와 전개 트러스
S = lerp(F, T, 0.20)
for s in (-1, 1):
    base = Vector((2.09, s * 0.95, Z0 + 0.12))
    legmb.rod(S, base, 0.05, M_SILVER, seg=10)
    mid = lerp(S, base, 0.45)
    legmb.rod(mid, Vector((2.09, s * 0.35, Z0 + 0.95)), 0.035, M_SILVER, seg=8)
    legmb.ball(base, 0.07, M_SILVER, seg=10, rings=6)
legmb.ball(S, 0.08, M_SILVER, seg=10, rings=6)
leg0 = legmb.build('LandingLeg_+X')
legs = [leg0]
for k, nm in ((1, 'LandingLeg_+Y'), (2, 'LandingLeg_-X'), (3, 'LandingLeg_-Y_Front')):
    legs.append(duplicate(leg0, rot=(0, 0, k * math.pi / 2), name=nm))

# ---------------------------------------------------------------- 앞 다리 사다리와 현관(porch)
FF = Vector((0, -RF, ZF))
TF = Vector((0, -RT, ZT))
lad = MB()
axis = (TF - FF).normalized()
out = Vector((0, -axis.z, axis.y)).normalized()   # 스트럿 바깥 방향(위·앞)
if out.y > 0:
    out = -out
s0, s1 = 0.27, 1.03
for sx in (-0.24, 0.24):
    a = lerp(FF, TF, s0) + out * 0.16 + Vector((sx, 0, 0))
    b = lerp(FF, TF, s1) + out * 0.16 + Vector((sx, 0, 0))
    lad.rod(a, b, 0.022, M_SILVER, seg=8)
    # 사다리를 스트럿에 잡아 주는 브래킷
    for s in (0.35, 0.62, 0.9):
        p = lerp(FF, TF, s)
        lad.rod(p + out * 0.05 + Vector((sx * 0.4, 0, 0)), p + out * 0.16 + Vector((sx, 0, 0)), 0.015, M_SILVER, seg=6)
for k in range(10):
    s = s0 + 0.03 + (s1 - s0 - 0.06) * k / 9
    p = lerp(FF, TF, s) + out * 0.16
    lad.rod(p + Vector((-0.24, 0, 0)), p + Vector((0.24, 0, 0)), 0.018, M_SILVER, seg=8)
# 현관 발판
lad.box((0, -1.95, Z1 + 0.025), (0.92, 1.30, 0.06), M_GOLD2)
for sx in (-0.46, 0.46):
    pts = [Vector((sx, -2.58, Z1 + 0.05)), Vector((sx, -2.58, Z1 + 0.78)), Vector((sx, -1.45, Z1 + 0.78)),
           Vector((sx, -1.38, Z1 + 0.30))]
    for p, q in zip(pts[:-1], pts[1:]):
        lad.rod(p, q, 0.02, M_SILVER, seg=8)
lad.build('FrontLadder_Porch')

# ---------------------------------------------------------------- 하강단 위 RCS 배기 차폐판(plume deflector)
pd = MB()
for sx in (-1, 1):
    for sy in (-1, 1):
        c = Vector((sx * 1.45, sy * 1.05, Z1))
        pd.rod(c, c + Vector((0, 0, 0.42)), 0.03, M_SILVER, seg=8)
        pd.box(c + Vector((0, 0, 0.44)), (0.36, 0.36, 0.02), M_MYLAR, rotz=math.pi / 4)
pd.build('RCS_PlumeDeflectors')

# ================================================================ 상승단(Ascent Stage)
A = Z1 - 0.01                # 상승단 바닥 높이(갑판에 살짝 묻힘)


def hexsec(y, wb, zb, wm, zm, wt, zt):
    return [Vector((x, y, A + z)) for x, z in ((-wb, zb), (wb, zb), (wm, zm), (wt, zt), (-wt, zt), (-wm, zm))]


SECS = [hexsec(-1.34, 0.50, 0.10, 0.58, 1.02, 0.36, 2.02),
        hexsec(-0.98, 0.95, 0.02, 1.22, 1.30, 0.82, 2.36),
        hexsec(0.95, 0.95, 0.02, 1.22, 1.30, 0.82, 2.36),
        hexsec(1.22, 0.85, 0.20, 1.05, 1.30, 0.70, 2.18)]
ZT_A = A + 2.36

cab = MB()
rings = [[cab.bm.verts.new(p) for p in s] for s in SECS]
for i in range(len(rings) - 1):
    for j in range(6):
        f = cab.bm.faces.new((rings[i][j], rings[i][(j + 1) % 6], rings[i + 1][(j + 1) % 6], rings[i + 1][j]))
        f.material_index = cab.mi(M_DARK if (j == 0 or (i == 0 and j in (1, 5))) else M_SKIN)
f = cab.bm.faces.new(list(reversed(rings[0])))
f.material_index = cab.mi(M_SKIN)
f = cab.bm.faces.new(rings[-1])
f.material_index = cab.mi(M_SKIN)
cab.build('AscentStage_Cabin')

# 삼각형 창 2개(정면 위쪽, 안쪽 아래로 기운 삼각형)
win = MB()
YF = SECS[0][0].y
for sx in (-1, 1):
    tri = [Vector((sx * x, YF, A + z)) for x, z in ((0.07, 1.18), (0.50, 1.08), (0.33, 1.88))]
    c = sum(tri, Vector()) / 3
    frame = [c.lerp(p, 1.17) + Vector((0, -0.006, 0)) for p in tri]
    glass_tri = [p + Vector((0, -0.014, 0)) for p in tri]
    if sx < 0:
        frame.reverse()
        glass_tri.reverse()
    win.poly(frame, M_BLACK)
    win.poly(glass_tri, M_GLASS)
win.build('Cabin_Windows')

# 정면 해치와 손잡이, 해치 위 패널
fr = MB()
fr.box((0, -1.355, A + 0.52), (0.80, 0.04, 0.80), M_SKIN)
fr.box((0, -1.38, A + 0.52), (0.70, 0.02, 0.70), M_MYLAR)
fr.rod((-0.12, -1.40, A + 0.36), (0.12, -1.40, A + 0.36), 0.015, M_SILVER, seg=8)
fr.build('Cabin_FrontHatch')

# 도킹 터널·드로그 링·윗면 도킹창
top = MB()
top.rod((0, -0.05, ZT_A - 0.02), (0, -0.05, ZT_A + 0.36), 0.42, M_SKIN, seg=32)
top.rod((0, -0.05, ZT_A + 0.36), (0, -0.05, ZT_A + 0.42), 0.46, M_SILVER, seg=32)
top.rod((0, -0.05, ZT_A + 0.40), (0, -0.05, ZT_A + 0.43), 0.30, M_DARK, seg=32)
top.box((0.45, -0.70, ZT_A + 0.01), (0.34, 0.24, 0.04), M_GLASS)
# 도킹 표적
top.rod((-0.55, 0.45, ZT_A), (-0.55, 0.45, ZT_A + 0.28), 0.02, M_SILVER, seg=8)
top.box((-0.55, 0.45, ZT_A + 0.30), (0.20, 0.03, 0.03), M_WHITE)
top.box((-0.55, 0.45, ZT_A + 0.30), (0.03, 0.03, 0.20), M_WHITE)
top.build('Cabin_DockingTunnel')

# 좌우 비대칭 추진제 탱크(산화제 좌, 연료 우) — 각진 피복
tk = MB()
tk.ball((-1.13, 0.20, A + 1.02), 0.62, M_SKIN, seg=14, rings=9, scale=(0.72, 1.0, 1.0))
tk.ball((1.10, 0.30, A + 0.98), 0.55, M_SKIN, seg=14, rings=9, scale=(0.72, 1.0, 1.0))
tk.build('Ascent_PropellantTanks')

# 뒤쪽 장비 베이
aft = MB()
aft.box((0, 1.50, A + 1.18), (2.10, 0.62, 1.20), M_DARK)
aft.box((0, 1.82, A + 1.18), (1.90, 0.04, 1.02), M_BLACK)
for sx in (-1, 1):
    aft.box((sx * 0.55, 1.86, A + 1.18), (0.62, 0.04, 0.86), M_SKIN)
aft.box((0, 1.30, A + 0.42), (1.5, 0.5, 0.40), M_BLACK)
aft.build('Ascent_AftEquipmentBay')

# ---------------------------------------------------------------- RCS 추력기 묶음 4개(각 노즐 4개)
rcs0 = MB()
q = Vector((1.45, 1.05, A + 1.66))
rcs0.box(q, (0.26, 0.26, 0.30), M_GOLD2)
rcs0.box((1.22, 1.05, A + 1.66), (0.42, 0.14, 0.14), M_SKIN)   # 지지대
for d in (Vector((0, 0, 1)), Vector((0, 0, -1)), Vector((1, 0, 0)), Vector((0, 1, 0))):
    p0 = q + d * 0.12
    rcs0.rod(p0, p0 + d * 0.06, 0.035, M_NOZZLE, seg=10)
    rcs0.rod(p0 + d * 0.06, p0 + d * 0.26, 0.04, M_NOZZLE, seg=12, r1=0.075)
rcs = rcs0.build('RCS_Quad_+X+Y')
duplicate(rcs, scale=(1, -1, 1), name='RCS_Quad_+X-Y')
duplicate(rcs, scale=(-1, 1, 1), name='RCS_Quad_-X+Y')
duplicate(rcs, scale=(-1, -1, 1), name='RCS_Quad_-X-Y')

# ---------------------------------------------------------------- 안테나
ant = MB()
# 랑데부 레이더(앞쪽 위)
rr = Vector((-0.30, -0.78, ZT_A))
ant.rod(rr, rr + Vector((0, 0, 0.22)), 0.06, M_SKIN, seg=10)
ant.box(rr + Vector((0, 0, 0.26)), (0.30, 0.12, 0.12), M_GOLD)
Mrr = Matrix.Translation(rr + Vector((0, -0.12, 0.42))) @ Matrix.Rotation(math.radians(-70), 4, 'X')
ant.lathe([(0.0, 0.0), (0.12, 0.012), (0.24, 0.05), (0.30, 0.08), (0.29, 0.06), (0.0, 0.02)], M_WHITE, M=Mrr, seg=24)
ant.rod(Mrr @ Vector((0, 0, 0)), Mrr @ Vector((0, 0, 0.22)), 0.015, M_SILVER, seg=6)
# S밴드 조향 안테나(오른쪽 붐)
sb = Vector((1.00, 0.55, A + 1.95))
ant.rod(sb, sb + Vector((0.25, 0, 0.55)), 0.03, M_SILVER, seg=8)
Msb = Matrix.Translation(sb + Vector((0.32, 0, 0.62))) @ Matrix.Rotation(math.radians(35), 4, 'Y') @ Matrix.Rotation(math.radians(-25), 4, 'X')
ant.lathe([(0.0, 0.0), (0.14, 0.015), (0.27, 0.06), (0.33, 0.10), (0.32, 0.08), (0.0, 0.025)], M_WHITE, M=Msb, seg=24)
ant.rod(Msb @ Vector((0, 0, 0)), Msb @ Vector((0, 0, 0.20)), 0.012, M_SILVER, seg=6)
# VHF 안테나 2개
for base, d in (((-0.55, 0.90, ZT_A - 0.1), (-0.45, 0.35, 1.0)), ((0.55, 1.05, ZT_A - 0.15), (0.35, 0.65, 0.75))):
    b = Vector(base)
    ant.rod(b, b + Vector(d).normalized() * 0.75, 0.012, M_SILVER, seg=6)
    ant.ball(b + Vector(d).normalized() * 0.75, 0.025, M_SILVER, seg=8, rings=5)
ant.build('Antennas')
