"""Shared-front kingpin topology in estimated body registration, never a fitted stack."""
from pathlib import Path
import json,math
import cadquery as cq
OUT=Path(__file__).parent
BODY=json.loads((OUT.parent/'body-data.json').read_text());FULCRUM=json.loads((OUT/'front-fulcrum-reference.json').read_text())
S=BODY['published_reference_dimensions_mm']['wheelbase']/2.4
W=BODY['published_reference_dimensions_mm']['front_track_steel_wheels']/2
parts=[];nodes={}
def V(p):return cq.Vector(*(x*S for x in p))
def cylinder(r,l,p,a):return cq.Solid.makeCylinder(r,l,p,a)
def tube(ro,ri,l,p,a):return cylinder(ro,l,p,a).cut(cylinder(ri,l+2,p-a,a))
def rod(p,q,r):
 d=q-p;return cylinder(r,d.Length,p,d.normalized())
def box(w,h,d,p):return cq.Workplane('XY').box(w,h,d).translate(p.toTuple()).val()
def compound(shapes):return cq.Compound.makeCompound(shapes)
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')]:
 n=FULCRUM['nodes'][label]['outer_estimated_construction'];base=V(n['center_units']);top=V(n['shaft_top_units']);axis=(top-base).normalized();L=(top-base).Length;R=n['shaft_radius_mm'];zaxis=cq.Vector(0,0,1)
 def at(t):return base+axis*t
 def P(x,y,z):return V([side*x,y,z])
 def upper_cyl(r,l,start):return cylinder(r,l,start,zaxis)
 def upper_ring(ro,ri,l,start):return tube(ro,ri,l,start,zaxis)
 lower=.09*S/axis.y;old_upper=.28*S/axis.y;neck_top=L-29
 bore=cylinder(R+2.5,L+40,at(-10),axis)
 # Keep prior carrier radius/end heights, add an estimated hollow upper neck.
 barrel=rod(at(lower),at(old_upper),.048*S)
 neck=rod(at(old_upper-2),at(neck_top),R+8)
 spindle_base=cylinder(.055*S,.12*S,P(.566-.06,.39,1.2),cq.Vector(side,0,0))
 # Prior stub-axle bearing stations, section radii and caliper-web endpoints.
 pieces=[barrel,neck,spindle_base]
 for x1,x2,r in [(.566*S,W-60,17.5),(W-60,W-25,12.5),(W-25,W+20.5,8.5)]:pieces.append(rod(cq.Vector(side*x1,.39*S,1.2*S),cq.Vector(side*x2,.39*S,1.2*S),r))
 # Port the prior Three.js box orientation (Y maps to delta; X/Z retain twist).
 for y in [-27,27]:
  a=cq.Vector(side*(W-100),.39*S+y,1.20*S);b=cq.Vector(side*(W+BODY['front_hub_reference']['disc_center_mm']-38),.39*S+y,1.20*S-76);d=(b-a).normalized();e=cq.Vector(0,1,0);k=e.cross(d);c=e.dot(d)
  def rotate(v):return v+k.cross(v)+k.cross(k.cross(v))*(1/(1+c))
  plane=cq.Plane(origin=(a+b)*.5,xDir=rotate(cq.Vector(1,0,0)),normal=rotate(cq.Vector(0,0,1)))
  pieces.append(cq.Workplane(plane).box(8,(b-a).Length+18,18).val())
 # Separate steering-arm asset replaces the old integral forward rod.
 arm_nodes=json.loads((OUT/'steering-arm-reference.json').read_text())['nodes'][label]
 for fixing in arm_nodes['fixings']:
  tool=cylinder(4.5,fixing['body_tool_length_mm'],cq.Vector(*fixing['body_tool_start_mm']),cq.Vector(*fixing['axis']))
  pieces=[p.cut(tool) for p in pieces]
 # Caliper mounting axes retain the prior ear/web endpoints; bores are estimates.
 for yy in [-27,27]:
  q=cq.Vector(side*(W+BODY['front_hub_reference']['disc_center_mm']-38),.39*S+yy,1.2*S-76)
  pieces=[p.cut(cylinder(4.5,44,q-cq.Vector(22,0,0),cq.Vector(1,0,0))) for p in pieces]
 # Estimated hub split-pin axis; this is not a fitted drilling template.
 q=cq.Vector(side*(W+17.5),.39*S,1.2*S)
 pieces=[p.cut(cylinder(1.4,28,q-cq.Vector(0,14,0),cq.Vector(0,1,0))) for p in pieces]
 # Bore every intersecting carrier primitive, including spindle base and webs.
 carrier=compound([p.cut(bore) for p in pieces]);add('front_'+label+'_bored_swivel_carrier_spindle_and_web_reference',carrier)
 bushes=compound([tube(R+2.4,R+.3,28,at(t),axis) for t in [lower,neck_top-28]])
 add('front_'+label+'_two_swivel_bush_section_references',bushes,(.56,.42,.24))
 dust=compound([tube(R+2.6,R+.6,34,at(22),axis),tube(R+4.9,R+2.9,lower-52,at(52),axis)])
 add('front_'+label+'_overlapping_lower_dust_tube_references',dust,(.39,.41,.36))
 # Two offset axes, as shown by the factory drawing and supplier product photo.
 joint=top-axis*13-cq.Vector(side*32,0,0)
 vertical=cylinder(R+5.5,26,top-axis*26,axis)
 barrel=upper_cyl(19,40,joint-cq.Vector(0,0,20))
 trunnion=vertical.fuse(barrel).cut(cylinder(R+.3,30,top-axis*28,axis))
 upper_bushes=[]
 for direction in [-1,1]:
  a=cq.Vector(0,0,direction)
  trunnion=trunnion.cut(cq.Solid.makeCone(11.7,14.7,20,joint,a))
  b=cq.Solid.makeCone(11.5,14.5,20,joint,a).fuse(cylinder(16,3,joint+a*20,a))
  b=b.cut(upper_cyl(6.35,48,joint-cq.Vector(0,0,24)));upper_bushes.append(b)
 add('front_'+label+'_offset_bored_upper_trunnion_reference',trunnion)
 add('front_'+label+'_paired_upper_tapered_bush_comparisons',compound(upper_bushes),(.13,.16,.13))
 pin=upper_cyl(6.25,62,joint-cq.Vector(0,0,31)).fuse(upper_cyl(10,5,joint-cq.Vector(0,0,36))).fuse(upper_cyl(9.525/2,14,joint+cq.Vector(0,0,31)))
 split_hole=cylinder(1.25,16,joint+cq.Vector(-8,0,42),cq.Vector(1,0,0));pin=pin.cut(split_hole)
 add('front_'+label+'_upper_fulcrum_pin_comparison',pin,(.48,.49,.44))
 nut=cq.Workplane('XY',origin=(joint+cq.Vector(0,0,33)).toTuple()).polygon(6,16.7).extrude(8).val().cut(upper_cyl(4.9,10,joint+cq.Vector(0,0,32)))
 # Slotted-nut and split-pin envelopes; no locking/torque or release sequence.
 for i in range(3):
  slot=box(20,2,3,joint+cq.Vector(0,0,40));slot=slot.rotate(joint.toTuple(),(joint+cq.Vector(0,0,1)).toTuple(),i*60);nut=nut.cut(slot)
 washer=upper_ring(10,4.9,2,joint+cq.Vector(0,0,31));split=cylinder(1,16,joint+cq.Vector(-8,0,42),cq.Vector(1,0,0))
 # Clamp screw at the inboard side of one arm eye; station is estimated.
 clamp_at=joint+cq.Vector(-side*12.5,0,-27);clamp=cylinder(3.96875,32,clamp_at-cq.Vector(0,16,0),cq.Vector(0,1,0))
 clamp_head=cylinder(6,4,clamp_at+cq.Vector(0,16,0),cq.Vector(0,1,0))
 add('front_'+label+'_upper_pin_retainer_and_arm_clamp_references',compound([nut,washer,split,clamp,clamp_head]),(.48,.49,.44))
 thrust=tube(R+5.5,R+.3,3,at(L-29),axis)
 topwasher=tube(R+5.5,8.25,2,top,axis)
 nutplane=cq.Plane(origin=top+axis*2,xDir=(0,0,1),normal=axis)
 topnut=cq.Workplane(nutplane).polygon(6,26).extrude(10).val().cut(cylinder(8.25,14,top,axis))
 add('front_'+label+'_kingpin_thrust_washer_and_top_retainer_references',compound([thrust,topwasher,topnut]),(.56,.42,.24))
 # Three unchanged damper-body envelopes; revised paired arms end at the joint.
 body=[box(.16*S,.025*S,.24*S,P(.36,.635,1.2)),box(.12*S,.12*S,.16*S,P(.36,.70,1.2)),upper_cyl(.055*S,.20*S,P(.38,.68,1.10))]
 arm_parts=[]
 for dz in [-27,27]:
  tip=joint+cq.Vector(0,0,dz);root=P(.39,.68,1.2+math.copysign(.05,dz))
  pin_bore=upper_cyl(6.5,150,joint-cq.Vector(0,0,75))
  beam=rod(root,tip,12).cut(pin_bore)
  ear=upper_ring(12,6.5,8,tip-cq.Vector(0,0,4))
  local=[beam,ear]
  if dz<0:
   clamp_bore=cylinder(4.1,40,clamp_at-cq.Vector(0,20,0),cq.Vector(0,1,0))
   local=[p.cut(clamp_bore) for p in local]
   local.append(tube(7,4.1,20,clamp_at-cq.Vector(0,10,0),cq.Vector(0,1,0)).cut(pin_bore))
  arm_parts.extend(local)
 damper=compound(body+arm_parts);add('front_'+label+'_lever_damper_and_bored_upper_arms_reference',damper)
 assert carrier.intersect(cylinder(R+.2,L+40,at(-10),axis)).Volume()<1e-5
 assert bushes.intersect(cylinder(R+.1,L+40,at(-10),axis)).Volume()<1e-5
 assert trunnion.intersect(cylinder(R+.1,28,top-axis*27,axis)).Volume()<1e-5
 assert trunnion.intersect(upper_cyl(6.3,48,joint-cq.Vector(0,0,24))).Volume()<1e-5
 assert damper.intersect(upper_cyl(6.3,70,joint-cq.Vector(0,0,35))).Volume()<1e-5
 assert carrier.intersect(bushes).Volume()<1e-5
 assert compound(upper_bushes).intersect(pin).Volume()<1e-5
 assert trunnion.intersect(pin).Volume()<1e-5
 nodes[label]=dict(kingpin_base_units=n['center_units'],kingpin_top_units=n['shaft_top_units'],axis=list(axis.toTuple()),shaft_radius_mm=R,carrier_bore_radius_mm=R+2.5,bush_inside_radius_mm=R+.3,bush_outside_radius_mm=R+2.4,bush_length_mm=28,bush_start_t_mm=[lower,neck_top-28],carrier_neck_top_t_mm=neck_top,upper_joint_mm=list(joint.toTuple()),estimated_axis_offset_mm=32,trunnion_kingpin_bore_radius_mm=R+.3,trunnion_axial_length_mm=26,upper_bush_radial_comparison_mm={'small_od':23,'large_od':29,'bore':12.7},estimated_upper_bush_half_length_mm=20,estimated_upper_bush_lip_length_mm=3,estimated_arm_eye_z_offsets_mm=[-27,27],upper_pin_shoulder_diameter_mm=12.5,upper_pin_terminal_nominal_diameter_mm=9.525,thrust_washer_start_t_mm=L-29,estimated_thrust_washer_thickness_mm=3,dust_tube_t_intervals_mm=[[22,56],[52,lower]])
assembly=cq.Assembly(name='estimated_front_kingpin_carrier_relationships');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 t in ts for i in t]))
assembly.export(str(OUT/'front-kingpin-reference.step'))
limits='No direct owner dimensions, fitted identities, measured alignment or clearances are established. Earlier shared-front factory drawings establish topology; Rimmer lists 2A4005 for Midget 1500 but does not identify the fitted part. Every new carrier/bush/neck/tube/thrust section, trunnion offset and arm span is estimated. Retained 37.25 mm approximate kingpin diameter is an old drawing envelope, not a factory specification. Caterham Y16/88G274 radial figures are supplier comparisons only; conflicting 16/17 mm axial descriptions are retained unapplied, while 20 mm displayed half-length and lip are estimated. Kingpin and upper-pin retaining threads, accurate split-pin shape, spring-loaded dust-excluder 6K653, seals, lubrication passages, shim stack, loaded rubber, reaming and thrust end float are unresolved. Smooth pin terminals do not reproduce thread engagement. No strength, articulation, steering/suspension travel, torque, actual locking state, safe spring release, tool access or verified removal path. Anti-roll forward-wall attachment now replaces the old unsupported horizontal pattern; its metric positions, sections and fits remain estimates. The old integral steering rod is replaced by a separate steering-arm reference; the two blind securing holes are estimated construction geometry, not actual thread seats or a drilling template. Inspection offsets are illustrative.'
limits+=' Two new estimated 4.5 mm-radius caliper mounting bores per carrier follow the retained caliper-ear/web endpoint axes; fitted thread, support strength, contact and alignment remain unverified.'
limits+=' A new estimated 1.4 mm-radius transverse split-pin bore crosses the spindle at +17.5 mm from the wheel-center datum. Its axis/clocking is construction, not a fitted drilling map or verified locking position.'
meta=dict(units='mm',source_frame='Canonical body +X vehicle-left, +Y up, +Z front, mm.',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=139','https://www.spridgets.de/downloads/Parts-1977.pdf#page=141','https://www.spridgets.de/downloads/Parts-1977.pdf#page=140'],supplier_sources=['https://rimmerbros.com/Item--i-2A4005','https://www.caterham.jp/c-item-detail?ic=Y16','https://www.mgocspares.co.uk/parts/mg/midget/suspension-steering/front-suspension/front-suspension','https://www.mgocspares.co.uk/DocumentHandler/136685/mgoc-spares-midget-catalogue-pdf#page=81'],basis='Factory swivel pin/bush kit, two lower dust tubes, thrust washer, upper trunnion and arm-to-trunnion pin/bushes are separated. Supplier 2A4005 photo corroborates offset orthogonal bores and lists Midget 1500 application. Original spindle, bearing stations and caliper-web endpoints are retained as construction datums. The old integral steering rod is removed; two construction blind holes now follow the separate later steering lever reference. The lever eye relocation is declared in that asset, not a fitted coordinate. MGOC lists 3/8 UNF upper-pin nut and 5/16 arm-clamp screw; terminal diameters use nominal designations only.',supplier_axial_readings_not_applied_mm=[16,17],owner_dimensions_applied=False,nodes=nodes,limits=limits,parts=meshes)
(OUT/'front-kingpin-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'front-kingpin-reference.step')).val();assert restored.isValid()
print(json.dumps(dict(groups=len(parts),solids=len(restored.Solids()),step_valid=True,carrier_bush_and_trunnion_passages=True,local_pin_bush_nonintersection=True)))
