"""Three original HUMAN Act sculptures, made in isolated background Blender.
Usage: Blender -b continuity.blend --python build-human-sculptures.py
Existing scenes/data are preserved. New assets are saved as separate libraries.
"""
import bpy, math, json, sys
from pathlib import Path
from mathutils import Vector
OUT=Path(__file__).resolve().parent
TAU=2*math.pi
print('PRESERVED_SCENES',[(s.name,len(s.objects)) for s in bpy.data.scenes])
scene=None; col=None; objects=[]; records=[]

def material(name,color,metal=0,rough=.55):
    m=bpy.data.materials.new('HUMAN / '+name);m.use_nodes=True;m.diffuse_color=(*color,1)
    n=m.node_tree.nodes;l=m.node_tree.links;p=n.get('Principled BSDF')
    p.inputs['Base Color'].default_value=(*color,1);p.inputs['Metallic'].default_value=metal;p.inputs['Roughness'].default_value=rough
    noise=n.new('ShaderNodeTexNoise');noise.inputs['Scale'].default_value=100;noise.inputs['Detail'].default_value=3
    bump=n.new('ShaderNodeBump');bump.inputs['Strength'].default_value=.14;bump.inputs['Distance'].default_value=.018
    l.new(noise.outputs['Fac'],bump.inputs['Height']);l.new(bump.outputs['Normal'],p.inputs['Normal'])
    return m
STONE=material('honed pale limestone',(.68,.615,.49))
ASH=material('warm ash stone',(.37,.35,.31))
BRONZE=material('brushed warm bronze',(.31,.175,.075),.78,.36)
RED=material('deep iron porphyry',(.18,.025,.019),0,.43)
GROUND=material('gallery mineral plaster',(.29,.27,.235),0,.86)

def link(name,data,asset=True):
    ob=bpy.data.objects.new('HUMAN / '+name,data);col.objects.link(ob)
    if asset:objects.append(ob)
    return ob
def mesh(name,verts,faces,mat,smooth=True,soft=0):
    me=bpy.data.meshes.new('HUMAN / '+name);me.from_pydata(verts,[],faces);me.update()
    ob=link(name,me);me.materials.append(mat)
    for p in me.polygons:p.use_smooth=smooth
    if soft:
        mod=ob.modifiers.new('Softened carved edge','BEVEL');mod.width=soft;mod.segments=3
        mod=ob.modifiers.new('Weighted normals','WEIGHTED_NORMAL');mod.keep_sharp=True
    return ob
def box(name,dims,loc,mat=STONE,asset=True):
    x,y,z=[a/2 for a in dims]
    ob=mesh(name,[(-x,-y,-z),(x,-y,-z),(x,y,-z),(-x,y,-z),(-x,-y,z),(x,-y,z),(x,y,z),(-x,y,z)],[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)],mat,False,.025)
    ob.location=loc
    if not asset:objects.remove(ob)
    return ob
def loft(name,rings,mat=STONE,n=64,pleat=0):
    verts=[]
    for z,rx,ry in rings:
        for i in range(n):
            a=TAU*i/n;f=1+pleat*math.cos(12*a)
            verts.append((rx*math.sin(a)*f,-ry*math.cos(a)*f,z))
    faces=[tuple(reversed(range(n))),tuple(range((len(rings)-1)*n,len(rings)*n))]
    for k in range(len(rings)-1):
        for i in range(n):a=k*n+i;b=k*n+(i+1)%n;faces.append((a,b,b+n,a+n))
    return mesh(name,verts,faces,mat)
def ellipsoid(name,loc,scale,mat=STONE):
    n=48;k=24;verts=[];faces=[]
    for i in range(k+1):
        p=math.pi*i/k
        for j in range(n):a=TAU*j/n;verts.append((scale[0]*math.sin(p)*math.cos(a),scale[1]*math.sin(p)*math.sin(a),scale[2]*math.cos(p)))
    for i in range(k):
        for j in range(n):a=i*n+j;b=i*n+(j+1)%n;faces.append((a,b,b+n,a+n))
    ob=mesh(name,verts,faces,mat);ob.location=loc;return ob
def tube(name,points,r,mat=BRONZE,cyclic=False):
    cu=bpy.data.curves.new('HUMAN / '+name,'CURVE');cu.dimensions='3D';cu.bevel_depth=r;cu.bevel_resolution=4;cu.resolution_u=16;cu.use_fill_caps=True
    sp=cu.splines.new('BEZIER');sp.bezier_points.add(len(points)-1)
    for p,v in zip(sp.bezier_points,points):p.co=v;p.handle_left_type='AUTO';p.handle_right_type='AUTO'
    sp.use_cyclic_u=cyclic;ob=link(name,cu);cu.materials.append(mat);return ob
def head(name,loc,scale=1,width=1,mat=STONE,turn=0,variant=0):
    # Carved mask-like faces: integrated nose, brow, cheeks and quiet mouth.
    # Forms deliberately abstract, with no claim to portrait likeness or ancestry.
    rows=38;n=96;verts=[];faces=[]
    profile=[(-.30,.08,.075),(-.24,.16,.135),(-.13,.225,.20),(0,.255,.235),(.13,.254,.244),(.25,.23,.22),(.34,.13,.13),(.375,.015,.015)]
    def interp(z):
        for i in range(len(profile)-1):
            a,b=profile[i:i+2]
            if a[0]<=z<=b[0]:t=(z-a[0])/(b[0]-a[0]);return a[1]*(1-t)+b[1]*t,a[2]*(1-t)+b[2]*t
        return profile[-1][1:]
    for k in range(rows+1):
        z=-.3+.675*k/rows;rx,ry=interp(z)
        for j in range(n):
            a=TAU*j/n;x=rx*math.sin(a)*width;y=-ry*math.cos(a)
            frontal=max(0,math.cos(a))**14
            nose=(.095+variant*.006)*math.exp(-((z-.018)/.115)**2)*math.exp(-(math.sin(a)/.16)**2)
            chin=.016*math.exp(-((z+.18)/.05)**2)
            mouth=-.012*math.exp(-((z+.117)/.013)**2)*math.exp(-(x/.115)**4)
            sockets=-.035*math.exp(-((abs(x)-.11)/.05)**2-((z-.10)/.039)**2)
            brow=.022*math.exp(-((z-.155)/.023)**2)
            y-=frontal*(nose+chin+mouth+sockets+brow)
            verts.append((x*scale,y*scale,z*scale))
    for k in range(rows):
        for j in range(n):a=k*n+j;b=k*n+(j+1)%n;faces.append((a,b,b+n,a+n))
    faces.extend([tuple(reversed(range(n))),tuple(range(rows*n,(rows+1)*n))])
    ob=mesh(name,verts,faces,mat);ob.location=loc;ob.rotation_euler.z=turn
    return ob
def person(name,x,y,h=3.0,width=1,mat=STONE,turn=0,gesture=0):
    start=len(objects);s=h/3;z=.25
    loft(name+' / draped body',[(z,.32*s*width,.21*s),(z+.20*s,.31*s*width,.20*s),(z+1.05*s,.22*s*width,.17*s),(z+1.42*s,.29*s*width,.185*s),(z+1.70*s,.405*s*width,.18*s),(z+1.85*s,.34*s*width,.15*s),(z+1.94*s,.11*s,.10*s)],mat,pleat=.015)
    loft(name+' / neck',[(z+1.91*s,.12*s,.10*s),(z+2.18*s,.105*s,.095*s)],mat)
    head(name+' / carved face',(0,-.008*s,z+2.44*s),s,width=.90+width*.1,mat=mat,variant=width)
    for side in [-1,1]:
        reach=side*(.42*s*width+gesture*.22*s)
        points=[(side*.34*s*width,0,z+1.78*s),(reach,-.10*s,z+1.37*s),(reach,-.16*s,z+1.08*s)]
        tube(name+' / arm '+str(side),points,.10*s,mat)
        ellipsoid(name+' / hand '+str(side),points[-1],(.068*s,.08*s,.105*s),mat)
    for ob in objects[start:]:
        local=ob.location.copy();ob.location=(x+local.x*math.cos(turn)-local.y*math.sin(turn),y+local.x*math.sin(turn)+local.y*math.cos(turn),local.z);ob.rotation_euler.z+=turn
    return objects[start:]
def seated(name,x,y,mat=STONE):
    start=len(objects);z=.25
    box(name+' / seat',(.88,.87,.12),(0,0,z+.80),BRONZE)
    box(name+' / back',(.85,.09,.71),(0,.38,z+1.18),BRONZE)
    loft(name+' / torso',[(z+.79,.28,.23),(z+1.32,.36,.20),(z+1.53,.37,.17),(z+1.63,.12,.10)],mat)
    loft(name+' / neck',[(z+1.6,.11,.10),(z+1.85,.105,.095)],mat)
    head(name+' / face',(0,-.035,z+2.06),.95,1.10,mat)
    for side in [-1,1]:
        tube(name+' / arm',[(side*.32,0,z+1.45),(side*.43,-.10,z+1.07),(side*.29,-.34,z+.95)],.095,mat)
        ellipsoid(name+' / hand',(side*.29,-.36,z+.93),(.075,.1,.065),mat)
        tube(name+' / legs',[(side*.18,-.16,z+.80),(side*.22,-.56,z+.66),(side*.22,-.70,z+.19)],.13,mat)
        ellipsoid(name+' / foot',(side*.22,-.83,z+.14),(.125,.23,.10),mat)
        points=[(side*.51,.11+.52*math.cos(TAU*i/48),z+.53+.52*math.sin(TAU*i/48)) for i in range(48)]
        tube(name+' / wheel',points,.035,BRONZE,True)
        for i in range(8):
            a=TAU*i/8;tube(name+' / spoke',[(side*.51,.11,z+.53),(side*.51,.11+.50*math.cos(a),z+.53+.50*math.sin(a))],.009,BRONZE)
        tube(name+' / chassis',[(side*.45,.40,z+.75),(side*.45,-.49,z+.45),(side*.40,-.56,z+.09)],.027,BRONZE)
        tube(name+' / caster',[(side*.40,-.55+.08*math.cos(TAU*i/24),z+.09+.08*math.sin(TAU*i/24)) for i in range(24)],.021,BRONZE,True)
    for ob in objects[start:]:ob.location.x+=x;ob.location.y+=y
def aim(ob,p):ob.rotation_euler=(Vector(p)-ob.location).to_track_quat('-Z','Y').to_euler()
def setup(key,title,scale=9,target=(0,0,1.7)):
    global scene,col,objects
    scene=bpy.data.scenes.new('HUMAN / '+title);bpy.context.window.scene=scene
    col=bpy.data.collections.new('HUMAN / '+key);scene.collection.children.link(col);objects=[]
    box('Gallery floor',(200,200,.1),(0,0,-.08),GROUND,False)
    cam=link('Gallery camera',bpy.data.cameras.new('HUMAN / '+key),False);cam.location=(6,-12,6.1);aim(cam,target);cam.data.type='ORTHO';cam.data.ortho_scale=scale;scene.camera=cam
    for name,loc,energy,size,color in [('Window',(-4,-5,9),1900,4.5,(1,.89,.77)),('Sky',(5,-3,7),650,6,(.79,.87,1)),('Rim',(-1,5,8),1500,3.5,(1,.95,.83))]:
        d=bpy.data.lights.new('HUMAN / '+name,'AREA');d.energy=energy;d.shape='DISK';d.size=size;d.color=color;ob=link(name,d,False);ob.location=loc;aim(ob,target)
    w=bpy.data.worlds.new('HUMAN / soft gallery');w.use_nodes=True;w.node_tree.nodes['Background'].inputs[0].default_value=(.26,.24,.21,1);w.node_tree.nodes['Background'].inputs[1].default_value=.30;scene.world=w
    scene.render.engine='CYCLES';scene.cycles.samples=48;scene.cycles.use_denoising=True;scene.cycles.max_bounces=6
    prefs=bpy.context.preferences.addons['cycles'].preferences
    try:
        prefs.compute_device_type='METAL';prefs.get_devices()
        for d in prefs.devices:d.use=(d.type=='METAL')
        if any(d.type=='METAL' for d in prefs.devices):scene.cycles.device='GPU'
    except Exception:pass
    scene.render.resolution_x=1800;scene.render.resolution_y=1200;scene.render.resolution_percentage=100
    scene.render.image_settings.file_format='PNG';scene.render.image_settings.color_mode='RGB';scene.render.filepath=str(OUT/(key+'.png'))
    scene.view_settings.view_transform='AgX';scene.view_settings.look='AgX - Medium High Contrast'
    scene['asset_title']=title;scene['concept']='Human equality, care and autonomy; imagined sculpture, no real subject or installed monument.'
def finish(key,title):
    bpy.context.view_layer.update()
    # The GLB includes only sculpture objects, never the gallery, camera or lights.
    for ob in scene.objects:ob.select_set(False)
    for ob in objects:ob.select_set(True)
    bpy.context.view_layer.objects.active=objects[0]
    bpy.ops.export_scene.gltf(filepath=str(OUT/(key+'.glb')),export_format='GLB',use_selection=True,use_active_scene=True,export_apply=True,export_animations=False,export_cameras=False,export_lights=False,export_extras=True)
    bpy.data.libraries.write(str(OUT/(key+'.blend')),{scene},fake_user=True,compress=True)
    record={'key':key,'title':title,'scene':scene.name,'objects':len(objects),'meshVertices':sum(len(ob.data.vertices) for ob in objects if ob.type=='MESH'),'blend':key+'.blend','model':key+'.glb','render':key+'.png','dimensions':[1800,1200],'engine':'Blender 5.1.2 / Cycles / AgX','authorship':'Original procedural geometry; no imported models or AI image-to-mesh conversion'}
    records.append(record);(OUT/'blender-manifest.json').write_text(json.dumps(records,indent=2))
    bpy.ops.render.render(write_still=True,scene=scene.name)
    print('HUMAN_ASSET',json.dumps(record))

setup('human-common-ground','Common ground',10.1,(0,0,1.55))
box('Shared low plinth',(7.85,3,.25),(0,0,.125),STONE)
tube('Bronze common ground',[(-3.70,-1.30,.257),(3.70,-1.30,.257)],.012,BRONZE)
person('Older person',-2.85,.2,3.10,1.03,STONE,turn=-.09)
person('Young person',-1.93,-.36,2.15,.92,STONE,turn=-.02)
person('Broad shouldered person',-.9,.18,3.5,1.30,BRONZE,turn=.04)
seated('Person with mobility chair',.28,-.47,STONE)
person('Adult',1.45,.16,3.22,.91,STONE,turn=-.09)
person('Broad bodied adult',2.40,-.22,3.12,1.23,STONE,turn=.08)
person('Adult in bronze',3.25,.23,3.45,.91,BRONZE,turn=.1)
finish('human-common-ground','Common ground')

setup('human-mutual-care','The space between',8.2,(0,0,1.8))
box('Shared care foundation',(4.5,3,.25),(0,0,.125),ASH)
person('Older adult',-1.13,.1,3.2,1.16,STONE,turn=-.17,gesture=.4)
person('Child',0,-.26,1.85,.85,BRONZE,gesture=.25)
person('Adult',1.15,.16,3.40,.96,STONE,turn=.17,gesture=.4)
tube('Reciprocal bronze connection',[(-1.70,.7,.26),(-1.38,.78,2.23),(0,.88,3.95),(1.46,.77,2.3),(1.85,.7,.26)],.045,BRONZE)
box('Porphyry present-generation inlay',(3.6,.07,.014),(0,-1.16,.257),RED)
finish('human-mutual-care','The space between')

setup('human-autonomy','Unmeasured',7.5,(0,0,1.48))
box('Equal dignity foundation',(5.65,2.7,.25),(0,0,.125),STONE)
for i,(x,w,t,mat) in enumerate([(-1.75,1.13,-.10,STONE),(0,.93,0,BRONZE),(1.75,1.04,.10,STONE)]):
    name='Individual %d'%(i+1)
    ob=loft(name+' / shoulder bust',[(.25,.58,.37),(.44,.68,.40),(.95,.64,.35),(1.18,.43,.25),(1.29,.23,.19)],mat);ob.location.x=x
    ob=loft(name+' / neck',[(1.16,.23,.19),(1.72,.21,.18)],mat);ob.location.x=x
    head(name+' / face',(x,-.045,2.18),1.76,w,mat,t,variant=i)
    tube(name+' / open bronze arc',[(x-.68,.66,.26),(x-.91,.69,1.7),(x,.72,3.17),(x+.91,.69,1.7),(x+.68,.66,.26)],.022,BRONZE)
box('A right held in common / red inlay',(5.15,.055,.018),(0,-1.16,.262),RED)
finish('human-autonomy','Unmeasured')
result={'created':records,'existingSourceUnchanged':True}
print('HUMAN_RESULT',json.dumps(result))
