"""Generated texture mesh -> matched CC0 rig. Candidate only, never overwrites a delivered GLB."""
import bpy,bmesh,sys,json,math,numpy as np
from pathlib import Path
from mathutils import Vector,Matrix
from mathutils.kdtree import KDTree
P=Path('/opt/creature-lib-20261017');kind=sys.argv[sys.argv.index('--')+1];D=P/'realistic'/kind
bpy.ops.wm.read_factory_settings(use_empty=True);bpy.context.scene.render.fps=30
bpy.ops.import_scene.gltf(filepath=str(D/'raw.glb'))
real=next(o for o in bpy.context.scene.objects if o.type=='MESH')
coords=np.array([list(real.matrix_world@v.co) for v in real.data.vertices]);cen=coords[:,:2].mean(axis=0);eigval,eigvec=np.linalg.eigh(np.cov(coords[:,:2].T));forward=eigvec[:,-1]
headxy=coords[coords[:,2]>np.percentile(coords[:,2],96),:2].mean(axis=0)
if np.dot(headxy-cen,forward)<0:forward=-forward
ang=-math.pi/2-math.atan2(forward[1],forward[0]);rot=Matrix.Rotation(ang,4,'Z');real.parent=None;real.matrix_world=Matrix.Identity(4)
for v,co in zip(real.data.vertices,coords):v.co=rot@Vector(co)
entry=json.loads((P/'evidence'/f'enemy-{kind}-entry.json').read_text());height=entry['height_m']
lo=Vector([min(v.co[i] for v in real.data.vertices) for i in range(3)]);hi=Vector([max(v.co[i] for v in real.data.vertices) for i in range(3)])
scale=height/(hi.z-lo.z);offset=Vector((-(lo.x+hi.x)/2,-(lo.y+hi.y)/2,-lo.z))
for v in real.data.vertices:v.co=(v.co+offset)*scale
real.name='Realistic'+kind.title();real.data.name=real.name+'Mesh'
for p in real.data.polygons:p.use_smooth=True
for im in bpy.data.images:
 if im.type=='IMAGE' and im.size[0]>1024:im.scale(1024,1024);im.pack()
# Generated metal maps sometimes label fur as solid metal. Fur is dielectric.
for mat in real.data.materials:
 if mat and mat.use_nodes:
  bs=mat.node_tree.nodes.get('Principled BSDF')
  if bs:
   for socket,value in [('Metallic',0.),('Roughness',.86)]:
    for link in list(bs.inputs[socket].links):mat.node_tree.links.remove(link)
    bs.inputs[socket].default_value=value
bm=bmesh.new();bm.from_mesh(real.data);bad=[f for f in bm.faces if f.calc_area()<1e-14]
if bad:bmesh.ops.delete(bm,geom=bad,context='FACES_ONLY');bm.to_mesh(real.data)
bm.free()
bpy.ops.import_scene.gltf(filepath=str(P/'glb'/f'enemy-{kind}.glb'))
rig=next(o for o in bpy.context.scene.objects if o.type=='ARMATURE');donors=[o for o in bpy.context.scene.objects if o.type=='MESH' and o!=real and (any(m.type=='ARMATURE' for m in o.modifiers) or o.parent_type=='BONE')]
rig.data.pose_position='REST';rig.animation_data.action=None
for t in rig.animation_data.nla_tracks:t.mute=True
bpy.context.view_layer.update()
pts=[];weights=[]
for donor in donors:
 for v in donor.data.vertices:
  pts.append(donor.matrix_world@v.co)
  ww={donor.vertex_groups[g.group].name:g.weight for g in v.groups if g.weight>1e-6}
  if not ww:ww={donor.parent_bone if donor.parent_type=='BONE' else 'Head':1.}
  weights.append(ww)
# Four paw clusters, split in front/back by low-foot centre instead of torso bbox.
def paws(vs):
 low=vs[vs[:,2]<height*(.055 if kind in ['stag','deer','fox'] else .22)];split=(np.percentile(low[:,1],10)+np.percentile(low[:,1],90))/2;xsplit=0 if kind in ['stag','deer','fox'] else (np.percentile(low[:,0],10)+np.percentile(low[:,0],90))/2
 output={}
 for xs in [-1,1]:
  for ys in [-1,1]:
   sub=low[((low[:,0]-xsplit)*xs>0)&((low[:,1]-split)*ys>0)]
   if len(sub)<3:raise ValueError('Cannot separate four paws: '+str((xs,ys,len(sub))))
   if kind not in ['stag','deer','fox']:sub=sub[sub[:,2]<sub[:,2].min()+height*.055]
   output[f'{xs},{ys}']=sub.mean(axis=0).tolist()
 return output

r=paws(np.array([list(v.co) for v in real.data.vertices]));d=paws(np.array([list(v) for v in pts]));rf=np.mean([r[f'{x},-1'][1] for x in [-1,1]]);rb=np.mean([r[f'{x},1'][1] for x in [-1,1]]);df=np.mean([d[f'{x},-1'][1] for x in [-1,1]]);db=np.mean([d[f'{x},1'][1] for x in [-1,1]])
along=(db-df)/(rb-rf);shift=df-along*rf
rx=[np.mean([r[f'{x},{y}'][0] for y in [-1,1]]) for x in [-1,1]];dx=[np.mean([d[f'{x},{y}'][0] for y in [-1,1]]) for x in [-1,1]];across=(dx[1]-dx[0])/(rx[1]-rx[0]);xshift=np.mean(dx)-np.mean(rx)*across
# Across correction fades above the legs, preserving a realistic chest width.
for v in real.data.vertices:
 v.co.y=v.co.y*along+shift
 blend=max(0.,min(1.,(.60*height-v.co.z)/(.30*height)));v.co.x=v.co.x*(1+blend*(across-1))+blend*xshift
kd=KDTree(len(pts))
for i,p in enumerate(pts):kd.insert(p,i)
kd.balance();bonepos={b.name:{'head':rig.matrix_world@b.head_local,'tail':rig.matrix_world@b.tail_local} for b in rig.data.bones}
for vg in list(real.vertex_groups):real.vertex_groups.remove(vg)
for b in rig.data.bones:real.vertex_groups.new(name=b.name)
diststats=[];rigid_antlers=0
for v in real.data.vertices:
 nearest=kd.find_n(v.co,8);ww={};den=0
 for p,idx,dist in nearest:
  fac=1/max(dist,height*.008)**2;den+=fac
  for name,w in weights[idx].items():ww[name]=ww.get(name,0)+w*fac
 ww={k:w/den for k,w in ww.items()}
 # Prevent cross-leg influence, especially at narrow hocks and under the belly.
 if v.co.z<height*.47:
  side='.L' if v.co.x>0 else '.R'
  side_candidates={k:w for k,w in ww.items() if not k.endswith(('.L','.R')) or k.endswith(side)}
  if sum(side_candidates.values())>.1:ww=side_candidates
 # Every antler vertex, including its root, follows Head rigidly.
 if kind=='stag' and v.co.z>height*.755:ww={'Head':1.};rigid_antlers+=1
 # Tail corridor behind rump, isolated from leg joints. Short deer tails have no such corridor.
 tail=bonepos.get('Tail1')
 if kind in ['fox','dog','horse'] and tail:
  root=tail['head'];limit=root.y+height*.06
  if v.co.y>limit and abs(v.co.x)<height*.20:
   blend=min(1.,max(0.,(v.co.y-limit)/(height*.08)));ww={k:w*(1-blend) for k,w in ww.items()};ww['Tail1']=ww.get('Tail1',0)+blend
 top=[(k,w) for k,w in sorted(ww.items(),key=lambda x:-x[1]) if k in real.vertex_groups and w>1e-6][:4];total=sum(w for _,w in top)
 if not total:raise ValueError('Unweighted vertex')
 for name,w in top:real.vertex_groups[name].add([v.index],w/total,'REPLACE')
 diststats.append(nearest[0][2])
mod=real.modifiers.new('QuadrupedSkin','ARMATURE');mod.object=rig
world=real.matrix_world.copy();real.parent=rig;real.matrix_world=world
for donor in list(bpy.context.scene.objects):
 if donor.type=='MESH' and donor!=real:bpy.data.objects.remove(donor,do_unlink=True)
rig.data.pose_position='POSE';actions={a.name:a for a in bpy.data.actions};rig.animation_data.action=actions['Idle'];rig.animation_data.action_slot=actions['Idle'].slots[0]
bpy.context.scene.frame_set(0);bpy.context.view_layer.update()
bpy.ops.object.select_all(action='DESELECT')
for o in bpy.context.scene.objects:
 if o.type in ['ARMATURE','MESH','EMPTY']:o.select_set(True)
version=sys.argv[sys.argv.index('--')+2] if len(sys.argv)>sys.argv.index('--')+2 else 'v1';out=D/f'bound-{version}.glb';assert not out.exists(),out
bpy.ops.export_scene.gltf(filepath=str(out),export_format='GLB',use_selection=True,export_yup=True,export_image_format='JPEG',export_jpeg_quality=86,export_animations=True,export_animation_mode='ACTIONS',export_anim_single_armature=True,export_force_sampling=True,export_optimize_animation_size=True,export_influence_nb=4,export_all_influences=False,export_leaf_bone=False)
entry.update(id=f'enemy-{kind}-realistic',file=f'../realistic/{kind}/{out.name}',style='realistic',tint=[],licence='CC0-1.0',credit='Forge generated reference and Pixal3D mesh, dedicated CC0; rig and six clips: Quaternius Ultimate Animated Animals, https://quaternius.com/packs/ultimateanimatedanimals.html',speeds={})
(D/f'entry-{version}.json').write_text(json.dumps(entry,indent=2));(D/f'fit-{version}.json').write_text(json.dumps({'rotation_z':ang,'scale':scale,'paws_before':r,'donor_paws':d,'length_scale':along,'side_scale':across,'forward_shift':shift,'surface_distance_p50':float(np.percentile(diststats,50)),'surface_distance_p95':float(np.percentile(diststats,95)),'antler_vertices_rigid':rigid_antlers,'bones':{n:{k:list(v) for k,v in val.items()} for n,val in bonepos.items()}},indent=2));print('BOUND',kind,out.stat().st_size,'fit',along,across,'distance',np.percentile(diststats,[50,95]),flush=True)
