# 피사의 사탑 (Torre di Pisa) — Blender Python 절차적 모델
# 높이 약 56m, 기단 지름 약 15.5m 의 대리석 원통 종탑을 실제 치수로 만들었다.
# 1층은 15개의 붙임기둥과 블라인드 아치(마름모 상감), 2~7층은 기둥 30개짜리 회랑 6단,
# 맨 위는 지름이 작은 종루(아치 개구부 12개와 청동 종)다. 아치 사이 벽(스팬드럴)은
# 원통 좌표로 bmesh 를 직접 짜서 휘어진 아치 띠를 만들고, 기둥·아치는 linked 복제로 메시를 공유한다.
# 처마 돌림띠와 기단은 회전체(lathe)로 만들었다. 탑 전체를 남쪽(-Y)으로 약 4° 기울였고,
# 공사 중 기울기를 바로잡으려 한 흔적처럼 종루는 0.8° 덜 기울게 했다. 바닥에는 기적의 광장 잔디와 포석을 깔았다.
import math
from atelier import *

SHOT = {"azimuth": -8, "elevation": 9, "lens": 70, "light": "day", "background": "#24303f"}

# ── 재질 ─────────────────────────────────────────────
M_MARBLE = mat('CarraraMarble', '#e4dccb', rough=0.55)
M_COLUMN = mat('ColumnMarble', '#ece5d6', rough=0.45)
M_WALL = mat('GalleryWallMarble', '#b8ad97', rough=0.6)
M_GREY = mat('GreyStoneBand', '#9a968c', rough=0.6)
M_INLAY = mat('GreenInlay', '#6e7c70', rough=0.5)
M_DARK = mat('DoorwayShadow', '#3b342c', rough=0.8)
M_BRONZE = mat('BellBronze', '#8c6b3c', rough=0.38, metal=0.85)
M_WOOD = mat('BellYokeOak', '#5a4129', rough=0.7)
M_IRON = mat('WroughtIron', '#3a3c3e', rough=0.5, metal=0.6)
M_LAWN = mat('PiazzaLawn', '#58733a', rough=0.92)
M_PAVE = mat('Paving', '#cbbfa8', rough=0.8)
M_ROOF = mat('LeadRoof', '#7b7e80', rough=0.5, metal=0.3)

LEAN = math.radians(3.97)
lean = bpy.data.objects.new('PisaTower_Lean', None)
link(lean)
lean.rotation_euler = (LEAN, 0, 0)

belfry_pivot = bpy.data.objects.new('Belfry_Correction', None)
link(belfry_pivot)
belfry_pivot.parent = lean


def own(ob, parent=None):
    ob.parent = parent or lean
    return ob


# ── 메시 도우미 ──────────────────────────────────────

def sector(name, r0, r1, a0, a1, z0, z1, seg=4, material=None):
    """원통 좌표의 휘어진 블록(r0~r1, 각도 a0~a1, 높이 z0~z1)."""
    bm = bmesh.new()
    rows = []
    for i in range(seg + 1):
        a = a0 + (a1 - a0) * i / seg
        c, s = math.cos(a), math.sin(a)
        rows.append([bm.verts.new((r * c, r * s, z)) for r, z in ((r0, z0), (r1, z0), (r1, z1), (r0, z1))])
    for i in range(seg):
        for j in range(4):
            bm.faces.new((rows[i][j], rows[i + 1][j], rows[i + 1][(j + 1) % 4], rows[i][(j + 1) % 4]))
    bm.faces.new(rows[0])
    bm.faces.new(list(reversed(rows[-1])))
    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)
    return assign(ob, material)


def arch_bay(name, rref, r_in, r_out, half, ra, z_s, z_top, n=14, material=None):
    """아치 하나 분의 휘어진 스팬드럴 벽. u(호 길이)·z 평면의 '사각형 - 반원'을 원통에 감는다."""
    H = z_top - z_s
    tc = math.atan2(H, half)
    ths = sorted(set([math.pi * i / n for i in range(n + 1)] + [tc, math.pi - tc]), reverse=True)
    arc, bnd = [], []
    for t in ths:
        c, s = math.cos(t), math.sin(t)
        arc.append((ra * c, z_s + ra * s))
        tt = min(half / abs(c) if abs(c) > 1e-9 else 1e9, H / s if s > 1e-9 else 1e9)
        bnd.append((tt * c, z_s + tt * s))
    bm = bmesh.new()

    def V(u, z, r):
        p = u / rref
        return bm.verts.new((r * math.cos(p), r * math.sin(p), z))

    ao = [V(u, z, r_out) for u, z in arc]
    ai = [V(u, z, r_in) for u, z in arc]
    bo = [V(u, z, r_out) for u, z in bnd]
    bi = [V(u, z, r_in) for u, z in bnd]
    m = len(ths)
    for k in range(m - 1):
        bm.faces.new((ao[k], ao[k + 1], bo[k + 1], bo[k]))       # 바깥면
        bm.faces.new((ai[k], bi[k], bi[k + 1], ai[k + 1]))       # 안쪽면
        bm.faces.new((ao[k], ai[k], ai[k + 1], ao[k + 1]))       # 아치 안쪽(내륜)
        if abs(bnd[k][1] - z_top) < 1e-6 and abs(bnd[k + 1][1] - z_top) < 1e-6:
            bm.faces.new((bo[k], bo[k + 1], bi[k + 1], bi[k]))   # 윗면
    for k in (0, m - 1):                                          # 기둥머리 위 받침면
        bm.faces.new((ao[k], bo[k], bi[k], ai[k]))
    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)
    return assign(ob, material)


def archivolt(name, rref, r0, r1, ra0, ra1, z_s, n=16, material=None):
    """아치 둘레를 따라 바깥으로 살짝 돋운 반원 테(아키볼트)."""
    bm = bmesh.new()
    rows = []
    for i in range(n + 1):
        t = math.pi * i / n
        row = []
        for r, ra in ((r0, ra0), (r1, ra0), (r1, ra1), (r0, ra1)):
            p = ra * math.cos(t) / rref
            row.append(bm.verts.new((r * math.cos(p), r * math.sin(p), z_s + ra * math.sin(t))))
        rows.append(row)
    for i in range(n):
        for j in range(4):
            bm.faces.new((rows[i][j], rows[i + 1][j], rows[i + 1][(j + 1) % 4], rows[i][(j + 1) % 4]))
    bm.faces.new(rows[0])
    bm.faces.new(list(reversed(rows[-1])))
    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)
    return assign(ob, material)


def ring_profile(name, pts, material, seg=96):
    """닫힌 (r,z) 단면을 회전시킨 돌림띠."""
    ob = lathe(name, pts + [pts[0]], material=material, seg=seg, shade_smooth=False)
    return ob


def column(name, h, rs, material, plinth=True, cap_scale=1.0):
    """로마네스크 기둥: 사각 주춧돌 + 아틱 주초 + 약간 배흘림 기둥몸 + 주두 + 사각 관판."""
    parts = []
    pl = 0.12 * cap_scale
    if plinth:
        parts.append(box(name + '_plinth', (rs * 2.7, rs * 2.7, pl), material=material, base=True))
    zb = pl if plinth else 0.0
    cap_h = 0.42 * cap_scale
    ab = 0.12 * cap_scale
    shaft_top = h - cap_h - ab
    prof = [(0, zb), (rs * 1.32, zb), (rs * 1.32, zb + 0.06 * cap_scale), (rs * 1.18, zb + 0.12 * cap_scale),
            (rs * 1.25, zb + 0.18 * cap_scale), (rs * 1.05, zb + 0.24 * cap_scale), (rs, zb + 0.3 * cap_scale),
            (rs * 1.03, zb + (shaft_top - zb) * 0.35), (rs * 0.9, shaft_top - 0.05), (rs * 1.0, shaft_top - 0.03),
            (rs * 1.0, shaft_top), (rs * 0.95, shaft_top + 0.02), (rs * 1.15, shaft_top + cap_h * 0.45),
            (rs * 1.38, shaft_top + cap_h * 0.85), (rs * 1.42, shaft_top + cap_h), (0, shaft_top + cap_h)]
    lt = lathe(name + '_shaft', prof, material=material, seg=14)
    parts.append(lt)
    parts.append(box(name + '_abacus', (rs * 3.0, rs * 3.0, ab), loc=(0, 0, h - ab), material=material, base=True))
    return join(parts, name)


def place_ring(proto, count, radius, z=0.0, start=0.0, parent=None, name=None):
    obs = []
    for i in range(count):
        a = start + 2 * math.pi * i / count
        ob = proto if i == 0 else duplicate(proto, name=f'{name or proto.name}.{i:02d}')
        ob.location = (radius * math.cos(a), radius * math.sin(a), z)
        ob.rotation_euler = (0, 0, a)
        own(ob, parent)
        obs.append(ob)
    return obs


def place_rot(proto, count, start=0.0, parent=None, name=None):
    """원점 기준으로 이미 반경에 놓인 부품을 Z 축 회전으로 복제."""
    for i in range(count):
        ob = proto if i == 0 else duplicate(proto, name=f'{name or proto.name}.{i:02d}')
        ob.rotation_euler = (0, 0, start + 2 * math.pi * i / count)
        own(ob, parent)


# ── 치수 ─────────────────────────────────────────────
Z_GROUND_TOP = 11.3          # 1층 처마(첫 돌림띠 윗면)
H_FLOOR = 6.0                # 회랑층 층고
N_LOGGIA = 6
R_CORE = 5.5                 # 회랑 안쪽 원통벽
R_COL = 7.15                 # 회랑 기둥 중심 반경
R_CORN = 7.62                # 돌림띠 바깥 반경
CORN_DN, CORN_UP = 0.6, 0.15

# ── 기단 · 1층 ───────────────────────────────────────
base = ring_profile('Tower_BaseMoulding', [(0.0, 0.0), (7.95, 0.0), (7.95, 0.55), (7.85, 0.7), (7.75, 0.78),
                                          (7.75, 0.95), (7.6, 1.05), (0.0, 1.05)], M_MARBLE)
own(base)
wall0 = cylinder('GroundFloor_Wall', r=7.35, h=Z_GROUND_TOP - CORN_DN - 1.0 + 0.05, loc=(0, 0, 1.0), seg=96,
                 material=M_MARBLE)
own(wall0)
for k, zb in enumerate((2.6, 4.4, 6.2, 8.0)):
    own(cylinder(f'GroundFloor_GreyCourse_{k}', r=7.37, h=0.22, loc=(0, 0, zb), seg=96, material=M_GREY))

N0 = 15
B0 = 2 * math.pi / N0
R0C = 7.47
col0 = column('GroundFloor_HalfColumn', 8.1, 0.3, M_COLUMN, cap_scale=1.4)
place_ring(col0, N0, R0C, z=1.05, name='GroundFloor_HalfColumn')
half0 = R0C * B0 / 2
bay0 = arch_bay('GroundFloor_BlindArch', R0C, 7.25, 7.72, half0, half0 - 0.36, 1.05 + 8.1, Z_GROUND_TOP - CORN_DN,
                material=M_MARBLE)
bay0 = join([bay0, archivolt('GroundFloor_Archivolt', R0C, 7.70, 7.78, half0 - 0.36, half0 - 0.18, 1.05 + 8.1,
                             material=M_MARBLE)], 'GroundFloor_BlindArch')
place_rot(bay0, N0, start=B0 / 2, name='GroundFloor_BlindArch')

# 블라인드 아치 안의 마름모 상감(로산게)
loz = prism('GroundFloor_Lozenge', [(0, -0.42), (0.55, 0), (0, 0.42), (-0.55, 0)], 0.06, material=M_INLAY)
loz.data.transform(Matrix.Rotation(math.pi / 2, 4, 'Y'))   # 돌출 방향을 +X(반경 바깥)로
loz.data.transform(Matrix.Translation((7.33, 0, 0)))
DOOR_BAY = 14   # 카메라 쪽(-12°) 아치에 정문
for i in range(N0):
    a = B0 / 2 + B0 * i
    if i == DOOR_BAY:
        continue
    ob = loz if i == 0 else duplicate(loz, name=f'GroundFloor_Lozenge.{i:02d}')
    ob.location = (0, 0, 9.75)
    ob.rotation_euler = (0, 0, a)
    own(ob)

# 정문: 문틀(대리석) + 어두운 문 개구부 + 위쪽 반달 상인방
a_d = B0 / 2 + B0 * DOOR_BAY
door = join([
    sector('MainDoor_JambL', 7.33, 7.52, -0.13, -0.095, 1.05, 4.75, seg=1, material=M_MARBLE),
    sector('MainDoor_JambR', 7.33, 7.52, 0.095, 0.13, 1.05, 4.75, seg=1, material=M_MARBLE),
    sector('MainDoor_Lintel', 7.33, 7.56, -0.15, 0.15, 4.75, 5.25, seg=3, material=M_MARBLE),
], 'MainDoor_Frame')
door.rotation_euler = (0, 0, a_d)
own(door)
door_open = sector('MainDoor_Opening', 7.34, 7.42, -0.095, 0.095, 1.05, 4.75, seg=3, material=M_DARK)
door_open.rotation_euler = (0, 0, a_d)
own(door_open)
door_tymp = prism('MainDoor_Lunette', [(math.cos(t) * 0.8, math.sin(t) * 0.8) for t in
                                       [math.pi * i / 12 for i in range(13)]], 0.06, material=M_INLAY)
door_tymp.data.transform(Matrix.Rotation(math.pi / 2, 4, 'X'))
door_tymp.data.transform(Matrix.Rotation(math.pi / 2, 4, 'Z'))
door_tymp.data.transform(Matrix.Translation((7.33, 0, 5.25)))
door_tymp.rotation_euler = (0, 0, a_d)
own(door_tymp)

# ── 돌림띠(처마) — 층마다 하나, 회랑 바닥과 천장을 겸함 ──
corn_prof = lambda zt: [(R_CORE - 0.2, zt - CORN_DN), (R_CORN - 0.2, zt - CORN_DN), (R_CORN - 0.05, zt - CORN_DN + 0.08),
                        (R_CORN - 0.05, zt - 0.32), (R_CORN + 0.1, zt - 0.24), (R_CORN + 0.1, zt - 0.1),
                        (R_CORN, zt), (R_CORN - 0.32, zt), (R_CORN - 0.36, zt + CORN_UP),
                        (R_CORE - 0.2, zt + CORN_UP)]
for k in range(N_LOGGIA + 1):
    zt = Z_GROUND_TOP + k * H_FLOOR
    own(ring_profile(f'Cornice_{k + 1}', corn_prof(zt), M_MARBLE))

# ── 회랑층 2~7 ───────────────────────────────────────
NL = 30
BL = 2 * math.pi / NL
COL_H = 3.95
lcol = column('Loggia_Column', COL_H, 0.19, M_COLUMN)
halfL = R_COL * BL / 2
lbay = arch_bay('Loggia_Arch', R_COL, 6.88, 7.42, halfL, halfL - 0.2, CORN_UP + COL_H,
                H_FLOOR - CORN_DN, material=M_MARBLE)
lbay = join([lbay, archivolt('Loggia_Archivolt', R_COL, 7.40, 7.46, halfL - 0.2, halfL - 0.1, CORN_UP + COL_H,
                             material=M_MARBLE)], 'Loggia_Arch')
core_door = join([
    sector('Loggia_CoreDoor', R_CORE - 0.05, R_CORE + 0.03, -0.09, 0.09, 0.0, 2.3, seg=2, material=M_DARK),
], 'Loggia_CoreDoor')
for f in range(N_LOGGIA):
    z0 = Z_GROUND_TOP + f * H_FLOOR
    fl = f + 2
    core = cylinder(f'Floor{fl}_GalleryWall', r=R_CORE, h=H_FLOOR, loc=(0, 0, z0), seg=64, material=M_WALL, cap=False)
    own(core)
    for b, zz in enumerate((1.6, 3.4)):
        own(cylinder(f'Floor{fl}_GreyCourse_{b}', r=R_CORE + 0.02, h=0.2, loc=(0, 0, z0 + CORN_UP + zz), seg=64,
                     cap=False, material=M_GREY))
    for c in range(NL):
        a = BL * c
        ob = lcol if (f == 0 and c == 0) else duplicate(lcol, name=f'Floor{fl}_Column.{c:02d}')
        ob.location = (R_COL * math.cos(a), R_COL * math.sin(a), z0 + CORN_UP)
        ob.rotation_euler = (0, 0, a)
        own(ob)
        ob = lbay if (f == 0 and c == 0) else duplicate(lbay, name=f'Floor{fl}_Arch.{c:02d}')
        ob.location = (0, 0, z0)
        ob.rotation_euler = (0, 0, a + BL / 2)
        own(ob)
    for d in range(5):
        ob = core_door if (f == 0 and d == 0) else duplicate(core_door, name=f'Floor{fl}_CoreDoor.{d}')
        ob.location = (0, 0, z0 + CORN_UP)
        ob.rotation_euler = (0, 0, BL / 2 + d * 2 * math.pi / 5 + f * 0.7)
        own(ob)

# ── 7층 위 테라스 난간 ───────────────────────────────
Z_TERR = Z_GROUND_TOP + N_LOGGIA * H_FLOOR + CORN_UP
post = box('Terrace_RailPost', (0.05, 0.05, 1.0), material=M_IRON, base=True)
place_ring(post, 72, 7.12, z=Z_TERR, name='Terrace_RailPost')
for k, zz in enumerate((0.45, 0.98)):
    own(torus(f'Terrace_Rail_{k}', R=7.12, r=0.03, loc=(0, 0, Z_TERR + zz), seg=96, rseg=6,
              material=M_IRON))

# ── 종루 ─────────────────────────────────────────────
own(cylinder('Terrace_Floor', r=5.6, h=0.4, loc=(0, 0, Z_TERR - 0.41), seg=64, material=M_MARBLE))
belfry_pivot.location = (0, 0, Z_TERR)
belfry_pivot.rotation_euler = (math.radians(-0.8), 0, 0)
BP = belfry_pivot
RB_O, RB_I = 4.55, 3.85
NB = 12
BB = 2 * math.pi / NB
own(ring_profile('Belfry_Plinth', [(0.0, -0.25), (4.85, -0.25), (4.85, 0.35), (4.7, 0.45), (4.6, 0.6), (4.6, 1.1),
                                  (0.0, 1.1)], M_MARBLE), BP)
own(cylinder('Belfry_Floor', r=RB_I + 0.05, h=0.02, loc=(0, 0, 1.1), seg=48, material=M_WALL), BP)
Z_SPRING_B = 5.0
pier = sector('Belfry_Pier', RB_I, RB_O, -0.15, 0.15, 1.1, Z_SPRING_B, seg=2, material=M_MARBLE)
place_rot(pier, NB, parent=BP, name='Belfry_Pier')
bcol = column('Belfry_HalfColumn', Z_SPRING_B - 1.1, 0.17, M_COLUMN)
place_ring(bcol, NB, RB_O + 0.02, z=1.1, parent=BP, name='Belfry_HalfColumn')
halfB = (RB_O + 0.02) * BB / 2
bbay = arch_bay('Belfry_Arch', RB_O + 0.02, RB_I, RB_O, halfB, halfB - 0.62, Z_SPRING_B, 6.5, material=M_MARBLE)
place_rot(bbay, NB, start=BB / 2, parent=BP, name='Belfry_Arch')
own(ring_profile('Belfry_Cornice', [(RB_I - 0.3, 6.5), (RB_O + 0.05, 6.5), (RB_O + 0.25, 6.7), (RB_O + 0.25, 6.95),
                                   (RB_O + 0.05, 7.05), (RB_O, 7.05), (RB_O, 7.75), (RB_O + 0.15, 7.85),
                                   (RB_O + 0.15, 8.0), (RB_O - 0.35, 8.0), (RB_O - 0.35, 7.25),
                                   (RB_I - 0.3, 7.25)], M_MARBLE), BP)
own(lathe('Belfry_Roof', [(RB_O - 0.35, 7.3), (2.0, 7.55), (0.9, 7.75), (0.9, 8.6), (0.0, 8.75)],
          material=M_ROOF, seg=48), BP)
own(cylinder('Belfry_Ceiling', r=RB_I + 0.05, h=0.2, loc=(0, 0, 6.3), seg=48, material=M_WALL), BP)

# 종(청동) — 아치 개구부 안쪽에 매달린 7개
bell = lathe('Bell', [(0.0, -0.95), (0.62, -0.95), (0.6, -0.86), (0.5, -0.75), (0.42, -0.45), (0.38, -0.1),
                      (0.3, 0.05), (0.0, 0.1)], material=M_BRONZE, seg=28)
yoke = box('Bell_Yoke', (0.25, 1.1, 0.25), loc=(0, 0, 0.12), material=M_WOOD, base=True)
hang = box('Bell_Hanger', (0.12, 0.12, 6.3 - 3.9 - 0.37), material=M_WOOD, base=True)
for i, k in enumerate((0, 2, 4, 6, 7, 9, 11)):
    a = BB / 2 + BB * k
    r = 2.9
    loc = (r * math.cos(a), r * math.sin(a), 3.9)
    b = bell if i == 0 else duplicate(bell, name=f'Bell.{i}')
    b.location = loc
    own(b, BP)
    y = yoke if i == 0 else duplicate(yoke, name=f'Bell_Yoke.{i}')
    y.location = loc
    y.rotation_euler = (0, 0, a)
    own(y, BP)
    h = hang if i == 0 else duplicate(hang, name=f'Bell_Hanger.{i}')
    h.location = (loc[0], loc[1], 3.9 + 0.37)
    own(h, BP)

# ── 바닥: 기단 계단 · 포석 · 잔디 (기울지 않음) ──────
lawn = box('PiazzaDeiMiracoli_Lawn', (26, 26, 0.6), loc=(0, -0.8, -0.6), material=M_LAWN, base=True)
pave = cylinder('Paving_Ring', r=11.5, h=0.64, loc=(0, -0.5, -0.6), seg=96, material=M_PAVE)
step1 = cylinder('Tower_Step_1', r=9.4, h=0.35 + 0.6 - 0.02, loc=(0, 0, -0.58), seg=96, material=M_MARBLE)
step2 = cylinder('Tower_Step_2', r=8.7, h=0.75 + 0.6 - 0.04, loc=(0, 0, -0.56), seg=96, material=M_MARBLE)
