# 파리지옥 (Dionaea muscipula) — 물이끼 위에 자란 한 포기
# 덫은 매개변수 곡면으로 만들었다: 중심맥(경첩)을 따라 u, 단면을 따라 v 를 두고
# 단면 각도를 적분해 오목한 조개 모양 두 잎(엽편)을 한 장의 곡면으로 접었다.
# 안쪽은 진홍색, 가장자리 띠는 연두색, 바깥은 초록색이며 두께는 Solidify 로 주고 재질을 나눴다.
# 가장자리 섬모(cilia)는 테두리 곡선의 법선을 따라 뻗은 가는 원뿔관이고, 닫힌 덫에서는 서로 엇갈린다.
# 잎자루는 넓적한 날개형 띠를 곡선을 따라 휘었고, 잎 3종을 링크 복제해 로제트로 돌려 배치했다.
# 바닥은 회전체 이탄 둔덕 위에 물이끼 머리(작은 구) 수백 개를 고정 시드로 흩뿌렸다.
import random
from atelier import *

random.seed(53)

SHOT = {"azimuth": -60, "elevation": 36, "lens": 60, "light": "day", "background": "#2a333d"}

M_LEAF = mat('LeafGreen', '#5b8a2c', rough=0.5)
M_TRAP_IN = mat('TrapInnerCrimson', '#a51f2a', rough=0.32, coat=0.4)
M_RIM = mat('TrapRimYellowGreen', '#a8b84a', rough=0.45)
M_OUT = mat('TrapOuterGreen', '#679a32', rough=0.45)
M_CILIA = mat('CiliaGreen', '#93a23e', rough=0.5)
M_PEAT = mat('PeatSoil', '#3b2b1f', rough=0.95)
M_MOSS = mat('SphagnumMoss', '#6f863a', rough=0.85)
M_MOSS2 = mat('SphagnumMossPale', '#959a5a', rough=0.9)
M_MOSS3 = mat('SphagnumMossDeep', '#566b2e', rough=0.85)


def mesh_from(name, verts, faces, mats, face_mat=None):
    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.update()
    for m in mats:
        me.materials.append(m)
    if face_mat:
        for p, mi in zip(me.polygons, face_mat):
            p.material_index = mi
    for p in me.polygons:
        p.use_smooth = True
    ob = bpy.data.objects.new(name, me)
    link(ob)
    return ob


def orient_faces(ob, center):
    """곡면 법선이 center(덫 안쪽)를 향하게 맞춘다."""
    bm = bmesh.new()
    bm.from_mesh(ob.data)
    flip = [f for f in bm.faces if f.normal.dot(Vector(center) - f.calc_center_median()) < 0]
    bmesh.ops.reverse_faces(bm, faces=flip)
    bm.to_mesh(ob.data)
    bm.free()


def tube_mesh(verts, faces, pts, radii, sides=5):
    """pts 를 따라가는 끝이 뾰족한 관을 verts/faces 에 덧붙인다."""
    rings = []
    for i, p in enumerate(pts):
        if i < len(pts) - 1:
            t = (Vector(pts[i + 1]) - Vector(p)).normalized()
        else:
            t = (Vector(p) - Vector(pts[i - 1])).normalized()
        a = t.orthogonal().normalized()
        b = t.cross(a)
        r = radii[i]
        ring = []
        for k in range(sides):
            ang = 2 * math.pi * k / sides
            ring.append(len(verts))
            verts.append(tuple(Vector(p) + r * (math.cos(ang) * a + math.sin(ang) * b)))
        rings.append(ring)
    for i in range(len(rings) - 1):
        r0, r1 = rings[i], rings[i + 1]
        for k in range(sides):
            faces.append((r0[k], r0[(k + 1) % sides], r1[(k + 1) % sides], r1[k]))
    tip = len(verts)
    last = Vector(pts[-1]) + (Vector(pts[-1]) - Vector(pts[-2])).normalized() * radii[-1] * 3
    verts.append(tuple(last))
    for k in range(sides):
        faces.append((rings[-1][k], rings[-1][(k + 1) % sides], tip))
    faces.append(tuple(reversed(rings[0])))


# ---------------------------------------------------------------- 덫(trap)
def section(alpha, cup, flare, n):
    """단면 곡선(경첩→테두리)을 각도 적분으로 만든다. 반환: [(y, z, angle)] (폭 1 기준)."""
    out = [(0.0, 0.0, alpha + cup)]
    y = z = 0.0
    for i in range(1, n + 1):
        t = (i - 0.5) / n
        a = alpha + cup * (1 - t) + flare * t ** 5
        y += math.sin(a) / n
        z += math.cos(a) / n
        out.append((y, z, alpha + flare))
    return out


def make_trap(name, L, Wmax, closed=False):
    nu, nv = 36, 14
    if closed:
        cup, flare = math.radians(55), math.radians(-8)
        lo, hi = math.radians(-30), math.radians(40)
        for _ in range(40):  # 테두리가 가운데서 거의 맞닿도록 열림 각을 이분 탐색
            mid = (lo + hi) / 2
            if section(mid, cup, flare, nv)[-1][0] > 0.05:
                hi = mid
            else:
                lo = mid
        alpha = (lo + hi) / 2
    else:
        alpha, cup, flare = math.radians(24), math.radians(38), math.radians(30)
    sec = section(alpha, cup, flare, nv)

    def width(u):
        return Wmax * max(math.sin(math.pi * u), 0.0) ** 0.6 * (0.85 + 0.3 * u) + Wmax * 0.03

    verts, faces, fmat = [], [], []
    grid = []
    for i in range(nu + 1):
        u = i / nu
        x = L * u
        W = width(u)
        row = []
        for j in range(-nv, nv + 1):
            sg = 1 if j >= 0 else -1
            y, z, _a = sec[abs(j)]
            row.append(len(verts))
            verts.append((x, sg * y * W, z * W))
        grid.append(row)
    for i in range(nu):
        for j in range(2 * nv):
            faces.append((grid[i][j], grid[i + 1][j], grid[i + 1][j + 1], grid[i][j + 1]))
            t = abs((j + 0.5) - nv) / nv
            fmat.append(1 if t > 0.84 else 0)
    trap = mesh_from(name + 'Lobes', verts, faces, [M_TRAP_IN, M_RIM, M_OUT, M_OUT], fmat)
    orient_faces(trap, (L * 0.5, 0, Wmax * 0.9))
    s = trap.modifiers.new('Solidify', 'SOLIDIFY')
    s.thickness = L * 0.03
    s.offset = -1
    s.material_offset = 2
    s.material_offset_rim = 2
    s.use_even_offset = True

    # 테두리 섬모와 감각모
    cv, cf = [], []
    n_cil = 17
    clen = Wmax * (0.42 if not closed else 0.55)
    rad = L * 0.012
    for sg in (1, -1):
        y1, z1, a_rim = sec[-1]
        for k in range(n_cil):
            u = 0.07 + 0.86 * (k + (0.5 if sg < 0 and closed else 0.0)) / (n_cil - (0 if closed else 1))
            if u > 0.95:
                continue
            W = width(u)
            dW = (width(u + 1e-3) - width(u - 1e-3)) / 2e-3 / L
            base = Vector((L * u, sg * y1 * W, z1 * W))
            if closed:
                ac = a_rim - math.radians(32)
            else:
                ac = a_rim + math.radians(18)
            e = Vector((0, sg * math.sin(ac), math.cos(ac)))
            d = (Vector((-dW, 0, 0)) * 0.9 + e).normalized()
            if closed:
                bend = Vector((0, -sg * 0.25, 0.15))
            else:
                bend = Vector((0, sg * 0.25, -0.35))
            length = clen * (0.75 + 0.25 * math.sin(math.pi * u))
            p0 = base - d * rad * 2
            p1 = base + d * length * 0.5
            p2 = base + (d + bend * 0.5).normalized() * length
            tube_mesh(cv, cf, [tuple(p0), tuple(p1), tuple(p2)], [rad, rad * 0.75, rad * 0.4])
        # 감각모: 각 엽편 안쪽에 3개
        if not closed:
            for uu, tt in ((0.35, 0.45), (0.55, 0.5), (0.72, 0.42)):
                W = width(uu)
                j = int(tt * nv)
                y, z, _ = sec[j]
                pin = Vector((L * uu, sg * y * W, z * W))
                nrm = Vector((0, -sg * math.cos(alpha), math.sin(alpha))).normalized()
                tube_mesh(cv, cf, [tuple(pin - nrm * 0.0004), tuple(pin + nrm * Wmax * 0.12),
                                   tuple(pin + nrm * Wmax * 0.22)], [rad * 0.5, rad * 0.35, rad * 0.2], 4)
    cil = mesh_from(name + 'Cilia', cv, cf, [M_CILIA])
    return [trap, cil]


# ---------------------------------------------------------------- 잎자루(petiole)
def make_petiole(name, Lp, phi0, phi1, wmax):
    ns, nw = 34, 6
    pts, angs = [], []
    x = z = 0.0
    for i in range(ns + 1):
        s = i / ns
        phi = phi0 + (phi1 - phi0) * s ** 0.7
        pts.append((x, z))
        angs.append(phi)
        x += math.cos(phi) * Lp / ns
        z += math.sin(phi) * Lp / ns

    def w(s):
        if s < 0.86:
            return 0.0018 + (wmax - 0.0018) * (s / 0.86) ** 1.4
        k = (s - 0.86) / 0.14
        return wmax * (1 - k) ** 1.6 + 0.0011 * k

    verts, faces = [], []
    for i in range(ns + 1):
        s = i / ns
        cx, cz = pts[i]
        phi = angs[i]
        N = Vector((-math.sin(phi), 0, math.cos(phi)))
        ww = w(s)
        for j in range(nw + 1):
            q = -1 + 2 * j / nw
            off = 0.22 * ww * q * q  # 가운데가 오목한 홈
            p = Vector((cx, q * ww, cz)) + N * off
            verts.append(tuple(p))
    for i in range(ns):
        for j in range(nw):
            a = i * (nw + 1) + j
            faces.append((a, a + 1, a + nw + 2, a + nw + 1))
    ob = mesh_from(name + 'Petiole', verts, faces, [M_LEAF])
    s = ob.modifiers.new('Solidify', 'SOLIDIFY')
    s.thickness = 0.0007
    s.offset = 0
    end = Vector((pts[-1][0], 0, pts[-1][1]))
    return ob, end, angs[-1]


def make_leaf(name, Lp, phi0, phi1, wmax, L, Wmax, closed=False, trap_pitch=12):
    pet, end, phi = make_petiole(name, Lp, phi0, phi1, wmax)
    parts = make_trap(name, L, Wmax, closed)
    pitch = phi + math.radians(trap_pitch)
    for p in parts:
        p.rotation_euler = (0, -pitch, 0)
        p.location = end - Vector((math.cos(pitch), 0, math.sin(pitch))) * L * 0.02
    leaf = join([pet] + parts, name)
    # 변환을 메시에 굽는다
    leaf.data.transform(leaf.matrix_basis)
    leaf.matrix_basis = Matrix.Identity(4)
    return leaf


LEAF_OPEN = make_leaf('LeafOpenLarge', 0.062, math.radians(38), math.radians(8), 0.0074, 0.032, 0.0145)
LEAF_MID = make_leaf('LeafOpenUpright', 0.048, math.radians(55), math.radians(22), 0.0066, 0.027, 0.0125, trap_pitch=18)
LEAF_SHUT = make_leaf('LeafClosed', 0.055, math.radians(42), math.radians(14), 0.007, 0.029, 0.0125, closed=True)

# ---------------------------------------------------------------- 바닥 둔덕
R_MOUND, H_MOUND = 0.09, 0.015
prof = [(0.0, H_MOUND)]
for i in range(1, 15):
    r = R_MOUND * i / 14
    prof.append((r, H_MOUND * (1 - (r / R_MOUND) ** 2.4) ** 0.6))
prof.append((R_MOUND, 0.0))
prof.append((0.0, 0.0))
mound = lathe('PeatMound', prof, material=M_PEAT, seg=64)


def mound_z(r):
    r = min(r, R_MOUND)
    return H_MOUND * (1 - (r / R_MOUND) ** 2.4) ** 0.6


BASE_Z = H_MOUND - 0.003

# ---------------------------------------------------------------- 로제트 배치
layout = [
    # (variant, azimuth deg, scale, roll deg)
    (LEAF_OPEN, 10, 1.00, 4), (LEAF_SHUT, 48, 0.95, -6), (LEAF_OPEN, 85, 0.92, -5),
    (LEAF_MID, 122, 1.0, 6), (LEAF_OPEN, 160, 1.05, 3), (LEAF_SHUT, 198, 0.9, 8),
    (LEAF_OPEN, 235, 0.97, -4), (LEAF_MID, 272, 0.95, 5), (LEAF_OPEN, 310, 1.02, -7),
    (LEAF_MID, 345, 0.85, 0),
    (LEAF_SHUT, 105, 0.72, -10), (LEAF_OPEN, 215, 0.74, 9), (LEAF_SHUT, 330, 0.7, 6),
    (LEAF_OPEN, 290, 0.7, -8), (LEAF_MID, 180, 0.72, 4),
    (LEAF_MID, 30, 0.55, 0), (LEAF_MID, 150, 0.5, 0), (LEAF_MID, 260, 0.52, 0),
]
for idx, (src, az, sc, roll) in enumerate(layout):
    ob = src if idx < 3 and src.get('placed') is None else duplicate(src)
    src['placed'] = True
    ob.name = f'Leaf_{idx:02d}_{src.name.split("_")[0]}'
    ob.location = (0, 0, BASE_Z)
    ob.rotation_euler = (math.radians(roll), 0, math.radians(az))
    ob.scale = (sc, sc, sc)

# 로제트 중심의 생장점
crown = sphere('RosetteCrown', r=0.006, loc=(0, 0, BASE_Z), material=M_LEAF, seg=16, rings=8, scale=(1, 1, 0.6))

# ---------------------------------------------------------------- 물이끼
moss_a = sphere('SphagnumHead', r=0.0045, material=M_MOSS, seg=10, rings=6, scale=(1, 1, 0.65))
moss_b = sphere('SphagnumHeadDry', r=0.004, material=M_MOSS2, seg=10, rings=6, scale=(1, 1, 0.6))
moss_c = sphere('SphagnumHeadDeep', r=0.005, material=M_MOSS3, seg=10, rings=6, scale=(1, 1, 0.7))
moss_srcs = [moss_a, moss_b, moss_c]
for m_, (x_, y_) in zip(moss_srcs, ((0.02, 0.0), (-0.03, 0.01), (0.0, -0.035))):
    m_.location = (x_, y_, mound_z(math.hypot(x_, y_)) - 0.0008)
count = 0
tries = 0
placed = [(0.02, 0.0), (-0.03, 0.01), (0.0, -0.035)]
while count < 680 and tries < 20000:
    tries += 1
    r = R_MOUND * 0.97 * math.sqrt(random.random())
    a = random.random() * 2 * math.pi
    x, y = r * math.cos(a), r * math.sin(a)
    if any((x - px) ** 2 + (y - py) ** 2 < 0.0042 ** 2 for px, py in placed):
        continue
    placed.append((x, y))
    src = random.choices(moss_srcs, weights=(5, 2, 3))[0]
    s = random.uniform(0.75, 1.3)
    duplicate(src, loc=(x, y, mound_z(r) - 0.0008 - random.uniform(0, 0.0015)), rot=(random.uniform(-0.3, 0.3), random.uniform(-0.3, 0.3), random.random() * 6.28), scale=(s, s, s * random.uniform(0.8, 1.3)),
              name='SphagnumHead')
    count += 1
