# 자유의 여신상 (Statue of Liberty, 뉴욕 리버티섬) — 양식화한 실물 크기 모델 (지면~횃불 끝 약 93m)
# - 기단: 11각 별 모양 포트 우드 성벽(별 다각형 돌출) → 계단식 콘크리트 기초(사각 원뿔대 3단)
#   → 화강암 받침대(러스티케이션 띠, 모서리 기둥, 로지아 열주, 원반 장식 띠, 코니스, 난간).
# - 조각상: 높이별 타원 단면을 쌓은 bmesh 격자에 사인파 주름을 더해 드레스(스톨라)를 만들고,
#   왼쪽 어깨에서 오른쪽 겨드랑이로 비스듬히 걸친 망토(팔라)는 띠 모양 격자 + 두께(solidify)로 덧입혔다.
# - 들어 올린 오른팔·소매·왼팔은 캣멀롬 곡선을 따라가는 관(tube), 머리·머리카락은 타원체,
#   일곱 갈래 왕관 빛살은 사각뿔, 횃불은 회전체(lathe)와 물결 친 불꽃 격자로 만들었다.
# - 왼손의 서판(키스톤 모양)에는 "JULY IV MDCCLXXVI" 를 양각했고, 발치에는 끊어진 쇠사슬을 두었다.
import random
from atelier import *

random.seed(1886)

SHOT = {"azimuth": -102, "elevation": 8, "lens": 60, "light": "day", "background": "#27303c"}

# ---------------------------------------------------------------- 재질
M_PATINA = mat('Copper Patina', '#74a08f', rough=0.55, metal=0.15)
M_PATINA_D = mat('Copper Patina Shadow', '#5f8a7b', rough=0.6, metal=0.12)
M_GOLD = mat('Gold Leaf Flame', '#e2ab3c', rough=0.28, metal=1.0)
M_GRANITE = mat('Stony Creek Granite', '#ab9d88', rough=0.75)
M_GRANITE_D = mat('Granite Trim', '#968872', rough=0.7)
M_CONCRETE = mat('Foundation Concrete', '#9c9686', rough=0.85)
M_FORT = mat('Fort Wood Stone', '#837a6b', rough=0.85)
M_PAVING = mat('Terrace Paving', '#76736b', rough=0.9)
M_WINDOW = mat('Crown Window Glass', '#24343a', rough=0.15, metal=0.3)

Z0 = 46.6           # 조각상 발밑(받침대 위) 높이
TAU = 2 * math.pi


def lerp(a, b, s):
    return a + (b - a) * s


def interp_table(table, t):
    if t <= table[0][0]:
        return table[0][1:]
    for (t0, *v0), (t1, *v1) in zip(table, table[1:]):
        if t <= t1:
            s = (t - t0) / (t1 - t0)
            s = s * s * (3 - 2 * s)
            return [lerp(a, b, s) for a, b in zip(v0, v1)]
    return table[-1][1:]


def grid_mesh(name, rows, material, closed=True, cap_bottom=False, cap_top=False, smooth_it=True):
    """rows: 같은 길이의 3D 점 목록들. 행끼리 사각면으로 잇는다(closed 면 둘레도 닫음)."""
    bm = bmesh.new()
    vs = [[bm.verts.new(p) for p in row] for row in rows]
    n = len(rows[0])
    m = n if closed else n - 1
    for i in range(len(rows) - 1):
        for j in range(m):
            k = (j + 1) % n
            bm.faces.new((vs[i][j], vs[i][k], vs[i + 1][k], vs[i + 1][j]))
    if cap_bottom:
        bm.faces.new(list(reversed(vs[0])))
    if cap_top:
        bm.faces.new(vs[-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)
    assign(ob, material)
    if smooth_it:
        smooth(ob)
    return ob


def catmull(pts, n):
    """제어점을 지나는 캣멀롬 곡선을 n 점으로 샘플."""
    P = [Vector(pts[0]) * 2 - Vector(pts[1])] + [Vector(p) for p in pts] + [Vector(pts[-1]) * 2 - Vector(pts[-2])]
    out = []
    segs = len(pts) - 1
    for i in range(n):
        u = i / (n - 1) * segs
        k = min(int(u), segs - 1)
        s = u - k
        p0, p1, p2, p3 = P[k], P[k + 1], P[k + 2], P[k + 3]
        out.append(0.5 * ((2 * p1) + (-p0 + p2) * s + (2 * p0 - 5 * p1 + 4 * p2 - p3) * s * s + (-p0 + 3 * p1 - 3 * p2 + p3) * s ** 3))
    return out


def tube(name, ctrl, radius_fn, material, n=40, seg=24, cap=True, fold_fn=None):
    """곡선 관. radius_fn(s)→반지름, fold_fn(s, ang)→반지름 가감."""
    path = catmull(ctrl, n)
    rows = []
    ref = Vector((0, 1, 0))
    prev_nrm = None
    for i, p in enumerate(path):
        tng = (path[min(i + 1, n - 1)] - path[max(i - 1, 0)]).normalized()
        if prev_nrm is None:
            nrm = tng.cross(ref).normalized()
        else:
            nrm = (prev_nrm - tng * prev_nrm.dot(tng)).normalized()
        prev_nrm = nrm
        bin_ = tng.cross(nrm)
        s = i / (n - 1)
        row = []
        for j in range(seg):
            a = TAU * j / seg
            r = radius_fn(s) + (fold_fn(s, a) if fold_fn else 0.0)
            row.append(p + (nrm * math.cos(a) + bin_ * math.sin(a)) * r)
        rows.append(row)
    return grid_mesh(name, rows, material, cap_bottom=cap, cap_top=cap)


def sq_frustum(name, side0, side1, z0, z1, material):
    """정사각 원뿔대(아래 한 변 side0, 위 side1)."""
    ob = cylinder(name, r=side0 / math.sqrt(2), r2=side1 / math.sqrt(2), h=z1 - z0, loc=(0, 0, z0), seg=4, material=material)
    ob.rotation_euler.z = math.pi / 4
    return ob


def aim(ob, direction):
    ob.rotation_mode = 'QUATERNION'
    ob.rotation_quaternion = Vector((0, 0, 1)).rotation_difference(Vector(direction).normalized())
    return ob


# ================================================================ 1. 포트 우드 별 성벽
N_STAR = 11
R_OUT, R_IN = 37.0, 27.5


def star_pts(ro, ri, n=N_STAR, rot=math.pi / 2):
    pts = []
    for i in range(2 * n):
        r = ro if i % 2 == 0 else ri
        a = rot + math.pi * i / n
        pts.append((r * math.cos(a), r * math.sin(a)))
    return pts


fort = prism('FortWood_StarWall', star_pts(R_OUT, R_IN), 6.6, material=M_FORT)
fort_base = prism('FortWood_Footing', star_pts(R_OUT + 0.6, R_IN + 0.45), 0.9, material=M_GRANITE_D)
coping = prism('FortWood_Coping', star_pts(R_OUT + 0.35, R_IN + 0.25), 0.55, loc=(0, 0, 6.35), material=M_GRANITE_D)
terrace = prism('FortWood_Terrace', star_pts(R_OUT - 0.4, R_IN - 0.3), 0.05, loc=(0, 0, 6.88), material=M_PAVING)

# ================================================================ 2. 계단식 콘크리트 기초
sq_frustum('Foundation_Tier1', 28.0, 27.0, 6.8, 11.2, M_CONCRETE)
sq_frustum('Foundation_Tier2', 25.0, 24.0, 11.2, 15.6, M_CONCRETE)
sq_frustum('Foundation_Tier3', 22.0, 21.0, 15.6, 20.0, M_CONCRETE)
for i, (s, z) in enumerate(((27.4, 11.2), (24.4, 15.6))):
    sq_frustum(f'Foundation_StepLedge_{i}', s + 0.4, s + 0.4, z - 0.35, z, M_GRANITE_D)

# ================================================================ 3. 화강암 받침대
sq_frustum('Pedestal_BaseCourse', 20.4, 20.0, 20.0, 21.2, M_GRANITE_D)
sq_frustum('Pedestal_Plinth', 19.4, 18.4, 21.2, 23.2, M_GRANITE)
sq_frustum('Pedestal_LowerShaft', 17.4, 17.0, 23.2, 30.2, M_GRANITE)
# 러스티케이션 띠(돌 쌓기 줄눈 느낌)
for i in range(5):
    z = 24.2 + i * 1.3
    s = lerp(17.4, 17.0, (z - 23.2) / 7.0) + 0.24
    sq_frustum(f'Pedestal_Rustication_{i}', s, s, z, z + 0.7, M_GRANITE)
sq_frustum('Pedestal_MidCornice_Lower', 17.8, 18.6, 30.2, 30.8, M_GRANITE_D)
sq_frustum('Pedestal_MidCornice', 18.6, 18.6, 30.8, 31.3, M_GRANITE_D)

# 로지아 층: 움푹 들어간 코어 + 모서리 기둥 + 면마다 4개의 기둥
sq_frustum('Pedestal_LoggiaCore', 14.6, 14.6, 31.3, 40.6, M_GRANITE)
PIER = 4.2
HALF = 8.2
for cx in (-1, 1):
    for cy in (-1, 1):
        box(f'Pedestal_CornerPier_{cx}{cy}', (PIER, PIER, 9.3), loc=(cx * (HALF - PIER / 2), cy * (HALF - PIER / 2), 31.3), base=True, material=M_GRANITE)
        for k in range(4):
            box(f'Pedestal_PierRustic_{cx}{cy}_{k}', (PIER + 0.2, PIER + 0.2, 0.55),
                loc=(cx * (HALF - PIER / 2), cy * (HALF - PIER / 2), 32.2 + k * 2.0), base=True, material=M_GRANITE)

col_proto = lathe('Pedestal_LoggiaColumn', [(0, 31.3), (0.62, 31.3), (0.62, 31.65), (0.5, 31.8), (0.46, 38.9),
                                             (0.6, 39.2), (0.6, 39.5), (0, 39.5)], seg=20, material=M_GRANITE_D)
cap_proto = box('Pedestal_ColumnAbacus', (1.4, 1.4, 0.35), loc=(0, 0, 39.5), base=True, material=M_GRANITE_D)
first = True
for face in range(4):
    a = face * math.pi / 2
    ca, sa = math.cos(a), math.sin(a)
    for x in (-2.9, -0.97, 0.97, 2.9):
        px, py = x * ca - (-7.75) * sa, x * sa + (-7.75) * ca
        if first:
            col_proto.location = (px, py, 0)
            cap_proto.location = (px, py, 39.5)
            first = False
        else:
            duplicate(col_proto, loc=(px, py, 0))
            duplicate(cap_proto, loc=(px, py, 39.5))
    # 로지아 발코니 바닥과 난간
    bx, by = -(-8.1) * sa, (-8.1) * ca
    bal = box(f'Pedestal_Balcony_{face}', (7.8, 1.6, 0.35), loc=(bx, by, 31.3), rot=(0, 0, a), base=True, material=M_GRANITE_D)
    for k in range(9):
        x = -3.6 + k * 0.9
        px, py = x * ca - (-8.6) * sa, x * sa + (-8.6) * ca
        box(f'Pedestal_Baluster_{face}_{k}', (0.28, 0.28, 1.0), loc=(px, py, 31.65), rot=(0, 0, a), base=True, material=M_GRANITE_D)
    rx, ry = -(-8.6) * sa, (-8.6) * ca
    box(f'Pedestal_BalconyRail_{face}', (7.8, 0.5, 0.25), loc=(rx, ry, 32.65), rot=(0, 0, a), base=True, material=M_GRANITE_D)

sq_frustum('Pedestal_Architrave', 16.6, 16.6, 39.85, 40.9, M_GRANITE_D)
sq_frustum('Pedestal_Frieze', 16.0, 16.0, 40.9, 43.2, M_GRANITE)
# 원반(방패) 장식 띠: 면마다 10개, 모두 40개
disc = cylinder('Pedestal_FriezeDisc', r=0.5, h=0.18, seg=24, base=False, material=M_GRANITE_D)
first = True
for face in range(4):
    a = face * math.pi / 2
    for k in range(10):
        x = -6.3 + k * 1.4
        px, py = x * math.cos(a) + 8.05 * math.sin(a), x * math.sin(a) - 8.05 * math.cos(a)
        ob = disc if first else duplicate(disc)
        first = False
        ob.location = (px, py, 42.05)
        ob.rotation_euler = (math.pi / 2, 0, a)
sq_frustum('Pedestal_CorniceBed', 16.2, 17.2, 43.2, 43.7, M_GRANITE_D)
sq_frustum('Pedestal_Cornice', 17.6, 17.6, 43.7, 44.4, M_GRANITE_D)
sq_frustum('Pedestal_Attic', 16.2, 16.2, 44.4, 45.95, M_GRANITE)
# 꼭대기 전망대 난간
for k, (sx, sy, x, y) in enumerate(((16.2, 0.6, 0, -7.8), (16.2, 0.6, 0, 7.8), (0.6, 15.0, -7.8, 0), (0.6, 15.0, 7.8, 0))):
    box(f'Pedestal_Parapet_{k}', (sx, sy, 1.15), loc=(x, y, 45.95), base=True, material=M_GRANITE_D)

# ================================================================ 4. 조각상 — 드레스 몸통
plinth = cylinder('Statue_Plinth', r=6.9, r2=6.6, h=0.75, seg=8, loc=(0, 0, Z0 - 0.75 + 0.05), material=M_PATINA_D)
plinth.rotation_euler.z = math.pi / 8

# (t, 좌우 반폭 a, 앞뒤 반폭 b)
BODY = [(-0.1, 5.75, 5.0), (0.8, 5.5, 4.9), (3, 5.15, 4.6), (7, 4.9, 4.35), (12, 4.75, 4.2), (16, 4.8, 4.3),
        (19, 4.7, 4.15), (22, 4.5, 3.9), (24, 4.4, 3.75), (25.4, 4.2, 3.45), (26.4, 3.7, 3.05), (27.3, 2.7, 2.4),
        (28.0, 1.7, 1.7), (28.8, 1.32, 1.42), (30.2, 1.28, 1.38)]
PH = [random.uniform(0, TAU) for _ in range(6)]


def gauss(x, s):
    return math.exp(-(x / s) ** 2)


def ang_d(a, b):
    return (a - b + math.pi) % TAU - math.pi


def body_r(th, t):
    a, b = interp_table(BODY, t)
    c, s = math.cos(th), math.sin(th)
    r = a * b / math.sqrt((b * c) ** 2 + (a * s) ** 2)
    low = max(0.0, 1.0 - t / 19.0)
    # 늘어진 세로 주름(아래로 갈수록 깊다)
    r += (0.4 * math.sin(9 * th + PH[0] + 0.015 * t) + 0.18 * math.sin(15 * th + PH[1]) + 0.07 * math.sin(26 * th + PH[2])) * (0.25 + 0.75 * low) * (1 if t < 26 else max(0, (28 - t) / 2))
    # 가슴의 비스듬한 주름
    if 17 < t < 26.5:
        r += 0.09 * math.sin(11 * th + 0.55 * t + PH[3]) * gauss(t - 22, 3)
    # 앞으로 내민 오른쪽 무릎, 가슴, 뒤로 끌리는 옷자락
    r += 0.55 * gauss(ang_d(th, math.radians(238)), 0.42) * gauss(t - 10, 3.5)
    r += 0.35 * gauss(ang_d(th, math.radians(-90)), 0.7) * gauss(t - 23, 1.6)
    if s > 0:
        r += 0.7 * s * max(0.0, 1 - t / 7) ** 1.5
    return r


TH_N = 120
ts = [-0.1 + i * 0.25 for i in range(int(30.3 / 0.25) + 1)]
rows = []
for t in ts:
    rows.append([(body_r(TAU * j / TH_N, t) * math.cos(TAU * j / TH_N), body_r(TAU * j / TH_N, t) * math.sin(TAU * j / TH_N), Z0 + t) for j in range(TH_N)])
grid_mesh('Statue_Robe_Stola', rows, M_PATINA, cap_bottom=True, cap_top=True)

# ---------------------------------------------------------------- 망토(팔라): 왼쪽 어깨→오른쪽 겨드랑이로 걸친 띠


def mantle_hem(th):
    return 8.5 + 5.0 * math.cos(th - math.radians(235)) + 0.9 * math.sin(3 * th) + 0.6 * math.sin(7 * th + 1.0)


def mantle_top(th):
    # 왼쪽 어깨(+X) 쪽에서 높고, 오른쪽 겨드랑이(-X) 쪽에서 낮다
    return 21.8 + 4.6 * math.cos(th) + 2.6 * math.sin(th) + 0.6 * max(0.0, math.cos(th)) ** 0.5


def mantle_off(th, t, s):
    return 0.5 + 0.32 * math.sin(7 * th + 0.35 * t + PH[4]) + 0.1 * math.sin(13 * th - 0.2 * t + PH[5]) + 0.15 * (1 - s)


M_ROWS = 34
mrows = []
top_edge, hem_edge = [], []
for i in range(M_ROWS + 1):
    s = i / M_ROWS
    row = []
    for j in range(TH_N):
        th = TAU * j / TH_N
        t = lerp(mantle_hem(th), mantle_top(th), s)
        r = body_r(th, t) + mantle_off(th, t, s)
        row.append((r * math.cos(th), r * math.sin(th), Z0 + t))
    mrows.append(row)
mantle = grid_mesh('Statue_Mantle_Palla', mrows, M_PATINA, cap_bottom=False, cap_top=False)
solidify(mantle, 0.3)
mantle.modifiers[-1].offset = 0.0
curve_tube('Statue_Mantle_RolledEdge', [mrows[-1][j] for j in range(TH_N)], radius=0.3, material=M_PATINA, closed=True, resolution=2)
curve_tube('Statue_Mantle_Hem', [mrows[0][j] for j in range(TH_N)], radius=0.17, material=M_PATINA, closed=True, resolution=2)

# 왼팔 아래로 늘어진 망토 자락(왼쪽 옆 실루엣)
tube('Statue_Mantle_Cascade', [(5.2, 0.2, Z0 + 20.5), (5.9, 0.0, Z0 + 15.5), (6.0, -0.2, Z0 + 10.5), (5.7, -0.3, Z0 + 5.0)],
     lambda s: lerp(1.25, 0.75, s), M_PATINA, n=30, seg=20,
     fold_fn=lambda s, a: 0.18 * math.sin(5 * a + 3 * s))

# ---------------------------------------------------------------- 발과 끊어진 쇠사슬
sphere('Statue_LeftFoot', r=1.0, loc=(1.6, -5.1, Z0 + 0.25), scale=(0.75, 1.35, 0.5), material=M_PATINA)
sphere('Statue_RightToes', r=1.0, loc=(-1.9, -5.0, Z0 + 0.15), scale=(0.6, 0.9, 0.35), material=M_PATINA)
link_proto = torus('Statue_BrokenChain_Link', R=0.42, r=0.12, seg=20, rseg=8, material=M_PATINA_D)
for k in range(5):
    x = -0.6 - k * 0.72
    ob = link_proto if k == 0 else duplicate(link_proto)
    if k % 2 == 0:
        ob.location = (x, -5.95, Z0 + 0.12)
        ob.rotation_euler = (0, 0, 0)
    else:
        ob.location = (x, -5.95, Z0 + 0.4)
        ob.rotation_euler = (math.pi / 2, 0, 0)
shackle = torus('Statue_Shackle', R=0.55, r=0.16, seg=24, rseg=8, loc=(-3.3, -5.25, Z0 + 0.5), rot=(math.pi / 2, 0, 0.5), material=M_PATINA_D)

# ================================================================ 5. 머리·왕관
HC = Vector((0, -0.2, Z0 + 31.35))
sphere('Statue_Face', r=1.0, loc=HC, scale=(1.45, 1.75, 2.45), seg=40, rings=24, material=M_PATINA)
sphere('Statue_Hair', r=1.0, loc=HC + Vector((0, 0.75, 0.45)), scale=(1.75, 1.7, 2.2), seg=40, rings=24, material=M_PATINA)
sphere('Statue_HairBun', r=0.95, loc=HC + Vector((0, 1.85, -1.1)), scale=(1.1, 0.9, 0.9), material=M_PATINA)
for side in (-1, 1):
    sphere(f'Statue_HairCurl_{side}', r=0.7, loc=HC + Vector((side * 1.45, 0.35, -0.9)), scale=(0.7, 1.0, 1.3), material=M_PATINA)
sphere('Statue_Nose', r=1.0, loc=HC + Vector((0, -1.72, -0.15)), scale=(0.22, 0.32, 0.55), material=M_PATINA)
sphere('Statue_Chin', r=1.0, loc=HC + Vector((0, -1.25, -1.75)), scale=(0.55, 0.45, 0.45), material=M_PATINA)
for side in (-1, 1):
    sphere(f'Statue_Brow_{side}', r=1.0, loc=HC + Vector((side * 0.6, -1.62, 0.42)), scale=(0.5, 0.18, 0.13), material=M_PATINA)
    sphere(f'Statue_Cheek_{side}', r=1.0, loc=HC + Vector((side * 0.75, -1.38, -0.5)), scale=(0.45, 0.35, 0.5), material=M_PATINA)

# 왕관 띠(타원 회전체) + 25개의 창 + 7개의 빛살
CR_Z = HC.z + 1.05
band = lathe('Statue_Crown_Diadem', [(1.62, -0.45), (1.92, -0.45), (2.0, -0.25), (2.0, 0.42), (1.92, 0.6), (1.62, 0.6), (1.62, -0.45)],
             seg=64, loc=(HC.x, HC.y + 0.2, CR_Z), material=M_PATINA)
band.scale = (1.0, 1.08, 1.0)
win_proto = box('Statue_Crown_Window', (0.2, 0.12, 0.42), material=M_WINDOW)
for k in range(25):
    a = math.radians(-90 + (k - 12) * 5.6)
    ob = win_proto if k == 0 else duplicate(win_proto)
    ob.location = (HC.x + 2.0 * math.cos(a), HC.y + 0.2 + 2.0 * 1.08 * math.sin(a), CR_Z + 0.08)
    ob.rotation_euler = (0, 0, a + math.pi / 2)
ray_proto = cone('Statue_Crown_Ray', r=0.5, h=3.5, seg=4, material=M_PATINA)
for k in range(7):
    a = math.radians(-90 + (k - 3) * 31)
    el = math.radians(24 + 4 * abs(k - 3))
    d = Vector((math.cos(a) * math.cos(el), math.sin(a) * math.cos(el), math.sin(el)))
    ob = ray_proto if k == 0 else duplicate(ray_proto)
    ob.location = Vector((HC.x + 1.55 * math.cos(a), HC.y + 0.2 + 1.65 * math.sin(a), CR_Z + 0.35))
    aim(ob, d)

# ================================================================ 6. 들어 올린 오른팔과 횃불
R_SH = (-3.0, 0.3, Z0 + 25.0)
R_ARM = [R_SH, (-4.3, 0.45, Z0 + 28.6), (-5.3, 0.6, Z0 + 32.6), (-6.05, 0.7, Z0 + 36.0), (-6.45, 0.7, Z0 + 37.7)]
tube('Statue_RightArm', R_ARM, lambda s: lerp(1.15, 0.72, s), M_PATINA, n=40, seg=24)
# 소매: 어깨 쪽으로 흘러내려 모인 넓은 천
sleeve = tube('Statue_RightSleeve', R_ARM[:3] + [(-5.6, 0.62, Z0 + 33.9)], lambda s: lerp(2.0, 1.45, s) + 0.2 * (1 - s), M_PATINA,
              n=30, seg=28, cap=False, fold_fn=lambda s, a: 0.17 * math.sin(7 * a + 2.0 * s) * (1 - 0.4 * s))
solidify(sleeve, 0.22)
HAND = Vector((-6.55, 0.7, Z0 + 38.5))
sphere('Statue_RightHand', r=1.0, loc=HAND, scale=(0.95, 0.9, 1.15), material=M_PATINA)
for k in range(4):
    torus(f'Statue_RightFinger_{k}', R=0.62, r=0.2, seg=20, rseg=8, loc=HAND + Vector((0.0, -0.05, -0.55 + k * 0.36)), material=M_PATINA)

TX, TY = HAND.x, HAND.y
lathe('Statue_Torch_Handle', [(0, Z0 + 36.6), (0.42, Z0 + 36.6), (0.5, Z0 + 37.0), (0.5, Z0 + 39.8), (0.62, Z0 + 40.1),
                               (0.78, Z0 + 40.3), (0.62, Z0 + 40.6), (0.62, Z0 + 40.9), (1.15, Z0 + 41.35), (1.75, Z0 + 41.95),
                               (2.05, Z0 + 42.3), (2.05, Z0 + 42.5), (2.62, Z0 + 42.5), (2.62, Z0 + 42.72), (0, Z0 + 42.72)],
      loc=(TX, TY, 0), seg=40, material=M_PATINA)
post = cylinder('Statue_Torch_RailPost', r=0.06, h=0.95, seg=6, loc=(TX + 2.52, TY, Z0 + 42.72), material=M_PATINA_D)
for k in range(1, 20):
    a = TAU * k / 20
    duplicate(post, loc=(TX + 2.52 * math.cos(a), TY + 2.52 * math.sin(a), Z0 + 42.72))
torus('Statue_Torch_Rail', R=2.52, r=0.08, seg=48, rseg=6, loc=(TX, TY, Z0 + 43.67), material=M_PATINA_D)
# 불꽃: 금박을 입힌 물결 친 혀 모양
frows = []
FH = 4.4
for i in range(29):
    h = i / 28
    row = []
    for j in range(48):
        th = TAU * j / 48
        r = 1.75 * (1 - h) ** 0.72 * (1 + 0.42 * math.sin(math.pi * min(h * 1.7, 1.0)))
        r *= 1 + 0.2 * h * math.sin(5 * th + 5.5 * h) + 0.06 * math.sin(9 * th - 3 * h)
        row.append((TX + r * math.cos(th + 0.9 * h), TY + r * math.sin(th + 0.9 * h), Z0 + 42.55 + h * FH))
    frows.append(row)
grid_mesh('Statue_Torch_Flame', frows, M_GOLD, cap_bottom=True)

# ================================================================ 7. 왼팔과 서판
L_ARM = [(3.2, 0.3, Z0 + 25.0), (4.75, 0.7, Z0 + 22.2), (5.0, 0.4, Z0 + 19.4), (4.7, -1.6, Z0 + 18.6), (4.3, -3.1, Z0 + 19.1)]
tube('Statue_LeftArm', L_ARM, lambda s: lerp(1.25, 0.75, s), M_PATINA, n=40, seg=24,
     fold_fn=lambda s, a: 0.12 * math.sin(6 * a + 4 * s) * (1 - s))
sphere('Statue_LeftHand', r=1.0, loc=(4.15, -3.35, Z0 + 19.45), scale=(0.8, 0.95, 0.7), material=M_PATINA)

TAB_LOC = Vector((5.0, -2.75, Z0 + 22.4))
TAB_ROT = Euler((-0.24, 0.12, math.radians(28)))
tablet = extrude_profile('Statue_Tablet', [(-1.75, -3.6), (1.75, -3.6), (2.07, 3.6), (-2.07, 3.6)], 0.61, material=M_PATINA)
tablet.location = TAB_LOC
tablet.rotation_euler = TAB_ROT
TM = Matrix.Translation(TAB_LOC) @ TAB_ROT.to_matrix().to_4x4()
# 서판 테두리(살짝 도드라진 액자)
frame_pts = [(-1.6, -3.42), (1.6, -3.42), (1.88, 3.42), (-1.88, 3.42), (-1.6, -3.42)]
curve_tube('Statue_Tablet_Border', [tuple(TM @ Vector((x, -0.31, z))) for x, z in frame_pts], radius=0.07, material=M_PATINA_D)
txt = text3d('Statue_Tablet_Inscription', 'JULY IV MDCCLXXVI', size=0.62, depth=0.03, material=M_PATINA_D)
txt.data.align_y = 'CENTER'
dg = bpy.context.evaluated_depsgraph_get()
tme = bpy.data.meshes.new_from_object(txt.evaluated_get(dg))
bpy.data.objects.remove(txt, do_unlink=True)
inscr = bpy.data.objects.new('Statue_Tablet_Inscription', tme)
link(inscr)
assign(inscr, M_PATINA_D)
# 글자: 서판 긴 변을 따라 아래에서 위로, 앞면(-Y) 위에 양각
inscr.matrix_world = TM @ Matrix.Translation((0, -0.31, 0)) @ Euler((math.pi / 2, 0, 0)).to_matrix().to_4x4() @ Euler((0, 0, math.pi / 2)).to_matrix().to_4x4()

