"""1500 slave, boss, housing and release relationships. All fitted registration unknown."""
from pathlib import Path
import json,math,hashlib
import cadquery as cq
from gearbox_reference_layout import FRONT,HOUSING_CASE_FIXING_XY,HOUSING_CASE_BORE_RADIUS,HOUSING_GASKET_THICKNESS,HOUSING_REAR_WEB_THICKNESS
from rear_engine_interface_layout import SCALE_BASIS,STARTER_AXIS_X,PLATE_GAUGE,rear_plate_outline
OUT=Path(__file__).parent;S=2032/2.4;parts=[];X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1)
def P(x,y,z):return cq.Vector(x*S,y*S,z*S)
def cyl(r,l,p,a=Z):return cq.Solid.makeCylinder(r,l,p,a)
def ring(ro,ri,l,p,a=Z):return cyl(ro,l,p,a).cut(cyl(ri,l+2,p-a,a))
def box(w,h,d,p):return cq.Workplane('XY').box(w,h,d).translate(p.toTuple()).val()
def comp(xs):return cq.Compound.makeCompound(xs)
def add(n,s,c=(.45,.48,.45)):
 assert s.isValid() and s.Volume()>0,n
 parts.append((n,s,c));return s
def fastener(r,l,p,a):
 head=cq.Workplane(cq.Plane(origin=p-a*5,normal=a)).polygon(6,14).extrude(5).val();return comp([cyl(r,l,p,a),head,ring(8,r+.2,1.2,p-a*1.2,a)])
# Factory p79/donor25147 support the housing architecture. Its new mouth
# shares the estimated rear-plate outline and back plane; fitted geometry unknown.
rear=FRONT;front=.63*S-PLATE_GAUGE;length=front-rear;web_start=rear+HOUSING_GASKET_THICKNESS
chamber_x=STARTER_AXIS_X;chamber_start=front-30
def rear_outline(thickness,z):
 # Estimated rear casting web includes the slave mounting projection.
 return (cq.Workplane('XY',origin=(0,0,z)).polyline([(-110,68),(104,77),(199,77),(199,-77),(85,-151),(-85,-151),(-110,-126)]).close().extrude(thickness).val())
rear_wire=cq.Workplane(obj=rear_outline(1,web_start-1)).faces('>Z').val().outerWire()
front_wire=rear_plate_outline(0,front).val()
rear_inner=rear_wire.offset2D(-6)[0]
front_inner=front_wire.offset2D(-6)[0]
outer=cq.Solid.makeLoft([rear_wire,front_wire],ruled=True)
inner=cq.Solid.makeLoft([rear_inner.translate((0,0,-1)),front_inner.translate((0,0,1))],ruled=True)
shell=outer.cut(inner)
flange_inner=front_wire.offset2D(-8)[0]
shell=shell.fuse(cq.Solid.extrudeLinear(front_wire,[flange_inner],cq.Vector(0,0,-6)))
shell=shell.cut(cyl(40,32,cq.Vector(chamber_x,0,chamber_start)))
web=rear_outline(HOUSING_REAR_WEB_THICKNESS,web_start).cut(cyl(17.4,HOUSING_REAR_WEB_THICKNESS+2,cq.Vector(0,0,web_start-1)))
slave_entry_end=.415*S+20
shell=shell.fuse(web).cut(cyl(.036*S,slave_entry_end-.326*S,P(.19,0,.326)))
# Keep the estimated gasket layer separate from the old conical skirt.
shell=shell.cut(box(1000,1000,HOUSING_GASKET_THICKNESS+2,cq.Vector(0,0,rear+(HOUSING_GASKET_THICKNESS-2)/2)))
A=P(.19,0,.30);boss_axes=[cq.Vector(A.x,dy,.34*S) for dy in [-58,58]]
# Unmeasured integral housing ear roles connect the formerly floating boss flange.
for q in boss_axes:
 ear=box(70,30,8,cq.Vector(q.x,q.y,rear+4));shell=shell.fuse(ear)
 shell=shell.cut(cyl(4.5,35,q-Z*12))
pivot=P(-.155,0,.415)
# Two open saddles and short webs connect the retained pivot center to the shell.
for sign in [-1,1]:
 saddle=box(34,16,40,pivot+Y*sign*12)
 web=box(30,12,34,cq.Vector(-145,sign*15,pivot.z))
 shell=shell.fuse(saddle).fuse(web)
shell=shell.cut(cyl(9.2,60,pivot-Y*30,Y)).clean()
# Estimated open space between pivot saddles permits the retained fork eye.
# This is a reference window, not a measured casting or verified swept path.
shell=shell.cut(cyl(14.5,8,pivot-Y*4,Y))
pivot_throat=cq.Workplane(obj=box(35,8,36,cq.Vector(-118,0,358))).edges('|Y').fillet(6).val()
shell=shell.cut(pivot_throat)
for x,y in HOUSING_CASE_FIXING_XY:shell=shell.cut(cyl(HOUSING_CASE_BORE_RADIUS,HOUSING_REAR_WEB_THICKNESS+4,cq.Vector(x,y,web_start-2)))
add('clutch_hollow_housing_integral_boss_ears_and_pivot_saddles_comparison',shell,(.53,.56,.54))
# Split boss: widened flange and lobes are new estimates, not a fitted casting.
boss=ring(.058*S,.036*S,.08*S,P(.19,0,.26))
flange=box(100,146,.018*S,cq.Vector(A.x,0,rear-.009*S)).cut(cyl(.036*S,24,cq.Vector(A.x,0,rear-.018*S-1)))
boss=boss.fuse(flange)
boss=boss.cut(box(30,3,.10*S,A+X*43))
for sign in [-1,1]:boss=boss.fuse(box(17,9,22,A+X*40+Y*sign*6))
pinch=A+X*40;boss=boss.cut(cyl(4.25,40,pinch-Y*20,Y))
for q in boss_axes:boss=boss.cut(cyl(4.5,35,q-Z*12))
add('clutch_split_slave_boss_flange_and_two_mount_bores_comparison',boss,(.53,.56,.54))
add('clutch_two_boss_to_housing_screw_envelopes',comp([fastener(4,28,q-Z*(.018*S),Z) for q in boss_axes]),(.54,.56,.49))
pinchbolt=fastener(4,30,pinch-Y*13,Y);nut=cq.Workplane(cq.Plane(origin=pinch+Y*12,normal=Y)).polygon(6,14).extrude(6).val().cut(cyl(4.2,10,pinch+Y*10,Y))
add('clutch_separate_boss_pinch_bolt_and_nut_envelopes',comp([pinchbolt,nut]),(.54,.56,.49))
# Cylinder/passage is a hollow exterior reference, not a measured hydraulic bore.
slave=ring(.034*S,.023*S,.20*S,P(.19,0,.16)).fuse(cyl(.034*S,.008*S,P(.19,0,.152)))
add('clutch_slave_hollow_exterior_and_closed_rear_envelope',slave,(.34,.38,.34))
boot=ring(.037*S,.014*S,.035*S,P(.19,0,.3425));add('clutch_slave_separate_open_dust_boot_comparison',boot,(.13,.16,.13))
# Separate open port and bleed-screw roles, without a master connection or inner fluid path.
ports=comp([ring(8.5,3,32,P(.19,.022,.23),Y),ring(7,2,26,P(.19,.025,.18),Y)])
add('clutch_slave_open_inlet_and_bleed_screw_envelopes',ports,(.54,.55,.48))
# The rod eye moves below the lever web; end retains Y=0 and catalog center/end length.
L=77.7875;eye=P(.19,0,.415)-Y*12;end=eye+Y*12-Z*math.sqrt(L*L-144);axis=(eye-end).normalized()
rod=cyl(6,(eye-end).Length,end,axis).fuse(ring(12,6.2,8,eye-Y*4,Y)).cut(cyl(6.2,14,eye-Y*7,Y)).clean()
add('clutch_tilted_catalog_length_pushrod_and_bored_eye_comparison',rod,(.54,.55,.48))
# Factory p79 and donor front view show an open fork around the guide/carrier.
# Replace the invented55 mm downward bow by an estimated open frame. Existing
# pivot/rod and carrier pin stations stay; no working leverage/travel inferred.
fork_z=.465*S
fork_sections=[(-.155*S,3,.415*S,22),(-112,3,361,20),(-105,10,365,18),(-85,18,378,18),(-45,45,fork_z,18),(0,58,fork_z,18),(45,45,fork_z,18),(100,20,365,18),(.19*S,3,.415*S,22)]
sections=[cq.Workplane('YZ',origin=(x,0,zc)).rect(2*half_y,depth).val() for x,half_y,zc,depth in fork_sections]
lever=cq.Solid.makeLoft(sections,ruled=True)
lever=lever.cut(cyl(38,150,P(0,0,.34)))
for q in [pivot,P(.19,0,.415)]:lever=lever.fuse(ring(14,6.2,6,q-Y*3,Y))
plug_axes=[P(sign*.047,0,.465) for sign in [-1,1]]
for q in [pivot,P(.19,0,.415)]:lever=lever.cut(cyl(6.2,14,q-Y*7,Y))
for q in plug_axes:lever=lever.cut(cyl(7,140,q-Y*70,Y))
lever=lever.clean()
add('clutch_release_lever_shared_pivot_rod_and_open_carrier_saddles_comparison',lever,(.40,.43,.39))
bushes=comp([ring(9,6.1,16,pivot+Y*s*4,Y*s) for s in [-1,1]])
add('clutch_two_housing_pivot_bush_comparisons',bushes,(.58,.49,.33))
pin=cyl(6,58,pivot-Y*29,Y).fuse(cyl(8,3,pivot-Y*32,Y));tolerance=ring(8.8,8.2,8,pivot+Y*21,Y)
add('clutch_pivot_pin_and_smooth_tolerance_ring_envelopes',comp([pin,tolerance]),(.54,.55,.48))
rod_pin=cyl(6,26,P(.19,0,.415)-Y*17,Y).fuse(cyl(8,2,P(.19,0,.415)-Y*19,Y));rod_wash=ring(9,6.2,1.2,P(.19,0,.415)+Y*6,Y)
add('clutch_separate_rod_eye_joint_pin_and_retainer_envelopes',comp([rod_pin,rod_wash]),(.54,.55,.48))
carrier=ring(.043*S,.022*S,.060*S,P(0,0,.43))
for q in plug_axes:carrier=carrier.cut(cyl(7,20,q-Y*10,Y))
add('clutch_hollow_release_carrier_reference',carrier,(.40,.43,.39))
bearing=ring(.053*S,.022*S,.015*S,P(0,0,.49));add('clutch_separate_annular_release_bearing_envelope',bearing,(.56,.57,.50))
plugs=comp([cyl(6.8,16,q-Y*8,Y) for q in plug_axes]);add('clutch_two_retained_carrier_engagement_pin_comparisons',plugs,(.54,.55,.48))
# Separate unloaded gasket and four screw/washer roles. The plan and drilling
# map are construction choices; no fitted gasket, thread or clamping is known.
gasket=cq.Workplane('XY').box(220,219,HOUSING_GASKET_THICKNESS).edges('|Z').fillet(25).translate((0,-41.5,rear+HOUSING_GASKET_THICKNESS/2)).val()
gasket=gasket.cut(cyl(17.4,HOUSING_GASKET_THICKNESS+2,cq.Vector(0,0,rear-1)))
case_screws=[]
for x,y in HOUSING_CASE_FIXING_XY:
 gasket=gasket.cut(cyl(HOUSING_CASE_BORE_RADIUS,HOUSING_GASKET_THICKNESS+2,cq.Vector(x,y,rear-1)))
 z=web_start+HOUSING_REAR_WEB_THICKNESS
 washer=ring(9,5.2,1.2,cq.Vector(x,y,z))
 head=cq.Workplane('XY',origin=(x,y,z+1.2)).polygon(6,16).extrude(6).val()
 shank=cyl(4.7,20,cq.Vector(x,y,z+1.2),-Z)
 case_screws.extend([washer,head.fuse(shank)])
add('clutch_housing_case_uncompressed_gasket_comparison',gasket,(.31,.25,.18))
add('clutch_four_housing_to_case_screws_and_unloaded_washers',comp(case_screws),(.54,.56,.49))
# Source topology checks. No fitted loads, actuation or seal compression implied.
for q in boss_axes:
 for s in [shell,boss]:assert s.intersect(cyl(4.3,32,q-Z*12)).Volume()<1e-5
assert boss.intersect(cyl(4.1,36,pinch-Y*18,Y)).Volume()<1e-5
assert shell.intersect(cyl(9,58,pivot-Y*29,Y)).Volume()<1e-5
assert lever.intersect(cyl(6,12,P(.19,0,.415)-Y*6,Y)).Volume()<1e-5
assert lever.intersect(rod).Volume()<1e-5
assert len(lever.Solids())==1
assert lever.intersect(shell).Volume()<1e-5
assert lever.intersect(cyl(37,150,P(0,0,.34))).Volume()<1e-5
assert lever.intersect(box(4,4,4,cq.Vector(0,50,fork_z))).Volume()>60
assert abs((eye-end).Length-L)<1e-8
assert len(shell.Solids())==1
assert shell.intersect(gasket).Volume()<1e-5
assert shell.intersect(boss).Volume()<1e-5
assert shell.intersect(cyl(17.2,HOUSING_REAR_WEB_THICKNESS+2,cq.Vector(0,0,web_start-1))).Volume()<1e-5
assert shell.intersect(cyl(1,HOUSING_REAR_WEB_THICKNESS-2,cq.Vector(30,0,web_start+1))).Volume()>1
for x,y in HOUSING_CASE_FIXING_XY:
 for s in [shell,gasket]:
  assert s.intersect(cyl(5,24,cq.Vector(x,y,rear-2))).Volume()<1e-5
 assert shell.intersect(cyl(1,6,cq.Vector(x+8,y,web_start+1))).Volume()>1
 assert gasket.intersect(cyl(1,.6,cq.Vector(x+8,y,rear+.1))).Volume()>.5
assert shell.intersect(box(4,4,4,cq.Vector(chamber_x,0,front-16))).Volume()<1e-5
assert shell.intersect(box(1,1,1,cq.Vector(chamber_x+50,0,front-3))).Volume()>.9
# Report contact among source roles and neighboring original transmission geometry.
contacts={}
pairs=[('boss_cylinder',boss,slave),('boot_rod',boot,rod),('housing_lever',shell,lever),('housing_carrier',shell,carrier),('boss_housing',boss,shell),('lever_carrier',lever,carrier),('lever_engagement_pins',lever,plugs)]
trans=cq.Assembly.importStep(str(OUT/'transmission-reference.step'))
for name in ['gearbox_main_case_reference','gearbox_input_guide_reference']:
 o=trans.objects[name].obj;o=o.val() if isinstance(o,cq.Workplane) else o
 for n,s,c in parts:
  a,b=s.BoundingBox(),o.BoundingBox()
  if all(getattr(a,k+'min')<=getattr(b,k+'max') and getattr(b,k+'min')<=getattr(a,k+'max') for k in ['x','y','z']):pairs.append((n+'__'+name,s,o))
for n,a,b in pairs:
 v=round(a.intersect(b).Volume(),4)
 if v>1e-4:contacts[n]=v
limits='No direct owner hidden clutch dimensions, fitted identities or common installed survey applied. Factory p79 and donor front photograph support the open fork, rear web, side chamber, split slave boss and separate attachment roles; every profile, gauge, passage and coordinate remains a construction estimate. The earlier55 mm downward lever bow is superseded by an open frame with estimatedR38 guide/carrier opening and lofted arms. Pivot/rod and carrier pin stations stay; hole shapes and frame sections do not establish operating leverage, motion or loaded pin contact. The slave-entry comparison extends along the existing axis through the housing skirt; this is an estimated architecture opening, not a fitted casting repair. Catalog rod77.7875 mm is hole-center to end per Moss p53, distinct from another suppliers overall-length wording. Rod eye stays12 mm below the lever plane; end geometry does not establish insertion, free play or working stroke. Boss/cylinder gap1.6933 mm remains an inherited construction estimate, not loaded clamp fit. Master circuit and slave inlet remain unassigned; no piston/seal/fluid circuit is reconstructed. Carrier pin/plug/dowel quantity map, bearing internals, pressure-plate registration, fitted seal branch and actual retention remain unresolved. No preload, torque, support/release sequence, tool clearance or verified replacement/removal path. Separation is illustrative.'
housing_joint=dict(conditional_scale_basis=SCALE_BASIS,factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=79',supplier_source='https://www.rwautoclassics.com/viewitem/?id=25147',supplier_front_image='https://www.rwautoclassics.com/img/1620/1620763910_615558117.jpg',case_front_z_mm=rear,case_fixing_axes_xy_mm=HOUSING_CASE_FIXING_XY,bore_radius_mm=HOUSING_CASE_BORE_RADIUS,uncompressed_gasket_thickness_mm=HOUSING_GASKET_THICKNESS,rear_web_z_bounds_mm=[web_start,web_start+HOUSING_REAR_WEB_THICKNESS],input_guide_passage_radius_mm=17.4,front_mouth_plane_local_z_mm=front,previous_mouth_nominal_plane_z_mm=.63*S,front_mouth_outer_bounds_local_xy_mm=[[front_wire.BoundingBox().xmin,front_wire.BoundingBox().xmax],[front_wire.BoundingBox().ymin,front_wire.BoundingBox().ymax]],estimated_wall_mm=6,estimated_front_flange_depth_width_mm=[6,8],side_chamber=dict(center_x_mm=chamber_x,start_z_mm=chamber_start,end_z_mm=front,front_opening_radius_mm=40,basis='Starter lobe shares the rear-plate silhouette and conditional replacement-pitch starter axis; previous independent228.6 mm cone cue is superseded.'),pivot_opening=dict(eye_radius_mm=14.5,eye_y_bounds_mm=[-4,4],throat_center_local_mm=[-118,0,358],throat_bounds_mm=[35,8,36],corner_radius_mm=6,limits='Estimated static clevis/pivot opening only; no measured casting or verified operating/removal sweep.'),boss_flange=dict(width_mm=100,height_mm=146,z_bounds_mm=[rear-.018*S,rear]),limits='Factory housing/case fixing and gasket roles remain documentary. Front contour shares the replacement-plate silhouette under a conditional125.45 mm modern replacement pitch scale; original starter pitch, photo hole roles and fitted scale remain unverified. All loft profiles,6 mm wall/flange depth,8 mm flange width, rear web, pivot/slave openings, bores and nominal joint planes are estimates. Housing mouth now meets the reference plate back face atZ527.4 mm rather than straddling the rear-block plane. No fitted casting312151 identity, material, complete bolt/dowel map, threads, loaded seal/joint, starter engagement or verified clearance/removal path is established.')
limits+=' Rear case/gasket and boss/ear stations stay. The enlarged circular mouth and independent starter cone are superseded by a asymmetric loft to the reference plate outline; the mouth ends at its estimated back face. Pivot and rod/carrier/bearing axes remain, with an estimated open pivot clevis and shorter supporting webs. Static source nonintersection does not verify a loaded joint or motion. Boot/rod and engine/starter/plate discrepancies remain explicit. '+housing_joint['limits']
meta=dict(units='mm',source_frame='Gearbox-local mm +X vehicle-left +Y up +Z front; translated to body [0,.39,0].',factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=79',supplier_sources=['https://rimmerbros.com/Item--i-206175','https://mossmotors.com/media/wysiwyg/catalogpdf/spm_catalog_web.pdf#page=53','https://bpnorthwest.com/clutch-hardware/clutch-slave-push-rod-spitfire-1500-midget/','https://www.rwautoclassics.com/viewitem/?id=25147','https://mossmotors.com/832-545-alloy-rear-engine-plate'],owner_dimensions_applied=False,basis='Factory 1500 housing/slave/boss/release roles, supplier split-boss silhouette and donor housing photograph. Connected rear web, four case fixings, unloaded gasket and open release fork clarify the assembly architecture. The housing front contour shares an unscaled replacement rear-plate silhouette under a conditional modern replacement-pitch scale. All geometry and common installed registration remain estimates; master circuit identities remain provisional.',registration_status='unregistered_1500_clutch_relationship_comparison',housing_case_joint=housing_joint,nodes=dict(boss_mount_axes_local_mm=[list(q.toTuple()) for q in boss_axes],pinch_axis_center_local_mm=list(pinch.toTuple()),slave_axis_local_x_mm=A.x,pivot_center_local_mm=list(pivot.toTuple()),rod_eye_center_local_mm=list(eye.toTuple()),rod_end_local_mm=list(end.toTuple()),rod_hole_center_to_end_catalog_mm=L,rod_joint_axis_center_local_mm=list(P(.19,0,.415).toTuple()),lever_center_construction_drop_mm=0,estimated_fork_sections_x_half_y_z_depth_mm=fork_sections,estimated_fork_guide_opening_radius_mm=38,estimated_slave_entry_z_bounds_mm=[.326*S,slave_entry_end],carrier_engagement_axes_local_mm=[list(q.toTuple()) for q in plug_axes]),sampled_source_contacts_mm3=contacts,limits=limits)
a=cq.Assembly(name='unregistered_1500_clutch_slave_mount_and_release_comparisons');meshes=[]
# Reconcile existing source frames without moving either assembly to force fit.
body=json.loads((OUT.parent/'body-data.json').read_text());placement=body['native_engine_placement']
engine_bytes=(OUT/placement['source_url'].split('/')[-1].split('?')[0]).read_bytes()
engine_parts={p['name']:p for p in json.loads(engine_bytes)['parts']}
offset=[placement['body_anchor_units'][i]*S-placement['native_anchor_mm'][i]-(.39*S if i==1 else 0) for i in range(3)]
def engine_z(name):
 v=engine_parts[name]['vertices'][2::3];return [min(v)+offset[2],max(v)+offset[2]]
cover_z=engine_z('clutch_cover_catalog_reference_provisional')
old_bearing_z=engine_z('clutch_release_bearing_catalog_reference_provisional')
pilot_z=engine_z('pilot_bushing_supplier_dimensions_provisional')
bush_ref=json.loads((OUT.parent/'cad-data.json').read_text())['flywheel_and_clutch_reconstruction']['matching_family_spigot_bush_reference_mm']
pilot_center=cq.Vector(placement['native_anchor_mm'][0]+offset[0],placement['native_anchor_mm'][1]+offset[1],pilot_z[0])
pilot_comparison=ring(bush_ref['outside_diameter']/2,bush_ref['inside_diameter']/2,bush_ref['length'],pilot_center)
actual_input=trans.objects['gearbox_input_shaft_reference'].obj
actual_input=actual_input.val() if isinstance(actual_input,cq.Workplane) else actual_input
input_box=actual_input.BoundingBox();bearing_box=bearing.BoundingBox()
assert actual_input.intersect(pilot_comparison).Volume()<1e-5
previous_plain_input=cyl(.012*S,.27*S,cq.Vector(0,0,FRONT))
meta['engine_interface_reconciliation']=dict(engine_source_url=placement['source_url'],engine_source_sha256=hashlib.sha256(engine_bytes).hexdigest(),engine_to_gearbox_translation_mm=offset,engine_cover_z_mm=cover_z,whole_car_release_bearing_z_mm=[bearing_box.zmin,bearing_box.zmax],omitted_engine_release_bearing_z_mm=old_bearing_z,reference_cover_to_bearing_separation_mm=cover_z[0]-bearing_box.zmax,reference_bearing_front_difference_mm=old_bearing_z[1]-bearing_box.zmax,pilot_bush_z_mm=pilot_z,pilot_bush_catalog_dimensions_mm=bush_ref,input_tip_to_bush_axial_interval_overlap_mm=max(0,min(input_box.zmax,pilot_z[1])-max(input_box.zmin,pilot_z[0])),previous_plain_rod_bush_comparison_intersection_mm3=round(previous_plain_input.intersect(pilot_comparison).Volume(),6),current_input_bush_comparison_intersection_mm3=round(actual_input.intersect(pilot_comparison).Volume(),6),limits='Unchanged estimated engine and gearbox placements compared in one frame. All axial gaps/overlaps are reference-model discrepancies, not actual-car clearance, insertion or free play. The reduced pilot tip clears an annular catalog-bush comparison at its exported station; neither shaft length nor fitted bush/shaft identity, running fit, spline engagement or release travel is established. The omitted engine bearing remains a documentary alternative, not a second displayed bearing. No engine, cover, bearing/carrier or mounting datum is moved to hide the axial mismatch.')
for n,s,c in parts:
 a.add(s,name=n,color=cq.Color(*c));vs,ts=s.tessellate(.5,.16);meshes.append(dict(name=n,color=c,vertices=[round(v,4) for p in vs for v in p.toTuple()],triangles=[i for t in ts for i in t]))
a.export(str(OUT/'clutch-release-reference.step'));meta['parts']=meshes;(OUT/'clutch-release-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n');s=cq.importers.importStep(str(OUT/'clutch-release-reference.step')).val();assert s.isValid()
assert all(q.isValid() and q.Volume(1e-9)>0 for q in s.Solids())
assert len(s.Solids())==sum(len(q.Solids()) for _,q,_ in parts)
assert math.isclose(s.Volume(1e-9),sum(q.Volume(1e-9) for _,q,_ in parts),rel_tol=1e-7)
print(json.dumps(dict(meshes=len(parts),solids=len(s.Solids()),step_valid=True,shared_housing_case_joint=True,hollow_side_chamber=True,shared_boss_pinch_pivot_rod_bores=True,catalog_rod_length_retained=True,rod_lever_nonintersection=True,source_contacts=contacts)))
