"""Quote-based kitchen, metres. Run with Blender 4.5: blender -b -t 8 -P build_kitchen.py
Optional environment: KITCHEN_SAMPLES=96 KITCHEN_RESOLUTION=1536 KITCHEN_RENDER=1.
See specification.json for all assumptions and the dimensional reconciliation.
"""
import bpy, math, os, json
from mathutils import Vector
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for d in list(bpy.data.materials): bpy.data.materials.remove(d)
scene = bpy.context.scene
scene.unit_settings.system = 'METRIC'
scene.unit_settings.length_unit = 'MILLIMETERS'
collections = {}
for name in ['01_Quoted_stainless_unit','02_Illustrative_appliances_and_tap','03_Interior_staging','04_Lights_and_cameras']:
    c = bpy.data.collections.new(name); scene.collection.children.link(c); collections[name[:2]] = c
current = '01'
def register(obj, name, material=None):
    obj.name = name
    for col in list(obj.users_collection): col.objects.unlink(obj)
    collections[current].objects.link(obj)
    if material: obj.data.materials.append(material)
    return obj

def mat(name, color, metallic=0, rough=.4):
    m = bpy.data.materials.new(name); m.use_nodes=True
    p=m.node_tree.nodes.get('Principled BSDF')
    p.inputs['Base Color'].default_value=(*color,1)
    p.inputs['Metallic'].default_value=metallic
    p.inputs['Roughness'].default_value=rough
    return m
steel = mat('AISI 304 | K320 satin directional brushed fronts',(.56,.59,.61),1,.32)
nt=steel.node_tree; p=nt.nodes.get('Principled BSDF')
tex=nt.nodes.new('ShaderNodeTexNoise'); tex.inputs['Scale'].default_value=1; tex.inputs['Detail'].default_value=2
coord=nt.nodes.new('ShaderNodeTexCoord'); mapping=nt.nodes.new('ShaderNodeVectorMath'); mapping.operation='MULTIPLY'; mapping.inputs[1].default_value=(4500,15,15)
nt.links.new(coord.outputs['Generated'],mapping.inputs[0]); nt.links.new(mapping.outputs[0],tex.inputs['Vector'])
bump=nt.nodes.new('ShaderNodeBump'); bump.inputs['Strength'].default_value=.11; bump.inputs['Distance'].default_value=.00008
nt.links.new(tex.outputs['Fac'],bump.inputs['Height']); nt.links.new(bump.outputs[0],p.inputs['Normal'])
p.inputs['Anisotropic'].default_value=.35
cloud=mat('Worktop | non-directional softly cloud-brushed stainless',(.60,.63,.64),1,.36)
nt=cloud.node_tree;p=nt.nodes.get('Principled BSDF')
coord=nt.nodes.new('ShaderNodeTexCoord'); noise=nt.nodes.new('ShaderNodeTexNoise'); noise.inputs['Scale'].default_value=210; noise.inputs['Detail'].default_value=3; noise.inputs['Roughness'].default_value=.75
nt.links.new(coord.outputs['Generated'],noise.inputs['Vector'])
bump=nt.nodes.new('ShaderNodeBump'); bump.inputs['Strength'].default_value=.10; bump.inputs['Distance'].default_value=.00014
nt.links.new(noise.outputs['Fac'],bump.inputs['Height']);nt.links.new(bump.outputs[0],p.inputs['Normal'])
ramp=nt.nodes.new('ShaderNodeMapRange'); ramp.inputs['From Min'].default_value=0; ramp.inputs['From Max'].default_value=1; ramp.inputs['To Min'].default_value=.28; ramp.inputs['To Max'].default_value=.42
nt.links.new(noise.outputs['Fac'],ramp.inputs['Value']);nt.links.new(ramp.outputs['Result'],p.inputs['Roughness'])
sinksteel=mat('Welded bowl satin stainless',(.59,.62,.64),1,.24)
black=mat('Soft black shadow gaps',(.016,.02,.022),.15,.35)
glass=mat('Black ceramic induction glass',(.009,.014,.018),.35,.16)
ovenglass=mat('Oven smoked glass',(.014,.022,.028),.25,.20)
ringmat=mat('Fine subtle induction zone markings',(.10,.115,.12),.15,.28)
plaster=mat('White painted walls | user specified',(.79,.78,.75),0,.85)
nt=plaster.node_tree; n=nt.nodes.new('ShaderNodeTexNoise');n.inputs['Scale'].default_value=5;n.inputs['Detail'].default_value=3
b=nt.nodes.new('ShaderNodeBump');b.inputs['Strength'].default_value=.13;b.inputs['Distance'].default_value=.008
nt.links.new(n.outputs['Fac'],b.inputs['Height']);nt.links.new(b.outputs[0],nt.nodes.get('Principled BSDF').inputs['Normal'])
floor=mat('Natural oak parquet | user specified, pattern assumed',(.40,.26,.13),0,.50)
nt=floor.node_tree;n=nt.nodes.new('ShaderNodeTexNoise');n.inputs['Scale'].default_value=1;n.inputs['Detail'].default_value=3
coord=nt.nodes.new('ShaderNodeTexCoord');mapping=nt.nodes.new('ShaderNodeVectorMath');mapping.operation='MULTIPLY';mapping.inputs[1].default_value=(5,160,8)
nt.links.new(coord.outputs['Generated'],mapping.inputs[0]);nt.links.new(mapping.outputs[0],n.inputs['Vector'])
ramp=nt.nodes.new('ShaderNodeValToRGB');ramp.color_ramp.elements[0].position=.2;ramp.color_ramp.elements[0].color=(.26,.145,.064,1);ramp.color_ramp.elements[1].position=.8;ramp.color_ramp.elements[1].color=(.56,.375,.185,1)
nt.links.new(n.outputs['Fac'],ramp.inputs[0]);nt.links.new(ramp.outputs[0],nt.nodes.get('Principled BSDF').inputs['Base Color'])
b=nt.nodes.new('ShaderNodeBump');b.inputs['Strength'].default_value=.15;b.inputs['Distance'].default_value=.002
nt.links.new(n.outputs['Fac'],b.inputs['Height']);nt.links.new(b.outputs[0],nt.nodes.get('Principled BSDF').inputs['Normal'])
grout=mat('Fine parquet joints',(.13,.08,.04),0,.8)
oak=mat('Light oak window lining',(.40,.26,.13),0,.52)
white=mat('Soft white ceiling',(.80,.78,.72),0,.8)

def box(name,loc,dim,material,bevel=.002):
    bpy.ops.mesh.primitive_cube_add(size=1,location=loc);o=register(bpy.context.object,name,material);o.dimensions=dim
    bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
    if bevel:
        m=o.modifiers.new('Manufactured softened edges','BEVEL');m.width=bevel;m.segments=3
        m=o.modifiers.new('Weighted surface normals','WEIGHTED_NORMAL')
    return o

def cyl(name,loc,radius,depth,material,rotation=None):
    bpy.ops.mesh.primitive_cylinder_add(vertices=48,radius=radius,depth=depth,location=loc)
    o=register(bpy.context.object,name,material)
    if rotation:o.rotation_euler=rotation
    m=o.modifiers.new('Edge radius','BEVEL');m.width=.0015;m.segments=3
    o.modifiers.new('Normals','WEIGHTED_NORMAL')
    for f in o.data.polygons:f.use_smooth=True
    return o

def tube(name,pts,radius,material):
    c=bpy.data.curves.new(name,'CURVE');c.dimensions='3D';c.resolution_u=24;c.bevel_depth=radius;c.bevel_resolution=5
    s=c.splines.new('POLY');s.points.add(len(pts)-1)
    for p,v in zip(s.points,pts):p.co=(*v,1)
    o=bpy.data.objects.new(name,c);collections[current].objects.link(o);o.data.materials.append(material);return o

# Envelope: worktop 2850 x 900 x 40 mm. Cabinet height 760 mm above assumed 100 mm plinth.
# Front y=-0.45; rear y=+0.45. Cabinet carcass occupies y=-0.43 .. +0.27.
box('Full-width removable recessed plinth | assumed H100',(0,-.01,.05),(2.806,.67,.10),steel)
box('Shadow under cabinet fronts',(0,-.355,.104),(2.818,.022,.020),black,.001)
widths=[.5175,.605,.5175,.605,.605]
x=-1.425;centers=[]
for i,w in enumerate(widths):
    cx=x+w/2;centers.append(cx)
    box(f'Bay {i+1} carcass back',(cx,.262,.48),(w-.005,.016,.76),steel)
    box(f'Bay {i+1} base',(cx,-.08,.108),(w-.005,.70,.016),steel)
    for label,px in [('left',x+.009),('right',x+w-.009)]:
        box(f'Bay {i+1} {label} wall',(px,-.08,.48),(.018,.7,.76),steel)
    box(f'Bay {i+1} upper finger recess',(cx,-.414,.847),(w-.009,.03,.024),black,.001)
    if i!=1:
        box(f'Bay {i+1} single stainless front',(cx,-.431,.472),(w-.006,.018,.722),steel)
        # Concealed folded edge, not an invented surface handle.
        box(f'Bay {i+1} concealed upper return',(cx,-.420,.827),(w-.009,.035,.010),steel,.001)
    else:
        box('Drawer below oven',(cx,-.431,.176),(w-.006,.018,.13),steel)
        box('Oven lower drawer finger recess',(cx,-.414,.248),(w-.009,.025,.012),black,.001)
    if i in (0,2):
        # Internal tray remains concealed behind the single external front.
        box(f'Bay {i+1} concealed internal drawer base',(cx,-.074,.686),(w-.070,.57,.014),steel)
        box(f'Bay {i+1} concealed internal drawer back',(cx,.208,.716),(w-.070,.012,.065),steel)
    x+=w
# Sink waste drawer internal bins.
for px in [centers[4]-.125,centers[4]+.125]:
    box('Concealed waste bin',(px,-.08,.325),(.22,.40,.38),black,.02)
# Worktop with genuine sink and hob cut-outs.
top=box('Single 40 mm worktop with welded sink opening',(0,0,.88),(2.85,.90,.04),cloud,.0012)
# Apply edge treatment before cutting, then bevel cut edges.
for m in list(top.modifiers):
    bpy.context.view_layer.objects.active=top;bpy.ops.object.modifier_apply(modifier=m.name)
sx=centers[4];sy=-.04
for name,loc,dim,r in [('Sink opening',(sx,sy,.88),(.50,.40,.20),.020),('Induction opening',(centers[1],-.045,.88),(.566,.496,.20),.006)]:
    cutter=box(name+' cutter',loc,dim,None,r)
    bpy.context.view_layer.objects.active=cutter
    for m in list(cutter.modifiers):bpy.ops.object.modifier_apply(modifier=m.name)
    bpy.context.view_layer.objects.active=top
    m=top.modifiers.new(name,'BOOLEAN');m.operation='DIFFERENCE';m.object=cutter
    bpy.ops.object.modifier_apply(modifier=m.name);bpy.data.objects.remove(cutter,do_unlink=True)
m=top.modifiers.new('Soft welded cut edges','BEVEL');m.width=.0012;m.segments=3
m=top.modifiers.new('Worktop normals','WEIGHTED_NORMAL')
for name,loc,dim in [('Rear',(0,.4485,.92),(2.85,.003,.04)),('Left',(-1.4235,0,.92),(.003,.90,.04)),('Right',(1.4235,0,.92),(.003,.90,.04))]:
    box(name+' 40 mm stainless upstand',loc,dim,cloud,.0008)
# Rounded bowl drawn from upper opening to rounded bottom, preserving 250 mm depth.
def rr(w,d,r,z):
    pts=[]
    for cx,cy,start in [(w/2-r,d/2-r,0),(-w/2+r,d/2-r,90),(-w/2+r,-d/2+r,180),(w/2-r,-d/2+r,270)]:
        for j in range(13):
            a=math.radians(start+j*90/12);pts.append((sx+cx+r*math.cos(a),sy+cy+r*math.sin(a),z))
    return pts
rings=[rr(.500,.400,.020,.8995),rr(.498,.398,.020,.891),rr(.472,.372,.030,.676),rr(.448,.348,.036,.653),rr(.426,.326,.040,.650)]
verts=sum(rings,[]);N=len(rings[0]);faces=[]
for k in range(len(rings)-1):
    for j in range(N):faces.append((k*N+j,k*N+(j+1)%N,(k+1)*N+(j+1)%N,(k+1)*N+j))
faces.append(tuple(reversed(range((len(rings)-1)*N,len(rings)*N))))
mesh=bpy.data.meshes.new('Welded sink bowl geometry');mesh.from_pydata(verts,[],faces);mesh.update()
o=bpy.data.objects.new('Seamlessly welded sink | 500 x 400 x 250 mm',mesh);collections[current].objects.link(o);o.data.materials.append(sinksteel)
for f in mesh.polygons:f.use_smooth=True
m=o.modifiers.new('Bowl steel thickness','SOLIDIFY');m.thickness=.0015
cyl('Sink drain flange',(sx,sy,.652),.042,.003,sinksteel)
cyl('Sink drain strainer',(sx,sy,.654),.031,.003,black)
for a in range(0,360,30):
    rad=math.radians(a);cyl('Drain perforation',(sx+.018*math.cos(rad),sy+.018*math.sin(rad),.656),.002,.001,sinksteel)
# Illustrative appliances and tap.
current='02'
cx=centers[1]
box('Illustrative 600 mm built-in oven',(cx,-.431,.553),(.589,.033,.594),black,.004)
box('Oven stainless perimeter',(cx,-.451,.550),(.588,.007,.582),steel,.003)
box('Oven upper control glass',(cx,-.457,.796),(.568,.006,.078),glass,.002)
box('Oven front smoked glass',(cx,-.457,.511),(.554,.008,.465),ovenglass,.005)
box('Oven lower vent shadow',(cx,-.464,.295),(.51,.003,.010),black,.001)
for px in [cx-.226,cx+.226]:
    cyl('Oven control knob',(px,-.473,.793),.018,.020,steel,(math.pi/2,0,0))
for px in [cx-.233,cx+.233]:box('Oven handle standoff',(px,-.482,.724),(.018,.038,.016),steel,.003)
box('Oven brushed handle',(cx,-.503,.724),(.49,.022,.022),steel,.006)
box('Oven dark control display',(cx,-.462,.795),(.115,.003,.034),black,.001)
# Deliberately no branding or invented display text.
box('Illustrative four-zone induction hob',(cx,-.045,.904),(.59,.52,.008),glass,.005)
for dx,dy,rad in [(-.142,-.127,.088),(.142,-.127,.077),(-.142,.127,.077),(.142,.127,.088)]:
    pts=[(cx+dx+rad*math.cos(t*2*math.pi/128),-.045+dy+rad*math.sin(t*2*math.pi/128),.9084) for t in range(129)]
    tube('Fine induction zone outline',pts,.00065,ringmat)
for i in range(4):
    px=cx-.057+i*.038
    tube('Subtle induction controls',[(px-.004,-.272,.9085),(px+.004,-.272,.9085)],.0006,ringmat)
# Swan-neck stainless mixer centered behind sink; position is assumed.
ty=.280
cyl('Mixer foot',(sx,ty,.908),.025,.016,sinksteel)
cyl('Mixer body',(sx,ty,.968),.019,.12,sinksteel)
pts=[(sx,ty,1.0),(sx,ty,1.145)]
for j in range(33):
    a=math.pi-j*math.pi/32
    pts.append((sx,ty-.085+.085*math.cos(a+math.pi),1.145+.085*math.sin(a)))
# Explicit smooth arch from rear stem to front spout.
pts=[(sx,ty,.982),(sx,ty,1.145)]
for j in range(33):
    a=math.pi-j*math.pi/32
    pts.append((sx,ty-.085-.085*math.cos(a),1.145+.085*math.sin(a)))
pts.append((sx,ty-.17,1.117))
tube('Swan neck stainless tap | illustrative',pts,.012,sinksteel)
cyl('Mixer side pivot',(sx+.027,ty,.965),.012,.032,sinksteel,(0,math.pi/2,0))
tube('Mixer lever',[(sx+.043,ty,.965),(sx+.043,ty,1.027)],.005,sinksteel)
# Loft geometry from ELEK plan 2026 03 20b.pdf, pages 1-2.
# Kitchen along west eaves in the 2900 mm bay; x points toward bedroom.
# White walls and parquet explicitly specified by user. Furniture is illustrative.
current='03'
XMIN=-4.71; XMAX=5.04; YBACK=.45; YFAR=-7.02
EAVE=2.17; RIDGE=5.10; RY=(YBACK+YFAR)/2
box('Oak parquet floor foundation',(0,-3.285,-.05),(9.75,7.47,.10),grout,0)
for iy in range(-39,3):
    for ix in range(-5,5):
        xx=ix*1.2+(iy%3)*.4
        if xx+.6<XMIN or xx-.6>XMAX:continue
        left=max(xx-.6,XMIN);right=min(xx+.6,XMAX)
        yy=iy*.18
        lo=max(yy-.09,YFAR);hi=min(yy+.09,YBACK)
        if hi>lo:box(f'Oak parquet plank {ix} {iy}',((left+right)/2,(lo+hi)/2,-.007),(right-left-.0015,hi-lo-.0015,.014),floor,.0005)
box('West eaves wall behind kitchen',((XMIN+XMAX)/2,YBACK+.10,EAVE/2),(XMAX-XMIN,.2,EAVE),plaster,.001)
box('East eaves wall',((XMIN+XMAX)/2,YFAR-.10,EAVE/2),(XMAX-XMIN,.2,EAVE),plaster,.001)
def surface(name,verts,faces,material,thick=.12):
    m=bpy.data.meshes.new(name);m.from_pydata(verts,[],faces);m.update();o=bpy.data.objects.new(name,m);collections[current].objects.link(o);o.data.materials.append(material)
    if thick:
        mod=o.modifiers.new('Thickness','SOLIDIFY');mod.thickness=thick
    return o
surface('White pitched ceiling - kitchen slope',[(XMIN,YBACK,EAVE),(XMAX,YBACK,EAVE),(XMAX,RY,RIDGE),(XMIN,RY,RIDGE)],[(0,1,2,3)],white)
surface('White pitched ceiling - opposite slope',[(XMIN,RY,RIDGE),(XMAX,RY,RIDGE),(XMAX,YFAR,EAVE),(XMIN,YFAR,EAVE)],[(0,1,2,3)],white)
# The plan shows shallow structural returns at each end of the kitchen bay.
for label,x,w in [('Stair side',-1.63,.36),('Bedroom side',1.60,.30)]:
    ymax=YBACK;ymin=YBACK-1.02
    zfront=EAVE+(YBACK-ymin)/(YBACK-RY)*(RIDGE-EAVE)
    vs=[(x-w/2,ymax,0),(x+w/2,ymax,0),(x+w/2,ymin,0),(x-w/2,ymin,0),(x-w/2,ymax,EAVE),(x+w/2,ymax,EAVE),(x+w/2,ymin,zfront),(x-w/2,ymin,zfront)]
    surface(label+' structural return',vs,[(0,3,2,1),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7),(4,5,6,7)],plaster,0)
# Gable walls and white timber window frames; heights taken from section A-A.
def gable(name,xx,windows,upper_window=False):
    # Rectangular eaves-level wall segmented around lower openings.
    sill=.57;head=2.07;start=YFAR
    for cy,ww in sorted(windows):
        lo=cy-ww/2;hi=cy+ww/2
        if lo>start:box(name+' solid pier',(xx,(start+lo)/2,EAVE/2),(.20,lo-start,EAVE),plaster,0)
        box(name+' below sill',(xx,cy,sill/2),(.20,ww,sill),plaster,0)
        box(name+' over window',(xx,cy,(head+EAVE)/2),(.20,ww,EAVE-head),plaster,0)
        for yy in [lo+.028,hi-.028]:box(name+' white window jamb',(xx,yy,(sill+head)/2),(.13,.055,head-sill),white,.003)
        for zz in [sill+.027,head-.027]:box(name+' white window rail',(xx,cy,zz),(.13,ww,.055),white,.003)
        box(name+' window mullion',(xx,cy,(sill+head)/2),(.13,.040,head-sill),white,.002)
        start=hi
    if start<YBACK:box(name+' end pier',(xx,(start+YBACK)/2,EAVE/2),(.20,YBACK-start,EAVE),plaster,0)
    if not upper_window:
        surface(name+' triangular gable',[(xx,YFAR,EAVE),(xx,YBACK,EAVE),(xx,RY,RIDGE)],[(0,1,2)],plaster,.20)
    else:
        # Upper square window shown in section; approximate 1300 mm opening.
        lo=RY-.65;hi=RY+.65;zlo=2.38;zhi=3.68
        surface(name+' upper wall below opening',[(xx,YFAR,EAVE),(xx,YBACK,EAVE),(xx,hi,zlo),(xx,lo,zlo)],[(0,1,2,3)],plaster,.2)
        surface(name+' upper left triangle',[(xx,YFAR,EAVE),(xx,lo,zlo),(xx,lo,zhi),(xx,RY,RIDGE)],[(0,1,2,3)],plaster,.2)
        surface(name+' upper right triangle',[(xx,YBACK,EAVE),(xx,hi,zlo),(xx,hi,zhi),(xx,RY,RIDGE)],[(0,1,2,3)],plaster,.2)
        surface(name+' upper head triangle',[(xx,lo,zhi),(xx,hi,zhi),(xx,RY,RIDGE)],[(0,1,2)],plaster,.2)
        for yy in [lo,hi]:box(name+' high window jamb',(xx,yy,(zlo+zhi)/2),(.13,.055,zhi-zlo),white,.002)
        for zz in [zlo,zhi]:box(name+' high window head',(xx,RY,zz),(.13,hi-lo,.055),white,.002)
gable('South gable',XMIN-.1,[(-1.31,1.40),(-3.34,1.40),(-5.35,1.40)],True)
gable('North gable',XMAX+.1,[(-1.86,1.48),(-4.84,1.48)],False)
# A restrained 6-seat oak dining table in the location drawn on the plan.
tablewood=mat('Dining table natural oak',(.38,.245,.125),0,.43)
seatmat=mat('Dining chair pale woven seat',(.53,.42,.28),0,.8)
tx=-.05;ty=-2.2
box('Dining table top | plan location, illustrative design',(tx,ty,.745),(2.40,.85,.04),tablewood,.023)
for xx in [tx-.96,tx+.96]:
    for yy in [ty-.29,ty+.29]:box('Dining table oak leg',(xx,yy,.355),(.065,.065,.71),tablewood,.008)
for xx in [tx-.79,tx,tx+.79]:
    for side in [-1,1]:
        yy=ty+side*.68
        box('Woven dining chair seat',(xx,yy,.45),(.43,.42,.032),seatmat,.033)
        for dx in [-.165,.165]:
            for dy in [-.155,.155]:
                box('Dining chair leg',(xx+dx,yy+dy,.226),(.03,.03,.452),tablewood,.007)
        for dx in [-.175,.175]:
            box('Dining chair back stile',(xx+dx,yy+side*.18,.65),(.027,.03,.44),tablewood,.006)
        box('Dining chair curved back',(xx,yy+side*.18,.80),(.40,.038,.085),tablewood,.025)
# Neighboring functions visible at the periphery, kept as simple plan-based massing.
box('Bathroom enclosure beyond bedroom-side return',(3.40,-.52,1.09),(3.28,.10,2.18),plaster,.003)
# Opposite-side low storage in the kitchen bay, shown as kasten in the plan.
box('East wall low white storage',(-.02,-6.72,.47),(2.60,.58,.94),white,.005)
# Lighting and four deliberate camera viewpoints.
current='04'
def area(name,loc,power,color,size,target,shape='DISK',size_y=None):
    data=bpy.data.lights.new(name,'AREA');data.energy=power;data.color=color;data.shape=shape;data.size=size
    if size_y is not None:data.size_y=size_y
    o=bpy.data.objects.new(name,data);collections[current].objects.link(o);o.location=loc;o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler();return o
area('South gable soft daylight',(-5.0,-3.4,2.1),1500,(1.0,.94,.85),4.8,(0,-.2,.8),'RECTANGLE',2.5)
area('North gable soft daylight',(5.5,-3.4,2.1),1800,(.88,.94,1.0),4.2,(0,0,.7),'RECTANGLE',2.1)
area('Large diffused loft fill',(0,-4.8,3.8),420,(1,.97,.92),4,(0,0,.8),'RECTANGLE',2.0)
world=bpy.data.worlds.new('Neutral daylight') if not bpy.data.worlds else bpy.data.worlds[0]
scene.world=world;world.use_nodes=True;world.node_tree.nodes['Background'].inputs[0].default_value=(.68,.76,.90,1);world.node_tree.nodes['Background'].inputs[1].default_value=.4
shots=[
 ('01_loft_from_stairs',(-2.0,-3.85,1.80),(0,-.08,1.20),32),
 ('02_kitchen_front',(0,-1.18,1.14),(0,0,.84),32),
 ('03_sink_end',(1.38,-2.80,1.85),(.0,0,.92),29),
 ('04_worktop_overview',(.45,-2.20,2.75),(0,-.015,.72),29),
]
cameras=[]
for name,loc,target,lens in shots:
    data=bpy.data.cameras.new(name);o=bpy.data.objects.new(name,data);collections[current].objects.link(o);o.location=loc;o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler();data.lens=lens;data.clip_end=100
    if name=='02_kitchen_front':data.type='ORTHO';data.ortho_scale=3.30
    cameras.append(o)
scene.camera=cameras[0]
scene.render.engine='CYCLES';scene.cycles.samples=int(os.getenv('KITCHEN_SAMPLES','64'));scene.cycles.use_denoising=True
scene.cycles.max_bounces=8;scene.cycles.glossy_bounces=4
# CPU is a portable fallback. Enable Metal if present.
try:
    prefs=bpy.context.preferences.addons['cycles'].preferences;prefs.compute_device_type='METAL';prefs.get_devices()
    if any(d.type=='METAL' for d in prefs.devices):
        for d in prefs.devices:d.use=d.type=='METAL'
        scene.cycles.device='GPU'
except Exception:pass
scene.render.resolution_x=int(os.getenv('KITCHEN_RESOLUTION','1536'));scene.render.resolution_y=int(scene.render.resolution_x*2/3);scene.render.resolution_percentage=100
scene.render.image_settings.file_format='PNG';scene.render.image_settings.color_mode='RGB'
scene.view_settings.view_transform='AgX';scene.view_settings.look='AgX - Medium High Contrast';scene.view_settings.exposure=-.65
scene.render.film_transparent=False
# Embed the dimension and assumption record in the native project.
spec=(ROOT/'model'/'specification.json').read_text()
t=bpy.data.texts.new('READ_ME - quotation and assumptions');t.write(spec)
scene['source_quote']='CV - 112639011 / 22 September 2026'
scene['worktop_envelope_mm']='2850 x 900 x 40'
scene['visualisation_not_shop_drawing']=True
# Export just the kitchen for easy exchange. glTF materials approximate procedural microtextures.
bpy.ops.object.select_all(action='DESELECT')
for col in [collections['01'],collections['02']]:
    for o in col.objects:
        o.select_set(True)
# Convert modeled tubes to meshes so the faucet and hob markings survive glTF export.
for obj in list(bpy.context.selected_objects):
    if obj.type=='CURVE':
        dg=bpy.context.evaluated_depsgraph_get()
        mesh=bpy.data.meshes.new_from_object(obj.evaluated_get(dg),depsgraph=dg)
        replacement=bpy.data.objects.new(obj.name+' mesh',mesh)
        for col in obj.users_collection:col.objects.link(replacement)
        replacement.matrix_world=obj.matrix_world.copy();replacement.select_set(True)
        bpy.data.objects.remove(obj,do_unlink=True)
bpy.ops.export_scene.gltf(filepath=str(ROOT/'model'/'kitchen.glb'),export_format='GLB',use_selection=True,export_apply=True,export_yup=True)
bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'model'/'kitchen.blend'))
if os.getenv('KITCHEN_RENDER','1')=='1':
    for camera in cameras:
        scene.camera=camera;scene.render.filepath=str(ROOT/'reference-views'/f'{camera.name}.png');bpy.ops.render.render(write_still=True)
scene.camera=cameras[0]
bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'model'/'kitchen.blend'))
print('KITCHEN_COMPLETE',str(ROOT),flush=True)
