# 성 바실리 대성당(포크로프스키 대성당, 모스크바 붉은 광장) — 코드로만 만든 모델.
# 높은 천막 지붕(샤툐르)의 중앙 교회를 큰 팔각 예배당 4개(동서남북)와 작은 예배당 4개(대각선)가 둘러싼
# 9개 예배당 구성을 높이 6.5m 의 기단 위에 세웠다. 양파 돔은 bmesh 로 직접 짠 회전체이며,
# 꼭짓점을 높이에 따라 비틀고 면마다 다른 재질을 배정해 나선·줄무늬·비늘 무늬를 만들었다(텍스처 없음).
# 코코시니크(용골형 장식 박공)는 측면 프로필을 돌출한 판을 팔각 벽면마다 배열했고,
# 같은 부품은 링크 복제로 메시를 공유한다. 종탑과 지붕 덮인 계단 현관 두 곳을 더했다. 단위 m, 높이 약 47.5m.
from atelier import *

SHOT = {"azimuth": -38, "elevation": 12, "lens": 55, "light": "golden", "background": "#1f2a36"}

# ---------------------------------------------------------------- 재질
BRICK = mat('Brick_Red', '#9a3b2b', 0.85)
STONE = mat('Whitestone_Trim', '#e4d9c3', 0.7)
GOLD = mat('Gilded_Metal', '#d8a943', 0.28, 1.0)
GREEN = mat('Dome_Green', '#2c7a4b', 0.4)
YELLOW = mat('Dome_Yellow', '#dcb43a', 0.42)
RED = mat('Dome_Red', '#b5302b', 0.42)
BLUE = mat('Dome_Blue', '#2e5c9c', 0.42)
CREAM = mat('Dome_Cream', '#efe7d2', 0.42)
ROOF = mat('Roof_Green_Metal', '#3f6b52', 0.5, 0.2)
WINDOW = mat('Window_Dark', '#231c1b', 0.35)
STEP = mat('Granite_Steps', '#8b877e', 0.8)
TENT = mat('Tent_Ochre', '#c99a52', 0.6)

PLAT_TOP = 6.5
C8 = math.cos(math.pi / 8)
S8 = math.sin(math.pi / 8)


def set_mats(ob, mats):
    ob.data.materials.clear()
    for m in mats:
        ob.data.materials.append(m)
    return ob


def bm_obj(name, bm, mats, loc=(0, 0, 0), smooth_on=False):
    me = bpy.data.meshes.new(name)
    for m in mats:
        me.materials.append(m)  # 재질 슬롯을 먼저 만들어야 면별 재질 번호가 유지된다
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new(name, me)
    link(ob)
    ob.location = Vector(loc)
    if smooth_on:
        smooth(ob)
    return ob


def catmull(pts, sub=3):
    """(r,z) 제어점을 Catmull-Rom 으로 부드럽게."""
    out = []
    n = len(pts)
    for i in range(n - 1):
        p0 = pts[max(i - 1, 0)]
        p1, p2 = pts[i], pts[i + 1]
        p3 = pts[min(i + 2, n - 1)]
        for s in range(sub):
            t = s / sub
            t2, t3 = t * t, t * t * t
            out.append(tuple(0.5 * ((2 * p1[k]) + (-p0[k] + p2[k]) * t + (2 * p0[k] - 5 * p1[k] + 4 * p2[k] - p3[k]) * t2
                                    + (-p0[k] + 3 * p1[k] - 3 * p2[k] + p3[k]) * t3) for k in range(2)))
    out.append(pts[-1])
    return out


ONION = catmull([(0.80, 0.00), (0.90, 0.06), (0.98, 0.15), (1.00, 0.25), (0.97, 0.35), (0.86, 0.46),
                 (0.68, 0.56), (0.48, 0.65), (0.31, 0.73), (0.18, 0.81), (0.10, 0.88), (0.055, 0.94), (0.0, 1.0)], 3)


def onion(name, rmax, H, loc, mats, pattern='spiral', bands=12, twist=1.0, seg=48, rib=0.035):
    """양파 돔. pattern: spiral(비틀린 줄무늬) / stripe(세로 줄무늬) / scale(비늘 체크) / plain.
    줄무늬 띠마다 반경을 살짝 부풀려 골(리브)이 진 입체감을 준다."""
    bm = bmesh.new()
    rows = []
    prof = ONION
    per = seg // bands
    if pattern in ('plain', 'scale'):
        rib = 0.0
    for j, (rn, zn) in enumerate(prof[:-1]):
        ring = []
        for i in range(seg):
            a = 2 * math.pi * i / seg + twist * math.pi * zn
            rr = rn * rmax * (1 + rib * math.sin(math.pi * (i % per) / per))
            ring.append(bm.verts.new((rr * math.cos(a), rr * math.sin(a), zn * H)))
        rows.append(ring)
    apex = bm.verts.new((0, 0, H))
    for j in range(len(rows) - 1):
        for i in range(seg):
            f = bm.faces.new((rows[j][i], rows[j][(i + 1) % seg], rows[j + 1][(i + 1) % seg], rows[j + 1][i]))
            if pattern in ('spiral', 'stripe'):
                f.material_index = (i // per) % len(mats)
            elif pattern == 'scale':
                f.material_index = ((i // per) + (j // 3)) % len(mats)
            else:
                f.material_index = 0
    for i in range(seg):
        f = bm.faces.new((rows[-1][i], rows[-1][(i + 1) % seg], apex))
        f.material_index = (i // per) % len(mats) if pattern != 'plain' else 0
    # 아래 덮개
    bot = bm.faces.new(list(reversed(rows[0])))
    bot.material_index = 0
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    ob = bm_obj(name, bm, mats, loc, smooth_on=True)
    bot_poly = ob.data.polygons[-1]
    bot_poly.use_smooth = False
    return ob


def octa(name, R, z0, z1, loc, material, R1=None):
    """면이 축 방향을 향하는 팔각기둥(외접반경 R)."""
    ob = cylinder(name, R, z1 - z0, (loc[0], loc[1], z0), (0, 0, math.pi / 8), material, seg=8, r2=R1)
    return ob


# ---------------------------------------------------------------- 공유 부품(링크 복제용)
def keel_profile(w, h, n=10):
    """용골형(끝이 뾰족한) 아치 박공의 XZ 프로필."""
    hw = w / 2
    pts = [(-hw, 0.0), (hw, 0.0)]
    shoulder = h * 0.34
    right = []
    for k in range(n + 1):
        t = k / n
        a = math.pi * 0.0 + t * math.pi * 0.62
        x = hw * math.cos(a)
        z = shoulder + hw * math.sin(a) * 0.9
        right.append((x, z))
    # 뾰족 끝으로 오목하게
    x_end, z_end = right[-1]
    for k in range(1, 5):
        t = k / 4
        x = x_end * (1 - t)
        z = z_end + (h - z_end) * (t ** 0.75)
        right.append((x, z))
    left = [(-x, z) for x, z in reversed(right[:-1])]
    return pts + right + left


_SHARED = {}


def kokoshnik_mesh(w, h, depth):
    key = ('k', round(w, 3), round(h, 3), round(depth, 3))
    if key not in _SHARED:
        ob = extrude_profile('Kokoshnik', keel_profile(w, h), depth, material=STONE)
        inner = extrude_profile('KokoshnikInset', keel_profile(w * 0.72, h * 0.78), depth * 0.5,
                                loc=(0, depth * 0.5, h * 0.08), material=BRICK)
        j = join([ob, inner], 'Kokoshnik')
        _SHARED[key] = j
        j.hide_render = True
        j.hide_viewport = True
        j['atelier_cutter'] = True  # 원본 템플릿은 숨김
    return _SHARED[key]


def place_on_face(template, name, cx, cy, ang, dist, z, scale=None):
    ob = duplicate(template, name=name)
    ob.hide_render = False
    ob.hide_viewport = False
    if 'atelier_cutter' in ob:
        del ob['atelier_cutter']
    ob.location = (cx + dist * math.cos(ang), cy + dist * math.sin(ang), z)
    ob.rotation_euler = (0, 0, ang - math.pi / 2)
    if scale:
        ob.scale = scale
    return ob


def window_mesh(w, h):
    key = ('w', round(w, 3), round(h, 3))
    if key not in _SHARED:
        frame = box('WinFrame', (w * 1.5, 0.16, h * 1.15), (0, 0, 0), material=STONE, base=True)
        cap = extrude_profile('WinCap', keel_profile(w * 1.7, h * 0.55), 0.2, loc=(0, 0, h * 1.12), material=STONE)
        glass_ = box('WinGlass', (w, 0.1, h), (0, 0.06, h * 0.08), material=WINDOW, base=True)
        j = join([frame, cap, glass_], 'Window')
        j.hide_render = True
        j.hide_viewport = True
        j['atelier_cutter'] = True
        _SHARED[key] = j
    return _SHARED[key]


def cross_mesh():
    if 'cross' not in _SHARED:
        parts = [
            box('CrossPost', (0.14, 0.14, 3.0), (0, 0, 0), material=GOLD, base=True),
            box('CrossBar', (1.3, 0.12, 0.13), (0, 0, 2.05), material=GOLD),
            box('CrossTop', (0.7, 0.12, 0.12), (0, 0, 2.55), material=GOLD),
            box('CrossFoot', (0.8, 0.12, 0.12), (0, 0, 1.15), rot=(0, math.radians(-22), 0), material=GOLD),
            sphere('CrossApple', 0.22, (0, 0, 0.15), material=GOLD, seg=16, rings=8),
        ]
        j = join(parts, 'Cross')
        j.hide_render = True
        j.hide_viewport = True
        j['atelier_cutter'] = True
        _SHARED['cross'] = j
    return _SHARED['cross']


def put_cross(name, x, y, z, s=1.0):
    ob = duplicate(cross_mesh(), name=name)
    ob.hide_render = False
    ob.hide_viewport = False
    del ob['atelier_cutter']
    ob.location = (x, y, z)
    ob.scale = (s, s, s)
    ob.rotation_euler = (0, 0, 0.3)
    return ob


# ---------------------------------------------------------------- 기단(포드클레트)
OUTLINE = [(13, -13), (13, -7), (20, -7), (20, 7), (13, 7), (13, 13), (7, 13), (7, 20), (-7, 20), (-7, 13),
           (-13, 13), (-13, 7), (-20, 7), (-20, -7), (-13, -7), (-13, -13), (-7, -13), (-7, -20), (7, -20), (7, -13)]


def offset_poly(pts, d):
    """반시계 다각형을 안쪽으로 d 만큼(음수면 바깥) 오프셋 — 직각 다각형용."""
    n = len(pts)
    out = []
    for i in range(n):
        p0, p1, p2 = Vector((*pts[i - 1], 0)), Vector((*pts[i], 0)), Vector((*pts[(i + 1) % n], 0))
        e1 = (p1 - p0).normalized()
        e2 = (p2 - p1).normalized()
        n1 = Vector((-e1.y, e1.x, 0))
        n2 = Vector((-e2.y, e2.x, 0))
        out.append(((p1 + d * (n1 + n2) / (1 + n1.dot(n2))).x, (p1 + d * (n1 + n2) / (1 + n1.dot(n2))).y))
    return out


def ring_wall(name, outer, inner, z0, z1, material):
    bm = bmesh.new()
    n = len(outer)
    ob_ = [bm.verts.new((x, y, z0)) for x, y in outer]
    ot = [bm.verts.new((x, y, z1)) for x, y in outer]
    ib = [bm.verts.new((x, y, z0)) for x, y in inner]
    it = [bm.verts.new((x, y, z1)) for x, y in inner]
    for i in range(n):
        k = (i + 1) % n
        bm.faces.new((ob_[i], ob_[k], ot[k], ot[i]))
        bm.faces.new((ib[k], ib[i], it[i], it[k]))
        bm.faces.new((ot[i], ot[k], it[k], it[i]))
        bm.faces.new((ob_[k], ob_[i], ib[i], ib[k]))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm_obj(name, bm, [material])


platform = prism('Podklet_Base', OUTLINE, PLAT_TOP, material=BRICK)
prism('Gallery_Paving', offset_poly(OUTLINE, 0.3), 0.12, loc=(0, 0, PLAT_TOP - 0.05), material=STEP)
ring_wall('Podklet_Plinth', offset_poly(OUTLINE, -0.25), offset_poly(OUTLINE, 0.6), 0.0, 0.7, STONE)
ring_wall('Podklet_Cornice', offset_poly(OUTLINE, -0.22), offset_poly(OUTLINE, 0.6), 5.7, 6.2, STONE)
ring_wall('Gallery_Parapet', offset_poly(OUTLINE, 0.0), offset_poly(OUTLINE, 0.45), PLAT_TOP - 0.2, PLAT_TOP + 1.1, BRICK)
ring_wall('Gallery_Parapet_Cap', offset_poly(OUTLINE, -0.08), offset_poly(OUTLINE, 0.53), PLAT_TOP + 1.1, PLAT_TOP + 1.28, STONE)

# 기단 아래층 아치 창(어두운 아치 판)
arch_t = None
for i in range(len(OUTLINE)):
    p0, p1 = Vector((*OUTLINE[i], 0)), Vector((*OUTLINE[(i + 1) % len(OUTLINE)], 0))
    e = p1 - p0
    L = e.length
    d = e.normalized()
    nrm = Vector((d.y, -d.x, 0))  # 바깥쪽
    ang = math.atan2(nrm.y, nrm.x)
    cnt = int(L // 3.4)
    if cnt < 1:
        continue
    for k in range(cnt):
        t = (k + 0.5) / cnt
        p = p0 + e * t
        w = window_mesh(0.9, 1.6)
        place_on_face(w, 'Podklet_Window', p.x, p.y, ang, 0.06, 2.0)

# ---------------------------------------------------------------- 예배당
def drum_arcade(prefix, cx, cy, r, z0, z1, n=16):
    """돔 아래 원통 드럼 + 흰 띠 + 벽기둥(블라인드 아케이드)."""
    cylinder(prefix + '_Drum', r, z1 - z0, (cx, cy, z0), material=BRICK, seg=32)
    cylinder(prefix + '_DrumBase', r + 0.18, 0.35, (cx, cy, z0), material=STONE, seg=32)
    cylinder(prefix + '_DrumCornice', r + 0.2, 0.3, (cx, cy, z1 - 0.3), material=STONE, seg=32)
    pil = cylinder(prefix + '_Pilaster', 0.09 * r, z1 - z0 - 0.6, (0, 0, z0 + 0.35), material=STONE, seg=8)
    for k in range(n):
        a = 2 * math.pi * (k + 0.5) / n
        ob = pil if k == 0 else duplicate(pil, name=prefix + '_Pilaster')
        ob.location = (cx + r * math.cos(a), cy + r * math.sin(a), z0 + 0.35)
    for k in range(4):
        a = math.pi / 4 + k * math.pi / 2 + 2 * math.pi / n * 0
        w = window_mesh(0.22 * r, 0.42 * (z1 - z0))
        place_on_face(w, prefix + '_DrumWindow', cx, cy, a, r - 0.03, z0 + 0.22 * (z1 - z0))


def chapel(prefix, cx, cy, R, top, dome_r, dome_h, mats, pattern, twist, bands, drum_r, drum_h, cross_s):
    z0 = PLAT_TOP - 0.3
    octa(prefix + '_Octagon', R, z0, top, (cx, cy), BRICK)
    ap = R * C8
    side = 2 * R * S8
    # 흰 돌 띠(코니스)
    for zz, hh in ((z0 + 0.3, 0.45), (z0 + (top - z0) * 0.45, 0.35), (top - 0.45, 0.45)):
        octa(prefix + '_Cornice', R + 0.22, zz, zz + hh, (cx, cy), STONE)
    # 모서리 벽기둥
    for k in range(8):
        a = math.pi / 8 + k * math.pi / 4
        b = box(prefix + '_CornerPier', (0.35, 0.35, top - z0 - 0.4), (cx + (R + 0.02) * math.cos(a), cy + (R + 0.02) * math.sin(a), z0 + 0.3),
                (0, 0, a), STONE, base=True)
    # 창
    wz = z0 + (top - z0) * 0.55
    for k in range(8):
        a = k * math.pi / 4
        w = window_mesh(side * 0.18, (top - z0) * 0.22)
        place_on_face(w, prefix + '_Window', cx, cy, a, ap + 0.02, wz)
    # 코코시니크 1단
    kk = kokoshnik_mesh(side * 0.96, side * 0.85, 0.5)
    for k in range(8):
        place_on_face(kk, prefix + '_Kokoshnik', cx, cy, k * math.pi / 4, ap + 0.05, top - 0.05)
    # 안쪽 단 + 코코시니크 2단(엇갈림)
    R2 = drum_r * 1.18
    octa(prefix + '_Tier', R2, top - 0.1, top + side * 0.75, (cx, cy), BRICK)
    s2 = 2 * R2 * S8
    k2 = kokoshnik_mesh(s2 * 0.95, s2 * 0.8, 0.4)
    for k in range(8):
        a = k * math.pi / 4
        place_on_face(k2, prefix + '_Kokoshnik2', cx, cy, a, R2 * C8 + 0.05, top + side * 0.75 - 0.15)
    dz0 = top + side * 0.75 - 0.15
    drum_arcade(prefix, cx, cy, drum_r, dz0, dz0 + drum_h)
    dtop = dz0 + drum_h
    onion(prefix + '_OnionDome', dome_r, dome_h, (cx, cy, dtop - 0.05), mats, pattern, bands, twist)
    put_cross(prefix + '_Cross', cx, cy, dtop + dome_h - 0.25, cross_s)


D_BIG = 14.0
D_SMALL = 9.6
big = [
    ('Chapel_West_EntryIntoJerusalem', (-D_BIG, 0), [GREEN, YELLOW], 'spiral', 1.1, 12),
    ('Chapel_East_HolyTrinity', (D_BIG, 0), [RED, CREAM], 'spiral', -1.0, 12),
    ('Chapel_South_StNicholas', (0, -D_BIG), [GREEN, RED, YELLOW], 'scale', 0.0, 12),
    ('Chapel_North_StCyprian', (0, D_BIG), [BLUE, CREAM], 'spiral', 1.0, 12),
]
for name, (x, y), mats, pat, tw, bd in big:
    chapel(name, x, y, 4.6, 17.0, 3.75, 7.0, mats, pat, tw, bd, 2.55, 4.6, 1.05)

small = [
    ('Chapel_SW_AlexanderSvirsky', (-D_SMALL, -D_SMALL), [YELLOW, GREEN], 'stripe', 0.0, 16),
    ('Chapel_SE_VarlaamKhutynsky', (D_SMALL, -D_SMALL), [RED, YELLOW], 'spiral', -1.2, 12),
    ('Chapel_NE_ThreePatriarchs', (D_SMALL, D_SMALL), [GREEN, CREAM], 'scale', 0.0, 16),
    ('Chapel_NW_GregoryArmenia', (-D_SMALL, D_SMALL), [BLUE, YELLOW], 'spiral', 1.2, 12),
]
for name, (x, y), mats, pat, tw, bd in small:
    chapel(name, x, y, 2.9, 14.5, 2.5, 5.0, mats, pat, tw, bd, 1.65, 3.4, 0.8)

# ---------------------------------------------------------------- 중앙 교회(포크로프) — 천막 지붕
CR = 6.4
z0 = PLAT_TOP - 0.3
ctop = 22.0
octa('Central_Octagon', CR, z0, ctop, (0, 0), BRICK)
for zz, hh in ((14.0, 0.45), (ctop - 0.5, 0.5)):
    octa('Central_Cornice', CR + 0.25, zz, zz + hh, (0, 0), STONE)
cside = 2 * CR * S8
for k in range(8):
    a = math.pi / 8 + k * math.pi / 4
    box('Central_CornerPier', (0.45, 0.45, ctop - z0 - 0.6), (CR * math.cos(a), CR * math.sin(a), z0 + 0.3), (0, 0, a), STONE, base=True)
    w = window_mesh(0.8, 2.6)
    place_on_face(w, 'Central_Window', 0, 0, k * math.pi / 4, CR * C8 + 0.02, 17.0)
# 코코시니크 3단(점점 작아지며 엇갈림)
tierR = [CR, CR * 0.86, CR * 0.74]
tz = ctop - 0.05
for t, Rt in enumerate(tierR):
    st = 2 * Rt * S8
    kh = st * 0.8
    if t > 0:
        octa('Central_Tier', Rt, tz - 0.6, tz + 0.05, (0, 0), BRICK)
    km = kokoshnik_mesh(st * 0.95, kh, 0.55)
    off = (math.pi / 8) * (t % 2)
    for k in range(8):
        place_on_face(km, 'Central_Kokoshnik', 0, 0, k * math.pi / 4 + off, Rt * C8 + 0.05 - (0.4 if t % 2 else 0), tz)
    tz += kh * 0.7
# 팔각 드럼(창이 있는 팔각 고층부)
dR = 4.7
dz0 = tz - 0.4
dz1 = dz0 + 4.4
octa('Central_Drum', dR, dz0, dz1, (0, 0), BRICK)
octa('Central_DrumCornice', dR + 0.22, dz1 - 0.35, dz1, (0, 0), STONE)
octa('Central_DrumBase', dR + 0.18, dz0, dz0 + 0.35, (0, 0), STONE)
for k in range(8):
    a = math.pi / 8 + k * math.pi / 4
    box('Central_DrumPier', (0.35, 0.35, dz1 - dz0 - 0.6), (dR * math.cos(a), dR * math.sin(a), dz0 + 0.3), (0, 0, a), STONE, base=True)
    w = window_mesh(0.55, 1.9)
    place_on_face(w, 'Central_DrumWindow', 0, 0, k * math.pi / 4, dR * C8 + 0.02, dz0 + 0.9)

# 천막 지붕: 팔각뿔, 면을 격자로 나눠 마름모 무늬
def tent(name, R0, R1, z0, z1, loc, mats, cols=4, rows=14):
    bm = bmesh.new()
    grid = []
    for j in range(rows + 1):
        t = j / rows
        R = R0 + (R1 - R0) * t
        ring = []
        for k in range(8):
            a0 = math.pi / 8 + k * math.pi / 4
            a1 = a0 + math.pi / 4
            p0 = Vector((R * math.cos(a0), R * math.sin(a0), 0))
            p1 = Vector((R * math.cos(a1), R * math.sin(a1), 0))
            for c in range(cols):
                p = p0.lerp(p1, c / cols)
                ring.append(bm.verts.new((p.x, p.y, z0 + (z1 - z0) * t)))
        grid.append(ring)
    n = 8 * cols
    for j in range(rows):
        for i in range(n):
            f = bm.faces.new((grid[j][i], grid[j][(i + 1) % n], grid[j + 1][(i + 1) % n], grid[j + 1][i]))
            if len(mats) == 3:  # 마름모 격자: 줄눈선 + 마름모 속
                if (i + j) % 4 == 0 or (i - j) % 4 == 0:
                    f.material_index = 1
                elif (i + j) % 4 == 2 and (i - j) % 4 == 2:
                    f.material_index = 2
                else:
                    f.material_index = 0
            else:
                f.material_index = (i + j) % 2 if len(mats) > 1 else 0
    bm.faces.new(list(reversed(grid[0])))
    bm.faces.new(grid[-1])
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm_obj(name, bm, mats, loc)


tz0 = dz1 - 0.05
tz1 = tz0 + 9.4
tent('Central_TentRoof', dR + 0.05, 1.05, tz0, tz1, (0, 0, 0), [STONE, TENT, GREEN], 8, 28)
# 천막 모서리 갈빗대
for k in range(8):
    a = math.pi / 8 + k * math.pi / 4
    pA = Vector(((dR + 0.12) * math.cos(a), (dR + 0.12) * math.sin(a), tz0))
    pB = Vector((1.12 * math.cos(a), 1.12 * math.sin(a), tz1))
    curve_tube('Central_TentRib', [tuple(pA), tuple(pB)], 0.1, material=GOLD)
octa('Central_LanternBase', 1.25, tz1 - 0.1, tz1 + 0.3, (0, 0), STONE)
cylinder('Central_Lantern', 0.95, 1.4, (0, 0, tz1 + 0.3), material=GOLD, seg=24)
onion('Central_GoldenDome', 1.35, 2.5, (0, 0, tz1 + 1.65), [GOLD], 'plain')
put_cross('Central_Cross', 0, 0, tz1 + 1.65 + 2.5 - 0.15, 0.75)

# ---------------------------------------------------------------- 종탑(남동쪽, 17세기)
bx, by = 15.8, 15.8
box('BellTower_Base', (6.4, 6.4, 11.0), (bx, by, 0), material=BRICK, base=True)
box('BellTower_BaseCornice', (6.8, 6.8, 0.45), (bx, by, 10.6), material=STONE, base=True)
box('BellTower_Plinth', (6.7, 6.7, 0.7), (bx, by, 0), material=STONE, base=True)
for sx in (-1, 1):
    for sy in (-1, 1):
        box('BellTower_Pier', (0.4, 0.4, 10.2), (bx + sx * 3.2, by + sy * 3.2, 0.5), material=STONE, base=True)
octa('BellTower_Floor', 3.6, 11.0, 11.6, (bx, by), STONE)
# 열린 종루: 팔각 기둥 8개
bz0, bz1 = 11.6, 16.6
for k in range(8):
    a = math.pi / 8 + k * math.pi / 4
    box('BellTower_Column', (0.55, 0.55, bz1 - bz0), (bx + 3.0 * math.cos(a), by + 3.0 * math.sin(a), bz0), (0, 0, a), BRICK, base=True)
cylinder('BellTower_Bell', 0.9, 1.4, (bx, by, bz1 - 2.3), material=GOLD, seg=24, r2=0.45)
octa('BellTower_Entablature', 3.55, bz1, bz1 + 1.2, (bx, by), BRICK)
octa('BellTower_EntCornice', 3.75, bz1 + 1.0, bz1 + 1.35, (bx, by), STONE)
tent('BellTower_Tent', 3.4, 0.55, bz1 + 1.3, bz1 + 9.0, (bx, by, 0), [ROOF], 2, 10)
for k in range(8):
    a = math.pi / 8 + k * math.pi / 4
    curve_tube('BellTower_TentRib', [(bx + 3.48 * math.cos(a), by + 3.48 * math.sin(a), bz1 + 1.3), (bx + 0.6 * math.cos(a), by + 0.6 * math.sin(a), bz1 + 9.0)], 0.07, material=STONE)
    w = window_mesh(0.28, 0.75)
    place_on_face(w, 'BellTower_Dormer', bx, by, k * math.pi / 4, 2.55, bz1 + 3.6)
for k in range(4):
    a = k * math.pi / 2
    for zz in (2.0, 6.6):
        w = window_mesh(0.8, 1.9)
        place_on_face(w, 'BellTower_BaseWindow', bx, by, a, 3.22, zz)
cylinder('BellTower_Lantern', 0.55, 0.9, (bx, by, bz1 + 8.9), material=STONE, seg=16)
onion('BellTower_Dome', 0.8, 1.6, (bx, by, bz1 + 9.75), [GOLD], 'plain', seg=32)
put_cross('BellTower_Cross', bx, by, bz1 + 9.75 + 1.5, 0.55)

# ---------------------------------------------------------------- 지붕 덮인 계단 현관 2곳(남쪽 정면)
for sgn in (-1, 1):
    xc = sgn * 8.6
    pre = 'Porch_' + ('W' if sgn < 0 else 'E')
    # 계단(톱니 형태)
    steps = []
    nst = 14
    for s in range(nst):
        h = PLAT_TOP * (s + 1) / nst
        y = -20.6 + s * (7.2 / nst)
        steps.append(box(pre + '_Step', (2.6, 7.2 / nst + 0.01, h), (xc, y + 3.6 / nst, 0), material=STEP, base=True))
    steps.append(box(pre + '_Landing', (2.6, 1.0, PLAT_TOP), (xc, -13.3, 0), material=STEP, base=True))
    join(steps, pre + '_Stair')
    # 바깥 난간벽
    extrude_profile(pre + '_StairWall', [(-20.6, 0), (-12.85, 0), (-12.85, PLAT_TOP + 1.2), (-20.6, 1.4)], 0.4,
                    loc=(0, 0, 0), material=BRICK, axis='X').location = (xc + sgn * 1.5, 0, 0)
    # 계단 위 경사 지붕(기둥 + 판)
    for yy in (-20.2, -17.6, -15.0):
        zt = 1.0 + (yy + 20.6) / 7.2 * PLAT_TOP
        box(pre + '_Post', (0.3, 0.3, 3.4), (xc - sgn * 1.15, yy, zt - 0.6), material=BRICK, base=True)
    a = math.atan2(PLAT_TOP, 7.2)
    box(pre + '_Roof', (3.4, 7.9, 0.25), (xc, -17.0, 0.6 + 3.6 / 7.2 * PLAT_TOP + 3.1), rot=(a, 0, 0), material=ROOF)
    # 아래쪽 현관 탑(천막 지붕)
    box(pre + '_PorchBase', (3.6, 3.6, 3.6), (xc, -22.6, 0), material=BRICK, base=True)
    box(pre + '_PorchCornice', (3.9, 3.9, 0.3), (xc, -22.6, 3.4), material=STONE, base=True)
    w = window_mesh(0.9, 1.8)
    place_on_face(w, pre + '_Arch', xc, -22.6, -math.pi / 2, 1.82, 0.4)
    tent(pre + '_Tent', 2.0, 0.25, 3.7, 8.6, (xc, -22.6, 0), [ROOF], 2, 8)
    onion(pre + '_Dome', 0.45, 0.9, (xc, -22.6, 8.5), [GOLD], 'plain', seg=24)
    put_cross(pre + '_Cross', xc, -22.6, 9.3, 0.35)
