"""Inner comparisons and estimated outer fulcrum topology; no fitted fit claimed."""
from pathlib import Path
import json,math
import cadquery as cq
OUT=Path(__file__).parent;S=json.loads((OUT.parent/'body-data.json').read_text())['published_reference_dimensions_mm']['wheelbase']/2.4
parts=[];nodes={}
def V(p):return cq.Vector(*(v*S for v in p))
def cm(r,l,c,z):return cq.Solid.makeCylinder(r,l,c+cq.Vector(0,0,z),cq.Vector(0,0,1))
def rm(ro,ri,l,c,z):return cm(ro,l,c,z).cut(cm(ri,l+2,c,z-1))
def add(name,shape,color=(.28,.31,.28)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color))
for side,label in [(1,'left'),(-1,'right')]:
 # Retained plan/height stations, with an estimated thin sheet and downturned lip.
 plane=cq.Plane(origin=(0,.28*S,0),xDir=(1,0,0),normal=(0,-1,0))
 outline=[(.34,.98),(.40,.98),(.61,1.14),(.61,1.26),(.40,1.42),(.34,1.42)]
 wire=cq.Workplane(plane).polyline([(side*x*S,z*S) for x,z in outline]).close()
 pan=wire.extrude(.025*S).val()
 innerwire=cq.Workplane(plane).polyline([(side*x*S,z*S) for x,z in outline]).close().wires().offset2D(-.003*S).val()
 cavity=cq.Solid.extrudeLinear(innerwire,[],cq.Vector(0,-.027*S,0)).translate((0,-.003*S,0))
 pan=pan.cut(cavity)
 spring=cq.Solid.makeCylinder(.095*S,.04*S,V([side*.45,.245,1.20]),cq.Vector(0,1,0));pan=pan.cut(spring)
 eyes=[];rubbers=[];pins=[];retainers=[];centers=[]
 for z in [.98,1.42]:
  c=V([side*.36,.27,z]);centers.append([side*.36,.27,z])
  pan=pan.cut(cm(12.6,48,c,-24))
  eye=cm(15.5,38,c,-19)
  for direction in [-1,1]:
   a=c;axis=cq.Vector(0,0,direction)
   seat=cq.Solid.makeCone(11.6,12.6,19,a,axis)
   eye=eye.cut(seat)
   # Listed radial section only. Axial body/lip dimensions are estimated.
   rubber=cq.Solid.makeCone(11.5,12.5,19,a,axis)
   flange=cq.Solid.makeCylinder(14.5,3,a+axis*19,axis)
   rubber=rubber.fuse(flange).cut(cm(6.25,48,c,-24))
   rubbers.append(rubber)
  eyes.append(eye)
  # 3/8 UNF catalog terminal diameter only; shoulder/length/head are estimates.
  pin=cm(11,5,c,-33).fuse(cm(6,56,c,-28)).fuse(cm(9.525/2,14,c,28))
  pins.append(pin)
  washer=rm(14.5,5.1,2,c,28)
  nut=(cq.Workplane('XY',origin=(c.x,c.y,c.z+30)).polygon(6,16.7).extrude(9).val().cut(cm(4.9,11,c,29)))
  retainers.extend([washer,nut])
 # Earlier factory shared-front drawing: two threaded arm ends surround the
 # kingpin foot. Sections below are estimates, not measured thread sizes.
 c=V([side*.58,.27,1.20]);zaxis=cq.Vector(0,0,1)
 slot=cq.Solid.makeBox(.14*S,.12*S,40,V([side*.58-.07,.21,1.20])+cq.Vector(0,0,-20))
 pan=pan.cut(slot).cut(cm(8.25,110,c,-55))
 fork=[]
 for start,end,bore in [(-.06*S,-20,9.25),(20,.06*S,10.25)]:
  fork.append(rm(.037*S,bore,end-start,c,start))
  pan=pan.cut(cm(bore,end-start+2,c,start-1))
 # Owner IMG_6914 shows the flat plate on the forward-facing pan wall,
 # not a horizontal base over the outer joint. Metric sections are estimates.
 tangent=cq.Vector(side*.21,0,-.16).normalized();normal=cq.Vector(side*.16,0,.21).normalized()
 front_x=.535;front_z=1.42-(front_x-.40)*.16/.21
 attach=V([side*front_x,.255,front_z]);up=cq.Vector(0,1,0)
 def prism(poly,origin,depth):
  points=[origin+tangent*u+up*v for u,v in poly]
  wire=cq.Wire.makePolygon(points+[points[0]])
  return cq.Solid.extrudeLinear(wire,[],normal*depth)
 # Extend only the front wall below the old 21.17 mm lip, to a declared 40 mm.
 web_t=[(.43-front_x)*S/abs(tangent.x),(.60-front_x)*S/abs(tangent.x)]
 depth=40-(.28-.255)*S
 web=prism([(web_t[0],0),(web_t[1],0),(web_t[1],-depth),(web_t[0],-depth)],attach,-.003*S)
 holes=[]
 for u,v in [(-32,-10),(-32,10),(10,-5)]:
  h=attach+tangent*u+up*v;holes.append(list(h.toTuple()))
  cut=cq.Solid.makeCylinder(4.5,20,h-normal*10,normal)
  pan=pan.cut(cut);web=web.cut(cut)
 arm=cq.Compound.makeCompound([pan,*eyes,*fork,web]);add('front_'+label+'_pressed_wishbone_and_eye_reference',arm)
 for h in holes:
  center=cq.Vector(*h)
  assert arm.intersect(cq.Solid.makeCylinder(4,18,center-normal*9,normal)).Volume()<1e-5
  assert arm.intersect(cq.Solid.makeCylinder(1,18,center+tangent*7-normal*9,normal)).Volume()>1

 add('front_'+label+'_four_tapered_inner_bush_comparisons',cq.Compound.makeCompound(rubbers),(.13,.16,.13))
 add('front_'+label+'_two_stepped_inner_pin_comparisons',cq.Compound.makeCompound(pins),(.48,.49,.44))
 add('front_'+label+'_inner_special_washer_nut_comparisons',cq.Compound.makeCompound(retainers),(.48,.49,.44))
 # Replace the old combined upright's full-height solid pin with a separate
 # kingpin. The inclined shaft retains the old drawing axis and radius.
 axis=(V([side*.55,.65,1.20])-c).normalized();shaft_start=c+axis*14
 shaft=cq.Solid.makeCylinder(.022*S,(V([side*.55,.65,1.20])-shaft_start).Length,shaft_start,axis)
 foot=cm(20,30,c,-15).fuse(shaft).cut(cm(8.25,36,c,-18))
 cotter_c=c+cq.Vector(side*8.5,0,0)
 cotter_hole=cq.Solid.makeCylinder(2.7,52,cotter_c+cq.Vector(0,-26,0),cq.Vector(0,1,0))
 foot=foot.cut(cotter_hole)
 # Estimated reduced terminal above the retained shaft-top datum; no thread.
 foot=foot.fuse(cq.Solid.makeCylinder(8,14,V([side*.55,.65,1.20]),axis))
 add('front_'+label+'_kingpin_and_bored_lower_foot_reference',foot,(.48,.49,.44))
 pin=cm(9,28,c,-48).fuse(cm(8,40,c,-20)).fuse(cm(10,28,c,20))
 flat=cq.Solid.makeBox(20,30,20,c+cq.Vector(6 if side==1 else -26,-15,-10))
 pin=pin.cut(flat)
 add('front_'+label+'_stepped_outer_fulcrum_pin_reference',pin,(.48,.49,.44))
 cotter=cq.Solid.makeCylinder(2.5,47,cotter_c+cq.Vector(0,-27,0),cq.Vector(0,1,0))
 washer=cq.Solid.makeCylinder(6,1.5,cotter_c+cq.Vector(0,-21.5,0),cq.Vector(0,1,0)).cut(cotter_hole)
 nutplane=cq.Plane(origin=cotter_c+cq.Vector(0,-26,0),xDir=(1,0,0),normal=(0,1,0))
 nut=cq.Workplane(nutplane).polygon(6,10).extrude(4.5).val().cut(cotter_hole)
 add('front_'+label+'_lower_cotter_washer_nut_reference',cq.Compound.makeCompound([cotter,washer,nut]),(.58,.48,.30))
 small=cm(9,4,c,-52);large=cm(10,4,c,48).fuse(cm(13,3,c,52))
 grease=cm(3,5,c,55).fuse(cm(4,2,c,60))
 seals=[rm(13,8.25,5,c,z) for z in [-20,15]]
 add('front_'+label+'_outer_end_plugs_grease_nipple_and_ring_references',cq.Compound.makeCompound([small,large,grease,*seals]),(.23,.26,.21))
 # Real threads, taper and lubrication drilling are deliberately absent.
 assert arm.intersect(cm(8,104,c,-52)).Volume()<1e-5
 assert foot.intersect(cm(8,40,c,-20)).Volume()<1e-5
 assert foot.intersect(cotter_hole).Volume()<1e-5
 assert arm.intersect(foot).Volume()<1e-5
 assert pin.intersect(foot).Volume()<1e-5
 nodes[label]=dict(inner_centers_units=centers,inner_eye_outer_radius_mm=15.5,eye_half_length_mm=19,bush_radial_comparison_mm={'small_outside_diameter':23,'large_outside_diameter':25,'inside_diameter':12.5},estimated_bush_body_half_length_mm=19,estimated_flange_thickness_mm=3,estimated_flange_outside_diameter_mm=29,estimated_bush_pair_outer_z_offsets_mm=[-22,22],estimated_pin_shoulder_diameter_mm=12,catalog_pin_terminal_diameter_mm=9.525,estimated_pin_underhead_z_mm=-28,estimated_pin_end_z_mm=42,estimated_eye_to_pan_section_mm=.003*S)
 nodes[label]['front_antiroll_attachment']=dict(plane_origin_mm=list(attach.toTuple()),tangent=list(tangent.toTuple()),outward_normal=list(normal.toTuple()),fixing_centers_mm=holes,fixing_bore_radius_mm=4.5,plate_outline_uv_mm=[[-45,-25],[-45,25],[65,0]],plate_back_normal_offset_mm=.4,plate_thickness_mm=3,upper_stud_hole_uv_mm=[32,0],upper_stud_hole_radius_mm=5.5,upper_ball_normal_offset_mm=20,lower_ball_y_units=.17,bar_eye_normal_offset_from_lower_ball_mm=20,front_wall_depth_mm=40,front_wall_extension_x_units=[.43,.60],basis='IMG_6914 forward-wall plate orientation and factory p144 flat bracket topology; all coordinates, sections and hole centers estimated, not fitted or a drill pattern.')
 nodes[label]['outer_estimated_construction']=dict(center_units=[side*.58,.27,1.20],axis=[0,0,1],fork_gap_mm=40,foot_length_mm=30,foot_outside_diameter_mm=40,foot_bore_diameter_mm=16.5,pin_center_diameter_mm=16,pin_end_diameters_mm=[18,20],pin_end_z_offsets_mm=[-48,48],fork_bore_diameters_mm=[18.5,20.5],fork_end_z_offsets_mm=[-.06*S,.06*S],cotter_center_x_offset_mm=side*8.5,cotter_diameter_mm=5,cotter_hole_diameter_mm=5.4,cotter_axis=[0,1,0],pin_flat_x_offset_mm=side*6,seal_z_offsets_mm=[-20,15],shaft_top_units=[side*.55,.65,1.20],shaft_radius_mm=.022*S,estimated_upper_terminal_radius_mm=8,estimated_upper_terminal_length_mm=14,thread_geometry_applied=False,cotter_taper_applied=False)
 for z in [.98,1.42]:
  c=V([side*.36,.27,z]);assert cq.Compound.makeCompound(rubbers).intersect(cm(6,48,c,-24)).Volume()<1e-5
  assert arm.intersect(cm(11.4,48,c,-24)).Volume()<1e-5
assembly=cq.Assembly(name='estimated_front_fulcrum_architecture');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=shape.tessellate(.6,.15)
 meshes.append(dict(name=name,color=color,vertices=[round(v,4) for p in vs for v in p.toTuple()],triangles=[i for tri in ts for i in tri]))
assembly.export(str(OUT/'front-fulcrum-reference.step'))
limits='No direct owner dimensions are applied. Supplier 8G621/A486 radial listing is a replacement comparison, not this car’s fitted identity or fit. Its two published axial lengths lack resolved section datums and are recorded without application. New axial body/lip, pin shoulder/head/length, washer/nut, clearances, taper/eye seat and pressed-pan lip are estimates. Metal insert construction and rubber compression are not reconstructed. Inner and outer pivot axes, shaft inclination and upper endpoint remain retained drawing stations. The outer solid sleeve becomes two bored fork ends around a separate bored kingpin foot, stepped pin, cotter and end closures. Every new outer-joint dimension, handed orientation, plug direction, pin flat and cotter section is estimated; earlier shared-front factory drawings establish topology only. Threads, oversize wear remedies, cotter taper, lubrication passages, loaded contacts and upper trunnion fit are not reconstructed. Body pickup pockets/ears are separately revised around this declared local construction. No strength, rubber loading, alignment, articulation, safe spring restraint, torque or verified replacement/removal path. Actual outer thread forms and engagement, cotter taper/clocking, fitted upper trunnion and rebound-buffer attachment remain unresolved. The anti-roll attachment is corrected from the old unsupported horizontal pattern to the forward wall, based on IMG_6914 and factory topology. Its declared 40 mm wall depth, extension, bores, plate pose and fixing map remain estimates, not fitted dimensions or a drilling template. An estimated 8 mm-radius, 14 mm-long upper terminal is added above the retained shaft-top datum for the separate kingpin-carrier reference; neither its thread nor fitted length is established. This export does not certify a complete kingpin-bearing stack.'
outer_topology=dict(factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=139',supplier_source='https://www.mgocspares.co.uk/parts/mg/midget/suspension-steering/front-suspension/front-suspension',basis='Earlier shared-front factory drawing separates 2A4020 outer fulcrum, 51K1769 standard cotter, 51K2751 oversize alternative, 51K3424 small-end plug, 8G589 screwed large-end plug, UHN400 nipple and 2A4205/2A4206 rings. MGOC current Midget listing corroborates roles of the pin and different end closures. No item is identified as fitted and no outer dimensions are sourced.',all_new_dimensions='estimated construction',oversize_cotter_selected=False,actual_thread_or_cotter_taper=False,owner_dimensions_applied=False)
meta=dict(outer_topology=outer_topology,units='mm',source_frame='Canonical body +X vehicle-left, +Y up, +Z front, mm.',factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=141',supplier_radial_source='https://basisnz.co.nz/products/sku273-486',supplier_terminal_source='https://www.mgocspares.co.uk/parts/mg/midget/suspension-steering/front-suspension/front-suspension',basis='Factory four 2A4272 pins, eight 8G621 bushes and four special washers/nuts. Supplier stock A486/8G621 lists 23–25 mm tapered OD and 12.5 mm ID; supplier GHF223 nut is 3/8 UNF. These radial/terminal comparisons do not identify fitted components.',supplier_axial_values_recorded_not_applied_mm={'overall_length':28,'length':17,'reason':'Different named lengths without section/datums sufficient for this assembly. Do not infer an axial fit, flange thickness or actual metal-insert section.'},prior_primitive_dimensions_mm={'bush_outside_diameter':.047*2*S,'bush_length':.10*S,'pin_outside_diameter':.018*2*S,'pin_length':.14*S},body_pickup_construction=dict(pocket_radius_mm=16.5,pocket_half_length_units=.027,ear_z_offsets_units=[-.030,.030],ear_outer_width_units=.075,ear_outer_height_units=.080,ear_center_y_units=.29,ear_bore_radius_mm=6.25),nodes=nodes,limits=limits,parts=meshes)
(OUT/'front-fulcrum-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'front-fulcrum-reference.step')).val();assert restored.isValid()
print(json.dumps(dict(groups=len(parts),solids=len(restored.Solids()),step_valid=True,four_eye_and_eight_bush_passages=True,pivot_centers_retained=True,outer_fork_and_foot_bores=True,outer_pin_and_foot_nonintersecting=True,six_front_wall_bores_and_adjacent_material=True)))
