import bpy, math, os, random
from mathutils import Vector
from math import sin, cos, pi

# -----------------------------------------------------------------------------
# Classical atelier bust: clean scene build
# -----------------------------------------------------------------------------
random.seed(21)

# Clear default scene completely
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for datablocks in (bpy.data.meshes, bpy.data.curves, bpy.data.materials, bpy.data.cameras, bpy.data.lights):
    pass

scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.scale_length = 1.0

# Collections for a readable interactive scene
root = scene.collection
collections = {}
for name in ['BUST • FORM', 'FACE • SCULPTED PLANES', 'HAIR • CLUMPED CURLS', 'PEDESTAL', 'STUDIO • LIGHT & BACKDROP']:
    col = bpy.data.collections.new(name)
    root.children.link(col)
    collections[name] = col

def move_to_collection(obj, col):
    # Objects are initially linked to the active collection; make the hierarchy intentional.
    for c in list(obj.users_collection):
        c.objects.unlink(obj)
    col.objects.link(obj)
    return obj

# Materials -------------------------------------------------------------------
def make_material(name, color, roughness=0.7, subsurface=0.0, noise_bump=0.0, noise_scale=4.0, metallic=0.0):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nt = mat.node_tree
    nt.nodes.clear()
    out = nt.nodes.new('ShaderNodeOutputMaterial')
    out.location = (430, 0)
    bsdf = nt.nodes.new('ShaderNodeBsdfPrincipled')
    bsdf.location = (80, 0)
    bsdf.name = 'Atelier Principled'
    bsdf.label = name
    def setv(key, value):
        if bsdf.inputs.get(key) is not None:
            bsdf.inputs[key].default_value = value
    setv('Base Color', (*color, 1.0))
    setv('Roughness', roughness)
    setv('Metallic', metallic)
    setv('IOR', 1.46)
    setv('Specular IOR Level', 0.22)
    # Blender 4/5 uses a weighted Subsurface input; remain version-tolerant.
    if bsdf.inputs.get('Subsurface Weight') is not None:
        bsdf.inputs['Subsurface Weight'].default_value = subsurface
    elif bsdf.inputs.get('Subsurface') is not None:
        bsdf.inputs['Subsurface'].default_value = subsurface
    if bsdf.inputs.get('Subsurface Radius') is not None:
        bsdf.inputs['Subsurface Radius'].default_value = (1.0, 0.75, 0.55)
    nt.links.new(bsdf.outputs.get('BSDF'), out.inputs.get('Surface'))
    if noise_bump:
        noise = nt.nodes.new('ShaderNodeTexNoise')
        noise.location = (-400, -170)
        noise.inputs['Scale'].default_value = noise_scale
        noise.inputs['Detail'].default_value = 3.0
        noise.inputs['Roughness'].default_value = 0.55
        bump = nt.nodes.new('ShaderNodeBump')
        bump.location = (-80, -150)
        bump.inputs['Strength'].default_value = noise_bump
        bump.inputs['Distance'].default_value = 0.045
        nt.links.new(noise.outputs['Fac'], bump.inputs['Height'])
        nt.links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])
    return mat

plaster = make_material('Atelier Plaster • warm ivory', (0.72, 0.69, 0.63), roughness=0.72, subsurface=0.065, noise_bump=0.075, noise_scale=5.5)
plaster_lip = make_material('Plaster • softened lip plane', (0.66, 0.625, 0.57), roughness=0.68, subsurface=0.055, noise_bump=0.055, noise_scale=7.0)
hair_mat = make_material('Plaster • hair mass', (0.47, 0.45, 0.41), roughness=0.82, subsurface=0.025, noise_bump=0.10, noise_scale=7.0)
recess_mat = make_material('Dust grey • carved recesses', (0.25, 0.24, 0.22), roughness=0.9, subsurface=0.0, noise_bump=0.015, noise_scale=8.0)
stone = make_material('Pedestal • cool limestone', (0.30, 0.315, 0.31), roughness=0.78, subsurface=0.025, noise_bump=0.06, noise_scale=3.0)
backdrop_mat = make_material('Studio backdrop • neutral grey', (0.105, 0.115, 0.12), roughness=0.92, subsurface=0.0, noise_bump=0.018, noise_scale=2.0)

# Helpers ---------------------------------------------------------------------
def assign_mat(obj, mat):
    if hasattr(obj.data, 'materials'):
        obj.data.materials.append(mat)

def smooth(obj):
    if hasattr(obj.data, 'polygons'):
        for p in obj.data.polygons:
            p.use_smooth = True

def add_bevel(obj, width=0.03, segments=3, apply=False):
    mod = obj.modifiers.new('Softly dressed edge', 'BEVEL')
    mod.width = width
    mod.segments = segments
    mod.limit_method = 'ANGLE'
    if hasattr(mod, 'harden_normals'):
        mod.harden_normals = True
    if apply:
        bpy.context.view_layer.objects.active = obj
        obj.select_set(True)
        try:
            bpy.ops.object.modifier_apply(modifier=mod.name)
        except Exception:
            pass
        obj.select_set(False)
    return obj

def uv_ellipsoid(name, loc, scale, mat, segments=48, rings=24, col=None):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, radius=1.0, location=loc)
    obj = bpy.context.object
    obj.name = name
    obj.scale = scale
    smooth(obj)
    assign_mat(obj, mat)
    if col: move_to_collection(obj, col)
    return obj

def cube_beveled(name, loc, dims, mat, bevel=0.06, col=None):
    bpy.ops.mesh.primitive_cube_add(location=loc)
    obj = bpy.context.object
    obj.name = name
    obj.dimensions = dims
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    assign_mat(obj, mat)
    add_bevel(obj, bevel, 3, apply=True)
    smooth(obj)
    if col: move_to_collection(obj, col)
    return obj

def cylinder(name, loc, radius, depth, mat, vertices=64, bevel=0.035, col=None):
    bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=depth, location=loc)
    obj = bpy.context.object
    obj.name = name
    assign_mat(obj, mat)
    if bevel:
        add_bevel(obj, bevel, 3, apply=True)
    smooth(obj)
    if col: move_to_collection(obj, col)
    return obj

def loft_rings(name, rings, n, mat, col=None, cap_bottom=True, cap_top=True, subsurf=0):
    # rings: (z, rx, ry, cy); an elliptical family of horizontal sections
    verts=[]
    for z, rx, ry, cy in rings:
        for j in range(n):
            a = 2*pi*j/n
            verts.append((rx*cos(a), cy + ry*sin(a), z))
    faces=[]
    for i in range(len(rings)-1):
        for j in range(n):
            nj=(j+1)%n
            faces.append((i*n+j, i*n+nj, (i+1)*n+nj, (i+1)*n+j))
    if cap_bottom:
        cb=len(verts); z,rx,ry,cy=rings[0]; verts.append((0,cy,z))
        for j in range(n): faces.append((cb,j,(j+1)%n))
    if cap_top:
        ct=len(verts); z,rx,ry,cy=rings[-1]; verts.append((0,cy,z))
        off=(len(rings)-1)*n
        for j in range(n): faces.append((ct,off+(j+1)%n,off+j))
    mesh=bpy.data.meshes.new(name+' • mesh')
    mesh.from_pydata(verts, [], faces); mesh.update()
    obj=bpy.data.objects.new(name, mesh)
    (col or root).objects.link(obj)
    assign_mat(obj,mat); smooth(obj)
    if subsurf:
        m=obj.modifiers.new('Sculpted continuity','SUBSURF'); m.levels=subsurf; m.render_levels=subsurf
    return obj

def curve_tube(name, pts, radius, mat, col=None, cyclic=False, convert=True):
    cu=bpy.data.curves.new(name+' • curve','CURVE')
    cu.dimensions='3D'; cu.resolution_u=16; cu.render_resolution_u=24
    cu.bevel_depth=radius; cu.bevel_resolution=4; cu.resolution_u=16
    cu.fill_mode='FULL'; cu.use_fill_caps=True
    sp=cu.splines.new('BEZIER'); sp.bezier_points.add(len(pts)-1)
    for bp, co in zip(sp.bezier_points, pts):
        bp.co=co; bp.handle_left_type='AUTO'; bp.handle_right_type='AUTO'
    sp.use_cyclic_u=cyclic
    obj=bpy.data.objects.new(name,cu); (col or root).objects.link(obj)
    assign_mat(obj,mat)
    if convert:
        bpy.context.view_layer.objects.active=obj; obj.select_set(True)
        try: bpy.ops.object.convert(target='MESH')
        except Exception: pass
        obj.select_set(False)
        smooth(obj)
    return obj

def raised_patch(name, outline, center, back_y, mat, col=None, bevel=0.018):
    # outline entries are (x,z,y_edge), ordered around the patch
    n=len(outline)
    verts=[(center[0],center[1],center[2])]
    verts += [(x,y,z) for x,z,y in outline]
    verts += [(x,back_y,z) for x,z,y in outline]
    faces=[]
    for i in range(n):
        j=(i+1)%n
        faces.append((0,1+i,1+j))
        faces.append((1+i,1+n+i,1+n+j,1+j))
    faces.append(tuple(1+n+i for i in range(n)))
    me=bpy.data.meshes.new(name+' • mesh'); me.from_pydata(verts,[],faces); me.update()
    obj=bpy.data.objects.new(name,me); (col or root).objects.link(obj)
    assign_mat(obj,mat); smooth(obj)
    if bevel: add_bevel(obj,bevel,2,apply=False)
    return obj

def polygon_prism(name, xz_points, front_y, depth, mat, col=None, bevel=0.018):
    n=len(xz_points)
    verts=[(x,front_y,z) for x,z in xz_points] + [(x,front_y+depth,z) for x,z in xz_points]
    faces=[]
    # front face; reverse winding not critical with material double-sided
    faces.append(tuple(range(n)))
    faces.append(tuple(range(2*n-1,n-1,-1)))
    for i in range(n):
        j=(i+1)%n
        faces.append((i,j,n+j,n+i))
    me=bpy.data.meshes.new(name+' • mesh'); me.from_pydata(verts,[],faces); me.update()
    obj=bpy.data.objects.new(name,me); (col or root).objects.link(obj)
    assign_mat(obj,mat); smooth(obj)
    if bevel: add_bevel(obj,bevel,3,apply=False)
    return obj

def small_chip(name, pts, mat, col=None):
    # little tetrahedral chip that catches a dusty grey highlight at the socle edge
    me=bpy.data.meshes.new(name+' • mesh')
    me.from_pydata(pts,[],[(0,1,2),(0,3,1),(1,3,2),(2,3,0)])
    me.update(); obj=bpy.data.objects.new(name,me); (col or root).objects.link(obj); assign_mat(obj,mat)
    for p in me.polygons: p.use_smooth=False
    return obj

# PEDESTAL --------------------------------------------------------------------
# A restrained, catalogue-like limestone column, with dressed transitions.
cube_beveled('Pedestal • broad lower plinth', (0,0,0.20), (2.95,2.55,0.40), stone, .075, collections['PEDESTAL'])
cube_beveled('Pedestal • upper plinth step', (0,0,0.49), (2.58,2.28,0.20), stone, .05, collections['PEDESTAL'])
cylinder('Pedestal • octagonal shaft', (0,0,1.40), .78, 1.72, stone, vertices=12, bevel=.045, col=collections['PEDESTAL'])
cylinder('Pedestal • lower collar', (0,0,0.58), .91, .16, stone, vertices=48, bevel=.035, col=collections['PEDESTAL'])
cylinder('Pedestal • upper collar', (0,0,2.24), .90, .16, stone, vertices=48, bevel=.035, col=collections['PEDESTAL'])
cube_beveled('Pedestal • square cap', (0,0,2.40), (2.00,1.95,0.23), stone, .055, collections['PEDESTAL'])
# integral light socle below the bust
cylinder('Bust • integral socle', (0,0,2.57), .80, .18, plaster, vertices=64, bevel=.035, col=collections['BUST • FORM'])

# BUST / SHOULDERS -------------------------------------------------------------
shoulder_rings=[
    (2.66,1.10,.54,0.00),
    (2.75,1.38,.66,0.00),
    (2.95,1.52,.74,0.00),
    (3.18,1.50,.74,0.01),
    (3.42,1.34,.68,0.02),
    (3.65,1.08,.58,0.03),
    (3.86,.78,.50,0.00),
    (4.02,.52,.42,-.03),
]
shoulders=loft_rings('Bust • broken-edge shoulders', shoulder_rings, 64, plaster, collections['BUST • FORM'], True, True, subsurf=1)
# tiny dressed break at the bust termination, intentionally geometric
cylinder('Bust • termination rim', (0,0,2.72), .92, .09, plaster, vertices=64, bevel=.018, col=collections['BUST • FORM'])

# Neck and anatomical transition
neck_rings=[
    (3.00,.36,.32,.02),
    (3.25,.40,.35,.02),
    (3.52,.43,.38,.00),
    (3.78,.46,.41,-.01),
    (4.02,.51,.44,-.04),
    (4.24,.59,.48,-.08),
    (4.39,.57,.48,-.10),
]
neck=loft_rings('Bust • tapered neck', neck_rings, 56, plaster, collections['BUST • FORM'], True, True, subsurf=1)
# subtle sternocleidomastoid sweeps, not costume—just the underlying anatomy
for s in (-1,1):
    curve_tube('Neck • sternomastoid plane '+('L' if s<0 else 'R'),
               [(s*.39,-.30,3.24),(s*.34,-.36,3.52),(s*.25,-.39,3.86),(s*.27,-.36,4.20),(s*.34,-.30,4.38)],
               .055, plaster, collections['FACE • SCULPTED PLANES'])

# HEAD ------------------------------------------------------------------------
head_rings=[
    (4.00,.50,.46,.00),
    (4.16,.70,.60,.00),
    (4.38,.90,.74,.00),
    (4.68,1.06,.84,.015),
    (5.06,1.15,.90,.025),
    (5.50,1.18,.92,.04),
    (5.92,1.14,.90,.065),
    (6.28,1.04,.84,.09),
    (6.58,.88,.75,.11),
    (6.83,.63,.60,.12),
    (7.00,.25,.34,.13),
]
head=loft_rings('Head • idealized antique cranium', head_rings, 72, plaster, collections['BUST • FORM'], True, True, subsurf=1)

# Chin / jaw mass, blended visually into the lower head
uv_ellipsoid('Face • projecting classical chin', (0,-.73,4.48), (.44,.22,.27), plaster, 48,24,collections['FACE • SCULPTED PLANES'])
# subtle mandible planes on each side
for s in (-1,1):
    uv_ellipsoid('Face • jaw plane '+('L' if s<0 else 'R'), (s*.50,-.56,4.57), (.38,.18,.27), plaster, 40,20,collections['FACE • SCULPTED PLANES'])

# EARS: integrated flattened masses plus a raised helix/antihelix
for s in (-1,1):
    side='L' if s<0 else 'R'
    uv_ellipsoid('Ear • carved auricle '+side, (s*1.06,-.03,5.29), (.27,.18,.48), plaster, 40,24,collections['FACE • SCULPTED PLANES'])
    helix=[(s*1.01,-.185,5.67),(s*1.17,-.19,5.55),(s*1.23,-.19,5.31),(s*1.19,-.19,5.07),(s*1.08,-.19,4.92),(s*.99,-.18,5.02),(s*1.00,-.19,5.26),(s*1.06,-.19,5.47)]
    curve_tube('Ear • helix rim '+side, helix, .035, plaster, collections['FACE • SCULPTED PLANES'])
    anti=[(s*1.055,-.205,5.48),(s*1.12,-.215,5.31),(s*1.07,-.215,5.12),(s*1.02,-.20,5.08)]
    curve_tube('Ear • antihelix '+side, anti, .028, plaster_lip, collections['FACE • SCULPTED PLANES'])
    uv_ellipsoid('Ear • concha shadow '+side, (s*1.10,-.205,5.30), (.095,.035,.19), recess_mat, 32,16,collections['FACE • SCULPTED PLANES'])

# BROW RIDGES and eye sockets -------------------------------------------------
for s in (-1,1):
    side='L' if s<0 else 'R'
    # angular but softened brow mass—this is the bridge-to-nose step
    brow=uv_ellipsoid('Face • brow ridge '+side, (s*.43,-.805,5.76), (.43,.135,.115), plaster, 48,20,collections['FACE • SCULPTED PLANES'])
    brow.rotation_euler[1] = s*math.radians(5)
    # small glabella wedge tying the brows together
    if s==1:
        uv_ellipsoid('Face • glabella plane', (0,-.83,5.72), (.19,.12,.16), plaster, 40,20,collections['FACE • SCULPTED PLANES'])
    # blank antique eye: smooth, sculpted convex eye with no painted iris/pupil
    uv_ellipsoid('Face • blank antique eye '+side, (s*.43,-.865,5.56), (.255,.115,.155), plaster_lip, 48,24,collections['FACE • SCULPTED PLANES'])
    # eyelid rolls; all same stone family, no graphic makeup
    upper=[(s*.20,-.975,5.56),(s*.30,-.995,5.665),(s*.46,-1.005,5.695),(s*.62,-.985,5.62),(s*.69,-.965,5.56)]
    lower=[(s*.20,-.972,5.54),(s*.31,-.987,5.47),(s*.47,-.988,5.445),(s*.62,-.972,5.49),(s*.68,-.958,5.54)]
    curve_tube('Face • upper eyelid '+side, upper, .038, plaster, collections['FACE • SCULPTED PLANES'])
    curve_tube('Face • lower eyelid '+side, lower, .027, plaster_lip, collections['FACE • SCULPTED PLANES'])
    # restrained outer canthus wedge, keeping the eyes embedded rather than pasted on
    uv_ellipsoid('Face • outer canthus '+side, (s*.69,-.94,5.56), (.085,.055,.065), plaster, 32,16,collections['FACE • SCULPTED PLANES'])

# Cheekbones as broad, sloping raised planes (not spherical stickers)
for s in (-1,1):
    side='L' if s<0 else 'R'
    outline=[
        (s*.18,5.34,-.86), (s*.37,5.46,-.90), (s*.66,5.39,-.88),
        (s*.88,5.20,-.82), (s*.72,5.04,-.84), (s*.43,5.02,-.88), (s*.20,5.16,-.89)
    ]
    raised_patch('Face • zygomatic plane '+side, outline, (s*.51,-.985,5.21), -.755, plaster, collections['FACE • SCULPTED PLANES'], .022)
    # soft plane under cheekbone gives the classical planar turn into the jaw
    uv_ellipsoid('Face • lower cheek turn '+side, (s*.62,-.70,4.92), (.40,.12,.25), plaster, 40,20,collections['FACE • SCULPTED PLANES'])

# NOSE: tapered bridge and a clear, strong classical tip ---------------------
nose_rings=[
    (5.74,.18,.095,-.79),
    (5.57,.145,.11,-.91),
    (5.32,.12,.13,-1.00),
    (5.06,.115,.14,-1.075),
    (4.84,.18,.16,-1.13),
    (4.70,.235,.135,-1.10),
    (4.62,.205,.105,-1.055),
]
nose=loft_rings('Face • straight classical nose', nose_rings, 48, plaster, collections['FACE • SCULPTED PLANES'], True, True, subsurf=1)
# wings and underside plane
for s in (-1,1):
    side='L' if s<0 else 'R'
    uv_ellipsoid('Face • alar wing '+side, (s*.19,-1.095,4.66), (.16,.12,.105), plaster, 40,20,collections['FACE • SCULPTED PLANES'])
    uv_ellipsoid('Face • nostril recess '+side, (s*.115,-1.208,4.655), (.070,.024,.040), recess_mat, 28,14,collections['FACE • SCULPTED PLANES'])
# nose underside bridge, kept subtle
curve_tube('Face • columella underside', [(-.075,-1.205,4.65),(0,-1.225,4.62),(.075,-1.205,4.65)], .022, plaster_lip, collections['FACE • SCULPTED PLANES'])

# MOUTH, philtrum, and strong antique lip planes -----------------------------
# raised muzzle plane ties nose to mouth without creating a cartoon muzzle
uv_ellipsoid('Face • restrained muzzle plane', (0,-.79,4.89), (.40,.115,.18), plaster, 44,22,collections['FACE • SCULPTED PLANES'])
upper_lip=[(-.285,4.86),(-.18,4.92),(-.065,4.84),(0,4.815),(.065,4.84),(.18,4.92),(.285,4.86),(.19,4.77),(0,4.755),(-.19,4.77)]
polygon_prism('Face • chiseled upper lip', upper_lip, -1.015, .075, plaster_lip, collections['FACE • SCULPTED PLANES'], .018)
lower_lip=[(-.265,4.755),(-.14,4.72),(0,4.695),(.14,4.72),(.265,4.755),(.19,4.635),(0,4.605),(-.19,4.635)]
polygon_prism('Face • full lower lip', lower_lip, -1.025, .085, plaster_lip, collections['FACE • SCULPTED PLANES'], .022)
# mouth seam and philtrum grooves in dusty recess grey, very fine and controlled
curve_tube('Face • mouth separation', [(-.25,-1.108,4.765),(-.12,-1.115,4.775),(0,-1.118,4.77),(.12,-1.115,4.775),(.25,-1.108,4.765)], .014, recess_mat, collections['FACE • SCULPTED PLANES'])
for s in (-1,1):
    curve_tube('Face • philtrum ridge '+('L' if s<0 else 'R'), [(s*.07,-1.055,4.835),(s*.075,-1.06,4.77),(s*.065,-1.065,4.715)], .022, plaster, collections['FACE • SCULPTED PLANES'])
# slight mentolabial plane below lower lip
curve_tube('Face • mentolabial crease', [(-.17,-.99,4.57),(0,-1.01,4.545),(.17,-.99,4.57)], .016, recess_mat, collections['FACE • SCULPTED PLANES'])

# HAIR ------------------------------------------------------------------------
# Deep cap establishes real volume; the visible surface is then built from discrete carved locks.
haircap=uv_ellipsoid('Hair • underlying carved mass', (0,.12,6.36), (1.30,.98,.78), hair_mat, 64,32,collections['HAIR • CLUMPED CURLS'])
# Back-side mass for a convincing 360-degree GLB orbit
uv_ellipsoid('Hair • occipital mass', (0,.38,5.99), (1.18,.76,.75), hair_mat, 56,28,collections['HAIR • CLUMPED CURLS'])

# Front classical comma-locks: each is a deliberate sculpted clump, not noise.
front_xs=[-.92,-.68,-.44,-.20,.07,.34,.61,.86]
for i,x in enumerate(front_xs):
    s= -1 if x<0 else 1
    ztop=6.84 - .06*abs(x)
    pts=[
        (x-.10*s,-.30,ztop),
        (x-.11*s,-.48,6.70-.03*abs(x)),
        (x-.03*s,-.69,6.52-.03*abs(x)),
        (x+.09*s,-.83,6.34-.02*abs(x)),
        (x+.16*s,-.88,6.22-.015*abs(x)),
        (x+.07*s,-.91,6.15-.01*abs(x)),
        (x-.06*s,-.87,6.19-.01*abs(x)),
    ]
    r=.105 + .018*(i%3==0)
    curve_tube('Hair • forehead clump %02d'%(i+1), pts, r, hair_mat, collections['HAIR • CLUMPED CURLS'])
# side locks descending in front of the ears and temples
for s in (-1,1):
    side='L' if s<0 else 'R'
    for k,off in enumerate((0.0,.20)):
        x0=s*(1.00+off)
        pts=[(s*(.92+off),-.30,6.66-off*.25),(s*(1.10+off),-.56,6.46-off*.1),
             (s*(1.12+off),-.75,6.18-off*.15),(s*(1.05+off),-.82,5.94-off*.15),
             (s*(1.16+off),-.75,5.78-off*.1),(s*(1.21+off),-.61,5.68-off*.1),
             (s*(1.10+off),-.53,5.62-off*.1)]
        curve_tube('Hair • temple curl '+side+' '+str(k+1), pts, .115 if k==0 else .095, hair_mat, collections['HAIR • CLUMPED CURLS'])
# crown waves running backward, visible in the three-quarter silhouette
for i,x in enumerate([-.85,-.45,0,.42,.82]):
    pts=[(x,-.34,6.92),(x+.08,-.08,7.04),(x+.14,.22,6.98),(x+.05,.48,6.82),(x-.10,.59,6.69)]
    curve_tube('Hair • crown wave %02d'%(i+1), pts, .12, hair_mat, collections['HAIR • CLUMPED CURLS'])
# a few rear curls give the orbit view recognizable layered hair
for s in (-1,1):
    for k,z in enumerate((6.52,6.15,5.82)):
        pts=[(s*(.94+.08*k),.48,z+.18),(s*(1.18+.08*k),.61,z+.05),(s*(1.28+.05*k),.50,z-.16),(s*(1.16+.03*k),.35,z-.27),(s*(1.03+.02*k),.32,z-.16)]
        curve_tube('Hair • rear side curl '+('L' if s<0 else 'R')+' '+str(k+1), pts, .11, hair_mat, collections['HAIR • CLUMPED CURLS'])
# small shadowed channels between selected locks, like incised carving
for x in [-.78,-.30,.28,.72]:
    curve_tube('Hair • carved channel', [(x,-.94,6.30),(x+.04,-.90,6.48),(x-.02,-.78,6.67)], .017, recess_mat, collections['HAIR • CLUMPED CURLS'])

# SOCLE chips and faint casting marks ----------------------------------------
# deliberately tiny, placed at lower visible termination; they break perfection without damage theatrics
small_chip('Socle • chip 01', [(.70,-.70,2.69),(.79,-.69,2.72),(.74,-.69,2.79),(.73,-.65,2.73)], recess_mat, collections['BUST • FORM'])
small_chip('Socle • chip 02', [(-.84,-.48,2.70),(-.76,-.50,2.72),(-.79,-.48,2.78),(-.82,-.44,2.73)], recess_mat, collections['BUST • FORM'])
# a fine, almost invisible mold seam around the base termination
curve_tube('Bust • faint casting seam', [(0,-.84,2.77),(.45,-.73,2.77),(.76,-.47,2.77),(.85,0,2.77)], .008, recess_mat, collections['BUST • FORM'])

# STUDIO BACKDROP -------------------------------------------------------------
# A large seamless sweep, so the bust reads as a cast-collection photograph rather than a room.
xs=[-12.0,12.0]
profile=[(-7.0,-.04),(1.6,-.04),(2.25,.02),(2.80,.22),(3.25,.62),(3.57,1.18),(3.74,1.90),(3.78,3.0),(3.78,11.0)]
verts=[]
for y,z in profile:
    verts.extend([(xs[0],y,z),(xs[1],y,z)])
faces=[]
for i in range(len(profile)-1):
    a=2*i; b=2*(i+1)
    faces.append((a,b,b+1,a+1))
me=bpy.data.meshes.new('Studio cyclorama • mesh'); me.from_pydata(verts,[],faces); me.update()
back=bpy.data.objects.new('Studio • seamless neutral sweep',me); collections['STUDIO • LIGHT & BACKDROP'].objects.link(back); assign_mat(back,backdrop_mat)
for p in me.polygons: p.use_smooth=True

# LIGHTS ----------------------------------------------------------------------
def add_area(name, loc, energy, size, color, target=(0,0,5.1)):
    data=bpy.data.lights.new(name,'AREA'); data.energy=energy; data.shape='DISK'; data.size=size; data.color=color
    obj=bpy.data.objects.new(name,data); collections['STUDIO • LIGHT & BACKDROP'].objects.link(obj); obj.location=loc
    direction=Vector(target)-obj.location; obj.rotation_euler=direction.to_track_quat('-Z','Y').to_euler()
    return obj
# broad key rakes across the facial planes from camera-left
add_area('Studio key • large softbox', (-4.8,-6.5,9.2), 950, 4.2, (1.0,.91,.78), (0,-.15,5.25))
# gentle cool bounce preserves shadow-side cheek and jaw planes
add_area('Studio fill • north bounce', (4.5,-1.0,6.1), 300, 5.5, (.72,.82,1.0), (0,0,5.0))
# restrained back/rim to separate hair from neutral sweep
add_area('Studio rim • top rear', (-3.2,4.2,8.3), 700, 3.0, (1.0,.86,.68), (0,.2,5.8))
# low front reflector, very soft
add_area('Studio fill • low front', (0,-4.0,2.7), 90, 4.0, (1.0,.95,.88), (0,0,4.8))

# CAMERA ----------------------------------------------------------------------
def look_at(obj, target):
    obj.rotation_euler=(Vector(target)-obj.location).to_track_quat('-Z','Y').to_euler()
cam_data=bpy.data.cameras.new('Catalogue camera • 3/4 eye level')
cam=bpy.data.objects.new('Catalogue camera • 3/4 eye level',cam_data); collections['STUDIO • LIGHT & BACKDROP'].objects.link(cam)
cam.location=(6.65,-10.5,5.60); look_at(cam,(0,-.12,5.18)); cam_data.lens=66; cam_data.sensor_width=36
cam_data.dof.use_dof=False
scene.camera=cam

# Render / color management ---------------------------------------------------
scene.render.engine='BLENDER_EEVEE_NEXT'  # rapid composition validation; switched to Cycles before save
scene.render.resolution_x=640; scene.render.resolution_y=640; scene.render.resolution_percentage=100
scene.render.image_settings.file_format='PNG'
scene.render.film_transparent=False
scene.render.filepath='/tmp/classical_bust_preview.png'
scene.render.image_settings.color_mode='RGBA'
scene.world.color=(0.025,0.03,0.035)
if scene.world.use_nodes:
    bg=scene.world.node_tree.nodes.get('Background')
    if bg:
        bg.inputs['Color'].default_value=(0.025,0.030,0.038,1)
        bg.inputs['Strength'].default_value=.22
try:
    scene.view_settings.look='AgX - Medium High Contrast'
except Exception:
    try: scene.view_settings.look='AgX - Medium High Contrast'
    except Exception: pass
scene.view_settings.exposure=0.0
scene.view_settings.gamma=1.0

# Ground contact: a very subtle shadow-catching disk under the pedestal (backdrop already receives shadows)
# Keep the neutral sweep as the only environment geometry.

# Select head for an intuitive initial interactive view
bpy.ops.object.select_all(action='DESELECT')
head.select_set(True); bpy.context.view_layer.objects.active=head

# Save a first complete file and render a quick validation image
bpy.ops.wm.save_as_mainfile(filepath='/tmp/classical_head_bust.blend')
bpy.ops.render.render(write_still=True)
print('Built objects', len(bpy.data.objects), 'meshes', len(bpy.data.meshes), 'materials', len(bpy.data.materials))
result={'objects':len(bpy.data.objects),'meshes':len(bpy.data.meshes),'materials':len(bpy.data.materials),'preview':'/tmp/classical_bust_preview.png','blend':'/tmp/classical_head_bust.blend'}


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 463, in <module>
TypeError: bpy_struct: item.attr = val: enum "BLENDER_EEVEE_NEXT" not found in ('BLENDER_EEVEE', 'BLENDER_WORKBENCH', 'CYCLES')