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

random.seed(7)
TAU = 2.0 * math.pi

# ------------------------------------------------------------------
# 0. Basic cleanup of default scene cube (not end-of-script clearing)
# ------------------------------------------------------------------
try:
    for nm in ("Cube",):
        ob = bpy.data.objects.get(nm)
        if ob is not None:
            bpy.data.objects.remove(ob, do_unlink=True)
except Exception as e:
    print("cleanup warning:", e)

# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def clamp(x, a=0.0, b=1.0):
    return max(a, min(b, x))


def smoothstep(e0, e1, x):
    if e1 == e0:
        return 0.0
    t = clamp((x - e0) / (e1 - e0))
    return t * t * (3.0 - 2.0 * t)


def _fallback_noise(x, y, z):
    return (math.sin(x * 1.7 + y * 0.3 + 1.1) * math.sin(y * 1.3 + z * 0.7 + 0.4)
            + 0.5 * math.sin(x * 3.1 - z * 2.3 + 2.0)) / 1.5


try:
    from mathutils import noise as _mnoise
    _mnoise.noise(Vector((0.1, 0.2, 0.3)))

    def nz(x, y, z):
        return _mnoise.noise(Vector((x, y, z)))
except Exception:
    def nz(x, y, z):
        return _fallback_noise(x, y, z)


def rand_vec(s):
    return Vector((random.uniform(-s, s), random.uniform(-s, s), random.uniform(-s, s)))


def bend(z):
    # gentle lean of the whole stipe
    return 0.25 * (max(z, 0.0) / 19.0) ** 2


def catmull(pts, sub):
    out = []
    n = len(pts)
    for i in range(n - 1):
        p0 = pts[max(i - 1, 0)]
        p1 = pts[i]
        p2 = pts[i + 1]
        p3 = pts[min(i + 2, n - 1)]
        for j in range(sub):
            t = j / float(sub)
            t2 = t * t
            t3 = t2 * t
            out.append(tuple(
                0.5 * ((2.0 * p1[k]) + (-p0[k] + p2[k]) * t
                       + (2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2
                       + (-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3)
                for k in range(2)))
    out.append((float(pts[-1][0]), float(pts[-1][1])))
    return out


def lathe(prof, n, fn, cyclic=False, colfn=None):
    bm = bmesh.new()
    rings = []
    cols = [] if colfn else None
    last = len(prof) - 1
    for i, (r, z) in enumerate(prof):
        is_pole = (not cyclic) and (i == 0 or i == last) and r < 1e-6
        if is_pole:
            v = bm.verts.new(fn(0.0, 0.0, z, i))
            rings.append([v])
            if colfn:
                cols.append(colfn(i, 0.0))
        else:
            rr = max(r, 0.01)
            ring = []
            for k in range(n):
                th = TAU * k / n
                ring.append(bm.verts.new(fn(rr, th, z, i)))
                if colfn:
                    cols.append(colfn(i, th))
            rings.append(ring)

    def connect(a, b):
        if len(a) == 1 and len(b) == 1:
            return
        if len(a) == 1:
            for k in range(len(b)):
                try:
                    bm.faces.new((a[0], b[(k + 1) % len(b)], b[k]))
                except Exception:
                    pass
        elif len(b) == 1:
            for k in range(len(a)):
                try:
                    bm.faces.new((a[k], a[(k + 1) % len(a)], b[0]))
                except Exception:
                    pass
        else:
            m = len(a)
            for k in range(m):
                try:
                    bm.faces.new((a[k], a[(k + 1) % m], b[(k + 1) % m], b[k]))
                except Exception:
                    pass

    for i in range(len(rings) - 1):
        connect(rings[i], rings[i + 1])
    if cyclic and len(rings) > 2:
        connect(rings[-1], rings[0])
    return bm, cols


def tube(bm, pts, radii, sides):
    rings = []
    prev_n = None
    npts = len(pts)
    for i, p in enumerate(pts):
        if i == 0:
            tng = pts[1] - pts[0]
        elif i == npts - 1:
            tng = pts[-1] - pts[-2]
        else:
            tng = pts[i + 1] - pts[i - 1]
        if tng.length < 1e-8:
            tng = Vector((0.0, 0.0, 1.0))
        tng = tng.normalized()
        if prev_n is None:
            a = Vector((0.0, 0.0, 1.0)) if abs(tng.z) < 0.9 else Vector((1.0, 0.0, 0.0))
            nrm = tng.cross(a)
        else:
            nrm = prev_n - tng * prev_n.dot(tng)
        if nrm.length < 1e-8:
            nrm = tng.orthogonal()
        nrm = nrm.normalized()
        prev_n = nrm
        bnm = tng.cross(nrm).normalized()
        ring = []
        for k in range(sides):
            ang = TAU * k / sides
            ring.append(bm.verts.new(p + (nrm * math.cos(ang) + bnm * math.sin(ang)) * radii[i]))
        rings.append(ring)
    for i in range(len(rings) - 1):
        a = rings[i]
        b = rings[i + 1]
        for k in range(sides):
            try:
                bm.faces.new((a[k], a[(k + 1) % sides], b[(k + 1) % sides], b[k]))
            except Exception:
                pass
    try:
        bm.faces.new(list(reversed(rings[0])))
    except Exception:
        pass
    try:
        bm.faces.new(rings[-1])
    except Exception:
        pass


# ------------------------------------------------------------------
# Collection + root
# ------------------------------------------------------------------
coll = bpy.data.collections.new("Amanita_muscaria_specimen")
bpy.context.scene.collection.children.link(coll)
root = bpy.data.objects.new("Amanita_muscaria", None)
coll.objects.link(root)
ROOT_NAME = root.name

# ------------------------------------------------------------------
# Materials (minimal, defensive)
# ------------------------------------------------------------------
def make_mat(name, color, rough=0.6, sss=0.0, use_vcol=False):
    mat = bpy.data.materials.new(name)
    try:
        mat.use_nodes = True
        nt = mat.node_tree
        bsdf = None
        for nd in nt.nodes:
            if nd.type == 'BSDF_PRINCIPLED':
                bsdf = nd
                break
        if bsdf is None:
            return mat
        try:
            bsdf.inputs['Base Color'].default_value = (color[0], color[1], color[2], 1.0)
        except Exception:
            pass
        try:
            bsdf.inputs['Roughness'].default_value = float(rough)
        except Exception:
            pass
        if sss > 0.0:
            try:
                bsdf.inputs['Subsurface Weight'].default_value = float(sss)
            except Exception:
                pass
            try:
                bsdf.inputs['Subsurface Radius'].default_value = (0.3, 0.15, 0.1)
            except Exception:
                pass
        if use_vcol:
            try:
                vc = nt.nodes.new('ShaderNodeVertexColor')
                vc.layer_name = "Col"
                vc.location = (-400, 200)
                nt.links.new(vc.outputs['Color'], bsdf.inputs['Base Color'])
            except Exception as e:
                print("vcol node warning:", e)
    except Exception as e:
        print("material warning:", name, e)
    try:
        mat.diffuse_color = (color[0], color[1], color[2], 1.0)
    except Exception:
        pass
    return mat


mat_cap = make_mat("Amanita_Cap_Pileus", (0.8, 0.08, 0.02), 0.3, 0.05, use_vcol=True)
mat_wart = make_mat("Amanita_Warts_UniversalVeil", (0.95, 0.93, 0.85), 0.9, 0.05)
mat_gill = make_mat("Amanita_Gills", (0.96, 0.95, 0.89), 0.7, 0.1)
mat_stem = make_mat("Amanita_Stipe", (0.94, 0.93, 0.86), 0.65, 0.1)
mat_ring = make_mat("Amanita_Annulus", (0.92, 0.9, 0.8), 0.7, 0.05, use_vcol=True)
mat_volva = make_mat("Amanita_Volva", (0.9, 0.88, 0.78), 0.85, 0.05)
mat_myc = make_mat("Amanita_Mycelium", (0.92, 0.9, 0.85), 0.9, 0.0)


def finish(bm, name, mat, cols, smooth=True):
    try:
        bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
    except Exception:
        pass
    me = bpy.data.meshes.new(name)
    bm.to_mesh(me)
    bm.free()
    if cols is not None and len(cols) == len(me.vertices):
        try:
            attr = me.color_attributes.new(name="Col", type='FLOAT_COLOR', domain='POINT')
            flat = []
            for c in cols:
                flat.extend((c[0], c[1], c[2], 1.0))
            attr.data.foreach_set("color", flat)
        except Exception as e:
            print("color attribute warning:", e)
    if smooth:
        try:
            me.polygons.foreach_set("use_smooth", [True] * len(me.polygons))
        except Exception:
            try:
                me.shade_smooth()
            except Exception:
                pass
    me.update()
    obj = bpy.data.objects.new(name, me)
    coll.objects.link(obj)
    try:
        obj.parent = bpy.data.objects[ROOT_NAME]
    except Exception:
        pass
    try:
        me.materials.append(mat)
    except Exception:
        pass
    return obj


# ------------------------------------------------------------------
# 1. PILEUS (cap) geometry definition
# ------------------------------------------------------------------
R = 7.0
RING_Z = 16.0
CAP_UND0 = 18.95
BX = bend(19.0)


def z_und(u):
    return CAP_UND0 - 1.9 * (max(u, 0.0) ** 1.7)


def cap_thick(u):
    return 2.2 * max(0.0, 1.0 - u * u) ** 0.8 + 0.2


def z_top(u):
    return z_und(u) + cap_thick(u)


def cap_off(r, th):
    u = r / R
    c = math.cos(th)
    s = math.sin(th)
    return (u * 0.14 * nz(c * u * 1.5, s * u * 1.5, 0.7)
            + 0.14 * u ** 4 * math.sin(5.0 * th + 0.7)
            + 0.05 * u ** 3 * math.sin(11.0 * th + 2.0))


def cap_xyz(r, th, z, upper):
    u = r / R
    c = math.cos(th)
    s = math.sin(th)
    rr = r * (1.0 + 0.012 * u ** 3 * math.sin(7.0 * th + 1.3))
    zz = z + cap_off(r, th)
    if upper:
        st = smoothstep(0.84, 1.0, u)
        zz -= 0.035 * st * (0.5 + 0.5 * math.sin(60.0 * th + 0.8 * nz(c * 4.0, s * 4.0, 2.0)))
    return Vector((rr * c + BX, rr * s, zz))


# ------------------------------------------------------------------
# 2. Build CAP with color gradient
# ------------------------------------------------------------------
cap_prof = []
cap_info = []
cap_prof.append((0.0, z_top(0.0)))
cap_info.append((0.0, 'top'))
NT = 50
for i in range(1, NT + 1):
    t = i / float(NT)
    u = 1.0 - (1.0 - t) ** 1.3
    cap_prof.append((u * R, z_top(u)))
    cap_info.append((u, 'top'))
zt1 = z_top(1.0)
zu1 = z_und(1.0)
cap_prof.append((R + 0.06, zt1 - 0.04))
cap_info.append((1.0, 'margin'))
cap_prof.append((R + 0.1, 0.5 * (zt1 + zu1)))
cap_info.append((1.0, 'margin'))
cap_prof.append((R + 0.06, zu1 + 0.04))
cap_info.append((1.0, 'margin'))
NU = 40
for i in range(NU, 0, -1):
    t = i / float(NU)
    u = 1.0 - (1.0 - t) ** 1.3
    cap_prof.append((u * R, z_und(u)))
    cap_info.append((u, 'under'))
cap_prof.append((0.0, z_und(0.0)))
cap_info.append((0.0, 'under'))


def lerp3(a, b, t):
    return tuple(a[k] * (1.0 - t) + b[k] * t for k in range(3))


def cap_col(i, th):
    u, reg = cap_info[i]
    if reg == 'under':
        return (0.94, 0.92, 0.85)
    if reg == 'margin':
        return (0.92, 0.5, 0.12)
    deep = (0.5, 0.02, 0.015)
    scar = (0.82, 0.07, 0.02)
    oran = (0.93, 0.36, 0.06)
    if u < 0.45:
        c = lerp3(deep, scar, smoothstep(0.0, 0.45, u))
    else:
        c = lerp3(scar, oran, smoothstep(0.55, 1.0, u))
    v = 0.06 * nz(math.cos(th) * 2.0 * u, math.sin(th) * 2.0 * u, 3.3)
    return (clamp(c[0] + v), clamp(c[1] + v * 0.3), clamp(c[2]))


def cap_fn(r, th, z, i):
    return cap_xyz(r, th, z, cap_info[i][1] == 'top')


bm, cols = lathe(cap_prof, 256, cap_fn, cyclic=False, colfn=cap_col)
finish(bm, "Pileus_Cap", mat_cap, cols, smooth=True)

# ------------------------------------------------------------------
# 2b. Universal veil warts on cap surface
# ------------------------------------------------------------------
def top_pt(r, th):
    return cap_xyz(r, th, z_top(r / R), True)


bm = bmesh.new()
placed = []
target = 230
attempts = 0
while len(placed) < target and attempts < 4000:
    attempts += 1
    u = math.sqrt(random.random()) * 0.92
    th = random.random() * TAU
    s = (0.34 - 0.2 * u) * random.uniform(0.6, 1.3)
    r = max(u * R, 0.2)
    p = top_pt(r, th)
    ok = True
    for (q, sq) in placed:
        dx = p.x - q.x
        dy = p.y - q.y
        if dx * dx + dy * dy < (0.75 * (s + sq)) ** 2:
            ok = False
            break
    if not ok:
        continue
    placed.append((p, s))
    pr = top_pt(r + 0.02, th) - top_pt(max(r - 0.02, 0.01), th)
    pt = top_pt(r, th + 0.01) - top_pt(r, th - 0.01)
    nvec = pr.cross(pt)
    if nvec.length < 1e-9:
        nvec = Vector((0.0, 0.0, 1.0))
    nvec = nvec.normalized()
    if nvec.z < 0:
        nvec = -nvec
    tg = pt.normalized() if pt.length > 1e-9 else Vector((1.0, 0.0, 0.0))
    tg = (tg - nvec * tg.dot(nvec))
    if tg.length < 1e-9:
        tg = nvec.orthogonal()
    tg = tg.normalized()
    bn = nvec.cross(tg).normalized()
    h = random.uniform(0.35, 0.55)
    rot = Matrix(((tg.x, bn.x, nvec.x), (tg.y, bn.y, nvec.y), (tg.z, bn.z, nvec.z)))
    sc = Matrix.Diagonal((s * random.uniform(0.9, 1.3), s * random.uniform(0.8, 1.2), s * h))
    M = (rot @ sc).to_4x4()
    M.translation = p + nvec * (s * h * 0.25)
    try:
        ret = bmesh.ops.create_icosphere(bm, subdivisions=2, radius=1.0, matrix=M, calc_uvs=False)
        for v in ret['verts']:
            v.co += rand_vec(0.1 * s)
    except Exception as e:
        print("wart warning:", e)
finish(bm, "Universal_Veil_Warts", mat_wart, None, smooth=True)

# ------------------------------------------------------------------
# 3. LAMELLAE (free, crowded white gills with lamellulae)
# ------------------------------------------------------------------
bm = bmesh.new()
NF = 80
step = TAU / NF
gill_specs = []
for k in range(NF):
    base = step * k
    gill_specs.append((base, 1.25, 0.95, 'full'))
    gill_specs.append((base + step * 0.5, R * 0.45, 0.85, 'l1'))
    gill_specs.append((base + step * 0.25, R * 0.72, 0.6, 'l2'))
    gill_specs.append((base + step * 0.75, R * 0.72, 0.6, 'l2'))

r_end = R - 0.12
MG = 26
for th0, r0, dmax, kind in gill_specs:
    th0 += random.uniform(-0.06, 0.06) * step
    if kind != 'full':
        r0 *= random.uniform(0.94, 1.06)
    dmax *= random.uniform(0.92, 1.08)
    va, vb, vc, vd = [], [], [], []
    for j in range(MG):
        s = j / (MG - 1)
        r = r0 + (r_end - r0) * s
        u = r / R
        zu = z_und(u)
        if kind == 'full':
            prof = (4.0 * s * (1.0 - s)) ** 0.55
        else:
            prof = min(1.0, s / 0.06) ** 0.5 * (1.0 - s) ** 0.55
        d = max(0.05, dmax * prof) * (1.0 + 0.04 * math.sin(23.0 * s + th0 * 7.0))
        th = th0 + 0.004 * math.sin(9.0 * s + th0 * 5.0)
        ht = 0.035
        hb = 0.018
        top_z = zu + 0.1
        bot_z = zu - d
        va.append(bm.verts.new(cap_xyz(r, th - ht / r, top_z, False)))
        vb.append(bm.verts.new(cap_xyz(r, th - hb / r, bot_z, False)))
        vc.append(bm.verts.new(cap_xyz(r, th + ht / r, top_z, False)))
        vd.append(bm.verts.new(cap_xyz(r, th + hb / r, bot_z, False)))
    try:
        for j in range(MG - 1):
            bm.faces.new((va[j], va[j + 1], vb[j + 1], vb[j]))
            bm.faces.new((vc[j], vd[j], vd[j + 1], vc[j + 1]))
            bm.faces.new((vb[j], vb[j + 1], vd[j + 1], vd[j]))
            bm.faces.new((va[j], vc[j], vc[j + 1], va[j + 1]))
        bm.faces.new((va[0], vb[0], vd[0], vc[0]))
        bm.faces.new((va[-1], vc[-1], vd[-1], vb[-1]))
    except Exception as e:
        print("gill face warning:", e)
finish(bm, "Lamellae_Gills", mat_gill, None, smooth=True)

# ------------------------------------------------------------------
# 4. STIPE (stem) with basal bulb
# ------------------------------------------------------------------
stem_ctrl = [(0.0, 0.0), (0.8, 0.06), (1.45, 0.32), (1.9, 0.8), (2.18, 1.5),
             (2.3, 2.3), (2.22, 3.1), (1.98, 3.8), (1.68, 4.4), (1.46, 5.0),
             (1.33, 6.0), (1.24, 8.0), (1.16, 10.0), (1.09, 12.0), (1.03, 14.0),
             (0.99, 15.5), (0.96, 17.0), (0.98, 17.8), (1.06, 18.3), (0.92, 18.85),
             (0.0, 19.0)]
stem_prof = catmull(stem_ctrl, 5)


def stem_at(z):
    P = stem_prof
    for i in range(1, len(P) - 2):
        r1, z1 = P[i]
        r2, z2 = P[i + 1]
        if z2 > z1 and z1 <= z <= z2:
            t = (z - z1) / (z2 - z1)
            r = r1 + (r2 - r1) * t
            dr = r2 - r1
            dz = z2 - z1
            L = math.hypot(dr, dz)
            return r, dz / L, -dr / L
    return 1.0, 1.0, 0.0


def stem_xyz(r, th, z, i):
    c = math.cos(th)
    s = math.sin(th)
    n1 = nz(c * 1.3, s * 1.3, z * 0.45)
    sc = 0.0
    if 4.6 < z < RING_Z - 0.2:
        band = 0.5 + 0.5 * math.sin(z * 5.0 + 2.0 * n1)
        w = smoothstep(4.6, 5.5, z) * (1.0 - smoothstep(RING_Z - 1.0, RING_Z - 0.2, z))
        sc = 0.035 * w * band * nz(c * 3.0, s * 3.0, z * 2.2)
    fib = 0.006 * math.sin(48.0 * th + 3.0 * n1)
    rr = r * (1.0 + 0.02 * n1 + sc + fib)
    return Vector((rr * c + bend(z), rr * s, z))


bm, _ = lathe(stem_prof, 96, stem_xyz, cyclic=False)
finish(bm, "Stipe_Stem_BasalBulb", mat_stem, None, smooth=True)

# ------------------------------------------------------------------
# 5. ANNULUS (pendulous skirt-like ring)
# ------------------------------------------------------------------
outer_ctrl = [(0.93, 15.98), (1.12, 15.93), (1.42, 15.72), (1.7, 15.3),
              (1.9, 14.8), (2.02, 14.3), (2.08, 13.9)]
inner_ctrl = [(1.99, 13.88), (1.94, 14.3), (1.82, 14.78), (1.62, 15.2),
              (1.35, 15.52), (1.08, 15.72), (0.9, 15.82)]
ann_prof = catmull(outer_ctrl, 3) + [(2.05, 13.78)] + catmull(inner_ctrl, 3)


def ann_w(z):
    return clamp((RING_Z - z) / 2.2)


def ann_xyz(r, th, z, i):
    w = ann_w(z)
    c = math.cos(th)
    s = math.sin(th)
    rr = r + w ** 1.5 * (0.16 * math.sin(9 * th + 0.5) + 0.07 * math.sin(17 * th + 1.3)
                         + 0.05 * nz(c * 2.0, s * 2.0, 5.3))
    rr += 0.01 * math.sin(48 * th) * (1.0 - w)
    zz = z + w * w * (0.15 * math.sin(5 * th + 2.1) + 0.06 * math.sin(11 * th))
    return Vector((rr * c + bend(zz), rr * s, zz))


def ann_col(i, th):
    w = ann_w(ann_prof[i][1])
    e = smoothstep(0.8, 1.0, w)
    base = (0.85, 0.84, 0.78)
    edge = (0.85, 0.75, 0.4)
    return tuple(base[k] * (1 - e) + edge[k] * e for k in range(3))


bm, cols = lathe(ann_prof, 160, ann_xyz, cyclic=True, colfn=ann_col)
finish(bm, "Annulus_Ring", mat_ring, cols, smooth=True)

# ------------------------------------------------------------------
# 6. VOLVA remnants: concentric rings of warty scales on the bulb
# ------------------------------------------------------------------
bm = bmesh.new()
rows = [(4.35, 0.30, 0.08), (3.8, 0.27, 0.10), (3.25, 0.23, 0.15), (2.7, 0.17, 0.4)]
for zc, s0, skip in rows:
    r_here, nr, nzv = stem_at(zc)
    n = max(8, int(TAU * r_here / (s0 * 1.7)))
    off = random.random() * TAU
    for k in range(n):
        if random.random() < skip:
            continue
        th = off + TAU * k / n + random.uniform(-0.3, 0.3) * TAU / n
        z = zc + random.uniform(-0.08, 0.08)
        r_here, nr, nzv = stem_at(z)
        s = s0 * random.uniform(0.8, 1.25)
        c = math.cos(th)
        sn = math.sin(th)
        nvec = Vector((c * nr, sn * nr, nzv)).normalized()
        t = Vector((-sn, c, 0.0))
        b = t.cross(nvec)
        pos = Vector((r_here * c + bend(z), r_here * sn, z)) + nvec * (s * 0.25)
        rot = Matrix(((t.x, nvec.x, b.x), (t.y, nvec.y, b.y), (t.z, nvec.z, b.z)))
        sc = Matrix.Diagonal((s * random.uniform(1.1, 1.7), s * 0.6, s * 0.55))
        M = (rot @ sc).to_4x4()
        M.translation = pos
        try:
            ret = bmesh.ops.create_icosphere(bm, subdivisions=2, radius=1.0, matrix=M, calc_uvs=False)
            for v in ret['verts']:
                v.co += rand_vec(0.08 * s)
        except Exception as e:
            print("volva warning:", e)
finish(bm, "Volva_Remnant_Rings", mat_volva, None, smooth=True)

# ------------------------------------------------------------------
# 7. Basal mycelium / rhizomorph strands
# ------------------------------------------------------------------
bm = bmesh.new()


def strand(start, direction, length, r0, depth):
    pts = [start.copy()]
    d = direction.normalized()
    stp = 0.3
    nseg = max(3, int(length / stp))
    for k in range(nseg):
        d = (d + Vector((random.uniform(-0.35, 0.35), random.uniform(-0.35, 0.35),
                         random.uniform(-0.25, 0.15)))).normalized()
        pts.append(pts[-1] + d * stp)
    radii = [r0 * (1.0 - 0.8 * k / nseg) for k in range(nseg + 1)]
    try:
        tube(bm, pts, radii, 6)
    except Exception as e:
        print("strand warning:", e)
    if depth > 0:
        for _ in range(random.randint(1, 2)):
            mi = random.randint(1, max(1, nseg - 2))
            bd = (pts[mi + 1] - pts[mi]).normalized() + rand_vec(0.8)
            strand(pts[mi], bd, random.uniform(1.0, 2.0), radii[mi] * 0.6, depth - 1)


NS = 9
for k in range(NS):
    th = TAU * k / NS + random.uniform(-0.25, 0.25)
    z = random.uniform(0.15, 0.9)
    r_here, _, _ = stem_at(z)
    c = math.cos(th)
    s = math.sin(th)
    start = Vector((r_here * 0.9 * c + bend(z), r_here * 0.9 * s, z))
    direction = Vector((c, s, -0.8))
    strand(start, direction, random.uniform(2.5, 4.5), random.uniform(0.06, 0.09), 1)

finish(bm, "Basal_Mycelium", mat_myc, None, smooth=True)

# ------------------------------------------------------------------
# Done - leave specimen in scene
# ------------------------------------------------------------------
try:
    bpy.ops.object.select_all(action='DESELECT')
except Exception:
    pass
try:
    root = bpy.data.objects.get(ROOT_NAME)
    if root is not None:
        bpy.context.view_layer.objects.active = root
        root.select_set(True)
except Exception:
    pass

print("Amanita muscaria specimen generated:",
      [o.name for o in coll.objects])