"""Reimport the delivered GLB and bind external maps; render reproducible QA views."""
import bpy,math,json,sys
from pathlib import Path
from mathutils import Vector
ROOT=Path(__file__).resolve().parent
SHELF=json.loads((ROOT/'materials.json').read_text())['materials']
scene=bpy.context.scene;bpy.ops.object.select_all(action='SELECT');bpy.ops.object.delete(use_global=False)
for m in list(bpy.data.materials):bpy.data.materials.remove(m)
bpy.ops.import_scene.gltf(filepath=str(ROOT/'kit.glb'))
masters={o.name:o for o in scene.objects if o.parent is None and o.type in ('MESH','EMPTY')}
for m in bpy.data.materials:
 if m.name not in SHELF:continue
 nt=m.node_tree;bs=next(n for n in nt.nodes if n.type=='BSDF_PRINCIPLED')
 for kind in ['BaseColor','Normal','ORM']:
  tex=nt.nodes.new('ShaderNodeTexImage');tex.image=bpy.data.images.load(str(ROOT/SHELF[m.name]['maps'][kind]),check_existing=True);tex.image.colorspace_settings.name='sRGB' if kind=='BaseColor' else 'Non-Color';tex.extension='REPEAT'
  if kind=='BaseColor':nt.links.new(tex.outputs['Color'],bs.inputs['Base Color'])
  elif kind=='Normal':
   n=nt.nodes.new('ShaderNodeNormalMap');nt.links.new(tex.outputs['Color'],n.inputs['Color']);nt.links.new(n.outputs[0],bs.inputs['Normal'])
  else:
   sep=nt.nodes.new('ShaderNodeSeparateColor');nt.links.new(tex.outputs['Color'],sep.inputs[0]);nt.links.new(sep.outputs['Green'],bs.inputs['Roughness']);nt.links.new(sep.outputs['Blue'],bs.inputs['Metallic'])
scene.render.engine='CYCLES';scene.cycles.samples=32;scene.cycles.use_denoising=True
try:
 prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='OPTIX';prefs.get_devices()
 for d in prefs.devices:d.use=d.type!='CPU'
 scene.cycles.device='GPU'
except:scene.cycles.device='CPU'
scene.render.image_settings.file_format='PNG';scene.view_settings.view_transform='AgX'
scene.world.use_nodes=True;bg=scene.world.node_tree.nodes.get('Background');bg.inputs[0].default_value=(.70,.77,.86,1);bg.inputs[1].default_value=.65
bpy.ops.object.light_add(type='AREA',location=(-4,-6,7));key=bpy.context.object;key.data.energy=1600;key.data.shape='DISK';key.data.size=4;key.rotation_euler=(-key.location).to_track_quat('-Z','Y').to_euler()
bpy.ops.object.light_add(type='AREA',location=(4,2,4));rim=bpy.context.object;rim.data.energy=800;rim.data.size=3;rim.rotation_euler=(-rim.location).to_track_quat('-Z','Y').to_euler()
bpy.ops.object.light_add(type='SUN');sun=bpy.context.object;sun.rotation_euler=(.45,-.4,-.5);sun.data.energy=1.2;sun.data.angle=.10
bpy.ops.mesh.primitive_plane_add(size=100,location=(0,0,-.02));floor=bpy.context.object;floor.name='STUDIO_floor'
mat=bpy.data.materials.new('STUDIO_floor');mat.use_nodes=True;mat.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value=(.42,.43,.43,1);mat.node_tree.nodes.get('Principled BSDF').inputs['Roughness'].default_value=.85;floor.data.materials.append(mat)
bpy.ops.object.camera_add();cam=bpy.context.object;scene.camera=cam;cam.data.type='ORTHO'
clay=bpy.data.materials.new('STUDIO_clay');clay.use_nodes=True;clay.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value=(.42,.42,.42,1)
def parts(ob):return [ob]+list(ob.children_recursive)
def visible(ob,v):
 for o in parts(ob):o.hide_render=not v
for ob in masters.values():visible(ob,False)
def bounds(obs):
 bpy.context.view_layer.update();pts=[o.matrix_world@Vector(c) for ob in obs for o in parts(ob) if o.type=='MESH' for c in o.bound_box]
 return Vector([min(v[k] for v in pts) for k in range(3)]),Vector([max(v[k] for v in pts) for k in range(3)])
def render(name,obs,direction=(.28,-1,.40),res=(1400,1000),claymode=False):
 lo,hi=bounds(obs);target=(lo+hi)*.5;cam.location=target+Vector(direction).normalized()*30;cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler()
 # Fit projected bbox corners, maintaining actual camera-plane aspect.
 bpy.context.view_layer.update();inv=cam.matrix_world.inverted();pp=[inv@(o.matrix_world@Vector(c)) for ob in obs for o in parts(ob) if o.type=='MESH' for c in o.bound_box]
 wx=max(p.x for p in pp)-min(p.x for p in pp);hy=max(p.y for p in pp)-min(p.y for p in pp)
 cam.data.ortho_scale=max(wx,hy*res[0]/res[1])*1.18
 scene.render.resolution_x=res[0];scene.render.resolution_y=res[1];scene.render.resolution_percentage=100
 scene.view_layers[0].material_override=clay if claymode else None;scene.render.filepath=str(ROOT/name);bpy.ops.render.render(write_still=True);scene.view_layers[0].material_override=None
(ROOT/'renders').mkdir(exist_ok=True)
if '--details-only' not in sys.argv:
 for name,ob in masters.items():
  visible(ob,True);ob.location=(0,0,0);lo,hi=bounds([ob]);ob.location.z=-lo.z
  render('renders/'+name+'.png',[ob],(.85,-1,.50) if name=='tavern_sign' else (.28,-1,.42),(560,480))
  visible(ob,False);ob.location=(0,0,0)

def sheet(names,file,dir=(.22,-1,.32),cl=False):
 for ob in masters.values():visible(ob,False);ob.location=(0,0,0)
 x=0;obs=[]
 for name in names:
  ob=masters[name];visible(ob,True);lo,hi=bounds([ob]);ob.location=(x-lo.x,0,-lo.z);x+=hi.x-lo.x+.35;obs.append(ob)
 for ob in obs:ob.location.x-=x/2
 render(file,obs,dir,(1920,1100),cl)
sheet(['window_leaded','window_leaded_stone','window_small','door_plank','door_arched_double','window_shop'],'details_joinery.png')
sheet(['brace_storey','brace_straight','bressummer_1m','joist_end','jetty_bracket'],'details_timber.png')
sheet(['dormer','chimney_stone','tile_strip_a','ridge_tile_round'],'details_roof.png')
sheet(['tavern_sign','lantern_iron','barrel','bench_wood'],'details_props.png',(.65,-1,.38))
sheet(['door_arched_double','dormer','jetty_bracket','window_leaded'],'clay_check.png',cl=True)
sheet(['window_leaded_stone','door_plank','dormer','tavern_sign'],'reverse_check.png',(-.40,1,.40))
print('RENDERS_COMPLETE',flush=True)
