import bpy, math, random, json, os, sys
import numpy as np
from mathutils import Vector
from math import sin, cos, pi, sqrt

# -------------------- scene reset --------------------
random.seed(24)
np.random.seed(24)
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)
for datablocks in (bpy.data.meshes, bpy.data.curves, bpy.data.materials, bpy.data.cameras, bpy.data.lights):
    pass
scene = bpy.context.scene
scene.render.engine = 'BLENDER_EEVEE_NEXT'
scene.render.resolution_x = 900
scene.render.resolution_y = 900
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = 'PNG'
scene.render.film_transparent = False
scene.render.image_settings.color_mode = 'RGBA'
scene.world.color = (0.008, 0.003, 0.002)
# color management
try:
    scene.view_settings.look = 'AgX - Medium High Contrast'
except Exception:
    pass
scene.view_settings.exposure = 0.25

# collections
root = scene.collection

def coll(name):
    c = bpy.data.collections.get(name) or bpy.data.collections.new(name)
    if c.name not in root.children.keys():
        root.children.link(c)
    return c
C_cap = coll('MUSHROOM • CAP + LIVING UNDERSIDE')
C_rd = coll('REACTION DIFFUSION • ITERATIVE FOLDS')
C_stem = coll('MUSHROOM • FIBROUS STEM + MYCELIUM')
C_hab = coll('TREEHOUSE HABITAT • BRIDGES + LADDERS')
C_forest = coll('OLD FOREST • TRUNKS + ROOTS + FERNS')
C_spores = coll('SPORES • RIM DUST + DRIFT')
C_light = coll('LIGHTING • TEAL UNDERSIDE + AMBER LANTERNS')

# unlink default collection links after move helper
def move_to(obj, collection):
    for c in list(obj.users_collection):
        c.objects.unlink(obj)
    collection.objects.link(obj)

def mat_principled(name, base, rough=0.5, metallic=0.0, emission=None, emission_strength=0.0, sss=0.0, alpha=1.0):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    nt = m.node_tree; nt.nodes.clear()
    out = nt.nodes.new('ShaderNodeOutputMaterial')
    bs = nt.nodes.new('ShaderNodeBsdfPrincipled')
    bs.name = 'Physically grounded surface'
    def sv(key, value):
        if bs.inputs.get(key) is not None:
            bs.inputs[key].default_value = value
    sv('Base Color', (*base, 1))
    sv('Roughness', rough)
    sv('Metallic', metallic)
    sv('Alpha', alpha)
    sv('Subsurface Weight', sss)
    if emission is not None:
        sv('Emission Color', (*emission, 1))
        sv('Emission Strength', emission_strength)
    nt.links.new(bs.outputs['BSDF'], out.inputs['Surface'])
    if alpha < 1:
        try:
            m.surface_render_method = 'DITHERED'
        except Exception:
            try: m.use_transparency_overlap = False
            except: pass
    return m

def make_cap_top_mat():
    m=bpy.data.materials.new('CAP TOP • dry leathery burnt ochre')
    m.use_nodes=True; nt=m.node_tree; nt.nodes.clear()
    out=nt.nodes.new('ShaderNodeOutputMaterial')
    bs=nt.nodes.new('ShaderNodeBsdfPrincipled')
    tex=nt.nodes.new('ShaderNodeTexNoise'); tex.inputs['Scale'].default_value=2.8; tex.inputs['Detail'].default_value=5.0; tex.inputs['Roughness'].default_value=.72
    tex.inputs['Distortion'].default_value=.22
    ramp=nt.nodes.new('ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].position=.24; ramp.color_ramp.elements[0].color=(.045,.008,.003,1)
    ramp.color_ramp.elements[1].position=.78; ramp.color_ramp.elements[1].color=(.34,.075,.018,1)
    e=ramp.color_ramp.elements.new(.52); e.color=(.14,.022,.006,1)
    bump=nt.nodes.new('ShaderNodeBump'); bump.inputs['Strength'].default_value=.22; bump.inputs['Distance'].default_value=.16
    nt.links.new(tex.outputs['Fac'],ramp.inputs['Fac']); nt.links.new(ramp.outputs['Color'],bs.inputs['Base Color']); nt.links.new(tex.outputs['Fac'],bump.inputs['Height']); nt.links.new(bump.outputs['Normal'],bs.inputs['Normal']); nt.links.new(bs.outputs['BSDF'],out.inputs['Surface'])
    for k,v in [('Roughness',.40),('Subsurface Weight',.10),('Coat Weight',.08),('Coat Roughness',.28)]:
        if bs.inputs.get(k): bs.inputs[k].default_value=v
    return m

def make_rd_surface_mat():
    m=bpy.data.materials.new('RD UNDERSIDE • cell field controls turquoise bioluminescence')
    m.use_nodes=True; nt=m.node_tree; nt.nodes.clear()
    out=nt.nodes.new('ShaderNodeOutputMaterial')
    bs=nt.nodes.new('ShaderNodeBsdfPrincipled')
    attr=nt.nodes.new('ShaderNodeAttribute'); attr.attribute_name='rd_value'
    ramp=nt.nodes.new('ShaderNodeValToRGB')
    cr=ramp.color_ramp
    cr.elements[0].position=.02; cr.elements[0].color=(.003,.012,.014,1)
    cr.elements[1].position=.62; cr.elements[1].color=(.015,.22,.25,1)
    e=cr.elements.new(.25); e.color=(.005,.055,.07,1)
    e2=cr.elements.new(.88); e2.color=(.12,.72,.74,1)
    mult=nt.nodes.new('ShaderNodeMath'); mult.operation='MULTIPLY'; mult.inputs[1].default_value=3.2
    nt.links.new(attr.outputs['Fac'],ramp.inputs['Fac']); nt.links.new(ramp.outputs['Color'],bs.inputs['Base Color']); nt.links.new(ramp.outputs['Color'],bs.inputs['Emission Color']); nt.links.new(attr.outputs['Fac'],mult.inputs[0]); nt.links.new(mult.outputs[0],bs.inputs['Emission Strength']); nt.links.new(bs.outputs['BSDF'],out.inputs['Surface'])
    for k,v in [('Roughness',.26),('Subsurface Weight',.16),('Metallic',.04)]:
        if bs.inputs.get(k): bs.inputs[k].default_value=v
    return m

mat_cap_top=make_cap_top_mat()
mat_cap_under=mat_principled('CAP EDGE • moist blue-green flesh',(.018,.055,.047),.32,0.0,(.002,.035,.032),.22,.18)
mat_rd_surface=make_rd_surface_mat()
mat_rd_ridge=mat_principled('RD FOLDS • raised teal lumen',(.006,.17,.18),.24,.04,(.005,.62,.67),3.0,.12)
mat_rd_fine=mat_principled('RD CHANNEL EDGES • fine aqua light',(.008,.23,.24),.22,.02,(.0,.78,.78),4.0,.08)
mat_stem=mat_principled('STEM • ivory fibrous flesh',(.28,.20,.10),.48,0.0,(.015,.045,.035),.14,.23)
mat_stem_hi=mat_principled('STEM FIBERS • teal reflected moisture',(.08,.18,.14),.34,0.0,(.005,.18,.16),.8,.1)
mat_bark=mat_principled('BARK • ochre old forest',(.16,.047,.018),.72,0.0,(.01,.006,.002),.05,.03)
mat_bark_hi=mat_principled('BARK HIGHLIGHTS • copper ridges',(.38,.12,.035),.62,0.0,(.018,.006,.001),.08,.02)
mat_wood=mat_principled('HABITAT WOOD • dark amber',(.20,.065,.018),.58,0.0,(.015,.004,.001),.08,.02)
mat_roof=mat_principled('HABITAT ROOFS • russet copper',(.33,.075,.018),.48,0.0,(.018,.003,.001),.12,.02)
mat_rope=mat_principled('ROPE • sun-baked hemp',(.34,.17,.055),.76,0.0,(.015,.006,.001),.05,.02)
mat_moss=mat_principled('MOSS • deep emerald',(.025,.13,.055),.82,0.0,(.004,.018,.006),.10,.10)
mat_moss_hi=mat_principled('MOSS TIPS • teal damp green',(.045,.26,.11),.60,0.0,(.005,.075,.04),.35,.14)
mat_fern=mat_principled('FERN • copper green',(.035,.18,.08),.68,0.0,(.008,.025,.006),.12,.08)
mat_ground=mat_principled('GROUND • charcoal loam',(.018,.009,.006),.96,0.0,(.004,.002,.001),.02,.02)
mat_log=mat_principled('DECAYING LOG • rust brown',(.12,.028,.009),.88,0.0,(.008,.002,.001),.03,.02)
mat_stone=mat_principled('STONE • warm shadow',(.10,.065,.038),.90,0.0,(.008,.003,.001),.03,.01)
mat_amber=mat_principled('LANTERNS • warm amber emission',(.58,.12,.015),.24,.0,(1.0,.16,.015),6.0,.02)
mat_amber_soft=mat_principled('WINDOW GLOW • candle gold',(.66,.22,.035),.28,0.0,(1.0,.28,.025),4.0,.04)
mat_spore=mat_principled('SPORES • turquoise drifting light',(.05,.24,.22),.34,0.0,(.02,.70,.66),5.0,.05)
mat_spore_warm=mat_principled('SPORES • ember dust',(.55,.12,.02),.40,0.0,(1.0,.16,.01),3.0,.02)
mat_mist=mat_principled('MIST • translucent rust atmosphere',(.32,.075,.025),.92,0.0,(.08,.01,.002),.15,0.0,.11)

# helpers
def add_cube(name, loc, dims, mat, rot=0.0, collection=None, bevel=0.0):
    bpy.ops.mesh.primitive_cube_add(size=1, location=loc, rotation=(0,0,rot))
    o=bpy.context.object; o.name=name; o.dimensions=dims
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    if mat: o.data.materials.append(mat)
    if bevel:
        mod=o.modifiers.new('soft worn edges','BEVEL'); mod.width=bevel; mod.segments=2
    if collection: move_to(o,collection)
    return o

def add_cylinder_between(name, p1, p2, r1, r2, mat, verts=10, collection=None, bevel=0.0):
    p1,p2=Vector(p1),Vector(p2); d=p2-p1; L=d.length
    if L<1e-5: return None
    bpy.ops.mesh.primitive_cone_add(vertices=verts, radius1=r1, radius2=r2, depth=L, location=(p1+p2)/2)
    o=bpy.context.object; o.name=name; o.rotation_mode='QUATERNION'; o.rotation_quaternion=d.to_track_quat('Z','Y')
    if mat: o.data.materials.append(mat)
    if bevel:
        mod=o.modifiers.new('weathered rounding','BEVEL'); mod.width=bevel; mod.segments=2
    if collection: move_to(o,collection)
    return o

def add_ico(name, loc, scale, mat, collection=None, subdivisions=1):
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdivisions, radius=1, location=loc)
    o=bpy.context.object; o.name=name; o.scale=scale
    if mat:o.data.materials.append(mat)
    if collection:move_to(o,collection)
    return o

def add_uvsphere(name, loc, scale, mat, collection=None, seg=16, rings=8):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=seg, ring_count=rings, radius=1, location=loc)
    o=bpy.context.object; o.name=name; o.scale=scale
    if mat:o.data.materials.append(mat)
    if collection:move_to(o,collection)
    return o

def add_curve(name, paths, bevel, mat, collection=None, resolution=2):
    cu=bpy.data.curves.new(name,'CURVE'); cu.dimensions='3D'; cu.resolution_u=2; cu.bevel_depth=bevel; cu.bevel_resolution=resolution; cu.resolution_u=2; cu.fill_mode='FULL'
    ob=bpy.data.objects.new(name,cu)
    (collection or root).objects.link(ob)
    for path in paths:
        if len(path)<2: continue
        sp=cu.splines.new('POLY'); sp.points.add(len(path)-1)
        for p,co in zip(sp.points,path): p.co=(*co,1)
    if mat: cu.materials.append(mat)
    return ob

def add_leaf(name, base, tip, width, mat, collection=None):
    base=Vector(base); tip=Vector(tip); d=tip-base
    if d.length<1e-4:return None
    mid=base+d*.50
    side=d.cross(Vector((0,0,1)))
    if side.length<.01: side=d.cross(Vector((1,0,0)))
    side.normalize(); side*=width
    # slightly folded center ridge for highlight
    verts=[base,mid+side,tip,mid-side,mid+Vector((0,0,width*.18))]
    faces=[(0,1,4),(1,2,4),(2,3,4),(3,0,4)]
    me=bpy.data.meshes.new(name+' mesh'); me.from_pydata(verts,[],faces); me.materials.append(mat); me.update()
    ob=bpy.data.objects.new(name,me); (collection or root).objects.link(ob)
    for p in me.polygons:p.use_smooth=True
    return ob

# -------------------- mushroom cap shell --------------------
RX,RY=7.5,6.35
CAP_Z=0.0
ns=192; nr=34
# deterministic irregular rim
def rimfactor(a):
    return 1.0 + .050*sin(3*a+.35)+.032*sin(7*a-1.2)+.018*sin(13*a+.4)
def curl(a,r):
    e=max(0.0,min(1.0,(r-.70)/.30))
    lob=.52*max(0.0,sin(2*a-.55))**5 + .26*max(0.0,sin(5*a+1.0))**7
    return (e**2.2)*lob

def ztop(x,y):
    r=min(1.2,sqrt((x/RX)**2+(y/RY)**2)); a=math.atan2(y/RY,x/RX)
    return 9.55 + .50*(1-r*r) + .055*sin(5*a+2.0)*r**2 + .025*sin(11*a)*r + .50*curl(a,r)
def zbottom(x,y):
    r=min(1.2,sqrt((x/RX)**2+(y/RY)**2)); a=math.atan2(y/RY,x/RX)
    return 8.18 + .12*r*r + .28*r**4 + .045*sin(6*a+.3)*r**2 + .52*curl(a,r)

verts=[]; faces=[]; mids=[]
def build_surface(zfun, reverse=False):
    center=len(verts); verts.append((0,0,zfun(0,0)))
    rings=[]
    for j in range(1,nr+1):
        rr=j/nr; ring=[]
        for k in range(ns):
            a=2*pi*k/ns; rf=rimfactor(a) if j==nr else (1+(rimfactor(a)-1)*(rr**4))
            x=RX*rr*rf*cos(a); y=RY*rr*rf*sin(a); ring.append(len(verts)); verts.append((x,y,zfun(x,y)))
        rings.append(ring)
    for k in range(ns):
        kn=(k+1)%ns
        if not reverse: faces.append((center,rings[0][k],rings[0][kn])); mids.append(0)
        else: faces.append((center,rings[0][kn],rings[0][k])); mids.append(1)
    for j in range(nr-1):
        r0,r1=rings[j],rings[j+1]
        for k in range(ns):
            kn=(k+1)%ns
            if not reverse: faces.append((r0[k],r1[k],r1[kn],r0[kn])); mids.append(0)
            else: faces.append((r0[k],r0[kn],r1[kn],r1[k])); mids.append(1)
    return rings
rtop=build_surface(ztop,False)
rbot=build_surface(zbottom,True)
# side wall
for k in range(ns):
    kn=(k+1)%ns
    faces.append((rtop[-1][k],rtop[-1][kn],rbot[-1][kn],rbot[-1][k])); mids.append(2)
me=bpy.data.meshes.new('Colossal flattened oyster cap shell mesh'); me.from_pydata(verts,[],faces); me.update()
me.materials.append(mat_cap_top); me.materials.append(mat_cap_under); me.materials.append(mat_cap_top)
for p,mi in zip(me.polygons,mids): p.material_index=mi; p.use_smooth=True
cap=bpy.data.objects.new('COLOSSAL MUSHROOM CAP • undulating curled rim',me); C_cap.objects.link(cap)
cap['form']='broad low flattened convex oyster / shelf mushroom'; cap['rim_curl']='several upward curled margins'; cap['underside_material']='moist translucent teal-brown flesh'
# a small bevel/weighted normal to make silhouette soft without losing folds
bev=cap.modifiers.new('soft leathery rim','BEVEL'); bev.width=.035; bev.segments=2

# -------------------- iterative Gray-Scott reaction diffusion --------------------
size=181
yy,xx=np.mgrid[0:size,0:size]
sx=xx/(size-1)*2-1; sy=yy/(size-1)*2-1
rr=np.sqrt(sx*sx+sy*sy)
u=np.ones((size,size),dtype=np.float32); v=np.zeros_like(u)
rng=np.random.default_rng(106)
# seeded biological colonies, concentrated toward the outer growth front
for _ in range(125):
    a=rng.random()*2*pi; rad=.10+.86*(rng.random()**.58)
    cx=rad*cos(a); cy=rad*sin(a); sr=.008+.018*rng.random()
    mask=(sx-cx)**2+(sy-cy)**2<sr*sr
    u[mask]=.52; v[mask]=.30
# a few center inoculations create the sparse radial-to-labyrinth transition
for a in np.linspace(0,2*pi,12,endpoint=False):
    cx=.18*cos(a); cy=.18*sin(a); mask=(sx-cx)**2+(sy-cy)**2<.018**2
    u[mask]=.52; v[mask]=.30

def lap(a):
    return (np.roll(a,1,0)+np.roll(a,-1,0)+np.roll(a,1,1)+np.roll(a,-1,1)-4*a)
for it in range(270):
    lu=lap(u); lv=lap(v)
    # spatially changing chemistry: the colony becomes faster and more tightly packed as it grows outward
    F=.026 + .012*np.clip(rr,0,1) + .003*np.sin(5*np.arctan2(sy,sx)+rr*8)**2
    K=.061 - .010*np.clip(rr,0,1)
    uvv=u*v*v
    u += .16*lu - uvv + F*(1-u)
    v += .08*lv + uvv -(F+K)*v
    u=np.clip(u,0,1); v=np.clip(v,0,1)
# radial weighting makes the final measured folds visibly progress from broad center to dense rim
rd=np.clip((v*(.52+1.25*np.clip(rr,0,1)**1.45))/0.44,0,1).astype(np.float32)
# suppress the very outer edge so the cap remains an organic rim
rd*=np.clip((1.02-rr)/.08,0,1) + (rr<=.94)*0.0
# above expression left inner? rebuild edge factor cleanly
edgefade=np.clip((1.015-rr)/.075,0,1)
rd=np.where(rr<.94,rd,rd*edgefade).astype(np.float32)

# underside displaced grid, with rd_value custom attribute for the procedural shader and GN modifier
sg=161; gy,gx=np.mgrid[0:sg,0:sg]; qx=gx/(sg-1)*2-1; qy=gy/(sg-1)*2-1
# bilinear sampling from simulation grid
coords=((np.stack([(qy+1)*(size-1)/2,(qx+1)*(size-1)/2],axis=-1)))
def sample_arr(arr, X, Y):
    X=np.clip(X,0,size-1); Y=np.clip(Y,0,size-1)
    x0=np.floor(X).astype(int); y0=np.floor(Y).astype(int); x1=np.minimum(x0+1,size-1); y1=np.minimum(y0+1,size-1)
    tx=X-x0; ty=Y-y0
    return arr[y0,x0]*(1-tx)*(1-ty)+arr[y0,x1]*tx*(1-ty)+arr[y1,x0]*(1-tx)*ty+arr[y1,x1]*tx*ty
field=sample_arr(rd, (qx+1)*(size-1)/2, (qy+1)*(size-1)/2)
sv=[]; sf=[]; sval=[]; vid={}
for i in range(sg):
  for j in range(sg):
    r=math.sqrt(float(qx[i,j]**2+qy[i,j]**2))
    if r<=.985:
      x=float(qx[i,j]*RX); y=float(qy[i,j]*RY); val=float(field[i,j])
      # underside flesh curves upward at the rim; reaction field pushes actual surface toward camera
      z=zbottom(x,y)-.035-.34*val
      vid[(i,j)]=len(sv); sv.append((x,y,z)); sval.append(val)
for i in range(sg-1):
  for j in range(sg-1):
    ks=[(i,j),(i,j+1),(i+1,j+1),(i+1,j)]
    if all(k in vid for k in ks):
      # reverse winding: this is the down-facing underside
      sf.append((vid[ks[0]],vid[ks[3]],vid[ks[2]],vid[ks[1]]))
sm=bpy.data.meshes.new('Iterated RD displaced underside surface mesh'); sm.from_pydata(sv,[],sf); sm.update(); sm.materials.append(mat_rd_surface)
attr=sm.attributes.new('rd_value','FLOAT','POINT')
for d,val in zip(attr.data,sval): d.value=float(val)
rd_surface=bpy.data.objects.new('RD UNDERSIDE • displaced reaction-diffusion field',sm); C_rd.objects.link(rd_surface)
for p in sm.polygons:p.use_smooth=True
rd_surface['generation_method']='iterative Gray-Scott reaction-diffusion (numpy field baked into geometry)'
rd_surface['simulation_iterations']=270
rd_surface['spatial_growth']='diffusion/reaction rates increase toward cap edge'
rd_surface['drives_geometry']='rd_value controls displacement depth and teal emission'

# optional Geometry Nodes stage: named rd_value drives an additional real Set Position offset
try:
    ng=bpy.data.node_groups.new('RD FIELD • Geometry Nodes displacement driver','GeometryNodeTree')
    ng.interface.new_socket(name='Geometry',in_out='INPUT',socket_type='NodeSocketGeometry')
    ng.interface.new_socket(name='Geometry',in_out='OUTPUT',socket_type='NodeSocketGeometry')
    ni=ng.nodes.new('NodeGroupInput'); no=ng.nodes.new('NodeGroupOutput'); sp=ng.nodes.new('GeometryNodeSetPosition'); na=ng.nodes.new('GeometryNodeInputNamedAttribute'); na.data_type='FLOAT'; na.inputs['Name'].default_value='rd_value'
    mul=ng.nodes.new('ShaderNodeMath'); mul.operation='MULTIPLY'; mul.inputs[1].default_value=-.045
    comb=ng.nodes.new('ShaderNodeCombineXYZ')
    ng.links.new(ni.outputs['Geometry'],sp.inputs['Geometry']); ng.links.new(na.outputs['Attribute'],mul.inputs[0]); ng.links.new(mul.outputs[0],comb.inputs['Z']); ng.links.new(comb.outputs['Vector'],sp.inputs['Offset']); ng.links.new(sp.outputs['Geometry'],no.inputs['Geometry'])
    mod=rd_surface.modifiers.new('RD Geometry Nodes • field displacement','NODES'); mod.node_group=ng
except Exception as e:
    rd_surface['geometry_nodes_note']='GN stage unavailable: '+str(e)

# marching squares contours from the iterated field
def contours(arr, thresh, min_r=.12, max_r=.975):
    h,w=arr.shape; xs=np.linspace(-1,1,w); ys=np.linspace(-1,1,h); segs=[]
    table={1:[(3,0)],2:[(0,1)],3:[(3,1)],4:[(1,2)],5:[(3,2),(0,1)],6:[(0,2)],7:[(3,2)],8:[(2,3)],9:[(2,0)],10:[(0,1),(2,3)],11:[(2,1)],12:[(1,3)],13:[(0,1)],14:[(3,0)]}
    for i in range(h-1):
      for j in range(w-1):
        x0=xs[j]; x1=xs[j+1]; y0=ys[i]; y1=ys[i+1]
        # p0 bottom-left, p1 bottom-right, p2 top-right, p3 top-left
        vals=[float(arr[i,j]),float(arr[i,j+1]),float(arr[i+1,j+1]),float(arr[i+1,j])]
        rcell=max(math.hypot(x0,y0),math.hypot(x1,y0),math.hypot(x1,y1),math.hypot(x0,y1))
        if rcell<min_r or min(math.hypot(x0,y0),math.hypot(x1,y0),math.hypot(x1,y1),math.hypot(x0,y1))>max_r: continue
        case=sum((1<<k) for k,val in enumerate(vals) if val>thresh)
        if case==0 or case==15: continue
        ps=[((x0,y0),(x1,y0)),((x1,y0),(x1,y1)),((x1,y1),(x0,y1)),((x0,y1),(x0,y0))]
        def ip(e):
            a,b=ps[e]; va=vals[e]; vb=vals[(e+1)%4]
            t=0.5 if abs(vb-va)<1e-8 else (thresh-va)/(vb-va); t=max(0,min(1,t))
            return (a[0]+(b[0]-a[0])*t,a[1]+(b[1]-a[1])*t)
        for ea,eb in table[case]: segs.append((ip(ea),ip(eb)))
    # stitch quantized endpoints
    def key(p):return (round(p[0],5),round(p[1],5))
    adj={}; points={}
    for a,b in segs:
      ka,kb=key(a),key(b); points[ka]=a; points[kb]=b; adj.setdefault(ka,[]).append(kb); adj.setdefault(kb,[]).append(ka)
    used=set(); paths=[]
    for a,b in segs:
      ka,kb=key(a),key(b); ek=tuple(sorted((ka,kb)))
      if ek in used:continue
      used.add(ek); path=[ka,kb]; prev=ka; cur=kb
      for _ in range(800):
        choices=[n for n in adj.get(cur,[]) if tuple(sorted((cur,n))) not in used]
        if not choices:break
        nxt=choices[0]; used.add(tuple(sorted((cur,nxt)))); path.append(nxt); prev,cur=cur,nxt
        if cur==path[0]:break
      pp=[points[k] for k in path]
      # reject tiny contours and retain meaningful arcs
      if len(pp)>=5 and sum(math.dist(pp[n-1],pp[n]) for n in range(1,len(pp)))>.10:
        paths.append(pp)
    return paths

def contour_paths_to_3d(paths, offset, wobble=0.0):
    out=[]
    for pp in paths:
      p3=[]
      for x,y in pp:
        r=math.hypot(x,y); a=math.atan2(y,x)
        z=zbottom(x*RX,y*RY)-offset-.015*math.sin(a*9+r*13)*wobble
        p3.append((x*RX,y*RY,z))
      out.append(p3)
    return out

fine1=contours(field,.155,min_r=.20,max_r=.972)
fine2=contours(field,.285,min_r=.25,max_r=.958)
# keep object scale readable, but avoid thousands of tiny splines
fine1=[p for p in fine1 if len(p)>=8]
fine2=[p for p in fine2 if len(p)>=7]
rd_curve1=add_curve('RD FOLDS • branching contour network (outer dense growth)',contour_paths_to_3d(fine1,.115,.7),.060,mat_rd_ridge,C_rd,2)
rd_curve2=add_curve('RD FOLDS • secondary split and reconnect edges',contour_paths_to_3d(fine2,.155,.45),.038,mat_rd_fine,C_rd,2)
rd_curve1['pattern']='marching-square isocontours extracted from Gray-Scott field'; rd_curve1['growth_direction']='sparse stem / dense outer rim'

# Inner broad channels: sparse, partly radial, then fork as they meet the reaction front
inner_paths=[]; branch_paths=[]
for k in range(14):
    a0=2*pi*k/14 + .08*sin(k*2.4)
    pts=[]
    for q in np.linspace(.07,.39,12):
        a=a0+.15*sin(q*9+k)
        pts.append((RX*q*cos(a),RY*q*sin(a),zbottom(RX*q*cos(a),RY*q*sin(a))-.18-.03*q))
    inner_paths.append(pts)
    # two diverging daughter folds from each primary
    for sgni in (-1,1):
        pts2=[]
        for q in np.linspace(.28,.68,16):
            a=a0+sgni*(.16+.58*(q-.28))+.10*sin(q*13+k*1.5)
            pts2.append((RX*q*cos(a),RY*q*sin(a),zbottom(RX*q*cos(a),RY*q*sin(a))-.13-.02*q))
        branch_paths.append(pts2)
add_curve('RD FOLDS • broad inner radial channels',inner_paths,.105,mat_rd_ridge,C_rd,2)
add_curve('RD FOLDS • first generation forks',branch_paths,.072,mat_rd_ridge,C_rd,2)
# an irregular thin outer seam emphasizes the organic rim but is not a perfect ring
rimpath=[]
for k in range(181):
    a=2*pi*k/180; q=.945+.012*sin(5*a+1)+.010*sin(11*a)
    rimpath.append((RX*q*cos(a),RY*q*sin(a),zbottom(RX*q*cos(a),RY*q*sin(a))-.07))
add_curve('RD edge growth front • living seam',[rimpath],.032,mat_rd_fine,C_rd,2)

# -------------------- stem and mycelium --------------------
# irregular tapered stem mesh
sv=[]; sf=[]; rings=[]; nst=20; nrings=16
for j in range(nrings):
    t=j/(nrings-1); z=.42+t*7.92; cx=.26+.18*sin(t*2.3+.4); cy=.05+.16*cos(t*2.0)
    rad=1.27-.48*t+.075*sin(t*7.5)
    ring=[]
    for k in range(nst):
        a=2*pi*k/nst; rr=rad*(1+.07*sin(3*a+2*t)+.035*sin(7*a-t))
        ring.append(len(sv)); sv.append((cx+rr*cos(a),cy+rr*sin(a),z))
    rings.append(ring)
for j in range(nrings-1):
    for k in range(nst):
        kn=(k+1)%nst; sf.append((rings[j][k],rings[j][kn],rings[j+1][kn],rings[j+1][k]))
# caps
bot=len(sv); sv.append((.26,.05,.42)); top=len(sv); sv.append((.26+.18*sin(2.7+.4),.05+.16*cos(2.0),8.34))
for k in range(nst):
    kn=(k+1)%nst; sf.append((bot,rings[0][kn],rings[0][k])); sf.append((top,rings[-1][k],rings[-1][kn]))
stme=bpy.data.meshes.new('Fibrous slightly off-center stem mesh'); stme.from_pydata(sv,[],sf); stme.materials.append(mat_stem); stme.update()
stem=bpy.data.objects.new('MUSHROOM STEM • thick off-center fibrous trunk',stme); C_stem.objects.link(stem)
for p in stme.polygons:p.use_smooth=True
bev=stem.modifiers.new('soft flesh rounding','BEVEL'); bev.width=.055; bev.segments=2
stem['role']='supports colossal cap from decaying wood and moss'; stem['subsurface']='subtle subsurface scattering'
# helical fibrous strands on stem
fibpaths=[]
for k in range(22):
    a0=2*pi*k/22; pts=[]
    for j in range(26):
        t=j/25; z=.5+t*7.62; cx=.26+.18*sin(t*2.3+.4); cy=.05+.16*cos(t*2.0); rad=1.27-.48*t+.075*sin(t*7.5)
        a=a0+2.2*t+.10*sin(t*9+k)
        pts.append((cx+rad*cos(a)*.93,cy+rad*sin(a)*.93,z))
    fibpaths.append(pts)
add_curve('STEM FIBERS • vertical biological striations',fibpaths,.026,mat_stem_hi,C_stem,2)
# roots and mycelium
rootpaths=[]
for k in range(11):
    a=2*pi*k/11+.15*sin(k)
    L=3.0+random.random()*1.8
    pts=[]
    for q in np.linspace(0,1,14):
        r=.45+L*q; z=.45*(1-q)+.12+.10*sin(q*5+k)
        pts.append((.26+r*cos(a)+.18*sin(q*4+k),.05+r*sin(a)+.18*cos(q*5-k),z))
    rootpaths.append(pts)
    # fine mycelium side branch
    if k%2==0:
        a2=a+.5; pts2=[]
        for q in np.linspace(.25,.85,12):
            r=.6+L*q; pts2.append((.26+r*cos(a2)+.08*sin(q*9),.05+r*sin(a2),.16+.035*sin(q*8)))
        rootpaths.append(pts2)
add_curve('MYCELIUM • visible branching root threads',rootpaths,.045,mat_stem_hi,C_stem,1)
# decaying logs at base
for i in range(7):
    a=2*pi*i/7+.3; r=2.0+random.random()*1.5; p=(r*cos(a),r*sin(a),.45+random.random()*.10); d=(-sin(a),cos(a),.03)
    p2=Vector(p)+Vector(d)*(1.4+random.random()*1.3)
    add_cylinder_between('DECAYING WOOD • log '+str(i),p,p2,.32+random.random()*.12,.20,mat_log,10,C_stem,.025)

# moss clumps around roots
for i in range(28):
    a=2*pi*i/28+random.random()*.25; r=1.5+random.random()*3.3; z=.20+random.random()*.14
    add_ico('MOSS CLUMP • ground colony '+str(i),(r*cos(a),r*sin(a),z),(0.32+random.random()*.25,0.24+random.random()*.20,.16+random.random()*.12),mat_moss if i%3 else mat_moss_hi,C_stem,1)

# -------------------- old forest trunks and hanging moss --------------------
trunk_specs=[]
for i,a in enumerate([-2.85,-2.25,-1.65,-.95,-.15,.55,1.2,1.85,2.45,3.0]):
    r=10.5+random.random()*4.0; x=r*cos(a); y=r*sin(a); ztopr=10.5+random.random()*4.2
    # keep central silhouette open by making front outer trunks shorter
    if y< -5: ztopr-=1.5
    p1=(x,y,0); p2=(x+random.uniform(-1,1),y+random.uniform(-1,1),ztopr)
    trunk_specs.append((a,p1,p2))
    add_cylinder_between('OLD FOREST TRUNK • '+str(i),p1,p2,.60+random.random()*.42,.25,mat_bark,12,C_forest,.05)
    # branching limbs, two per trunk, angled out and inward
    base=Vector(p1)+(Vector(p2)-Vector(p1))*.52
    for j in range(2):
        az=a+(j-.5)*1.0+random.uniform(-.18,.18); start=base+Vector((0,0,random.uniform(-.3,1.2)))
        end=start+Vector((cos(az)* (3.0+random.random()*2),sin(az)*(3.0+random.random()*2),2.0+random.random()*2.5))
        add_cylinder_between('OLD TRUNK BRANCH • '+str(i)+'-'+str(j),start,end,.18,.055,mat_bark_hi,9,C_forest,.025)
# branch/ridge texture as a few curves
barkpaths=[]
for i,(a,p1,p2) in enumerate(trunk_specs):
    for q in range(3):
        off=(q-1)*.16; pts=[]
        for t in np.linspace(0,1,14):
            v=Vector(p1).lerp(Vector(p2),t); pts.append((v.x+off*cos(a+pi/2)+.08*sin(t*10+q),v.y+off*sin(a+pi/2),v.z))
        barkpaths.append(pts)
add_curve('BARK RIDGES • old vertical furrows',barkpaths,.035,mat_bark_hi,C_forest,1)
# hanging moss tassels from selected branches and cap margins
moss_paths=[]
for i in range(24):
    a=2*pi*i/24+.15; r=9.6+random.random()*3.5; x=r*cos(a); y=r*sin(a); z=7.4+random.random()*4.0
    L=.7+random.random()*2.4
    pts=[]
    for j in range(9):
        t=j/8; pts.append((x+.14*sin(t*5+i),y+.14*cos(t*4+i),z-L*t))
    moss_paths.append(pts)
# special moss curtain under upward cap curls
for a in [-2.1,-.75,.55,2.35]:
    q=.86; x=RX*q*cos(a); y=RY*q*sin(a); z=zbottom(x,y)+.05
    for j in range(4):
        pts=[]
        for k in range(7):
            t=k/6; pts.append((x+(j-1.5)*.12+.06*sin(k+j),y+(j-1.5)*.10+.05*cos(k),z-(.35+random.random()*.55)*t))
        moss_paths.append(pts)
add_curve('HANGING MOSS • forest curtains and cap rim tassels',moss_paths,.055,mat_moss_hi,C_forest,2)

# -------------------- ferns and leaf understory --------------------
for i in range(26):
    a=2*pi*i/26+random.random()*.20; r=3.1+random.random()*5.0
    base=Vector((r*cos(a),r*sin(a),.12)); h=.55+random.random()*.85
    tip=base+Vector((.20*cos(a),.20*sin(a),h))
    add_curve('FERN STEM '+str(i),[[tuple(base),tuple(base+Vector((0,0,h*.45))),tuple(tip)]],.018,mat_fern,C_forest,1)
    # alternating leaves, directed outward and up
    for j in range(5):
        t=.18+j*.15; c=base.lerp(tip,t); side=(-sin(a),cos(a),0)
        for sgn in (-1,1):
            d=Vector((cos(a)*(.35+.16*random.random()),sin(a)*(.35+.16*random.random()),.18+.12*random.random())) + Vector(side)*(.22*sgn)
            add_leaf('FERN LEAF '+str(i)+'-'+str(j)+'-'+str(sgn),c,c+d,.12+random.random()*.06,mat_fern if j%2 else mat_moss_hi,C_forest)

# -------------------- habitat: platforms, huts, bridges, ladders --------------------
house_specs=[(-2.62,9.45,6.05,0.95),(-1.02,10.0,6.55,1.0),(.52,10.8,7.10,1.1),(2.28,10.0,6.75,.92)]
houses=[]; platforms=[]
for i,(a,r,z,scale) in enumerate(house_specs):
    radial=Vector((cos(a),sin(a),0)); tang=Vector((-sin(a),cos(a),0)); p=radial*r
    # attachment trunk behind the platform
    trunkbase=radial*(r+.75)+Vector((0,0,.1)); trunktop=radial*(r+.55)+Vector((0,0,z+1.3))
    add_cylinder_between('HABITAT SUPPORT TRUNK '+str(i),trunkbase,trunktop,.42,.20,mat_bark,11,C_hab,.035)
    # root-like brace to ground
    add_cylinder_between('HABITAT ROOT BRACE '+str(i),trunkbase,radial*(r+.5)+Vector((0,0,.2)),.20,.08,mat_bark_hi,8,C_hab,.02)
    # platform disk
    bpy.ops.mesh.primitive_cylinder_add(vertices=12,radius=1.45*scale,depth=.18,location=(p.x,p.y,z))
    pl=bpy.context.object; pl.name='ELEVATED LOOKOUT PLATFORM '+str(i); pl.data.materials.append(mat_wood); move_to(pl,C_hab); platforms.append((p,z,radial,tang))
    # support beams toward trunk
    for off in (-.55,.55):
        start=p+tang*off+Vector((0,0,-.1)); end=radial*(r+.68)+tang*off+Vector((0,0,-.1))
        add_cylinder_between('PLATFORM BEAM '+str(i),start,end,.10,.08,mat_wood,8,C_hab,.015)
    # hut body, local x radial, local y tangential
    body=p+Vector((0,0,.95*scale))
    bodyob=add_cube('TREEHOUSE '+str(i)+' • tiny dwelling',body,(1.65*scale,1.35*scale,1.45*scale),mat_wood,rot=a,collection=C_hab,bevel=.07)
    # pyramidal roof
    bpy.ops.mesh.primitive_cone_add(vertices=4,radius=1.34*scale,depth=.72*scale,location=p+Vector((0,0,1.98*scale)))
    roof=bpy.context.object; roof.name='TREEHOUSE '+str(i)+' • copper pyramid roof'; roof.rotation_euler[2]=a+pi/4; roof.data.materials.append(mat_roof); move_to(roof,C_hab)
    # outward front window and side window
    winpos=p+radial*(.70*scale)+Vector((0,0,1.02*scale))
    win=add_cube('WARM WINDOW '+str(i)+' • amber',winpos,(.58*scale,.055,.56*scale),mat_amber_soft,rot=a+pi/2,collection=C_hab,bevel=.02)
    # window cross bars
    add_cube('window mullion',winpos+Vector((0,0,.01)),(.06*scale,.07,.58*scale),mat_wood,rot=a+pi/2,collection=C_hab)
    add_cube('window mullion',winpos+Vector((0,0,.01)),(.60*scale,.07,.06*scale),mat_wood,rot=a+pi/2,collection=C_hab)
    sidepos=p+tang*(.68*scale)+Vector((0,0,1.02*scale))
    add_cube('SIDE WINDOW '+str(i)+' • amber',sidepos,(.055,.48*scale,.48*scale),mat_amber_soft,rot=a,collection=C_hab,bevel=.02)
    # door on inward side, dark with small lantern
    doorpos=p-radial*(.70*scale)+Vector((0,0,.72*scale))
    add_cube('TREEHOUSE DOOR '+str(i),doorpos,(.58*scale,.055,1.10*scale),mat_bark,rot=a+pi/2,collection=C_hab,bevel=.025)
    # railing posts and rope around outer half
    railpaths=[]
    for sgn in (-1,1):
        for j in range(2):
            off=sgn*(.9*scale) if j==0 else sgn*(.45*scale)
            base=p+tang*off+radial*.35*scale+Vector((0,0,.1)); top=base+Vector((0,0,.75*scale))
            add_cylinder_between('DECK RAIL POST',base,top,.045,.035,mat_rope,7,C_hab)
        railpaths.append([tuple(p+tang*(sgn*.9*scale)+radial*.35*scale+Vector((0,0,.82*scale))),tuple(p+tang*(sgn*.45*scale)+radial*.35*scale+Vector((0,0,.82*scale))),tuple(p+tang*(0)+radial*.35*scale+Vector((0,0,.82*scale)))])
    add_curve('DECK RAIL ROPE '+str(i),railpaths,.035,mat_rope,C_hab,2)
    # lantern hanging outside
    lamp=add_ico('AMBER LANTERN '+str(i),winpos+radial*.15+Vector((0,0,-.55)),(.11,.11,.15),mat_amber,C_hab,1)
    # point light in window
    bpy.ops.object.light_add(type='POINT',location=p+radial*.45+Vector((0,0,1.0)))
    li=bpy.context.object; li.name='LANTERN LIGHT '+str(i); li.data.energy=42; li.data.color=(1.0,.16,.025); li.data.shadow_soft_size=1.0; move_to(li,C_light)
    houses.append((p,z,radial,tang))
# rope bridge helper
def bridge(i,j):
    p1,z1,r1,t1=platforms[i]; p2,z2,r2,t2=platforms[j]
    p1=Vector((p1.x,p1.y,z1+.13)); p2=Vector((p2.x,p2.y,z2+.13)); d=p2-p1; horiz=Vector((d.x,d.y,0)); L=horiz.length; tang=horiz.normalized(); side=Vector((-tang.y,tang.x,0))
    n=max(7,int(L/1.15))
    for k in range(n):
        t=(k+.5)/n; c=p1.lerp(p2,t); c.z-=.55*sin(pi*t)
        add_cube('BRIDGE PLANK '+str(i)+'-'+str(j)+'-'+str(k),c,(.78,.28,.12),mat_wood,rot=math.atan2(tang.y,tang.x),collection=C_hab,bevel=.025)
    railpaths=[]
    for ss in (-1,1):
        pts=[]
        for k in range(9):
            t=k/8; c=p1.lerp(p2,t); c.z-=.55*sin(pi*t); c+=side*(.55*ss); c.z+=.72
            pts.append(tuple(c))
        railpaths.append(pts)
    add_curve('ROPE BRIDGE RAILS '+str(i)+'-'+str(j),railpaths,.045,mat_rope,C_hab,2)
    # lower support rope
    add_curve('ROPE BRIDGE LOWER SAG '+str(i)+'-'+str(j),[[tuple(p1+side*.56),tuple((p1+p2)/2+Vector((0,0,-.65))+side*.56),tuple(p2+side*.56)]],.035,mat_rope,C_hab,2)
for i in range(len(platforms)):
    bridge(i,(i+1)%len(platforms))
# ladders at outer side of two habitats
for i in (0,2):
    p,z,radial,tang=platforms[i]; top=p+radial*.62+Vector((0,0,.15)); bottom=top+Vector((0,0,-2.55))
    for sgn in (-1,1):
        a=top+tang*(.38*sgn); b=bottom+tang*(.38*sgn)
        add_cylinder_between('LADDER SIDE RAIL',b,a,.055,.045,mat_rope,8,C_hab)
    for k in range(6):
        t=(k+1)/7; c=bottom.lerp(top,t)
        add_cylinder_between('LADDER RUNG',c-tang*.4,c+tang*.4,.045,.045,mat_rope,8,C_hab)

# -------------------- spores and luminous dust --------------------
def make_spore_mesh(name, centers, material, warm_fraction=0.0):
    verts=[]; faces=[]; mats=[]; seg=7; rings=4
    for idx,(cx,cy,cz,rad) in enumerate(centers):
        base=len(verts)
        # top/bottom poles
        verts.append((cx,cy,cz+rad)); top=base
        for j in range(1,rings):
            ph=pi*j/rings; rr=rad*sin(ph); zz=rad*cos(ph)
            for k in range(seg):
                a=2*pi*k/seg; verts.append((cx+rr*cos(a),cy+rr*sin(a),cz+zz))
        verts.append((cx,cy,cz-rad)); bot=len(verts)-1
        # top fan
        for k in range(seg):
            kn=(k+1)%seg; faces.append((top,base+1+k,base+1+kn)); mats.append(0)
        for j in range(rings-2):
            st=base+1+j*seg; sn=st+seg
            for k in range(seg):
                kn=(k+1)%seg; faces.append((st+k,sn+k,sn+kn,st+kn)); mats.append(0)
        st=base+1+(rings-2)*seg
        for k in range(seg):
            kn=(k+1)%seg; faces.append((st+k,bot,st+kn)); mats.append(0)
    me=bpy.data.meshes.new(name+' mesh'); me.from_pydata(verts,[],faces); me.update(); me.materials.append(material)
    ob=bpy.data.objects.new(name,me); C_spores.objects.link(ob)
    for p in me.polygons:p.use_smooth=True
    return ob
spore_centers=[]; warm_centers=[]
for i in range(105):
    a=random.random()*2*pi; r=5.9+random.random()*1.75; x=RX*(r/7.5)*cos(a); y=RY*(r/7.5)*sin(a); z=7.15+random.random()*1.6
    rad=.025+random.random()*.045*(1.4 if i%9==0 else 1)
    (warm_centers if i%13==0 else spore_centers).append((x,y,z,rad))
# dust collecting below rim: clustered in arcs under outer cap
for i in range(130):
    a=random.random()*2*pi; r=4.0+random.random()*3.0; x=RX*(r/7.5)*cos(a); y=RY*(r/7.5)*sin(a); z=.35+random.random()*1.0
    rad=.018+random.random()*.035
    spore_centers.append((x,y,z,rad))
make_spore_mesh('DRIFTING SPORES • teal rim cloud',spore_centers,mat_spore)
make_spore_mesh('SPORE DUST • occasional ember motes',warm_centers,mat_spore_warm)

# atmospheric low-poly translucent layers, placed behind habitat
for i,(loc,sc) in enumerate([((0,5.5,4.7),(11,.8,1.5)),((-9,6,6),(5,.7,2.4)),((10,7,5),(5,.8,2.0))]):
    ob=add_uvsphere('ATMOSPHERIC RUST MIST '+str(i),loc,sc,mat_mist,C_forest,20,10)

# ground plane and scattered stones
add_cube('DEEP FOREST LOAM GROUND',(0,0,-.08),(42,42,.18),mat_ground,collection=C_forest,bevel=.04)
for i in range(20):
    a=random.random()*2*pi; r=4+random.random()*10
    add_ico('warm shadow stone '+str(i),(r*cos(a),r*sin(a),.15),(random.uniform(.15,.45),random.uniform(.12,.35),random.uniform(.12,.28)),mat_stone,C_forest,1)

# -------------------- lighting --------------------
def point_at(obj, target):
    obj.rotation_euler=(Vector(target)-obj.location).to_track_quat('-Z','Y').to_euler()
# orange sunset key
bpy.ops.object.light_add(type='AREA', location=(10,-12,12)); key=bpy.context.object; key.name='SUNSET KEY • amber raking light'; key.data.energy=1050; key.data.shape='DISK'; key.data.size=8.0; key.data.color=(1.0,.18,.035); point_at(key,(0,0,7)); move_to(key,C_light)
# warm back fill
bpy.ops.object.light_add(type='AREA', location=(-12,8,9)); fill=bpy.context.object; fill.name='COPPER HAZE FILL'; fill.data.energy=900; fill.data.size=10; fill.data.color=(.72,.12,.025); point_at(fill,(0,0,6)); move_to(fill,C_light)
# teal source inside underside, raking across physical ridges
bpy.ops.object.light_add(type='AREA', location=(0,-1.2,7.0)); teal=bpy.context.object; teal.name='TEAL UNDERSIDE • low raking bioluminescent source'; teal.data.energy=780; teal.data.size=5.5; teal.data.color=(.01,.55,.62); point_at(teal,(.4,-.4,8.4)); move_to(teal,C_light)
bpy.ops.object.light_add(type='POINT', location=(0,-.5,7.35)); tp=bpy.context.object; tp.name='TEAL CORE GLOW'; tp.data.energy=260; tp.data.color=(.01,.42,.45); tp.data.shadow_soft_size=2.5; move_to(tp,C_light)
# low orange floor rim light
bpy.ops.object.light_add(type='AREA', location=(0,-10,1.5)); low=bpy.context.object; low.name='LOW WARM FLOOR RIM'; low.data.energy=650; low.data.size=7; low.data.color=(1.0,.09,.015); point_at(low,(0,0,5)); move_to(low,C_light)
# world nodes for deep rust ambient
world=bpy.data.worlds.get('World') or bpy.data.worlds.new('World'); scene.world=world; world.use_nodes=True; wn=world.node_tree; wn.nodes.clear(); wo=wn.nodes.new('ShaderNodeOutputWorld'); bg=wn.nodes.new('ShaderNodeBackground'); bg.inputs['Color'].default_value=(.018,.004,.002,1); bg.inputs['Strength'].default_value=.16; wn.links.new(bg.outputs['Background'],wo.inputs['Surface'])

# -------------------- camera --------------------
bpy.ops.object.camera_add(location=(15.5,-19.5,5.6))
cam=bpy.context.object; cam.name='PRESENTATION CAMERA • low three-quarter beneath cap'; cam.data.lens=39; cam.data.sensor_width=36; point_at(cam,(0,0,7.35)); scene.camera=cam; move_to(cam,C_light)
cam.data.dof.use_dof=True; cam.data.dof.focus_object=rd_surface; cam.data.dof.aperture_fstop=7.5
cam['composition']='low three-quarter upward view; underside dominates while ring of habitat establishes scale'

# convert curves to meshes for robust GLB geometry, while preserving all visual forms
bpy.ops.object.select_all(action='DESELECT')
curve_objs=[o for o in list(bpy.data.objects) if o.type=='CURVE']
for o in curve_objs:
    bpy.context.view_layer.objects.active=o; o.select_set(True)
    try: bpy.ops.object.convert(target='MESH')
    except Exception: pass
    o.select_set(False)

# organize metadata and render settings
scene['ARTWORK_TITLE']='THE LUMINOUS MYCELIUM HABITAT'
scene['RD_METHOD']='Gray-Scott reaction-diffusion field iterated 270 steps; sampled to displaced underside and contour-fold meshes'
scene['RD_VISUAL_READ']='sparse broad stem channels split into reconnecting, increasingly dense outer maze'
scene['PALETTE']='warm rust-orange forest / teal bioluminescent underside contrast'
scene['SCALE']='colossal cap 15m wide; treehouses ring the mushroom on independent trunks and platforms'
scene.render.filepath='/tmp/luminous_mushroom_preview.png'
# set render preview quality; benchmark will override to Cycles 1536 square
scene.render.engine='BLENDER_EEVEE_NEXT'
scene.render.resolution_x=900; scene.render.resolution_y=900; scene.render.resolution_percentage=100
# ambient occlusion / soft shadows in Eevee 5 likely defaults
# select none and save
bpy.ops.object.select_all(action='DESELECT')
# save
bpy.ops.wm.save_as_mainfile(filepath='/tmp/luminous_mushroom_habitat.blend')
result={'objects':len(bpy.data.objects),'meshes':len(bpy.data.meshes),'materials':len(bpy.data.materials),'fine_contours':len(fine1),'secondary_contours':len(fine2),'path':'/tmp/luminous_mushroom_habitat.blend','rd_range':[float(rd.min()),float(rd.max())]}


Traceback (most recent call last):
  File "<repo>\.runner\blender-mcp-v1.0.0\addon\mcp_to_blender_server.py", line 230, in _execute_code
    exec(code, namespace)
    ~~~~^^^^^^^^^^^^^^^^^
  File "<string>", line 14, in <module>
TypeError: bpy_struct: item.attr = val: enum "BLENDER_EEVEE_NEXT" not found in ('BLENDER_EEVEE', 'BLENDER_WORKBENCH', 'CYCLES')