"""RS06 whole-robot layout r01, generated in Autodesk Fusion.

Run from the repository with the existing local Fusion MCP runner. Set __file__
to this file when loading via exec, or set SHAPPY_ROOT to the repository root.
Reuses v2_leg/link_candidates/fusion_build.py for the reviewed C lower link.
This is a placement model, not a released production assembly.
"""
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import adsk.core as ac
import adsk.fusion as af

ROOT = Path(os.environ.get('SHAPPY_ROOT', Path(__file__).resolve().parents[2]))
HERE = ROOT / 'v2_robot/rs06_concept'
P = json.loads((HERE / 'parameters.json').read_text())
DOCUMENT = 'Shappy V2 - RS06 Whole Robot r01'
spec = importlib.util.spec_from_file_location('lower_link_helpers', ROOT / 'v2_leg/link_candidates/fusion_build.py')
L = importlib.util.module_from_spec(spec)
spec.loader.exec_module(L)
TM = af.TemporaryBRepManager.get()
RECORDS = []
TEMPLATES = {}
COLORS = {'frame':[.47,.54,.62], 'motors':[.16,.19,.23], 'upper_links':[.18,.36,.57],
          'lower_links':[.16,.55,.48], 'transmission':[.66,.69,.73],
          'electronics':[.46,.39,.63], 'hardware':[.62,.65,.69]}


def pt(xyz):
    return ac.Point3D.create(*[v/10 for v in xyz])


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 matrix(origin=(0,0,0), x=(1,0,0), y=(0,1,0)):
    m=ac.Matrix3D.create()
    m.setToAlignCoordinateSystems(ac.Point3D.create(0,0,0),ac.Vector3D.create(1,0,0),
        ac.Vector3D.create(0,1,0),ac.Vector3D.create(0,0,1),pt(origin),
        ac.Vector3D.create(*x),ac.Vector3D.create(*y),ac.Vector3D.create(*cross(x,y)))
    return m


def box(center, dimensions):
    return TM.createBox(ac.OrientedBoundingBox3D.create(pt(center),ac.Vector3D.create(1,0,0),
        ac.Vector3D.create(0,1,0),*[v/10 for v in dimensions]))


def cylinder(a,b,r):
    return TM.createCylinderOrCone(pt(a),r/10,pt(b),r/10)


def subtract(body,tool):
    if not TM.booleanOperation(body,tool,af.BooleanTypes.DifferenceBooleanType):
        raise RuntimeError('Boolean difference failed')
    return body


def union(body,tool):
    if not TM.booleanOperation(body,tool,af.BooleanTypes.UnionBooleanType):
        raise RuntimeError('Boolean union failed')
    return body


def add_temp(comp,name,body):
    f=comp.features.baseFeatures.add();f.name=name
    f.startEdit();comp.bRepBodies.add(body,f);f.finishEdit()
    b=f.bodies.item(0);b.name=name
    return b


def component(root,name,group,kind,leg=None,transform=None):
    occurrence,comp=L.component(root,name)
    RECORDS.append({'occurrence':occurrence,'group':group,'kind':kind,'leg':leg,'role':name,'transform':transform or ac.Matrix3D.create()})
    comp.attributes.add('ShappyRS06','group',group)
    return occurrence,comp


def instance(root,comp,name,group,kind,leg,transform):
    if comp.name in TEMPLATES:
        o=TEMPLATES.pop(comp.name);o.isLightBulbOn=True
    else:
        o=root.occurrences.addExistingComponent(comp,ac.Matrix3D.create())
    RECORDS.append({'occurrence':o,'group':group,'kind':kind,'leg':leg,'role':name,'transform':transform})
    return o


def circles(radius,count,angle=0):
    return [(radius*math.cos(angle+2*math.pi*i/count),radius*math.sin(angle+2*math.pi*i/count)) for i in range(count)]


def motor(comp,rotor_comp):
    housing=L.disk(comp,'RS06_D90_L55_reference_housing',0,0,45,55,y=-27.5)
    L.holes(comp,'Front_rotor_recess',[(0,0)],28,6,y=0,bodies=[housing])
    L.holes(comp,'Stator_8_M3_on_PCD82_reference',circles(41,8,math.pi/8),1.6,9,y=-4.5,bodies=[housing])
    rotor=L.disk(rotor_comp,'RS06_output_pilot_D52_reference',0,0,26,4.5,y=-.75)
    L.holes(rotor_comp,'Output_6_M4_on_PCD24_reference',circles(12,6,math.pi/2),2,6,y=-1.5,bodies=[rotor])
    L.holes(rotor_comp,'Output_center_visual_relief',[(0,0)],3,8,bodies=[rotor])
    housing.name='RS06_D90_L55_reference_housing'
    rotor.name='RS06_output_pilot_D52_reference'
    # Rear connector block is an additional reserved envelope, not a selected cable.
    add_temp(comp,'Connector_clearance_REFERENCE',subtract(box((0,-51,-46),(20,12,10)),TM.copy(housing)))


def stator_ring(comp,name,y):
    b=L.disk(comp,name,0,0,48,4,y=y,inner=28)
    L.holes(comp,name+'_8_M3_clearance',circles(41,8,math.pi/8),1.7,6,y=y,bodies=[b])
    return b


def upper(comp):
    body=L.disk(comp,'Hip_drive_eye',0,0,26,6,y=133,inner=4)
    L.poly(comp,'Inner_PETG_cheek',[(0,-24),(65,-22),(200,-19),(200,19),(65,22),(0,24)],6,y=133,radius=3,operation=L.JOIN)
    L.disk(comp,'Inner_knee_bearing_seat',200,0,23,6,y=133,operation=L.JOIN)
    L.holes(comp,'Inner_knee_6001_seat_REFERENCE',[(200,0)],14.1,10,y=133)
    L.holes(comp,'Hip_6_M4_clearance',circles(12,6,math.pi/2),2.2,12,y=133)
    for i,(a,b) in enumerate([(48,84),(96,133),(146,174)]):
        L.poly(comp,'Inner_lightening_'+str(i),[(a,-12),(b,-10),(b,10),(a,12)],12,y=133,radius=3,operation=L.CUT)
    outer=L.poly(comp,'Outer_PETG_cheek',[(66,-22),(200,-19),(200,19),(66,22)],6,y=187,radius=3)
    L.disk(comp,'Outer_knee_seat',200,0,23,6,y=187,operation=L.JOIN,bodies=[outer])
    L.holes(comp,'Outer_knee_6001_seat_REFERENCE',[(200,0)],14.1,10,y=187,bodies=[outer])
    for i,(a,b) in enumerate([(78,130),(145,174)]):
        L.poly(comp,'Outer_lightening_'+str(i),[(a,-11),(b,-9),(b,9),(a,11)],10,y=187,radius=3,operation=L.CUT,bodies=[outer])
    # Bridges sit inside the open belt loop; no belt force qualification is implied.
    for x in (89,140,160):
        for z in (-16,16):
            add_temp(comp,'Cheek_bridge_%s_%s'%(x,z),box((x,160,z),(10,48,6)))
    bodies=list(comp.bRepBodies)
    join=comp.features.combineFeatures.createInput(bodies[0],L.collect(bodies[1:]))
    join.operation=L.JOIN
    feature=comp.features.combineFeatures.add(join);feature.name='Single_connected_printed_upper_link'
    if comp.bRepBodies.count!=1:raise RuntimeError('Upper link must be one connected solid')
    comp.bRepBodies.item(0).name='PETG_upper_200_integral_cheeks_and_bridges'


def belt_and_pulleys(comp):
    radius=P['belt']['teeth_each']*P['belt']['pitch_mm']/(2*math.pi)
    y=P['belt']['center_y_mm']
    for x in (0,200):
        pulley=L.disk(comp,'S5M_40T_pitch_envelope_'+str(x),x,0,radius-.65,25,y=y,inner=6.1)
        L.disk(comp,'Pulley_front_flange_'+str(x),x,0,radius+2.5,1,y=140.5,inner=6.1,operation=L.JOIN,bodies=[pulley])
        L.disk(comp,'Pulley_back_flange_'+str(x),x,0,radius+2.5,1,y=166.5,inner=6.1,operation=L.JOIN,bodies=[pulley])
        # Web window is cosmetic weight allowance; actual selected hub is unresolved.
        for p in circles(19,6):
            L.holes(comp,'Pulley_web_hole',[(p[0]+x,p[1])],3.3,30,y=y,bodies=[pulley])
    # A continuous capsule-shaped pitch path, 3mm thick. Teeth are intentionally absent.
    outside=union(cylinder((0,y-12.5,0),(0,y+12.5,0),radius+1.5),box((100,y,0),(200,25,2*(radius+1.5))))
    union(outside,cylinder((200,y-12.5,0),(200,y+12.5,0),radius+1.5))
    inside=union(cylinder((0,y-13.5,0),(0,y+13.5,0),radius-1.5),box((100,y,0),(200,27,2*(radius-1.5))))
    union(inside,cylinder((200,y-13.5,0),(200,y+13.5,0),radius-1.5))
    add_temp(comp,'600S5M250_belt_PITCH_PATH_not_teeth',subtract(outside,inside))


def lower_hardware(comp):
    # Integral output flange and shoulder shaft concept: torque is NOT transmitted by
    # bearing friction. Pulley keyed/clamping interface remains an explicit open task.
    shaft=cylinder((0,125,0),(0,194,0),6)
    union(shaft,cylinder((0,120,0),(0,125,0),24))
    union(shaft,cylinder((0,116,0),(0,120,0),6))
    union(shaft,cylinder((0,125,0),(0,128,0),8))
    for x,z in L.BOLT_CENTERS:
        subtract(shaft,cylinder((x,119,z),(x,126,z),2.15))
    for x,z in L.DOWEL_CENTERS:
        subtract(shaft,cylinder((x,119,z),(x,126,z),2))
    add_temp(comp,'Knee_integral_shoulder_shaft_and_flange_PROVISIONAL',shaft)
    back=L.disk(comp,'C_link_backing_plate',0,0,24,2,y=99,inner=6.2)
    L.holes(comp,'Backing_bolts',L.BOLT_CENTERS,2.15,4,y=99,bodies=[back])
    L.holes(comp,'Backing_dowels',L.DOWEL_CENTERS,2.1,4,y=99,bodies=[back])
    for i,(x,z) in enumerate(L.BOLT_CENTERS):
        L.disk(comp,'Root_crush_sleeve_'+str(i),x,z,3,20,y=110,inner=2.15)
        b=L.disk(comp,'Root_M4_shank_envelope_'+str(i),x,z,2,27,y=111.5)
        L.disk(comp,'Root_M4_head_envelope_'+str(i),x,z,3.5,4,y=96,operation=L.JOIN,bodies=[b])
    for i,(x,z) in enumerate(L.DOWEL_CENTERS):
        L.disk(comp,'Root_dowel_'+str(i),x,z,2,12,y=119)
    L.disk(comp,'Shaft_retention_nut_ENVELOPE',0,0,9,4,y=192,inner=6)
    for i,(x,z) in enumerate(L.FOOT_CENTERS):
        L.disk(comp,'Foot_crush_sleeve_'+str(i),x,z,2.5,17,y=110,inner=1.65)
        b=L.disk(comp,'Foot_M3_shank_'+str(i),x,z,1.5,20,y=111.5)
        L.disk(comp,'Foot_M3_head_'+str(i),x,z,2.75,3,y=100,operation=L.JOIN,bodies=[b])
        L.disk(comp,'Foot_M3_nut_'+str(i),x,z,3.18,2.4,y=119.7,inner=1.65)


def bearing(comp,y):
    return L.disk(comp,'6001ZZ_12x28x8_ENVELOPE',0,0,14,8,y=y,inner=6)


def appearance(design,body,color_name):
    library=ac.Application.get().materialLibraries.itemByName('Fusion Appearance Library')
    name='RS06_'+color_name
    app=design.appearances.itemByName(name)
    if not app:app=design.appearances.addByCopy(library.appearances.itemByName('Plastic - Matte ('+color_name+')'),name)
    body.appearance=app


def run(_context):
    RECORDS.clear()
    TEMPLATES.clear()
    app=ac.Application.get()
    if any(d.name==DOCUMENT for d in app.documents):
        raise RuntimeError('Preserving existing r01 document; inspect it or use a new revision before rebuilding')
    doc=app.documents.add(ac.DocumentTypes.FusionDesignDocumentType);doc.name=DOCUMENT
    design=af.Design.cast(app.activeProduct);design.designType=af.DesignTypes.ParametricDesignType
    root=design.rootComponent;root.attributes.add('ShappyRS06','owner','rs06-concept-r01')
    for name,n in [('upper_length',200),('lower_length',195),('belt_pitch_length',600),('motor_envelope_diameter',90),('haa_half_spacing',95)]:
        design.userParameters.add(name,L.value(n),'mm','Source parameters.json; regenerate code to change this layout')
    alpha=math.radians(P['pose']['hip_deg']);beta=math.radians(P['pose']['knee_relative_deg']);abd=math.radians(P['pose']['haa_outward_deg'])
    upper_dir=(-math.sin(alpha),0,-math.cos(alpha));lower_dir=(-math.sin(alpha+beta),0,-math.cos(alpha+beta))
    drop=200*math.cos(alpha)+195*math.cos(alpha+beta)
    haa_z=20+drop*math.cos(abd)-(110-95)*math.sin(abd)
    # Pocketed 5mm stock frames, not solid 5mm covers.
    _,frame=component(root,'Pocketed_A6061_body_frames','frame','aluminum')
    for z in (-52.5,52.5):
        body=box((0,0,haa_z+z),(340,290,5))
        for x,length in [(-105,100),(25,110),(125,55)]:
            subtract(body,box((x,0,haa_z+z),(length,250,9)))
        add_temp(frame,'Pocketed_plate_'+str(z),body)
    for y in (-142.5,142.5):
        for z in (-40,40):add_temp(frame,'Side_rail',box((0,y,haa_z+z),(340,5,20)))
    for x in (-172,172):
        bulk=box((x,0,haa_z),(4,290,110))
        for y in (-95,95):
            subtract(bulk,cylinder((x-4,y,haa_z),(x+4,y,haa_z),28))
            for u,v in circles(41,8,math.pi/8):
                subtract(bulk,cylinder((x-4,y+u,haa_z+v),(x+4,y+u,haa_z+v),1.7))
        subtract(bulk,box((x,0,haa_z),(8,75,75)))
        add_temp(frame,'HAA_mount_bulkhead_'+str(x),bulk)
    # Reserved volumes are intentionally identifiable, no product performance implied.
    _,electronics=component(root,'Battery_and_electronics_RESERVED_SPACES','electronics','reserved')
    add_temp(electronics,'Battery_RESERVED_160x90x45',box((-55,0,haa_z-15),(160,90,45)))
    add_temp(electronics,'Computer_RESERVED_105x80x25',box((92,0,haa_z+15),(105,80,25)))
    add_temp(electronics,'Power_RESERVED_80x70x18',box((-55,0,haa_z+26.5),(80,70,18)))
    # Build reusable native templates once. Each occurrence has explicit world transform.
    _,motor_comp=component(root,'RS06_reference_motor','motors','catalog_motor')
    RECORDS.pop()['occurrence'].isLightBulbOn=False
    _,rotor_comp=component(root,'RS06_reference_rotor','motors','motor_rotor')
    RECORDS.pop()['occurrence'].isLightBulbOn=False
    motor(motor_comp,rotor_comp)
    _,upper_comp=component(root,'PETG_upper_link_200','upper_links','PETG');upper(upper_comp)
    upper_template=RECORDS.pop()['occurrence'];upper_template.isLightBulbOn=False
    _,belt_comp=component(root,'S5M_belt_drive_40T_600_25','transmission','belt_drive');belt_and_pulleys(belt_comp)
    belt_template=RECORDS.pop()['occurrence'];belt_template.isLightBulbOn=False
    _,lower_comp=component(root,'C_diagonal_lower_link_195','lower_links','PETG');L.build_link(lower_comp,L.IDS[2])
    lower_template=RECORDS.pop()['occurrence'];lower_template.isLightBulbOn=False
    cup_occ,cup_comp=L.foot_carrier(root);cup_occ.isLightBulbOn=False
    _,lower_hw=component(root,'Lower_shaft_flange_sleeves','hardware','steel');lower_hardware(lower_hw)
    hw_template=RECORDS.pop()['occurrence'];hw_template.isLightBulbOn=False
    _,ball_comp=component(root,'D40_foot_ball_REFERENCE','lower_links','elastomer');L.add_sphere(ball_comp,'Foot_ball_reference',(195,0,0),20)
    ball_template=RECORDS.pop()['occurrence'];ball_template.isLightBulbOn=False
    # Reuse each source occurrence as the first instance, then instance its component.
    TEMPLATES.update({o.component.name:o for o in root.occurrences if not o.isLightBulbOn})
    for leg,front,side in [('FL',1,1),('FR',1,-1),('RL',-1,1),('RR',-1,-1)]:
        xhip=front*P['body']['pitch_axis_x_mm']; xhaa=front*170
        def point_leg(x,y,z):
            dy=y-95
            return (x,side*(95+dy*math.cos(abd)-z*math.sin(abd)),haa_z+dy*math.sin(abd)+z*math.cos(abd))
        def vector_leg(v):
            # Reflection of Y is combined with reversed local Z via matrix() so
            # each occurrence transform remains a proper rigid rotation.
            return (v[0],side*(v[1]*math.cos(abd)-v[2]*math.sin(abd)),v[1]*math.sin(abd)+v[2]*math.cos(abd))
        def link_matrix(origin,dir,y_sign=1):
            return matrix(point_leg(*origin),vector_leg(dir),vector_leg((0,y_sign,0)))
        # HAA is fixed to chassis. Its output drives the complete pitch carrier.
        instance(root,motor_comp,leg+'_HAA','motors','catalog_motor',leg,
                 matrix((xhaa,side*95,haa_z),(0,-front,0),(front,0,0)))
        instance(root,rotor_comp,leg+'_HAA_rotor','motors','motor_rotor',leg,
                 matrix((xhaa,side*95,haa_z),(0,side*math.cos(abd),math.sin(abd)),(front,0,0)))
        instance(root,motor_comp,leg+'_HFE','motors','catalog_motor',leg,
                 matrix(point_leg(xhip,120,0),vector_leg((side,0,0)),vector_leg((0,1,0))))
        instance(root,rotor_comp,leg+'_HFE_rotor','motors','motor_rotor',leg,
                 matrix(point_leg(xhip,120,0),vector_leg(upper_dir),vector_leg((0,1,0))))
        instance(root,motor_comp,leg+'_KFE','motors','catalog_motor',leg,
                 matrix(point_leg(xhip,174,0),vector_leg((-side,0,0)),vector_leg((0,-1,0))))
        instance(root,rotor_comp,leg+'_KFE_rotor','motors','motor_rotor',leg,
                 matrix(point_leg(xhip,174,0),vector_leg(lower_dir),vector_leg((0,-1,0))))
        # Motor carrier ring and overhead bridge; all dimensions in left-leg frame.
        _,carrier=component(root,leg+'_metal_pitch_carrier','frame','aluminum',leg,
                            matrix(point_leg(xhip,0,0),vector_leg((side,0,0)),vector_leg((0,1,0))))
        r1=stator_ring(carrier,'HFE_stator_ring',122);r2=stator_ring(carrier,'KFE_stator_ring',172)
        # Ring tabs terminate at z50; bridge joins these tabs above R48 envelope.
        tabs=[]
        for y in (122,172):
            tabs.append(add_temp(carrier,'Ring_top_tab',box((0,y,48),(22,4,12))))
        bridge=add_temp(carrier,'Top_bridge',box((0,147,56.5),(24,54,5)))
        r1=L.combine(carrier,r1,tabs[0],L.JOIN,'Ring_one_tab_join')
        r2=L.combine(carrier,r2,tabs[1],L.JOIN,'Ring_two_tab_join')
        r1=L.combine(carrier,r1,bridge,L.JOIN,'Ring_bridge_join')
        L.combine(carrier,r1,r2,L.JOIN,'Complete_pitch_carrier')
        # Native mirrored templates use localZ sign flip on the right, so keep
        # carrier top geometry explicitly corrected to world-up if necessary.
        if side<0:
            for b in carrier.bRepBodies:
                # The carrier is symmetric in X, and its transform above keeps Z up.
                pass
        # HAA output adapter and offset arm, created in global geometry then Rx.
        _,arm=component(root,leg+'_abduction_carrier_arm','frame','aluminum',leg,
                        matrix((0,side*95,haa_z),(1,0,0),(0,side*math.cos(abd),math.sin(abd))))
        # localZ points upward on left and downward on right; compensate geometry.
        zs=side
        block=box((front*179,0,zs*23),(6,60,72))
        union(block,cylinder((front*171.5,0,0),(front*176,0,0),26))
        bridge_end=P['body']['pitch_axis_x_mm']-12
        union(block,box((front*(176+bridge_end)/2,52,zs*56.5),(bridge_end-176,110,5)))
        subtract(block,cylinder((front*170,0,0),(front*183,0,0),4))
        for u,v in circles(12,6,math.pi/2):
            subtract(block,cylinder((front*170,u,v),(front*183,u,v),2.15))
        add_temp(arm,'HAA_output_arm_and_bridge',block)
        # Upper assembly rotates about pitch axis; Y coordinates are absolute in leg frame.
        upper_m=link_matrix((xhip,0,0),upper_dir)
        instance(root,upper_comp,leg+'_upper','upper_links','PETG',leg,upper_m)
        instance(root,belt_comp,leg+'_belt','transmission','belt_drive',leg,upper_m)
        # HFE rotor to printed inner cheek, and KFE rotor to proximal pulley.
        _,couplers=component(root,leg+'_HFE_output_adapter','transmission','aluminum',leg,upper_m)
        h=L.disk(couplers,'HFE_output_adapter',0,0,26,8.5,y=125.75,inner=4)
        L.holes(couplers,'HFE_6M4',circles(12,6,math.pi/2),2.15,10,y=126,bodies=[h])
        _,kfe_adapter=component(root,leg+'_KFE_output_adapter','transmission','aluminum',leg,link_matrix((xhip,0,0),lower_dir))
        k=L.disk(kfe_adapter,'KFE_pulley_adapter',0,0,26,5.5,y=169.75,inner=6.1)
        L.holes(kfe_adapter,'KFE_6M4',circles(12,6,math.pi/2),2.15,7,y=170,bodies=[k])
        knee=(xhip+200*upper_dir[0],0,200*upper_dir[2])
        lower_m=link_matrix((knee[0],110,knee[2]),lower_dir)
        instance(root,lower_comp,leg+'_lower_C','lower_links','PETG',leg,lower_m)
        instance(root,cup_comp,leg+'_foot_cup','lower_links','PETG',leg,lower_m)
        instance(root,ball_comp,leg+'_ball','lower_links','elastomer',leg,lower_m)
        # Shaft/flange and bearings share knee axis but follow different rotations.
        hw_m=link_matrix(knee,lower_dir)
        instance(root,lower_hw,leg+'_knee_hardware','hardware','steel',leg,hw_m)
        _,bearings=component(root,leg+'_knee_bearings','hardware','bearing',leg,link_matrix(knee,upper_dir))
        bearing(bearings,132);bearing(bearings,186)
        print('Built '+leg)
    if TEMPLATES:raise RuntimeError('Uninstanced source components: '+str(list(TEMPLATES)))
    # Fusion parametric designs need a captured position after all native features.
    # Applying transforms during feature construction can be rolled back by recompute.
    for rec in RECORDS:rec['occurrence'].transform2=rec['transform']
    if design.snapshots.hasPendingSnapshot:design.snapshots.add()
    for rec in RECORDS:
        for b in rec['occurrence'].component.bRepBodies:
            color='Gray'
            if rec['group']=='motors':color='Gray' if 'output_pilot' in b.name else 'Black'
            elif rec['kind']=='PETG':color='Green' if rec['group']=='lower_links' else 'Blue'
            elif rec['kind']=='elastomer' or 'belt_PITCH' in b.name:color='Black'
            elif rec['group']=='electronics':color='Blue'
            appearance(design,b,color)
    if not design.computeAll():raise RuntimeError('Native recompute failed')
    issues=[{'name':i.name,'message':i.errorOrWarningMessage} for i in design.timeline if i.healthState!=af.FeatureHealthStates.HealthyFeatureHealthState]
    if issues:raise RuntimeError('Timeline issues: '+json.dumps(issues))
    root.attributes.add('ShappyRS06','complete','true')
    output=HERE/'cad';output.mkdir(exist_ok=True)
    # Export native geometry and exactly the same tessellated shapes for the web.
    parts=[];body_records=[];entities=ac.ObjectCollection.create();points_all=[]
    for rec in RECORDS:
        occ=rec['occurrence'];trans=occ.transform2
        for body in occ.component.bRepBodies:
            proxy=body.createForAssemblyContext(occ);entities.add(proxy)
            calculator=body.meshManager.createMeshCalculator()
            calculator.setQuality(af.TriangleMeshQualityOptions.LowQualityTriangleMesh)
            calculator.surfaceTolerance=.025;calculator.maxSideLength=5
            calculator.maxNormalDeviation=0
            mesh=calculator.calculate()
            positions=[];normals=[]
            for p in mesh.nodeCoordinates:
                p.transformBy(trans);xyz=[round(getattr(p,a)*10,5) for a in 'xyz'];positions.extend(xyz);points_all.append(xyz)
            for n in mesh.normalVectors:
                n.transformBy(trans);normals.extend([round(getattr(n,a),6) for a in 'xyz'])
            color=COLORS[rec['group']]
            if 'belt_PITCH' in body.name or rec['kind']=='elastomer':color=[.09,.11,.14]
            if 'output_pilot' in body.name:color=[.67,.70,.74]
            parts.append({'name':body.name,'component':rec['role'],'group':rec['group'],'kind':rec['kind'],'leg':rec['leg'],
                          'positions':positions,'normals':normals,'indices':list(mesh.nodeIndices),'color':color})
            body_records.append({'component':rec['role'],'body':body.name,'group':rec['group'],'kind':rec['kind'],'leg':rec['leg'],
                'volume_mm3':body.volume*1000,'is_solid':body.isSolid,
                'bounds_mm':[[getattr(proxy.boundingBox.minPoint,a)*10 for a in 'xyz'],[getattr(proxy.boundingBox.maxPoint,a)*10 for a in 'xyz']]})
    bounds=[[min(p[i] for p in points_all) for i in range(3)],[max(p[i] for p in points_all) for i in range(3)]]
    scene={'revision':P['revision'],'units':'mm','source':'Native Fusion BRep tessellation, 0.25 mm requested tolerance',
           'bounds_mm':bounds,'parts':parts,'limitations':P['limitations']}
    (HERE/'viewer/scene.json').write_text(json.dumps(scene,separators=(',',':'))+'\n')
    inp=design.createInterferenceInput(entities);inp.areCoincidentFacesIncluded=False
    interferences=design.analyzeInterference(inp)
    def identity(entity):
        return (entity.assemblyContext.name if entity.assemblyContext else entity.parentComponent.name)+' / '+entity.name
    collisions=[{'one':identity(i.entityOne),'two':identity(i.entityTwo),'volume_mm3':i.interferenceBody.volume*1000} for i in interferences if i.interferenceBody.volume*1000>1e-4]
    report={'revision':P['revision'],'document':DOCUMENT,'body_count':len(body_records),'motor_count':12,'timeline_features':design.timeline.count,
       'timeline_issues':issues,'haa_height_mm':haa_z,'bounds_mm':bounds,'body_records':body_records,'standing_interferences':collisions,
       'collision_scope':'single standing pose; pitch-path belt/pulley tooth envelopes intentionally overlap; unresolved structural overlaps require correction',
       'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),'parameters_sha256':hashlib.sha256((HERE/'parameters.json').read_bytes()).hexdigest(),
       'status':'layout only; no full-robot FEA, dynamic simulation, physical test, or manufacturing release'}
    (output/'cad_checks.json').write_text(json.dumps(report,indent=2)+'\n')
    manager=design.exportManager
    for option in [manager.createSTEPExportOptions(str(output/'shappy_rs06_r01.step')),
                   manager.createFusionArchiveExportOptions(str(output/'shappy_rs06_r01.f3d'))]:
        if not manager.execute(option):raise RuntimeError('CAD export failed')
    camera=app.activeViewport.camera;camera.eye=pt((850,-1050,700));camera.target=pt((0,0,180));camera.upVector=ac.Vector3D.create(0,0,1)
    camera.cameraType=ac.CameraTypes.OrthographicCameraType;camera.isFitView=True;camera.isSmoothTransition=False
    app.activeViewport.camera=camera;app.activeViewport.fit()
    image=ac.SaveImageFileOptions.create(str(output/'assembly.png'));image.width=1800;image.height=1200
    if not app.activeViewport.saveAsImageFileWithOptions(image):raise RuntimeError('Image export failed')
    print(json.dumps({'bodies':len(body_records),'triangles':sum(len(p['indices'])//3 for p in parts),'collisions':len(collisions),'haa_height_mm':haa_z,'bounds_mm':bounds}))
