# 엘리자베스 타워(빅벤), 영국 런던 웨스트민스터 궁전 — 높이 약 96m 실측 비례 모델.
# 정사각 탑신·시계층·종루·주철 지붕·랜턴·첨탑을 아래에서 위로 쌓았다.
# 한 면(-Y 쪽)의 패널·아치·시계판을 만들어 하나로 합친 뒤, 탑 중심축으로 90°씩 돌린
# 링크 복제로 나머지 세 면을 채웠다. 시계판은 원판 + 토러스 고리 + 방사형 살 + 로마 숫자(텍스트→메시),
# 뾰족아치 테두리는 2D 윤곽을 돌출(extrude)해 만들고, 지붕과 첨탑은 4분할 회전체로 사각뿔을 냈다.
import math
from atelier import *

SHOT = {"azimuth": -58, "elevation": 9, "lens": 60, "light": "day", "background": "#26303d"}

# ── 재질 ────────────────────────────────────────────────
STONE = mat('Anston_Limestone', '#cdb88f', 0.8)
RECESS = mat('Limestone_Shadowed', '#a8916b', 0.85)
PLINTH = mat('Plinth_Stone', '#a59479', 0.85)
DARK = mat('Belfry_Interior', '#2a2622', 0.9)
WIN = mat('Window_Glass', '#2e3742', 0.2, 0.1)
DIAL = mat('Opal_Dial_Glass', '#efe6cf', 0.35, emit='#fff1d0', emit_strength=0.25)
IRON = mat('Prussian_Blue_Iron', '#1d2a48', 0.4, 0.5)
GOLD = mat('Gold_Leaf', '#d4a84a', 0.28, 1.0)
ROOF = mat('Cast_Iron_Roof', '#3a4652', 0.5, 0.45)
LAMP = mat('Ayrton_Light_Glass', '#ead9a6', 0.3, emit='#ffe2a0', emit_strength=0.6)

H = 6.0      # 탑신 반폭(12m 정사각)
SQ = math.sqrt(2)
HB = 6.4     # 버트레스·시계층 반폭


def face_group(objs, name):
    """한 면 부품을 합치고, 원점을 탑 중심축에 둔 뒤 나머지 세 면을 링크 복제."""
    ob = join(objs, name)
    ob.data.transform(ob.matrix_world)
    ob.matrix_world = Matrix.Identity(4)
    for k in (1, 2, 3):
        duplicate(ob, loc=(0, 0, 0), rot=(0, 0, k * math.pi / 2), name=f'{name}_{k}')
    return ob


def arch_curve(w, rise, n=8):
    """폭 w, 높이 rise 인 뾰족아치 곡선 점(왼쪽 밑→정점→오른쪽 밑), 기준 z=0."""
    a = w / 2
    R = (a * a + rise * rise) / (2 * a)          # 정점 높이가 rise 가 되는 반지름
    cx = -a + R
    left = []
    for i in range(n + 1):
        t = i / n
        ang0 = math.pi
        ang1 = math.pi - math.asin(rise / R)
        ang = ang0 + (ang1 - ang0) * t
        left.append((cx + R * math.cos(ang), R * math.sin(ang)))
    left[-1] = (0.0, rise)
    right = [(-x, z) for x, z in reversed(left[:-1])]
    return left + right


def arch_spandrel(name, w, rise, top, x, z, y, depth, material):
    """패널 윗부분: 사각형에서 뾰족아치를 빼낸 돌 테두리."""
    pts = arch_curve(w, rise) + [(w / 2, top), (-w / 2, top)]
    ob = extrude_profile(name, pts, depth, loc=(x, y, z), material=material)
    return ob


def lancet(name, w, h, rise, x, z, y, depth, material):
    """직사각 + 뾰족아치 머리의 창(창유리 판)."""
    arc = arch_curve(w, rise)
    pts = [(-w / 2, -h)] + [(px, pz) for px, pz in arc] + [(w / 2, -h)]
    pts = list(reversed(pts))
    return extrude_profile(name, pts, depth, loc=(x, y, z + h), material=material)


def sq_lathe(name, profile, material):
    """4분할 회전체를 45° 돌려 사각뿔(대)로 만들고, 회전을 메시에 굳힌다. profile 의 r 은 반폭."""
    ob = lathe(name, [(r * SQ, z) for r, z in profile], material=material, seg=4, shade_smooth=False)
    ob.data.transform(Matrix.Rotation(math.pi / 4, 4, 'Z'))
    return ob


def text_mesh(name, body, size, depth, material):
    t = text3d(name + '_txt', body, size=size, depth=depth, material=material)
    t.data.align_y = 'CENTER'
    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)
    ob = bpy.data.objects.new(name, me)
    link(ob)
    assign(ob, material)
    return ob


# ── 기단과 탑신 몸체 ──────────────────────────────────────
box('Plinth', (13.4, 13.4, 2.5), material=PLINTH, base=True)
box('Plinth_Cap', (13.6, 13.6, 0.35), loc=(0, 0, 2.5), material=STONE, base=True)
box('Shaft_Core', (2 * H, 2 * H, 48.0), loc=(0, 0, 2.5), material=RECESS, base=True)
for sx in (-1, 1):
    for sy in (-1, 1):
        b = box('Corner_Buttress', (1.5, 1.5, 47.6), loc=(sx * (HB - 0.75), sy * (HB - 0.75), 2.85), material=STONE, base=True)

# ── 탑신 한 면: 수직 패널 5열 × 층별 뾰족아치, 가운데 3열에 창 ─────────
parts = []
X0, X1 = -4.9, 4.9
NCOL = 5
colw = (X1 - X0) / NCOL
RIB = 0.32
YF = -H - 0.3            # 리브 바깥면
DEP = 0.36               # 리브 깊이(코어 속으로 0.06 박힘)
YC = -H - 0.3 + DEP / 2 - 0.0
TIER_Z = [2.85 + i * 5.9 for i in range(9)]   # 8층 패널 + 꼭대기
for i in range(NCOL + 1):
    x = X0 + i * colw
    w = RIB if 0 < i < NCOL else 0.5
    parts.append(box('Shaft_Rib', (w, DEP, 47.15), loc=(x, YC, 2.85), material=STONE, base=True))
for zi, z in enumerate(TIER_Z):
    parts.append(box('Shaft_StringCourse', (10.5, DEP + 0.1, 0.34), loc=(0, YC - 0.05, z), material=STONE, base=True))
    if zi < len(TIER_Z) - 1:
        parts.append(box('Shaft_Transom', (10.0, DEP - 0.08, 0.2), loc=(0, YC + 0.04, z + 2.6), material=STONE, base=True))
for zi in range(len(TIER_Z) - 1):
    z0, z1 = TIER_Z[zi] + 0.34, TIER_Z[zi + 1]
    for c in range(NCOL):
        cx = X0 + (c + 0.5) * colw
        pw = colw - RIB
        # 아치 머리(돌 테두리)
        parts.append(arch_spandrel('Panel_Arch', pw, 1.15, 1.3, cx, z1 - 1.3, YC, DEP, STONE))
        parts.append(arch_spandrel('Panel_Arch_Low', pw, 0.8, 0.9, cx, z0 + 2.46 - 0.9, YC + 0.04, DEP - 0.08, STONE))
        # 가운데 3열: 좁은 쌍창
        if 1 <= c <= 3 and zi >= 1:
            for dx in (-0.3, 0.3):
                parts.append(lancet('Shaft_Window', 0.42, 1.75, 0.35, cx + dx, z1 - 3.15, -H - 0.04, 0.12, WIN))
            parts.append(box('Window_Mullion', (0.12, 0.2, 2.2), loc=(cx, -H - 0.08, z1 - 3.2), material=STONE, base=True))
face_group(parts, 'Shaft_Face')

# ── 시계층(50~62m) ───────────────────────────────────────
ZC0, ZC1 = 50.0, 62.0
DZ = 56.2                     # 시계판 중심 높이
R = 3.5                       # 시계판 반지름(지름 7m)
HC = 6.75                     # 시계층 반폭(탑신보다 내민다)
box('Clock_Stage', (2 * HC - 0.2, 2 * HC - 0.2, ZC1 - ZC0), loc=(0, 0, ZC0), material=STONE, base=True)
box('Clock_Stage_Corbel', (2 * HC + 0.2, 2 * HC + 0.2, 0.7), loc=(0, 0, ZC0 - 0.1), material=STONE, base=True)
sq_lathe('Clock_Stage_Corbel_Slope', [(0, ZC0 - 1.6), (HB + 0.05, ZC0 - 1.6), (HC + 0.1, ZC0 - 0.1), (0, ZC0 - 0.1)], STONE)

parts = []
YS = -HC + 0.1                # 시계층 벽면
S = 4.3                       # 금박 사각 테두리 반폭
# 사각 안의 원: 네 모서리 스팬드럴(금박)
for qx in (-1, 1):
    for qz in (-1, 1):
        pts = [(R + 0.05, 0), (S, 0), (S, S), (0, S), (0, R + 0.05)]
        for i in range(1, 12):
            a = math.pi / 2 * (1 - i / 12)
            pts.append(((R + 0.05) * math.cos(a), (R + 0.05) * math.sin(a)))
        pts = [(qx * x, qz * z) for x, z in pts]
        if qx * qz < 0:
            pts = list(reversed(pts))
        parts.append(extrude_profile('Dial_Spandrel', pts, 0.2, loc=(0, YS - 0.08, DZ), material=GOLD))
        # 스팬드럴 장미창 장식
        rx, rz = qx * 3.42, qz * 3.42
        parts.append(cylinder('Spandrel_Rosette', r=0.42, h=0.08, loc=(rx, YS - 0.27, DZ + rz), rot=(math.pi / 2, 0, 0), material=IRON, seg=24))
        parts.append(torus('Spandrel_Rosette_Ring', R=0.42, r=0.06, loc=(rx, YS - 0.33, DZ + rz), rot=(math.pi / 2, 0, 0), material=GOLD, seg=24, rseg=6))
        parts.append(sphere('Spandrel_Boss', r=0.12, loc=(rx, YS - 0.35, DZ + rz), material=GOLD, seg=10, rings=6))
# 돌 사각 테두리
for sz in (-1, 1):
    parts.append(box('Dial_Frame_H', (2 * S + 1.0, 0.4, 0.5), loc=(0, YS - 0.1, DZ + sz * (S + 0.25)), material=STONE))
for sx in (-1, 1):
    parts.append(box('Dial_Frame_V', (0.5, 0.4, 2 * S + 1.0), loc=(sx * (S + 0.25), YS - 0.1, DZ), material=STONE))
# 테두리 위 작은 박공과 아래 금박 명문 띠
parts.append(box('Inscription_Band', (2 * S, 0.12, 0.55), loc=(0, YS - 0.06, DZ - S - 1.05), material=GOLD))
parts.append(box('Inscription_Frame', (2 * S + 1.0, 0.25, 0.25), loc=(0, YS - 0.05, DZ - S - 1.55), material=STONE))
# 시계판 유리
YD = YS - 0.05
parts.append(cylinder('Dial_Glass', r=R, h=0.25, loc=(0, YD + 0.2, DZ), rot=(math.pi / 2, 0, 0), material=DIAL, seg=96))
YD -= 0.05                    # 유리 앞면
# 철골: 고리
for rr, tube in ((R, 0.11), (3.15, 0.05), (2.5, 0.06), (1.55, 0.05), (0.6, 0.05)):
    parts.append(torus('Dial_Ring', R=rr, r=tube, loc=(0, YD, DZ), rot=(math.pi / 2, 0, 0), material=IRON, seg=96, rseg=6))
# 분 눈금
for m in range(60):
    a = 2 * math.pi * m / 60
    L = 0.3 if m % 5 == 0 else 0.18
    rc = 3.15 + 0.05 + L / 2
    parts.append(box('Dial_Minute', (0.05 if m % 5 else 0.09, 0.06, L), loc=(rc * math.sin(a), YD, DZ + rc * math.cos(a)), rot=(0, a, 0), material=IRON))
# 방사형 살(안쪽 장식 트레이서리)
for k in range(24):
    a = 2 * math.pi * k / 24
    r0, r1 = (0.6, 2.5) if k % 2 == 0 else (1.55, 2.5)
    rc, L = (r0 + r1) / 2, r1 - r0
    parts.append(box('Dial_Spoke', (0.06, 0.06, L), loc=(rc * math.sin(a), YD, DZ + rc * math.cos(a)), rot=(0, a, 0), material=IRON))
# 로마 숫자(바깥을 향해 방사형으로)
ROMAN = ['XII', 'I', 'II', 'III', 'IIII', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI']
for h, s in enumerate(ROMAN):
    a = 2 * math.pi * h / 12
    rc = 2.83
    t = text_mesh('Dial_Numeral_' + s, s, 0.6, 0.025, IRON)
    t.location = (rc * math.sin(a), YD - 0.02, DZ + rc * math.cos(a))
    t.rotation_euler = (math.pi / 2, a, 0)
    parts.append(t)
# 바늘(10시 10분): 평판 창 모양을 돌출


def hand(name, length, tail, w, angle, y):
    pts = [(0, -tail), (w * 0.9, -tail + 0.15), (w * 0.5, 0), (w, length * 0.7), (w * 0.35, length * 0.85),
           (0, length), (-w * 0.35, length * 0.85), (-w, length * 0.7), (-w * 0.5, 0), (-w * 0.9, -tail + 0.15)]
    pts = list(reversed(pts))
    return extrude_profile(name, pts, 0.07, loc=(0, y, DZ), rot=(0, angle, 0), material=IRON)


parts.append(hand('Hour_Hand', 2.2, 0.45, 0.17, 2 * math.pi * (10 + 10 / 60) / 12, YD - 0.12))
parts.append(hand('Minute_Hand', 3.1, 0.8, 0.12, 2 * math.pi * 10 / 60, YD - 0.21))
parts.append(cylinder('Dial_Hub', r=0.22, h=0.32, loc=(0, YD + 0.02, DZ), rot=(math.pi / 2, 0, 0), material=GOLD, seg=24))
# 시계층 위·아래 장식 아치 띠
for c in range(7):
    cx = -4.8 + c * 1.6
    parts.append(arch_spandrel('Clock_Upper_Arcade', 1.3, 0.6, 0.85, cx, ZC1 - 1.25, YS - 0.1, 0.3, STONE))
face_group(parts, 'Clock_Face')

# 시계층 모서리 첨탑
pin = cylinder('Clock_Corner_Pinnacle', r=0.75, h=ZC1 + 2.6 - ZC0, loc=(HC - 0.5, HC - 0.5, ZC0), material=STONE, seg=8)
pin_cap = cone('Clock_Corner_Pinnacle_Spire', r=0.82, h=3.4, loc=(HC - 0.5, HC - 0.5, ZC1 + 2.6), material=ROOF, seg=8)
pin_tip = sphere('Clock_Corner_Pinnacle_Orb', r=0.22, loc=(HC - 0.5, HC - 0.5, ZC1 + 6.05), material=GOLD, seg=12, rings=8)
for ob in (pin, pin_cap, pin_tip):
    for k in (1, 2, 3):
        a = k * math.pi / 2
        x, y = ob.location.x, ob.location.y
        duplicate(ob, loc=(x * math.cos(a) - y * math.sin(a), x * math.sin(a) + y * math.cos(a), ob.location.z))

# ── 코니스와 난간 ───────────────────────────────────────
box('Clock_Cornice', (2 * HC + 0.5, 2 * HC + 0.5, 0.6), loc=(0, 0, ZC1), material=STONE, base=True)
parts = []
for i in range(17):
    x = -5.6 + i * 0.7
    parts.append(box('Parapet_Post', (0.16, 0.2, 1.0), loc=(x, -HC - 0.05, ZC1 + 0.6), material=GOLD, base=True))
parts.append(box('Parapet_Rail', (11.6, 0.26, 0.18), loc=(0, -HC - 0.05, ZC1 + 1.6), material=GOLD, base=True))
face_group(parts, 'Parapet')

# ── 종루(62.6~73m) ──────────────────────────────────────
ZB0, ZB1 = ZC1 + 0.6, 73.0
HBF = 5.5
box('Belfry_Core', (2 * HBF, 2 * HBF, ZB1 - ZB0), loc=(0, 0, ZB0), material=DARK, base=True)
parts = []
YB = -HBF - 0.2
NB = 5
span = 2 * (HBF - 0.9)
pier = 0.42
ow = (span - (NB - 1) * pier) / NB
OZ0, OZ1 = ZB0 + 1.6, 70.6
parts.append(box('Belfry_Sill', (2 * HBF + 0.3, 0.5, OZ0 - ZB0), loc=(0, YB, ZB0), material=STONE, base=True))
for sx in (-1, 1):
    parts.append(box('Belfry_Corner_Pier', (0.95, 0.5, ZB1 - ZB0), loc=(sx * (HBF - 0.475 + 0.1), YB, ZB0), material=STONE, base=True))
for i in range(NB):
    cx = -span / 2 + ow / 2 + i * (ow + pier)
    if i < NB - 1:
        parts.append(box('Belfry_Pier', (pier, 0.5, OZ1 - OZ0), loc=(cx + ow / 2 + pier / 2, YB, OZ0), material=STONE, base=True))
    parts.append(arch_spandrel('Belfry_Arch', ow, 1.1, 1.4, cx, OZ1 - 1.4, YB, 0.5, STONE))
    # 루버
    for j in range(7):
        parts.append(box('Belfry_Louvre', (ow - 0.02, 0.06, 0.12), loc=(cx, -HBF - 0.02, OZ0 + 0.5 + j * 0.62), rot=(0.6, 0, 0), material=ROOF))
parts.append(box('Belfry_Frieze', (2 * HBF + 0.3, 0.5, ZB1 - OZ1), loc=(0, YB, OZ1), material=STONE, base=True))
for i in range(12):
    parts.append(box('Belfry_Frieze_Shield', (0.36, 0.08, 0.5), loc=(-4.4 + i * 0.8, YB - 0.28, OZ1 + 0.95), material=GOLD))
face_group(parts, 'Belfry_Face')
box('Belfry_Cornice', (2 * HBF + 1.0, 2 * HBF + 1.0, 0.5), loc=(0, 0, ZB1), material=STONE, base=True)
box('Belfry_Crest', (2 * HBF + 0.7, 2 * HBF + 0.7, 0.35), loc=(0, 0, ZB1 + 0.5), material=GOLD, base=True)
# 종루 모서리 소첨탑
bp = cylinder('Belfry_Pinnacle', r=0.42, h=1.6, loc=(HBF + 0.1, HBF + 0.1, ZB1 + 0.85), material=STONE, seg=8)
bc = cone('Belfry_Pinnacle_Spire', r=0.48, h=2.4, loc=(HBF + 0.1, HBF + 0.1, ZB1 + 2.45), material=GOLD, seg=8)
for ob in (bp, bc):
    for k in (1, 2, 3):
        a = k * math.pi / 2
        x, y = ob.location.x, ob.location.y
        duplicate(ob, loc=(x * math.cos(a) - y * math.sin(a), x * math.sin(a) + y * math.cos(a), ob.location.z))

# ── 주철 지붕(사각뿔) ───────────────────────────────────
ZR0, ZR1 = ZB1 + 0.85, 87.6
RW0, RW1 = 5.35, 1.35


def roof_half(z):
    t = (z - ZR0) / (ZR1 - ZR0)
    return RW0 + (RW1 - RW0) * t


sq_lathe('Roof_Pyramid', [(0.0, ZR0), (RW0, ZR0), (RW1, ZR1), (0.0, ZR1)], ROOF)
parts = []
# 도머창(아래 3개, 위 1개)
for zc, xs, sc in ((ZR0 + 2.2, (-2.6, 0, 2.6), 1.0), (ZR0 + 6.4, (-1.2, 1.2), 0.75), (ZR0 + 10.0, (0,), 0.6)):
    hz = roof_half(zc)
    for x in xs:
        dw, dh = 1.1 * sc, 1.6 * sc
        y = -hz - 0.25
        parts.append(box('Dormer_Body', (dw, 1.6, dh), loc=(x, y + 0.8, zc - dh / 2), material=ROOF, base=True))
        parts.append(lancet('Dormer_Window', dw * 0.55, dh * 0.55, 0.25 * sc, x, zc - dh / 2 + 0.2, y - 0.02, 0.05, LAMP))
        parts.append(extrude_profile('Dormer_Gable', [(-dw / 2 - 0.12, 0), (dw / 2 + 0.12, 0), (0, 0.9 * sc)], 1.7,
                                     loc=(x, y + 0.82, zc + dh / 2), material=GOLD))
        parts.append(cone('Dormer_Finial', r=0.06, h=0.5 * sc, loc=(x, y - 0.02, zc + dh / 2 + 0.85 * sc), material=GOLD, seg=6))
face_group(parts, 'Roof_Face')
# 용마루(모서리) 금박 크로켓
parts = []
NCR = 11
for i in range(NCR):
    t = (i + 0.5) / NCR
    z = ZR0 + (ZR1 - ZR0) * t
    hw = roof_half(z) + 0.02
    parts.append(cone('Hip_Crocket', r=0.17, h=0.45, loc=(hw, -hw, z), material=GOLD, seg=6))
hip = curve_tube('Hip_Rib', [(RW0, -RW0, ZR0), (RW1, -RW1, ZR1)], radius=0.09, material=GOLD, resolution=1, bevel_res=1)
face_group(parts, 'Roof_Hip_Crockets')
for k in (1, 2, 3):
    duplicate(hip, loc=(0, 0, 0), rot=(0, 0, k * math.pi / 2), name='Hip_Rib')

# ── 에어턴 라이트 랜턴(86~89.6m) ─────────────────────────
ZL0, ZL1 = ZR1, 90.4
HL = 1.35
box('Lantern_Base', (2 * HL + 0.5, 2 * HL + 0.5, 0.4), loc=(0, 0, ZL0), material=GOLD, base=True)
box('Lantern_Light', (2 * HL - 0.3, 2 * HL - 0.3, ZL1 - ZL0 - 0.4), loc=(0, 0, ZL0 + 0.4), material=LAMP, base=True)
parts = []
for sx in (-1, 0, 1):
    w = 0.42 if sx else 0.2
    parts.append(box('Lantern_Post', (w, 0.3, ZL1 - ZL0 - 0.4), loc=(sx * (HL - w / 2) if sx else 0, -HL + 0.1, ZL0 + 0.4), material=ROOF, base=True))
for sx in (-1, 1):
    parts.append(arch_spandrel('Lantern_Arch', HL - 0.31, 0.6, 0.8, sx * (HL / 2 - 0.05), ZL1 - 0.8, -HL + 0.1, 0.3, ROOF))
face_group(parts, 'Lantern_Face')
box('Lantern_Cornice', (2 * HL + 0.5, 2 * HL + 0.5, 0.35), loc=(0, 0, ZL1), material=GOLD, base=True)

# ── 첨탑과 피니얼 ───────────────────────────────────────
ZS0, ZS1 = ZL1 + 0.35, 95.2
sq_lathe('Spire', [(0.0, ZS0), (1.25, ZS0), (0.6, ZS0 + 1.2), (0.06, ZS1), (0.0, ZS1)], ROOF)
for zt, hw in ((ZS0 + 1.2, 0.63), (ZS0 + 2.5, 0.42)):
    box('Spire_Gold_Band', (2 * hw, 2 * hw, 0.18), loc=(0, 0, zt), material=GOLD, base=True)
cylinder('Finial_Rod', r=0.06, h=1.6, loc=(0, 0, ZS1 - 0.3), material=GOLD, seg=8)
sphere('Finial_Orb', r=0.24, loc=(0, 0, ZS1 + 0.25), material=GOLD, seg=16, rings=10)
box('Finial_Cross_Bar', (0.7, 0.08, 0.08), loc=(0, 0, ZS1 + 0.95), material=GOLD)
box('Finial_Cross_Bar', (0.08, 0.7, 0.08), loc=(0, 0, ZS1 + 0.95), material=GOLD)
