"""Codex-authored parametric implementation of a saved, provider-reviewed contract.

Build (inside the offline build123d container): python3 cookbook-mechanical-cad.py /work
Render saved meshes (host Python with NumPy/Pillow): --render OUTPUT_DIRECTORY
The models reviewed mechanical-design.json; they did not emit this Python source.
"""
import base64, collections, csv, hashlib, io, json, math, pathlib, struct, sys, time
from datetime import datetime, timezone

def sha(data):return hashlib.sha256(data).hexdigest()
def write_json(path,value):path.write_text(json.dumps(value,indent=2)+'\n')
def cross(a,b):return (a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0])
def sub(a,b):return tuple(x-y for x,y in zip(a,b))
def dot(a,b):return sum(x*y for x,y in zip(a,b))
def unit(a):
    n=math.sqrt(dot(a,a));return tuple(v/n for v in a)
COLORS={'alloy':(0.63,0.69,0.71,1),'dark':(0.19,0.27,0.29,1),'blue':(0.22,0.43,0.51,1),'steel':(0.77,0.79,0.79,1),'bronze':(0.64,0.45,0.22,1),'rubber':(0.16,0.18,0.17,1),'red':(0.68,0.25,0.18,1),'green':(0.20,0.46,0.36,1),'glass':(0.51,0.74,0.78,0.22),'white':(0.89,0.88,0.78,1)}

def build(directory):
    from build123d import Box,Cylinder,Compound,Color,Location,Align,export_step,import_step,export_stl,Polygon,make_face,extrude
    started=time.time();out=pathlib.Path(directory);design=json.loads((out/'mechanical-design.json').read_text());p=design['parameters'];kind=design['product']
    assert design['schema']=='tce-cookbook-mechanical-design-v1' and design['units']=='mm'
    rows=[];shapes={};groups={};checks=[]
    def box(size,at):return Box(*size,align=(Align.MIN,Align.MIN,Align.MIN)).moved(Location(at))
    def cylinder(radius,length,at,axis='Z'):
        shape=Cylinder(radius,length,align=(Align.CENTER,Align.CENTER,Align.MIN))
        if axis=='X':shape=shape.rotate(__import__('build123d').Axis.Y,90)
        if axis=='Y':shape=shape.rotate(__import__('build123d').Axis.X,-90)
        return shape.moved(Location(at))
    def tube(ro,ri,length,at,axis='Z'):return cylinder(ro,length,at,axis)-cylinder(ri,length,at,axis)
    def drilled(shape,radius,length,at,axis='Z'):return shape-cylinder(radius,length,at,axis)
    def plate(size,at,holes=()):
        shape=box(size,at)
        for x,y,r in holes:shape=drilled(shape,r,size[2]+2,(x,y,at[2]-1))
        return shape
    def add(identity,label,design_id,group,shape,color='alloy',explode=(0,0,0),category='component',material='Nominal envelope; material unqualified'):
        assert identity not in shapes and shape.is_valid and len(shape.solids())==1,identity+' must be exactly one valid solid'
        shape.label=identity;shape.color=Color(*COLORS[color]);shapes[identity]=shape
        if group not in groups:groups[group]=[]
        groups[group].append(identity);bb=shape.bounding_box()
        rows.append({'id':identity,'nodeName':identity,'label':label,'designId':design_id,'category':category,'subassembly':group,'explodeOffsetMm':list(explode),'bboxMm':{'x':round(bb.size.X,4),'y':round(bb.size.Y,4),'z':round(bb.size.Z,4)},'minMm':list(bb.min),'maxMm':list(bb.max),'volumeMm3':round(shape.volume,5),'color':COLORS[color],'material':material,'meshPath':'parts/'+identity+'.stl'})
        return shape
    def clearance(name,a,b,minimum):
        distance=shapes[a].distance_to(shapes[b]);assert distance+1e-5>=minimum,(name,distance,minimum)
        checks.append({'name':name,'parts':[a,b],'method':'OpenCascade minimum BRep distance','distanceMm':round(distance,6),'minimumMm':minimum,'passed':True})
    def no_overlap(name,a,b):
        common=shapes[a].intersect(shapes[b]);volume=sum(abs(s.volume) for s in common) if common else 0
        assert volume<0.001,(name,volume)
        checks.append({'name':name,'parts':[a,b],'method':'OpenCascade boolean common; touching faces permitted','intersectionVolumeMm3':round(volume,8),'maximumMm3':0.001,'passed':True})
    def unresolved_overlap(name,a,b):
        common=shapes[a].intersect(shapes[b]);volume=sum(abs(s.volume) for s in common) if common else 0
        checks.append({'name':name,'parts':[a,b],'method':'OpenCascade boolean common; recorded unresolved design conflict','intersectionVolumeMm3':round(volume,8),'maximumMm3':0.001,'passed':volume<0.001,'status':'unresolved; retained reviewed component placement'})
    if kind=='inspection-fixture':
        b=p['base'];g=p['guide'];j=p['jaws'];s=p['endSupports'];u=p['bushings'];lead=p['leadScrew'];ft=p['feet']
        add('F01','Drilled base plate','fixture-base','01_base',plate((b['width'],b['depth'],b['thickness']),(0,0,0),[(x,y,3.3) for x,y in ft['centres']]),explode=(0,0,-45))
        for i,(x,y) in enumerate(ft['centres']):add(f'F02_{i+1}','Elastomer foot','fixture-foot','01_base',cylinder(ft['diameter']/2,ft['height'],(x,y,-ft['height'])),'rubber',(0,0,-80))
        for i,y in enumerate(g['centresY']):add(f'F03_{i+1}','Ground guide rod','guide-rod-10x200','02_guides',cylinder(g['rodDiameter']/2,g['length'],(g['startX'],y,g['centreZ']),'X'),'steel',(0,(-1 if i==0 else 1)*65,30))
        for i,x in enumerate(s['centresX']):
            support=box((s['widthX'],s['depthY'],s['heightZ']),(x-s['widthX']/2,s['minY'],s['minZ']))
            for y in g['centresY']:support=drilled(support,5.15,18,(x-9,y,g['centreZ']),'X')
            support=drilled(support,4.2,18,(x-9,lead['centreY'],lead['centreZ']),'X')
            add(f'F04_{i+1}','Bored end support','fixture-end-support','02_guides',support,'blue',((-1 if i==0 else 1)*65,0,30))
        for i,x in enumerate((j['fixedMinX'],j['movingMinX'])):
            body=box((j['widthX'],j['depthY'],j['heightZ']),(x,j['minY'],j['minZ']))
            for y in g['centresY']:body=drilled(body,u['housingBoreDiameter']/2,20,(x-1,y,g['centreZ']),'X')
            body=drilled(body,4.3,20,(x-1,lead['centreY'],lead['centreZ']),'X')
            add(f'F05_{i+1}',('Fixed' if i==0 else 'Moving')+' jaw','fixture-jaw','03_jaws',body,'alloy',(0,0,75+i*35))
            for k,y in enumerate(g['centresY']):add(f'F06_{i+1}_{k+1}','Replaceable guide bushing','fixture-bushing','03_jaws',tube(u['outerDiameter']/2,u['boreDiameter']/2,u['length'],(x,y,g['centreZ']),'X'),'bronze',((-1 if i==0 else 1)*30,0,110+i*35))
            px=x+j['widthX'] if i==0 else x-j['padThickness']
            add(f'F07_{i+1}','Replaceable jaw face','fixture-jaw-pad','03_jaws',box((j['padThickness'],j['padDepth'],j['padHeight']),(px,40,20)),'dark',((-1 if i==0 else 1)*25,-50,80))
        add('F08','Simplified lead screw (thread omitted)','fixture-screw','04_handwheel',cylinder(lead['diameter']/2,lead['length'],(lead['startX'],lead['centreY'],lead['centreZ']),'X'),'steel',(-75,0,15))
        add('F09','Travel nut, nominal bore','fixture-travel-nut','04_handwheel',tube(lead['nutOuterDiameter']/2,lead['nutBoreDiameter']/2,lead['nutLength'],(160,90,32),'X'),'bronze',(65,0,50))
        knob=cylinder(p['knob']['diameter']/2,p['knob']['thickness'],(p['knob']['startX'],90,32),'X')
        knob=drilled(knob,4.2,12,(-3,90,32),'X')
        add('F10','Handwheel','fixture-handwheel','04_handwheel',knob,'dark',(-110,0,15))
        clearance('Moving jaw above base','F05_2','F01',1)
        clearance('Guide rod/bushing radial clearance','F03_1','F06_2_1',0.19)
        clearance('Lead screw/nut radial clearance','F08','F09',0.19)
        clearance('Jaw face opening','F07_1','F07_2',78)
        no_overlap('Jaw/guide seating','F05_2','F03_1')
        title='Adjustable inspection fixture';group_labels={'01_base':'Base and leveling','02_guides':'Guide rods and end supports','03_jaws':'Jaws and replaceable bushings','04_handwheel':'Manual screw drive'}
    else:
        assert kind=='inspection-workstation';b=p['base'];ya=p['yAxis'];ga=p['gantry'];xa=p['xAxis'];fo=p['focus'];op=p['optics'];wd=p['workDeck'];co=p['controller'];en=p['enclosure'];ca=p['cableCarrier'];ft=p['feet']
        add('W01','Machined base plate','station-base','01_base',plate((b['width'],b['depth'],b['thickness']),(0,0,0),[(x,y,4.3) for x,y in ft['centres']]),explode=(0,0,-80))
        for i,(x,y) in enumerate(ft['centres']):add(f'W02_{i+1}','Leveling foot envelope','station-foot','01_base',cylinder(ft['diameter']/2,ft['height'],(x,y,-ft['height'])),'rubber',(0,0,-140))
        for side,x in enumerate(ya['railCentresX']):
            sign=-1 if side==0 else 1
            add(f'W03_{side}','Rail support riser','y-riser','02_y_axis',box((28,ya['railLength'],8),(x-14,ya['railStartY'],12)),'dark',(sign*85,0,20))
            rail=box((ya['railWidth'],ya['railLength'],ya['railHeight']),(x-8,ya['railStartY'],ya['railSeatZ']))
            for y in range(50,381,55):rail=drilled(rail,2.8,14,(x,y,19))
            add(f'W04_{side}','Drilled Y guide rail','y-guide-rail','02_y_axis',rail,'steel',(sign*85,0,55))
            for k,y in enumerate((ya['gantryCentreY']-ya['carriageSpacingY']/2,ya['gantryCentreY']+ya['carriageSpacingY']/2)):
                carriage=box((ya['carriageWidth'],ya['carriageLength'],ya['carriageHeight']),(x-21,y-23,32))
                # Nominal dovetail/ball carriage envelope; internal bearing train omitted.
                carriage=carriage-box((16.4,48,5),(x-8.2,y-24,31))
                add(f'W05_{side}_{k}','Y bearing carriage envelope','y-carriage','02_y_axis',carriage,'green',(sign*90,0,100))
            saddle=plate((64,150,12),(x-32,135,56),[(x+dx,210+dy,3.3) for dx in (-15,15) for dy in (-15,15)])
            add(f'W06_{side}','Gantry saddle plate','y-saddle','02_y_axis',saddle,'alloy',(sign*105,0,135))
            tower=box((40,40,ga['towerHeight']),(x-20,190,ga['towerBaseZ']))-box((32,32,ga['towerHeight']+2),(x-16,194,ga['towerBaseZ']-1))
            add(f'W07_{side}','Hollow gantry tower','gantry-tower','03_gantry',tower,'alloy',(sign*150,0,180))
            for k,y in enumerate((166,230)):
                # Triangular structural gusset, extruded 6 mm in X.
                profile=make_face(Polygon((0,0),(24,0),(24,50),align=None));gusset=extrude(profile,amount=6)
                gusset=gusset.rotate(__import__('build123d').Axis.Y,90).rotate(__import__('build123d').Axis.X,90).moved(Location((x-3,y,68)))
                add(f'W08_{side}_{k}','Triangular saddle gusset','gantry-gusset','03_gantry',gusset,'blue',(sign*145,(-1 if k==0 else 1)*45,120))
        bridge=box(ga['bridgeSize'],ga['bridgeMin'])-box((442,52,22),(49,184,314))
        add('W09','Hollow X bridge','gantry-bridge','03_gantry',bridge,'alloy',(0,0,260))
        xr=box((410,16,16),(65,164,318))
        for x in range(85,466,55):xr=drilled(xr,2.5,18,(x,163,326),'Y')
        add('W10','X guide rail','x-guide-rail','04_x_axis',xr,'steel',(0,-80,210))
        xc=box(xa['carriageSize'],(240,140,301));xc=xc-box((62,5,16.4),(239,159,317.8))
        xc=drilled(xc,9.2,26,(257,150,357),'X')
        add('W11','X bearing carriage envelope','x-carriage','04_x_axis',xc,'green',(0,-120,200))
        add('W12','X lead screw; thread omitted','x-lead-screw','04_x_axis',cylinder(4,410,(65,150,357),'X'),'steel',(0,-70,270))
        for i,x in enumerate((65,457)):
            bb=drilled(box((18,28,34),(x,136,340)),4.3,20,(x-1,150,357),'X')
            add(f'W13_{i}','X shaft bearing support','x-bearing-support','04_x_axis',bb,'blue',((-1 if i==0 else 1)*65,-40,250))
        motor=box((42,42,42),(0,129,336));motor=drilled(motor,2.7,44,(-1,134,341),'X')
        add('W14','X motor nominal envelope','x-motor-envelope','04_x_axis',motor,'dark',(-140,0,200))
        add('W15','X motor L mount','x-motor-mount','04_x_axis',box((8,54,54),(42,123,330))-box((10,38,38),(41,131,338)),'blue',(-100,0,240))
        add('W16','X flexible coupling envelope','x-coupler','04_x_axis',tube(8,4.3,15,(50,150,357),'X'),'alloy',(-70,-45,260))
        add('W17','X lead nut','x-lead-nut','04_x_axis',tube(9,4.2,24,(258,150,357),'X'),'bronze',(0,-125,250))
        back=plate(fo['backplateSize'],fo['backplateMin'])
        # Builder-authored relief, not a change to the locked outer envelope.
        # Keeps the reviewed camera/ring centreline; exposes the attachment issue
        # in implementationDecisions instead of claiming an approved design.
        back=back-box((52,10,72),(244,123,163))
        # Two integrated bosses bridge the backplate to the X carriage.
        back=back+box((14,8,20),(239,132,310))+box((14,8,20),(287,132,310))
        add('W18','Focus backplate with mounting bosses','focus-backplate','05_focus',back,'blue',(0,-160,120))
        for i,x in enumerate(fo['rodCentresX']):add(f'W19_{i}','Vertical focus guide rod','focus-guide-rod','05_focus',cylinder(4,120,(x,114,188)),'steel',((-1 if i==0 else 1)*40,-180,80))
        for i,z in enumerate((180,308)):
            block=box((70,20,8),(235,104,z))
            for x in (249,291):block=drilled(block,4.15,10,(x,114,z-1))
            block=drilled(block,3.2,10,(270,114,z-1))
            if i==0:block=drilled(block,13,10,(270,110,z-1))
            add(f'W20_{i}','Focus shaft end block','focus-end-block','05_focus',block,'alloy',(0,-170,(-30 if i==0 else 150)))
        slider=box((60,12,40),(240,92,246))
        add('W21','Focus carriage plate','focus-slider','05_focus',slider,'green',(0,-225,40))
        for i,x in enumerate((249,291)):
            add(f'W22_{i}','Focus linear bushing','focus-bushing','05_focus',tube(10,4.2,32,(x,114,250)),'bronze',((-1 if i==0 else 1)*35,-225,40))
        add('W23','Focus lead screw; thread omitted','focus-lead-screw','05_focus',cylinder(3,132,(270,114,180)),'steel',(0,-180,180))
        add('W24','Focus motor nominal envelope','focus-motor-envelope','05_focus',box((28,28,28),(256,100,316)),'dark',(0,-160,230))
        # Optical mounting bracket is a single joined solid; an 8 mm neck reaches the slider.
        mount=box((64,38,8),(238,86,230))+box((60,8,8),(240,96,238))
        for x in fo['rodCentresX']:mount=drilled(mount,4.2,10,(x,114,229))
        mount=drilled(mount,3.2,10,(270,114,229))
        add('W25','Camera angle mounting bracket','camera-mount','06_optics',mount,'alloy',(0,-250,-10))
        add('W26','Inspection camera envelope','camera-envelope','06_optics',box(op['bodySize'],(254,94,op['bodyBottomZ'])),'dark',(0,-280,-40))
        add('W27','Lens barrel envelope','lens-barrel','06_optics',tube(10,7,22,(270,110,168)),'dark',(0,-290,-85))
        add('W28','Annular illuminator envelope','ring-light','06_optics',tube(24,13,10,(270,110,158)),'white',(0,-310,-130))
        add('W29','Optical window envelope','lens-window','06_optics',cylinder(7,2,(270,110,170)),'glass',(0,-330,-105))
        ring_mount=box((8,22,40),(288,100,168));ring_mount=ring_mount-box((10,14,32),(287,104,172))
        ring_mount=drilled(ring_mount,4.2,42,(291,114,167))
        add('W30','Illuminator support bracket','ring-bracket','06_optics',ring_mount,'blue',(55,-270,-60))
        deck=box(wd['size'],wd['min'])
        for x in range(160,391,25):
            for y in range(70,241,25):deck=drilled(deck,2.5,10,(x,y,55))
        add('W31','Perforated fixture deck','perforated-deck','07_workholding',deck,'alloy',(0,0,80))
        for i,(x,y) in enumerate(wd['standoffCentres']):add(f'W32_{i}','Deck spacer','deck-spacer','07_workholding',tube(10,3.3,44,(x,y,12)),'blue',((x-270)*0.18,(y-150)*0.18,35))
        for i,(x,y) in enumerate(((210,120),(335,195))):add(f'W33_{i}','Locating pin','locating-pin','07_workholding',cylinder(2.3,22,(x,y,58)),'steel',((-1 if i==0 else 1)*25,-20,115))
        for i,(x,y) in enumerate(((198,111),(298,111))):
            clamp=box((48,18,8),(x,y,66))
            slot=cylinder(4.5,10,(x+10,y+9,65))+cylinder(4.5,10,(x+18,y+9,65))+box((8,9,10),(x+10,y+4.5,65))
            clamp=clamp-slot
            add(f'W34_{i}','Slotted hold-down clamp','hold-down-clamp','07_workholding',clamp,'dark',((-1 if i==0 else 1)*70,-30,125))
            add(f'W35_{i}','Clamp hand screw envelope','clamp-hand-screw','07_workholding',cylinder(2.3,18,(x+12,y+9,56))+cylinder(8,6,(x+12,y+9,74)),'bronze',((-1 if i==0 else 1)*70,-30,170))
        target=box((70,60,2),(235,90,64))
        for x in range(241,300,10):
            for y in range(96,145,10):target=drilled(target,1.5,4,(x,y,63))
        add('W36','Perforated calibration coupon; uncalibrated','calibration-coupon','07_workholding',target,'white',(0,-50,135))
        case=box(co['outerSize'],co['min'])-box((149,94,58),(363,289,15))
        for x in (380,415):case=drilled(case,6,5,(x,285,42),'Y')
        for y in (315,345):case=drilled(case,6,5,(512,y,42),'X')
        add('W37','Controller enclosure shell','controller-shell','08_controller',case,'dark',(130,80,10))
        lid=box((155,100,3),(360,286,70))
        for i in range(co['ventCount']):lid=lid-box((4,55,5),(376+i*15,308,69))
        add('W38','Vented controller lid','controller-lid','08_controller',lid,'alloy',(130,80,110))
        add('W39','Manual switch envelope; not wired','switch-envelope','08_controller',box((16,16,12),(466,291,73)),'red',(140,60,155))
        for i,x in enumerate((380,415)):add(f'W40_{i}','Panel connector envelope','panel-connector','08_controller',tube(7,4,10,(x,280,42),'Y'),'steel',(140,20,25))
        for i,y in enumerate((315,345)):add(f'W41_{i}','Cable gland envelope','cable-gland','08_controller',tube(8,5,16,(510,y,42),'X'),'rubber',(180,80,30))
        add('W42','Rear enclosure panel; not a safety guard','rear-panel','09_enclosure',box(en['rearPanelSize'],en['rearPanelMin']),'blue',(0,160,70))
        for i,x in enumerate((20,517)):add(f'W43_{i}','Transparent side screen; not a safety guard','side-screen','09_enclosure',box((3,330,180),(x,30,12)),'glass',((-1 if i==0 else 1)*200,0,30))
        for i,x in enumerate((20,505)):add(f'W44_{i}','Rear panel post','rear-post','09_enclosure',box((15,15,360),(x,385,12)),'alloy',((-1 if i==0 else 1)*170,140,60))
        for i in range(ca['linkCount']):
            x=ca['start'][0]+i*ca['linkPitch'];y,z=ca['start'][1:]
            link=box((16,22,16),(x,y,z))-box((18,16,10),(x-1,y+3,z+3))
            add(f'W45_{i+1:02d}','Cable carrier link','carrier-link','10_cable_carrier',link,'dark',(0,90,120+i*2))
        for i,x in enumerate((104,336)):add(f'W46_{i}','Cable carrier end bracket','carrier-end-bracket','10_cable_carrier',box((16,28,6),(x,247,334))+box((6,28,22),(x,247,340)),'blue',((-1 if i==0 else 1)*30,90,95))
        for i,(x,y) in enumerate(((55,195),(85,195),(55,225),(85,225),(455,195),(485,195),(455,225),(485,225))):
            bolt=cylinder(3,12,(x,y,56))+cylinder(5,4,(x,y,68))
            add(f'B01_{i+1}','Nominal M6 mounting screw envelope','M6x12-envelope','11_fasteners',bolt,'steel',(0,0,220),'fastener')
        clearance('Left guide rails clear work deck','W04_0','W31',50)
        clearance('Right guide rails clear work deck','W04_1','W31',60)
        clearance('Ring light above empty work deck','W28','W31',90)
        clearance('Focus rod/bushing nominal radial gap','W19_0','W22_0',0.19)
        clearance('X screw/nut nominal radial gap','W12','W17',0.19)
        no_overlap('Tower interiors do not intersect deck','W07_0','W31')
        no_overlap('Controller remains behind work deck','W37','W31')
        no_overlap('Camera barrel clears light aperture','W27','W28')
        no_overlap('Camera clears authored backplate relief','W26','W18')
        no_overlap('Ring light clears authored backplate relief','W28','W18')
        no_overlap('X lead nut clears authored carriage seat','W17','W11')
        no_overlap('Camera bracket meets focus carriage without overlap','W25','W21')
        no_overlap('Camera bracket rod passage','W25','W19_0')
        no_overlap('Camera bracket screw passage','W25','W23')
        no_overlap('Camera bracket clears backplate','W25','W18')
        no_overlap('Lower guide block clears lens aperture','W20_0','W27')
        no_overlap('Illuminator bracket rod passage','W30','W19_1')
        clearance('Clamp screw fits nominal deck hole','W35_0','W31',.19)
        no_overlap('Clamp meets coupon without overlap','W34_0','W36')
        unresolved_overlap('Focus lead screw intersects locked camera envelope','W23','W26')
        title='Benchtop inspection workstation'
        group_labels={'01_base':'Base and leveling','02_y_axis':'Y rails and moving saddles','03_gantry':'Gantry structure','04_x_axis':'X positioning drive','05_focus':'Z focus cartridge','06_optics':'Camera and illumination','07_workholding':'Fixture deck and workholding','08_controller':'Controller enclosure and interfaces','09_enclosure':'Enclosure screens and rear posts','10_cable_carrier':'Cable carrier assembly','11_fasteners':'Standard fastener envelopes'}
    # Keep real functional hierarchy; do not fuse neighboring assembly parts.
    assembly=Compound(label=kind,children=[Compound(label=group,children=[shapes[identity] for identity in identities]) for group,identities in groups.items()])
    assert len(assembly.solids())==len(rows)
    assert all(shapes[row['id']].is_valid for row in rows)
    assert export_step(assembly,out/'assembly.step')
    loaded=import_step(out/'assembly.step');loaded_solids=loaded.solids();assert len(loaded_solids)==len(rows)
    volume=sum(shapes[row['id']].volume for row in rows);roundtrip_volume=sum(s.volume for s in loaded_solids)
    assert abs(roundtrip_volume-volume)<0.01
    def descendants(node):
        return [node]+[desc for child in node.children for desc in descendants(child)]
    restored={node.label:node for node in descendants(loaded) if not node.children}
    assert set(restored)==set(shapes)
    assert all(restored[identity].is_valid for identity in restored)
    assert all(max(abs(a-b) for a,b in zip(tuple(restored[row['id']].color)[:3],row['color'][:3]))<1e-5 for row in rows)
    (out/'parts').mkdir(exist_ok=True)
    meshes=[]
    for row in rows:
        shape=shapes[row['id']];vertices,faces=shape.tessellate(0.18,0.22)
        mesh={'id':row['id'],'vertices':[[v.X,v.Y,v.Z] for v in vertices],'triangles':faces,'color':row['color']}
        meshes.append(mesh);export_stl(shape,out/row['meshPath'],tolerance=0.18,angular_tolerance=0.22)
    write_json(out/'mesh-data.json',meshes)
    complexity={'partInstances':len(rows),'uniquePartDesigns':len({r['designId'] for r in rows}),'subassemblyCount':len([g for g in groups if g!='11_fasteners']),'fastenerInstances':sum(r['category']=='fastener' for r in rows),'componentInstances':sum(r['category']=='component' for r in rows)}
    if kind=='inspection-workstation':assert 60<=complexity['componentInstances']<=100 and complexity['uniquePartDesigns']>=25 and complexity['subassemblyCount']>=8
    inventory={'schema':'tce-cookbook-cad-parts-v1','title':title,'glbUnits':'metres','glbUpAxis':'Y','cadUnits':'mm','cadUpAxis':'Z','partNodeTransforms':'Identity; geometry has global placed coordinates with CAD-to-glTF axis and unit conversion baked in','complexity':complexity,'parts':rows,'subassemblies':[{'id':g,'label':group_labels[g]} for g in groups]}
    write_json(out/'parts.json',inventory)
    glb(meshes,inventory,out/'assembly.glb',False);glb(meshes,inventory,out/'exploded.glb',True)
    exploded=Compound(label=kind+'_exploded',children=[shapes[r['id']].moved(Location(r['explodeOffsetMm'])) for r in rows])
    assert export_step(exploded,out/'exploded.step')
    with (out/'BOM.csv').open('w',newline='') as f:
        writer=csv.writer(f);writer.writerow(['Part ID','Design ID','Description','Category','Subassembly','Material status','Quantity','BRep volume mm3'])
        for r in rows:writer.writerow([r['id'],r['designId'],r['label'],r['category'],group_labels[r['subassembly']],r['material'],1,r['volumeMm3']])
    bbox=assembly.bounding_box()
    manifest={'schema':'tce-cookbook-mechanical-cad-v1','createdAt':datetime.now(timezone.utc).isoformat(),'product':kind,'title':title,'designSource':{'path':'mechanical-design.json','sha256':sha((out/'mechanical-design.json').read_bytes())},'implementation':{'authorship':'Codex-authored parametric CAD implementation of the supplied, provider-reviewed mechanical contract; not Python generated by the model API','source':'cookbook-mechanical-cad.py','sha256':sha(pathlib.Path(__file__).read_bytes())},'engine':{'name':'build123d','version':'0.11.1'},'complexity':complexity,'metrics':{'validBrepSolids':len(rows),'bboxMm':dict(zip(('x','y','z'),map(float,bbox.size))),'minMm':list(bbox.min),'maxMm':list(bbox.max),'summedPartVolumeMm3':volume},'checks':{'everyPartExactlyOneValidSolid':True,'stepRoundTrip':{'solids':len(loaded_solids),'allNamesPreserved':True,'rgbColorsPreserved':True,'summedVolumeDeltaMm3':abs(roundtrip_volume-volume)},'selectedClearancesAndInterferences':checks,'glbNamedParts':len(rows),'meshTriangles':sum(len(m['triangles']) for m in meshes)},'elapsedBuildSeconds':round(time.time()-started,2),'limitations':design['limitations']+['Selected interference checks are not an exhaustive collision or motion analysis.','Fastener envelopes do not establish an attachment/torque specification.','Exploded offsets are illustration transforms, not a validated assembly or disassembly path.','Transparent part colors are presentation properties; STEP preserves RGB but not necessarily alpha.']}
    manifest['implementationDecisions']=(['Added explicit through-bores for the rods and screw, replaceable bushing seats, a nominal travel nut and a bored handwheel; these are CAD implementation details, not tested retention interfaces.','The shown jaw opening is 78 mm. The proposed 60 mm stroke is a closing-direction hypothesis only; opening by 60 mm would collide with the right support. No motion simulation was performed.'] if kind=='inspection-fixture' else ['Builder-authored focus-backplate relief: removed X244..296, Y123..133, Z163..235 mm from the locked plate envelope to clear the centered camera and ring light. The original outer plate dimensions and optical axis are unchanged. This relief was not approved by the model review.','Builder-authored integral mounting bosses bridge the 8 mm gap from focus backplate to X carriage at X239..253 and X287..301, Y132..140, Z310..330 mm. Joint fastening and strength are unresolved.','Builder-authored radius9.2 mm seat in the X carriage gives the nominal radius9 mm lead nut clearance; retention and screw alignment are unvalidated.','Builder-authored camera bracket ends at Y124 mm and its neck at Z246 mm, meeting the backplate/carriage without positive-volume overlap. Guide-rod and screw passages are cut through its shelf. The lower guide block has a radius13 mm lens aperture, and the illuminator bracket a radius4.2 mm guide passage. These clearances do not establish fastening, stiffness or retention.','Builder-authored clamp slots and diameter4.6 mm screw envelopes align to existing grid holes at (210,120) and (310,120); clamp faces touch the coupon at Z66 mm. Threads, axial retention and clamping force are undefined.','Detailed holes, hollow sections, nominal motor/connector shapes, cable links and workholding hardware implement the concept for inspection; they do not represent qualified commercial parts.'])
    manifest['remainingConflicts']=(['Guide/screw axial retention, jaw locking, practical travel stops and manufacturing fits remain undefined.'] if kind=='inspection-fixture' else ['KNOWN STATIC INTERFERENCE: the reviewed focus-screw axis passes through the reviewed camera envelope (W23/W26). The exact positive overlap is recorded as a failed digital check; the reviewed placements are deliberately retained. A revised mechanical layout and model review are required to resolve this.','No validated fastener/retention scheme for the focus bosses, X nut, optics, rails or cable carrier.','No swept-motion collision analysis; limits and cable flex have not been established.','The side/rear screens are visual concept panels and do not establish guarding or safe energized operation.','The CAD implementation decisions resolve selected static geometric clashes only; they do not constitute an approved engineering resolution of all model-review findings.'])
    manifest['implementation']['source']='source.py'
    write_json(out/'cad-manifest.json',manifest)
    print(json.dumps({'product':kind,'complexity':complexity,'buildSeconds':manifest['elapsedBuildSeconds'],'bbox':manifest['metrics']['bboxMm']},indent=2))

def glb(meshes,inventory,target,exploded):
    """Faithful BRep tessellation, baked metre/Y-up coordinates, named hierarchy."""
    data=bytearray();views=[];accessors=[];materials=[];gmeshes=[];nodes=[{'name':inventory['title'],'children':[]}];parts={r['id']:r for r in inventory['parts']};group_nodes={}
    for group in inventory['subassemblies']:
        group_nodes[group['id']]=len(nodes);nodes[0]['children'].append(len(nodes));nodes.append({'name':group['id'],'children':[]})
    for m in meshes:
        row=parts[m['id']];offset=row['explodeOffsetMm'] if exploded else (0,0,0);positions=[];normals=[]
        for face in m['triangles']:
            pts=[]
            for i in face:
                x,y,z=(m['vertices'][i][k]+offset[k] for k in range(3));pts.append((x/1000,z/1000,-y/1000))
            n=unit(cross(sub(pts[1],pts[0]),sub(pts[2],pts[0])))
            positions.extend(pts);normals.extend([n]*3)
        attrs={}
        for semantic,values in [('POSITION',positions),('NORMAL',normals)]:
            start=len(data)
            for value in values:data.extend(struct.pack('<3f',*value))
            views.append({'buffer':0,'byteOffset':start,'byteLength':len(data)-start,'target':34962});accessor={'bufferView':len(views)-1,'componentType':5126,'count':len(values),'type':'VEC3'}
            if semantic=='POSITION':accessor.update({'min':[min(v[i] for v in values) for i in range(3)],'max':[max(v[i] for v in values) for i in range(3)]})
            accessors.append(accessor);attrs[semantic]=len(accessors)-1
        material={'name':m['id']+'_presentation','pbrMetallicRoughness':{'baseColorFactor':m['color'],'metallicFactor':0.2,'roughnessFactor':0.55},'doubleSided':False}
        if m['color'][3]<1:material.update({'alphaMode':'BLEND','doubleSided':True})
        materials.append(material);gmeshes.append({'name':m['id'],'primitives':[{'attributes':attrs,'material':len(materials)-1,'mode':4}]});nodes[group_nodes[row['subassembly']]]['children'].append(len(nodes));nodes.append({'name':m['id'],'mesh':len(gmeshes)-1,'extras':{'designId':row['designId'],'subassembly':row['subassembly']}})
    obj={'asset':{'version':'2.0','generator':'Synatrail cookbook: build123d tessellation / baked part transforms'},'scene':0,'scenes':[{'nodes':[0]}],'nodes':nodes,'meshes':gmeshes,'materials':materials,'accessors':accessors,'bufferViews':views,'buffers':[{'byteLength':len(data)}],'extras':{'units':'metres','upAxis':'Y','originalUnits':'mm','originalUpAxis':'Z'}}
    payload=json.dumps(obj,separators=(',',':')).encode();payload+=b' '*((-len(payload))%4)
    target.write_bytes(struct.pack('<III',0x46546c67,2,12+8+len(payload)+8+len(data))+struct.pack('<II',len(payload),0x4e4f534a)+payload+struct.pack('<II',len(data),0x004e4942)+data)
    assert all(sum(n.get('name')==r['nodeName'] for n in nodes)==1 for r in inventory['parts'])

def render(out):
    import numpy as np
    from PIL import Image
    inventory=json.loads((out/'parts.json').read_text());meshes=json.loads((out/'mesh-data.json').read_text());manifest=json.loads((out/'cad-manifest.json').read_text());by_id={r['id']:r for r in inventory['parts']}
    def esc(s):return str(s).replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')
    def scene(subset,exploded=False,view=(1,-1.6,1.1),width=1300,height=900,padding=(70,65)):
        direction=np.array(unit(view));up_hint=np.array((0,1,0) if abs(direction[2])>.99 else (0,0,1));right=np.cross(up_hint,direction);right=right/np.linalg.norm(right);up=np.cross(direction,right)
        prepared=[];all_vertices=[]
        for m in subset:
            vertices=np.array(m['vertices'],dtype=float)
            if exploded:vertices+=np.array(by_id[m['id']]['explodeOffsetMm'])
            projected=np.column_stack((vertices@right,-vertices@up));all_vertices.append(projected);prepared.append((m,vertices,projected))
        extent=np.vstack(all_vertices);lo=extent.min(axis=0);hi=extent.max(axis=0);scale=min((width-padding[0])/(hi[0]-lo[0]),(height-padding[1])/(hi[1]-lo[1]));centre=(lo+hi)/2
        pixels=np.zeros((height,width,4),dtype=np.uint8);zbuffer=np.full((height,width),-np.inf);normalbuffer=np.zeros((height,width,3));light=np.array(unit((-.4,-.8,1.4)));transparent=[];anchors={}
        def triangle(pts,depths,normal,color,alpha):
            xmin,ymin=np.floor(pts.min(axis=0)).astype(int);xmax,ymax=np.ceil(pts.max(axis=0)).astype(int);xmin=max(0,xmin);ymin=max(0,ymin);xmax=min(width-1,xmax);ymax=min(height-1,ymax)
            if xmin>xmax or ymin>ymax:return
            ax,ay=pts[0];bx,by=pts[1];cx,cy=pts[2];den=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy)
            if abs(den)<1e-9:return
            yy,xx=np.mgrid[ymin:ymax+1,xmin:xmax+1];xx=xx+.5;yy=yy+.5
            wa=((by-cy)*(xx-cx)+(cx-bx)*(yy-cy))/den;wb=((cy-ay)*(xx-cx)+(ax-cx)*(yy-cy))/den;wc=1-wa-wb;depth=wa*depths[0]+wb*depths[1]+wc*depths[2]
            old=zbuffer[ymin:ymax+1,xmin:xmax+1];mask=(wa>=-1e-8)&(wb>=-1e-8)&(wc>=-1e-8)&(depth>=old-1e-5)
            shade=.58+.4*max(0,float(normal@light));rgb=np.array([min(255,int(c*255*shade)) for c in color[:3]])
            target=pixels[ymin:ymax+1,xmin:xmax+1]
            if alpha>=.99:old[mask]=depth[mask];target[mask]=[*rgb,255];normalbuffer[ymin:ymax+1,xmin:xmax+1][mask]=normal
            else:
                existing=target[mask].astype(float);back=existing[:,:3];empty=existing[:,3]==0;back[empty]=[248,245,235];target[mask]=np.column_stack((rgb*alpha+back*(1-alpha),np.full(len(back),255))).astype(np.uint8)
        for m,vertices,projected in prepared:
            screen=(projected-centre)*scale+[width/2,height/2];depth=vertices@direction;anchors[m['id']]=screen.mean(axis=0).tolist()
            for face in m['triangles']:
                face=list(face);v=vertices[face];n=np.cross(v[1]-v[0],v[2]-v[0]);length=np.linalg.norm(n)
                if length<1e-10:continue
                n=n/length
                if m['color'][3]>=.99 and n@direction<=1e-8:continue
                args=(screen[face],depth[face],n,m['color'],m['color'][3])
                if m['color'][3]<.99:transparent.append((float(depth[face].mean()),args))
                else:triangle(*args)
        # Visible crease and silhouette lines derived from the real depth/normal
        # buffers. This does not invent a vector representation of the CAD.
        occupied=np.isfinite(zbuffer);edges=np.zeros((height,width),dtype=bool)
        for axis in (0,1):
            neighbor=np.roll(normalbuffer,1,axis=axis);neighbor_occupied=np.roll(occupied,1,axis=axis)
            crease=(normalbuffer*neighbor).sum(axis=2)<.72
            edges|=occupied & ((~neighbor_occupied) | crease)
        pixels[edges,:3]=(pixels[edges,:3]*.61).astype(np.uint8)
        for _,args in sorted(transparent,key=lambda v:v[0]):triangle(*args)
        def project(point):
            point=np.array(point);return ((np.array([point@right,-point@up])-centre)*scale+[width/2,height/2]).tolist()
        buffer=io.BytesIO();Image.fromarray(pixels).save(buffer,format='PNG');return base64.b64encode(buffer.getvalue()).decode(),{'anchors':anchors,'project':project,'occupied':pixels[:,:,3]>0}
    def svg(title,subtitle,image,legend=None,width=1600,height=1120,meta=None,targets=None):
        parts=[f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img"><title>{esc(title)}</title><rect width="100%" height="100%" fill="#fbf7ed"/><g font-family="Arial,Helvetica,sans-serif" fill="#243d35">',f'<text x="40" y="48" font-size="19" letter-spacing="2">SYNATRAIL / PARAMETRIC ASSEMBLY STUDY</text><text x="40" y="91" font-size="34" font-weight="bold">{esc(title)}</text><text x="40" y="125" font-size="20">{esc(subtitle)}</text>',f'<image x="0" y="145" width="1300" height="900" href="data:image/png;base64,{image}"/>']
        if legend:
            stride=min(78,(height-255)/len(legend));placed=[]
            for i,(identity,label) in enumerate(legend):
                y=183+i*stride;number=f'{i+1:02d}'
                parts.append(f'<text x="1295" y="{y}" font-size="17" font-weight="bold">{number} / {esc(identity)}</text>')
                words=label.split();lines=[];line=''
                for word in words:
                    if len(line+' '+word)>28:lines.append(line);line=''
                    line+=((' ' if line else '')+word)
                if line:lines.append(line)
                for k,line in enumerate(lines[:2]):parts.append(f'<text x="1295" y="{y+23+k*19}" font-size="16">{esc(line)}</text>')
                if meta and targets and identity in targets:
                    anchor=meta['anchors'][targets[identity]];ax,ay=anchor
                    candidates=[]
                    for radius in (65,90,120,150):
                        for angle in np.linspace(0,2*math.pi,16,endpoint=False):
                            bx=ax+radius*math.cos(angle);by=ay+radius*math.sin(angle)
                            if not (30<bx<1240 and 25<by<850):continue
                            if any(math.hypot(bx-x,by-y)<48 for x,y in placed):continue
                            patch=meta['occupied'][int(by-18):int(by+19),int(bx-18):int(bx+19)]
                            score=float(patch.mean())*500+radius+abs(by-ay)*.04
                            candidates.append((score,bx,by))
                    if not candidates:continue
                    _,bx,by=min(candidates);placed.append((bx,by));ay+=145;by+=145
                    parts.append(f'<g stroke="#243d35" stroke-width="1.6"><path d="M{ax:.1f},{ay:.1f} L{bx:.1f},{by:.1f}" fill="none"/><circle cx="{ax:.1f}" cy="{ay:.1f}" r="3" fill="#243d35"/><circle cx="{bx:.1f}" cy="{by:.1f}" r="18" fill="#fbf7ed"/></g><text x="{bx:.1f}" y="{by+5:.1f}" font-size="14" font-weight="bold" text-anchor="middle">{number}</text>')
        parts.append(f'<text x="40" y="{height-40}" font-size="18">Actual BRep tessellation · nominal millimetres · concept geometry, not manufacturing approval</text><text x="40" y="{height-14}" font-size="16">No motion, load, optical, electrical, safety or fabrication test has been performed.</text></g></svg>')
        return ''.join(parts)
    complexity=inventory['complexity'];summary=f"{complexity['componentInstances']} components + {complexity['fastenerInstances']} fastener envelopes · {complexity['uniquePartDesigns']} distinct designs · {complexity['subassemblyCount']} functional subassemblies"
    group_legend=[(g['id'],g['label']) for g in inventory['subassemblies']]
    group_targets={g['id']:max((r for r in inventory['parts'] if r['subassembly']==g['id']),key=lambda r:r['volumeMm3'])['id'] for g in inventory['subassemblies']}
    if inventory['title']=='Benchtop inspection workstation':group_targets.update({'04_x_axis':'W12','06_optics':'W28','10_cable_carrier':'W45_06'})
    for name,exploded in [('assembled',False),('exploded',True)]:
        raster,meta=scene(meshes,exploded);caption=summary if not exploded else 'Exploded inspection view; offsets illustrate grouping and are not a proven assembly sequence'
        (out/(name+'.svg')).write_text(svg(inventory['title']+(' — exploded' if exploded else ''),caption,raster,group_legend,meta=meta,targets=group_targets))
    details=out/'detail-sheets';details.mkdir(exist_ok=True)
    for group in inventory['subassemblies']:
        subset=[m for m in meshes if by_id[m['id']]['subassembly']==group['id']]
        raster,meta=scene(subset,False);legend=[(m['id'],by_id[m['id']]['label']) for m in subset]
        (details/(group['id']+'.svg')).write_text(svg(group['label'],f"{len(subset)} placed parts · part identities match STEP, GLB and BOM.csv",raster,legend,meta=meta,targets={m['id']:m['id'] for m in subset}))
    # Dimensions use the same camera transform as the actual rendered mesh.
    # Extension-line endpoints come from measured placed BRep bounds.
    def dimension(project,a,b,offset,label,vertical=False):
        ax,ay=project(a);bx,by=project(b)
        style='stroke="#345d53" stroke-width="1.6" fill="none"'
        if vertical:
            x=offset;path=f'M{ax},{ay} H{x+6} M{bx},{by} H{x+6}';line=f'M{x},{ay} V{by}'
            tx=x-9;ty=(ay+by)/2;rotation=f' transform="rotate(-90 {tx} {ty})"'
        else:
            y=offset;path=f'M{ax},{ay} V{y-6} M{bx},{by} V{y-6}';line=f'M{ax},{y} H{bx}'
            tx=(ax+bx)/2;ty=y-9;rotation=''
        return f'<path d="{path}" {style}/><path d="{line}" {style} marker-start="url(#dim)" marker-end="url(#dim)"/><text x="{tx}" y="{ty}"{rotation} text-anchor="middle" font-size="18" font-weight="bold" stroke="#f4f0e4" stroke-width="5" paint-order="stroke">{esc(label)}</text>'
    # Four exact mesh projections, each kept large enough for engineering review.
    panels=[];views=[('FRONT · X / Z',(0,-1,0)),('TOP · X / Y',(0,0,1)),('RIGHT · Y / Z',(1,0,0)),('ISOMETRIC',(1,-1.6,1.1))]
    bbox=manifest['metrics']['bboxMm']
    for i,(label,direction) in enumerate(views):
        raster,meta=scene(meshes,False,direction,width=940,height=680,padding=(190,175));x=40+(i%2)*1000;y=150+(i//2)*790
        dims=[f"Overall X {bbox['x']:.1f} × Z {bbox['z']:.1f} mm",f"Overall X {bbox['x']:.1f} × Y {bbox['y']:.1f} mm",f"Overall Y {bbox['y']:.1f} × Z {bbox['z']:.1f} mm",'Colors identify nominal part groups; transparent panels are presentation only'][i]
        project=meta['project'];low=manifest['metrics']['minMm'];high=manifest['metrics']['maxMm'];annotation=''
        if i<3:
            horizontal_axis=0 if i<2 else 1;vertical_axis=1 if i==1 else 2
            a=list(low);b=list(low);b[horizontal_axis]=high[horizontal_axis]
            bottom=max(project(a)[1],project(b)[1]);annotation+=dimension(project,a,b,bottom+43,f"{bbox['xyz'[horizontal_axis]]:g} mm")
            b=list(low);b[vertical_axis]=high[vertical_axis];left=min(project(a)[0],project(b)[0])
            annotation+=dimension(project,a,b,left-43,f"{bbox['xyz'[vertical_axis]]:g} mm",True)
            if inventory['title']=='Benchtop inspection workstation':
                if i==0:
                    # Rear panel height measured between its actual Z faces.
                    r=by_id['W42'];a=r['minMm'];b=[a[0],a[1],r['maxMm'][2]]
                    annotation+=dimension(project,a,b,project(a)[0]+45,f"{r['bboxMm']['z']:g} panel",True)
                elif i==1:
                    r=by_id['W31'];a=r['minMm'];b=[r['maxMm'][0],a[1],a[2]]
                    annotation+=dimension(project,a,b,project(a)[1]+32,f"{r['bboxMm']['x']:g} deck")
                    a=r['maxMm'];b=[a[0],r['minMm'][1],a[2]]
                    annotation+=dimension(project,a,b,project(a)[0]+35,f"{r['bboxMm']['y']:g} deck",True)
                elif i==2:
                    # The actual static ring/deck vertical separation is 94 mm.
                    a=[270,110,by_id['W31']['maxMm'][2]];b=[270,110,by_id['W28']['minMm'][2]]
                    annotation+=dimension(project,a,b,project(a)[0]+42,f"{b[2]-a[2]:g} clear",True)
        panels.append(f'<g><rect x="{x}" y="{y}" width="960" height="760" fill="#f4f0e4" stroke="#a1aea6"/><text x="{x+18}" y="{y+34}" font-size="26" font-weight="bold">{label}</text><image x="{x+10}" y="{y+42}" width="940" height="680" href="data:image/png;base64,{raster}"/><g transform="translate({x+10} {y+42})">{annotation}</g><text x="{x+18}" y="{y+741}" font-size="21">{esc(dims)}</text></g>')
    drawing=f'<svg xmlns="http://www.w3.org/2000/svg" width="2080" height="1800" viewBox="0 0 2080 1800"><title>{esc(inventory["title"])} — engineering review projections</title><defs><marker id="dim" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M8,0 L0,4 L8,8" fill="none" stroke="#345d53" stroke-width="1.2"/></marker></defs><rect width="2080" height="1800" fill="#fbf7ed"/><g font-family="Arial,Helvetica,sans-serif" fill="#243d35"><text x="40" y="60" font-size="38" font-weight="bold">{esc(inventory["title"])}</text><text x="40" y="105" font-size="24">Engineering review projections · measured placed geometry · millimetres · no section cut</text>{"".join(panels)}<text x="40" y="1760" font-size="22">Not to scale on screen. Detail sheets and BOM identify every placed part. Concept only; no manufacturing or safety approval.</text></g></svg>'
    (out/'engineering-drawing.svg').write_text(drawing)
    manifest['rendering']={'method':'Z-buffered orthographic rasterization of actual per-part build123d tessellations; annotated SVG embeds rendered pixels; visible crease lines come from depth/normal buffers','callouts':'Numbered leaders target projected vertex centroids of the identified CAD component; main sheets identify functional groups, detail sheets identify placed parts','dimensions':'Extension-line endpoints use measured BRep bounds projected through the same camera as the rendered mesh; nominal millimetres','views':['assembled','exploded','engineering-drawing'],'detailSheets':[g['id'] for g in inventory['subassemblies']],'transparency':'Nominal clear panels are alpha blended; material colors are presentation properties','meshSourceSha256':sha((out/'mesh-data.json').read_bytes())};write_json(out/'cad-manifest.json',manifest)
    print('Rendered assembly, exploded, engineering review and '+str(len(inventory['subassemblies']))+' detail sheets.')

if __name__=='__main__':
    if sys.argv[1]=='--render':render(pathlib.Path(sys.argv[2]))
    else:build(sys.argv[1])
