#!/usr/bin/env python3
"""Original patrol corvette, metres. Blender 5.2 clean build, bake, export and QA.
Usage: blender -b --python-exit-code 1 --python build_corvette-gunship.py
Optional -- --preview skips final-resolution captures; -- --preflight runs GPU fixture.
Source maps: Poly Haven CC0. No external Python dependencies beyond Blender/numpy.
"""
import bpy, bmesh, math, os, sys, json, random, struct, subprocess, time
from pathlib import Path
from mathutils import Vector, Matrix
import numpy as np
ROOT=Path(__file__).resolve().parent
TEX=ROOT/'textures'; TEX.mkdir(exist_ok=True)
SOURCE=TEX/'source'; SOURCE.mkdir(exist_ok=True)
QA=ROOT/'qa'; QA.mkdir(exist_ok=True)
random.seed(173)
START=time.time()
PREVIEW='--preview' in sys.argv
bpy.ops.wm.read_factory_settings(use_empty=True)
sc=bpy.context.scene
sc.unit_settings.system='METRIC';sc.unit_settings.scale_length=1
sc.render.engine='CYCLES'; sc.cycles.samples=24;sc.cycles.use_denoising=True
prefs=bpy.context.preferences.addons['cycles'].preferences
for backend in ['OPTIX','CUDA']:
 try:
  prefs.compute_device_type=backend;prefs.get_devices()
  gpu=[d for d in prefs.devices if d.type==backend]
  if gpu:
   for d in prefs.devices:d.use=(d in gpu)
   sc.cycles.device='GPU';print('GPU_DEVICE',backend,[d.name for d in gpu],flush=True);break
 except Exception:pass
else: raise RuntimeError('A Cycles CUDA/OptiX GPU is required')
sc.render.image_settings.file_format='PNG'
sc.render.film_transparent=False
sc.view_settings.view_transform='AgX'
sc.view_settings.look='AgX - Medium High Contrast'
sc.render.bake.margin=12
sc.cycles.max_bounces=6;sc.cycles.transparent_max_bounces=6

def active(obj):
 bpy.ops.object.select_all(action='DESELECT');obj.select_set(True);bpy.context.view_layer.objects.active=obj

def download(asset,kind):
 p=SOURCE/f'{asset}_{kind}_1k.jpg'
 if not p.exists():
  url=f'https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/{asset}/{asset}_{kind}_1k.jpg'
  subprocess.run(['curl','-fL','--retry','2','-A','Mozilla/5.0','-sS',url,'-o',str(p)],check=True)
 return p

# Geometry is authored Z up / nose -Y. glTF export maps this to Y up / nose +Z.
MATS={}; SOURCES={}
def node(nt,typ):return nt.nodes.new(typ)
def image_node(nt,path,noncolor=False):
 n=node(nt,'ShaderNodeTexImage');n.image=bpy.data.images.load(str(path),check_existing=True)
 if noncolor:n.image.colorspace_settings.name='Non-Color'
 return n

def mathn(nt,op,a=None,b=None):
 n=node(nt,'ShaderNodeMath');n.operation=op
 for i,v in enumerate([a,b]):
  if v is None:continue
  if hasattr(v,'node'):nt.links.new(v,n.inputs[i])
  else:n.inputs[i].default_value=v
 return n.outputs[0]

def bake_material(name,asset,tint,metal,rough,normal_strength=0.25,neutral=False,trim=False,emission=0,alpha=1):
 """Bake photographic + procedural finish on a reusable tile, not a ship atlas."""
 mat=bpy.data.materials.new(name);mat.use_nodes=True
 nt=mat.node_tree;nt.nodes.clear();out=node(nt,'ShaderNodeOutputMaterial');bs=node(nt,'ShaderNodeBsdfPrincipled');nt.links.new(bs.outputs['BSDF'],out.inputs['Surface'])
 uv=node(nt,'ShaderNodeTexCoord')
 if asset:
  diff=image_node(nt,download(asset,'diff'));nor=image_node(nt,download(asset,'nor_gl'),True);arm=image_node(nt,download(asset,'arm'),True)
  for n in [diff,nor,arm]:nt.links.new(uv.outputs['UV'],n.inputs['Vector'])
  gray=node(nt,'ShaderNodeRGBToBW');nt.links.new(diff.outputs['Color'],gray.inputs[0])
  ramp=node(nt,'ShaderNodeValToRGB');nt.links.new(gray.outputs[0],ramp.inputs[0])
  # Neutral baked pigment detail; faction colour is only the runtime factor.
  low=0.32 if neutral else .40;high=1.0 if neutral else 1.12
  ramp.color_ramp.elements[0].position=.025;ramp.color_ramp.elements[0].color=tuple(min(1,c*low) for c in tint)+(1,)
  ramp.color_ramp.elements[1].position=.10 if neutral else .22;ramp.color_ramp.elements[1].color=tuple(min(1,c*high) for c in tint)+(1,)
  base=ramp.outputs['Color']
  nm=node(nt,'ShaderNodeNormalMap');nm.inputs['Strength'].default_value=normal_strength;nt.links.new(nor.outputs['Color'],nm.inputs['Color'])
  sep=node(nt,'ShaderNodeSeparateColor');nt.links.new(arm.outputs['Color'],sep.inputs[0])
  rr=mathn(nt,'ADD',mathn(nt,'MULTIPLY',sep.outputs['Green'],.35),rough-.18)
  ao=mathn(nt,'ADD',mathn(nt,'MULTIPLY',sep.outputs['Red'],.16),.84)
  norm=nm.outputs['Normal']
 else:
  rgb=node(nt,'ShaderNodeRGB');rgb.outputs[0].default_value=tuple(tint)+(1,);base=rgb.outputs[0];rr=rough;ao=1;norm=None
 if name=='Lamps':
  u=node(nt,'ShaderNodeSeparateXYZ');nt.links.new(uv.outputs['UV'],u.inputs[0])
  ramp=node(nt,'ShaderNodeValToRGB');ramp.color_ramp.interpolation='CONSTANT';nt.links.new(u.outputs['X'],ramp.inputs[0])
  ramp.color_ramp.elements.remove(ramp.color_ramp.elements[1])
  ramp.color_ramp.elements[0].color=(.7,.84,1,1)
  for pos,col in [(.25,(1,.005,.002,1)),(.5,(.005,1,.015,1)),(.75,(1,.32,.025,1))]:ramp.color_ramp.elements.new(pos).color=col
  base=ramp.outputs[0]
 # Small pitting and stretched residue, baked into each shared finish.
 noise=node(nt,'ShaderNodeTexNoise');noise.inputs['Scale'].default_value=85;noise.inputs['Detail'].default_value=3;nt.links.new(uv.outputs['UV'],noise.inputs['Vector'])
 bump=node(nt,'ShaderNodeBump');bump.inputs['Strength'].default_value=.16 if asset else 0;bump.inputs['Distance'].default_value=.008
 nt.links.new(noise.outputs['Fac'],bump.inputs['Height'])
 if norm:nt.links.new(norm,bump.inputs['Normal'])
 nt.links.new(bump.outputs[0],bs.inputs['Normal'])
 # Paint tile has a quiet seam strip at its perimeter, a photographic field, and worn rim.
 if trim:
  sepUV=node(nt,'ShaderNodeSeparateXYZ');nt.links.new(uv.outputs['UV'],sepUV.inputs[0])
  edge=mathn(nt,'MINIMUM',mathn(nt,'MINIMUM',sepUV.outputs['X'],mathn(nt,'SUBTRACT',1,sepUV.outputs['X'])),mathn(nt,'MINIMUM',sepUV.outputs['Y'],mathn(nt,'SUBTRACT',1,sepUV.outputs['Y'])))
  seam=mathn(nt,'LESS_THAN',edge,.003)
  mix=node(nt,'ShaderNodeMixRGB');mix.blend_type='MIX';nt.links.new(seam,mix.inputs[0]);nt.links.new(base,mix.inputs[1]);mix.inputs[2].default_value=(.30,.30,.30,1);base=mix.outputs[0]
 if asset:
  stretch=node(nt,'ShaderNodeVectorMath');stretch.operation='MULTIPLY';stretch.inputs[1].default_value=(9,.65,1);nt.links.new(uv.outputs['UV'],stretch.inputs[0])
  streak=node(nt,'ShaderNodeTexNoise');streak.inputs['Scale'].default_value=5;streak.inputs['Detail'].default_value=4;nt.links.new(stretch.outputs[0],streak.inputs['Vector'])
  shade=mathn(nt,'ADD',mathn(nt,'MULTIPLY',streak.outputs['Fac'],.30),.70)
  multiply=node(nt,'ShaderNodeMixRGB');multiply.blend_type='MULTIPLY';multiply.inputs[0].default_value=1;nt.links.new(base,multiply.inputs[1]);nt.links.new(shade,multiply.inputs[2]);base=multiply.outputs[0]
 nt.links.new(base,bs.inputs['Base Color']);bs.inputs['Metallic'].default_value=metal
 if hasattr(rr,'node'):nt.links.new(rr,bs.inputs['Roughness'])
 else:bs.inputs['Roughness'].default_value=rr
 # The plane UV is the unit tile. Normal bake incorporates source tangent detail + pitting.
 bpy.ops.mesh.primitive_plane_add(size=4)
 plane=bpy.context.object;plane.name='_BakeTile';plane.data.materials.append(mat)
 target=node(nt,'ShaderNodeTexImage')
 images={}
 for kind,res in [('base',1024 if asset else 64),('normal',1024 if asset else 64),('orm',512 if asset else 64)]:
  im=bpy.data.images.new(f'{name}_{kind}',res,res,alpha=False)
  if kind!='base':im.colorspace_settings.name='Non-Color'
  target.image=im;nt.nodes.active=target;active(plane)
  if kind=='normal':
   nt.links.new(bs.outputs['BSDF'],out.inputs['Surface']);bpy.ops.object.bake(type='NORMAL')
  else:
   em=node(nt,'ShaderNodeEmission')
   if kind=='base':nt.links.new(base,em.inputs['Color'])
   else:
    comb=node(nt,'ShaderNodeCombineColor')
    for inp,val in zip(['Red','Green','Blue'],[ao,rr,metal]):
     if hasattr(val,'node'):nt.links.new(val,comb.inputs[inp])
     else:comb.inputs[inp].default_value=val
    nt.links.new(comb.outputs[0],em.inputs['Color'])
   nt.links.new(em.outputs[0],out.inputs['Surface']);bpy.ops.object.bake(type='EMIT')
   nt.nodes.remove(em)
  fmt='JPEG' if asset and kind in ('base','normal') else 'PNG'
  im.filepath_raw=str(TEX/(f'{name}_{kind}.'+('jpg' if fmt=='JPEG' else 'png')));im.file_format=fmt;im.save();images[kind]=im
 bpy.data.objects.remove(plane,do_unlink=True)
 # Portable glTF node graph. Every runtime surface uses the baked result.
 nt.nodes.clear();out=node(nt,'ShaderNodeOutputMaterial');bs=node(nt,'ShaderNodeBsdfPrincipled');nt.links.new(bs.outputs[0],out.inputs[0])
 maps={}
 for kind,im in images.items():
  tex=node(nt,'ShaderNodeTexImage');tex.image=im;tex.label=f'Cycles baked {kind}';maps[kind]=tex
 if name=='Paint':
  mul=node(nt,'ShaderNodeMixRGB');mul.blend_type='MULTIPLY';mul.inputs[0].default_value=1;mul.inputs[2].default_value=(.35,.37,.40,1);mul.name='FactionPaintFactor';nt.links.new(maps['base'].outputs['Color'],mul.inputs[1]);nt.links.new(mul.outputs[0],bs.inputs['Base Color'])
 else:nt.links.new(maps['base'].outputs['Color'],bs.inputs['Base Color'])
 normal=node(nt,'ShaderNodeNormalMap');nt.links.new(maps['normal'].outputs['Color'],normal.inputs['Color']);nt.links.new(normal.outputs['Normal'],bs.inputs['Normal'])
 split=node(nt,'ShaderNodeSeparateColor');nt.links.new(maps['orm'].outputs[0],split.inputs[0]);nt.links.new(split.outputs['Green'],bs.inputs['Roughness']);nt.links.new(split.outputs['Blue'],bs.inputs['Metallic'])
 # Blender glTF recognises this standard occlusion group.
 group=bpy.data.node_groups.get('glTF Material Output')
 if not group:
  group=bpy.data.node_groups.new('glTF Material Output','ShaderNodeTree');group.interface.new_socket(name='Occlusion',in_out='INPUT',socket_type='NodeSocketFloat')
 occ=node(nt,'ShaderNodeGroup');occ.node_tree=group;nt.links.new(split.outputs['Red'],occ.inputs['Occlusion'])
 if emission:
  nt.links.new(maps['base'].outputs[0],bs.inputs['Emission Color']);bs.inputs['Emission Strength'].default_value=emission
 if alpha<1:
  bs.inputs['Alpha'].default_value=alpha;mat.surface_render_method='BLENDED';bs.inputs['Coat Weight'].default_value=.4
 mat.diffuse_color=tuple(tint)+(alpha,)
 MATS[name]=mat
 print('BAKED',name,flush=True)
 return mat

class Assembly:
 def __init__(self,name):self.name=name;self.v=[];self.f=[];self.mi=[];self.smooth=[];self.width=[];self.materials=[]
 def add(self,verts,faces,mat='Paint',bevel=.07,smooth=False):
  idx=len(self.v);self.v.extend(verts)
  if mat not in self.materials:self.materials.append(mat)
  mid=self.materials.index(mat)
  for face in faces:self.f.append(tuple(idx+i for i in face));self.mi.append(mid);self.smooth.append(smooth);self.width.append(bevel)
 def finish(self,pivot=(0,0,0)):
  mesh=bpy.data.meshes.new(self.name);mesh.from_pydata(self.v,[],self.f);mesh.update()
  ob=bpy.data.objects.new(self.name,mesh);sc.collection.objects.link(ob)
  for m in self.materials:mesh.materials.append(MATS[m])
  for p,m,s in zip(mesh.polygons,self.mi,self.smooth):p.material_index=m;p.use_smooth=s
  # Component-sensitive bevels; rounded axial sides retain smooth normals.
  bm=bmesh.new();bm.from_mesh(mesh);layer=bm.edges.layers.float.new('bevel_weight_edge')
  bm.faces.ensure_lookup_table()
  for e in bm.edges:
   if len(e.link_faces)==2 and e.calc_face_angle(0)>math.radians(28):e[layer]=min(self.width[f.index] for f in e.link_faces)
  bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces));bm.to_mesh(mesh);bm.free()
  active(ob)
  mod=ob.modifiers.new('Manufactured edge radii','BEVEL');mod.width=1;mod.segments=2;mod.limit_method='WEIGHT';mod.harden_normals=True
  bpy.ops.object.modifier_apply(modifier=mod.name)
  # Reduce only after all bevels exist; large face planes and cylindrical profiles survive.
  count=sum(len(p.vertices)-2 for p in ob.data.polygons)
  budget=113000 if self.name=='Hull' else (2800 if self.name.startswith('Turret_') else 2600)
  if count>budget:
   dec=ob.modifiers.new('Game triangle allocation','DECIMATE');dec.ratio=budget/count;dec.use_collapse_triangulate=True
   bpy.ops.object.modifier_apply(modifier=dec.name)
  # Remove bevel/decimation slivers before export; validate serialized triangles separately.
  clean=bmesh.new();clean.from_mesh(ob.data)
  bmesh.ops.triangulate(clean,faces=list(clean.faces),quad_method='BEAUTY',ngon_method='BEAUTY')
  dead=[f for f in clean.faces if f.calc_area()<1e-10]
  if dead:bmesh.ops.delete(clean,geom=dead,context='FACES')
  clean.to_mesh(ob.data);clean.free();ob.data.update()
  mod=ob.modifiers.new('Face-weighted normals','WEIGHTED_NORMAL');mod.keep_sharp=True;mod.weight=35
  bpy.ops.object.modifier_apply(modifier=mod.name)
  mesh=ob.data
  # Projection in metres: 4m tiling across major plates, 2m on small metals.
  uv=mesh.uv_layers.new(name='UVMap')
  for p in mesh.polygons:
   n=p.normal;axis=max(range(3),key=lambda i:abs(n[i]));scale=4 if mesh.materials[p.material_index].name=='Paint' else 2
   for li in p.loop_indices:
    co=mesh.vertices[mesh.loops[li].vertex_index].co
    xy=(co.y,co.z) if axis==0 else ((co.x,co.z) if axis==1 else (co.x,co.y))
    uv.data[li].uv=(xy[0]/scale,xy[1]/scale)
    if mesh.materials[p.material_index].name=='Lamps':
     cx,cy,cz=p.center
     swatch=(.375 if cx>0 else .625) if abs(cx)>15 and 23<cy<25 else (.875 if cz>9 else .125)
     uv.data[li].uv=(swatch+xy[0]*.0001,.5+xy[1]*.0001)
  # Move vertices to the pivot without applying transforms on any moving node.
  p=Vector(pivot)
  for v in mesh.vertices:v.co-=p
  ob.location=p
  mesh.update()
  return ob

def cube(A,loc,size,mat='Paint',bev=.07,rot=None):
 x,y,z=size[0]/2,size[1]/2,size[2]/2
 vs=[(-x,-y,-z),(x,-y,-z),(x,y,-z),(-x,y,-z),(-x,-y,z),(x,-y,z),(x,y,z),(-x,y,z)]
 if rot:vs=[tuple(rot@Vector(v)+Vector(loc)) for v in vs]
 else:vs=[tuple(a+b for a,b in zip(v,loc)) for v in vs]
 A.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)],mat,bev)

def cyl(A,loc,r,depth,mat='Alloy',axis=(0,0,1),n=24,r2=None,bev=.035):
 r2=r if r2 is None else r2
 q=Vector((0,0,1)).rotation_difference(Vector(axis));vs=[]
 for z,rad in [(-depth/2,r),(depth/2,r2)]:
  for i in range(n):vs.append(tuple(q@Vector((rad*math.cos(i*2*math.pi/n),rad*math.sin(i*2*math.pi/n),z))+Vector(loc)))
 fs=[tuple(reversed(range(n))),tuple(range(n,2*n))]+[(i,(i+1)%n,(i+1)%n+n,i+n) for i in range(n)]
 A.add(vs,fs,mat,bev,True)

def tube(A,loc,outer,inner,length,mat='Alloy',axis=(0,1,0),n=48,bevel=.04):
 q=Vector((0,0,1)).rotation_difference(Vector(axis));vs=[]
 for z,r in [(-length/2,outer),(length/2,outer),(length/2,inner),(-length/2,inner)]:
  for i in range(n):vs.append(tuple(q@Vector((r*math.cos(i*2*math.pi/n),r*math.sin(i*2*math.pi/n),z))+Vector(loc)))
 fs=[]
 for k in range(4):
  for i in range(n):j=(i+1)%n;fs.append((k*n+i,k*n+j,((k+1)%4)*n+j,((k+1)%4)*n+i))
 A.add(vs,fs,mat,bevel,True)

def beam(A,a,b,r=.06,mat='Alloy',n=8):
 d=Vector(b)-Vector(a);cyl(A,(Vector(a)+Vector(b))/2,r,d.length,mat,d,n,bev=.008)

def loft(A,sections,mat='Paint',bev=.12):
 # Sections: y, halfwidth, bottom, top, chamferX, chamferZ.
 vs=[]
 for y,w,bot,top,cx,cz in sections:
  vs.extend([(w,y,bot+cz),(w,y,top-cz),(w-cx,y,top),(-w+cx,y,top),(-w,y,top-cz),(-w,y,bot+cz),(-w+cx,y,bot),(w-cx,y,bot)])
 fs=[tuple(reversed(range(8))),tuple(range(len(vs)-8,len(vs)))]+[(k*8+i,k*8+(i+1)%8,(k+1)*8+(i+1)%8,(k+1)*8+i) for k in range(len(sections)-1) for i in range(8)]
 A.add(vs,fs,mat,bev)

def slab(A,points,thick=.12,mat='Paint',bev=.04):
 # Polygon extruded along its surface normal, gives actual plate thickness.
 p=[Vector(v) for v in points];normal=(p[1]-p[0]).cross(p[2]-p[0]).normalized();N=len(p)
 vs=[tuple(v-normal*thick) for v in p]+[tuple(v) for v in p]
 fs=[tuple(reversed(range(N))),tuple(range(N,2*N))]+[(i,(i+1)%N,(i+1)%N+N,i+N) for i in range(N)]
 A.add(vs,fs,mat,bev)

def frame(A,pts,r=.1,mat='Alloy'):
 for a,b in zip(pts,pts[1:]+pts[:1]):beam(A,a,b,r,mat)

def bolt(A,p,axis=(0,0,1),r=.085):cyl(A,p,r,.065,'Alloy',axis,6,bev=.007)

def grille(A,loc,size,side=False):
 x,y,z=loc;w,l,h=size
 cube(A,loc,size,'DarkMetal',.05)
 if side:
  for j in range(max(3,int(h/.25))):cube(A,(x,y,z-h/2+.12+j*.25),(w+.05,l-.22,.07),'Alloy',.015)
 else:
  for j in range(max(3,int(l/.27))):cube(A,(x,y-l/2+.12+j*.27,z+h/2+.025),(w-.2,.065,.06),'DarkMetal',.012)

def empty(name,pos,parent=None):
 o=bpy.data.objects.new(name,None);sc.collection.objects.link(o);o.empty_display_type='ARROWS';o.empty_display_size=.7
 if parent:o.parent=parent;o.location=parent.matrix_world.inverted()@Vector(pos)
 else:o.location=pos
 return o

# One representative bake/export/import fixture runs before the expensive build.
if '--preflight' in sys.argv:
 bake_material('Probe','metal_plate_02',(.7,.7,.7),.6,.5)
 a=Assembly('ProbeMesh');cube(a,(0,0,0),(2,3,1),'Probe',.1);o=a.finish()
 active(o);bpy.ops.export_scene.gltf(filepath=str(QA/'fixture.glb'),export_format='GLB',use_selection=True,export_yup=True)
 bpy.data.objects.remove(o,do_unlink=True)
 bpy.data.node_groups.remove(bpy.data.node_groups['glTF Material Output'])
 bpy.ops.import_scene.gltf(filepath=str(QA/'fixture.glb'))
 print('PREFLIGHT_OK',len([o for o in sc.objects if o.type=='MESH']),flush=True)
 sys.exit(0)

# Scope: 85 x 32 x 18 m; <=140k triangles, <=9 mats, <=12 MB, 1024/512 px.
# Cameras: hero 3/4 front, true orthographic side, clay, rear, close-up, 30-degree pivots.
# Three revision maximum; photographic tile + real panel seams, no unique hull atlas.
# ---------------- PRIMARY CONSTRUCTION ----------------
# Source photograph grades are baked to 1024px neutral tiles and 512px ORM.
bake_material('Paint','metal_plate_02',(.98,.98,.98),.60,.30,.25,neutral=True,trim=False)
bake_material('Alloy','rusty_metal_04',(.46,.49,.51),.87,.39,.18)
bake_material('DarkMetal','rusty_metal_04',(.095,.115,.13),.72,.52,.22)
bake_material('Scorched','rusty_metal_04',(.19,.145,.11),.82,.44,.28)
bake_material('Weapons','metal_plate_02',(.24,.26,.27),.78,.46,.22)
bake_material('Glass',None,(.012,.029,.042),.28,.10,0,alpha=.72)
bake_material('Engines',None,(.10,.47,1.0),.25,.25,.05,emission=3)
bake_material('Lamps',None,(.7,.84,1.0),.1,.29,.05,emission=2)
H=Assembly('Hull')
# A continuous pressure hull beneath separated, thick armour plates.
SEC=[(-42.5,3.25,-1.35,1.25,.70,.42),(-36,4.75,-2.15,2.45,1.2,.85),(-28,6.8,-3.65,3.75,1.65,1.4),(-19,9,-4.65,4.8,2.1,1.8),(-13,10,-5.25,5.0,2.15,1.9),(-3,10,-5.4,5.0,2.15,1.9),(3,9.3,-5.05,4.8,1.6,1.65),(18,9.3,-5.05,4.8,1.6,1.65),(24,10,-5.0,4.8,2.1,1.5),(34,10,-4.5,4.4,2.1,1.5)]
loft(H,SEC,'DarkMetal',.16)
def profile(y):
 for a,b in zip(SEC,SEC[1:]):
  if a[0]<=y<=b[0]:
   t=(y-a[0])/(b[0]-a[0]);return [y]+[a[i]*(1-t)+b[i]*t for i in range(1,6)]
 return SEC[0] if y<SEC[0][0] else SEC[-1]
def ring(y):
 _,w,bot,top,cx,cz=profile(y)
 return [Vector(p) for p in [(w,y,bot+cz),(w,y,top-cz),(w-cx,y,top),(-w+cx,y,top),(-w,y,top-cz),(-w,y,bot+cz),(-w+cx,y,bot),(w-cx,y,bot)]]
def point_surface(y,face,t):
 r=ring(y);return r[face].lerp(r[(face+1)%8],t)
# Authored broad panels with continuous recessed joints and staggered longitudinal seams.
ys=[-42.4,-37,-31,-25,-19.1,-13.1,-8.1,-3.1,2.9,7.9,12.9,17.9,23.9,28.9,33.9]
for f in range(8):
 lanes=3 if f in [2,6] else (2 if f in [0,4] else 1)
 for lane in range(lanes):
  cuts=[ys[0]]+[y+(1.05 if lane%2 else -.45) for y in ys[1:-1]]+[ys[-1]]
  for k,(ya,yb) in enumerate(zip(cuts,cuts[1:])):
   if f in [0,4] and ya<-13:
    lo=.008 if lane==0 else .60;hi=.40 if lane==0 else .992
   else:lo=lane/lanes+.003;hi=(lane+1)/lanes-.003
   points=[point_surface(ya+.025,f,lo),point_surface(ya+.025,f,hi),point_surface(yb-.025,f,hi),point_surface(yb-.025,f,lo)]
   normal=(points[1]-points[0]).cross(points[2]-points[0]).normalized()
   center=sum(points,Vector())/4
   if normal.dot(Vector((center.x,0,center.z)))<0:points.reverse();normal=-normal
   points=[tuple(p+normal*.09) for p in points]
   slab(H,points,.13,'Paint',.028)
# Broad lower and upper lips frame the prow's recessed sensor/service mouth.
slab(H,[(-3.10,-42.54,.48),(3.10,-42.54,.48),(2.53,-42.54,1.22),(-2.53,-42.54,1.22)],.14,'Paint',.065)
slab(H,[(-2.53,-42.54,-1.29),(2.53,-42.54,-1.29),(3.1,-42.54,-.48),(-3.1,-42.54,-.48)],.14,'Paint',.065)
for z in [-.52,.52]:beam(H,(-3.04,-42.58,z),(3.04,-42.58,z),.055,'Alloy',10)
for x in [-2.6,-1.6,1.6,2.6]:cube(H,(x,-42.56,0),(.13,.16,.65),'Weapons',.025)
# The split prow has visible longitudinal rails and protected machinery, not a painted stripe.
for s in [-1,1]:
 for ya,yb in zip(ys[:6],ys[1:7]):
  xa=profile(ya)[1]*s;xb=profile(yb)[1]*s
  for z in [-.53,.53]:beam(H,(xa,ya,z),(xb,yb,z),.10,'Alloy',10)
 # Three unequal recessed service troughs replace repeated external pegs.
 for bay,(ya,yb) in enumerate([(-39.8,-34.3),(-31.6,-24.1),(-22.0,-14.9)]):
  if s<0:ya+=.5;yb-=.3
  xa=profile(ya)[1]*s;xb=profile(yb)[1]*s
  pts=[(xa+s*.025,ya,-.40),(xb+s*.025,yb,-.40),(xb+s*.025,yb,.40),(xa+s*.025,ya,.40)]
  slab(H,pts,.13,'DarkMetal',.035)
  for z in [-.22,.18]:
   beam(H,(xa+s*.08,ya+.3,z),(xb+s*.08,yb-.3,z),.065 if bay!=1 else .09,'Weapons',10)
  for yy in [ya+.6,yb-.65]:
   xx=profile(yy)[1]*s
   cube(H,(xx+s*.10,yy,0),(.12,.18,.70),'Weapons',.028)
  if bay==1:
   yy=(ya+yb)*.5;xx=profile(yy)[1]*s
   cube(H,(xx+s*.08,yy,0),(.10,1.3,.62),'DarkMetal',.04)
   cube(H,(xx+s*.145,yy,0),(.045,.93,.37),'Glass',.025)
 # Shoulder's heavy recessed service hatches, gaskets, hinges and latches.
 for y in [-11,-5.5]:
  x=s*10.09
  cube(H,(x,y,-2.65),(.10,4.14,3.15),'DarkMetal',.08)
  cube(H,(x+s*.055,y,-2.65),(.10,3.97,2.98),'Paint',.08)
  for yy in [y-1.72,y+1.72]:
   for zz in [-3.9,-1.4]:bolt(H,(x+s*.12,yy,zz),(s,0,0),.055)
  for zz in [-3.5,-1.8]:cube(H,(x+s*.12,y+1.9,zz),(.09,.12,.12),'Weapons',.018)
  cube(H,(x+s*.12,y-1.3,-2.3),(.05,.12,.08),'DarkMetal',.015)
 # Torso service belts and exposed compression conduits.
 for z in [-3.65,3.6]:
  beam(H,(s*9.65,-2.7,z),(s*9.5,4,z),.13,'Alloy',12)
 for z in [-.9,-.3,.3,.9]:
  beam(H,(s*9.6,-3,z),(s*9.6,2.5,z),.14,'Weapons',12)
  for y in [-2.8,1.9]:cube(H,(s*9.6,y,z),(.45,.3,.47),'Paint',.04)
# Foredeck maintenance inset panels, hinge pins, sunk fasteners, and structural ribs.
for y in [-32,-16,-6]:
 w=profile(y)[1]-profile(y)[4];z=profile(y)[3]+.12
 for s in [-1,1]:
  x=s*w*.57
  cube(H,(x,y,z),(.95,2.30,.12),'DarkMetal',.06)
  cube(H,(x,y,z+.06),(.78,2.12,.12),'Paint',.05)
  cube(H,(x+.22,y-.48,z+.15),(.07,.4,.07),'Alloy',.015)
  for yy in [y-.8,y+.8]:bolt(H,(x-.25,yy,z+.14),r=.065)
# Thick armour shoulders frame long service wells.
for s in [-1,1]:
 for y in [-11,-5,1]:
  cube(H,(s*7.5,y,4.4),(1.20,4.7,.28),'Paint',.08)
  for dy in [-1.9,1.9]:bolt(H,(s*7.5,y+dy,4.58),r=.09)
# Reinforced ventral keel and flush closed landing/maintenance doors.
loft(H,[( -17,4.8,-5.8,-4.3,.8,.3),(-10,5.6,-6.3,-4.6,.9,.3),(4,5.6,-6.3,-4.6,.9,.3),(20,4.1,-5.6,-4.3,.8,.3)],'Paint',.13)
for s in [-1,1]:
 for y in [-9,5,16]:
  cube(H,(s*3.0,y,-6.35),(3.4,4.6,.15),'DarkMetal',.08)
  cube(H,(s*3.0,y,-6.45),(3.15,4.34,.15),'Paint',.06)
  for yy in [y-1.5,y+1.5]:cube(H,(s*3,y+yy-y,-6.54),(1.6,.15,.12),'Alloy',.02)
# ---------------- RAISED BRIDGE ----------------
loft(H,[(-12,5.5,4.4,5.5,1.0,.35),(-9.9,5.2,4.6,9.4,1.0,.6),(1.5,4.55,4.7,9.4,.8,.6),(3.2,4.3,4.6,5.5,.7,.25)],'Paint',.16)
# Continuous dark upper band with real framed glazing on angled front and side surfaces.
# Front is sloped from y -12 at bottom to -10 at roof.
slab(H,[(-4.18,-10.58,8.0),(4.18,-10.58,8.0),(4.18,-10.06,9.04),(-4.18,-10.06,9.04)],.16,'DarkMetal',.06)
for x in [-2.90,0,2.90]:
 pts=[(x-1.30,-10.63,8.16),(x+1.30,-10.63,8.16),(x+1.25,-10.22,8.91),(x-1.25,-10.22,8.91)]
 slab(H,pts,.055,'Glass',.018);frame(H,pts,.042,'Weapons')
for s in [-1,1]:
 for ya,yb in [(-8.9,-5.55),(-5.15,-1.8),(-1.4,1.0)]:
  wa=5.2-(ya+9.9)*.65/11.4;wb=5.2-(yb+9.9)*.65/11.4
  pts=[(s*(wa+.08),ya,8.0),(s*(wb+.08),yb,8.0),(s*(wb+.08),yb,8.85),(s*(wa+.08),ya,8.85)]
  slab(H,pts,.12,'DarkMetal',.05)
  pts=[(s*(wa+.13),ya+.13,8.15),(s*(wb+.13),yb-.13,8.15),(s*(wb+.13),yb-.13,8.70),(s*(wa+.13),ya+.13,8.70)]
  slab(H,pts,.055,'Glass',.018)
  frame(H,pts,.038,'Weapons')
 # Recessed bridge equipment access plates with human-scale captive latches.
 cube(H,(s*5.02,-4.2,6.35),(.22,3.4,2.15),'DarkMetal',.07)
 cube(H,(s*5.14,-4.2,6.35),(.20,3.12,1.9),'Paint',.07)
 for y in [-5.45,-2.95]:
  for z in [5.65,7.05]:bolt(H,(s*5.255,y,z),(s,0,0),.045)
# Stepped armoured bridge foundation and individually framed blast-shutter panels.
loft(H,[(-13.1,5.9,4.65,5.0,1.0,.18),(-11.7,5.9,4.65,5.75,1.1,.30),(2.5,5.1,4.65,5.75,.85,.28),(4.0,4.6,4.65,5.0,.75,.18)],'Paint',.10)
for x,w in [(-2.55,2.0),(.2,1.4),(2.75,1.5)]:
 pts=[(x-w/2,-11.77,5.9),(x+w/2,-11.77,5.9),(x+w/2-.15,-11.00,7.65),(x-w/2,-11.00,7.65)]
 slab(H,pts,.09,'DarkMetal',.04)
 ctr=sum([Vector(p) for p in pts],Vector())/4
 slab(H,[tuple(ctr+(Vector(p)-ctr)*.91+Vector((0,-.045,0))) for p in pts],.07,'Paint',.045)
 for xx in [x-w*.33,x+w*.33]:bolt(H,(xx,-11.50,6.55),(0,-1,.2),.06)
for x in [-2.8,0,2.8]:
 beam(H,(x,2.95,5.35),(x,1.94,8.90),.085,'Alloy',10)
# Narrow compression gaskets seat the command citadel on its armored collar.
for s in [-1,1]:
 beam(H,(s*5.76,-11.55,5.72),(s*4.99,2.35,5.72),.055,'DarkMetal',8)
 # Hairline exposed metal on the visor's leading return, rather than bright window borders.
beam(H,(-4.2,-10.87,9.12),(4.2,-10.87,9.12),.025,'Alloy',8)
# Protected ventilation access below rear glazing.
cube(H,(0,2.40,7.0),(3.4,.22,1.15),'DarkMetal',.06)
for x in [-1.3,-.8,-.3,.2,.7,1.2]:cube(H,(x,2.55,7.0),(.08,.20,1.0),'Alloy',.02)
# Armoured visor cap reaches the exact 18m height envelope with ventral turret.
loft(H,[(-10.9,4.95,9.08,9.70,.6,.18),(-9.9,4.95,9.08,9.80,.6,.18),(1.7,4.85,9.08,9.80,.65,.18),(2.4,4.0,9.08,9.65,.6,.18)],'Paint',.11)
cube(H,(0,-3.4,9.87),(3.1,4.7,.26),'Paint',.07)
grille(H,(0,-3.4,10.01),(2.5,3.7,.08))
# Navigation sensors are low, armoured military fittings, no decorative giant antennae.
for s in [-1,1]:
 cube(H,(s*2.8,.1,9.85),(.85,1.8,.22),'Weapons',.06)
 cyl(H,(s*3.05,-7.7,9.83),.24,.25,'Alloy',n=16)
# Scaled windows in the pressure-hull accommodation belt (about 0.5m high).
for s in [-1,1]:
 for i in range(13):
  y=-12+i*.94;x=s*10.12
  cube(H,(x,y,1.9),(.14,.63,.40),'DarkMetal',.045)
  cube(H,(x+s*.08,y,1.9),(.045,.44,.22),'Glass',.025)
  if i in [1,4,7,11]:cube(H,(x+s*.11,y,1.9),(.035,.32,.11),'Lamps',.01)
# ---------------- FLANK MISSILE CASSETTES ----------------
# Five armoured vertical cells per flank, two retained missiles in each.
HARD=[]
for s in [-1,1]:
 cube(H,(s*9.72,10.3,-.1),(.80,14.1,8.4),'Weapons',.20)
 for y in [3.1,17.5]:cube(H,(s*10.15,y,-.1),(.95,.65,8.5),'Paint',.16)
 for z in [-4.5,4.25]:cube(H,(s*10.12,10.3,z),(1.05,14.7,.52),'Paint',.13)
 for i in range(5):
  y=4.6+i*2.85
  cube(H,(s*10.29,y,-.1),(.65,2.58,7.8),'DarkMetal',.20)
  pts=[(s*10.75,y-1.18,-3.83),(s*10.75,y+1.18,-3.83),(s*10.75,y+1.18,3.7),(s*10.75,y-1.18,3.7)]
  frame(H,pts,.12,'Alloy')
  for dy in [-.53,.53]:
   cyl(H,(s*10.74,y+dy,-.26),.34,5.5,'Alloy',n=16,bev=.022)
   cyl(H,(s*10.74,y+dy,2.7),.34,.7,'Weapons',n=16,r2=.13,bev=.015)
   cyl(H,(s*10.74,y+dy,-3.12),.34,.34,'Weapons',n=16,r2=.24,bev=.02)
   for zz in [-2.4,1.9]:cyl(H,(s*10.74,y+dy,zz),.355,.16,'Scorched',n=16,bev=.015)
   for zz in [-1.4,.7]:cube(H,(s*11.04,y+dy,zz),(.14,.7,.12),'Weapons',.025)
  HARD.append((s*10.75,y,3.9))
  for zz in [-3.5,3.45]:bolt(H,(s*10.94,y,zz),(s,0,0),.09)
# Swept cheek armor joins the missile bays to the pressure hull: recess, not a crate.
for sign in [-1,1]:
 for za,zb in [(3.85,4.75),(-5.0,-4.1)]:
  pts=[(sign*10.0,-2.4,zb),(sign*10.95,3.1,za),(sign*10.95,17.7,za),(sign*11.3,18.5,zb)]
  slab(H,pts,.30,'Paint',.10)
 for ya,yb,xa,xb in [(-2.4,3.1,10.0,10.95),(17.5,18.5,10.95,11.3)]:
  pts=[(sign*xa,ya,-4.65),(sign*xb,yb,-4.1),(sign*xb,yb,3.9),(sign*xa,ya,4.65)]
  slab(H,pts,.32,'Paint',.12)
 for y in [3.2,6.0,8.85,11.7,14.55,17.45]:
  cube(H,(sign*10.84,y,-.1),(.26,.22,7.45),'Weapons',.055)
# Recessed forward heat-exchanger trough follows each swept missile fairing.
for sign in [-1,1]:
 def cheekx(y):return sign*(10.0+(y+2.4)*.95/5.5+.045)
 ya,yb=-1.75,2.15
 pts=[(cheekx(ya),ya,-1.65),(cheekx(yb),yb,-1.65),(cheekx(yb),yb,1.65),(cheekx(ya),ya,1.65)]
 slab(H,pts,.08,'DarkMetal',.07)
 for z in [-1.05,-.35,.35,1.05]:
  beam(H,(cheekx(ya+.22)+sign*.11,ya+.22,z),(cheekx(yb-.22)+sign*.11,yb-.22,z),.13,'Weapons',12)
  for y in [ya+.4,yb-.4]:
   cube(H,(cheekx(y)+sign*.12,y,z),(.22,.19,.38),'Alloy',.025)
 for z in [-3.55,3.40]:
  beam(H,(cheekx(-1.85),-1.85,z),(cheekx(2.35),2.35,z),.045,'DarkMetal',8)
# Raised dorsal gun deck with a distinct neck behind the command citadel.
loft(H,[(3.5,3.5,4.65,5.0,.65,.15),(5.0,3.8,4.65,6.05,.65,.30),(18.4,3.8,4.65,6.05,.65,.30),(20.5,3.3,4.65,5.0,.60,.15)],'Paint',.12)
for sign in [-1,1]:
 for y in [6.8,9.5,16.7]:
  cube(H,(sign*3.79,y,5.50),(.13,1.9,.45),'DarkMetal',.045)
  for dy in [-.7,-.35,0,.35,.7]:cube(H,(sign*3.87,y+dy,5.50),(.10,.07,.35),'Alloy',.015)
# Deep flank air/heat mouths behind missile cells, swept shoulder pods.
for s in [-1,1]:
 # Broad octagonal nacelle fairing, maximum width is set by armour at 16m.
 sec=[(18.4,.65,-3.9,3.9,.25,.9),(23,4.4,-5.2,5.1,1.0,1.2),(29.5,3.9,-5.1,3.8,1.25,1.5),(35.3,3.1,-4.9,1.7,1.0,1.15)]
 tmp=Assembly('pod')
 loft(tmp,sec,'Paint',.19)
 tmp.v=[(x+s*(10.65 if i<8 else 11.4),y,z) for i,(x,y,z) in enumerate(tmp.v)]
 # Preserve local component materials while transferring.
 # Leave the forward outer flank physically open for the recessed cooling intake.
 tmp.f.pop(2+(0 if s>0 else 4))
 H.add(tmp.v,tmp.f,'Paint',.19)
 # Side intake: a large black recessed trapezoid with heavy lip and deep louvres.
 pts=[(s*11.34,18.5,-1.72),(s*12.0,19.16,-3.11),(s*15.82,22.95,-3.98),(s*15.82,22.95,3.88),(s*12.0,19.16,3.08),(s*11.34,18.5,1.72)]
 # Clipped forward corners make a thick hexagonal intake mouth.
 slab(H,[(s*11.34,18.5,-2.94),pts[0],pts[1]],.22,'Paint',.06)
 slab(H,[(s*11.34,18.5,2.94),pts[-2],pts[-1]],.22,'Paint',.06)
 back=[(x-s*.50,y,z) for x,y,z in pts]
 slab(H,back,.15,'DarkMetal',.06)
 for i in range(len(pts)):slab(H,[pts[i],pts[(i+1)%len(pts)],back[(i+1)%len(pts)],back[i]],.12,'Weapons',.04)
 frame(H,pts,.19,'Paint')
 frame(H,[(x+s*.02,y,z) for x,y,z in pts],.055,'Alloy')
 for j in range(15):
  z=-3.05+j*.42
  beam(H,(s*11.90,19.10,z*.85),(s*15.49,22.65,z),.085,'Weapons',8)
 # Swept service cover follows the nacelle taper; attachment points stay on the skin.
 for ya,yb in [(24,28.0),(30,33.0)]:
  def wallx(y):return 15.8-(y-23)*.5/6.5 if y<29.5 else 15.3-(y-29.5)*.8/5.8
  def walltop(y):return 3.9-(y-23)*1.6/6.5-.25 if y<29.5 else 2.3-(y-29.5)*1.75/5.8-.25
  pts=[(s*(wallx(ya)+.16),ya,-3.3),(s*(wallx(yb)+.16),yb,-3.3),(s*(wallx(yb)+.16),yb,walltop(yb)),(s*(wallx(ya)+.16),ya,walltop(ya))]
  slab(H,pts,.16,'DarkMetal',.07)
  ctr=sum([Vector(p) for p in pts],Vector())/4
  slab(H,[tuple(ctr+(Vector(p)-ctr)*.93+Vector((s*.07,0,0))) for p in pts],.13,'Paint',.08)
  for y in [ya+.4,yb-.4]:bolt(H,(s*(wallx(y)+.30),y,-2.5),(s,0,0),.08)
 # Distinct upper, inboard engine cowls. Broad bevels catch light around each housing.
 c=Assembly('uppercowl')
 loft(c,[(22.0,2.4,2.0,4.95,.70,.35),(25.0,3.15,1.4,6.20,1.45,1.40),(31.0,3.1,.0,5.85,1.45,1.45),(35.6,2.80,-.55,5.10,1.25,1.20)],'Paint',.16)
 c.v=[(x+s*6.4,y,z) for x,y,z in c.v];H.add(c.v,c.f,'Paint',.24)
 for y in [26.0,30.0]:
  z=6.16-(y-25)*.35/6
  cube(H,(s*6.4,y,z),(2.45,2.0,.14),'DarkMetal',.07)
  cube(H,(s*6.4,y,z+.06),(2.2,1.75,.15),'Paint',.06)
 # Two fitted transverse seams follow the cowl's octagonal upper contour.
 for y in [27.5,32.3]:
  if y<31:t=(y-25)/6;w=3.15-.05*t;top=6.2-.35*t;cx=1.45;cz=1.40+.05*t
  else:t=(y-31)/4.6;w=3.10-.3*t;top=5.85-.75*t;cx=1.45-.20*t;cz=1.45-.25*t
  pts=[(s*6.4+w+.025,y,top-cz),(s*6.4+w-cx,y,top+.025),(s*6.4-w+cx,y,top+.025),(s*6.4-w-.025,y,top-cz)]
  for a,b in zip(pts,pts[1:]):beam(H,a,b,.028,'DarkMetal',8)
 # Paired conduits run into individual engine housings.
 for dx in [-.52,.52]:beam(H,(s*6.4+dx,31,5.6),(s*6.4+dx,35.3,4.85),.09,'Alloy',12)
# Central aft service spine stays narrow so the four cowls read individually.
loft(H,[(20.0,2.5,4.4,4.95,.4,.2),(23.0,2.5,4.4,5.25,.4,.2),(31.0,2.25,4.3,5.0,.4,.2),(34.0,2.0,4.1,4.65,.4,.2)],'Paint',.09)
for y in [24.5,29.0]:grille(H,(0,y,5.20),(2.9,2.2,.18))
# ---------------- FOUR LARGE ENGINE BELLS ----------------
ENG=[]
for i,(x,z) in enumerate([(-6.4,2.2),(6.4,2.2),(-11.5,-2.1),(11.5,-2.1)],1):
 # Inboard/outboard spacing follows the reference's staggered four-nozzle trapezoid.
 front=32.0;end=42.5;r=2.72
 cyl(H,(x,34.0,z),r*1.02,4.0,'Weapons',(0,1,0),48,r2=r*.91,bev=.09)
 for y,rad in [(33.0,2.85),(35.0,2.75),(36.4,2.71)]:tube(H,(x,y,z),rad,rad-.22,.33,'Alloy',n=48,bevel=.045)
 tube(H,(x,39.4,z),2.83,2.48,6.2,'Scorched',n=48,bevel=.065)
 # Armour petals are wedge sectors, not a featureless cylinder.
 for j in range(16):
  a=j*2*math.pi/16+.025;b=(j+1)*2*math.pi/16-.025
  vs=[]
  for y,rad in [(36.6,2.90),(40.4,3.02),(42.34,2.81)]:
   vs.extend([(x+rad*math.cos(a),y,z+rad*math.sin(a)),(x+rad*math.cos(b),y,z+rad*math.sin(b))])
  slab(H,[vs[0],vs[1],vs[3],vs[2]],.15,'Weapons',.04)
  slab(H,[vs[2],vs[3],vs[5],vs[4]],.15,'Scorched',.04)
  a=(a+b)/2
  beam(H,(x+2.79*math.cos(a),33.8,z+2.79*math.sin(a)),(x+2.87*math.cos(a),37,z+2.87*math.sin(a)),.075,'Alloy',8)
  bolt(H,(x+2.95*math.cos(a),39.6,z+2.95*math.sin(a)),(math.cos(a),0,math.sin(a)),.085)
 for y in [38.0,38.43]:tube(H,(x,y,z),3.02,2.89,.18,'Scorched',n=64,bevel=.025)
 tube(H,(x,42.30,z),2.86,2.44,.40,'Alloy',n=64,bevel=.045)
 tube(H,(x,41.75,z),2.46,2.19,.30,'Engines',n=64,bevel=.025)
 # Recessed ceramic throat, concentric emitter and physical radial vanes.
 cyl(H,(x,40.78,z),2.35,.24,'DarkMetal',(0,1,0),64,bev=.035)
 cyl(H,(x,40.95,z),1.81,.10,'Engines',(0,1,0),64,bev=.025)
 tube(H,(x,41.10,z),1.97,1.78,.18,'Alloy',n=48,bevel=.025)
 cyl(H,(x,41.17,z),.67,.22,'Engines',(0,1,0),32,bev=.04)
 for j in range(16):
  a=j*math.pi/8
  beam(H,(x+2.15*math.cos(a),41.25,z+2.15*math.sin(a)),(x+1.23*math.cos(a),41.06,z+1.23*math.sin(a)),.10,'Weapons',8)
 ENG.append((x,end,z))
# ---------------- TURRETS AND GUN CRADLES ----------------
MOV=[];MUZZLES=[]
def turret(index,y,z,ventral=False):
 d=-1 if ventral else 1
 # Fixed barbette belongs to Hull. Turret shell rotates on the centred bearing.
 cyl(H,(0,y,z),2.8,.42,'DarkMetal',n=48,bev=.07)
 tube(H,(0,y,z+d*.23),2.68,2.33,.24,'Alloy',axis=(0,0,1),n=48,bevel=.04)
 A=Assembly(f'Turret_{index}');G=Assembly(f'Gun_{index}')
 cyl(A,(0,y,z+d*.30),2.34,.30,'Weapons',n=48,bev=.06)
 # Angled low casemate. Ventral shell mirrors the dorsal construction.
 ts=[(y-2.3,2.05,.35,1.35,.50,.30),(y-.75,2.35,.35,2.05,.50,.40),(y+1.7,1.9,.35,1.65,.45,.30)]
 sections=[]
 for yy,w,bo,to,cx,cz in ts:
  zs=sorted([z+d*bo,z+d*to]);sections.append((yy,w,zs[0],zs[1],cx,cz))
 loft(A,sections,'Weapons',.12)
 for s in [-1,1]:
  cube(A,(s*2.1,y+.15,z+d*1.05),(.20,2.3,.8),'Alloy',.06)
  for yy in [y-.6,y+.8]:bolt(A,(s*2.24,yy,z+d*1.05),(s,0,0),.075)
  cyl(A,(s*1.4,y-1.55,z+d*1.1),.55,.28,'Weapons',(1,0,0),24,bev=.05)
 cube(A,(0,y+.15,z+d*1.95),(1.5,1.15,.20),'Weapons',.05)
 cube(A,(.85,y-.6,z+d*1.92),(.36,.55,.22),'DarkMetal',.035)
 cube(A,(.85,y-.88,z+d*1.92),(.22,.06,.10),'Glass',.012)
 pivot=(0,y-1.55,z+d*1.10)
 # Twin barrels are one elevating node, with a real trunnion axle joining them.
 cyl(G,pivot,.42,2.65,'Alloy',(1,0,0),24,bev=.035)
 for s in [-1,1]:
  x=s*.88
  cube(G,(x,y-2.35,pivot[2]),(.85,1.7,.82),'Weapons',.065)
  cyl(G,(x,y-4.18,pivot[2]),.30,3.2,'Weapons',(0,1,0),24,bev=.035)
  cyl(G,(x,y-5.52,pivot[2]),.20,3.0,'Alloy',(0,1,0),24,bev=.025)
  for yy in [y-3,y-3.8,y-4.7]:tube(G,(x,yy,pivot[2]),.35,.26,.16,'Weapons',n=24,bevel=.025)
  tube(G,(x,y-7.12,pivot[2]),.29,.14,.68,'Weapons',n=24,bevel=.035)
  MUZZLES.append((f'Muzzle_{len(MUZZLES)+1}',(x,y-7.47,pivot[2]),index))
 # Parent coordinates are established explicitly after meshes get real pivot origins.
 to=A.finish((0,y,z));go=G.finish(pivot)
 go.parent=to;go.location=Vector(pivot)-to.location
 MOV.extend([to,go])
 return to,go
TURRETS=[turret(1,-20.2,4.91),turret(2,12.6,6.15),turret(3,6,-6.25,True)]
# ---------------- HUMAN SCALE FITTINGS ----------------
# Spine heat exchangers, electrical cables, small sealed inspection plates.
for s in [-1,1]:
 for y in [-.5,4,17.5]:
  grille(H,(s*6.35,y,5.0),(1.8,2.7,.20))
 for y in [-9,26]:
  w=profile(min(y,33))[1];z=profile(min(y,33))[3]
  x=s*(w-.7)
  cube(H,(x,y,z-1.1),(.30,.70,.13),'Weapons',.035)
 # Cable runs sit in armoured channels along the aft decking.
 for x in [s*6.7,s*7.15]:
  beam(H,(x,18,5.01),(x,32,5.01),.07,'DarkMetal',12)
  for y in [19,23,27,31]:cube(H,(x,y,5.08),(.30,.18,.16),'Alloy',.02)
 # Stub handrails on restricted maintenance routes, 1.1 m tall.
 for x in [s*2.8]:
  for y in [20,22,24]:beam(H,(x,y,4.65),(x,y,5.65),.045,'Alloy',8)
  beam(H,(x,20,5.65),(x,24,5.65),.045,'Alloy',8)
 # Navigation lamp housings at extremities.
 cube(H,(s*15.72,24.0,3.0),(.45,.9,.46),'Weapons',.07)
 cube(H,(s*15.97,24.0,3.0),(.10,.65,.22),'Lamps',.035)
# Fastener rows have 14cm heads, scaled for a real 85m ship.
for y in [-36,-30.5,-24.5,-18.7,-13,-7.9,-2.9,23.6,28.6,33.5]:
 w=profile(y)[1]-profile(y)[4];z=profile(y)[3]+.10
 for s in [-1,1]:
  for u in [.18,.50,.85]:bolt(H,(s*w*u,y,z+.015),r=.075)
# Additional repeated belly and flank access fittings supply close-range mechanical scale.
for s in [-1,1]:
 for i in range(16):
  y=-31+i*4;pr=profile(y);z=pr[2]+1.2
  skinx=pr[1]-pr[4]*max(0,1-1.2/pr[5]);x=s*(skinx+.12)
  cube(H,(x,y,z),(.32,.70,.52),'Weapons',.035)
  for dy in [-.22,.22]:bolt(H,(x+s*.17,y+dy,z),(s,0,0),.06)
# Beacon base, rear docking collar and recessed aft hatch.
cyl(H,(0,1.2,9.80),.20,.25,'Weapons',n=16)
cyl(H,(0,1.2,9.95),.13,.12,'Lamps',n=16)
cube(H,(0,34.20,-1.6),(4.8,.32,3.4),'DarkMetal',.18)
cube(H,(0,34.41,-1.6),(4.25,.20,2.95),'Paint',.14)
for x in [-1.4,1.4]:cube(H,(x,34.58,-1.6),(.18,.16,2.8),'Alloy',.035)
# Armored stern bulkhead around the four engine roots and central docking collar.
rear=[tuple(v+Vector((0,.15,0))) for v in ring(34)]
slab(H,rear,.12,'Paint',.07)
for sign in [-1,1]:
 cube(H,(sign*3.05,34.24,1.45),(1.75,.15,3.20),'DarkMetal',.08)
 cube(H,(sign*3.05,34.33,1.45),(1.54,.12,2.95),'Paint',.065)
 for z in [.35,2.5]:bolt(H,(sign*3.05,34.42,z),(0,1,0),.055)
 beam(H,(sign*2.7,34.3,-3.5),(sign*4.1,34.3,-1.3),.10,'Weapons',10)
cube(H,(0,34.26,2.8),(3.5,.17,1.05),'DarkMetal',.07)
for x in [-1.4,-1.05,-.7,-.35,0,.35,.7,1.05,1.4]:cube(H,(x,34.38,2.8),(.065,.15,.85),'Weapons',.015)
# A ring of welded mounting shoes around each engine's front flange.
for x,y,z in ENG:
 for j in range(12):
  a=j*math.pi/6
  cube(H,(x+2.7*math.cos(a),33.2,z+2.7*math.sin(a)),(.30,.8,.30),'Alloy',.04)

hull=H.finish()
bpy.context.view_layer.update()
# Contract sockets, in exported Y-up coordinates via Blender's glTF conversion.
for i,p in enumerate(ENG,1):empty(f'Thruster_{i}',p,hull)
for name,p,i in MUZZLES:empty(name,p,TURRETS[i-1][1])
for i,p in enumerate(HARD,1):empty(f'Hardpoint_{i}',p,hull)
empty('Camera_Chase',(0,69,24),hull)
empty('Light_Nav_L',(16.05,24.0,3.0),hull)['color']=[1,0,0]
empty('Light_Nav_R',(-16.05,24.0,3.0),hull)['color']=[0,1,0]
empty('Light_Beacon',(0,1.2,10.05),hull)
# Bridge is a large warship superstructure; there is no hinged fighter canopy.
ASSET=[o for o in sc.objects if o.type in ['MESH','EMPTY']]
# ---------------- DIMENSIONAL / PIVOT CONTRACT ----------------
for to,go in TURRETS:to.parent=hull
bpy.context.view_layer.update()
pts=[o.matrix_world@v.co for o in ASSET if o.type=='MESH' for v in o.data.vertices]
lo=Vector([min(v[i] for v in pts) for i in range(3)]);hi=Vector([max(v[i] for v in pts) for i in range(3)])
factor=Vector((32/(hi.x-lo.x),85/(hi.y-lo.y),18/(hi.z-lo.z)))
for o in ASSET:
 o.location=Vector([o.location[i]*factor[i] for i in range(3)])
 if o.type=='MESH':
  for v in o.data.vertices:v.co=Vector([v.co[i]*factor[i] for i in range(3)])
  o.data.update()
bpy.context.view_layer.update()
# Centre the physical width exactly on X=0, retaining Hull's COM origin and all pivots.
world_pts=[o.matrix_world@v.co for o in ASSET if o.type=='MESH' for v in o.data.vertices]
centre_x=(min(p.x for p in world_pts)+max(p.x for p in world_pts))/2
for v in hull.data.vertices:v.co.x-=centre_x
for o in ASSET:
 if o.parent==hull:o.location.x-=centre_x
hull.data.update();bpy.context.view_layer.update()
TRIS=sum(sum(len(p.vertices)-2 for p in o.data.polygons) for o in ASSET if o.type=='MESH')
print('GEOMETRY_TRIANGLES',TRIS,'SCALE_CORRECTION',list(factor),flush=True)
if TRIS>140000:raise RuntimeError(f'Triangle budget exceeded: {TRIS}')

def y_up(v):return [round(v.x,5),round(v.z,5),round(-v.y,5)]
pts=[o.matrix_world@v.co for o in ASSET if o.type=='MESH' for v in o.data.vertices]
yp=[y_up(p) for p in pts]
low=[min(p[i] for p in yp) for i in range(3)];high=[max(p[i] for p in yp) for i in range(3)]
PAINTS=[('Naval grey',[.35,.37,.40]),('Gunmetal',[.14,.17,.20]),('Olive drab',[.24,.29,.17]),('Desert sand',[.62,.48,.30]),('Arctic white',[.80,.83,.84]),('Deep navy',[.055,.105,.18])]
manifest={'asset':'Original military patrol corvette','units':'metres','coordinate_system':{'forward':'+Z','up':'+Y','left':'+X'},'bounding_box':{'min':low,'max':high,'size':[round(b-a,5) for a,b in zip(low,high)]},'triangles':TRIS,'materials':list(MATS),'nodes':{},'axes':{},'paints':[{'name':n,'linear_rgb':c} for n,c in PAINTS],'sources':[{'id':a,'license':'CC0-1.0','url':'https://polyhaven.com/a/'+a} for a in ['metal_plate_02','rusty_metal_04']]}
for o in ASSET:
 manifest['nodes'][o.name]={'world_position':y_up(o.matrix_world.translation),'parent':o.parent.name if o.parent else None,'type':'mesh' if o.type=='MESH' else 'empty'}
 if o.name.startswith('Turret_'):manifest['axes'][o.name]=[0,1,0]
 elif o.name.startswith('Gun_'):manifest['axes'][o.name]=[1,0,0]
 if o.name.startswith('Thruster_'):manifest['nodes'][o.name]['exhaust_direction']=[0,0,-1]
 if o.name.startswith('Light_Nav'):manifest['nodes'][o.name]['color']=[1,0,0] if o.name.endswith('_L') else [0,1,0]
(ROOT/'nodes.json').write_text(json.dumps(manifest,indent=2)+'\n')
# ---------------- LIGHT / CAMERA STUDIO ----------------
world=bpy.data.worlds.new('Near-black blue space');world.use_nodes=True;sc.world=world
nt=world.node_tree;nt.nodes.clear();out=node(nt,'ShaderNodeOutputWorld');lp=node(nt,'ShaderNodeLightPath');mix=node(nt,'ShaderNodeMixShader');nt.links.new(lp.outputs['Is Camera Ray'],mix.inputs[0]);nt.links.new(mix.outputs[0],out.inputs[0])
ambient=node(nt,'ShaderNodeBackground');ambient.inputs[0].default_value=(.17,.21,.29,1);ambient.inputs[1].default_value=.30;nt.links.new(ambient.outputs[0],mix.inputs[1])
bg=node(nt,'ShaderNodeBackground');nt.links.new(bg.outputs[0],mix.inputs[2]);bg.inputs[1].default_value=1
tex=node(nt,'ShaderNodeTexCoord');sep=node(nt,'ShaderNodeSeparateXYZ');nt.links.new(tex.outputs['Normal'],sep.inputs[0]);ramp=node(nt,'ShaderNodeValToRGB');nt.links.new(sep.outputs['Z'],ramp.inputs[0]);ramp.color_ramp.elements[0].position=0;ramp.color_ramp.elements[0].color=(.002,.004,.008,1);ramp.color_ramp.elements[1].position=.85;ramp.color_ramp.elements[1].color=(.012,.022,.042,1);nt.links.new(ramp.outputs[0],bg.inputs[0])
def sun(name,pos,power,color,angle):
 light=bpy.data.lights.new(name,'SUN');light.energy=power;light.color=color;light.angle=angle
 o=bpy.data.objects.new(name,light);sc.collection.objects.link(o);o.location=pos;o.rotation_euler=(-Vector(pos)).to_track_quat('-Z','Y').to_euler();return o
sun('White daylight key',(45,-60,80),3.8,(1,.985,.965),.12)
sun('Soft cool fill',(-35,-10,25),.85,(.55,.70,1),.6)
sun('Cool aft rim',(10,65,40),1.7,(.65,.78,1),.24)
sun('Belly bounce',(15,-25,-40),.35,(.62,.71,1),.6)
light=bpy.data.lights.new('Broad daylight reflection','AREA');light.energy=45000;light.shape='DISK';light.size=35
soft=bpy.data.objects.new('Broad daylight reflection',light);sc.collection.objects.link(soft);soft.location=(22,-48,56);soft.rotation_euler=(-soft.location).to_track_quat('-Z','Y').to_euler()
cam_data=bpy.data.cameras.new('QA_camera');cam=bpy.data.objects.new('QA_camera',cam_data);sc.collection.objects.link(cam);sc.camera=cam;cam_data.type='ORTHO';cam_data.clip_end=1000
TARGET=Vector((0,0,.6))
def camera(direction,scale=94,target=TARGET):
 cam.location=target+Vector(direction).normalized()*150;cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler();cam_data.ortho_scale=scale
camera((1.32,-1.65,.88),94)
def render(name,width=1920,height=1080,samples=32):
 sc.render.resolution_x=width;sc.render.resolution_y=height;sc.render.resolution_percentage=100;sc.cycles.samples=samples;sc.render.filepath=str(ROOT/name)
 bpy.ops.render.render(write_still=True);print('RENDER_DONE',name,round(time.time()-START,1),flush=True)
# Clay gate is generated first, from the actual export meshes.
clay=bpy.data.materials.new('_QA_Clay');clay.diffuse_color=(.40,.40,.40,1);clay.use_nodes=True;clay.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value=(.40,.40,.40,1);clay.node_tree.nodes.get('Principled BSDF').inputs['Roughness'].default_value=.58
sc.view_layers[0].material_override=clay
render('clay.png',1280 if PREVIEW else 1920,720 if PREVIEW else 1080,16 if PREVIEW else 28)
sc.view_layers[0].material_override=None
# 30-degree traverse and elevation test, with barrel assembly and sockets attached.
initial={o.name:o.matrix_world.translation.copy() for o in MOV}
for to,go in TURRETS:to.rotation_euler.z=math.radians(30);go.rotation_euler.x=math.radians(30)
bpy.context.view_layer.update()
# Every traverse pivot must stay exactly on its hull bearing.
for to,go in TURRETS:assert (to.matrix_world.translation-initial[to.name]).length<1e-5
render('qa/pivots-30deg.png',1280,720,24)
for to,go in TURRETS:to.rotation_euler.z=0;go.rotation_euler.x=0
bpy.context.view_layer.update()

# Export before final beauty captures, keeping a complete end-to-end asset early.
bpy.ops.object.select_all(action='DESELECT')
for o in ASSET:o.select_set(True)
bpy.context.view_layer.objects.active=hull
# The runtime paint factor is explicit in glTF. This also avoids baking colour into the tile.
paint=MATS['Paint'];pnt=paint.node_tree;pBS=next(n for n in pnt.nodes if n.type=='BSDF_PRINCIPLED');pMix=pnt.nodes['FactionPaintFactor'];pImage=next(n for n in pnt.nodes if n.type=='TEX_IMAGE' and n.image.name=='Paint_base')
pnt.links.new(pImage.outputs['Color'],pBS.inputs['Base Color'])
GLB=ROOT/'corvette-gunship.glb'
bpy.ops.export_scene.gltf(filepath=str(GLB),export_format='GLB',use_selection=True,export_yup=True,export_apply=False,export_materials='EXPORT',export_image_format='AUTO',export_texcoords=True,export_normals=True,export_tangents=True,export_extras=True,export_cameras=False,export_lights=False)
pnt.links.new(pMix.outputs[0],pBS.inputs['Base Color'])
# Patch declarative PBR factors without changing any mesh / image / hierarchy data.
def patch_glb(path):
 data=path.read_bytes();jlen,jtype=struct.unpack_from('<II',data,12);doc=json.loads(data[20:20+jlen]);binary=data[28+jlen:]
 for m in doc.get('materials',[]):
  if m['name']=='Paint':m['pbrMetallicRoughness']['baseColorFactor']=[.35,.37,.40,1]
  if m['name']=='Glass':m['alphaMode']='BLEND';m['doubleSided']=True
 # Explicit MikkTSpace tangents survive export. Standard glTF normalized storage
 # keeps them compact: signed 16-bit normals, signed 8-bit tangent directions.
 packed={};source_views=[dict(v) for v in doc['bufferViews']];source_accessors=[dict(a) for a in doc['accessors']]
 for mesh in doc['meshes']:
  for primitive in mesh['primitives']:
   for semantic in ['NORMAL','TANGENT']:
    ai=primitive['attributes'].get(semantic)
    if ai is None:continue
    a=doc['accessors'][ai];sa=source_accessors[ai];vi=a['bufferView'];v=doc['bufferViews'][vi];sv=source_views[vi]
    if vi in packed:continue
    n=3 if semantic=='NORMAL' else 4;stride=sv.get('byteStride',n*4)
    arr=np.ndarray((a['count'],n),dtype='<f4',buffer=binary,offset=sv.get('byteOffset',0)+sa.get('byteOffset',0),strides=(stride,4)).copy()
    if semantic=='TANGENT':
     # MikkTSpace returns zero on a few sub-pixel bevel UVs. Supply a stable,
     # orthogonal basis there; retain its authored tangent on every other vertex.
     na=source_accessors[primitive['attributes']['NORMAL']];nv=source_views[na['bufferView']]
     normals=np.ndarray((na['count'],3),dtype='<f4',buffer=binary,offset=nv.get('byteOffset',0)+na.get('byteOffset',0),strides=(nv.get('byteStride',12),4)).copy()
     bad=np.linalg.norm(arr[:,:3],axis=1)<1e-6
     if np.any(bad):
      nn=normals[bad];ref=np.zeros_like(nn);ref[:,0]=1;parallel=np.abs(nn[:,0])>.9;ref[parallel]=[0,1,0]
      fallback=ref-nn*np.sum(ref*nn,axis=1)[:,None];fallback/=np.maximum(np.linalg.norm(fallback,axis=1)[:,None],1e-12);arr[bad,:3]=fallback
     arr[:,3]=np.where(arr[:,3]<0,-1,1)
    arr[:,:3]/=np.maximum(np.linalg.norm(arr[:,:3],axis=1)[:,None],1e-12)
    if semantic=='NORMAL':
     q=np.zeros((len(arr),4),dtype='<i2');q[:,:3]=np.round(np.clip(arr,-1,1)*32767).astype('<i2');a['componentType']=5122;v['byteStride']=8
    else:
     q=np.round(np.clip(arr,-1,1)*127).astype('i1');a['componentType']=5120;v['byteStride']=4
    a['normalized']=True;a['byteOffset']=0;a.pop('min',None);a.pop('max',None);packed[vi]=q.tobytes()
 newbin=bytearray()
 for vi,v in enumerate(doc['bufferViews']):
  payload=packed.get(vi)
  if payload is None:payload=binary[v.get('byteOffset',0):v.get('byteOffset',0)+v['byteLength']]
  newbin.extend(b'\0'*((-len(newbin))%4));v['byteOffset']=len(newbin);v['byteLength']=len(payload);newbin.extend(payload)
 newbin.extend(b'\0'*((-len(newbin))%4));doc['buffers'][0]['byteLength']=len(newbin)
 for key in ['extensionsUsed','extensionsRequired']:
  if 'KHR_mesh_quantization' not in doc.setdefault(key,[]):doc[key].append('KHR_mesh_quantization')
 j=json.dumps(doc,separators=(',',':')).encode();j+=b' '*((-len(j))%4)
 path.write_bytes(struct.pack('<III',0x46546C67,2,28+len(j)+len(newbin))+struct.pack('<II',len(j),0x4E4F534A)+j+struct.pack('<II',len(newbin),0x004E4942)+newbin)
 return doc
doc=patch_glb(GLB)
(QA/'corvette-uncompressed.glb').write_bytes(GLB.read_bytes())
if GLB.stat().st_size>12000000:raise RuntimeError(f'GLB size exceeds 12 MB: {GLB.stat().st_size}')
print('EXPORT_OK',GLB.stat().st_size,'bytes',flush=True)
# Archive the matching source scene for audit / recovery, not included in the GLB.
bpy.ops.wm.save_as_mainfile(filepath=str(QA/'corvette-source.blend'))
render('final.png',1280 if PREVIEW else 1920,720 if PREVIEW else 1080,24 if PREVIEW else 48)
camera((1,0,.0),94,Vector((0,0,0)))
render('side.png',1920,700,24)
camera((1.15,1.65,.70),94)
render('qa/rear.png',1280,720,24)
camera((1.32,-1.65,.88),94)
# Six identical camera / lighting captures. No compositing with external artwork.
paint_frames=[]
for i,(name,color) in enumerate(PAINTS):
 pMix.inputs[2].default_value=tuple(color)+(1,)
 render(f'qa/paint-{i}.png',640,360,16)
 im=bpy.data.images.load(str(QA/f'paint-{i}.png'),check_existing=False)
 arr=np.array(im.pixels[:],dtype=np.float32).reshape(360,640,4);paint_frames.append(arr);bpy.data.images.remove(im)
pMix.inputs[2].default_value=(.35,.37,.40,1)
# Pixel buffers are bottom-up; first palette row goes at the top.
grid=np.concatenate([np.concatenate(paint_frames[3:],axis=1),np.concatenate(paint_frames[:3],axis=1)],axis=0)
im=bpy.data.images.new('Paint comparison',1920,720,alpha=True);im.pixels.foreach_set(grid.ravel());im.filepath_raw=str(ROOT/'paints.png');im.file_format='PNG';im.save()
# A true close inspection, same model and finish at a nearer camera framing.
camera((1.35,-1.1,.65),36,Vector((5,-8,3)))
render('qa/close.png',1280,960,32)
(ROOT/'NOTES.md').write_text('\n'.join([
'Original 85 × 32 × 18 m military corvette: layered arrowhead hull, slit bridge, two dorsal twin turrets, one ventral twin turret, ten missile cells and four engines; metres, +Z forward / +Y up.',
f'{TRIS:,} triangles; 7 mesh nodes; 3 traverse + 3 elevation pivots verified at 30 degrees; GLB {GLB.stat().st_size/1048576:.2f} MiB.',
'8 materials: Paint, Alloy, DarkMetal, Scorched, Weapons, Glass (BLEND), Engines and Lamps; Paint neutral texture × naval-grey factor [0.35, 0.37, 0.40].',
'Cycles OptiX baked shared tiling base/normal 1024² and ORM 512²; uniform glass/emissive finishes 64²; hull tile scale 4 m; six linear-RGB paint choices in nodes.json.',
'Poly Haven CC0 ids: metal_plate_02 (https://polyhaven.com/a/metal_plate_02), rusty_metal_04 (https://polyhaven.com/a/rusty_metal_04); licence: https://polyhaven.com/license .',
'Reference differences: original segmented engine petals, one dorsal turret forward of bridge, sculpted individual engine cowls and restrained roof sensors; no text, insignia, flags or deployed landing gear.'
])+'\n')
print('BUILD_COMPLETE',round(time.time()-START,1),flush=True)
