"""Cycles captures of reimported deliverable. No source geometry used for atom renders."""
import bpy,bmesh,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=48;scene.cycles.use_denoising=True
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';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=.55
bpy.ops.object.light_add(type='AREA',location=(-4,-6,7));key=bpy.context.object;key.data.energy=1500;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.3;sun.data.angle=.10
bpy.ops.mesh.primitive_plane_add(size=100,location=(0,0,-.025));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=(.65,-1,.45),res=(1400,1000),claymode=False):
 lo,hi=bounds(obs);target=(lo+hi)*.5;cam.location=target+Vector(direction).normalized()*max(30,(hi-lo).length*2);cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler()
 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.13
 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
# Rendering and assembly operate only on round-tripped GLB atoms.
from mathutils import Matrix
sys.path.insert(0,str(ROOT))
import geometry as G
from geometry import Mesh,box,prism
from details import beam
NAMES=['Rubble_stone','Rubble_stone_dark','Oak_frame','Oak_frame_grey','Plaster_ochre','Plaster_white','Plaster_pink','Clay_tile','Clay_tile_dark','Dressed_limestone','Wrought_iron','Leaded_glass','Door_paint','Sign_paint','Thatch_reed','Thatch_reed_cut']
G.MATS=[bpy.data.materials.get(n) or bpy.data.materials["Oak_frame"] for n in NAMES];G.TILES=[SHELF[n].get('tile_size_metres',1) for n in NAMES]
C=Matrix.Rotation(math.pi/2,4,'X')
obs=[]
def put(name,x,y,z,rot=None,group=None):
 src=masters[name];copies={}
 for orig in parts(src):
  ob=orig.copy()
  if orig.type=='MESH':ob.data=orig.data
  bpy.context.collection.objects.link(ob);copies[orig]=ob
 for orig,ob in copies.items():
  ob.parent=copies.get(orig.parent);ob.matrix_local=orig.matrix_local.copy();ob.hide_render=False
 root=copies[src];mat=Matrix.Translation((x,y,z))@(rot or Matrix.Identity(4));root.matrix_world=C@mat@C.inverted()@src.matrix_world
 (obs if group is None else group).append(root)
 return root
def make(g,group=None):
 o=g.finish();o.matrix_world=C;(obs if group is None else group).append(o);return o
def block(name,c,s,mat=0,group=None):
 g=Mesh(name);box(g,c,s,mat,0);return make(g,group)
def cut_arch(wall,name,x,y,z,w,h):
 r=w/2;spring=h-r
 poly=[(-r,0),(r,0)]+[(r*math.cos(i*math.pi/20),spring+r*math.sin(i*math.pi/20)) for i in range(21)]
 g=Mesh(name);prism(g,[(xx+x,yy+y) for xx,yy in poly],[z-.7,z+.7],0);o=g.finish();o.matrix_world=C
 mod=wall.modifiers.new('clear_opening','BOOLEAN');mod.operation='DIFFERENCE';mod.solver='EXACT';mod.object=o
 bpy.context.view_layer.objects.active=wall;bpy.ops.object.modifier_apply(modifier=mod.name);bpy.data.objects.remove(o,do_unlink=True)
 bm=bmesh.new();bm.from_mesh(wall.data);bmesh.ops.triangulate(bm,faces=list(bm.faces));bm.to_mesh(wall.data);bm.free()
def cut_box(wall,name,c,s):
 g=Mesh(name);box(g,c,s,0,0);o=g.finish();o.matrix_world=C
 mod=wall.modifiers.new('clear_opening','BOOLEAN');mod.operation='DIFFERENCE';mod.solver='EXACT';mod.object=o
 bpy.context.view_layer.objects.active=wall;bpy.ops.object.modifier_apply(modifier=mod.name);bpy.data.objects.remove(o,do_unlink=True)
 bm=bmesh.new();bm.from_mesh(wall.data);bmesh.ops.triangulate(bm,faces=list(bm.faces));bm.to_mesh(wall.data);bm.free()
(ROOT/'renders').mkdir(exist_ok=True)
# Contact sheet clay overview first, with per-atom scalar normalization only for display.
if '--assembly-only' not in sys.argv and '--export-only' not in sys.argv:
 gallery=[]
 for i,(name,src) in enumerate(masters.items()):
  lo,hi=bounds([src]);sz=max(hi-lo);s=2.1/max(sz,.2)
  ob=put(name,0,0,0,group=gallery)
  ob.scale*=s;bpy.context.view_layer.update();a,b=bounds([ob]);ob.location+=Vector(((i%5)*3-(a.x+b.x)/2,(i//5)*3,-a.z))
 render('clay_sheet.png',gallery,(.18,-1,.78),(1800,1300),True)
 for ob in gallery:visible(ob,False)
 for name,ob in masters.items():
  if '--revision-only' in sys.argv and name not in ['thatch_panel','thatch_ridge','sign_boot']:continue
  visible(ob,True);lo,hi=bounds([ob]);orig=ob.location.copy();ob.location.z-=lo.z
  render('renders/'+name+'.png',[ob],(.55,-1,.36),(720,640));visible(ob,False);ob.location=orig
 for name in ['dormer_gabled','dormer_hipped','dormer_wall','chimney_cluster','oriel','pillory','well_wheel','thatch_panel']:
  if '--revision-only' in sys.argv and name!='thatch_panel':continue
  ob=masters[name];visible(ob,True)
  render('details_'+name+'.png',[ob],(-.60,-1,.30),(1300,1150))
  render('rear_'+name+'.png',[ob],(-.60,1,.35),(1100,950),True)
  visible(ob,False)
# Rich plain box with generated timber upper storey, 55-degree roof, declared holes.
x=-4.2;front=-1.0;eave=5.2;depth=4.4;half=depth/2;ridge=eave+half*math.tan(math.radians(55))
wall=block('GENERATED_rich_stone_front',(x,1.3,front-.15),(6,2.6,.30),0)
cut_arch(wall,'door_clear',x-1.48,0,front,1.1,2.3)
# True oval cellar aperture through generated wall.
g=Mesh('cellar_clear');poly=[(x+.1+.235*math.cos(i*math.tau/24),.72+.19+.19*math.sin(i*math.tau/24)) for i in range(24)];prism(g,poly,[front-.6,front+.4],0);o=g.finish();o.matrix_world=C
mod=wall.modifiers.new('cellar_open','BOOLEAN');mod.operation='DIFFERENCE';mod.object=o;bpy.context.view_layer.objects.active=wall;bpy.ops.object.modifier_apply(modifier=mod.name);bpy.data.objects.remove(o,do_unlink=True)
bm=bmesh.new();bm.from_mesh(wall.data);bmesh.ops.triangulate(bm,faces=list(bm.faces));bm.to_mesh(wall.data);bm.free()
for p in wall.data.polygons:p.material_index=0
block('GENERATED_rich_left',(x-2.85,1.3,front-half),(.3,2.6,depth-.60),0)
block('GENERATED_rich_right',(x+2.85,1.3,front-half),(.3,2.6,depth-.60),0)
block('GENERATED_rich_rear',(x,1.3,front-depth+.15),(6,2.6,.3),0)
upper=block('GENERATED_rich_upper_front',(x,3.9,front-.14),(6,2.6,.28),5)
# Bay behind may be opaque inside, but wall cutout exposes its glazed box.
cut_box(upper,'oriel_open',(x+.8,3.63,front),(1.40,1.75,1.0))
for p in upper.data.polygons:p.material_index=5
block('GENERATED_rich_upper_left',(x-2.86,3.9,front-half),(.28,2.6,depth-.56),4)
block('GENERATED_rich_upper_right',(x+2.86,3.9,front-half),(.28,2.6,depth-.56),4)
block('GENERATED_rich_upper_rear',(x,3.9,front-depth+.14),(6,2.6,.28),5)
g=Mesh('GENERATED_rich_timber')
for y in [2.65,3.35,5.13]:box(g,(x,y,front+.025),(6.12,.13,.13),2,.006)
for off in [-2.92,-1.95,-.95,1.62,2.92]:
 beam(g,(x+off,2.66,front+.024),(x+off,5.12,front+.024),.12,.13,2,.005)
for a,b in [(-2.87,-2.00),(-1.90,-1.00),(1.7,2.84)]:
 beam(g,(x+a,2.73,front+.035),(x+b,3.27,front+.035),.06,.075,2,.003)
 beam(g,(x+b,2.73,front+.035),(x+a,3.27,front+.035),.06,.075,2,.003)
for z in [front-.10,front-1.2,front-2.2,front-3.3,front-4.3]:
 beam(g,(x+3.02,2.6,z),(x+3.02,5.15,z),.12,.13,2,.005)
for y in [2.66,3.35,5.13]:box(g,(x+3.02,y,front-half),(.13,.13,4.4),2,.005)
make(g)
# Closed gables and exactly 55-degree tile stand-in, with shallow horizontal tile-course relief.
g=Mesh('GENERATED_rich_roof')
for xx in [x-3,x+2.86]:
 poly=[(front,eave-.08),(front-half,ridge-.08),(front-depth,eave-.08)]
 vs=[(u,y,z) for u in [xx,xx+.14] for z,y in poly];g.add(vs,[(0,2,1),(3,4,5),(0,1,4,3),(1,2,5,4),(2,0,3,5)],4)
for side in [-1,1]:
 z0=front-half;z1=z0+side*(half+.12);y1=ridge-(half+.12)*math.tan(math.radians(55))
 vs=[(xx,yy+dy,zz) for dy in [0,-.07] for xx,yy,zz in [(x-3.17,ridge,z0),(x+3.17,ridge,z0),(x+3.17,y1,z1),(x-3.17,y1,z1)]]
 g.add(vs,[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)],7)
 for row in range(1,11):
  z=z0+side*row*(half+.12)/11;y=ridge-abs(z-z0)*math.tan(math.radians(55))+.008
  beam(g,(x-3.17,y,z),(x+3.17,y,z),.023,.024,7,0)
make(g)
for dx in [-1.75,1.5]:put('dormer_gabled',x+dx,eave+.65*math.tan(math.radians(55)),front-.65)
put('chimney_cluster',x+.30,ridge-.55,front-half)
put('door_arched_stone',x-1.48,0,front)
put('window_cellar',x+.1,.72,front)
put('oriel',x+.8,2.90,front)
put('sign_boot',x+2.1,2.5,front)
# Cottage: 3 by 2 EXACTLY on each side, aligned 1m grids in local slope frame.
cx=4.3;cz=-2.0;pitch=math.radians(55);cRidge=3.62;cEave=cRidge-2*math.sin(pitch);run=2*math.cos(pitch)
frontC=cz+run
wall=block('GENERATED_cottage_front',(cx,cEave/2,frontC-.10),(3,cEave,.2),4)
cut_box(wall,'shutter_clear',(cx,.62+.5,frontC),(.7,1.0,.8))
block('GENERATED_cottage_rear',(cx,cEave/2,cz-run+.1),(3,cEave,.2),4)
for xx in [cx-1.40,cx+1.40]:
 block('GENERATED_cottage_side',(xx,cEave/2,cz),(.20,cEave,2*run-.4),4)
 g=Mesh('GENERATED_cottage_gable');poly=[(cz-run,cEave),(cz,cRidge),(cz+run,cEave)];vs=[(x,y,z) for x in [xx-.10,xx+.10] for z,y in poly];g.add(vs,[(0,2,1),(3,4,5),(0,1,4,3),(1,2,5,4),(2,0,3,5)],4);make(g)
g=Mesh('GENERATED_cottage_frame')
for off in [-1.44,1.44]:beam(g,(cx+off,0,frontC+.01),(cx+off,cEave,frontC+.01),.11,.11,2,.005)
for y in [.08,cEave-.04]:box(g,(cx,y,frontC+.01),(3,.10,.12),2,.005)
make(g)
for side in [-1,1]:
 rot=Matrix.Rotation(pitch if side==1 else -pitch,4,'X')
 if side==-1:rot=rot@Matrix.Rotation(math.pi,4,'Y')
 for col in range(3):
  for row in range(2):
   disp=rot@Vector((0,0,float(row),1));put('thatch_panel',cx+col-1,cRidge+disp.y,cz+disp.z,rot)
for col in range(3):put('thatch_ridge',cx+col-1,cRidge,cz)
put('louvre_ridge',cx,cRidge+.25,cz)
put('window_shuttered',cx,.62,frontC)
# Square furniture, separated so every silhouette and bearing is visible.
put('pillory',.7,0,2.6)
put('well_wheel',8.0,0,3.2)
# Export assembly before adding any studio node. Exact placed glTF atoms retained.
bpy.ops.object.select_all(action='DESELECT')
for ob in obs:
 for p in parts(ob):p.select_set(True)
# Keep named material factors, temporarily disconnecting external texture inputs.
saved_links=[]
for material in bpy.data.materials:
 if not material.use_nodes:continue
 nt=material.node_tree
 for link in list(nt.links):
  if link.to_node.type=='BSDF_PRINCIPLED':
   saved_links.append((nt,link.from_socket,link.to_socket));nt.links.remove(link)
bpy.ops.export_scene.gltf(filepath=str(ROOT/'test_house.glb'),export_format='GLB',use_selection=True,export_apply=True,export_yup=True,export_materials='EXPORT',export_texcoords=True,export_normals=True,export_tangents=True,export_cameras=False,export_lights=False)
for nt,source,target in saved_links:nt.links.new(source,target)
if '--export-only' in sys.argv:sys.exit(0)
render('test_house.png',obs,(.47,-1,.43),(2200,1450))
render('assembly_clay.png',obs,(.47,-1,.43),(1800,1200),True)
render('test_house_rear.png',obs,(-.60,1,.45),(1800,1200),True)
# Dormer integration and thatch seam views use the actual assembled objects.
rich=[o for o in obs if o.name.startswith('dormer_gabled') or o.name.startswith('GENERATED_rich_roof')]
for o in obs:visible(o,o in rich)
render('details_dormer_contact.png',rich,(.05,-1,.25),(1500,1000))
cottage=[o for o in obs if o.name.startswith(('thatch_','GENERATED_cottage','window_shuttered','louvre_ridge'))]
for o in obs:visible(o,o in cottage)
render('details_thatch_grid.png',cottage,(.7,-1,.4),(1500,1100))
print('RENDERS_COMPLETE',flush=True)
