# 판테온 (로마, 서기 125년경) — Blender Python 코드로만 만든 모델.
# 원형 본당(로툰다)과 얕은 돔은 단면 프로필을 Z 축으로 돌린 회전체(lathe)로 만들고,
# 돔 아래쪽의 계단식 고리, 지름 약 8m 의 오쿨루스, 안쪽 반구 천장과 격자 천장(코퍼)까지 넣었다.
# 정면 포르티코는 화강암 기둥 16개(앞줄 8개 + 안쪽 두 줄 4개씩)를 회전체+아칸서스 잎 배열로 만든
# 기둥 두 종류의 메시 공유 복제로 세우고, 엔타블러처·박공(페디먼트)·납 지붕은 상자와 돌출 단면으로 쌓았다.
# 실제 치수(내부 지름·높이 43.3m, 포르티코 폭 약 33m)를 기준으로 했고, 벽돌 아치 등은 단순화했다.
from atelier import *

SHOT = {"azimuth": -58, "elevation": 13, "lens": 45, "light": "golden", "background": "#27303b"}

# ── 재질 ──────────────────────────────────────────────
M_BRICK = mat('Roman_Brick_Stucco', '#877565', 0.85)
M_ARCH = mat('Relieving_Arch_Brick', '#665243', 0.9)
M_MARBLE = mat('Weathered_Marble', '#d0c6b2', 0.55)
M_TRAV = mat('Travertine_Steps', '#c9bc9f', 0.75)
M_GRAN_G = mat('Egyptian_Grey_Granite', '#86827d', 0.35)
M_GRAN_P = mat('Pink_Aswan_Granite', '#a07f74', 0.35)
M_LEAD = mat('Lead_Dome_Sheet', '#8a8c8a', 0.6, 0.1)
M_STUCCO = mat('Dome_Step_Rings', '#b4ada1', 0.8)
M_ROOF = mat('Portico_Roof_Lead', '#5c6265', 0.55, 0.3)
M_BRONZE = mat('Bronze_Doors', '#5b4630', 0.4, 0.85)
M_INSCR = mat('Inscription_Bronze', '#4a3a28', 0.45, 0.7)
M_INNER = mat('Interior_Coffer_Concrete', '#bfb5a3', 0.8)
M_NICHE = mat('Niche_Shadow_Stone', '#6b5f52', 0.9)

TAN = math.tan(math.radians(23.0))   # 박공 경사

# ── 로툰다 치수 ───────────────────────────────────────
R_OUT = 28.5          # 바깥 벽 반지름
R_IN = 21.65          # 안쪽 반지름 (지름 43.3m)
DRUM_TOP = 31.0

seg = 144
drum = lathe('Rotunda_Drum_Wall', [(R_IN, 0.0), (29.0, 0.0), (29.0, 1.3), (R_OUT, 1.5), (R_OUT, DRUM_TOP), (27.8, DRUM_TOP)],
             material=M_BRICK, seg=seg, shade_smooth=True)

# 바깥 코니스 고리 3단(대리석)
def cornice_ring(name, h, proj=0.75):
    p = [(28.3, h - 0.95), (R_OUT + 0.15, h - 0.95), (R_OUT + 0.2, h - 0.7), (R_OUT + proj * 0.6, h - 0.5),
         (R_OUT + proj, h - 0.2), (R_OUT + proj, h), (28.3, h)]
    return lathe(name, p, material=M_MARBLE, seg=seg, shade_smooth=False)

cornice_ring('Rotunda_Cornice_Lower', 19.3)
cornice_ring('Rotunda_Cornice_Middle', 25.5)
cornice_ring('Rotunda_Cornice_Top', DRUM_TOP + 0.05, 0.85)

# 돔 하부 계단식 고리 (7단)
steps = [(27.8, DRUM_TOP)]
r, z = 27.8, DRUM_TOP
for i in range(7):
    z += 0.9
    steps.append((r, z))
    r -= 0.9
    steps.append((r, z))
lathe('Dome_Step_Rings', steps, material=M_STUCCO, seg=seg, shade_smooth=False)
R_CAP0, Z_CAP0 = r, z      # (21.5, 37.3)

# 납판 돔 — 두 점을 지나는 구면 호
R_OC, Z_OC = 4.6, 44.3
c = ((R_OC ** 2 + Z_OC ** 2) - (R_CAP0 ** 2 + Z_CAP0 ** 2)) / (2 * (Z_OC - Z_CAP0))
rho = math.hypot(R_OC, Z_OC - c)
a0 = math.atan2(Z_CAP0 - c, R_CAP0)
a1 = math.atan2(Z_OC - c, R_OC)
cap = []
for i in range(25):
    a = a0 + (a1 - a0) * i / 24
    cap.append((rho * math.cos(a), c + rho * math.sin(a)))
cap += [(R_OC, Z_OC + 0.25)]
lathe('Dome_Lead_Shell', cap, material=M_LEAD, seg=seg)

# 오쿨루스 청동 테
torus('Oculus_Bronze_Rim', R=R_OC - 0.15, r=0.28, loc=(0, 0, Z_OC + 0.25), material=M_BRONZE, seg=64, rseg=10)

# 내부: 원통 벽 + 반구 천장
inner = [(R_OC + 0.1, Z_OC + 0.25), (4.1, Z_OC + 0.25), (4.1, 42.9)]
for i in range(1, 31):
    a = math.radians(79.1 - 79.1 * i / 30)
    inner.append((R_IN * math.cos(a), R_IN + R_IN * math.sin(a)))
inner += [(R_IN, 0.3)]
lathe('Rotunda_Interior_Surface', inner, material=M_INNER, seg=seg)
cylinder('Rotunda_Floor', r=R_IN + 0.05, h=0.3, material=M_MARBLE, seg=seg)

# 내부 격자 천장(코퍼) — 5단 × 28열, 오쿨루스로 내려다보인다
coffer = box('Coffer_Rib', size=(0.5, 2.2, 0.35), material=M_INNER)
rows = [(25, 2.9), (37, 2.6), (49, 2.3), (61, 1.9), (71, 1.5)]
cof = []
for k, (deg, ln) in enumerate(rows):
    el = math.radians(deg)
    for j in range(28):
        az = 2 * math.pi * (j + 0.5) / 28
        rr = (R_IN - 0.15) * math.cos(el)
        zz = R_IN + (R_IN - 0.15) * math.sin(el)
        o = duplicate(coffer, loc=(rr * math.cos(az), rr * math.sin(az), zz),
                      rot=(0, math.pi / 2 - el, az), scale=(1, ln / 2.2, 1), name=f'Coffer_{k}_{j}')
        cof.append(o)
bpy.data.objects.remove(coffer, do_unlink=True)

# ── 중간 블록(아반코르포) ─────────────────────────────
BX = 17.4
B_FRONT = -33.5
B_BACK = -21.0
B_TOP = 25.5
box('Intermediate_Block', size=(2 * BX, B_BACK - B_FRONT, B_TOP), loc=(0, (B_FRONT + B_BACK) / 2, 0), material=M_BRICK, base=True)
# 블록 코니스(대리석) 두 단
for nm, h in (('Block_Cornice_Lower', 19.3), ('Block_Cornice_Upper', B_TOP + 0.05)):
    box(nm, size=(2 * BX + 1.4, B_BACK - B_FRONT + 0.7, 0.95), loc=(0, (B_FRONT + B_BACK) / 2 - 0.35, h - 0.95), material=M_MARBLE, base=True)
# 블록 박공
apex_b = B_TOP + 0.05 + (BX + 0.7) * TAN
BPW = BX + 0.7
extrude_profile('Block_Pediment', [(-BPW, B_TOP), (BPW, B_TOP), (0, B_TOP + BPW * TAN)], depth=8.0,
                loc=(0, B_FRONT + 4.0, 0.0), material=M_BRICK)

def gable_roof(prefix, half_w, base_z, y0, y1, thick=0.6):
    """경사 지붕 납판 두 장 + 앞쪽 경사 코니스(대리석). 처마선은 (±half_w, base_z)."""
    a = math.radians(23.0)
    L = half_w / math.cos(a) + 0.4
    mz = base_z + half_w * TAN / 2
    out = []
    for s in (-1, 1):
        side = 'L' if s < 0 else 'R'
        for nm, off, size, ya, yb, m in (('Roof', thick / 2, thick, y0, y1, M_ROOF),
                                          ('Raking_Cornice', thick / 2 - 0.3, thick + 0.5, y0 - 0.15, y0 + 1.0, M_MARBLE)):
            cx = s * (half_w / 2 + math.sin(a) * off)
            cz = mz + math.cos(a) * off
            out.append(box(f'{prefix}_{nm}_{side}', size=(L if nm == 'Roof' else L - 0.3, yb - ya, size),
                           loc=(cx, (ya + yb) / 2, cz), rot=(0, s * a, 0), material=m))
        # 지붕 덮개 기와 이음(리브)
        off = thick + 0.06
        cx = s * (half_w / 2 + math.sin(a) * off)
        cz = mz + math.cos(a) * off
        n = int((y1 - y0 - 1.2) / 0.9)
        rib = box(f'{prefix}_Roof_Rib_{side}_0', size=(L - 0.2, 0.2, 0.16), loc=(cx, y0 + 1.2, cz), rot=(0, s * a, 0), material=M_ROOF)
        for i in range(1, n):
            duplicate(rib, loc=(cx, y0 + 1.2 + i * 0.9, cz), name=f'{prefix}_Roof_Rib_{side}_{i}')
    return out

gable_roof('Block', BX + 0.7, B_TOP + 0.05, B_FRONT - 0.35, B_FRONT + 6.5)

# ── 포르티코 ─────────────────────────────────────────
P_FRONT = -48.9       # 기단 앞면
STYLO = 1.5
COL_H = 14.2
ENT_Z = STYLO + COL_H  # 15.7
ROW_Y = (-47.6, -43.1, -38.6)
XS = [(i - 3.5) * 4.45 for i in range(8)]

# 기단 + 계단 (단면을 X 방향으로 돌출)
prof = [(B_FRONT + 0.5, 0), (P_FRONT - 2.0, 0)]
for k in range(5):
    y = P_FRONT - 2.0 + 0.4 * k
    prof += [(y, 0.3 * (k + 1)), (y + 0.4, 0.3 * (k + 1))]
prof[-1] = (P_FRONT, STYLO)
prof += [(B_FRONT + 0.5, STYLO)]
# 마지막 계단 단을 기단 윗면과 맞춘다
extrude_profile('Portico_Podium_Steps', prof, depth=2 * 17.0, axis='X',
                loc=(0, 0, 0), material=M_TRAV)

# 기둥 원형 만들기
def make_column(name, shaft_mat):
    parts = []
    parts.append(box(name + '_Plinth', size=(1.95, 1.95, 0.3), material=M_MARBLE, base=True))
    base_p = [(0, 0.3), (0.93, 0.3), (0.96, 0.4), (0.92, 0.5), (0.82, 0.54), (0.80, 0.6), (0.87, 0.66),
              (0.85, 0.74), (0.77, 0.78)]
    parts.append(lathe(name + '_Base', base_p + [(0, 0.78)], material=M_MARBLE, seg=32))
    sh = []
    for i in range(10):
        t = i / 9
        sh.append((0.76 - 0.12 * t ** 1.6, 0.76 + (12.1 - 0.76) * t))
    parts.append(lathe(name + '_Shaft', [(0, 0.76)] + sh + [(0, 12.1)], material=shaft_mat, seg=32))
    bell = [(0, 12.05), (0.70, 12.05), (0.71, 12.22), (0.64, 12.25), (0.65, 12.6), (0.70, 13.1),
            (0.78, 13.55), (0.86, 13.8), (0, 13.8)]
    parts.append(lathe(name + '_Capital_Bell', bell, material=M_MARBLE, seg=32))
    parts.append(box(name + '_Abacus', size=(2.0, 2.0, 0.42), loc=(0, 0, 13.78), material=M_MARBLE, base=True))
    # 아칸서스 잎: 바깥으로 말린 얇은 판
    def leaf(nm, h, w):
        bm = bmesh.new()
        n = 6
        fr, bk = [], []
        for i in range(n):
            t = i / (n - 1)
            ww = w * (1 - 0.55 * t) * (0.7 + 0.3 * math.sin(math.pi * min(1, t * 1.6)))
            x = 0.32 * t ** 2
            zz = h * t
            fr.append((bm.verts.new((x + 0.05, -ww / 2, zz)), bm.verts.new((x + 0.05, ww / 2, zz))))
            bk.append((bm.verts.new((x - 0.03, -ww / 2, zz)), bm.verts.new((x - 0.03, ww / 2, zz))))
        for i in range(n - 1):
            bm.faces.new((fr[i][0], fr[i][1], fr[i + 1][1], fr[i + 1][0]))
            bm.faces.new((bk[i][1], bk[i][0], bk[i + 1][0], bk[i + 1][1]))
            bm.faces.new((bk[i][0], fr[i][0], fr[i + 1][0], bk[i + 1][0]))
            bm.faces.new((fr[i][1], bk[i][1], bk[i + 1][1], fr[i + 1][1]))
        bm.faces.new((fr[-1][0], fr[-1][1], bk[-1][1], bk[-1][0]))
        bm.faces.new((bk[0][0], bk[0][1], fr[0][1], fr[0][0]))
        me = bpy.data.meshes.new(nm)
        bm.to_mesh(me)
        bm.free()
        ob = bpy.data.objects.new(nm, me)
        link(ob)
        return assign(ob, M_MARBLE)
    for ring, (z0, h, w, rr, off) in enumerate(((12.25, 0.75, 0.5, 0.64, 0.0), (12.6, 0.85, 0.48, 0.68, 0.5))):
        for j in range(8):
            a = 2 * math.pi * (j + off) / 8
            lf = leaf(f'{name}_Leaf_{ring}_{j}', h, w)
            lf.location = (rr * math.cos(a), rr * math.sin(a), z0)
            lf.rotation_euler = (0, 0, a)
            parts.append(lf)
    for j in range(4):   # 모서리 소용돌이(볼류트)
        a = math.pi / 4 + j * math.pi / 2
        parts.append(sphere(f'{name}_Volute_{j}', r=0.17, loc=(0.92 * math.cos(a), 0.92 * math.sin(a), 13.62),
                            material=M_MARBLE, seg=12, rings=8, scale=(1, 0.6, 1)))
    return join(parts, name)

col_g = make_column('Column_Grey_Granite', M_GRAN_G)
col_p = make_column('Column_Pink_Granite', M_GRAN_P)
col_g.location = (XS[0], ROW_Y[0], STYLO)
for i, x in enumerate(XS[1:], 1):
    duplicate(col_g, loc=(x, ROW_Y[0], STYLO), name=f'Column_Front_{i + 1}')
col_g.name = 'Column_Front_1'
first = True
for r_i, y in enumerate(ROW_Y[1:], 2):
    for i in (0, 2, 5, 7):
        if first:
            col_p.location = (XS[i], y, STYLO)
            col_p.name = f'Column_Row{r_i}_{i + 1}'
            first = False
        else:
            duplicate(col_p, loc=(XS[i], y, STYLO), name=f'Column_Row{r_i}_{i + 1}')

# 블록 앞면 벽기둥(안타)
for s in (-1, 1):
    box(f'Anta_Pilaster_{"L" if s < 0 else "R"}', size=(1.7, 0.5, COL_H), loc=(s * (BX - 1.0), B_FRONT - 0.25, STYLO), material=M_MARBLE, base=True)

# 엔타블러처
EX = 16.55
EF = ROW_Y[0] - 0.8
box('Architrave', size=(2 * EX, B_FRONT - EF, 1.3), loc=(0, (EF + B_FRONT) / 2, ENT_Z), material=M_MARBLE, base=True)
box('Frieze', size=(2 * EX - 0.2, B_FRONT - EF - 0.1, 1.25), loc=(0, (EF + B_FRONT) / 2 + 0.05, ENT_Z + 1.3), material=M_MARBLE, base=True)
box('Cornice_Bed', size=(2 * EX + 0.5, B_FRONT - EF + 0.25, 0.4), loc=(0, (EF + B_FRONT) / 2 - 0.125, ENT_Z + 2.55), material=M_MARBLE, base=True)
CX = EX + 0.6
CF = EF - 0.6
box('Cornice_Corona', size=(2 * CX, B_FRONT - CF, 0.65), loc=(0, (CF + B_FRONT) / 2, ENT_Z + 2.95), material=M_MARBLE, base=True)
ENT_TOP = ENT_Z + 3.6   # 19.3

# 모딜리온(코니스 받침) 배열
mod = box('Modillion', size=(0.28, 0.5, 0.25), material=M_MARBLE, base=True)
k = 0
n_front = 46
for i in range(n_front):
    x = -CX + 0.4 + (2 * CX - 0.8) * i / (n_front - 1)
    duplicate(mod, loc=(x, EF - 0.25 + 0.0, ENT_Z + 2.7), name=f'Modillion_Front_{i}')
for s in (-1, 1):
    n_side = 20
    for i in range(n_side):
        y = EF + 0.4 + (B_FRONT - EF - 0.8) * i / (n_side - 1)
        duplicate(mod, loc=(s * (EX + 0.25), y, ENT_Z + 2.7), rot=(0, 0, math.pi / 2), name=f'Modillion_Side_{s}_{i}')
# 경사 코니스 아래 모딜리온
a23 = math.radians(23.0)
for sd in (-1, 1):
    for i in range(18):
        t = (i + 0.5) / 18
        x = sd * (CX - 0.9) * (1 - t)
        zz = ENT_TOP + (0.9 + (CX - 0.9) * t) * TAN - 0.62 - 0.25
        duplicate(mod, loc=(x, CF + 0.55, zz), rot=(0, sd * a23, 0), name=f'Modillion_Rake_{sd}_{i}')
bpy.data.objects.remove(mod, do_unlink=True)

# 명문: M·AGRIPPA·L·F·COS·TERTIVM·FECIT
txt = text3d('Inscription_Agrippa', 'M·AGRIPPA·L·F·COS·TERTIVM·FECIT', size=0.95, depth=0.04,
             loc=(0, EF + 0.08, ENT_Z + 1.62), rot=(math.pi / 2, 0, 0), material=M_INSCR)
txt.data.space_character = 1.15

# 포르티코 박공 + 지붕
PW = CX
extrude_profile('Portico_Pediment_Tympanum', [(-PW + 0.3, 0), (PW - 0.3, 0), (0, (PW - 0.3) * TAN)],
                depth=B_FRONT - CF - 0.8, loc=(0, (CF + 0.8 + B_FRONT) / 2, ENT_TOP - 0.05), material=M_MARBLE)
gable_roof('Portico', PW, ENT_TOP, CF, B_FRONT + 0.3)

# 청동 문과 문틀, 양옆 벽감
box('Door_Frame_Marble', size=(7.6, 0.5, 11.4), loc=(0, B_FRONT - 0.2, STYLO), material=M_MARBLE, base=True)
box('Bronze_Door_Leaves', size=(5.6, 0.4, 9.0), loc=(0, B_FRONT - 0.5, STYLO), material=M_BRONZE, base=True)
box('Door_Grille', size=(5.6, 0.3, 1.8), loc=(0, B_FRONT - 0.48, STYLO + 9.2), material=M_BRONZE, base=True)
box('Door_Leaf_Seam', size=(0.08, 0.06, 9.0), loc=(0, B_FRONT - 0.73, STYLO), material=M_INSCR, base=True)
for s in (-1, 1):
    x = s * 11.1
    side = 'L' if s < 0 else 'R'
    box(f'Apse_Niche_{side}', size=(5.0, 0.1, 8.0), loc=(x, B_FRONT - 0.04, STYLO), material=M_NICHE, base=True)
    cylinder(f'Apse_Niche_Arch_{side}', r=2.5, h=0.1, loc=(x, B_FRONT - 0.04, STYLO + 8.0), rot=(math.pi / 2, 0, 0),
             material=M_NICHE, seg=32, base=False)

# ── 로툰다 벽의 벽돌 하중분산 아치(블라인드 아치) ──────
def arch_mesh(name, rad, tube, ang=math.pi):
    bm = bmesh.new()
    n, m = 20, 8
    rings = []
    for i in range(n + 1):
        t = (math.pi - ang) / 2 + ang * i / n
        cx, cz = rad * math.cos(t), rad * math.sin(t)
        ring = []
        for j in range(m):
            b = 2 * math.pi * j / m
            rr = rad + tube * math.cos(b)
            ring.append(bm.verts.new((tube * math.sin(b), rr * math.cos(t), rr * math.sin(t))))
        rings.append(ring)
    for i in range(n):
        for j in range(m):
            bm.faces.new((rings[i][j], rings[i + 1][j], rings[i + 1][(j + 1) % m], rings[i][(j + 1) % m]))
    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, M_ARCH)

for zone, (zc, rad, count, ang) in enumerate(((9.0, 3.6, 18, math.pi), (21.4, 2.2, 28, math.pi * 0.8), (27.4, 2.0, 32, math.pi * 0.8))):
    proto = arch_mesh(f'Relieving_Arch_Z{zone}', rad, 0.22, ang)
    used = False
    for j in range(count):
        az = 2 * math.pi * (j + 0.5) / count
        x, y = (R_OUT - 0.08) * math.cos(az), (R_OUT - 0.08) * math.sin(az)
        if y < 0 and abs(x) < BX + rad + 0.6 and zc < apex_b:   # 중간 블록과 겹치는 곳은 건너뜀
            continue
        if not used:
            proto.location, proto.rotation_euler, used = (x, y, zc), (0, 0, az), True
        else:
            duplicate(proto, loc=(x, y, zc), rot=(0, 0, az), name=f'Relieving_Arch_Z{zone}_{j}')

# 중간 블록 옆면 벽감
for s in (-1, 1):
    side = 'L' if s < 0 else 'R'
    for k, yc in enumerate((-30.0, -25.2)):
        box(f'Block_Side_Niche_{side}{k}', size=(0.1, 3.2, 6.0), loc=(s * (BX + 0.04), yc, 6.0), material=M_NICHE, base=True)
        cylinder(f'Block_Side_Niche_Arch_{side}{k}', r=1.6, h=0.1, loc=(s * (BX + 0.04), yc, 12.0), rot=(0, math.pi / 2, 0),
                 material=M_NICHE, seg=24, base=False)
        box(f'Block_Side_Niche_Sill_{side}{k}', size=(0.3, 3.8, 0.3), loc=(s * (BX + 0.1), yc, 5.7), material=M_MARBLE, base=True)
