import bpy,json,math,numpy as np
from pathlib import Path
P=Path('/opt/creature-lib-20261017/realistic/horse');bpy.ops.wm.read_factory_settings(use_empty=True);bpy.context.scene.render.fps=30;bpy.ops.import_scene.gltf(filepath=str(P/'hair-v7.glb'))
o=next(o for o in bpy.context.scene.objects if o.type=='MESH' and any(m.type=='ARMATURE' for m in o.modifiers));bs=o.data.materials[0].node_tree.nodes.get('Principled BSDF');im=bs.inputs['Base Color'].links[0].from_node.image;a=np.array(im.pixels[:],dtype=np.float32).reshape(im.size[1],im.size[0],4);W,H=im.size;uv=o.data.uv_layers.active.data;count=0
for poly in o.data.polygons:
 ids=list(poly.loop_indices)
 if len(ids)!=3:continue
 ps=np.array([list(o.matrix_world@o.data.vertices[o.data.loops[i].vertex_index].co) for i in ids]);cen=ps.mean(axis=0)
 if not (abs(cen[0])<.17 and -1.225<cen[1]<-1.00 and 1.67<cen[2]<1.90):continue
 ts=np.array([list(uv[i].uv) for i in ids])*np.array([W,H]);lo=np.maximum(np.floor(ts.min(axis=0)).astype(int)-2,0);hi=np.minimum(np.ceil(ts.max(axis=0)).astype(int)+2,[W-1,H-1]);xs,ys=np.meshgrid(np.arange(lo[0],hi[0]+1),np.arange(lo[1],hi[1]+1));q=np.stack([xs+.5,ys+.5],axis=-1);v0=ts[1]-ts[0];v1=ts[2]-ts[0];den=v0[0]*v1[1]-v1[0]*v0[1]
 if abs(den)<1e-8:continue
 v2=q-ts[0];u=(v2[:,:,0]*v1[1]-v1[0]*v2[:,:,1])/den;v=(v0[0]*v2[:,:,1]-v2[:,:,0]*v0[1])/den;inside=(u>=-.06)&(v>=-.06)&(u+v<=1.06)
 # Dark bay forelock, subtle strand variation rather than a bright painted disc.
 noise=.045+.022*(.5+.5*np.sin(xs*1.8+np.sin(ys*.1)*.4))+.008*np.sin(ys*.27)
 colors=np.stack([noise*1.08,noise*.91,noise*.75],axis=-1);region=a[lo[1]:hi[1]+1,lo[0]:hi[0]+1,:3];region[inside]=colors[inside];count+=int(inside.sum())
im.pixels.foreach_set(a.reshape(-1));im.update();im.pack();rig=next(x for x in bpy.context.scene.objects if x.type=='ARMATURE')
for t in rig.animation_data.nla_tracks:t.mute=True
rig.animation_data.action=bpy.data.actions['Idle'];rig.animation_data.action_slot=rig.animation_data.action.slots[0];bpy.context.scene.frame_set(0);bpy.context.view_layer.update();bpy.ops.object.select_all(action='DESELECT')
for x in bpy.context.scene.objects:
 if x.type in ['ARMATURE','EMPTY'] or x==o:x.select_set(True)
out=P/'forelock-v8.glb';assert not out.exists()
bpy.ops.export_scene.gltf(filepath=str(out),export_format='GLB',use_selection=True,export_yup=True,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,export_image_format='JPEG',export_jpeg_quality=90)
e=json.loads((P/'entry-v7.json').read_text());e['file']='../realistic/horse/forelock-v8.glb';(P/'entry-v8.json').write_text(json.dumps(e,indent=2));(P/'forelock-repair.json').write_text(json.dumps({'pixel_writes':count,'method':'Dark strand paint in UV polygons of the measured forelock, removes generated pale disc. No changes to geometry or clips.'},indent=2));print('FORELOCK',count)
