# 첨성대 (경주, 신라 7세기) — 코드로만 쌓은 화강암 27단 석탑
#
# - 몸통: 아래 지름 약 4.93m, 위 지름 약 2.85m 의 병 모양 곡선 프로필을 수식으로 정하고,
#   27개 단마다 둥근 원호 모양 석재를 bmesh 로 한 개씩 깎아(모서리 모따기·면 볼록) 엇갈려 쌓았다.
# - 남쪽 13~15단에 네모난 창구를 비우고, 안쪽은 12단 높이까지 흙으로 채웠다.
# - 19~20단, 25~26단에는 안을 가로지르는 장대석의 끝이 바깥으로 튀어나온다.
# - 꼭대기에는 장대석 넷을 井 자로 맞물린 정자석 2단과 동쪽 절반을 덮은 판석을 얹었다.
# - 아래에는 네모난 2단 기단을 판석들로 둘렀다. 석재 색은 세 가지 화강암 톤을 섞었다.
from atelier import *
import random

random.seed(31)

SHOT = {"azimuth": -76, "elevation": 13, "lens": 60, "light": "day", "background": "#2a3442"}

# ---------------------------------------------------------------- 재질
GRANITE = [
    mat('Granite_Light', '#a39a88', rough=0.88),
    mat('Granite_Warm', '#928a7a', rough=0.9),
    mat('Granite_Grey', '#8b8579', rough=0.9),
    mat('Granite_Weathered', '#76716a', rough=0.95),
]
M_BASE = mat('Granite_Plinth', '#8e897e', rough=0.92)
M_CROWN = mat('Granite_Crown', '#938b7c', rough=0.88)
M_SOIL = mat('Packed_Earth', '#5a4936', rough=1.0)

# ---------------------------------------------------------------- 치수
BASE_LOW = (5.36, 0.20)     # 하층 기단 한 변, 높이
BASE_UP = (5.10, 0.19)      # 상층 기단
Z0 = BASE_LOW[1] + BASE_UP[1]
N_COURSE = 27
CH = 0.30                   # 한 단 높이
R_BOT, R_TOP = 2.465, 1.425
TOP_Z = Z0 + N_COURSE * CH
WIN_COURSES = (12, 13, 14)  # 0부터 센 13~15단
WIN_HALF = 0.48             # 창구 반폭
WIN_ANGLE = -math.pi / 2    # 남쪽(-Y)


def r_out(z):
    """몸통 바깥 반지름: 아래가 넓고 오목하게 좁아지다 위에서 살짝 벌어지는 병 모양."""
    t = max(0.0, min(1.0, (z - Z0) / (N_COURSE * CH)))
    return R_TOP + (R_BOT - R_TOP) * (1 - t) ** 1.95 + 0.04 * t ** 8


def wall(z):
    t = (z - Z0) / (N_COURSE * CH)
    return 0.46 - 0.13 * t


def r_in(z):
    return r_out(z) - wall(z)


# ---------------------------------------------------------------- 몸통 석재
def add_block(bm, a0, a1, z0, z1, mi, c=0.02):
    """원호 석재 하나: 바깥면은 모따기 + 살짝 볼록, 안쪽 면·옆면으로 닫힌 덩어리."""
    rm = (r_out(z0) + r_out(z1)) / 2
    da = c / rm
    n_mid = max(2, int((a1 - a0) * rm / 0.13))
    angs = [a0, a0 + da] + [a0 + da + (a1 - a0 - 2 * da) * i / n_mid for i in range(1, n_mid)] + [a1 - da, a1]
    zs = [z0, z0 + c, (z0 + z1) / 2, z1 - c, z1]
    bulge = random.uniform(0.006, 0.022)
    push = random.uniform(-0.012, 0.016)
    na, nz = len(angs), len(zs)

    outer = []
    for j, z in enumerate(zs):
        row = []
        for i, a in enumerate(angs):
            edge = i in (0, na - 1) or j in (0, nz - 1)
            r = r_out(z) + push
            if edge:
                r -= c
            else:
                u = (a - a0) / (a1 - a0)
                v = (z - z0) / (z1 - z0)
                r += bulge * math.sin(math.pi * u) * math.sin(math.pi * v)
            row.append(bm.verts.new((r * math.cos(a), r * math.sin(a), z)))
        outer.append(row)
    inner = []
    for z in (z0, z1):
        ri = r_in(z)
        inner.append([bm.verts.new((ri * math.cos(a), ri * math.sin(a), z)) for a in angs])

    faces = []
    for j in range(nz - 1):
        for i in range(na - 1):
            f = bm.faces.new((outer[j][i], outer[j][i + 1], outer[j + 1][i + 1], outer[j + 1][i]))
            f.smooth = 0 < i < na - 2 and 0 < j < nz - 2
            faces.append(f)
    for i in range(na - 1):
        faces.append(bm.faces.new((inner[0][i + 1], inner[0][i], inner[1][i], inner[1][i + 1])))
        faces.append(bm.faces.new((outer[0][i + 1], outer[0][i], inner[0][i], inner[0][i + 1])))
        faces.append(bm.faces.new((outer[-1][i], outer[-1][i + 1], inner[1][i + 1], inner[1][i])))
    left = [outer[j][0] for j in range(nz)] + [inner[1][0], inner[0][0]]
    right = [outer[j][-1] for j in range(nz)] + [inner[1][-1], inner[0][-1]]
    faces.append(bm.faces.new(list(reversed(left))))
    faces.append(bm.faces.new(right))
    for f in faces:
        f.material_index = mi


def course_cuts(k, r):
    """k 단의 석재 경계 각도 목록(엇갈림 + 약간의 폭 변화)."""
    n = max(12, round(2 * math.pi * r / 0.7))
    step = 2 * math.pi / n
    start = (k % 2) * 0.5 * step + random.uniform(-0.1, 0.1) * step
    cuts = [start + i * step + (random.uniform(-0.28, 0.28) * step if i else 0) for i in range(n)]
    return cuts, step


def build_course(k):
    z0, z1 = Z0 + k * CH, Z0 + (k + 1) * CH
    rm = (r_out(z0) + r_out(z1)) / 2
    cuts, step = course_cuts(k, rm)
    spans = [(cuts[i], cuts[(i + 1) % len(cuts)] + (2 * math.pi if i == len(cuts) - 1 else 0)) for i in range(len(cuts))]
    if k in WIN_COURSES:
        rw = r_out(Z0 + 13.5 * CH)
        ha = math.asin(WIN_HALF / rw)
        lo, hi = WIN_ANGLE - ha, WIN_ANGLE + ha
        # 창구 양옆에 경계를 강제하고 그 사이는 비운다
        lo2 = lo + 2 * math.pi * math.floor((spans[0][0] - lo) / (2 * math.pi) + 1)
        hi2 = lo2 + 2 * ha
        new = []
        a = hi2
        end = lo2 + 2 * math.pi
        n = max(1, round((end - a) / step))
        for i in range(n):
            new.append((a + (end - a) * i / n, a + (end - a) * (i + 1) / n))
        spans = new
    bm = bmesh.new()
    for a0, a1 in spans:
        add_block(bm, a0, a1, z0, z1, random.choice((0, 0, 0, 1, 1, 1, 2, 2, 3)))
    me = bpy.data.meshes.new(f'Course_{k + 1:02d}')
    bm.to_mesh(me)
    bm.free()
    ob = bpy.data.objects.new(f'Course_{k + 1:02d}', me)
    link(ob)
    for m in GRANITE:
        me.materials.append(m)
    return ob


courses = [build_course(k) for k in range(N_COURSE)]

# ---------------------------------------------------------------- 내부 흙 채움(12단까지)
z_fill = Z0 + 12 * CH
fill = cylinder('Interior_Earth_Fill', r=r_in(z_fill) + 0.03, h=0.5, loc=(0, 0, z_fill - 0.5), seg=48, material=M_SOIL)

# 창구 아래턱 판석
sill_r = r_out(z_fill)
sill = box('Window_Sill_Stone', (2 * WIN_HALF + 0.02, wall(z_fill) + 0.04, 0.05),
           loc=(0, -(sill_r - wall(z_fill) / 2 + 0.01), z_fill - 0.025 + 0.0), material=M_CROWN)
bevel(sill, 0.01, 1)
sill.location.z = z_fill - 0.024

# ---------------------------------------------------------------- 장대석(끝이 밖으로 나옴)
beam_proto = {}


def cross_beam(name, k, axis, off):
    zc = Z0 + (k + 0.5) * CH
    L = 2 * (max(r_out(zc - CH / 2), r_out(zc + CH / 2)) + 0.11)
    size = (L, 0.30, CH - 0.012) if axis == 'X' else (0.30, L, CH - 0.012)
    loc = (0, off, zc) if axis == 'X' else (off, 0, zc)
    b = box(name, size, loc=loc, material=M_CROWN)
    bevel(b, 0.02, 2)
    return b


for k, axis in ((18, 'X'), (19, 'Y'), (24, 'X'), (25, 'Y')):
    for s, off in (('A', -0.55), ('B', 0.55)):
        cross_beam(f'Cross_Beam_C{k + 1}_{axis}{s}', k, axis, off)

# ---------------------------------------------------------------- 정자석 2단 + 판석
CW, CHH, CL = 0.32, 0.33, 3.10
CA = 1.22


def crown_tier(t):
    z = TOP_Z + t * CHH
    parts = []
    for s in (-1, 1):
        b = box(f'Jeongjaseok_T{t + 1}_EW_{"S" if s < 0 else "N"}', (CL, CW, CHH), loc=(0, s * CA, z), material=M_CROWN, base=True)
        parts.append(b)
    # 남북 방향 장대석은 동서 장대석 사이사이로 나눠 맞물린 것처럼 만든다
    segs = [(-CL / 2, -CA - CW / 2), (-CA + CW / 2, CA - CW / 2), (CA + CW / 2, CL / 2)]
    for s in (-1, 1):
        pieces = []
        for y0, y1 in segs:
            pieces.append(box('tmp', (CW, y1 - y0, CHH), loc=(s * CA, (y0 + y1) / 2, z), material=M_CROWN, base=True))
        parts.append(join(pieces, f'Jeongjaseok_T{t + 1}_NS_{"W" if s < 0 else "E"}'))
    for p in parts:
        bevel(p, 0.022, 2)
    return parts


crown = crown_tier(0) + crown_tier(1)
inner_edge = CA - CW / 2
slab = box('Top_Half_Cover_Slab', (inner_edge - 0.02, 2 * inner_edge - 0.01, 0.16),
           loc=((inner_edge + 0.02) / 2 + 0.0, 0, TOP_Z + 0.003), material=M_CROWN, base=True)
bevel(slab, 0.015, 1)

# ---------------------------------------------------------------- 2단 기단
def plinth_tier(name, side, h, z, depth, n_seg):
    out = []
    hs = side / 2
    idx = 0
    # 남북 긴 변(전체 길이), 동서 변(안쪽 길이)
    for s in (-1, 1):
        cuts = sorted([-hs, hs] + [-hs + side * (i + random.uniform(-0.25, 0.25)) / n_seg for i in range(1, n_seg)])
        for a, b in zip(cuts, cuts[1:]):
            idx += 1
            out.append(box(f'{name}_Slab_{idx:02d}', (b - a, depth, h), loc=((a + b) / 2, s * (hs - depth / 2), z), material=M_BASE, base=True))
    for s in (-1, 1):
        inner = hs - depth
        cuts = sorted([-inner, inner] + [-inner + 2 * inner * (i + random.uniform(-0.2, 0.2)) / (n_seg - 1) for i in range(1, n_seg - 1)])
        for a, b in zip(cuts, cuts[1:]):
            idx += 1
            out.append(box(f'{name}_Slab_{idx:02d}', (depth, b - a, h), loc=(s * (hs - depth / 2), (a + b) / 2, z), material=M_BASE, base=True))
    core = box(f'{name}_Core', (side - 2 * depth + 0.002, side - 2 * depth + 0.002, h - 0.01), loc=(0, 0, z), material=M_BASE, base=True)
    for o in out:
        bevel(o, 0.018, 2)
    return out + [core]


plinth_tier('Plinth_Lower', BASE_LOW[0], BASE_LOW[1], 0.0, 1.05, 4)
plinth_tier('Plinth_Upper', BASE_UP[0], BASE_UP[1], BASE_LOW[1], 0.95, 4)
