"""Clean, deterministic enduro asset build. Blender 5.2 / Cycles GPU.
Authoring coordinates: x rider-left, f forward, z up; mesh coordinates (x,-f,z).
Run: blender -b --python-exit-code 1 --python build_dirt-bike.py
Only external imagery: CC0 Poly Haven metal_plate, rubberized_track, leather_white.
"""
import bpy, bmesh, math, os, json, urllib.request, random, struct, sys
import numpy as np
from mathutils import Vector, Matrix
from math import sin, cos, pi
from pathlib import Path
ROOT=Path(__file__).resolve().parent
os.chdir(ROOT)
random.seed(19)
START=__import__('time').time()
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)
sc=bpy.context.scene
sc.unit_settings.system='METRIC'; sc.unit_settings.scale_length=1
sc.render.engine='CYCLES'; sc.cycles.samples=24
prefs=bpy.context.preferences.addons['cycles'].preferences
prefs.compute_device_type='OPTIX'; prefs.get_devices()
for d in prefs.devices: d.use=(d.type=='OPTIX')
sc.cycles.device='GPU'; sc.cycles.use_denoising=True
sc.render.image_settings.file_format='PNG'
sc.view_settings.view_transform='Khronos PBR Neutral'
sc.view_settings.look='None'
sc.view_settings.exposure=-.45
sc.world.color=(.3,.3,.3)
print('SOURCE_VERSION enduro_final4e',flush=True)
print('GPU',[(d.name,d.type,d.use) for d in prefs.devices],flush=True)
TEX=ROOT/'textures'; TEX.mkdir(exist_ok=True)
PH=['metal_plate','rubberized_track','leather_white']
for ident in PH:
 for typ in ['diff','nor_gl','arm']:
  p=TEX/f'{ident}_{typ}_1k.jpg'
  if not p.exists():
   url=f'https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/{ident}/{ident}_{typ}_1k.jpg'
   urllib.request.urlretrieve(url,p)
# Seven portable material slots. Tile bakes are shared by all appropriate parts.
MATS=[]
specs=[('Paint','metal_plate',(.80,.80,.80),(1.,1.,1.),.0,.43),('Rubber','rubberized_track',(.006,.007,.008),(.055,.049,.040),.0,.78),('Alloy','metal_plate',(.23,.25,.27),(.43,.46,.48),.86,.34),('Steel','metal_plate',(.32,.35,.38),(.62,.65,.68),.95,.29),('Engine','metal_plate',(.023,.027,.032),(.090,.100,.112),.50,.55),('Saddle','leather_white',(.009,.011,.014),(.027,.031,.037),.0,.63),('Lamps','metal_plate',(.8,.8,.8),(1,1,1),.0,.2)]
# Material tile bake fixture exercises the exact GPU, baking, and export path.
bpy.ops.mesh.primitive_plane_add(size=1)
fixture=bpy.context.object; fixture.name='BakeFixture'
for mi,(name,ident,c1,c2,metal,rough) in enumerate(specs):
 mat=bpy.data.materials.new(name); mat.use_nodes=True; MATS.append(mat)
 nt=mat.node_tree; nt.nodes.clear(); nd=nt.nodes; lk=nt.links
 out=nd.new('ShaderNodeOutputMaterial'); bs=nd.new('ShaderNodeBsdfPrincipled'); lk.new(bs.outputs['BSDF'],out.inputs['Surface'])
 uv=nd.new('ShaderNodeTexCoord')
 photo=nd.new('ShaderNodeTexImage'); photo.image=bpy.data.images.load(str(TEX/f'{ident}_diff_1k.jpg'),check_existing=True); lk.new(uv.outputs['UV'],photo.inputs['Vector'])
 ramp=nd.new('ShaderNodeValToRGB'); ramp.color_ramp.elements[0].position=.12; ramp.color_ramp.elements[0].color=(*c1,1); ramp.color_ramp.elements[1].position=.8; ramp.color_ramp.elements[1].color=(*c2,1); lk.new(photo.outputs['Color'],ramp.inputs[0])
 noise=nd.new('ShaderNodeTexNoise'); noise.inputs['Scale'].default_value=110; noise.inputs['Detail'].default_value=3; lk.new(uv.outputs['UV'],noise.inputs['Vector'])
 mix=nd.new('ShaderNodeMixRGB'); mix.blend_type='MULTIPLY'; mix.inputs[0].default_value=.04 if name=='Paint' else .18; lk.new(ramp.outputs['Color'],mix.inputs[1]); lk.new(noise.outputs['Fac'],mix.inputs[2]); col=mix.outputs[0]
 # Directional micro-abrasion and broader embedded dust are baked, not export-only nodes.
 stretch=nd.new('ShaderNodeVectorMath');stretch.operation='MULTIPLY';stretch.inputs[1].default_value=(24,1.3,1);lk.new(uv.outputs['UV'],stretch.inputs[0])
 scratches=nd.new('ShaderNodeTexNoise');scratches.inputs['Scale'].default_value=9;scratches.inputs['Detail'].default_value=2;lk.new(stretch.outputs[0],scratches.inputs[0])
 mask=nd.new('ShaderNodeValToRGB');mask.color_ramp.elements[0].position=.64;mask.color_ramp.elements[0].color=(0,0,0,1);mask.color_ramp.elements[1].position=.73;mask.color_ramp.elements[1].color=(.25,.25,.25,1);lk.new(scratches.outputs['Fac'],mask.inputs[0])
 wear=nd.new('ShaderNodeMixRGB');lk.new(mask.outputs['Color'],wear.inputs[0]);lk.new(col,wear.inputs[1]);wear.inputs[2].default_value=(.46,.46,.46,1) if name=='Paint' else ((*c2,1));col=wear.outputs[0]
 normalimg=nd.new('ShaderNodeTexImage'); normalimg.image=bpy.data.images.load(str(TEX/f'{ident}_nor_gl_1k.jpg'),check_existing=True); normalimg.image.colorspace_settings.name='Non-Color'; lk.new(uv.outputs['UV'],normalimg.inputs['Vector'])
 norm=nd.new('ShaderNodeNormalMap'); norm.inputs['Strength'].default_value=.08 if name in ['Paint','Steel','Alloy','Engine'] else .22; lk.new(normalimg.outputs['Color'],norm.inputs['Color']); lk.new(norm.outputs[0],bs.inputs['Normal'])
 ar=nd.new('ShaderNodeTexImage'); ar.image=bpy.data.images.load(str(TEX/f'{ident}_arm_1k.jpg'),check_existing=True); ar.image.colorspace_settings.name='Non-Color'; lk.new(uv.outputs['UV'],ar.inputs[0]); sep=nd.new('ShaderNodeSeparateColor'); lk.new(ar.outputs[0],sep.inputs[0])
 rmath=nd.new('ShaderNodeMath'); rmath.operation='MULTIPLY_ADD'; rmath.inputs[1].default_value=.16; rmath.inputs[2].default_value=rough-.08; lk.new(sep.outputs[1],rmath.inputs[0])
 # Lamps are UV-separated warm white, red, amber, all in this one baked material.
 if name=='Lamps':
  suv=nd.new('ShaderNodeSeparateXYZ'); lk.new(uv.outputs['UV'],suv.inputs[0]); cr=nd.new('ShaderNodeValToRGB'); cr.color_ramp.interpolation='CONSTANT'; cr.color_ramp.elements.remove(cr.color_ramp.elements[1]); cr.color_ramp.elements[0].color=(1,.82,.58,1); cr.color_ramp.elements.new(.333).color=(.8,.006,.002,1); cr.color_ramp.elements.new(.666).color=(1,.22,.003,1); lk.new(suv.outputs['X'],cr.inputs[0]); col=cr.outputs[0]
 lk.new(col,bs.inputs['Base Color']); bs.inputs['Metallic'].default_value=metal; lk.new(rmath.outputs[0],bs.inputs['Roughness'])
 fixture.data.materials.clear(); fixture.data.materials.append(mat)
 bpy.context.view_layer.objects.active=fixture; fixture.select_set(True)
 sc.cycles.samples=8; sc.render.bake.margin=8; sc.render.bake.use_clear=True
 emission=nd.new('ShaderNodeEmission'); target=nd.new('ShaderNodeTexImage')
 def bake(name2,size,kind,socket=None):
  im=bpy.data.images.new(f'{name}_{name2}',width=size,height=size,alpha=False)
  if name2!='Base': im.colorspace_settings.name='Non-Color'
  target.image=im; nd.active=target
  if socket:
   lk.new(socket,emission.inputs['Color']); lk.new(emission.outputs[0],out.inputs[0])
  else: lk.new(bs.outputs[0],out.inputs[0])
  bpy.ops.object.bake(type=kind)
  im.filepath_raw=str(TEX/f'{name}_{name2}.png'); im.file_format='PNG'; im.save()
  return im
 base=bake('Base',1024 if name in ['Paint','Rubber','Saddle'] else 512,'EMIT',col)
 normal=bake('Normal',512,'NORMAL')
 comb=nd.new('ShaderNodeCombineColor'); comb.inputs[0].default_value=1; lk.new(sep.outputs[0],comb.inputs[0]); lk.new(rmath.outputs[0],comb.inputs[1]); comb.inputs[2].default_value=metal
 orm=bake('ORM',512,'EMIT',comb.outputs[0])
 nd.clear(); out=nd.new('ShaderNodeOutputMaterial'); bs=nd.new('ShaderNodeBsdfPrincipled'); lk.new(bs.outputs[0],out.inputs[0])
 tex=nd.new('ShaderNodeTexImage'); tex.image=base
 if name=='Paint':
  mul=nd.new('ShaderNodeMixRGB'); mul.name='PAINT_FACTOR'; mul.blend_type='MULTIPLY'; mul.inputs[0].default_value=1; mul.inputs[2].default_value=(.85,.25,.01,1); lk.new(tex.outputs[0],mul.inputs[1]); lk.new(mul.outputs[0],bs.inputs['Base Color']); bs.inputs['Coat Weight'].default_value=0; bs.inputs['Coat Roughness'].default_value=.05; bs.inputs['Specular IOR Level'].default_value=.25
 else: lk.new(tex.outputs[0],bs.inputs['Base Color'])
 ntex=nd.new('ShaderNodeTexImage'); ntex.image=normal; nm=nd.new('ShaderNodeNormalMap'); lk.new(ntex.outputs[0],nm.inputs['Color']); lk.new(nm.outputs[0],bs.inputs['Normal'])
 otex=nd.new('ShaderNodeTexImage'); otex.image=orm; separate=nd.new('ShaderNodeSeparateColor'); lk.new(otex.outputs[0],separate.inputs[0]); lk.new(separate.outputs[1],bs.inputs['Roughness']); lk.new(separate.outputs[2],bs.inputs['Metallic'])
 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')
 gn=nd.new('ShaderNodeGroup'); gn.node_tree=group; lk.new(separate.outputs[0],gn.inputs['Occlusion'])
 if name=='Lamps': lk.new(tex.outputs[0],bs.inputs['Emission Color']); bs.inputs['Emission Strength'].default_value=1.5
 print('BAKED',name,flush=True)
 if mi==0:
  bpy.ops.export_scene.gltf(filepath=str(ROOT/'preflight.glb'),export_format='GLB',use_selection=True)
bpy.data.objects.remove(fixture,do_unlink=True)
# Fast batched constructive geometry. Each element has real wall thickness.
class Batch:
 def __init__(self,name): self.name=name; self.v=[]; self.f=[]; self.m=[]; self.s=[]; self.uv=[]
 def add(self,verts,faces,mat,smooth=False,uvmode=None):
  k=len(self.v); self.v.extend([(p[0],-p[1],p[2]) for p in verts]); self.f.extend([tuple(k+i for i in f) for f in faces]); self.m.extend([mat]*len(faces)); self.s.extend(smooth if isinstance(smooth,list) else [smooth]*len(faces)); self.uv.extend([uvmode]*len(faces))
 def make(self):
  me=bpy.data.meshes.new(self.name); me.from_pydata(self.v,[],self.f); me.materials.clear()
  for m in MATS: me.materials.append(m)
  ob=bpy.data.objects.new(self.name,me); sc.collection.objects.link(ob)
  uv=me.uv_layers.new(name='UVMap')
  for p,mi,sm,um in zip(me.polygons,self.m,self.s,self.uv):
   p.material_index=mi;p.use_smooth=sm
   n=p.normal; axis=max(range(3),key=lambda i:abs(n[i])); ij=[i for i in range(3) if i!=axis]
   for li in p.loop_indices:
    co=me.vertices[me.loops[li].vertex_index].co
    uv.data[li].uv=(um,.5) if um is not None else (co[ij[0]]*3.4,co[ij[1]]*3.4)
  return ob
B={n:Batch(n) for n in ['Frame','Fork','Wheel_F','Wheel_R','Kickstand']}
# Bevelled box templates generated with bmesh (shared vertices, real bevels).
BOX={}
def box(g,c,d,mat=2,bev=.003,basis=None):
 key=tuple(round(a,6) for a in (*d,bev))
 if key not in BOX:
  bm=bmesh.new(); bmesh.ops.create_cube(bm,size=1)
  for v in bm.verts:
   for i in range(3):v.co[i]*=d[i]
  if bev: bmesh.ops.bevel(bm,geom=list(bm.edges),offset=min(bev,min(d)*.23),segments=1,affect='EDGES')
  bm.verts.ensure_lookup_table();bm.verts.index_update(); BOX[key]=([tuple(v.co) for v in bm.verts],[tuple(v.index for v in f.verts) for f in bm.faces]);bm.free()
 vs,fs=BOX[key]; cc=Vector(c)
 if basis: vs=[tuple(cc+sum((Vector(basis[i])*v[i] for i in range(3)),Vector())) for v in vs]
 else: vs=[tuple(cc+Vector(v)) for v in vs]
 g.add(vs,fs,mat)
def tube(g,a,b,r,mat=2,n=16,r2=None):
 a=Vector(a);b=Vector(b); axis=(b-a).normalized();u=axis.cross(Vector((0,0,1)))
 if u.length<.01:u=axis.cross(Vector((0,1,0)))
 u.normalize();v=axis.cross(u);r2=r if r2 is None else r2
 vs=[tuple(c+rr*(u*cos(2*pi*i/n)+v*sin(2*pi*i/n))) for c,rr in [(a,r),(b,r2)] for i in range(n)]
 fs=[(i,(i+1)%n,(i+1)%n+n,i+n) for i in range(n)]+[tuple(range(n-1,-1,-1)),tuple(range(n,2*n))]
 g.add(vs,fs,mat,[True]*n+[False,False])
def pipe(g,points,r,mat=2,n=10,steps=4):
 pts=[Vector(p) for p in points]; path=[]
 for i in range(len(pts)-1):
  p0=pts[max(0,i-1)];p1=pts[i];p2=pts[i+1];p3=pts[min(len(pts)-1,i+2)]
  for j in range(steps):
   t=j/steps;path.append(.5*((2*p1)+(-p0+p2)*t+(2*p0-5*p1+4*p2-p3)*t*t+(-p0+3*p1-3*p2+p3)*t*t*t))
 path.append(pts[-1]); vs=[]
 for i,p in enumerate(path):
  tangent=(path[min(i+1,len(path)-1)]-path[max(0,i-1)]).normalized(); u=tangent.cross(Vector((1,0,0)))
  if u.length<.01:u=tangent.cross(Vector((0,0,1)))
  u.normalize();v=tangent.cross(u)
  vs.extend([tuple(p+r*(u*cos(2*pi*j/n)+v*sin(2*pi*j/n))) for j in range(n)])
 fs=[(i*n+j,i*n+(j+1)%n,(i+1)*n+(j+1)%n,(i+1)*n+j) for i in range(len(path)-1) for j in range(n)]
 fs += [tuple(range(n-1,-1,-1)),tuple((len(path)-1)*n+j for j in range(n))]
 g.add(vs,fs,mat,True)
def ring(g,c,ro,ri,width,mat=2,n=64,wave=0):
 x,f,z=c;vs=[]
 for xx in [x-width/2,x+width/2]:
  for rr in [ri,ro]:
   vs.extend([(xx,f+(rr+(wave*sin(i*2*pi/n*9) if rr==ro else 0))*sin(i*2*pi/n),z+(rr+(wave*sin(i*2*pi/n*9) if rr==ro else 0))*cos(i*2*pi/n)) for i in range(n)])
 fs=[]
 for i in range(n):
  j=(i+1)%n
  fs.extend([(i,j,n+j,n+i),(2*n+i,3*n+i,3*n+j,2*n+j),(i,2*n+i,2*n+j,j),(n+i,n+j,3*n+j,3*n+i)])
 g.add(vs,fs,mat,True)
def rotor(g,c,ro,ri,mat=3,N=24):
 x,f,z=c;rm=(ro+ri)/2;vs=[];fs=[]
 for sector in range(N):
  a0=2*pi*sector/N;a1=2*pi*(sector+1)/N;am=(a0+a1)/2
  outer=[(ri,a0),(ri,am),(ri,a1),(rm,a1),(ro,a1),(ro,am),(ro,a0),(rm,a0)]
  contour=[(f+r*sin(a),z+r*cos(a)) for r,a in outer]
  hole=[]
  for deg in [-135,-90,-45,0,45,90,135,180]:
   ph=deg*pi/180;t=.007*cos(ph);r=.0035*sin(ph)
   hole.append((f+(rm+r)*sin(am)+t*cos(am),z+(rm+r)*cos(am)-t*sin(am)))
  k=len(vs)
  for xx in [x-.0015,x+.0015]:vs.extend([(xx,ff,zz) for ff,zz in contour+hole])
  for j in range(8):
   h=(j+1)%8
   fs.extend([(k+j,k+h,k+8+h,k+8+j),(k+16+j,k+24+j,k+24+h,k+16+h),(k+8+j,k+8+h,k+24+h,k+24+j)])
  for j in [0,1,4,5]:
   h=(j+1)%8;fs.append((k+j,k+16+j,k+16+h,k+h))
 g.add(vs,fs,mat,False)
def bolt(g,c,axis=(1,0,0),r=.007,mat=3):
 start=len(g.f);c=Vector(c);a=Vector(axis); tube(g,c-a*.001,c+a*.003,r,mat,6);tube(g,c+a*.003,c+a*.0036,r*.40,4,6);g.s[start:]=[False]*(len(g.f)-start)
def panel(g,outline,mat=0,thick=.008,bevel=.003,crown=0):
 # outline is a gently faceted, contoured skin; solidify then bevel the shell.
 vs=[(x,-f,z) for x,f,z in outline];faces=[tuple(range(len(vs)))]
 if crown:
  c=sum((Vector(v) for v in vs),Vector())/len(vs);c.x+=crown*(1 if c.x>0 else -1);nn=len(vs);vs.append(tuple(c));faces=[(i,(i+1)%nn,nn) for i in range(nn)]
 me=bpy.data.meshes.new('paneltmp');me.from_pydata(vs,[],faces); ob=bpy.data.objects.new('paneltmp',me);sc.collection.objects.link(ob)
 bpy.context.view_layer.objects.active=ob;ob.select_set(True)
 sol=ob.modifiers.new('wall','SOLIDIFY');sol.thickness=thick
 be=ob.modifiers.new('moulded edges','BEVEL');be.width=bevel;be.segments=3
 deps=bpy.context.evaluated_depsgraph_get();ev=ob.evaluated_get(deps);mm=ev.to_mesh();g.add([(v.co.x,-v.co.y,v.co.z) for v in mm.vertices],[tuple(p.vertices) for p in mm.polygons],mat,False);ev.to_mesh_clear();bpy.data.objects.remove(ob,do_unlink=True)
def shaped_skin(g,upper,lower,s,mat=0):
 def spline(points,t):
  tt=t*(len(points)-1);i=min(int(tt),len(points)-2);u=tt-i
  a=Vector(points[max(0,i-1)]);b=Vector(points[i]);c=Vector(points[i+1]);d=Vector(points[min(len(points)-1,i+2)])
  return .5*(2*b+(-a+c)*u+(2*a-5*b+4*c-d)*u*u+(-a+3*b-3*c+d)*u*u*u)
 vs=[];N=16;M=6
 for back in [False,True]:
  for i in range(N+1):
   t=i/N;a=spline(upper,t);b=spline(lower,t)
   for j in range(M+1):
    v=j/M;p=a.lerp(b,v);p.x+=.012*sin(pi*v)*sin(pi*(.12+.76*t));p.x-=.008 if back else 0;vs.append((s*p.x,p.y,p.z))
 k=(N+1)*(M+1);fs=[]
 for layer in [0,k]:
  for i in range(N):
   for j in range(M):
    a=layer+i*(M+1)+j;fs.append((a,a+1,a+M+2,a+M+1))
 for i in range(N):
  for j in [0,M]:
   a=i*(M+1)+j;b=a+M+1;fs.append((a,b,b+k,a+k))
 for j in range(M):
  for i in [0,N]:
   a=i*(M+1)+j;fs.append((a,a+k,a+k+1,a+1))
 g.add(vs,fs,mat,True)
def loft(g,sections,mat=0):
 # Rounded section loft with Catmull-Rom longitudinal curvature.
 samples=[]
 for i in range(len(sections)-1):
  p0=np.array(sections[max(0,i-1)]);p1=np.array(sections[i]);p2=np.array(sections[i+1]);p3=np.array(sections[min(len(sections)-1,i+2)])
  for j in range(4):
   t=j/4;samples.append(.5*(2*p1+(-p0+p2)*t+(2*p0-5*p1+4*p2-p3)*t*t+(-p0+3*p1-3*p2+p3)*t*t*t))
 samples.append(sections[-1]);sections=samples
 vs=[]
 for f,z,w,t in sections:
  vs.extend([(x,f,z+dz) for x,dz in [(-w*.86,-t/2),(-w,-t*.1),(-w*.90,t*.35),(-w*.58,t*.5),(w*.58,t*.5),(w*.90,t*.35),(w,t*-.1),(w*.86,-t/2)]])
 fs=[tuple(range(7,-1,-1)),tuple((len(sections)-1)*8+j for j in range(8))]
 fs.extend([(i*8+j,i*8+(j+1)%8,(i+1)*8+(j+1)%8,(i+1)*8+j) for i in range(len(sections)-1) for j in range(8)])
 g.add(vs,fs,mat,True)
F=B['Frame'];K=B['Fork']
# Wheels: correct bead diameters, individual tread blocks, alloy rims and crossed spokes.
for front,f,z,rad,w,rim in [(True,.74,.35,.338,.083,.2667),(False,-.74,.320,.307,.115,.2286)]:
 g=B['Wheel_F' if front else 'Wheel_R'];N=96;vs=[]
 # Curved shoulder and sidewall profile. Knobs determine the final rolling radius.
 prof=[(-w*.33,rim-.002),(-w*.61,rad-.051),(-w*.55,rad-.024),(-w*.36,rad-.008),(0,rad),(w*.36,rad-.008),(w*.55,rad-.024),(w*.61,rad-.051),(w*.33,rim-.002)]
 for x,r in prof:
  vs.extend([(x,f+r*sin(i*2*pi/N),z+r*cos(i*2*pi/N)) for i in range(N)])
 fs=[(j*N+i,j*N+(i+1)%N,(j+1)*N+(i+1)%N,(j+1)*N+i) for j in range(len(prof)-1) for i in range(N)]
 g.add(vs,fs,1,True)
 for lane in [-2,-1,0,1,2]:
  count=52 if front else 48
  for i in range(count):
   a=(i+(lane%2)*.5)*2*pi/count; side=lane*.215*w;rr=rad-(.0015 if front else .0005)-abs(lane)*.008
   radial=Vector((lane*.17,sin(a),cos(a))).normalized();tangent=Vector((0,cos(a),-sin(a)));lateral=tangent.cross(radial)
   c=(side,f+rr*sin(a),z+rr*cos(a))
   box(g,c,(w*.23,.027 if front else .033,.027),1,.002,basis=[lateral,tangent,radial])
 # Fine moulded sidewall ribs and beads.
 for s in [-1,1]:
  ring(g,(s*w*.50,f,z),rim+.017,rim+.011,.003,1,96)
  ring(g,(s*w*.34,f,z),rim+.002,rim-.011,.009,4,96)
 ring(g,(0,f,z),rim-.001,rim-.021,w*.63,2,96)
 tube(g,(-.060,f,z),(.060,f,z),.035,2,32)
 for s in [-1,1]:
  ring(g,(s*.045,f,z),.045,.023,.006,2,40)
  bolt(g,(s*.082,f,z),axis=(s,0,0),r=.013)
  for i in range(18):
   a=2*pi*i/18+(0 if s==1 else pi/18)
   for shift in [(.6981317 if i%2 else -.6981317)]:
    b=a+shift
    tube(g,(s*.039,f+.038*sin(a),z+.038*cos(a)),(s*w*.25,f+(rim-.018)*sin(b),z+(rim-.018)*cos(b)),.00165,3,6)
    tube(g,(s*w*.25,f+(rim-.030)*sin(b),z+(rim-.030)*cos(b)),(s*w*.25,f+(rim-.018)*sin(b),z+(rim-.018)*cos(b)),.003,2,6)
 # Brake rotors are open annular lattices, not solid circles with painted holes.
 dx=.062 if front else -.072;rr=.127 if front else .112
 rotor(g,(dx,f,z),rr,rr-.020)
 ring(g,(dx,f,z),.060,.041,.003,4,48)
 for i in range(8):
  a=2*pi*i/8
  tube(g,(dx,f+.052*sin(a),z+.052*cos(a)),(dx,f+(rr-.009)*sin(a+.24),z+(rr-.009)*cos(a+.24)),.006,2,6)
  bolt(g,(dx+.003,f+.050*sin(a),z+.050*cos(a)),r=.004)
 # valve stem
 tube(g,(0,f,z-rim+.01),(.002,f,z-rim+.029),.003,1,8)
# Main frame cradles; welded steel members with visible engine clearances.
for s in [-1,1]:
 pipe(F,[(s*.060,.36,1.015),(s*.105,.17,.80),(s*.125,-.18,.55),(s*.105,-.24,.34),(s*.10,.13,.30),(s*.09,.28,.37),(s*.060,.38,.84),(s*.060,.36,1.015)],.017,4,12,4)
 pipe(F,[(s*.065,.34,.96),(s*.080,-.22,.77),(s*.075,-.61,.89),(s*.050,-.93,.945)],.014,4,12,3)
 pipe(F,[(s*.085,-.22,.51),(s*.075,-.46,.70),(s*.055,-.85,.94)],.013,4,10,3)
 for f,z in [(.27,.84),(-.21,.54),(-.18,.75)]:
  ring(F,(s*.115,f,z),.022,.014,.018,4,24)
  bolt(F,(s*.132,f,z),axis=(s,0,0),r=.009)
# Steering head lies on the raked axis, topped by cast triple clamps.
tube(F,(0,.405,.90),(0,.293,1.09),.040,4,32)
for h in [.91,1.075]:
 ff=.405-(h-.90)*.589
 box(K,(0,ff,h),(.24,.071,.034),2,.012)
 for s in [-1,1]:bolt(K,(s*.117,ff,h),axis=(s,0,0),r=.007)
# Front suspension: inner polished stanchions, anodized outer sliders, seals, protectors.
for s in [-1,1]:
 a=Vector((s*.092,.73,.35));b=Vector((s*.092,.292,1.095));delta=b-a
 tube(K,a,a+delta*.67,.020,3,32)
 tube(K,a+delta*.56,b,.029,2,32)
 for t in [.56,.60,.94,.99]:
  p=a+delta*t;tube(K,p-delta.normalized()*.010,p+delta.normalized()*.010,.031,4 if t<.7 else 2,24)
 tube(K,(s*.092,.73,.35),(s*.092,.745,.41),.026,2,24)
 # Injection-moulded half-shell, tapering around the lower slider.
 axis=delta.normalized();u=Vector((1,0,0));v=axis.cross(u);vs=[];N=12
 for t,radius in [(.018,.034),(.09,.038),(.34,.034),(.43,.028)]:
  c=a+delta*t
  for rr in [radius,radius-.004]:
   vs.extend([tuple(c+rr*(u*cos(j*pi/N)+v*sin(j*pi/N))) for j in range(N+1)])
 fs=[];stride=2*(N+1)
 for row in range(3):
  for layer in range(2):
   for j in range(N):
    k=row*stride+layer*(N+1)+j;fs.append((k,k+1,k+1+stride,k+stride))
  for j in [0,N]:
   k=row*stride+j;fs.append((k,k+N+1,k+N+1+stride,k+stride))
 for row in [0,3]:
  for j in range(N):
   k=row*stride+j;fs.append((k,k+1,k+N+2,k+N+1))
 K.add(vs,fs,0,True)
 bolt(K,(s*.12,.739,.366),axis=(s,0,0),r=.009)
tube(K,(-.13,.74,.35),(.13,.74,.35),.011,3,24)
box(K,(.078,.66,.409),(.032,.043,.066),4,.008)
for z in [.39,.425]: bolt(K,(.098,.662,z),r=.005)
# Brake hose rises along the left fork; retainers and a natural loop above the plate.
pipe(K,[(.105,.65,.41),(.119,.51,.72),(.125,.39,.98),(.115,.27,1.18),(.23,.255,1.22)],.0035,1,8,6)
for f,z in [(.53,.69),(.39,.96)]:tube(K,(.09,f,z),(.125,f,z),.006,4,10)
# Shaped elevated front fender, longitudinal crown and slightly flared nose.
loft(K,[(.25,.833,.044,.013),(.37,.866,.073,.023),(.53,.879,.080,.025),(.73,.865,.084,.025),(.89,.827,.079,.019),(.956,.791,.050,.011),(.972,.780,.025,.008)],0)
# Blank, faceted front number plate and its black rear housing.
panel(K,[(-.105,.35,1.15),(.105,.35,1.15),(.116,.426,1.061),(.078,.49,.955),(0,.508,.926),(-.078,.49,.955),(-.116,.426,1.061)],0,.018,.008)
for s in [-1,1]:bolt(K,(s*.081,.365,1.121),axis=(0,1,.2),r=.005)
for ss in [-1,1]:
 pipe(K,[(ss*.081,.356,1.126),(ss*.079,.319,1.108),(ss*.092,.302,1.082)],.007,5,10,3)
 pipe(K,[(ss*.070,.496,.959),(ss*.085,.437,.94),(ss*.092,.403,.915)],.006,5,10,3)
# A small original enduro headlamp recessed at the upper edge of the blank plate.
box(K,(0,.400,1.126),(.098,.016,.026),4,.005)
box(K,(0,.410,1.127),(.078,.005,.015),6,.003)
# handlebar risers, bar with bent sweep, ribbed grips and levers.
for s in [-1,1]:
 tube(K,(s*.044,.295,1.086),(s*.044,.26,1.181),.013,4,16)
 box(K,(s*.044,.265,1.177),(.035,.035,.018),2,.004)
 bolt(K,(s*.044,.265,1.188),axis=(0,0,1),r=.0045)
pipe(K,[(-.401,.19,1.236),(-.25,.21,1.244),(-.16,.27,1.225),(-.085,.27,1.195),(.085,.27,1.195),(.16,.27,1.225),(.25,.21,1.244),(.401,.19,1.236)],.011,4,14,4)
for s in [-1,1]:
 tube(K,(s*.29,.201,1.24),(s*.412835,.186,1.234),.017,1,24)
 for i in range(13):
  x=s*(.302+i*.008)
  tube(K,(x,.199-(i*.00085),1.240-i*.00036),(x+s*.0018,.199-i*.00085,1.240-i*.00036),.018,1,16)
 tube(K,(s*.286,.201,1.24),(s*.293,.201,1.24),.022,1,24)
 box(K,(s*.261,.213,1.246),(.042,.037,.027),4,.004)
 pipe(K,[(s*.266,.235,1.24),(s*.288,.27,1.23),(s*.34,.28,1.225),(s*.398,.254,1.217)],.005,2,10,4)
 tube(K,(s*.398,.254,1.214),(s*.398,.254,1.22),.007,2,12)
 pipe(K,[(s*.25,.216,1.25),(s*.19,.29,1.267),(s*.08,.33,1.24),(s*.045,.31,1.10)],.003,1,8,5)
 # discreet amber bar-end marker
 tube(K,(s*.405,.209,1.236),(s*.405,.215,1.236),.007,6,12)
# Tank, seat and tail: continuous compound sections, narrow at the knees.
loft(F,[(-.37,.79,.078,.10),(-.19,.845,.116,.16),(.08,.88,.120,.18),(.21,.925,.08,.115),(.25,.97,.06,.07)],5)
tube(F,(0,.14,.995),(0,.14,1.009),.025,4,24)
pipe(F,[(0,.14,1.009),(0,.13,1.058),(0,.27,1.064),(0,.30,1.04)],.003,1,8,5)
loft(F,[(-.74,.958,.072,.030),(-.64,.951,.086,.047),(-.49,.937,.092,.054),(-.28,.916,.098,.058),(-.05,.926,.086,.061),(.12,.973,.066,.050),(.18,.985,.043,.024)],5)
# Saddle seam follows the raised edge (same dark upholstery, catches light).
for s in [-1,1]:pipe(F,[(s*.067,-.71,.963),(s*.085,-.59,.955),(s*.09,-.3,.942),(s*.08,-.06,.95),(s*.055,.13,.996)],.0015,5,6,4)
loft(F,[(-1.09,.958,.026,.009),(-1.04,.969,.059,.015),(-.90,.970,.086,.02),(-.71,.948,.095,.025),(-.55,.933,.09,.018)],0)
# angular side number panels and radiator shrouds; every plastic uses Paint.
for s in [-1,1]:
 panel(F,[(s*.094,-.94,.944),(s*.119,-.69,.916),(s*.142,-.31,.871),(s*.146,-.23,.74),(s*.12,-.28,.605),(s*.119,-.52,.741),(s*.097,-.84,.874)],0,.009,.004,crown=.010)
 # under-seat fairing integrates with sharp forward radiator tip.
 shaped_skin(F,[(.092,-.43,.911),(.127,-.22,.911),(.16,.055,.951),(.155,.29,.966)],[(.133,-.17,.865),(.163,.045,.855),(.165,.27,.785),(.12,.443,.822)],s)
 # inset lower radiator lobe
 panel(F,[(s*.159,.278,.923),(s*.149,.412,.839),(s*.146,.31,.792),(s*.117,.244,.672),(s*.128,.158,.686),(s*.164,.188,.854)],0,.011,.004)
 # moulded ridge running through the fairing creates a broad highlight
 pipe(F,[(s*.165,.27,.943),(s*.175,.22,.879),(s*.168,.11,.858)],.0015,0,6,4)
 for ff,zz,xx in [(-.62,.882,.124),(-.29,.69,.137),(.256,.929,.17),(.25,.74,.145)]:bolt(F,(s*xx,ff,zz),axis=(s,0,0),r=.0055)
# radiators (core channels, end tanks, hose fittings), behind side shrouds.
for s in [-1,1]:
 box(F,(s*.086,.255,.75),(.065,.089,.23),4,.008)
 for i in range(17):box(F,(s*.085,.307,.65+i*.012),(.065,.009,.004),2,.0008)
 for j in range(5):box(F,(s*(.054+j*.015),.313,.746),(.003,.004,.213),4,.0005)
 pipe(F,[(s*.12,.27,.661),(s*.107,.22,.60),(s*.086,.13,.60)],.011,1,10,4)
 for zz in [.655,.841]:tube(F,(s*.08,.25,zz),(s*.12,.25,zz),.013,2,16)
# Engine crankcase: overlapping cast rounded casings with machined covers and perimeter hardware.
outline=[(-.24,.43),(-.20,.38),(-.08,.355),(.045,.37),(.145,.415),(.167,.50),(.14,.565),(.06,.585),(-.05,.56),(-.12,.58),(-.215,.53)]
vs=[(x,-f,z) for x in [-.108,.108] for f,z in outline];n=len(outline)
me=bpy.data.meshes.new('casting');me.from_pydata(vs,[],[tuple(range(n-1,-1,-1)),tuple(range(n,2*n))]+[(i,(i+1)%n,(i+1)%n+n,i+n) for i in range(n)])
ob=bpy.data.objects.new('casting',me);sc.collection.objects.link(ob);be=ob.modifiers.new('Cast radii','BEVEL');be.width=.013;be.segments=3
mm=ob.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh();F.add([(v.co.x,-v.co.y,v.co.z) for v in mm.vertices],[tuple(p.vertices) for p in mm.polygons],2,False);bpy.data.objects.remove(ob,do_unlink=True)

for s in [-1,1]:
 tube(F,(s*.102,-.075,.459),(s*.148,-.075,.459),.102,2,48)
 tube(F,(s*.148,-.075,.459),(s*.153,-.075,.459),.083,2,48)
 tube(F,(s*.152,-.075,.459),(s*.156,-.075,.459),.068,2,48)
 for i in range(10):
  a=2*pi*i/10;bolt(F,(s*.156,-.075+.09*sin(a),.459+.09*cos(a)),axis=(s,0,0),r=.005)
 tube(F,(s*.111,.092,.487),(s*.139,.092,.487),.058,2,32)
 for i in range(6):
  a=2*pi*i/6;bolt(F,(s*.141,.092+.048*sin(a),.487+.048*cos(a)),axis=(s,0,0),r=.0045)
 # cast raised ribs
 for j in range(3):pipe(F,[(s*.13,-.20,.45+j*.025),(s*.144,-.16,.47+j*.025),(s*.133,-.11,.52+j*.025)],.003,2,6,3)
for ss in [-1,1]:
 # offset perimeter gasket, half hidden behind the alloy casting
 pipe(F,[(ss*.108,ff,zz) for ff,zz in outline+[outline[0]]],.0022,4,6,2)
 for ff,zz in [(-.202,.43),(-.13,.389),(.044,.399),(.14,.456),(.102,.549),(-.115,.55)]:bolt(F,(ss*.115,ff,zz),axis=(ss,0,0),r=.004)
 # mounting bosses, starter housing and cast bridge on the cylinder base
 tube(F,(ss*.090,-.15,.585),(ss*.123,-.15,.585),.022,2,20)
 bolt(F,(ss*.128,-.15,.585),axis=(ss,0,0),r=.006)
 panel(F,[(ss*.115,-.11,.52),(ss*.116,-.055,.574),(ss*.113,.051,.592),(ss*.118,.065,.536)],2,.008,.004)
pipe(F,[(-.13,.078,.522),(-.145,.165,.566),(-.13,.18,.633),(-.11,.19,.68)],.009,1,10,4)
for zz,ff in [(.564,.164),(.653,.181)]:tube(F,(-.14,ff,zz),(-.114,ff,zz),.011,2,12)
# welded collars interrupt the header, with a short hydroformed bulge at the mid-pipe.
for aa,bb in [((-.16,.270,.569),(-.161,.279,.557)),((-.156,-.165,.635),(-.156,-.181,.641))]:tube(F,aa,bb,.0245,3,20)
tube(F,(-.156,-.205,.65),(-.16,-.258,.670),.023,2,24,r2=.029)
tube(F,(-.16,-.258,.670),(-.16,-.320,.687),.029,2,24,r2=.024)
# inclined cylinder barrel, head fins, cam cover and plug cap.
box(F,(0,.052,.635),(.154,.122,.137),4,.012)
for i in range(5):
 zz=.582+i*.025;ff=.03+(zz-.57)*.30
 box(F,(0,ff,zz),(.171,.140,.018),2,.007)
box(F,(0,.075,.717),(.177,.150,.050),4,.010)
box(F,(0,.075,.747),(.163,.145,.027),2,.010)
for s in [-1,1]:
 for ff in [.022,.126]:bolt(F,(s*.075,ff,.765),axis=(0,0,1),r=.005)
tube(F,(0,.09,.759),(0,.105,.80),.011,1,12)
pipe(F,[(0,.11,.792),(.05,.07,.818),(.085,-.1,.80)],.004,1,8,5)
# Intake boot and airbox behind cylinder.
tube(F,(0,-.005,.688),(0,-.16,.733),.036,1,24)
for f0 in [-.13,-.10,-.07]:tube(F,(-.038,f0,.718),(.038,f0,.718),.005,4,10)
box(F,(0,-.29,.777),(.185,.29,.157),5,.025)
# Wrapped header sweeping out of the head, low around the engine and back to high muffler.
pipe(F,[(-.07,.133,.70),(-.123,.245,.67),(-.16,.29,.552),(-.166,.19,.498),(-.169,.105,.588),(-.164,.015,.61),(-.152,-.18,.638),(-.157,-.39,.709),(-.163,-.60,.82)],.023,2,16,7)
# Muffler on rider right with taper, rolled end cap and dark outlet.
ma=Vector((-.16,-.39,.735));mb=Vector((-.15,-.84,.887));axis=(mb-ma).normalized()
tube(F,ma,ma+(mb-ma)*.12,.034,2,32,r2=.052)
tube(F,ma+(mb-ma)*.12,mb,.052,2,32,r2=.047)
tube(F,mb,mb+axis*.018,.047,4,32,r2=.033)
tube(F,mb+axis*.018,mb+axis*.020,.023,1,24)
tube(F,mb+axis*.020,mb+axis*.029,.018,2,24)
tube(F,mb+axis*.029,mb+axis*.030,.013,1,24)
for t in [.18,.78]:
 p=ma+(mb-ma)*t;tube(F,p-axis*.009,p+axis*.009,.054,4,32)
 bolt(F,tuple(p+Vector((-.055,0,0))),axis=(-1,0,0),r=.004)
# Rear swingarm and rising-rate shock.
for s in [-1,1]:
 a=Vector((s*.103,-.23,.423));b=Vector((s*.085,-.74,.320));d=b-a
 basis=[Vector((1,0,0)),d.normalized(),Vector((1,0,0)).cross(d.normalized())]
 box(F,(a+b)/2,(.044,d.length,.064),2,.007,basis=basis)
 box(F,(s*.089,-.715,.344),(.047,.115,.045),2,.006)
 bolt(F,(s*.120,-.739,.320),axis=(s,0,0),r=.015)
 bolt(F,(s*.130,-.24,.427),axis=(s,0,0),r=.016)
 # slider along upper chain side of the swingarm
 pipe(F,[(s*.108,-.26,.468),(s*.097,-.53,.413),(s*.089,-.67,.379)],.007,1,8,3)
tube(F,(-.109,-.29,.431),(.109,-.29,.431),.026,2,20)
sa=Vector((0,-.36,.46));sb=Vector((0,-.20,.824));sd=(sb-sa).normalized()
tube(F,sa,sb,.020,3,20);tube(F,sa+(sb-sa)*.55,sb,.029,4,24)
u=Vector((1,0,0));v=sd.cross(u);points=[]
for i in range(181):
 t=i/180; p=sa+(sb-sa)*(.15+.75*t)+.039*(u*cos(t*2*pi*9)+v*sin(t*2*pi*9));points.append(tuple(p))
pipe(F,points,.006,4,8,1)
for t in [.12,.91]:
 p=sa+(sb-sa)*t;tube(F,p-sd*.009,p+sd*.009,.047,2,24)
# Rear sprocket and drive chain. Tangent path has individual outer links and pins.
g=B['Wheel_R'];ring(g,(.146,-.74,.320),.117,.098,.006,2,72,.002)
ring(g,(.146,-.74,.320),.048,.034,.007,4,36)
tube(g,(.05,-.74,.320),(.135,-.74,.320),.023,2,20)
tube(g,(.125,-.74,.320),(.148,-.74,.320),.036,2,24)
bolt(F,(.160,-.74,.320),r=.014)
for i in range(7):
 a=i*2*pi/7;tube(g,(.146,-.74+.040*sin(a),.320+.040*cos(a)),(.146,-.74+.105*sin(a+.2),.320+.105*cos(a+.2)),.010,2,6)
 bolt(g,(.153,-.74+.046*sin(a),.320+.046*cos(a)),r=.005)
for i in range(42):
 a=i*2*pi/42
 box(g,(.146,-.74+.118*sin(a),.320+.118*cos(a)),(.006,.012,.011),2,.001,basis=[(1,0,0),(0,cos(a),-sin(a)),(0,sin(a),cos(a))])
ring(F,(.146,-.185,.453),.048,.016,.013,4,36)
# Elliptical chain capsule follows both sprockets with upper/lower straight runs.
chain=[]
for i in range(25):
 a=pi/2+i*pi/24;chain.append(Vector((.146,-.185+.049*sin(a),.453+.049*cos(a))))
for i in range(25):
 a=3*pi/2+i*pi/24;chain.append(Vector((.146,-.74+.122*sin(a),.320+.122*cos(a))))
# Correct exterior tangent path: forward semicircle right of drive sprocket, rear semicircle left of rear sprocket.
chain=[]
for i in range(20):
 a=-pi/2+i*pi/19;chain.append(Vector((.146,-.185+.049*cos(a),.453+.049*sin(a))))
for i in range(30):
 a=pi/2+i*pi/29;chain.append(Vector((.146,-.74+.122*cos(a),.320+.122*sin(a))))
chain.append(chain[0]); cumulative=[0]
for a,b in zip(chain,chain[1:]):cumulative.append(cumulative[-1]+(b-a).length)
length=cumulative[-1];count=int(length/.0127)
for i in range(count):
 dist=i*length/count;idx=next(j for j in range(len(cumulative)-1) if cumulative[j+1]>=dist);t=(dist-cumulative[idx])/(cumulative[idx+1]-cumulative[idx]);c=chain[idx].lerp(chain[idx+1],t);direction=(chain[idx+1]-chain[idx]).normalized();side=Vector((1,0,0));up=side.cross(direction).normalized()
 for s in [-1,1]:box(F,c+side*s*.007,(.003,.015,.006),4,0,basis=[side,direction,up])
 tube(F,c-side*.009,c+side*.009,.003,2,6)
# Chain guide, rear caliper, brake hose.
box(F,(.146,-.60,.26),(.035,.087,.038),4,.006)
# Foot controls and serrated pegs; narrow metal loops with serrations.
for s in [-1,1]:
 tube(F,(s*.1,-.21,.40),(s*.165,-.21,.397),.013,4,16)
 for ff in [-.235,-.185]:tube(F,(s*.15,ff,.398),(s*.225,ff,.398),.007,2,10)
 for xx in [.157,.221]:tube(F,(s*xx,-.235,.398),(s*xx,-.185,.398),.007,2,10)
 for j in range(7):
  for ff in [-.235,-.185]:tube(F,(s*(.16+j*.010),ff,.40),(s*(.16+j*.010),ff,.409),.0025,3,6)
 pipe(F,[(s*.145,-.19,.401),(s*.158,-.06,.383),(s*.19,-.036,.397)],.006,2,10,4)
 tube(F,(s*.181,-.036,.397),(s*.22,-.036,.397),.008,1,12)
# Skid plate with rolled edges and slots represented by separated strips.
loft(F,[(-.21,.323,.088,.018),(-.10,.293,.10,.018),(.09,.293,.106,.018),(.24,.365,.075,.016)],2)
for s in [-1,1]:bolt(F,(s*.085,.05,.287),axis=(0,0,-1),r=.006)
# Rear brake caliper, carrier, hydraulic line and pedal master cylinder.
box(F,(-.103,-.643,.363),(.039,.050,.069),4,.007)
box(F,(-.09,-.686,.343),(.018,.074,.025),2,.005)
for ff,zz in [(-.65,.380),(-.65,.345)]:bolt(F,(-.126,ff,zz),axis=(-1,0,0),r=.0045)
pipe(F,[(-.117,-.638,.393),(-.121,-.53,.421),(-.124,-.34,.457),(-.143,-.18,.464)],.003,1,8,5)
tube(F,(-.142,-.18,.418),(-.142,-.18,.477),.012,2,16)
box(F,(-.143,-.184,.489),(.029,.027,.025),5,.004)
# Retracted kickstand follows left swingarm, fully clear of the ground.
S=B['Kickstand'];pipe(S,[(.15,-.24,.40),(.16,-.32,.395),(.16,-.55,.36),(.17,-.65,.365)],.009,4,12,4);box(S,(.17,-.65,.366),(.035,.048,.008),4,.002)
bolt(S,(.16,-.24,.40),r=.010)
pipe(F,[(.151,-.26,.437),(.165,-.36,.423),(.16,-.43,.408)],.003,3,8,5)
# Tail hardware: fender liner, miniature tail/brake lamp and two amber indicators.
loft(F,[(-1.047,.938,.055,.012),(-.98,.925,.067,.012),(-.79,.862,.065,.012)],5)
box(F,(0,-1.013,.937),(.085,.012,.018),4,.004)
box(F,(0,-1.021,.938),(.071,.005,.010),6,.002)
for s in [-1,1]:
 tube(F,(s*.049,-.967,.924),(s*.087,-.967,.924),.004,1,10)
 box(F,(s*.090,-.969,.925),(.019,.025,.012),6,.003)
# Assemble, repair winding, origins, transform contract, and UV lamp regions.
objects=[]
origins={'Frame':(0,0,0),'Fork':(0,.34316,1.005),'Wheel_F':(0,.74,.35),'Wheel_R':(0,-.74,.320),'Kickstand':(.15,-.24,.40)}
for name,g in B.items():
 ob=g.make();bpy.context.view_layer.objects.active=ob
 # Global winding repair handles geometric templates authored in a reflected basis.
 bm=bmesh.new();bm.from_mesh(ob.data);bmesh.ops.recalc_face_normals(bm,faces=list(bm.faces))
 for e in bm.edges:
  if len(e.link_faces)==2 and e.calc_face_angle(0)>1.10:e.smooth=False
 bm.to_mesh(ob.data);bm.free()
 # Resolve lamp colour from position before shifting object origins.
 uv=ob.data.uv_layers.active
 for p in ob.data.polygons:
  if p.material_index==6:
   center=sum((ob.data.vertices[i].co for i in p.vertices),Vector())/len(p.vertices)
   uu=.83 if abs(center.x)>.075 else (.50 if center.y>.7 else .16)
   for li in p.loop_indices:uv.data[li].uv=(uu,.5)
 o=Vector((origins[name][0],-origins[name][1],origins[name][2]));ob.data.transform(Matrix.Translation(-o));ob.location=o
 objects.append(ob)
# Planar/cylindrical detail is simplified conservatively; no textures are geometry substitutes.
for ob in objects:
 bpy.context.view_layer.objects.active=ob
 dec=ob.modifiers.new('Game topology budget','DECIMATE');dec.ratio=.87 if ob.name in ['Wheel_F','Wheel_R'] else .88
 bpy.ops.object.modifier_apply(modifier=dec.name)
# Front wheel is explicitly a child of the complete steering assembly.
bpy.context.view_layer.update()
fork=bpy.data.objects['Fork'];wheel=bpy.data.objects['Wheel_F'];mw=wheel.matrix_world.copy();wheel.parent=fork;wheel.matrix_world=mw
# Sockets are real empty glTF nodes with game-coordinate world positions.
sockets={'Seat_Rider':(0,-.26,.974),'Hand_L':(.351,.193,1.237),'Hand_R':(-.351,.193,1.237),'Foot_L':(.19,-.21,.409),'Foot_R':(-.19,-.21,.409),'Light_Head':(0,.416,1.127),'Light_Tail':(0,-1.026,.938)}
for name,p in sockets.items():
 ob=bpy.data.objects.new(name,None);sc.collection.objects.link(ob);ob.location=(p[0],-p[1],p[2]);ob.empty_display_size=.025
 if name.startswith('Hand') or name=='Light_Head':
  bpy.context.view_layer.update();mw=ob.matrix_world.copy();ob.parent=fork;ob.matrix_world=mw
 objects.append(ob)
bpy.context.view_layer.update()
# Presentation scene, intentionally excluded from export.
world=bpy.data.worlds.new('Soft daylight');sc.world=world;world.use_nodes=True;world.node_tree.nodes['Background'].inputs[0].default_value=(.64,.72,.85,1);world.node_tree.nodes['Background'].inputs[1].default_value=.45
floor=bpy.data.materials.new('Studio floor');floor.diffuse_color=(.23,.25,.27,1);floor.use_nodes=True;floor.node_tree.nodes.get('Principled BSDF').inputs['Roughness'].default_value=.8
bpy.ops.mesh.primitive_plane_add(size=200,location=(0,0,-.003));ground=bpy.context.object;ground.name='STUDIO';ground.data.materials.append(floor)
def area(name,loc,power,size,color):
 bpy.ops.object.light_add(type='AREA',location=loc);o=bpy.context.object;o.name=name;o.data.energy=power;o.data.shape='DISK';o.data.size=size;o.data.color=color;o.rotation_euler=(Vector((0,0,.6))-o.location).to_track_quat('-Z','Y').to_euler()
area('Key',(3,-4,6),650,3,(1,.91,.80));area('Fill',(-3,-1,3),400,3,(.72,.83,1));area('Rim',(1,4,4),700,3,(1,.98,.93))
bpy.ops.object.camera_add();cam=bpy.context.object;sc.camera=cam;cam.data.type='ORTHO'
def camera(side=False):
 cam.location=(-3.8,-4.8,2.10) if not side else (4.5,0,.70)
 target=Vector((0,0,.64));cam.rotation_euler=(target-cam.location).to_track_quat('-Z','Y').to_euler();cam.data.ortho_scale=3.10 if not side else 2.70
camera()
sc.render.resolution_x=1920;sc.render.resolution_y=1080;sc.render.resolution_percentage=100;sc.cycles.samples=32
# Clay is rendered first and leaves no export override.
clay=bpy.data.materials.new('QA clay');clay.use_nodes=True;clay.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value=(.34,.34,.34,1);clay.node_tree.nodes['Principled BSDF'].inputs['Roughness'].default_value=.68
sc.view_layers[0].material_override=clay;sc.render.filepath=str(ROOT/'clay.png');bpy.ops.render.render(write_still=True);sc.view_layers[0].material_override=None
# Write full uncompressed glTF binary with no helper scene nodes.
bpy.ops.object.select_all(action='DESELECT')
for ob in objects:ob.select_set(True)
bpy.context.view_layer.objects.active=objects[0]
bpy.ops.export_scene.gltf(filepath=str(ROOT/'dirt-bike.glb'),export_format='GLB',use_selection=True,export_yup=True,export_apply=True,export_texcoords=True,export_normals=True,export_materials='EXPORT',export_cameras=False,export_lights=False,export_image_format='AUTO')
# Assert serialized paint contract; standard factor also used by the Blender preview.
p=ROOT/'dirt-bike.glb';raw=p.read_bytes();jl=struct.unpack_from('<I',raw,12)[0];doc=json.loads(raw[20:20+jl]);binary=raw[20+jl:]
for m in doc['materials']:
 if m['name']=='Paint':
  m['pbrMetallicRoughness']['baseColorFactor']=[.85,.25,.01,1]
  assert 'baseColorTexture' in m['pbrMetallicRoughness']
# Explicitly make all texture repeats portable.
for sm in doc.get('samplers',[]):sm['wrapS']=10497;sm['wrapT']=10497
js=json.dumps(doc,separators=(',',':')).encode();js+=b' '*((-len(js))%4);p.write_bytes(struct.pack('<III',0x46546c67,2,20+len(js)+len(binary))+struct.pack('<II',len(js),0x4e4f534a)+js+binary)
tri=sum(len(p.vertices)-2 for ob in objects if ob.type=='MESH' for p in ob.data.polygons)
coords=[ob.matrix_world@v.co for ob in objects if ob.type=='MESH' for v in ob.data.vertices]
lo=[min(v[i] for v in coords) for i in range(3)];hi=[max(v[i] for v in coords) for i in range(3)]
paints=[{'name':'Factory orange','linear_rgb':[.85,.25,.01]},{'name':'Rally red','linear_rgb':[.62,.018,.012]},{'name':'Glacier white','linear_rgb':[.82,.86,.89]},{'name':'Petrol blue','linear_rgb':[.012,.20,.29]},{'name':'Olive','linear_rgb':[.18,.24,.07]},{'name':'Graphite','linear_rgb':[.055,.065,.078]}]
meta={'units':'metres','axes':{'front':'+Z','up':'+Y','rider_left':'+X'},'nodes':{},'bounding_box':{'min':[lo[0],lo[2],-hi[1]],'max':[hi[0],hi[2],-lo[1]]},'triangles':tri,'materials':[m.name for m in MATS],'paints':paints,'steering_axis':[0,.86154,-.5077],'wheel_spin_axis':[1,0,0],'wheelbase':1.48}
for ob in objects:
 v=ob.matrix_world.translation;meta['nodes'][ob.name]={'world_position':[v.x,v.z,-v.y],'type':'mesh' if ob.type=='MESH' else 'empty','parent':ob.parent.name if ob.parent else None}
(ROOT/'nodes.json').write_text(json.dumps(meta,indent=2))
(ROOT/'NOTES.md').write_text(f'Modern full-scale enduro bike, articulated steering/wheels, retracted stand and rider/light sockets.\n{tri:,} triangles; GLB {p.stat().st_size/1024/1024:.2f} MiB; 1.48 m wheelbase.\n7 materials: Paint, Rubber, Alloy, Steel, Engine, Saddle, Lamps; neutral Paint texture and linear orange factor.\nCycles GPU baked base colour up to 1024 px, normal 512 px, packed ORM 512 px; shared tiled sets.\nPoly Haven CC0: metal_plate, rubberized_track, leather_white; https://polyhaven.com/license https://polyhaven.com/a/metal_plate ; https://polyhaven.com/a/rubberized_track ; https://polyhaven.com/a/leather_white.\nDifferences: original miniature enduro lamps; all body and number panels recolour; dark frame finish; less intricate engine casting detail; clean rather than heavily scratched plastics.\n')
print('COMPLETE_GLB',tri,p.stat().st_size,meta['bounding_box'],flush=True)
sc.cycles.samples=48;camera();sc.render.filepath=str(ROOT/'final.png');bpy.ops.render.render(write_still=True)
camera(True);sc.render.filepath=str(ROOT/'side.png');bpy.ops.render.render(write_still=True)
camera();sc.render.resolution_x=640;sc.render.resolution_y=360;sc.cycles.samples=16
paint=MATS[0].node_tree.nodes['PAINT_FACTOR']
panels=[]
for i,entry in enumerate(paints):
 paint.inputs[2].default_value=(*entry['linear_rgb'],1);sc.render.filepath=str(ROOT/f'paint_{i}.png');bpy.ops.render.render(write_still=True)
 im=bpy.data.images.load(str(ROOT/f'paint_{i}.png'),check_existing=False); panels.append(np.array(im.pixels[:],dtype=np.float32).reshape(360,640,4));bpy.data.images.remove(im)
canvas=np.zeros((720,1920,4),dtype=np.float32)
for i,a in enumerate(panels):canvas[(1-i//3)*360:(2-i//3)*360,(i%3)*640:(i%3+1)*640,:]=a
im=bpy.data.images.new('Six paint variants',width=1920,height=720);im.pixels.foreach_set(canvas.ravel());im.filepath_raw=str(ROOT/'paints.png');im.file_format='PNG';im.save()
paint.inputs[2].default_value=(.85,.25,.01,1)
sc.render.resolution_x=1920;sc.render.resolution_y=1080
bpy.ops.wm.save_as_mainfile(filepath=str(ROOT/'dirt-bike.blend'))
print('DONE',round(__import__('time').time()-START,1),flush=True)
