import bpy, math, random
from mathutils import Vector
random.seed(14)
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for d in bpy.data.materials: bpy.data.materials.remove(d)
scene=bpy.context.scene
scene.unit_settings.system='METRIC'
scene.unit_settings.scale_length=1.0

# Manufacturer-CAD PCBA datum-pin loading cell. Engineering geometry is in world metres.
def mat(name,color,metal=0,rough=.45,trans=0,emit=0):
    m=bpy.data.materials.new(name); m.use_nodes=True
    p=m.node_tree.nodes.get('Principled BSDF')
    p.inputs['Base Color'].default_value=(*color,1)
    p.inputs['Metallic'].default_value=metal
    p.inputs['Roughness'].default_value=rough
    p.inputs['Transmission Weight'].default_value=trans
    if trans: p.inputs['IOR'].default_value=1.46
    if emit:
        p.inputs['Emission Color'].default_value=(*color,1)
        p.inputs['Emission Strength'].default_value=emit
    return m
ivory=mat('Cool off-white powder coat',(.72,.76,.77),.12,.42)
wall=mat('Off-white wall panels',(.69,.735,.76),0,.63)
floor_mat=mat('Pale grey epoxy',(.43,.47,.49),.06,.44)
grout=mat('Fine cool grey tile joints',(.25,.29,.31),0,.7)
alu=mat('Brushed aluminium',(.55,.61,.65),.78,.32)
edge=mat('Dark anodised aluminium',(.18,.23,.26),.7,.33)
black=mat('Graphite ESD polymer',(.019,.026,.032),.13,.4)
rubber=mat('Rubber and cables',(.008,.014,.018),0,.68)
turq=mat('Deep turquoise powder coat',(.012,.235,.275),.32,.36)
turq_light=mat('Turquoise identification',(.025,.43,.46),.15,.37)
worktop=mat('Pale grey ESD laminate',(.55,.62,.635),.05,.5)
steel=mat('Fastener stainless steel',(.56,.59,.60),.87,.26)
gold=mat('Gold plated contacts',(.58,.36,.075),.75,.25)
ochre=mat('Warm ochre module carrier',(.56,.29,.050),.20,.36)
pcb=mat('Solder mask green',(.016,.17,.076),.22,.34)
lens=mat('Optical black glass',(.008,.021,.033),.3,.1)
screen=mat('Subdued instrument display',(.012,.043,.06),.05,.4,emit=.22)
screen_ink=mat('Instrument cyan',(.12,.47,.51),.05,.43,emit=.2)
white_ink=mat('Quiet label ink',(.61,.7,.71),0,.53)
glass=mat('Glazed partition 8mm',(.70,.84,.87),.02,.23,.78)
lightmat=mat('Ceiling opal diffuser',(.83,.93,1),0,.35,emit=2)
red=mat('Muted status red',(.4,.045,.026),.2,.4)
blue=mat('Blue marker',(.035,.10,.28),.1,.45)

def tags(o,body=None,motion='static',mass=1,collision='visual'):
    o['robotgym_body']=body or o.name
    o['robotgym_motion']=motion
    o['robotgym_mass_kg']=float(mass)
    o['robotgym_collision']=collision
    return o
def finish(o,name,material,body=None,motion='static',mass=1,collision='visual',bevel=0,smooth=False):
    o.name=name
    if material: o.data.materials.append(material)
    tags(o,body,motion,mass,collision)
    if bevel:
        mod=o.modifiers.new('Restrained machined edge','BEVEL'); mod.width=bevel;mod.segments=2
        bpy.context.view_layer.objects.active=o
        bpy.ops.object.modifier_apply(modifier=mod.name)
    if smooth:
        for p in o.data.polygons:p.use_smooth=True
    return o
def box(n,loc,size,m,bevel=0,body=None,motion='static',mass=1,collision='visual'):
    bpy.ops.mesh.primitive_cube_add(size=1,location=loc)
    o=bpy.context.object;o.dimensions=size
    bpy.ops.object.transform_apply(location=False,rotation=False,scale=True)
    return finish(o,n,m,body,motion,mass,collision,bevel)
def cyl(n,loc,r,depth,m,axis='Z',body=None,motion='static',mass=1,collision='visual',vertices=24):
    bpy.ops.mesh.primitive_cylinder_add(vertices=vertices,radius=r,depth=depth,location=loc)
    o=bpy.context.object
    if axis=='Y':o.rotation_euler[0]=math.pi/2
    if axis=='X':o.rotation_euler[1]=math.pi/2
    return finish(o,n,m,body,motion,mass,collision,0,True)
def rod(n,a,b,r,m,body='Lab small hardware'):
    v=Vector(b)-Vector(a)
    o=cyl(n,(Vector(a)+Vector(b))/2,r,v.length,m,body=body,vertices=12)
    o.rotation_euler=v.to_track_quat('Z','Y').to_euler()
    return o
def cable(n,points,r=.007,m=rubber,body='Cable infrastructure'):
    cu=bpy.data.curves.new(n,'CURVE');cu.dimensions='3D';cu.resolution_u=10
    cu.bevel_depth=r;cu.bevel_resolution=2
    sp=cu.splines.new('BEZIER');sp.bezier_points.add(len(points)-1)
    for p,v in zip(sp.bezier_points,points):p.co=v;p.handle_left_type='AUTO';p.handle_right_type='AUTO'
    o=bpy.data.objects.new(n,cu);bpy.context.collection.objects.link(o)
    bpy.context.view_layer.objects.active=o;o.select_set(True)
    bpy.ops.object.convert(target='MESH');o=bpy.context.object;o.select_set(False)
    return finish(o,n,m,body)
def text_obj(n,txt,loc,size,m,rotation=(0,0,0),body='Grounded identification'):
    cu=bpy.data.curves.new(n,'FONT');cu.body=txt;cu.size=size;cu.extrude=0;cu.resolution_u=2
    o=bpy.data.objects.new(n,cu);bpy.context.collection.objects.link(o);o.location=loc;o.rotation_euler=rotation
    bpy.context.view_layer.objects.active=o;o.select_set(True);bpy.ops.object.convert(target='MESH');o=bpy.context.object;o.select_set(False)
    return finish(o,n,m,body)
def screw(n,x,y,z,r=.004,axis='Z',body='Fixture fasteners'):
    cyl(n,(x,y,z),r,.0018,steel,axis,body=body,vertices=12)
    if axis=='Z':box(n+' drive',(x,y,z+.001),(.0045,.001,.0004),black,body=body)
def ring(n,loc,ro,ri,h,m,body='Lab small hardware',motion='static',mass=1):
    verts=[];N=32
    for z,r in [(-h/2,ro),(h/2,ro),(h/2,ri),(-h/2,ri)]:
        for k in range(N):
            t=2*math.pi*k/N;verts.append((loc[0]+r*math.cos(t),loc[1]+r*math.sin(t),loc[2]+z))
    faces=[]
    for j in range(4):
        for k in range(N):faces.append((j*N+k,j*N+(k+1)%N,((j+1)%4)*N+(k+1)%N,((j+1)%4)*N+k))
    me=bpy.data.meshes.new(n);me.from_pydata(verts,[],faces);me.update()
    o=bpy.data.objects.new(n,me);bpy.context.collection.objects.link(o)
    return finish(o,n,m,body,motion,mass,'visual',0,True)
def profile_plate(n,x,y,z,points,thick,m,body):
    verts=[(x+dx,y+py,z+pz) for dx in (-thick/2,thick/2) for py,pz in points]
    N=len(points);faces=[tuple(range(N-1,-1,-1)),tuple(range(N,2*N))]
    for i in range(N):faces.append((i,(i+1)%N,(i+1)%N+N,i+N))
    me=bpy.data.meshes.new(n);me.from_pydata(verts,[],faces);me.update()
    o=bpy.data.objects.new(n,me);bpy.context.collection.objects.link(o)
    return finish(o,n,m,body,bevel=.002)
def handle(n,x,y,z,w=.15,body='Cabinet hardware'):
    rod(n+' left',(x-w/2,y+.014,z),(x-w/2,y-.02,z),.007,steel,body)
    rod(n+' right',(x+w/2,y+.014,z),(x+w/2,y-.02,z),.007,steel,body)
    rod(n+' grip',(x-w/2,y-.02,z),(x+w/2,y-.02,z),.007,steel,body)

# 4.6 x 3.8 m room footprint, front and right elevations cut away for viewing.
box('Lab floor',(.6,.25,-.06),(4.6,3.8,.12),floor_mat,collision='solid',mass=1000)
for i in range(1,8):
    x=-1.7+i*.575
    box('Epoxy tile joint X %02d'%i,(x,.25,.00015),(.002,3.8,.0003),grout,body='Floor joints')
for i in range(1,7):
    y=-1.65+i*(3.8/7)
    box('Epoxy tile joint Y %02d'%i,(.6,y,.00015),(4.6,.002,.0003),grout,body='Floor joints')
box('Left finished wall',(-1.745,.25,1.4),(.09,3.8,2.8),wall,collision='solid',mass=600)
box('Rear partition sill',(.1325,2.195,.42),(3.665,.09,.84),wall,collision='solid',mass=400)
box('Rear partition header',(.6,2.195,2.615),(4.6,.09,.37),wall,collision='solid',mass=200)
box('Left aluminium skirting',(-1.687,.25,.05),(.018,3.8,.10),edge,body='Room trim')
box('Rear aluminium skirting',(.1325,2.139,.05),(3.665,.018,.10),edge,body='Room trim')
for i in range(8):
    y=-1.42+i*.46
    box('Wall vertical reveal %02d'%i,(-1.696,y,1.46),(.002,.005,2.60),alu,body='Wall panel seams')
# A solid rear service panel at left; broad glazed panes and separate door at right.
box('Rear service panel',(-1.12,2.145,1.65),(1.16,.026,1.58),ivory,body='Partition framing')
for a,b in [(-.52,.36),(.39,1.26),(1.29,1.94),(1.99,2.85)]:
    box('Rear glass pane %.2f'%a,((a+b)/2,2.164,1.69),(b-a,.008,1.68),glass,body='Partition glazing')
for x in [-1.69,-.55,.375,1.275,1.965,2.88]:
    box('Partition mullion %.2f'%x,(x,2.135,1.64),(.034,.055,1.74),alu,body='Partition framing')
for z in [.835,2.435]:
    box('Partition horizontal frame %.2f'%z,(.61,2.132,z),(4.54,.06,.038),alu,body='Partition framing')
# Frosted privacy stripe is separate finite-thickness glazing appearance.
for a,b in [(-.52,.36),(.39,1.26),(1.29,1.94),(1.99,2.85)]:
    box('Privacy stripe %.2f'%a,((a+b)/2,2.158,1.39),(b-a,.001,.13),worktop,body='Partition glazing')
box('Door lower glass',(2.42,2.128,.45),(.84,.008,.77),glass,body='Glazed service door')
for x in [1.965,2.88]:box('Door lower jamb %.2f'%x,(x,2.13,.42),(.036,.06,.84),alu,body='Glazed service door')
rod('Door vertical pull',(2.09,2.087,.98),(2.09,2.087,1.33),.012,steel,'Glazed service door')
for z in [1.02,1.29]:rod('Door handle bracket '+str(z),(2.09,2.085,z),(2.09,2.132,z),.007,steel,'Glazed service door')
box('Door kick plate',(2.42,2.119,.16),(.81,.012,.21),alu,body='Glazed service door')
# Ceiling fixtures, visible open structure with restrained suspended services.
for ix,x in enumerate([-.95,.58,2.1]):
    box('Linear luminaire housing %d'%ix,(x,.24,2.67),(.072,2.5,.065),alu,.01,body='Ceiling lights')
    box('Linear opal diffuser %d'%ix,(x,.24,2.632),(.057,2.42,.012),lightmat,.004,body='Ceiling lights')
    for y in [-.7,1.16]:rod('Luminaire suspension %d %.1f'%(ix,y),(x,y,2.7),(x,y,2.87),.0025,steel,'Ceiling suspension')
for y in [1.34,1.58]:box('Cable tray rail '+str(y),(.38,y,2.73),(3.9,.026,.09),edge,body='Suspended cable tray')
for i in range(19):box('Cable tray rung %02d'%i,(-1.43+i*.2,1.46,2.697),(.022,.25,.018),alu,body='Suspended cable tray')
for x in [-1.24,.32,1.87]:
    for y in [1.34,1.58]:rod('Tray hanger %.2f %.2f'%(x,y),(x,y,2.73),(x,y,2.91),.003,steel,'Ceiling suspension')
for i in range(3):cable('Suspended service cable %d'%i,[(-1.5,1.40+i*.05,2.73),(.1,1.40+i*.05,2.73),(1.7,1.40+i*.05,2.73)],.009)

# Central aluminium extrusion workbench, exact top extent and height.
box('Main tabletop',(.725,0,.720),(1.25,1.30,.040),worktop,.002,body='main_tabletop',collision='solid',mass=45)
for y in [-.642,.642]:box('Main tabletop edge Y '+str(y),(.725,y,.72),(1.248,.009,.038),edge,.001,body='Main bench trim')
for x in [.105,1.345]:box('Main tabletop edge X '+str(x),(x,0,.72),(.009,1.28,.038),edge,.001,body='Main bench trim')
for x in [.16,1.29]:
    for y in [-.59,.59]:
        label='%.2f %.2f'%(x,y)
        box('Extrusion leg '+label,(x,y,.383),(.045,.045,.626),alu,.001,body='Main bench frame')
        for dx in [-.023,.023]:box('T slot leg '+label+str(dx),(x+dx,y,.383),(.001,.009,.604),black,body='Extrusion channels')
        for dy in [-.023,.023]:box('T slot leg face '+label+str(dy),(x,y+dy,.383),(.009,.001,.604),black,body='Extrusion channels')
        cyl('Levelling threaded stem '+label,(x,y,.062),.011,.042,steel,body='Main bench feet')
        cyl('Black levelling foot '+label,(x,y,.031),.033,.022,rubber,body='Main bench feet')
        box('Corner mounting cleat '+label,(x,y,.664),(.065,.065,.018),edge,.003,body='Main bench brackets')
        screw('Bench bracket bolt '+label,x,y,.675,body='Main bench brackets')
for y in [-.59,.59]:
    for z in [.67,.17]:
        box('Bench X beam %.2f %.2f'%(y,z),(.725,y,z),(1.12,.044,.044),alu,.001,body='Main bench frame')
        box('Bench X beam channel %.2f %.2f'%(y,z),(.725,y-.023,z),(1.07,.001,.009),black,body='Extrusion channels')
for x in [.16,1.29]:
    for z in [.67,.17]:box('Bench Y beam %.2f %.2f'%(x,z),(x,0,z),(.044,1.18,.044),alu,.001,body='Main bench frame')
box('Underbench graphite cabinet',(.46,.05,.419),(.48,.96,.47),black,.012,body='Main equipment cabinet')
box('Turquoise cabinet front',(.46,-.44,.42),(.454,.018,.432),turq,.004,body='Main equipment cabinet')
for z in [.33,.54]:
    box('Cabinet drawer reveal '+str(z),(.46,-.452,z),(.414,.002,.003),edge,body='Main equipment cabinet')
    handle('Central drawer handle '+str(z),.46,-.461,z+.054,.20,'Main equipment cabinet')
box('Controller chassis',(.95,.34,.35),(.30,.39,.37),edge,.006,body='Underbench controller')
box('Controller face',(.95,.137,.35),(.283,.018,.35),black,.005,body='Underbench controller')
for i in range(9):box('Controller ventilation %02d'%i,(.95,.126,.253+i*.016),(.20,.002,.004),alu,body='Controller details')
cyl('Controller power button',(.86,.122,.482),.009,.003,steel,'Y',body='Controller details')
cyl('Controller status light',(.89,.121,.482),.003,.004,turq_light,'Y',body='Controller details')
box('Controller serial plate',(1.024,.121,.48),(.065,.002,.018),white_ink,body='Controller details')
box('Underbench rear cable conduit',(.73,.61,.51),(1.05,.045,.062),ivory,.004,body='Bench electrical')
box('Grounded outlet rail',(1.316,0,.57),(.032,.50,.07),black,.003,body='Bench electrical')
for y in [-.16,0,.16]:
    box('Ground outlet '+str(y),(1.334,y,.57),(.012,.10,.046),ivory,.002,body='Bench electrical')
    for dy in [-.015,.015]:cyl('Outlet aperture '+str(y)+str(dy),(1.342,y+dy,.57),.005,.002,black,'X',body='Bench electrical')
cable('Controller harness',[(.98,.50,.5),(1.08,.58,.51),(1.18,.58,.32),(1.22,.58,.17)],.012)
cable('Main ground wire',[(.24,.58,.71),(.24,.615,.6),(.3,.63,.3),(.35,.62,.18)],.003,turq_light)
# Robot location is intentionally clear. There is no pedestal or proxy arm.

CAD_PATH='/work/assets/db6303421748495eb016d8cef5f7effb.glb'
COLLISION_PROFILE={'units': 'mm', 'board_thickness_mm': 1.276, 'outline_mm': [[82.0, 0.0], [3.0, 0.0], [1.8519497029047307, 0.22836140246614], [0.8786796564403576, 0.8786796564403572], [0.22836140246614, 1.8519497029047303], [0.0, 3.0], [0.0, 53.0], [0.22836140246614, 54.14805029709527], [0.8786796564403576, 55.121320343559645], [1.8519497029047307, 55.77163859753386], [3.0, 56.0], [82.0, 56.0], [83.14805029709527, 55.77163859753386], [84.12132034355965, 55.121320343559645], [84.77163859753387, 54.14805029709527], [85.0, 53.0], [85.0, 3.0], [84.77163859753387, 1.8519497029047307], [84.12132034355965, 0.8786796564403576], [83.14805029709527, 0.22836140246614], [82.0, 0.0]], 'holes_mm': [[3.5, 3.5, 1.35, 24], [61.5, 52.5, 1.35, 24], [3.5, 52.5, 1.35, 16], [61.5, 3.5, 1.35, 16], [3.5, 9.5, 1.55, 12], [61.5, 46.5, 1.55, 12]], 'pieces_mm': [[[3.0, 0.0], [3.4999999999999996, 2.15], [3.8494057108884023, 2.1960001345097577]], [[0.8786796564403576, 0.8786796564403572], [0.22836140246614, 1.8519497029047303], [0.0, 3.0], [2.1960001345097577, 3.150594289111597], [2.3308657048910075, 2.825]], [[0.0, 3.0], [0.0, 53.0], [2.1576606241341203, 10.275], [1.95, 9.5]], [[0.0, 53.0], [0.22836140246614, 54.14805029709527], [0.8786796564403576, 55.121320343559645], [1.8519497029047307, 55.77163859753386], [2.545405845398161, 53.45459415460184], [2.252762631109763, 53.01662263369287]], [[0.0, 3.0], [2.1960001345097577, 3.849405710888403], [2.15, 3.5]], [[60.25276263110976, 4.016622633692871], [5.05, 9.5], [4.84233937586588, 10.275], [4.454594154601839, 51.54540584539816], [4.747237368890237, 51.98337736630713], [60.15766062413412, 45.725], [60.725, 45.157660624134124], [60.54540584539816, 4.454594154601839]], [[60.25276263110976, 2.983377366307129], [4.803999865490242, 3.150594289111596], [4.85, 3.5], [60.15, 3.5]], [[59.95, 46.5], [4.747237368890237, 51.98337736630713], [4.85, 52.5], [60.15, 52.5], [60.19600013450976, 52.150594289111595], [60.15766062413412, 47.275]], [[4.84233937586588, 10.275], [4.275, 10.84233937586588], [4.016622633692871, 51.25276263110976], [4.454594154601839, 51.54540584539816]], [[2.2527626311097624, 51.98337736630713], [0.0, 53.0], [2.15, 52.5]], [[85.0, 3.0], [62.842339375865876, 45.725], [63.05, 46.5], [82.0, 56.0], [83.14805029709527, 55.77163859753386], [84.12132034355965, 55.121320343559645], [84.77163859753387, 54.14805029709527], [85.0, 53.0]], [[82.0, 0.0], [62.85, 3.5], [62.74723736889024, 4.016622633692871], [85.0, 3.0], [84.77163859753387, 1.8519497029047307], [84.12132034355965, 0.8786796564403576], [83.14805029709527, 0.22836140246614]], [[3.0, 0.0], [1.8519497029047307, 0.22836140246614], [0.8786796564403576, 0.8786796564403572], [2.8249999999999993, 2.330865704891008], [3.1505942891115972, 2.1960001345097577]], [[0.0, 3.0], [1.95, 9.5], [2.15766062413412, 8.725], [2.330865704891008, 4.175], [2.1960001345097577, 3.849405710888403]], [[4.85, 3.5], [4.803999865490242, 3.849405710888403], [4.84233937586588, 8.725], [5.05, 9.5], [60.25276263110976, 4.016622633692871], [60.15, 3.5]], [[2.1576606241341203, 10.275], [0.0, 53.0], [2.2527626311097624, 51.98337736630713], [2.5454058453981605, 51.54540584539816], [2.725, 10.84233937586588]], [[0.0, 53.0], [2.252762631109763, 53.01662263369287], [2.15, 52.5]], [[61.5, 53.85], [3.0, 56.0], [82.0, 56.0]], [[62.45459415460184, 4.454594154601839], [85.0, 3.0], [62.74723736889024, 4.016622633692871]], [[60.33086570489101, 51.825], [60.15766062413412, 47.275], [60.19600013450976, 52.150594289111595]], [[2.1960001345097577, 3.150594289111597], [0.0, 3.0], [2.15, 3.5]], [[2.330865704891008, 4.175], [2.15766062413412, 8.725], [2.724999999999999, 8.15766062413412], [2.545405845398161, 4.454594154601839]], [[60.15766062413412, 47.275], [60.33086570489101, 51.825], [60.54540584539816, 51.545405845398165], [60.725, 47.84233937586588]], [[2.545405845398161, 53.45459415460184], [1.8519497029047307, 55.77163859753386], [3.0, 56.0], [2.9833773663071286, 53.74723736889024]], [[3.0, 0.0], [4.454594154601839, 2.5454058453981605], [4.669134295108992, 2.8249999999999993], [60.25276263110976, 2.983377366307129], [60.54540584539816, 2.545405845398161]], [[60.54540584539816, 4.454594154601839], [60.725, 45.157660624134124], [61.5, 44.95], [60.98337736630713, 4.747237368890238]], [[60.15766062413412, 45.725], [4.747237368890237, 51.98337736630713], [59.95, 46.5]], [[2.725, 10.84233937586588], [2.5454058453981605, 51.54540584539816], [2.983377366307128, 51.25276263110976], [3.5, 11.05]], [[0.8786796564403576, 0.8786796564403572], [2.3308657048910075, 2.825], [2.54540584539816, 2.5454058453981614]], [[2.545405845398161, 4.454594154601839], [2.724999999999999, 8.15766062413412], [2.825, 4.669134295108992]], [[3.0, 0.0], [60.54540584539816, 2.545405845398161], [60.98337736630713, 2.2527626311097633]], [[60.825, 51.33086570489101], [60.725, 47.84233937586588], [60.54540584539816, 51.545405845398165]], [[2.9833773663071286, 53.74723736889024], [3.0, 56.0], [3.5, 53.85]], [[2.8249999999999993, 2.330865704891008], [0.8786796564403576, 0.8786796564403572], [2.54540584539816, 2.5454058453981614]], [[2.825, 4.669134295108992], [2.724999999999999, 8.15766062413412], [3.4999999999999996, 7.95], [3.1505942891115972, 4.803999865490242]], [[4.669134295108992, 4.175], [4.84233937586588, 8.725], [4.803999865490242, 3.849405710888403]], [[60.98337736630713, 4.747237368890238], [61.5, 44.95], [61.5, 4.85]], [[60.725, 47.84233937586588], [60.825, 51.33086570489101], [61.150594289111595, 51.19600013450976], [61.5, 48.05]], [[3.4999999999999996, 51.15], [3.5, 11.05], [2.983377366307128, 51.25276263110976]], [[3.5, 53.85], [3.0, 56.0], [4.016622633692871, 53.74723736889024]], [[3.0, 0.0], [60.98337736630713, 2.2527626311097633], [61.5, 2.15]], [[85.0, 3.0], [62.01662263369287, 4.747237368890238], [61.5, 4.85], [61.5, 44.95], [62.275, 45.15766062413412]], [[4.275, 10.84233937586588], [3.5, 11.05], [3.4999999999999996, 51.15], [4.016622633692871, 51.25276263110976]], [[4.016622633692871, 53.74723736889024], [3.0, 56.0], [4.454594154601839, 53.45459415460184]], [[3.1505942891115972, 4.803999865490242], [3.4999999999999996, 7.95], [3.5, 4.85]], [[61.5, 51.15], [61.5, 48.05], [61.150594289111595, 51.19600013450976]], [[4.747237368890238, 53.01662263369287], [4.454594154601839, 53.45459415460184], [3.0, 56.0], [60.54540584539816, 53.45459415460184], [60.33086570489101, 53.175]], [[3.0, 0.0], [3.1505942891115972, 2.1960001345097577], [3.4999999999999996, 2.15]], [[3.849405710888403, 4.803999865490242], [3.5, 4.85], [3.4999999999999996, 7.95], [4.275, 8.15766062413412]], [[62.275, 47.84233937586588], [61.5, 48.05], [61.5, 51.15], [61.849405710888405, 51.19600013450976]], [[61.150594289111595, 53.80399986549024], [3.0, 56.0], [61.5, 53.85]], [[4.175, 2.330865704891008], [3.0, 0.0], [3.8494057108884023, 2.1960001345097577]], [[85.0, 3.0], [62.275, 45.15766062413412], [62.842339375865876, 45.725]], [[60.825, 53.66913429510899], [3.0, 56.0], [61.150594289111595, 53.80399986549024]], [[4.454594154601839, 2.5454058453981605], [3.0, 0.0], [4.175, 2.330865704891008]], [[3.849405710888403, 4.803999865490242], [4.275, 8.15766062413412], [4.175, 4.669134295108992]], [[62.01662263369287, 4.747237368890238], [85.0, 3.0], [62.45459415460184, 4.454594154601839]], [[62.175, 51.33086570489101], [62.275, 47.84233937586588], [61.849405710888405, 51.19600013450976]], [[4.85, 52.5], [4.747237368890238, 53.01662263369287], [60.19600013450976, 52.849405710888405], [60.15, 52.5]], [[60.54540584539816, 53.45459415460184], [3.0, 56.0], [60.825, 53.66913429510899]], [[4.454594154601839, 4.454594154601839], [4.175, 4.669134295108992], [4.275, 8.15766062413412], [4.84233937586588, 8.725]], [[62.84233937586588, 47.275], [62.275, 47.84233937586588], [62.175, 51.33086570489101], [62.454594154601835, 51.54540584539816]], [[60.19600013450976, 52.849405710888405], [4.747237368890238, 53.01662263369287], [60.33086570489101, 53.175]], [[3.0, 0.0], [61.5, 2.15], [82.0, 0.0]], [[4.454594154601839, 4.454594154601839], [4.84233937586588, 8.725], [4.669134295108992, 4.175]], [[62.66913429510899, 51.825], [62.84233937586588, 47.275], [62.454594154601835, 51.54540584539816]], [[63.05, 46.5], [62.84233937586588, 47.275], [62.66913429510899, 51.825], [62.80399986549024, 52.150594289111595], [82.0, 56.0]], [[82.0, 56.0], [61.849405710888405, 53.80399986549024], [61.5, 53.85]], [[82.0, 56.0], [62.175, 53.66913429510899], [61.849405710888405, 53.80399986549024]], [[60.25276263110976, 2.983377366307129], [4.669134295108992, 2.8249999999999993], [4.803999865490242, 3.150594289111596]], [[82.0, 56.0], [62.80399986549024, 52.150594289111595], [62.85, 52.5]], [[82.0, 56.0], [62.45459415460184, 53.45459415460184], [62.175, 53.66913429510899]], [[62.01662263369287, 2.2527626311097633], [82.0, 0.0], [61.5, 2.15]], [[82.0, 56.0], [62.66913429510899, 53.175], [62.45459415460184, 53.45459415460184]], [[62.454594154601835, 2.5454058453981605], [82.0, 0.0], [62.01662263369287, 2.2527626311097633]], [[82.0, 56.0], [62.80399986549024, 52.849405710888405], [62.66913429510899, 53.175]], [[62.74723736889024, 2.9833773663071277], [82.0, 0.0], [62.454594154601835, 2.5454058453981605]], [[82.0, 56.0], [62.85, 52.5], [62.80399986549024, 52.849405710888405]], [[62.85, 3.5], [82.0, 0.0], [62.74723736889024, 2.9833773663071277]]], 'total_area_mm2': 4714.658413209734, 'holes_open': True}
# Executed inside Blender after the shared architectural helpers.
from mathutils import Matrix
import json
SOURCE=(.55,.20);SOURCE_YAW=math.radians(0)
FIXTURE=(.49,-.17);OUTPUT=(.77,-.29)
BOARD_BOTTOM=.835;FIXTURE_BOTTOM=.837;OUTPUT_BOTTOM=.835
BOARD_THICKNESS=.001276
MASS=.046 # authored estimate; no physical product weighing was performed

clear=mat('Invisible compound collision envelope',(0,0,0))
clear.node_tree.nodes.get('Principled BSDF').inputs['Alpha'].default_value=0
clear.surface_render_method='DITHERED'
pcb_green=mat('ESD board stock green',(.015,.21,.065),.05,.47)
yellow=mat('Production aisle yellow',(.65,.43,.025),.1,.53)
fixture_blue=mat('Fixture anodized blue',(.022,.11,.19),.64,.33)

def pose_xy(origin,xy,angle=0):
    x,y=xy;c=math.cos(angle);s=math.sin(angle)
    return (origin[0]+c*x-s*y,origin[1]+s*x+c*y)

def board_transform(origin,z,angle):
    return Matrix.Translation((*origin,z-.00003))@Matrix.Rotation(angle,4,'Z')@Matrix.Translation((-.0425,-.028,0))

# Manufacturer CAD, in its original millimetre-derived metre geometry.
before=set(bpy.data.objects)
bpy.ops.import_scene.gltf(filepath=CAD_PATH)
cad=[o for o in bpy.data.objects if o not in before and o.type=='MESH']
T=board_transform(SOURCE,BOARD_BOTTOM,SOURCE_YAW)
for o in cad:
    o.matrix_world=T@o.matrix_world
    o.name='Raspberry Pi 5 official CAD '+o.name
    tags(o,'pcba','dynamic',MASS,'visual')
    o['pcba_source_yaw_degrees']=math.degrees(SOURCE_YAW)
    # Smooth tessellated curved faces while preserving genuine hard edges.
    # Flat normals on every CAD triangle otherwise triple the GLB vertex data.
    import bmesh
    bm=bmesh.new();bm.from_mesh(o.data)
    for face in bm.faces:face.smooth=True
    for e in bm.edges:
        e.smooth=len(e.link_faces)==2 and e.calc_face_angle(0)<math.radians(35)
    bm.to_mesh(o.data);bm.free();o.data.update()

# Photo-observed board printing, extracted by the platform from the saved
# manufacturer photograph. Geometry still comes from the licensed STEP.
photo_mat=mat('Observed Raspberry Pi PCB surface',(.015,.25,.09),.05,.53)
tex=photo_mat.node_tree.nodes.new('ShaderNodeTexImage');tex.image=bpy.data.images.load('/work/assets/pcb_top_observed.png')
photo_mat.node_tree.links.new(tex.outputs['Color'],photo_mat.node_tree.nodes.get('Principled BSDF').inputs['Base Color'])
for o in cad:
    back=T.inverted()@o.matrix_world
    materials=[m.name for m in o.data.materials]
    if any(n.startswith('Manufacturer material 23') for n in materials):
        o.data.materials.append(photo_mat);mi=len(o.data.materials)-1
        uv=o.data.uv_layers.new(name='Manufacturer photo XY')
        for face in o.data.polygons:
            points=[back@o.data.vertices[o.data.loops[li].vertex_index].co for li in face.loop_indices]
            if min(p.z for p in points)>.00130:face.material_index=mi
            for li,p in zip(face.loop_indices,points):uv.data[li].uv=(p.x/.085,p.y/.056)
    # The supplier's STEP uses a gold display color on one complete USB shell.
    # Match that shell's steel appearance to the saved product photograph.
    o.data.materials.append(steel);mi=len(o.data.materials)-1
    for face in o.data.polygons:
        p=back@face.center
        if .0703<p.x<.088 and .0397<p.y<.0543 and -.0002<p.z<.0175:
            old=o.data.materials[face.material_index]
            c=old.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value
            if old.name.startswith('Manufacturer material 7'):face.material_index=mi

def prism(n,xy,z0,z1,material,body,motion='static',mass=1,T=None):
    verts=[(x,y,z) for z in (z0,z1) for x,y in xy];N=len(xy)
    faces=[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)]
    me=bpy.data.meshes.new(n);me.from_pydata(verts,[],faces);me.update()
    o=bpy.data.objects.new(n,me);scene.collection.objects.link(o)
    # Explicit outward normals are needed for convexity and consistent inertia.
    bpy.context.view_layer.objects.active=o;o.select_set(True)
    bpy.ops.object.mode_set(mode='EDIT');bpy.ops.mesh.select_all(action='SELECT');bpy.ops.mesh.normals_make_consistent(inside=False);bpy.ops.object.mode_set(mode='OBJECT');o.select_set(False)
    if T is not None:o.matrix_world=T
    return finish(o,n,material,body,motion,mass,'solid')

for i,polygon in enumerate(COLLISION_PROFILE['pieces_mm']):
    prism('PCB solid sector %03d'%i,[(x*.001,y*.001) for x,y in polygon],.00003,.001306,clear,'pcba','dynamic',MASS,T)

# CAD-bounded rigid envelopes for populated protrusions; the exact CAD supplies
# appearance. Small solder joints, flexible connectors and board flex are omitted.
envelopes=[('Ethernet', (0.07725, 0.01025, 0.006986), (0.0217, 0.01603, 0.0154)), ('USB stack 1', (0.079235, 0.029000105, 0.008681), (0.01763, 0.0146, 0.01779)), ('USB stack 2', (0.0790549992, 0.0469996806, 0.0085975108), (0.01755, 0.0146, 0.0176269799)), ('GPIO housing', (0.0325, 0.0525, 0.002606), (0.0509, 0.00518, 0.00264)), ('HDMI 1', (0.0258, 0.00261965, 0.0024587233), (0.0072928932, 0.0086, 0.0045464466)), ('HDMI 2', (0.0392, 0.00261965, 0.0024587233), (0.0072928932, 0.0086, 0.0045464466)), ('USB C', (0.0112, 0.002475, 0.002466), (0.00904, 0.00745, 0.00436)), ('CPU', (0.03315, 0.02275, 0.00262), (0.0171, 0.0171, 0.002168)), ('RP1', (0.0585, 0.0351, 0.002041), (0.0121, 0.0121, 0.00109)), ('RAM', (0.03315, 0.0392, 0.001976), (0.0146, 0.0101, 0.00082)), ('Radio shield', (0.0123, 0.0427, 0.002186), (0.0109, 0.0131, 0.0018)), ('Ribbon connector 1', (0.055205, 0.0085, 0.003361), (0.0024, 0.0161, 0.00395)), ('Ribbon connector 2', (0.049005, 0.0085, 0.003361), (0.0024, 0.0161, 0.00395)), ('Ribbon connector 3', (0.003005, 0.03, 0.003361), (0.0024, 0.0131, 0.00395)), ('Fan connector', (0.06675, 0.052, 0.003511), (0.003, 0.0061, 0.00435)), ('POE header', (0.0615, 0.0095, 0.002606), (0.00518, 0.00518, 0.00264)), ('GPIO pins', (0.0325, 0.0525, 0.0065), (0.0508, 0.00254, 0.006))]

for name,loc,size in envelopes:
    o=box('PCB component envelope '+name,loc,size,clear,body='pcba',motion='dynamic',mass=MASS,collision='solid')
    o.matrix_world=T@o.matrix_world

HOLES=[(-.039,-.0245),(.019,.0245),(-.039,.0245),(.019,-.0245)]
def nest(name,origin,top,angle=0):
    x,y=origin
    o=box(name+' base',(x,y,.767),(.14,.13,.018),edge,.004,body=name+' base',collision='solid',mass=.7)
    o.rotation_euler.z=angle
    for i,p in enumerate(HOLES):
        px,py=pose_xy(origin,p,angle)
        cyl(name+' support %d'%i,(px,py,(.778+top)/2),.0028,top-.778,black,body=name+' support %d'%i,collision='solid',mass=.02)
        ring(name+' support brass collar %d'%i,(px,py,top-.008),.004,.0037,.002,gold)
    for dx in [-.059,.059]:
        for dy in [-.049,.049]:
            px,py=pose_xy(origin,(dx,dy),angle);screw(name+' fastener '+str((dx,dy)),px,py,.778,.003)
    for dy in [-.056,.056]:
        for dx in [-.044,.044]:
            px,py=pose_xy(origin,(dx,dy),angle)
            o=box(name+' flush corner '+str((dx,dy)),(px,py,.7762),(.014,.001,.0003),turq_light,body=name+' graphics');o.rotation_euler.z=angle

nest('Input nest',SOURCE,BOARD_BOTTOM,SOURCE_YAW)
nest('Output nest',OUTPUT,OUTPUT_BOTTOM,0)

cx,cy=FIXTURE
box('Functional test base chassis',(cx,cy,.756),(.30,.27,.030),ivory,.006,body='fixture_chassis',collision='solid',mass=4)
box('Replaceable anodized tooling plate',(cx,cy,.779),(.276,.246,.016),fixture_blue,.002,body='fixture_plate',collision='solid',mass=2)
box('Fixture front fascia',(cx,cy-.137,.756),(.284,.007,.025),turq,.002,body='Fixture graphic fascia')
for dx in [-.121,.121]:
    for dy in [-.105,.105]:screw('Tooling plate captive screw '+str((dx,dy)),cx+dx,cy+dy,.788,.004)

# Genuine support contacts and two hollow-hole mating features.
for i,(dx,dy) in enumerate(HOLES):
    px,py=cx+dx,cy+dy
    cyl('fixture_support_%d'%i,(px,py,(.788+FIXTURE_BOTTOM)/2),.0028,FIXTURE_BOTTOM-.788,black,body='fixture_support_%d'%i,collision='solid',mass=.015)
    cyl('Support steel base %d'%i,(px,py,.791),.006,.006,steel,body='Support hardware')
    ring('Support engraved collar %d'%i,(px,py,.827),.00375,.0035,.002,alu)
pin_axis=math.atan2(.049,.058)
for i,(dx,dy) in enumerate(HOLES[:2]):
    px,py=cx+dx,cy+dy
    if i==0:
        xy=[(px+.001*math.cos(2*math.pi*k/32),py+.001*math.sin(2*math.pi*k/32)) for k in range(32)]
    else:
        xy=[pose_xy((px,py),(.0006*math.cos(2*math.pi*k/32),.001*math.sin(2*math.pi*k/32)),pin_axis) for k in range(32)]
    prism('fixture_locator_%d'%i,xy,FIXTURE_BOTTOM-.001,FIXTURE_BOTTOM+.003,steel,'fixture_locator_%d'%i,mass=.001)
    # A finite lead-in chamfer, built separately; both pieces belong to the same
    # anchored pin body. The cylindrical/relieved register remains full diameter.
    v=[(x,y,FIXTURE_BOTTOM+.003) for x,y in xy]+[(px+(x-px)*.3,py+(y-py)*.3,FIXTURE_BOTTOM+.004) for x,y in xy]
    N=len(xy);faces=[tuple(range(N-1,-1,-1)),tuple(range(N,2*N))]+[(k,(k+1)%N,(k+1)%N+N,k+N) for k in range(N)]
    me=bpy.data.meshes.new('Pin lead-in');me.from_pydata(v,[],faces);me.update();o=bpy.data.objects.new('Locator %d lead-in'%i,me);scene.collection.objects.link(o)
    finish(o,o.name,steel,'fixture_locator_%d'%i,mass=.001,collision='solid')

# The electrical probe cassette is scenery: no fabricated continuity reading.
box('Probe cassette',(cx-.005,cy,.799),(.074,.039,.018),black,.001,body='Probe cassette',collision='solid',mass=.15)
for i in range(4):
    for j in range(6):
        px,py=cx-.032+j*.010,cy-.014+i*.009
        cyl('Probe barrel %d %d'%(i,j),(px,py,.819),.0008,.023,gold,body='Retracted probe appearance',vertices=10)
        cyl('Probe tip %d %d'%(i,j),(px,py,.833),.00035,.005,steel,body='Retracted probe appearance',vertices=10)

# Side-opening clamshell stays open during the mechanical handling demo.
hx=cx-.15
for dy in [-.104,.104]:
    cyl('Hood hinge '+str(dy),(hx,cy+dy,.805),.011,.026,steel,'Y',body='Hood hinge')
    box('Hood foot '+str(dy),(hx+.01,cy+dy,.786),(.044,.026,.022),edge,.003,body='Hood frame',collision='solid')
box('Open hood shell',(hx-.018,cy,.916),(.018,.254,.228),ivory,.008,body='open_fixture_hood',collision='solid',mass=2)
box('Open hood inner plate',(hx-.007,cy,.916),(.007,.229,.200),fixture_blue,.002,body='Hood detail')
for dy in [-.095,.095]:
    rod('Hood pressure rail '+str(dy),(hx+.003,cy+dy,.839),(hx+.003,cy+dy,.991),.004,steel,'Hood detail')
for dy in [-.068,0,.068]:
    for z in [.867,.931,.979]:
        cyl('Retracted pressure foot '+str((dy,z)),(hx+.01,cy+dy,z),.006,.014,black,'X',body='Hood pressure tools')
rod('Hood handle',(hx-.037,cy-.06,1.047),(hx-.037,cy+.06,1.047),.009,black,'Hood handle')
for dy in [-.06,.06]:rod('Handle standoff '+str(dy),(hx-.018,cy+dy,1.02),(hx-.037,cy+dy,1.047),.006,steel)
for dy in [-.121,.121]:
    rod('Hood stay cylinder '+str(dy),(hx+.04,cy+dy,.78),(hx-.015,cy+dy,.916),.006,edge)
    rod('Hood stay rod '+str(dy),(hx-.015,cy+dy,.916),(hx-.018,cy+dy,.969),.0025,steel)
cable('Test harness loom',[(cx+.09,cy+.12,.76),(cx+.13,cy+.18,.756),(.87,.43,.75),(1.23,.51,.66)],.012)
for i in range(4):cable('Harness conductor %d'%i,[(cx+.028+i*.012,cy+.03,.80),(cx+.061+i*.012,cy+.08,.788),(cx+.09,cy+.12,.78)],.0015,red if i%2 else blue)

for name,label,origin in [('Source','01 / INPUT',SOURCE),('Fixture','02 / LOCATE',FIXTURE),('Output','03 / OUTPUT',OUTPUT)]:
    x,y=origin
    box(name+' station label',(x,y-.092,.741),(.106,.026,.0016),black,.001,body='Station labels')
    text_obj(name+' label',label,(x-.048,y-.096,.742),.0085,white_ink)

# Rich production surroundings: electrostatic storage, barcode hardware and
# guarded-cell details taken from the photographed commercial installation.
for j in range(2):
    bx,by=.38+j*.36,.48
    box('ESD bin floor %d'%j,(bx,by,.749),(.27,.19,.018),black,.003,body='ESD stock display')
    for dx in [-.132,.132]:box('ESD bin side %d %.2f'%(j,dx),(bx+dx,by,.795),(.006,.19,.09),black,.002,body='ESD stock display')
    for dy in [-.091,.091]:box('ESD bin rim %d %.2f'%(j,dy),(bx,by+dy,.795),(.258,.008,.09),black,.002,body='ESD stock display')
    for k in range(3):
        bx2=bx-.08+k*.08
        box('Fixed stock PCB %d %d'%(j,k),(bx2,by,.822),(.061,.090,.0013),pcb_green,body='ESD stock display')
        for q in range(3):box('Stock IC %d %d %d'%(j,k,q),(bx2-.016+q*.016,by,.825),(.010,.014,.004),black,.0004,body='ESD stock display')
        for q in range(2):box('Stock port %d %d %d'%(j,k,q),(bx2-.015+q*.029,by+.03,.831),(.020,.016,.015),steel,.001,body='ESD stock display')
    box('ESD stock placard %d'%j,(bx,by-.096,.79),(.15,.002,.024),ivory,body='ESD stock labels')

# Reference-style yellow perimeter marks and sparse black safety mesh, outside
# the robot/camera working envelope. Architecture is reused and adapted.
for x in [-.55,1.75]:box('Production aisle tape '+str(x),(x,-.05,.0015),(.032,2.3,.001),yellow,body='Floor graphics')
for x in [-.72,1.94]:
    box('Safety fence post '+str(x),(x,.78,.80),(.035,.035,1.6),yellow,.001,body='Cell guarding')
    for z in [.18,1.52]:rod('Fence cross member '+str((x,z)),(x,.78,z),(x,1.46,z),.009,edge,'Cell guarding')
    for i in range(10):rod('Fence wire vertical '+str((x,i)),(x,.8+i*.068,.20),(x,.8+i*.068,1.5),.0012,edge,'Cell mesh')
    for i in range(17):rod('Fence wire horizontal '+str((x,i)),(x,.79,.22+i*.075),(x,1.46,.22+i*.075),.0012,edge,'Cell mesh')

# Rear workbench at y 1.62, outside the task reach and camera corridor.
box('Rear bench surface',(.20,1.67,.835),(3.24,.65,.04),worktop,.004,body='Rear bench',mass=130)
for x in [-1.07,-.22,.64,1.47]:
    box('Rear cabinet shell '+str(x),(x,1.70,.426),(.66,.56,.75),ivory,.007,body='Rear storage cabinets')
    box('Rear cabinet toe '+str(x),(x,1.68,.06),(.59,.51,.095),black,.002,body='Rear storage cabinets')
    for z in [.23,.46,.69]:
        box('Rear cabinet drawer %.2f %.2f'%(x,z),(x,1.408,z),(.623,.022,.208),turq if x<.5 else ivory,.003,body='Rear storage cabinets')
        handle('Rear cabinet pull %.2f %.2f'%(x,z),x,1.388,z+.053,.18,'Rear storage cabinets')
# Fixed reference-style inspection fixtures: shaped cheeks, trays, brackets and handles.
def inspection_fixture(n,x,y):
    b=n
    z=.855
    box(n+' turquoise base',(x,y,z+.063),(.53,.43,.126),turq,.009,body=b)
    box(n+' graphite deck',(x,y,z+.137),(.555,.454,.022),black,.006,body=b)
    box(n+' insert floor',(x,y+.025,z+.164),(.39,.31,.028),alu,.002,body=b)
    box(n+' recessed insert',(x,y+.018,z+.182),(.35,.265,.008),black,.002,body=b)
    for dx in [-.195,.195]:
        profile_plate(n+' shaped cheek '+str(dx),x+dx,y,z+.155,[(-.175,0),(-.165,.058),(.105,.20),(.173,.20),(.178,0)],.016,black,b)
        rod(n+' pull bracket '+str(dx),(x+dx,y-.16,z+.206),(x+dx,y-.275,z+.138),.012,alu,b)
        cyl(n+' cheek pivot '+str(dx),(x+dx,y+.117,z+.29),.020,.025,alu,'X',body=b)
        for yy in [-.12,.08]:cyl(n+' side rivet %.2f %.2f'%(dx,yy),(x+dx+(.009 if dx>0 else -.009),y+yy,z+.187),.005,.004,steel,'X',body=b,vertices=12)
    rod(n+' front cross handle',(x-.195,y-.275,z+.138),(x+.195,y-.275,z+.138),.013,alu,b)
    box(n+' rear bridge',(x,y+.16,z+.314),(.386,.022,.031),alu,.002,body=b)
    for dx in [-.12,0,.12]:
        for dy in [-.07,.04]:
            box(n+' pocket %.2f %.2f'%(dx,dy),(x+dx,y+dy,z+.188),(.078,.073,.006),edge,.003,body=b)
            ring(n+' inspection nest %.2f %.2f'%(dx,dy),(x+dx,y+dy,z+.197),.018,.012,.014,black,b)
    handle(n+' case carry handle',x,y-.22,z+.063,.15,b)
    for dx in [-.235,.235]:
        for dy in [-.18,.18]:screw(n+' deck screw %.2f %.2f'%(dx,dy),x+dx,y+dy,z+.15,.004,body=b)
    cyl(n+' connector',(x+.15,y-.221,z+.063),.018,.011,black,'Y',body=b)
    cyl(n+' small status',(x+.21,y-.223,z+.063),.004,.012,turq_light,'Y',body=b)
inspection_fixture('Rear inspection fixture A',-.99,1.65)
inspection_fixture('Rear inspection fixture B',-.32,1.65)
# Oscilloscope with recessed display, controls and front ports.
box('Oscilloscope chassis',(.40,1.66,.970),(.37,.24,.23),ivory,.02,body='Oscilloscope')
box('Oscilloscope bezel',(.35,1.532,.987),(.25,.009,.171),black,.009,body='Oscilloscope')
box('Oscilloscope display',(.35,1.526,.989),(.215,.002,.137),screen,.004,body='Oscilloscope')
for i in range(5):box('Scope grid horizontal '+str(i),(.35,1.524,.941+i*.024),(.20,.001,.0007),edge,body='Oscilloscope')
for i in range(9):box('Scope grid vertical '+str(i),(.254+i*.024,1.524,.989),(.0007,.001,.124),edge,body='Oscilloscope')
pts=[(.25+i*.005,1.522,.990+.025*math.sin(i*.44)) for i in range(40)]
for a,b in zip(pts,pts[1:]):rod('Scope trace',a,b,.0009,screen_ink,'Oscilloscope')
for z in [.94,1.005,1.064]:
    cyl('Scope knob '+str(z),(.54,1.521,z),.017,.014,edge,'Y',body='Oscilloscope')
for x in [.285,.355,.425]:
    cyl('Scope BNC '+str(x),(x,1.522,.89),.009,.014,steel,'Y',body='Oscilloscope')
    cyl('Scope BNC centre '+str(x),(x,1.513,.89),.004,.002,black,'Y',body='Oscilloscope')
cable('Oscilloscope probe',[(.285,1.512,.89),(.18,1.36,.871),(.16,1.26,.87),(.37,1.24,.87),(.45,1.32,.87)],.003,body='Oscilloscope')
# Monitor, subdued work diagram, keyboard.
box('Monitor foot',(1.14,1.76,.863),(.25,.19,.018),black,.008,body='Operator monitor')
box('Monitor column',(1.14,1.81,1.047),(.055,.055,.35),edge,.005,body='Operator monitor')
box('Monitor surround',(1.14,1.79,1.20),(.53,.036,.324),black,.01,body='Operator monitor')
box('Monitor screen',(1.14,1.769,1.205),(.489,.003,.282),screen,.002,body='Operator monitor')
box('Diagram title rule',(1.14,1.766,1.302),(.43,.001,.009),edge,body='Operator monitor')
text_obj('Monitor header','MODULE INSPECTION',( .924,1.764,1.317),.016,white_ink,(math.pi/2,0,0),body='Operator monitor')
for i,x in enumerate([.98,1.13,1.28]):
    box('Task diagram tile '+str(i),(x,1.765,1.202),(.087,.001,.070),edge,.004,body='Operator monitor')
    box('Task diagram inset '+str(i),(x,1.763,1.202),(.037,.001,.036),turq_light if i!=0 else ochre,body='Operator monitor')
for x in [1.055,1.205]:box('Task diagram connector '+str(x),(x,1.764,1.20),(.05,.001,.003),screen_ink,body='Operator monitor')
for i in range(3):box('Monitor quiet log '+str(i),(1.093,1.764,1.10+i*.014),(.33-i*.04,.001,.003),edge,body='Operator monitor')
box('Keyboard body',(1.15,1.398,.873),(.40,.145,.025),black,.008,body='Operator keyboard')
for row in range(4):
    for col in range(13):
        box('Keyboard key %02d %02d'%(row,col),(.974+col*.028,1.349+row*.029,.889),(.023,.024,.009),edge,.002,body='Operator keyboard')
box('Keyboard spacebar',(1.15,1.325,.889),(.132,.02,.008),edge,.002,body='Operator keyboard')
box('Operator mouse',(1.435,1.40,.875),(.053,.088,.028),black,.019,body='Operator mouse')
cable('Keyboard lead',[(1.15,1.466,.88),(1.30,1.52,.86),(1.39,1.72,.86),(1.25,1.85,.9)],.003)
# Two articulated-looking but static task-lamp arms.
cyl('Task lamp clamp',(1.83,1.91,.885),.041,.06,edge,body='Task lamp')
rod('Task lamp lower arm',(1.83,1.91,.905),(1.87,1.86,1.36),.014,alu,'Task lamp')
rod('Task lamp upper arm',(1.87,1.86,1.36),(1.59,1.63,1.49),.013,alu,'Task lamp')
for p in [(1.83,1.91,.925),(1.87,1.86,1.36),(1.59,1.63,1.49)]:cyl('Lamp joint',p,.026,.033,black,'Y',body='Task lamp')
box('Task lamp head',(1.58,1.59,1.467),(.245,.08,.033),edge,.012,body='Task lamp')
box('Task lamp lens',(1.58,1.59,1.448),(.219,.065,.006),lightmat,.006,body='Task lamp')
cable('Lamp cable',[(1.84,1.92,.88),(1.90,1.89,1.37),(1.62,1.65,1.50)],.003)
# Service outlets above rear workbench.
box('Rear electrical raceway',(.12,2.09,1.23),(2.98,.045,.10),ivory,.004,body='Rear electrical')
for i,x in enumerate([-.98,-.62,-.26,.12,.48,.82,1.2]):
    box('Rear grounded outlet '+str(i),(x,2.061,1.23),(.087,.012,.071),edge,.004,body='Rear electrical')
    for dx in [-.016,.016]:
        cyl('Rear outlet opening %d %.2f'%(i,dx),(x+dx,2.053,1.23),.006,.002,black,'Y',body='Rear electrical')
for i,x in enumerate([-.98,-.32,.40,1.14]):
    cable('Rear fixture loom '+str(i),[(x,1.83,.89),(x+.03,1.99,.93),(x+.06,2.03,1.14)],.009)
# Compact parts shelving on left side, facing into room.
for y in [.55,1.9]:
    for x in [-1.58,-1.10]:box('Parts shelving post %.2f %.2f'%(x,y),(x,y,1.22),(.035,.035,2.25),alu,.002,body='Parts shelving')
for z in [.21,.65,1.10,1.55,2.04]:
    box('Parts shelf '+str(z),(-1.34,1.225,z),(.53,1.43,.027),ivory,.002,body='Parts shelving')
    box('Parts shelf graphite lip '+str(z),(-1.065,1.225,z+.015),(.012,1.43,.06),edge,.001,body='Parts shelving')
# Hollow ESD bins: open tops, scooped front outline.
for row,z in enumerate([.67,1.12,1.57]):
    for col,y in enumerate([.76,1.20,1.64]):
        n='ESD bin %d %d'%(row,col);x=-1.33;b='Fixed shelf stock'
        box(n+' bottom',(x,y,z+.008),(.40,.35,.016),black,.003,body=b)
        box(n+' rear',(x-.191,y,z+.103),(.018,.35,.19),black,.004,body=b)
        for dy in [-.166,.166]:box(n+' side '+str(dy),(x,y+dy,z+.09),(.4,.018,.16),black,.003,body=b)
        box(n+' low open front',(x+.191,y,z+.040),(.018,.318,.065),black,.003,body=b)
        box(n+' label',(x+.202,y,z+.038),(.003,.105,.028),white_ink,.001,body=b)
        for k in range(2):box(n+' wrapped component '+str(k),(x-.05+k*.13,y,z+.025),(.09,.18,.028),edge,.002,body=b)
# Antistatic tray stacks at rear right, shallow real perimeter lips.
for stack,x in enumerate([1.85,2.08]):
    for k in range(5):
        z=.86+k*.016;n='ESD tray stack %d %d'%(stack,k)
        box(n+' base',(x,1.50,z),(.196,.24,.005),black,.002,body='Fixed tray stacks')
        for dx in [-.096,.096]:box(n+' lip X '+str(dx),(x+dx,1.50,z+.007),(.005,.24,.014),edge,.001,body='Fixed tray stacks')
        for dy in [-.117,.117]:box(n+' lip Y '+str(dy),(x,1.50+dy,z+.007),(.187,.005,.014),edge,.001,body='Fixed tray stacks')
# Elevated wall storage and useful labels, not advertisements.
for x in [-1.13,-.52]:
    box('Overhead cabinet '+str(x),(x,2.00,1.935),(.565,.245,.58),ivory,.006,body='Overhead cabinets')
    box('Overhead cabinet door '+str(x),(x,1.867,1.935),(.543,.018,.553),wall,.003,body='Overhead cabinets')
    handle('Overhead cabinet pull '+str(x),x,1.849,1.739,.13,'Overhead cabinets')
text_obj('Room small identity','PCBA TEST ENGINEERING',(-1.60,2.132,2.54),.055,edge,(math.pi/2,0,0))
text_obj('Bench ESD label','ESD PROTECTED',(.25,-.454,.613),.018,white_ink,(math.pi/2,0,0))
# Earth point on front cabinet, with restrained green/yellow grounding indication.
cyl('Earth stud',(.63,-.459,.25),.009,.006,gold,'Y',body='Bench electrical')
box('Ground label',(.625,-.461,.285),(.04,.002,.022),white_ink,body='Grounded identification')
text_obj('Ground symbol','PE',(.615,-.463,.28),.011,black,(math.pi/2,0,0))

# Refined circulation: shelving moved forward, rear bench ends before the glazed door.
for o in list(scene.objects):
    b=o.get('robotgym_body')
    if b in ['Parts shelving','Fixed shelf stock']:o.location.y-=.90
    if b=='Task lamp' or o.name=='Lamp cable':o.location.x-=.10
    if o.name.startswith('ESD tray stack 0 '):o.location+=Vector((-3.19,-1.15,-.634))
    if o.name.startswith('ESD tray stack 1 '):o.location+=Vector((-3.42,-.72,-.634))
# Door transom and kick plate form a clearly framed static glazed door.
box('Door threshold',(2.42,2.132,.012),(.87,.075,.024),alu,.001,body='Glazed service door')
box('Door upper transom',(2.42,2.129,2.43),(.87,.055,.036),alu,body='Glazed service door')
# Merge cosmetic duplicates to keep rendering/physics ingestion efficient, but preserve task bodies.
def combine_visual(body):
    obs=[o for o in scene.objects if o.type=='MESH' and o.get('robotgym_body')==body and o.get('robotgym_collision')=='visual']
    if len(obs)<2:return
    bpy.ops.object.select_all(action='DESELECT')
    for o in obs:o.select_set(True)
    bpy.context.view_layer.objects.active=obs[0]
    bpy.ops.object.join();bpy.context.object.name=body+' visual assembly'
for b in ['Floor joints','Wall panel seams','Extrusion channels','Fixture fasteners','Test plate engraved grid','Plate fiducials','Controller details','Grounded identification']:
    combine_visual(b)
# Verify exactly one dynamic assembly and all authored annotations.
for o in scene.objects:
    if o.type=='MESH':
        assert all(k in o for k in ['robotgym_body','robotgym_motion','robotgym_mass_kg','robotgym_collision']),o.name
dynamic={o['robotgym_body'] for o in scene.objects if o.type=='MESH' and o['robotgym_motion']=='dynamic'}
assert dynamic=={'pcba'},dynamic

def area(n,loc,target,power,color,size,size_y=None):
    d=bpy.data.lights.new(n,'AREA');d.energy=power;d.color=color
    if size_y:d.shape='RECTANGLE';d.size=size;d.size_y=size_y
    else:d.shape='RECTANGLE';d.size=size;d.size_y=size*.55
    o=bpy.data.objects.new(n,d);bpy.context.collection.objects.link(o);o.location=loc;o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler()
area('Large front softbox',(1.5,-2.7,3.6),(.3,.55,.8),250,(.94,.97,1),3.2)
area('Left ceiling illumination',(-.95,.24,2.59),(-.8,.24,0),72,(.93,.97,1),.25,2.35)
area('Central ceiling illumination',(.58,.24,2.59),(.58,.24,0),82,(.95,.98,1),.25,2.35)
area('Right ceiling illumination',(2.1,.24,2.59),(1.9,.24,0),72,(.94,.97,1),.25,2.35)
area('Rear warm-neutral bounce',(.5,1.7,2.25),(.5,.3,.7),55,(1,.97,.93),2.0)
scene.world.color=(.2,.2,.2)
scene.world.use_nodes=True;scene.world.node_tree.nodes['Background'].inputs['Color'].default_value=(.60,.69,.78,1)
scene.world.node_tree.nodes['Background'].inputs['Strength'].default_value=.18
def camera(n,loc,target,lens):
    d=bpy.data.cameras.new(n);o=bpy.data.objects.new(n,d);bpy.context.collection.objects.link(o);o.location=loc
    o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler();d.lens=lens;d.clip_start=.01;d.clip_end=100
    return o
hero=camera('Hero camera',(5.4,-6.1,4.20),(.43,.40,1.12),46)
close=camera('Workbench camera',(1.90,-1.88,1.65),(.66,.07,.73),54)
scene.camera=hero
scene.render.resolution_x=1500;scene.render.resolution_y=1125;scene.render.resolution_percentage=100
scene.view_settings.view_transform='AgX'
scene.view_settings.look='AgX - Medium High Contrast'
scene.view_settings.exposure=-.25
scene.render.image_settings.file_format='PNG'

# Precision collision revision: preserve explicit finite sectors below 0.5 mm.

scene["demo_provenance"]="BTL/KINALI source photos + Raspberry Pi MIT STEP + engineering fixture design + adapted earlier RobotGym lab architecture"
