import bpy
import math
import random
from mathutils import Vector, Matrix

random.seed(7)

TAU = 2.0 * math.pi
ZUP = Vector((0.0, 0.0, 1.0))


# ---------------------------------------------------------------- helpers
def clamp(t, lo=0.0, hi=1.0):
    return max(lo, min(hi, t))


def sstep(e0, e1, x):
    if e1 == e0:
        return 1.0 if x >= e1 else 0.0
    t = clamp((x - e0) / (e1 - e0))
    return t * t * (3.0 - 2.0 * t)


def interp(table, x):
    if x <= table[0][0]:
        return table[0][1]
    if x >= table[-1][0]:
        return table[-1][1]
    for i in range(len(table) - 1):
        x0, y0 = table[i]
        x1, y1 = table[i + 1]
        if x0 <= x <= x1:
            t = (x - x0) / (x1 - x0) if x1 != x0 else 0.0
            return y0 + (y1 - y0) * t
    return table[-1][1]


class Builder:
    def __init__(self):
        self.v = []
        self.f = []

    def add(self, verts, faces):
        o = len(self.v)
        self.v.extend([(p[0], p[1], p[2]) for p in verts])
        self.f.extend([tuple(i + o for i in fc) for fc in faces])


BUILD = {}


def B(key):
    if key not in BUILD:
        BUILD[key] = Builder()
    return BUILD[key]


def any_perp(t):
    a = Vector((1.0, 0.0, 0.0)) if abs(t.x) < 0.9 else Vector((0.0, 1.0, 0.0))
    n = a - t * a.dot(t)
    if n.length < 1e-9:
        n = Vector((0.0, 0.0, 1.0)) - t * t.z
    return n.normalized()


def tube(b, pts, radii, seg=8, cap_start=True, cap_end=True):
    n = len(pts)
    if n < 2:
        return
    pts = [Vector(p) for p in pts]
    tans = []
    for k in range(n):
        if k == 0:
            t = pts[1] - pts[0]
        elif k == n - 1:
            t = pts[-1] - pts[-2]
        else:
            t = pts[k + 1] - pts[k - 1]
        if t.length < 1e-10:
            t = tans[-1].copy() if tans else Vector((0.0, 0.0, 1.0))
        tans.append(t.normalized())
    verts, faces = [], []
    nrm = any_perp(tans[0])
    for k in range(n):
        t = tans[k]
        nrm = nrm - t * nrm.dot(t)
        if nrm.length < 1e-8:
            nrm = any_perp(t)
        nrm.normalize()
        bin_ = t.cross(nrm).normalized()
        r = max(float(radii[k]), 1e-5)
        for j in range(seg):
            a = TAU * j / seg
            verts.append(pts[k] + (nrm * math.cos(a) + bin_ * math.sin(a)) * r)
    for k in range(n - 1):
        for j in range(seg):
            a0 = k * seg + j
            a1 = k * seg + (j + 1) % seg
            faces.append((a0, a1, a1 + seg, a0 + seg))
    if cap_start:
        ci = len(verts)
        verts.append(pts[0].copy())
        for j in range(seg):
            faces.append((ci, (j + 1) % seg, j))
    if cap_end:
        ci = len(verts)
        verts.append(pts[-1].copy())
        base = (n - 1) * seg
        for j in range(seg):
            faces.append((ci, base + j, base + (j + 1) % seg))
    b.add(verts, faces)


_SPH_V = []
_SPH_F = []


def _init_sphere(nseg=8, nring=6):
    _SPH_V.append(Vector((0.0, 0.0, -1.0)))
    for i in range(1, nring):
        th = -math.pi / 2 + math.pi * i / nring
        for j in range(nseg):
            a = TAU * j / nseg
            _SPH_V.append(Vector((math.cos(th) * math.cos(a), math.cos(th) * math.sin(a), math.sin(th))))
    _SPH_V.append(Vector((0.0, 0.0, 1.0)))
    top = len(_SPH_V) - 1
    for j in range(nseg):
        _SPH_F.append((0, 1 + (j + 1) % nseg, 1 + j))
    for i in range(nring - 2):
        for j in range(nseg):
            a0 = 1 + i * nseg + j
            a1 = 1 + i * nseg + (j + 1) % nseg
            _SPH_F.append((a0, a1, a1 + nseg, a0 + nseg))
    last = 1 + (nring - 2) * nseg
    for j in range(nseg):
        _SPH_F.append((last + j, last + (j + 1) % nseg, top))


_init_sphere()


def ellipsoid(b, M):
    b.add([M @ v for v in _SPH_V], _SPH_F)


def align_z(axis):
    a = Vector(axis)
    if a.length < 1e-9:
        return Matrix.Identity(4)
    a.normalize()
    return a.to_track_quat('Z', 'Y').to_matrix().to_4x4()


def trs(c, axis, sx, sy, sz, rot=0.0):
    return (Matrix.Translation(c) @ align_z(axis) @ Matrix.Rotation(rot, 4, 'Z')
            @ Matrix.Diagonal(Vector((sx, sy, sz, 1.0))))


# ---------------------------------------------------------------- materials
MAT_DEF = {
    'stalk': ((0.22, 0.42, 0.12), 0.5, 0.05),
    'sheath': ((0.20, 0.42, 0.12), 0.5, 0.05),
    'ligule': ((0.80, 0.82, 0.65), 0.6, 0.1),
    'leaf': ((0.13, 0.38, 0.08), 0.45, 0.05),
    'deadleaf': ((0.45, 0.36, 0.20), 0.8, 0.0),
    'midrib': ((0.60, 0.70, 0.45), 0.5, 0.05),
    'husk': ((0.35, 0.55, 0.20), 0.5, 0.05),
    'silk': ((0.75, 0.55, 0.30), 0.6, 0.1),
    'kernel': ((0.95, 0.85, 0.45), 0.35, 0.1),
    'cob': ((0.90, 0.88, 0.75), 0.6, 0.0),
    'tassel': ((0.40, 0.45, 0.18), 0.6, 0.0),
    'spikelet': ((0.50, 0.42, 0.25), 0.6, 0.0),
    'anther': ((0.90, 0.75, 0.25), 0.5, 0.0),
    'filament': ((0.85, 0.82, 0.60), 0.6, 0.0),
    'root': ((0.85, 0.78, 0.60), 0.7, 0.0),
    'braceroot': ((0.55, 0.45, 0.30), 0.6, 0.0),
}
MATS = {}


def get_mat(key):
    if key in MATS:
        return MATS[key]
    col, rough, sss = MAT_DEF.get(key, ((0.5, 0.5, 0.5), 0.5, 0.0))
    m = bpy.data.materials.new("Maize_" + key)
    try:
        m.diffuse_color = (col[0], col[1], col[2], 1.0)
    except Exception:
        pass
    try:
        m.use_nodes = True
        bsdf = m.node_tree.nodes.get("Principled BSDF")
        if bsdf is not None:
            try:
                bsdf.inputs["Base Color"].default_value = (col[0], col[1], col[2], 1.0)
            except Exception:
                pass
            try:
                bsdf.inputs["Roughness"].default_value = float(rough)
            except Exception:
                pass
            try:
                bsdf.inputs["Subsurface Weight"].default_value = float(sss)
            except Exception:
                pass
    except Exception:
        pass
    MATS[key] = m
    return m


# ---------------------------------------------------------------- stalk layout
LENS = [0.03, 0.05, 0.07, 0.10, 0.13, 0.15, 0.17, 0.18, 0.18, 0.18, 0.17, 0.17, 0.16, 0.15, 0.14]
NODE_Z = [0.02]
for _L in LENS:
    NODE_Z.append(NODE_Z[-1] + _L)
NN = len(NODE_Z)
Z_TOP = NODE_Z[-1]
Z_TB = Z_TOP + 0.20
ZIG = [0.0022 * (1 if i % 2 == 0 else -1) for i in range(NN)]
R_TAB = [(-0.08, 0.012), (-0.05, 0.016), (0.0, 0.018), (0.3, 0.016), (1.0, 0.013),
         (1.6, 0.0105), (2.05, 0.0078), (2.25, 0.005)]


def center(z):
    if z <= NODE_Z[0]:
        return Vector((ZIG[0], 0.0, z))
    if z >= Z_TOP:
        return Vector((ZIG[-1], 0.0, z))
    for i in range(NN - 1):
        if NODE_Z[i] <= z <= NODE_Z[i + 1]:
            t = (z - NODE_Z[i]) / (NODE_Z[i + 1] - NODE_Z[i])
            return Vector((ZIG[i] * (1 - t) + ZIG[i + 1] * t, 0.0, z))
    return Vector((ZIG[-1], 0.0, z))


def stalk_r(z):
    r = interp(R_TAB, z)
    for zn in NODE_Z:
        r += 0.0012 * math.exp(-((z - zn) / 0.007) ** 2)
    return r


zs = []
zz = -0.075
while zz < Z_TB:
    zs.append(zz)
    zz += 0.004
zs.append(Z_TB)
s_pts = [center(z) for z in zs]
s_rad = [stalk_r(z) for z in zs]
for k in range(len(zs)):
    if zs[k] < -0.06:
        s_rad[k] *= math.sqrt(max(0.05, 1 - ((-0.06 - zs[k]) / 0.016) ** 2))
tube(B('stalk'), s_pts, s_rad, seg=14, cap_start=True, cap_end=False)


# ---------------------------------------------------------------- leaves
def sheath_r(i, z):
    r = stalk_r(z) + 0.0016
    for k in range(i + 1, NN):
        r += 0.0024 * sstep(NODE_Z[k] - 0.004, NODE_Z[k] + 0.004, z)
    return r


def build_sheath(i, z0, z1, az):
    nz, na = 12, 28
    span = TAU + 0.45
    verts, faces = [], []
    for a_ in range(nz + 1):
        z = z0 + (z1 - z0) * a_ / nz
        c = center(z)
        rb = sheath_r(i, z)
        for j in range(na + 1):
            f = j / na
            ang = az - span / 2 + span * f
            r = rb * (1.0 + 0.09 * f)
            verts.append(c + Vector((math.cos(ang) * r, math.sin(ang) * r, 0.0)))
    for a_ in range(nz):
        for j in range(na):
            p = a_ * (na + 1) + j
            faces.append((p, p + 1, p + na + 2, p + na + 1))
    B('sheath').add(verts, faces)


def build_ligule(i, zc, az):
    na = 16
    span = 4.2
    verts, faces = [], []
    r = sheath_r(i, zc) * 1.045 - 0.0006
    for a_ in range(2):
        z = zc + 0.008 * a_
        c = center(z)
        for j in range(na + 1):
            ang = az - span / 2 + span * j / na
            verts.append(c + Vector((math.cos(ang) * r, math.sin(ang) * r, 0.0)))
    for j in range(na):
        faces.append((j, j + 1, j + na + 2, j + na + 1))
    B('ligule').add(verts, faces)


def blade_w(u, W):
    if u < 0.45:
        return W * (0.6 + 0.4 * math.sin(u / 0.45 * math.pi / 2))
    return W * max(0.0, (1 - u) / 0.55) ** 0.8


def build_blade(key, base, d, L, W, elev0, droop, lat, twist, wave, nu=40, nv=10):
    side0 = Vector((-d.y, d.x, 0.0))
    ds = L / nu
    pts = [base.copy()]
    pos = base.copy()
    for k in range(nu):
        u = k / nu
        th = elev0 - droop * (u ** 1.5)
        T = d * math.cos(th) + ZUP * math.sin(th) + side0 * (lat * u)
        T.normalize()
        pos = pos + T * ds
        if pos.z < 0.003:
            pos.z = 0.003
        pts.append(pos.copy())
    tans = []
    for k in range(nu + 1):
        if k == 0:
            t = pts[1] - pts[0]
        elif k == nu:
            t = pts[-1] - pts[-2]
        else:
            t = pts[k + 1] - pts[k - 1]
        if t.length < 1e-9:
            t = d.copy()
        tans.append(t.normalized())
    verts, faces = [], []
    frames = []
    ph1 = random.uniform(0, TAU)
    ph2 = random.uniform(0, TAU)
    for k in range(nu + 1):
        u = k / nu
        T = tans[k]
        S = ZUP.cross(T)
        if S.length < 1e-6:
            S = side0.copy()
        S.normalize()
        N = T.cross(S).normalized()
        rot = Matrix.Rotation(twist * u, 3, T)
        S = rot @ S
        N = rot @ N
        frames.append((T, S, N))
        w = max(blade_w(u, W), 0.0006)
        C = pts[k]
        cup = 1.0 - sstep(0.0, 0.06, u)
        for jv in range(nv + 1):
            v = -1.0 + 2.0 * jv / nv
            av = abs(v)
            ph = ph1 if v > 0 else ph2
            wav = wave * av ** 3 * math.sin(u * L * TAU / 0.07 + ph) * min(1.0, 1.5 * w / W)
            off = 0.10 * av * w * 0.5 + 0.35 * av * av * w * cup + wav
            verts.append(C + S * (v * w * 0.5) + N * off)
    row = nv + 1
    for k in range(nu):
        for j in range(nv):
            p = k * row + j
            faces.append((p, p + 1, p + row + 1, p + row))
    B(key).add(verts, faces)
    mpts, mrad = [], []
    kmax = int(nu * 0.9)
    for k in range(kmax + 1):
        u = k / nu
        T, S, N = frames[k]
        mpts.append(pts[k] - N * 0.0008)
        mrad.append(0.0026 * (1 - u) ** 1.2 + 0.0003)
    tube(B('midrib' if key == 'leaf' else 'deadleaf'), mpts, mrad, seg=5, cap_start=False, cap_end=True)


LEAF_AZ = []
for i in range(NN):
    az = (0.0 if i % 2 == 0 else math.pi) + random.uniform(-0.12, 0.12) + 0.03 * i
    LEAF_AZ.append(az)

for i in range(NN):
    az = LEAF_AZ[i]
    d = Vector((math.cos(az), math.sin(az), 0.0))
    zn = NODE_Z[i]
    sl = max(0.06, 0.85 * LENS[i]) if i < NN - 1 else 0.13
    zc = zn + sl
    build_sheath(i, zn, zc, az)
    build_ligule(i, zc, az)
    base = center(zc) + d * (sheath_r(i, zc) * 1.045)
    L = 0.22 + 0.72 * math.exp(-((i - 9) / 5.0) ** 2)
    W = 0.03 + 0.065 * math.exp(-((i - 9) / 5.5) ** 2)
    elev0 = math.radians(30 + 2.5 * i)
    droop = math.radians(35 + 75 * (L - 0.2) / 0.75)
    dead = i <= 1
    if dead:
        droop += math.radians(30)
    build_blade('deadleaf' if dead else 'leaf', base, d, L, W, elev0, droop,
                lat=random.uniform(-0.15, 0.15), twist=random.uniform(-0.5, 0.5),
                wave=0.006 if dead else 0.0035)

# ---------------------------------------------------------------- tassel
LT = 0.34
NOD = Vector((math.cos(0.7), math.sin(0.7), 0.0))
T_BASE = center(Z_TB)


def spike_pt(s):
    u = s / LT
    return T_BASE + ZUP * s + NOD * (0.06 * u ** 2.2)


def add_anthers(c, axis):
    if random.random() > 0.4:
        return
    tip = c + axis * 0.0042
    for _a in range(random.choice([1, 2, 3])):
        off = Vector((random.uniform(-1, 1) * 0.0015, random.uniform(-1, 1) * 0.0015, 0.0))
        fe = tip + off + Vector((0, 0, -random.uniform(0.004, 0.008)))
        mid = (tip + fe) * 0.5 + off * 0.3
        tube(B('filament'), [tip, mid, fe], [0.00018] * 3, seg=3, cap_start=False, cap_end=False)
        ellipsoid(B('anther'), trs(fe + Vector((0, 0, -0.0022)), ZUP, 0.0007, 0.0007, 0.0024))


def add_spikelet(c, axis):
    ellipsoid(B('spikelet'), trs(c, axis, 0.0019, 0.0019, 0.0045, random.uniform(0, TAU)))
    add_anthers(c, axis)


cp = [spike_pt(LT * k / 40) for k in range(41)]
cr = [0.0045 * (1 - k / 40) + 0.0009 for k in range(41)]
tube(B('tassel'), cp, cr, seg=8, cap_start=False, cap_end=True)
s = 0.10
lvl = 0
while s < LT - 0.005:
    P = spike_pt(s)
    T = (spike_pt(min(LT, s + 0.002)) - spike_pt(max(0.0, s - 0.002))).normalized()
    S1 = T.cross(Vector((1, 0, 0))).normalized()
    S2 = T.cross(S1).normalized()
    ra = 0.0045 * (1 - s / LT) + 0.0009
    for q in range(4):
        a = lvl * 0.785 + q * TAU / 4
        off = S1 * math.cos(a) + S2 * math.sin(a)
        add_spikelet(P + off * (ra + 0.0018) + T * 0.003, (T + off * 0.35).normalized())
    s += 0.0065
    lvl += 1

NB = 11
for i in range(NB):
    s_i = 0.005 + i * 0.008
    az = i * 2.4 + 0.3
    Lb = 0.25 - 0.012 * i
    phi0 = math.radians(50 - 2 * i)
    dr = 0.9 - 0.04 * i
    N = 40
    p = spike_pt(s_i)
    bp = [p.copy()]
    for k in range(N):
        u = k / N
        phi = phi0 + dr * u ** 1.5
        dv = Vector((math.sin(phi) * math.cos(az), math.sin(phi) * math.sin(az), math.cos(phi)))
        p = p + dv * (Lb / N)
        bp.append(p.copy())
    brad = [0.0018 * (1 - k / N) + 0.0006 for k in range(N + 1)]
    tube(B('tassel'), bp, brad, seg=6, cap_start=False, cap_end=True)
    for k in range(6, N):
        T = (bp[k + 1] - bp[k - 1]).normalized()
        Sv = T.cross(ZUP)
        if Sv.length < 1e-6:
            Sv = Vector((1, 0, 0))
        Sv.normalize()
        Dv = Sv.cross(T)
        if Dv.z > 0:
            Dv = -Dv
        for sgn in (-1, 1):
            off = (Dv * 0.5 + Sv * (0.87 * sgn)).normalized()
            add_spikelet(bp[k] + off * (brad[k] + 0.0018) + T * 0.002, (T + off * 0.35).normalized())


# ---------------------------------------------------------------- ears
def build_ear(node_i, az_off, Lc, R, rows, shank_len, tilt, n_husk, n_silk):
    az = LEAF_AZ[node_i] + az_off
    d = Vector((math.cos(az), math.sin(az), 0.0))
    z0 = NODE_Z[node_i] + 0.012
    start = center(z0) + d * (stalk_r(z0) * 0.8)
    t1 = math.radians(tilt * 0.7)
    sdir = (d * math.sin(t1) + ZUP * math.cos(t1)).normalized()
    axis = (d * math.sin(math.radians(tilt)) + ZUP * math.cos(math.radians(tilt))).normalized()
    shank_end = start + sdir * shank_len
    spts = [start.lerp(shank_end, k / 8) for k in range(9)]
    tube(B('stalk'), spts, [0.0075 + 0.001 * k / 8 for k in range(9)], seg=10,
         cap_start=False, cap_end=False)
    M = Matrix.Translation(shank_end) @ align_z(axis)

    def rc(t):
        t = clamp(t)
        return R * (0.82 + 0.18 * sstep(0.0, 0.15, t)) * (1 - 0.42 * t ** 2.5)

    cpts = [M @ Vector((0, 0, Lc * k / 12)) for k in range(13)]
    tube(B('cob'), cpts, [0.55 * rc(k / 12) for k in range(13)], seg=12)
    nk = int(Lc / 0.0068)
    for j in range(rows):
        pair = j // 2
        ang0 = TAU * pair / (rows // 2) + (j % 2) * (TAU / rows) * 0.9
        for k in range(nk):
            t = (k + 0.5) / nk * 0.97
            r = rc(t)
            ang = ang0 + (k % 2) * 0.03
            pos = Vector((math.cos(ang) * r * 0.77, math.sin(ang) * r * 0.77, t * Lc))
            Mk = (Matrix.Translation(pos) @ Matrix.Rotation(ang, 4, 'Z')
                  @ Matrix.Diagonal(Vector((0.23 * r, math.pi * r / rows * 0.95,
                                            Lc / nk * 0.52, 1.0))))
            ellipsoid(B('kernel'), M @ Mk)

    rc1 = rc(1.0)
    for h in range(n_husk):
        off = 0.0011 * (h + 1)
        zb = -0.003 - 0.004 * h
        ztip = Lc + 0.035 + 0.007 * h
        a0 = h * 2.4
        span0 = 3.4
        nz, na = 26, 14
        verts, faces = [], []
        for a_ in range(nz + 1):
            z = zb + (ztip - zb) * a_ / nz
            if z < 0:
                e = 0.0085 + (rc(0.0) + 0.003 - 0.0085) * sstep(-0.025, 0.0, z)
            elif z <= Lc:
                e = rc(z / Lc) + 0.003
            else:
                f = clamp((z - Lc) / (ztip - Lc))
                e = 0.0025 + (rc1 + 0.003 - 0.0025) * (1 - f) ** 0.8
            r = e + off
            if h >= n_husk - 3:
                r += 0.006 * sstep(Lc + 0.01, ztip, z) ** 2
            span = span0 * (1 - 0.75 * sstep(Lc * 0.85, ztip, z))
            for j in range(na + 1):
                a = a0 + span * (j / na - 0.5)
                verts.append(M @ Vector((math.cos(a) * r, math.sin(a) * r, z)))
        for a_ in range(nz):
            for j in range(na):
                p = a_ * (na + 1) + j
                faces.append((p, p + 1, p + na + 2, p + na + 1))
        B('husk').add(verts, faces)

    gl = (M.to_3x3().inverted() @ Vector((0, 0, -1))).normalized()
    ztip_max = Lc + 0.035 + 0.007 * (n_husk - 1)
    for _s in range(n_silk):
        phi = random.uniform(0, TAU)
        rr = random.uniform(0, 0.0025)
        p = Vector((rr * math.cos(phi), rr * math.sin(phi), Lc * 0.97))
        lat = Vector((math.cos(phi), math.sin(phi), 0.0))
        dv = Vector((0, 0, 1))
        pts = [p.copy()]
        ext = random.uniform(0.05, 0.17)
        out_len = 0.0
        for _k in range(45):
            if p.z < ztip_max - 0.005:
                dv = (Vector((0, 0, 1)) + lat * 0.04).normalized()
                step = 0.01
            else:
                jit = Vector((random.uniform(-0.08, 0.08), random.uniform(-0.08, 0.08),
                              random.uniform(-0.08, 0.08)))
                dv = (dv + lat * 0.22 + gl * 0.16 + jit).normalized()
                step = 0.008
                out_len += step
                if out_len > ext:
                    break
            p = p + dv * step
            pts.append(p.copy())
        if len(pts) >= 2:
            tube(B('silk'), [M @ q for q in pts], [0.00055] * len(pts), seg=4,
                 cap_start=False, cap_end=False)


build_ear(9, 0.45, 0.19, 0.022, 16, 0.10, 22, 9, 70)
build_ear(8, -0.45, 0.12, 0.015, 14, 0.07, 26, 7, 35)


# ---------------------------------------------------------------- roots
def root_path(start, dir0, length, step, grav, jitter):
    pts = [start.copy()]
    dv = dir0.normalized()
    p = start.copy()
    n = max(2, int(length / step))
    for _k in range(n):
        jit = Vector((random.uniform(-jitter, jitter), random.uniform(-jitter, jitter),
                      random.uniform(-jitter, jitter)))
        dv = (dv + Vector((0, 0, -grav)) + jit).normalized()
        p = p + dv * step
        pts.append(p.copy())
    return pts


def add_laterals(pts, every=3):
    for k in range(3, len(pts) - 2, every):
        T = (pts[k + 1] - pts[k - 1]).normalized()
        a = random.uniform(0, TAU)
        hv = Vector((math.cos(a), math.sin(a), -0.3))
        hv = hv - T * hv.dot(T)
        if hv.length < 1e-6:
            continue
        lp = root_path(pts[k], hv, random.uniform(0.03, 0.10), 0.012, 0.02, 0.2)
        n = len(lp)
        tube(B('root'), lp, [0.0009 - 0.0006 * q / (n - 1) for q in range(n)], seg=4,
             cap_start=False, cap_end=True)


def main_root(start, dir0, length, r0, r1, key='root'):
    pts = root_path(start, dir0, length, 0.02, 0.06, 0.12)
    n = len(pts)
    tube(B(key), pts, [r0 + (r1 - r0) * q / (n - 1) for q in range(n)], seg=6,
         cap_start=False, cap_end=True)
    add_laterals(pts)


for k in range(20):
    az = TAU * k / 20 + random.uniform(-0.15, 0.15)
    z = random.uniform(-0.05, -0.02)
    d = Vector((math.cos(az), math.sin(az), 0.0))
    st = center(z) + d * stalk_r(z) * 0.8
    dn = math.tan(math.radians(random.uniform(10, 40)))
    main_root(st, d + Vector((0, 0, -dn)), random.uniform(0.35, 0.7), 0.0028, 0.0007)

main_root(Vector((ZIG[0], 0, -0.075)), Vector((0, 0, -1)), 0.8, 0.0022, 0.0006)
for k in range(4):
    az = TAU * k / 4 + 0.4
    main_root(Vector((ZIG[0], 0, -0.07)), Vector((math.cos(az), math.sin(az), -1.0)),
              0.45, 0.0018, 0.0006)

for ni, cnt in ((1, 9), (2, 11)):
    zn = NODE_Z[ni]
    for k in range(cnt):
        az = TAU * k / cnt + random.uniform(-0.2, 0.2) + ni * 0.3
        d = Vector((math.cos(az), math.sin(az), 0.0))
        P0 = center(zn) + d * stalk_r(zn) * 0.9 + Vector((0, 0, 0.003))
        P1 = P0 + d * (0.05 + 0.03 * random.random()) + Vector((0, 0, -0.005))
        P2 = center(0.0) + d * (0.10 + 0.08 * random.random() + 0.04 * ni) + Vector((0, 0, -0.08))
        bez = []
        for q in range(15):
            t = q / 14
            bez.append(P0 * (1 - t) ** 2 + P1 * (2 * (1 - t) * t) + P2 * (t * t))
        under = root_path(P2, (P2 - bez[-2]).normalized(), 0.12, 0.02, 0.1, 0.1)
        pts = bez + under[1:]
        n = len(pts)
        tube(B('braceroot'), pts, [0.0042 - 0.003 * q / (n - 1) for q in range(n)], seg=7,
             cap_start=False, cap_end=True)
        add_laterals(under, every=2)

# ---------------------------------------------------------------- objects
scene_coll = bpy.context.scene.collection
root_obj = bpy.data.objects.new("Zea_mays", None)
scene_coll.objects.link(root_obj)
SOLID = {'leaf': 0.0006, 'deadleaf': 0.0005, 'sheath': 0.0006, 'husk': 0.0005, 'ligule': 0.0003}

for key, b in BUILD.items():
    if not b.v:
        continue
    me = bpy.data.meshes.new("Maize_" + key)
    me.from_pydata(b.v, [], b.f)
    try:
        me.validate(clean_customdata=False)
    except Exception:
        pass
    me.update()
    try:
        me.shade_smooth()
    except Exception:
        try:
            me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
        except Exception:
            pass
    ob = bpy.data.objects.new("Maize_" + key, me)
    scene_coll.objects.link(ob)
    try:
        me.materials.append(get_mat(key))
    except Exception:
        pass
    ob.parent = root_obj
    if key in SOLID:
        try:
            mod = ob.modifiers.new("Solidify", 'SOLIDIFY')
            mod.thickness = SOLID[key]
            mod.offset = 0.0
        except Exception:
            pass