# 숭례문(서울 남대문) — 석축 홍예문 위에 올린 정면 5칸·측면 2칸, 2층 우진각 지붕 문루.
# 석축은 켜마다 조금씩 들여 쌓은 화강암 블록을 난수(고정 시드)로 나눠 줄눈을 만들고, 불리언으로 홍예 통로를 뚫은 뒤
# 홍예석·측벽석을 따로 쌓았다. 지붕은 '정사각 둘레 → 직사각 고리' 매개화로 우진각 면을 만들고
# 처마 곡선(앙곡·안허리)을 귀 쪽으로 갈수록 들리게 했으며, 그 위에 수키와 줄을 한 줄씩 쓸어(sweep) 얹었다.
# 공포(다포)는 한 세트 메시를 연결 복제로 둘레에 배열했고, 기둥·창방·평방·단청 띠·난간·현판은 상자 조합으로 만들었다.
# 실측 도면이 아니라 사진 비례에 맞춘 근사 모델이다(단위 m).
from atelier import *
import random

random.seed(1398)

SHOT = {"azimuth": -56, "elevation": 8, "lens": 50, "light": "day", "background": "#4b5d72"}

# ---------------------------------------------------------------- 재질
GRANITE = mat('화강암', '#a19b8f', 0.85)
GRANITE2 = mat('화강암_짙은돌', '#8b867c', 0.9)
BRICK = mat('여장_회색전돌', '#7d7a74', 0.9)
TILE = mat('기와_회흑색', '#3b3e44', 0.72)
MORTAR = mat('양성바름_백회', '#e2ddd1', 0.85)
COL = mat('석간주_기둥', '#7a2e24', 0.6)
GREEN = mat('단청_뇌록', '#3f8a72', 0.6)
BLUE = mat('단청_군청', '#2d5b8e', 0.55)
RED = mat('단청_주홍', '#b5402d', 0.55)
OCHRE = mat('단청_황토', '#d5aa52', 0.55)
WOOD = mat('마루_목재', '#76583c', 0.7)
PLAQUE = mat('현판_바탕', '#22252d', 0.5)
GOLD = mat('현판_금박', '#c9a347', 0.35, 0.6)


class MB:
    """여러 상자·스윕을 한 메시로 모으는 작은 빌더(재질 슬롯 자동)."""

    def __init__(self):
        self.v, self.f, self.m, self.slots = [], [], [], []

    def _mi(self, m):
        if m not in self.slots:
            self.slots.append(m)
        return self.slots.index(m)

    def face(self, idx, m):
        self.f.append(list(idx))
        self.m.append(self._mi(m))

    def obox(self, c, ax, ay, az, m):
        b = len(self.v)
        for k in range(8):
            sx = 1 if k & 1 else -1
            sy = 1 if k & 2 else -1
            sz = 1 if k & 4 else -1
            self.v.append(c + ax * sx + ay * sy + az * sz)
        for q in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)):
            self.face([b + i for i in q], m)

    def box(self, mn, mx, m):
        c = Vector(((mn[0] + mx[0]) / 2, (mn[1] + mx[1]) / 2, (mn[2] + mx[2]) / 2))
        self.obox(c, Vector(((mx[0] - mn[0]) / 2, 0, 0)), Vector((0, (mx[1] - mn[1]) / 2, 0)),
                  Vector((0, 0, (mx[2] - mn[2]) / 2)), m)

    def prism_y(self, pts, y0, y1, m):
        """XZ 다각형을 y0~y1 로 돌출."""
        b = len(self.v)
        n = len(pts)
        for x, z in pts:
            self.v.append(Vector((x, y0, z)))
        for x, z in pts:
            self.v.append(Vector((x, y1, z)))
        self.face([b + i for i in range(n)], m)
        self.face([b + n + i for i in reversed(range(n))], m)
        for i in range(n):
            j = (i + 1) % n
            self.face([b + i, b + j, b + n + j, b + n + i], m)

    def sweep(self, pts, prof, m, mcap0=None, mcap1=None):
        """점 목록을 따라 닫힌 단면 prof=[(가로, 법선)] 을 쓸어 만든다."""
        rings = []
        n = len(pts)
        for i, p in enumerate(pts):
            t = (pts[min(i + 1, n - 1)] - pts[max(i - 1, 0)]).normalized()
            lat = t.cross(Vector((0, 0, 1)))
            if lat.length < 1e-6:
                lat = Vector((1, 0, 0))
            lat.normalize()
            nor = lat.cross(t).normalized()
            if nor.z < 0:
                nor = -nor
            rings.append(len(self.v))
            for a, b in prof:
                self.v.append(p + lat * a + nor * b)
        k = len(prof)
        for i in range(n - 1):
            for j in range(k):
                j2 = (j + 1) % k
                self.face([rings[i] + j, rings[i] + j2, rings[i + 1] + j2, rings[i + 1] + j], m)
        self.face([rings[0] + j for j in reversed(range(k))], mcap0 or m)
        self.face([rings[-1] + j for j in range(k)], mcap1 or m)

    def build(self, name, recalc=True):
        me = bpy.data.meshes.new(name)
        me.from_pydata([tuple(v) for v in self.v], [], self.f)
        for s in self.slots:
            me.materials.append(s)
        me.polygons.foreach_set('material_index', self.m)
        me.update()
        if recalc:
            bm = bmesh.new()
            bm.from_mesh(me)
            bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
            bm.to_mesh(me)
            bm.free()
        ob = bpy.data.objects.new(name, me)
        link(ob)
        return ob


def ring_beams(mb, Wx, Wy, off, hw, z0, z1, m):
    """기둥선(Wx,Wy)에서 off 만큼 바깥에 도는 사각 띠(앞뒤 통짜, 옆면은 맞닿게)."""
    ox, oy = Wx + off, Wy + off
    for s in (-1, 1):
        mb.box((-(ox + hw), s * oy - hw, z0), (ox + hw, s * oy + hw, z1), m)
        mb.box((s * ox - hw, -(oy - hw), z0), (s * ox + hw, oy - hw, z1), m)


# ---------------------------------------------------------------- 석축(石築)과 홍예문
W, D, H = 30.0, 15.0, 8.0          # 석축 밑변 너비·깊이·높이
NC, BATTER = 11, 0.04               # 켜 수, 켜마다 들여쌓기
Z_PL, Z_MI = 0.35, H - 0.32         # 지대석 윗면, 미석 아랫면
CH = (Z_MI - Z_PL) / NC
R_IN, R_OUT, ZS = 2.75, 3.45, 3.95   # 홍예 안 반지름, 홍예석 바깥 반지름, 홍예 기점 높이
G = 0.022                            # 줄눈


def half_at(k):
    return W / 2 - k * BATTER, D / 2 - k * BATTER


def build_platform():
    blocks = MB()
    blocks.box((-W / 2 - 0.15, -D / 2 - 0.15, 0), (W / 2 + 0.15, D / 2 + 0.15, Z_PL), GRANITE2)
    for k in range(NC):
        hx, hy = half_at(k)
        z0, z1 = Z_PL + k * CH, Z_PL + (k + 1) * CH
        for s in (-1, 1):
            # 앞·뒷면 블록
            x = -hx
            first = True
            while x < hx - 0.05:
                ln = random.uniform(0.9, 2.1) * (0.6 if first and k % 2 else 1.0)
                first = False
                x1 = min(hx, x + ln)
                if hx - x1 < 0.6:
                    x1 = hx
                pr = random.uniform(0, 0.025)
                m = GRANITE if random.random() < 0.7 else GRANITE2
                if s < 0:
                    blocks.box((x + G, -hy - pr, z0 + G), (x1 - G, -hy + 1.0, z1 - G), m)
                else:
                    blocks.box((x + G, hy - 1.0, z0 + G), (x1 - G, hy + pr, z1 - G), m)
                x = x1
            # 옆면 블록
            y = -hy + 1.0
            while y < hy - 1.05:
                y1 = min(hy - 1.0, y + random.uniform(0.9, 2.0))
                if hy - 1.0 - y1 < 0.5:
                    y1 = hy - 1.0
                pr = random.uniform(0, 0.025)
                m = GRANITE if random.random() < 0.7 else GRANITE2
                if s < 0:
                    blocks.box((-hx - pr, y + G, z0 + G), (-hx + 1.0, y1 - G, z1 - G), m)
                else:
                    blocks.box((hx - 1.0, y + G, z0 + G), (hx + pr, y1 - G, z1 - G), m)
                y = y1
    ob_blocks = blocks.build('Seokchuk_Masonry')

    core = MB()
    core.box((-(W / 2 - 0.7), -(D / 2 - 0.7), 0.3), (W / 2 - 0.7, D / 2 - 0.7, H - 0.3), GRANITE2)
    ob_core = core.build('Seokchuk_Core')

    cut = MB()
    prof = [(-R_OUT, -1.0), (R_OUT, -1.0)]
    for i in range(25):
        a = math.pi * i / 24
        prof.append((R_OUT * math.cos(a), ZS + R_OUT * math.sin(a)))
    cut.prism_y(prof, -D, D, GRANITE)
    ob_cut = cut.build('Hongye_Cutter')
    for o in (ob_blocks, ob_core):
        boolean(o, ob_cut)
        apply_all(o)
    bpy.data.objects.remove(ob_cut, do_unlink=True)

    # 미석(眉石)과 윗면 박석
    hx, hy = half_at(NC)
    top = MB()
    top.box((-hx - 0.14, -hy - 0.14, Z_MI), (hx + 0.14, hy + 0.14, H), GRANITE)
    # 통로 바닥 박석
    top.box((-R_OUT - 0.05, -D / 2 - 0.15, 0), (R_OUT + 0.05, D / 2 + 0.15, 0.06), GRANITE2)
    # 홍예 측벽석(기점 아래)
    nj = 5
    for side in (-1, 1):
        for k in range(nj):
            z0, z1 = k * ZS / nj, (k + 1) * ZS / nj
            kk = (z0 + z1) / 2 / CH
            hyk = D / 2 - kk * BATTER
            y = -hyk - 0.02
            while y < hyk:
                y1 = min(hyk + 0.02, y + random.uniform(1.4, 2.2))
                if hyk - y1 < 0.6:
                    y1 = hyk + 0.02
                x_in, x_out = R_IN, R_OUT + 0.05
                xa, xb = (x_in, x_out) if side > 0 else (-x_out, -x_in)
                top.box((xa, y + G, max(z0, 0.06) + G), (xb, y1 - G, z1 - G),
                        GRANITE if random.random() < 0.6 else GRANITE2)
                y = y1
    # 홍예석(무지개돌) — 부채꼴 돌을 통로 깊이로 나눠 쌓음
    NV = 13
    for i in range(NV):
        a0 = math.pi * i / NV + 0.006
        a1 = math.pi * (i + 1) / NV - 0.006
        pts = []
        for j in range(3):
            a = a0 + (a1 - a0) * j / 2
            pts.append((R_IN * math.cos(a), ZS + R_IN * math.sin(a)))
        for j in range(3):
            a = a1 - (a1 - a0) * j / 2
            pts.append(((R_OUT + 0.05) * math.cos(a), ZS + (R_OUT + 0.05) * math.sin(a)))
        zm = ZS + (R_IN + R_OUT) / 2 * math.sin((a0 + a1) / 2)
        hyk = D / 2 - (zm - Z_PL) / CH * BATTER + 0.03
        segs = 7
        for s in range(segs):
            y0 = -hyk + 2 * hyk * s / segs + (0 if s == 0 else G)
            y1 = -hyk + 2 * hyk * (s + 1) / segs - (0 if s == segs - 1 else G)
            top.prism_y(pts, y0, y1, GRANITE if (i + s) % 3 else GRANITE2)
    ob_top = top.build('Seokchuk_Mison_Hongyeseok')

    # 성문 문짝(열린 상태로 측벽에 붙임)
    door = MB()
    for side in (-1, 1):
        xa = side * (R_IN - 0.02)
        xb = side * (R_IN - 0.16)
        door.box((min(xa, xb), 1.2, 0.08), (max(xa, xb), 3.9, ZS + 0.55), COL)
        for zz in (0.6, 1.6, 2.6, 3.4):  # 띠쇠
            door.box((min(xa, xb) - 0.015, 1.2 + 0.05, zz), (max(xa, xb) + 0.015, 3.9 - 0.05, zz + 0.12), GRANITE2)
    door.build('Gate_Doors')

    # 여장(女墻) — 석축 위 낮은 담과 기와 지붕돌
    par = MB()
    t, ph = 0.55, 1.05
    ex, ey = hx - 0.05, hy - 0.05
    segs = []
    n = 9
    for i in range(n):
        x0 = -ex + 2 * ex * i / n
        x1 = -ex + 2 * ex * (i + 1) / n
        for s in (-1, 1):
            segs.append(((x0 + (0.14 if i else 0), s * ey - t / 2), (x1 - (0.14 if i < n - 1 else 0), s * ey + t / 2)))
    n2 = 4
    for i in range(n2):
        y0 = -(ey - t / 2) + 2 * (ey - t / 2) * i / n2
        y1 = -(ey - t / 2) + 2 * (ey - t / 2) * (i + 1) / n2
        for s in (-1, 1):
            segs.append(((s * ex - t / 2, y0 + (0.14 if i else 0)), (s * ex + t / 2, y1 - (0.14 if i < n2 - 1 else 0))))
    for (xa, ya), (xb, yb) in segs:
        if xb - xa > yb - ya:
            xa, xb = xa - (t / 2 if xa < -ex + 0.01 else 0), xb + (t / 2 if xb > ex - 0.01 else 0)
        par.box((xa, ya, H), (xb, yb, H + ph), BRICK)
        par.box((xa - 0.08, ya - 0.08, H + ph), (xb + 0.08, yb + 0.08, H + ph + 0.12), TILE)
        # 총안(銃眼) 자리 — 작은 어두운 구멍판
        if xb - xa > yb - ya:
            for k in range(2):
                cx = xa + (xb - xa) * (k + 1) / 3
                par.box((cx - 0.12, ya - 0.02, H + 0.45), (cx + 0.12, yb + 0.02, H + 0.68), TILE)
    par.build('Yeojang_Parapet')
    return H


# ---------------------------------------------------------------- 지붕
def f_prof(t):
    return 0.5 * t + 0.5 * t * t


class Roof:
    def __init__(self, ax, ay, r, h, ze, lift, bulge, th=0.36):
        self.ax, self.ay, self.r, self.h, self.ze = ax, ay, r, h, ze
        self.lift, self.bulge, self.th = lift, bulge, th

    def X(self, t):
        return self.ax - (self.ax - self.r) * t

    def Y(self, t):
        return self.ay * (1 - t)

    def pt(self, u, v, t, dz=0.0):
        c = min(abs(u), abs(v)) ** 3
        w = max(0.0, 1 - t / 0.55) ** 2
        b = self.bulge * c * w
        return Vector((u * (self.X(t) + b), v * (self.Y(t) + b),
                       self.ze + self.h * f_prof(t) + self.lift * c * w + dz))


def build_roof(name, R, tmax, t_purlin, full, n_figs):
    Nf, Ne, K = 56, 28, 24
    per = []
    for i in range(Nf):
        per.append((-1 + 2 * i / Nf, -1))
    for i in range(Ne):
        per.append((1, -1 + 2 * i / Ne))
    for i in range(Nf):
        per.append((1 - 2 * i / Nf, 1))
    for i in range(Ne):
        per.append((-1, 1 - 2 * i / Ne))
    P = len(per)
    ts = [tmax * (k / K) ** 1.2 for k in range(K + 1)]
    sk = MB()
    bases = []
    for dz in (0.0, -R.th):
        bases.append(len(sk.v))
        for t in ts:
            for u, v in per:
                sk.v.append(R.pt(u, v, t, dz))
    tb, bb = bases
    for k in range(K):
        for i in range(P):
            a, b = k * P + i, k * P + (i + 1) % P
            sk.face([tb + a, tb + b, tb + b + P, tb + a + P], TILE)
            sk.face([bb + a + P, bb + b + P, bb + b, bb + a], COL)
    for i in range(P):
        j = (i + 1) % P
        sk.face([bb + i, bb + j, tb + j, tb + i], TILE)
        if not full:
            o = K * P
            sk.face([tb + o + i, tb + o + j, bb + o + j, bb + o + i], TILE)
    sk.build(name + '_Surface', recalc=False)

    # 수키와 줄
    tl = MB()
    pitch = 0.34
    hp = [(0.13 * math.cos(math.pi * i / 6), 0.11 * math.sin(math.pi * i / 6) - 0.02) for i in range(7)]
    span = R.ax - R.r
    for v in (-1, 1):
        x = -R.ax + pitch / 2
        while x < R.ax:
            lim = abs(x) / 0.975
            if lim < R.ax:
                te = min(tmax, (R.ax - lim) / span)
                if te > 0.015:
                    pts = []
                    for j in range(19):
                        t = te * j / 18
                        u = max(-0.975, min(0.975, x / R.X(t)))
                        pts.append(R.pt(u, v, t, 0.0))
                    tl.sweep(pts, hp, TILE)
            x += pitch
    for u in (-1, 1):
        y = -R.ay + pitch / 2
        while y < R.ay:
            te = min(tmax, 1 - abs(y) / (0.975 * R.ay))
            if te > 0.015:
                pts = []
                for j in range(13):
                    t = te * j / 12
                    v = max(-0.975, min(0.975, y / R.Y(t)))
                    pts.append(R.pt(u, v, t, 0.0))
                tl.sweep(pts, hp, TILE)
            y += pitch

    # 마루: 추녀마루(백회 양성 + 기와 덮개)
    hip_body = [(-0.27, -0.2), (0.27, -0.2), (0.27, 0.42), (-0.27, 0.42)]
    hip_cap = [(-0.32, 0.4), (0.32, 0.4), (0.32, 0.58), (-0.32, 0.58)]
    tend = 0.985 if full else tmax
    for su in (-1, 1):
        for sv in (-1, 1):
            pts = [R.pt(su, sv, -0.03 + (tend + 0.03) * j / 30) for j in range(31)]
            tl.sweep(pts, hip_body, MORTAR)
            tl.sweep(pts, hip_cap, TILE)
            # 잡상
            for q in range(n_figs):
                t = 0.1 + q * 0.03
                p = R.pt(su, sv, t) + Vector((0, 0, 0.62))
                s = 0.1 if q else 0.13
                tl.box((p.x - s, p.y - s, p.z - 0.05), (p.x + s, p.y + s, p.z + 0.22), TILE)
                tl.box((p.x - s * 0.7, p.y - s * 0.7, p.z + 0.22), (p.x + s * 0.7, p.y + s * 0.7, p.z + 0.36), TILE)
            # 추녀·사래(귀 서까래)
            pts = [R.pt(su, sv, -0.07 + (t_purlin + 0.07) * j / 14, -R.th - 0.2) for j in range(15)]
            tl.sweep(pts, [(-0.2, -0.22), (0.2, -0.22), (0.2, 0.2), (-0.2, 0.2)], GREEN, mcap0=OCHRE)
    if full:
        zt = R.ze + R.h * f_prof(0.985)
        rr = R.r + 0.25
        tl.box((-rr, -0.36, zt - 0.5), (rr, 0.36, zt + 0.55), MORTAR)
        tl.box((-rr - 0.05, -0.43, zt + 0.55), (rr + 0.05, 0.43, zt + 0.78), TILE)
        for s in (-1, 1):  # 취두
            x0 = s * (rr - 0.1)
            prof = [(0, -0.2), (0.85, -0.2), (0.95, 0.5), (1.15, 0.95), (1.0, 1.35), (0.55, 1.55), (0.15, 1.35), (0, 1.0)]
            pts = [(x0 + s * a, zt + b) for a, b in prof]
            if s < 0:
                pts.reverse()
            tl.prism_y(pts, -0.42, 0.42, TILE)
            tl.box((min(x0 + s * 0.2, x0 + s * 0.75), -0.47, zt + 0.25), (max(x0 + s * 0.2, x0 + s * 0.75), 0.47, zt + 0.4), MORTAR)
    tl.build(name + '_Tiles_Ridges')

    # 서까래(부연) — 처마 밑
    rf = MB()
    sq = [(-0.09, -0.09), (0.09, -0.09), (0.09, 0.09), (-0.09, 0.09)]
    rp = 0.42
    dz = -R.th - 0.085
    tr = t_purlin + 0.05
    for v in (-1, 1):
        x = -R.ax + rp / 2
        while x < R.ax:
            lim = abs(x) / 0.95
            if lim < R.ax:
                te = min(tr, (R.ax - lim) / span)
                if te > 0.01:
                    pts = []
                    for j in range(7):
                        t = -0.025 + (te + 0.025) * j / 6
                        u = max(-0.95, min(0.95, x / R.X(max(t, 0))))
                        pts.append(R.pt(u, v, t, dz))
                    rf.sweep(pts, sq, GREEN, mcap0=BLUE)
            x += rp
    for u in (-1, 1):
        y = -R.ay + rp / 2
        while y < R.ay:
            te = min(tr, 1 - abs(y) / (0.95 * R.ay))
            if te > 0.01:
                pts = []
                for j in range(7):
                    t = -0.025 + (te + 0.025) * j / 6
                    v = max(-0.95, min(0.95, y / R.Y(max(t, 0))))
                    pts.append(R.pt(u, v, t, dz))
                rf.sweep(pts, sq, GREEN, mcap0=BLUE)
            y += rp
    rf.build(name + '_Rafters')


# ---------------------------------------------------------------- 목조부
def column_mesh():
    return cylinder('Column_proto', r=0.32, h=1.0, seg=16, material=COL)


def make_bracket():
    """다포 공포 한 세트. 원점=벽선·공포대 바닥, 바깥 = -Y."""
    b = MB()
    b.box((-0.27, -0.27, 0.0), (0.27, 0.27, 0.27), GREEN)                 # 주두
    b.box((-0.45, -0.13, 0.27), (0.45, 0.13, 0.45), BLUE)                 # 첨차 1
    b.box((-0.13, -0.8, 0.27), (0.13, 0.35, 0.45), GREEN)                 # 살미 1
    b.box((-0.13, -0.8, 0.27), (0.13, -0.62, 0.45), RED)
    for x, y in ((-0.38, 0), (0.38, 0), (0, -0.62), (0, 0)):
        b.box((x - 0.1, y - 0.1, 0.45), (x + 0.1, y + 0.1, 0.52), OCHRE)  # 소로
    b.box((-0.58, -0.13, 0.52), (0.58, 0.13, 0.7), BLUE)                  # 첨차 2
    b.box((-0.58, -0.75, 0.52), (0.58, -0.49, 0.7), BLUE)
    b.box((-0.13, -1.15, 0.52), (0.13, 0.35, 0.7), GREEN)                 # 살미 2
    b.box((-0.135, -1.15, 0.52), (0.135, -0.97, 0.7), RED)
    for x, y in ((-0.5, 0), (0.5, 0), (-0.5, -0.62), (0.5, -0.62), (0, -1.0)):
        b.box((x - 0.1, y - 0.1, 0.7), (x + 0.1, y + 0.1, 0.77), OCHRE)
    b.box((-0.13, -1.5, 0.77), (0.13, 0.35, 0.92), GREEN)                 # 살미 3(쇠서)
    b.box((-0.135, -1.5, 0.77), (0.135, -1.32, 0.92), RED)
    return b.build('Gongpo_Dapo_Set')


BRACKET = None


def inst(proto, loc, rot=(0, 0, 0), scale=(1, 1, 1), name=None):
    """숨긴 원형 메시를 공유하는 보이는 복제본."""
    ob = duplicate(proto, loc=loc, rot=rot, scale=scale, name=name)
    ob.hide_render = False
    ob.hide_viewport = False
    if 'atelier_cutter' in ob:
        del ob['atelier_cutter']
    return ob


def floor_frame(name, xs, ys, z_col_top, gongpo_skip=()):
    """창방·평방·포벽·장혀·외목도리 띠와 단청 머리초, 공포 배열. 공포대 윗면 z 반환."""
    Wx, Wy = xs[-1], ys[-1]
    fr = MB()
    zc = z_col_top
    zb = zc + 0.22
    ring_beams(fr, Wx, Wy, 0, 0.19, zc - 0.5, zc, GREEN)          # 창방
    ring_beams(fr, Wx, Wy, 0, 0.29, zc, zb, GREEN)                # 평방
    ring_beams(fr, Wx, Wy, 0, 0.06, zb, zb + 0.74, RED)           # 포벽
    ring_beams(fr, Wx, Wy, 0, 0.2, zb + 0.74, zb + 1.06, GREEN)   # 장혀
    ring_beams(fr, Wx, Wy, 1.25, 0.14, zb + 0.9, zb + 1.13, GREEN)  # 외목도리
    # 머리초(창방·평방 기둥머리 단청)
    for (cx, cy) in [(x, s * Wy) for x in xs for s in (-1, 1)]:
        fr.box((cx - 0.6, cy - 0.205, zc - 0.48), (cx + 0.6, cy + 0.205, zc - 0.02), BLUE)
        fr.box((cx - 0.22, cy - 0.215, zc - 0.47), (cx + 0.22, cy + 0.215, zc - 0.03), RED)
        fr.box((cx - 0.5, cy - 0.3, zc + 0.02), (cx + 0.5, cy + 0.3, zb - 0.02), OCHRE)
    for (cx, cy) in [(s * Wx, y) for y in ys[1:-1] for s in (-1, 1)]:
        fr.box((cx - 0.205, cy - 0.6, zc - 0.48), (cx + 0.205, cy + 0.6, zc - 0.02), BLUE)
        fr.box((cx - 0.215, cy - 0.22, zc - 0.47), (cx + 0.215, cy + 0.22, zc - 0.03), RED)
        fr.box((cx - 0.3, cy - 0.5, zc + 0.02), (cx + 0.3, cy + 0.5, zb - 0.02), OCHRE)
    fr.build(name + '_Beams_Dancheong')
    # 공포 배열
    n = max(2, round(2 * Wx / 1.3))
    for i in range(n + 1):
        x = -Wx + 2 * Wx * i / n
        for s in (-1, 1):
            if s < 0 and abs(x) < 0.9 and 'front_center' in gongpo_skip:
                continue
            inst(BRACKET, loc=(x, s * Wy, zb), rot=(0, 0, 0 if s < 0 else math.pi), name=name + '_Gongpo')
    m = max(2, round(2 * Wy / 1.3))
    for i in range(1, m):
        y = -Wy + 2 * Wy * i / m
        for s in (-1, 1):
            inst(BRACKET, loc=(s * Wx, y, zb), rot=(0, 0, s * math.pi / 2), name=name + '_Gongpo')
    return zb + 1.13


def main():
    global BRACKET
    PT = build_platform()

    # 하층·상층 기둥 격자
    XL = [-11.0, -7.0, -2.6, 2.6, 7.0, 11.0]
    YL = [-5.0, 0.0, 5.0]
    XU = [-9.4, -6.0, -2.2, 2.2, 6.0, 9.4]
    YU = [-3.6, 0.0, 3.6]
    ZB = PT + 0.6                  # 기단 윗면
    ZC1 = ZB + 3.9                 # 하층 기둥머리

    # 기단
    gd = MB()
    gd.box((-12.3, -6.3, PT), (12.3, 6.3, ZB), GRANITE)
    gd.box((-12.15, -6.15, ZB), (12.15, 6.15, ZB + 0.02), GRANITE2)
    for x in XL:
        for y in YL:
            if abs(y) > 0.1 or abs(x) > 10:
                gd.box((x - 0.48, y - 0.48, ZB + 0.02), (x + 0.48, y + 0.48, ZB + 0.3), GRANITE)
    gd.build('Gidan_Base_Jucho')

    BRACKET = make_bracket()
    BRACKET.location = (0, 0, -100)
    BRACKET.hide_render = True
    BRACKET.hide_viewport = True
    BRACKET['atelier_cutter'] = True

    colp = column_mesh()
    colp.hide_render = True
    colp.hide_viewport = True
    colp['atelier_cutter'] = True
    # 하층 평주
    for x in XL:
        for y in YL:
            if abs(y) > 0.1 or abs(x) > 10:
                inst(colp, loc=(x, y, ZB + 0.3), scale=(1, 1, ZC1 - ZB - 0.3), name='Column_Lower')
    zbt1 = floor_frame('Lower', XL, YL, ZC1)

    # 하층 지붕: 상층 마루 둘레(9.9 x 4.1)에서 잘린 우진각 처마
    ax1, ay1 = 11.0 + 3.3, 5.0 + 3.2
    tc = 1 - (YU[-1] + 0.5) / ay1
    r1 = ax1 - (ax1 - (XU[-1] + 0.5)) / tc
    h1 = 6.0
    tp1 = 1 - (YL[-1] + 1.25) / ay1
    ze1 = zbt1 + 0.36 + 0.17 - h1 * f_prof(tp1)
    R1 = Roof(ax1, ay1, r1, h1, ze1, lift=0.75, bulge=0.55)
    build_roof('Roof_Lower', R1, tc, tp1, full=False, n_figs=5)
    zdeck = ze1 + h1 * f_prof(tc) + 0.03
    ZU = zdeck + 0.3
    ZC2 = ZU + 2.9

    # 상층 마루·고주(하층 바닥부터 올라가는 내진 기둥)
    dk = MB()
    DX, DY = XU[-1] + 0.95, YU[-1] + 0.95
    dk.box((-DX, -DY, zdeck), (DX, DY, ZU), WOOD)
    dk.box((-DX - 0.03, -DY - 0.03, zdeck + 0.02), (DX + 0.03, DY + 0.03, zdeck + 0.2), GREEN)
    # 계자난간
    hr = 0.95
    rail_pts = []
    nx, ny = 16, 6
    for i in range(nx + 1):
        for s in (-1, 1):
            rail_pts.append((-DX + 0.1 + 2 * (DX - 0.1) * i / nx, s * (DY - 0.1)))
    for i in range(1, ny):
        for s in (-1, 1):
            rail_pts.append((s * (DX - 0.1), -(DY - 0.1) + 2 * (DY - 0.1) * i / ny))
    for x, y in rail_pts:
        dk.box((x - 0.07, y - 0.07, ZU), (x + 0.07, y + 0.07, ZU + hr + 0.08), COL)
    ox, oy = DX - 0.1, DY - 0.1
    for z0, z1, hw, m in ((ZU + 0.05, ZU + 0.14, 0.06, COL), (ZU + 0.5, ZU + 0.58, 0.06, COL),
                          (ZU + hr - 0.06, ZU + hr + 0.04, 0.075, COL), (ZU + 0.14, ZU + 0.5, 0.025, GREEN)):
        for s in (-1, 1):
            dk.box((-ox - hw, s * oy - hw, z0), (ox + hw, s * oy + hw, z1), m)
            dk.box((s * ox - hw, -oy + hw, z0), (s * ox + hw, oy - hw, z1), m)
    dk.build('Upper_Floor_Deck_Railing')

    for x in XU:
        for y in YU:
            if abs(y) > 0.1 or abs(x) > 9:
                inst(colp, loc=(x, y, ZB + 0.3), scale=(0.95, 0.95, ZC2 - ZB - 0.3), name='Column_Upper_Gozu')

    # 상층 판벽
    wl = MB()
    Wx2, Wy2 = XU[-1], YU[-1]
    for s in (-1, 1):
        for i in range(len(XU) - 1):
            x0, x1 = XU[i] + 0.2, XU[i + 1] - 0.2
            wl.box((x0, s * Wy2 - 0.08, ZU), (x1, s * Wy2 + 0.08, ZC2 - 0.5), COL)
            wl.box((x0, s * Wy2 - 0.12, ZU), (x1, s * Wy2 + 0.12, ZU + 0.35), GREEN)          # 하인방
            wl.box((x0, s * Wy2 - 0.12, ZU + 1.55), (x1, s * Wy2 + 0.12, ZU + 1.75), GREEN)   # 중인방
            # 판문 띠장
            nb = 3
            for k in range(1, nb + 1):
                xx = x0 + (x1 - x0) * k / (nb + 1)
                wl.box((xx - 0.05, s * Wy2 - 0.1, ZU + 0.35), (xx + 0.05, s * Wy2 + 0.1, ZU + 1.55), GREEN)
            # 상부 창살(교창)
            xx = x0 + 0.15
            while xx < x1 - 0.15:
                wl.box((xx - 0.025, s * Wy2 - 0.11, ZU + 1.75), (xx + 0.025, s * Wy2 + 0.11, ZC2 - 0.5), WOOD)
                xx += 0.22
        for i in range(len(YU) - 1):
            y0, y1 = YU[i] + 0.2, YU[i + 1] - 0.2
            wl.box((s * Wx2 - 0.08, y0, ZU), (s * Wx2 + 0.08, y1, ZC2 - 0.5), COL)
            wl.box((s * Wx2 - 0.12, y0, ZU), (s * Wx2 + 0.12, y1, ZU + 0.35), GREEN)
            wl.box((s * Wx2 - 0.12, y0, ZU + 1.55), (s * Wx2 + 0.12, y1, ZU + 1.75), GREEN)
    wl.build('Upper_Panel_Walls')

    zbt2 = floor_frame('Upper', XU, YU, ZC2, gongpo_skip=('front_center',))

    # 상층 우진각 지붕
    ax2, ay2 = Wx2 + 3.2, Wy2 + 3.2
    h2 = 5.6
    tp2 = 1 - (Wy2 + 1.25) / ay2
    ze2 = zbt2 + 0.36 + 0.17 - h2 * f_prof(tp2)
    R2 = Roof(ax2, ay2, ax2 - ay2 * 1.02, h2, ze2, lift=0.95, bulge=0.65)
    build_roof('Roof_Upper', R2, 0.985, tp2, full=True, n_figs=7)

    # 현판(세로로 건 '崇禮門' 현판 — 글자는 획 덩어리로 암시)
    pq = MB()
    pw, ph_ = 0.72, 1.5
    pq.box((-pw, -0.07, -ph_), (pw, 0.07, ph_), GREEN)
    pq.box((-pw + 0.1, -0.09, -ph_ + 0.1), (pw - 0.1, 0.0, ph_ - 0.1), GOLD)
    pq.box((-pw + 0.15, -0.1, -ph_ + 0.15), (pw - 0.15, 0.0, ph_ - 0.15), PLAQUE)
    rnd = random.Random(7)
    for c in range(3):
        cz = ph_ - 0.14 - (2 * ph_ - 0.28) * (c + 0.5) / 3
        for k in range(7):
            if rnd.random() < 0.5:
                w, hh = rnd.uniform(0.25, 0.5), 0.06
            else:
                w, hh = 0.06, rnd.uniform(0.2, 0.32)
            px = rnd.uniform(-0.38 + w, 0.38 - w)
            pz = cz + rnd.uniform(-0.28 + hh, 0.28 - hh)
            pq.box((px - w, -0.13, pz - hh), (px + w, -0.08, pz + hh), GOLD)
    plq = pq.build('Hyeonpan_Sungnyemun')
    plq.location = (0, -(Wy2 + 0.95), ZC2 - 0.45)
    plq.rotation_euler = (math.radians(-6), 0, 0)
    # 현판 걸이
    hk = MB()
    hk.box((-0.06, -(Wy2 + 1.0), ZC2 + 0.9), (0.06, -(Wy2 + 0.2), ZC2 + 1.05), COL)
    hk.build('Hyeonpan_Hanger')
    for proto in (BRACKET, colp):
        bpy.data.objects.remove(proto, do_unlink=True)


main()
