{
  "units": "mm",
  "project": "me-devin/esp32-x86-pc-5u1t",
  "native_export_url": "https://cdn.flux.ai/document_assets/esp32-x86-pc-5u1t-e87eb1fbef9eeafe446736165be468d2.step",
  "original_electronics_reference_url": "https://cdn.flux.ai/document_assets/current-pcb-components-reference-887a035f-2b9f-4a95-8371-b2b538ad179c.stpz",
  "source_to_native_translation_mm": [
    46,
    -60,
    1.38436
  ],
  "files": {
    "base.build123d": "from build123d import *\n# Hinged refinement of esp32-retro-enclosure-base. Native STEP origin maps\n# from this scene frame by (+46,-60,+1.38436) mm. PCB solid underside is\n# -1.38436, NOT the earlier -1.30 assumption. Electronics are unchanged.\n# Prototype FDM: gray PETG, 0.4 mm nozzle, provisional 0.2 mm layers.\nBOARD_X, BOARD_Y = 72.0,100.0\nINNER_X, INNER_Y, WALL = 74.0,102.0,3.0\nOUTER_X, OUTER_Y, CORNER_R = 80.0,108.0,3.0\nFLOOR_TOP, FLOOR_T = -6.5,2.4\nSEAT_Z, PCB_TOP = -1.38436,-0.085\nWALL_TOP, LID_BOTTOM = 16.0,16.3\nHINGE_Y, HINGE_Z = 56.0,17.5\nBARREL_R, BORE_R, AXIAL_GAP = 3.5,1.7,0.3\nBASE_KNUCKLES=[(-31.0,-19.0),(-6.0,6.0),(19.0,31.0)]\nLID_KNUCKLES=[(-18.7,-6.3),(6.3,18.7)]\nMOUNTS=[(-33,-47),(33,-47),(33,47),(-33,47)]\nAUX=[(-25,0),(25,0),(0,-40),(0,0),(0,40)]\nINSERT_OD,INSERT_BORE,INSERT_DEPTH=3.63,3.20,4.18\n# Open-to-seam slots give the unchanged board a vertical installation path.\n# Values are actual transformed native connector bounding envelopes plus\n# conservative mating/finger space; plug-specific overmolds remain provisional.\nLEFT_PORTS=[(28.5878,36.0,-3.9),(-1.4122,10.0,-3.9),(-17.505,16.0,-3.9),(-35.005,16.0,-3.9)]\nRIGHT_PORTS=[(-27.893,18.0,-1.2),(31.833,16.0,-3.9),(15.756,9.5,-3.9),(5.11,9.5,-3.9),(-9.485,10.0,-3.9)]\ndef prism(w,d,h,r,z):\n    return Pos(0,0,z)*extrude(RectangleRounded(w,d,r),h)\ndef cylx(r,a,b):\n    return Pos((a+b)/2,HINGE_Y,HINGE_Z)*Rot(0,90,0)*Cylinder(r,b-a)\nbase=prism(OUTER_X,OUTER_Y,WALL_TOP-FLOOR_TOP+FLOOR_T,CORNER_R,FLOOR_TOP-FLOOR_T)\nbase-=Pos(0,0,(FLOOR_TOP+WALL_TOP+1)/2)*Box(INNER_X,INNER_Y,WALL_TOP+1-FLOOR_TOP)\n# 0.4 mm bed-side chamfer, selected by the bottom horizontal edges.\nbottom_edges=[e for e in base.edges() if abs(e.bounding_box().min.Z-(FLOOR_TOP-FLOOR_T))<1e-5 and abs(e.bounding_box().max.Z-(FLOOR_TOP-FLOOR_T))<1e-5]\nbase=chamfer(bottom_edges,0.4)\nfor side,ports in [(-1,LEFT_PORTS),(1,RIGHT_PORTS)]:\n    for y,w,z in ports:\n        base-=Pos(side*40,y,(z+18)/2)*Box(12,w,18-z)\n# Four heat-set retention bosses. M2 is named by the existing mounting-hole\n# part; ISO1478 ST2.2 does not fit a 2.2 mm clearance hole. SPIROL29 short.\nfor x,y in MOUNTS:\n    h=SEAT_Z-FLOOR_TOP\n    boss=Pos(x,y,FLOOR_TOP+h/2)*Cylinder(3.315,h)\n    # tapered top from lower OD6.63 to OD6.03, preserving 1.2 radial insert wall\n    boss-=Pos(x,y,SEAT_Z-1)*Cylinder(3.4,2)\n    boss+=Pos(x,y,SEAT_Z-1)*Cone(3.315,3.015,2)\n    base+=boss\n# Root transitions are smooth concave fillets at floor/boss intersections.\nroots=[e for e in base.edges() if e.geom_type==GeomType.CIRCLE and abs(e.bounding_box().min.Z-FLOOR_TOP)<1e-5 and abs(e.bounding_box().max.Z-FLOOR_TOP)<1e-5]\nif roots: base=fillet(roots,2.0)\nfor x,y in AUX:\n    h=SEAT_Z-FLOOR_TOP\n    p=Pos(x,y,FLOOR_TOP+(h-2)/2)*Cylinder(2.0,h-2)\n    p+=Pos(x,y,SEAT_Z-1)*Cone(2.0,1.5,2)\n    base+=p\nroots=[e for e in base.edges() if e.geom_type==GeomType.CIRCLE and abs(e.bounding_box().min.Z-FLOOR_TOP)<1e-5 and abs(e.bounding_box().max.Z-FLOOR_TOP)<1e-5 and abs(e.radius-2)<1e-4]\nif roots: base=fillet(roots,2.0)\nfor x,y in MOUNTS:\n    base-=Pos(x,y,SEAT_Z-INSERT_DEPTH/2)*Cylinder(INSERT_BORE/2,INSERT_DEPTH)\n# Alternating attached knuckles. Cylindrical reliefs clear the moving knuckles.\nfor a,b in LID_KNUCKLES: base-=cylx(BARREL_R+AXIAL_GAP,a-AXIAL_GAP,b+AXIAL_GAP)\nfor a,b in BASE_KNUCKLES:\n    web=Pos((a+b)/2,53.5,13.0)*Box(b-a,6.0,6.0)\n    web=fillet(web.edges().filter_by(Axis.X),1.0)\n    base+=web+cylx(BARREL_R,a,b)\nbase-=cylx(BORE_R,-32,32)\n# Two small seating lands; intentional contact with lid at z16.3.\nfor x in [-25,25]: base+=Pos(x,-52.5,15.65)*Box(6,3,1.3)\nif not base.is_valid or len(base.solids())!=1: reject('Base must remain a single valid connected solid')\nresult=base\nresult.label='Hinged Base'\nresult.color=Color('#9a9a9a')\n",
    "lid.build123d": "from build123d import *\n# Source frame is fixed project PCB scene frame; native STEP offset +46,-60,+1.38436.\n# Gray PETG, 0.4 nozzle; 0.2 layer provisional. Print broad outer face down.\nOUTER_X,OUTER_Y,CORNER_R=80.0,108.0,3.0\nLID_BOTTOM,LID_T=16.3,2.4\nHINGE_Y,HINGE_Z=56.0,17.5\nBARREL_R,BORE_R,GAP=3.5,1.7,0.3\nBASE_KNUCKLES=[(-31.0,-19.0),(-6.0,6.0),(19.0,31.0)]\nLID_KNUCKLES=[(-18.7,-6.3),(6.3,18.7)]\ndef cylx(r,a,b): return Pos((a+b)/2,HINGE_Y,HINGE_Z)*Rot(0,90,0)*Cylinder(r,b-a)\nlid=Pos(0,0,LID_BOTTOM)*extrude(RectangleRounded(OUTER_X,OUTER_Y,CORNER_R),LID_T)\n# Rear relief is invariant about the hinge axis and clears fixed knuckles.\nfor a,b in BASE_KNUCKLES: lid-=cylx(BARREL_R+GAP,a-GAP,b+GAP)\nfor a,b in LID_KNUCKLES:\n    # Plate extends to y54, cylinder reaches y52.5: positive-volume attachment.\n    lid+=cylx(BARREL_R,a,b)\nlid-=cylx(BORE_R,-32,32)\nif not lid.is_valid or len(lid.solids())!=1: reject('Lid and both alternating knuckles must be one connected valid solid')\nresult=lid\nresult.label='Hinged Lid'\nresult.color=Color('#9a9a9a')\n",
    "pin.build123d": "from build123d import *\n# Custom steel headed removable hinge pin. Machine from 3mm stock;\n# not an FDM pin. Diametral 0.4 mm clearance to 3.4mm printed bores.\nSHAFT_D, HEAD_D, X0, X1 = 3.0,6.0,-42.0,44.0\nHINGE_Y,HINGE_Z = 56.0,17.5\nHEAD_T,COTTER_X,COTTER_D = 2.0,43.0,1.2\npin=Pos((X0+X1)/2,HINGE_Y,HINGE_Z)*Rot(0,90,0)*Cylinder(SHAFT_D/2,X1-X0)\npin+=Pos(X0-HEAD_T/2,HINGE_Y,HINGE_Z)*Rot(0,90,0)*Cylinder(HEAD_D/2,HEAD_T)\npin-=Pos(COTTER_X,HINGE_Y,HINGE_Z)*Cylinder(COTTER_D/2,5)\nresult=pin\nresult.label='Removable steel pin'\nresult.color=Color('#d0d5db')\n",
    "retainer.build123d": "from build123d import *\n# Custom hand-bent 0.8 mm steel safety wire through the pin 1.2mm cross bore.\n# Straight central segment; bend ends after insertion to prevent withdrawal.\nX,Y,Z=43.0,56.0,17.5\nR=0.4\ns=Pos(X,Y,Z)*Cylinder(R,8)\ns+=Pos(X,Y,Z+3.7)*Rot(0,90,0)*Cylinder(R,3)\ns+=Pos(X,Y,Z-3.7)*Rot(0,90,0)*Cylinder(R,3)\nresult=s\nresult.color=Color('#d0d5db')\n",
    "screw.build123d": "from build123d import *\n# Simplified ISO7045/DIN7985 M2x5 pan-head envelope, dimensions from\n# https://www.westfieldfasteners.co.uk/Bolts-Screws-Metric/Machine-Screw-Phillips-Pan-M2x5-A2-Stainless.html\n# Maximum head height 1.72mm per Accu; no helical thread modeled.\nD,L,HEAD_D,HEAD_H=2.0,5.0,4.0,1.72\ns=Pos(0,0,-L/2)*Cylinder(D/2,L)\nh=Pos(0,0,HEAD_H/2)*Cylinder(HEAD_D/2,HEAD_H)\nh=fillet(h.edges().sort_by(Axis.Z)[-1:],0.3)\ns+=h\ns-=Pos(0,0,HEAD_H-0.3)*Box(2.5,0.55,0.8)\ns-=Pos(0,0,HEAD_H-0.3)*Box(0.55,2.5,0.8)\nresult=s\nresult.color=Color('#d0d5db')\n",
    "insert.build123d": "from build123d import *\n# SPIROL Series29 short M2 fallback, manufacturer guide:\n# https://www.spirol.com/assets/files/ins-threaded-inserts-design-guide-us.pdf\n# Simplified envelope, knurl and thread omitted; top at zero.\nOD,L,THREAD_MAJOR=3.63,3.18,2.0\nresult=Pos(0,0,-L/2)*(Cylinder(OD/2,L)-Cylinder(THREAD_MAJOR/2,L+0.2))\nresult.color=Color('#b5a642')\n",
    "check.py": "import json, os, math, copy\nfrom build123d import *\nROOT='/root/work/finish/'\nOUT='/root/out/hinge/'\nOFFSET=(46,-60,1.38436)\ndef leaves(s,parent=Location()):\n    if s.children:\n        p=parent*s.location\n        return [r for c in s.children for r in leaves(c,p)]\n    return [s.moved(parent)]\ndef flat(s): return Compound(children=leaves(s))\nnative=import_step('/root/work/hinge/current.step')\nentries=[]\nfor i,c in enumerate(native.children):\n    if c.label.startswith('esp32-retro'):continue\n    body=flat(c)\n    entries.append((c.label+'_'+str(i),body))\nboard=next(s for n,s in entries if n.startswith('input_PCB'))\ncomp=Compound(children=[s for n,s in entries if not n.startswith('input_PCB')])\nbase=flat(import_step(ROOT+'base.step')).translate(OFFSET)\nlid=flat(import_step(ROOT+'lid.step')).translate(OFFSET)\npin=flat(import_step(ROOT+'pin.step')).translate(OFFSET)\ndef bounds(s):return {'min':list(s.bounding_box().min),'max':list(s.bounding_box().max)}\ndef pair(a,b):\n    a,b=Compound(a.wrapped),Compound(b.wrapped)\n    d=a.distance_to(b)\n    v=0.0\n    if d<1e-5:\n        r=a&b\n        if r:v=abs(r.volume)\n    return {'distance_mm':d,'overlap_mm3':v}\nreport={'tolerance_mm':0.01,'volume_tolerance_mm3':0.001,'offset':OFFSET,'board_bounds':bounds(board),'component_bounds':bounds(comp),'base_bounds':bounds(base),'lid_bounds':bounds(lid),'pairs':{},'components':[]}\nfor n,a,b in [('base_board',base,board),('base_lid',base,lid),('base_pin',base,pin),('lid_pin',lid,pin),('lid_board',lid,board)]:\n    report['pairs'][n]=pair(a,b); print(n,report['pairs'][n],flush=True)\nfor n,s in entries:\n    if n.startswith('input_PCB'):continue\n    row={'name':n,'base':pair(base,s),'lid':pair(lid,s),'bounds':bounds(s)}\n    report['components'].append(row)\n    if row['base']['overlap_mm3']>0.001:print('COLLISION',row,flush=True)\nprint('component minima',min(r['base']['distance_mm'] for r in report['components']),min(r['lid']['distance_mm'] for r in report['components']),flush=True)\njson.dump(report,open(OUT+'initial-check.json','w'),indent=2)\n# Actual geometry snapshots, used for further verification and assembly exports.\nfor n,s in [('base-native',base),('lid-native',lid),('pin-native',pin),('board-native',board),('components-native',comp)]:export_step(s,ROOT+n+'.step')\n",
    "motion.py": "import json, math, runpy, os\nfrom build123d import *\nR='/root/work/finish/';O='/root/out/hinge/'\ndef load(n):return Compound(import_step(R+n+'.step').wrapped)\ndef pair(a,b):\n a,b=Compound(a.wrapped),Compound(b.wrapped)\n d=a.distance_to(b);v=0\n if d<1e-6:\n  c=a&b\n  if c:v=abs(c.volume)\n return dict(distance_mm=d,overlap_mm3=v)\nb=load('base-native');l=load('lid-native');board=load('board-native')\np=runpy.run_path('/root/out/hinge/hinge-pin-final.build123d')['result'].translate((46,-60,1.38436));p=Compound(p.wrapped)\nret=runpy.run_path(R+'retainer.build123d')['result'].translate((46,-60,1.38436));ret=Compound(ret.wrapped)\naxis=Axis((46,-4,18.88436),(1,0,0))\n# Conservative enclosure of ALL exported component geometry; motion passing\n# this box is a stricter test than passing the detailed individual models.\nlo=(.98572999,-110,-1.9156401);hi=(85.675,-10,14.8843601)\nenv=Pos(*[(a+c)/2 for a,c in zip(lo,hi)])*Box(*[c-a for a,c in zip(lo,hi)])\nrows=[]\nfor angle in range(111):\n q=Compound(l.rotate(axis,-angle).wrapped)\n row={'angle_deg':angle,'base':pair(q,b),'pcb':pair(q,board),'component_envelope':pair(q,env),'pin':pair(q,p),'retainer':pair(q,ret)}\n rows.append(row)\n if angle%10==0:print('motion',row,flush=True)\njson.dump({'step_deg':1,'range_deg':[0,110],'tolerance_mm':.01,'rows':rows},open(O+'motion-check.json','w'),indent=2)\nfor name,obj in [('base',b),('lid',l),('pin',p),('retainer',ret)]:\n export_step(obj,R+name+'-final-native.step')\n print('pair-pin',name,pair(obj,p),flush=True)\nexport_step(l.rotate(axis,-110),R+'lid-open-native.step')\n# Pin translation sweep, retainer removed: raw kernel shapes, 2mm steps.\nrows=[]\nfor dx in range(0,-95,-2):\n q=Compound(p.translate((dx,0,0)).wrapped)\n rows.append({'dx_mm':dx,'base':pair(q,b),'lid':pair(q,l)})\njson.dump(rows,open(O+'pin-path.json','w'),indent=2)\nprint('PIN PATH',max(r[k]['overlap_mm3'] for r in rows for k in ['base','lid']),flush=True)\n",
    "assembly_checks.py": "import json,runpy,math,os\nfrom build123d import *\nfrom OCP.BRepAlgoAPI import BRepAlgoAPI_Common\nR='/root/work/finish/';O='/root/out/hinge/';OFF=(46,-60,1.38436)\ndef baked(s):return Compound(s.wrapped)\ndef load(n):return baked(import_step(R+n+'.step'))\ndef common(a,b):\n c=Compound(BRepAlgoAPI_Common(a.wrapped,b.wrapped).Shape());return abs(c.volume)\ndef pair(a,b):\n a,b=baked(a),baked(b);d=a.distance_to(b)\n return {'distance_mm':d,'overlap_mm3':common(a,b) if d<1e-6 else 0}\nb=load('base-native');l=load('lid-native');board=load('board-native');comp=load('components-native')\ndef source(n):return runpy.run_path(R+n+'.build123d')['result']\nscrew=source('screw');ins=source('insert');ret=source('retainer').translate(OFF)\npin=runpy.run_path(O+'hinge-pin-final.build123d')['result'].translate(OFF)\n# Four screws and inserts, with local transforms copied from saved node rules.\nmounts=[(-33,-47),(33,-47),(33,47),(-33,47)]\nhardware=[];checks=[]\nfor i,(x,y) in enumerate(mounts):\n s=baked(screw.translate((x+46,y-60,1.29936)));t=baked(ins.translate((x+46,y-60,0)))\n s.label='M2 x 5 screw '+str(i+1);t.label='M2 insert '+str(i+1);hardware.extend([s,t])\n tool=Pos(x+46,y-60,22)*Cylinder(2.5,40)\n # screw/insert overlap is intentional threaded interface, insert/base is heat-set.\n checks.append({'xy_native_mm':[x+46,y-60],'screw_pcb':pair(s,board),'screw_components':pair(s,comp),'screw_base':pair(s,b),'insert_pcb':pair(t,board),'insert_components':pair(t,comp),'driver_components':pair(tool,comp),'driver_base':pair(tool,b)})\n print('mount',checks[-1],flush=True)\n# Verify conservative continuous vertical installation path: a swept AABB\n# for every component, and an exact extruded board profile. Actual mounted\n# geometry is unchanged. Lid and screws removed; inserts installed first.\ninitial=json.load(open(O+'initial-check.json'));sweeps=[]\nfor c in initial['components']:\n lo,hi=c['bounds']['min'],c['bounds']['max']; dz=25\n bb=Pos(*[(lo[i]+hi[i]+(dz if i==2 else 0))/2 for i in range(3)])*Box(hi[0]-lo[0],hi[1]-lo[1],hi[2]-lo[2]+dz)\n # Only components whose projected boxes can meet case walls or bosses.\n if lo[0]>21 and hi[0]<71 and lo[1]>-103 and hi[1]<-17 and lo[2]>=0:continue\n v=common(bb,b)\n sweeps.append({'component':c['name'],'swept_aabb_overlap_mm3':v})\n if v>.001:print('AABB SWEEP FAIL',sweeps[-1],flush=True)\n# Every base point is below PCB or outside board XY; use the board outline\n# extruded 25mm, preserving existing holes to check the complete board path.\nboard_sweep=Pos(46,-60,13.14968)*Box(72,100,26.29936)\nfor x,y in mounts:board_sweep-=Pos(x+46,y-60,13)*Cylinder(1.1,30)\nprint('board sweep',common(board_sweep,b),flush=True)\n# Mating/cable envelopes are deliberately bounded estimates, not universal\n# fit claims. All move horizontally to/from outside; connected envelopes\n# include a 30mm free straight cable run. PS/2 diameter14; USB upright6x14;\n# VGA34x15; SD18x3.2. Report clearance to printable shells only.\nports=[('VGA',-1,0.986,-31.4122,7,34,15),('USB',-1,4.9511,-61.4122,8.0,6,14),('Keyboard',-1,7.95,-77.505,8,14,14),('Mouse',-1,7.95,-95.005,8,14,14),('microSD',1,83.443,-87.893,2,16,3.2)]\ncables=[]\nfor name,d,x,y,z,w,h in ports:\n env=Pos(x+d*20,y,z)*Box(40,w,h)\n cables.append((name,env))\n print('plug',name,pair(env,b),pair(env,l),flush=True)\n# Preserve named native board and all 90 native component entries.\nnative=import_step('/root/work/hinge/current.step')\nparts=[c for c in native.children if not c.label.startswith('esp32-retro')]\nb.label='Base PETG';l.label='Lid PETG';pin.label='Removable 3mm pin';ret.label='Pin safety wire'\nfor angle in [0,110]:\n lid=baked(l.rotate(Axis((46,-4,18.88436),(1,0,0)),-angle));lid.label='Lid '+str(angle)+' degrees'\n assy=Compound(children=[baked(q) for q in parts+[b,lid,pin,ret]+hardware],label='ESP32 enclosure '+str(angle)+' degrees')\n export_step(assy,O+('enclosure-closed.step' if angle==0 else 'enclosure-open-110.step'))\n# Separate STL artifacts are from exact uploaded source geometry, in print poses.\nbase_print=load('base').translate((0,0,8.9))\nlid_print=load('lid').rotate(Axis.Y,90)\nzmin=lid_print.bounding_box().min.Z;lid_print=lid_print.translate((0,0,-zmin))\nexport_stl(base_print,O+'base-print.stl',tolerance=.03,angular_tolerance=.1)\nexport_stl(lid_print,O+'lid-print.stl',tolerance=.03,angular_tolerance=.1)\nexport_step(baked(pin),O+'hinge-pin.step')\n# Tessellation for actual assembly renders, in native frame.\nrender=[('base',b),('lid',l),('pin',pin),('retainer',ret),('board',board),('components',comp)]+[('hardware'+str(i),s) for i,s in enumerate(hardware)]\nfor n,s in render:export_stl(s,R+n+'-render.stl',tolerance=.05,angular_tolerance=.15)\nimport trimesh\nmesh={}\nfor n in ['base-print','lid-print']:\n m=trimesh.load(O+n+'.stl',force='mesh');mesh[n]={'watertight':bool(m.is_watertight),'winding_consistent':bool(m.is_winding_consistent),'bodies':len(m.split()),'volume_mm3':float(m.volume),'bounds':m.bounds.tolist(),'triangles':len(m.faces)}\nprint('MESH',mesh,flush=True)\njson.dump({'hardware':checks,'board_sweep_overlap_mm3':common(board_sweep,b),'component_sweeps':sweeps,'mating_envelopes':[{'name':n,'base':pair(s,b),'lid':pair(s,l)} for n,s in cables],'mesh':mesh},open(O+'assembly-checks.json','w'),indent=2)\n",
    "native_finalize.py": "import json,copy,math\nfrom build123d import *\nfrom OCP.BRepAlgoAPI import BRepAlgoAPI_Common\nR='/root/work/finish/';O='/root/out/hinge/'\ns=import_step(R+'final-native-export.step')\ndef baked(c):\n q=Compound(c.wrapped);q.label=c.label;return q\nbase=baked(next(c for c in s.children if c.label.startswith('hinged-base')))\nlid=baked(next(c for c in s.children if c.label.startswith('hinged-lid')))\npin=baked(next(c for c in s.children if c.label.startswith('hinge-pin')))\nboard=baked(next(c for c in s.children if c.label.startswith('input_PCB')))\nvga=baked(next(c for c in s.children if c.label.startswith('D-DMRH15')))\ndef pair(a,b):\n a,b=baked(a),baked(b);d=a.distance_to(b)\n v=0\n if d<1e-6:v=abs(Compound(BRepAlgoAPI_Common(a.wrapped,b.wrapped).Shape()).volume)\n return {'distance_mm':d,'overlap_mm3':v}\nrows=[]\nfor dz in [i*.1 for i in range(21)]+[3,5,10,15,20,25]:\n rows.append({'lift_mm':dz,**pair(vga.translate((0,0,dz)),base)})\nprint('VGA exact path',min(r['distance_mm'] for r in rows),max(r['overlap_mm3'] for r in rows),flush=True)\n# Analytic cylindrical surfaces from solids verify bore/shaft diameter and\n# axial interval endpoints, not just source parameters.\ngeometry={}\nfor name,obj in [('base',base),('lid',lid),('pin',pin)]:\n fs=[]\n for f in obj.faces().filter_by(GeomType.CYLINDER):\n  if name!='pin' and abs(f.radius-1.7)>.001:continue\n  if name=='pin' and abs(f.radius-1.5)>.001:continue\n  bb=f.bounding_box();fs.append({'radius_mm':f.radius,'min_mm':list(bb.min),'max_mm':list(bb.max)})\n geometry[name]=fs\n# Fixed source electronics must match native fresh export by name, bounds,\n# solid count and volume. Compare ordered non-enclosure entries.\nold=import_step('/root/work/hinge/current.step')\ndef electric(c):return not c.label.startswith(('esp32-retro','hinged-','hinge-pin','pin-retainer','pcb-screw','pcb-insert'))\na=[c for c in old.children if electric(c)]; b=[c for c in s.children if electric(c)]\nchanges=[]\nfor i,(x,y) in enumerate(zip(a,b)):\n xb,yb=x.bounding_box(),y.bounding_box()\n delta=max(abs(u-v) for u,v in zip(list(xb.min)+list(xb.max),list(yb.min)+list(yb.max)))\n if x.label!=y.label or delta>1e-6 or abs(x.volume-y.volume)>1e-5:changes.append({'i':i,'labels':[x.label,y.label],'bounds_delta_mm':delta,'volume_delta':x.volume-y.volume})\nprint('ELECTRONICS',len(a),len(b),'differences',changes,flush=True)\nfor angle in [0,110]:\n parts=[]\n for c in s.children:\n  q=baked(c)\n  if c.label.startswith('hinged-lid'):q=q.rotate(Axis((46,-4,18.88436),(1,0,0)),-angle);q.label='Hinged lid '+str(angle)+' degrees'\n  parts.append(q)\n assy=Compound(children=parts,label='ESP32 x86 PC enclosure '+str(angle)+' degrees')\n export_step(assy,O+('enclosure-closed.step' if angle==0 else 'enclosure-open-110.step'))\nexport_step(baked(pin),O+'hinge-pin.step')\njson.dump({'vga_path':rows,'cylindrical_faces':geometry,'unchanged_electronics_count':len(a),'fresh_electronics_count':len(b),'electronic_differences':changes,'native_pair_base_board':pair(base,board),'native_pair_base_lid':pair(base,lid),'native_pair_base_pin':pair(base,pin),'native_pair_lid_pin':pair(lid,pin)},open(O+'native-final-check.json','w'),indent=2)\nprint('FINAL EXPORT COMPLETE',flush=True)\n",
    "render.py": "import bpy,math,os\nfrom mathutils import Vector,Matrix\nfrom PIL import Image,ImageStat,ImageDraw\nbpy.ops.wm.read_factory_settings(use_empty=True)\nscene=bpy.context.scene\nscene.render.engine='BLENDER_WORKBENCH'\nscene.display.shading.light='STUDIO';scene.display.shading.color_type='MATERIAL'\nscene.display.shading.show_shadows=True;scene.display.shading.show_cavity=True\nscene.display.shading.cavity_type='BOTH'\nscene.view_settings.view_transform='Standard'\nscene.world=bpy.data.worlds.new('World');scene.world.color=(.14,.16,.19)\nscene.display.shading.background_type='WORLD'\nscene.render.resolution_x=scene.render.resolution_y=1024\nscene.render.resolution_percentage=100;scene.render.film_transparent=False\nscene.render.image_settings.file_format='PNG'\nR='/root/work/finish/';O='/root/out/hinge/'\ndef import_mesh(path):\n before=set(bpy.data.objects);bpy.ops.import_mesh.stl(filepath=path)\n bpy.context.view_layer.update()\n meshes=[o for o in bpy.data.objects if o not in before and o.type=='MESH']\n for o in meshes:\n  mw=o.matrix_world.copy();o.parent=None;o.matrix_world=mw\n return meshes\ndef material(name,rgb,metallic=0,roughness=.5,coat=0,transmission=0,alpha=1):\n m=bpy.data.materials.new(name);m.use_nodes=True;m.diffuse_color=(*rgb,1);m.metallic=metallic;m.roughness=roughness\n p=m.node_tree.nodes['Principled BSDF'];p.inputs['Base Color'].default_value=(*rgb,1);p.inputs['Metallic'].default_value=metallic;p.inputs['Roughness'].default_value=roughness\n p.inputs['Coat Weight'].default_value=coat;p.inputs['Transmission Weight'].default_value=transmission;p.inputs['Alpha'].default_value=alpha\n if alpha<1:m.blend_method='BLEND';m.shadow_method='HASHED'\n return m\nPBR={'solder_mask_black':((.02,.02,.02),0,.45,.25),'metal_shield':((.75,.76,.78),1,.30,0),'black_plastic':((.03,.03,.03),0,.55,0),'fr4':((.30,.26,.12),0,.60,0),'enig_gold':((1,.77,.34),1,.25,0),'white_silkscreen':((.92,.92,.90),0,.70,0),'glass_led_lens':((.90,.95,.95),0,.05,0)}\ndef pbr(name,alpha=1):\n rgb,met,rough,coat=PBR[name]\n return bpy.data.materials.get(name) or material(name,rgb,met,rough,coat,transmission=1 if name=='glass_led_lens' else 0,alpha=alpha)\nMASK='solder_mask_black'\nPBR['masked_copper']=(tuple(min(1,c*2.4+.10) for c in PBR[MASK][0]),0,.45,0)\nBOARD={'_soldermask':MASK,'_silkscreen':'white_silkscreen','_copper':'masked_copper','_pad':'enig_gold','_via':'enig_gold','_pcb':MASK}\nMETAL=('copper','shield','screw','metal','steel','gold');GLASS=('lens','glass')\ndef dress(obj):\n key=(obj.name+' '+obj.data.name).lower()\n for token,name in BOARD.items():\n  if token in key:\n   alpha=.6 if token=='_soldermask' else 1;obj.data.materials.clear();obj.data.materials.append(pbr(name,alpha));return\n if not any(s.material for s in obj.material_slots):\n  obj.data.materials.clear();obj.data.materials.append(pbr('metal_shield' if any(t in key for t in METAL) else 'black_plastic'));return\n for slot in obj.material_slots:\n  m=slot.material;p=m.node_tree.nodes.get('Principled BSDF') if m and m.use_nodes else None\n  if not p:continue\n  r,g,b,a=p.inputs['Base Color'].default_value;words=m.name.lower()+' '+key\n  if a<.95 or any(t in words for t in GLASS):p.inputs['Metallic'].default_value=0;p.inputs['Roughness'].default_value=.05;p.inputs['Transmission Weight'].default_value=1;continue\n  bright_grey=max(r,g,b)-min(r,g,b)<.08 and max(r,g,b)>.5\n  metal=bright_grey or any(t in words for t in METAL)\n  p.inputs['Metallic'].default_value=1 if metal else 0;p.inputs['Roughness'].default_value=.35 if metal else .55\nnames=['base','lid','pin','retainer','board','components']+['hardware'+str(i) for i in range(8)]\ngroups={n:import_mesh(R+n+'-render.stl') for n in names};parts=[o for g in groups.values() for o in g]\nfor n,g in groups.items():\n for o in g:\n  dress(o);o.data.materials.clear()\n  if n in ('base','lid'):o.data.materials.append(material(n+' PETG',(.32,.32,.32)))\n  elif n=='board':o.data.materials.append(pbr(MASK))\n  elif n=='components':o.data.materials.append(material('component simplified CAD finish',(.30,.32,.34)))\n  else:o.data.materials.append(pbr('metal_shield'))\ndef bounds(objects):\n pts=[o.matrix_world@Vector(c) for o in objects for c in o.bound_box]\n lo=Vector([min(p[i] for p in pts) for i in range(3)]);hi=Vector([max(p[i] for p in pts) for i in range(3)])\n return lo,hi\nlo,hi=bounds(parts);center=(lo+hi)/2;diagonal=max((hi-lo).length,1e-3)\ndef add_camera(center,diagonal,direction=Vector((-1,-1,.8)),margin=1.1):\n cam=bpy.data.cameras.new('Camera');cam.lens=50;cam.sensor_width=36;cam.clip_start=diagonal/100;cam.clip_end=max(100*diagonal,1000)\n obj=bpy.data.objects.new('Camera',cam);scene.collection.objects.link(obj)\n fov=2*math.atan(cam.sensor_width/(2*cam.lens));distance=(diagonal/2)*margin/math.tan(fov/2)\n obj.location=center+direction.normalized()*distance;obj.rotation_euler=(center-obj.location).to_track_quat('-Z','Y').to_euler();scene.camera=obj\n return obj\ndef add_sun(name,direction,energy):\n light=bpy.data.lights.new(name,'SUN');light.energy=energy;obj=bpy.data.objects.new(name,light);scene.collection.objects.link(obj);obj.rotation_euler=(-Vector(direction)).to_track_quat('-Z','Y').to_euler()\nadd_sun('Key',(1,-1,2),3);add_sun('Fill',(-1,1,1),1)\noriginal={o:o.matrix_world.copy() for o in parts}\naxis=Vector((46,-4,18.88436))\nfor pose in ['closed','open-110','exploded']:\n for o in parts:o.matrix_world=original[o]\n if pose=='open-110':\n  mat=Matrix.Translation(axis)@Matrix.Rotation(math.radians(-110),4,'X')@Matrix.Translation(-axis)\n  for o in groups['lid']:o.matrix_world=mat@original[o]\n if pose=='exploded':\n  moves={'lid':(0,0,100),'board':(0,0,40),'components':(0,0,40),'pin':(-80,0,0),'retainer':(30,0,0)}\n  for i in range(8):moves['hardware'+str(i)]=(0,0,65 if i%2==0 else 15)\n  for n,v in moves.items():\n   for o in groups[n]:o.matrix_world=Matrix.Translation(v)@original[o]\n bpy.context.view_layer.update();lo,hi=bounds(parts);center=(lo+hi)/2;diagonal=max((hi-lo).length,1e-3)\n add_camera(center,diagonal,Vector((-1,-1,.8)),1.10)\n path=O+pose+'.png';scene.render.filepath=path;bpy.ops.render.render(write_still=True)\n im=Image.open(path).convert('RGB');d=ImageDraw.Draw(im);d.text((38,30),'ESP32 retro PC | '+pose.replace('-',' ')+' | actual CAD',(225,230,236))\n im.save(path)\n im=im.convert('L');st=ImageStat.Stat(im);w,h=im.size;bg=im.getpixel((1,1));edges=[im.crop(b) for b in ((0,0,w,2),(0,h-2,w,h),(0,0,2,h),(w-2,0,w,h))];cut=any(abs(px-bg)>24 for e in edges for px in e.getdata())\n print('PIXELS',pose,st.mean[0],st.stddev[0],cut,flush=True)\n"
  },
  "instructions": "Part files execute with build123d and expose result. They are deterministic editable exact solids. Transform part sources by (+46,-60,+1.38436) for native STEP. Screw seats are native z1.29936; insert tops z0. Rotate only lid -110deg around X-axis through (46,-4,18.88436). Verification scripts retain original workspace paths for evidence; replace paths when rerunning elsewhere. Use the current-project native export URL for untouched PCB/components. No electronics rebuilt."
}