"""Separate later steering levers and joint stack. Every fitted dimension unknown."""
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());S=BODY['published_reference_dimensions_mm']['wheelbase']/2.4
X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1);parts=[];nodes={}
def V(p):return cq.Vector(*(v*S for v in p))
def cyl(r,l,p,a):return cq.Solid.makeCylinder(r,l,p,a)
def ring(ro,ri,l,p,a):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 sphere(r,p):return cq.Solid.makeSphere(r,p,angleDegrees1=-90)
def compound(xs):return cq.Compound.makeCompound(xs)
def add(name,shape,color=(.34,.37,.33)):
 # Mirrored generic profiles avoid opposite-orientation loft/boolean artifacts.
 # Actual handed forging differences are not surveyed.
 if name.startswith('steering_right_'):
  left_name=name.replace('steering_right_','steering_left_',1);shape=next(p for n,p,c in parts if n==left_name).mirror('YZ')
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color));return shape
for sign,side in [(1,'left'),(-1,'right')]:
 face=sign*.506*S;root_y=.39*S;root_z=1.20*S;a=X*sign
 ball=V([sign*.57,.40,1.43]);eye=ball-Y*26;lower=eye.y-7;upper=eye.y+7
 centers=[cq.Vector(face,root_y+dy,root_z) for dy in [-24,24]]
 # A two-hole foot on the old spindle-base inboard face, not a surveyed pad.
 ends=[p-a*12 for p in centers];foot=cyl(14,12,ends[0],a).fuse(cyl(14,12,ends[1],a)).fuse(box(12,48,28,cq.Vector(face-sign*6,root_y,root_z)))
 wires=[]
 for x,y,z,w in [(face-sign*14,root_y,root_z,28),(face-sign*14,root_y-4,root_z+55,24),(face+sign*20,eye.y+3,root_z+140,22),(eye.x,eye.y,eye.z-8,25)]:
  wires.append(cq.Workplane('XY',origin=(x,y,z)).ellipse(w/2,7).val())
 beam=cq.Solid.makeLoft(wires,False)
 lever=foot.fuse(beam).fuse(cyl(17,14,cq.Vector(eye.x,lower,eye.z),Y)).clean()
 taper_hole=cq.Solid.makeCone(6.3-1/14,7.3+1/14,16,cq.Vector(eye.x,lower-1,eye.z),Y)
 lever=lever.cut(taper_hole).clean()
 # One common two-hole plate; small lips are unverified bend envelopes.
 tab_start=centers[0]-a*14
 tab=cyl(11,2,tab_start,a).fuse(cyl(11,2,centers[1]-a*14,a)).fuse(box(2,48,22,cq.Vector(face-sign*13,root_y,root_z)))
 for dy in [-1,1]:tab=tab.fuse(box(6,2,12,cq.Vector(face-sign*16,root_y+dy*34,root_z)))
 bolts=[];fix_nodes=[]
 for i,p in enumerate(centers):
  depth=14 if i==0 else 20
  tool=cyl(4.5,12+depth+4,p-a*14,a);lever=lever.cut(tool);tab=tab.cut(tool)
  shaft=cyl(4.25,14+depth,p-a*14,a);head=cyl(7,5,p-a*19,a);bolts.append(shaft.fuse(head))
  fix_nodes.append(dict(center_mm=list(p.toTuple()),axis=list(a.toTuple()),estimated_blind_depth_mm=depth,arm_bore_radius_mm=4.5,body_tool_start_mm=list((p-a).toTuple()),body_tool_length_mm=depth+3))
 lever=add('steering_'+side+'_separate_offset_lever_with_tapered_eye_comparison',lever)
 add('steering_'+side+'_short_long_lever_screw_envelopes',compound(bolts),(.56,.57,.52))
 tab=add('steering_'+side+'_two_hole_lock_tab_and_bend_envelopes',tab,(.55,.53,.43))
 # Ball center and rack/rod axes stay fixed; only the new lever eye moves down.
 stud=sphere(8.8,ball).fuse(cyl(7,ball.y-upper,cq.Vector(ball.x,upper,ball.z),Y))
 stud=stud.fuse(cq.Solid.makeCone(6.2,7.2,14,cq.Vector(ball.x,lower,ball.z),Y)).fuse(cyl(5.5,12,cq.Vector(ball.x,lower-12,ball.z),Y))
 stud=add('steering_'+side+'_outer_ball_tapered_shoulder_and_smooth_terminal',stud,(.55,.57,.51))
 retainers=compound([ring(10.5,5.7,2,cq.Vector(ball.x,lower-2,ball.z),Y),ring(9,5.7,7,cq.Vector(ball.x,lower-9,ball.z),Y)])
 retainers=add('steering_'+side+'_outer_stud_washer_and_smooth_nut_envelopes',retainers,(.55,.57,.51))
 # Revolve an illustrative hollow dust bell about Y; no loaded seal inferred.
 profile=[(9.2,upper+1),(12.2,upper+3),(17.5,upper+6),(17.5,upper+8),(16,upper+8),(16,upper+6),(10.7,upper+3),(7.7,upper+1)]
 gaiter=cq.Workplane('XY').polyline(profile).close().revolve(360,(0,0),(0,1)).val().translate((ball.x,0,ball.z))
 gaiter=add('steering_'+side+'_hollow_outer_joint_gaiter_comparison',gaiter,(.12,.15,.13))
 for p in centers:
  gauge=cyl(4.3,40,p-a*22,a);assert lever.intersect(gauge).Volume()<1e-5 and tab.intersect(gauge).Volume()<1e-5
 assert lever.intersect(cyl(6.1,18,cq.Vector(eye.x,lower-2,eye.z),Y)).Volume()<1e-5
 assert lever.intersect(stud).Volume()<1e-5
 assert retainers.intersect(stud).Volume()<1e-5
 assert gaiter.intersect(stud).Volume()<1e-5
 nodes[side]=dict(retained_ball_center_mm=list(ball.toTuple()),retained_plan_axis_units=[sign*.57,1.43],estimated_arm_eye_center_mm=list(eye.toTuple()),eye_vertical_relocation_mm=-26,eye_y_bounds_mm=[lower,upper],estimated_taper_bore_radii_bottom_top_mm=[6.3,7.3],estimated_stud_taper_radii_bottom_top_mm=[6.2,7.2],estimated_smooth_terminal_radius_mm=5.5,estimated_mount_face_x_mm=face,estimated_mount_foot_thickness_mm=12,fixings=fix_nodes)
limits='No direct owner dimensions or fitted identities are applied. Factory p140 later AHA9957 RH/AHA9958 LH lever, one short/one long securing screw per side and common 2K5377 lock tab are topology comparisons; earlier BTA648/649 levers remain distinct. Factory p133 BHA5141 socket/143992 gaiter/PWZ106 washer/GHF223 nut are reference roles. Supplier GSJ158 photo supports a tapered stud, dust bell and locking nut, but its advertised M10x1 replacement nut is not equated to the factory nut or fitted hardware and is not applied to these smooth surfaces. The generic right comparison is mirrored from the left; actual handed forging differences remain unmeasured. All arm contours, sweep, mounting plane, two-hole map, blind depths, lengths, taper/eye diameters, clearances, stud/washer/nut stack, gaiter and folds are construction estimates. Existing spindle/carrier datums and outer ball center/rack/tie-rod axes remain fixed. New arm eye moves 26 mm below the former solid-rod endpoint to expose a joint stack beneath the retained ball center; this is a declared construction relocation, not a fitted height, steering arm identity, ride geometry or taper specification. Two estimated blind bores in the old spindle-base references follow these same construction centers. Threads, shank fits, actual captive retention, lock-tab bend/engagement, gaiter seating, sealing, loaded compression and torque are unmodeled. Estimated radial gaps describe source construction only, not fitted running/sealing clearance. No working articulation, steering/suspension travel, toe/caster/Ackermann result, strength, safe jacking/support/release order, puller access or verified replacement/withdrawal path. Display translations separate ownership only.'
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=140','https://www.spridgets.de/downloads/Parts-1977.pdf#page=133'],supplier_photo_source='https://rimmerbros.com/Item--i-GSJ158',supplier_metric_nut_comparison_not_applied='GSJ158 supplied replacement lists M10 x 1.0 nut; not imposed on factory GHF223 or this car.',owner_photos=['IMG_6914','IMG_6919','IMG_6341'],owner_dimensions_applied=False,basis='Factory later steering lever is a separate handed component with two securing screws and a common lock tab, not a rod cast into the carrier. Later rack drawing and supplier own GSJ158 photo separate socket, tapered stud, gaiter and nut roles. Owner IMG_6914 supplies front-corner context, without a calibrated hidden mounting map or taper measurement.',nodes=nodes,limits=limits)
assembly=cq.Assembly(name='estimated_separate_steering_lever_and_outer_joint_interfaces');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=shape.tessellate(.5,.16);meshes.append(dict(name=name,color=color,vertices=[round(x,4) for p in vs for x in p.toTuple()],triangles=[i for t in ts for i in t]))
assembly.export(str(OUT/'steering-arm-reference.step'));meta['parts']=meshes;(OUT/'steering-arm-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
r=cq.importers.importStep(str(OUT/'steering-arm-reference.step')).val();assert r.isValid();print(json.dumps(dict(source_parts=len(parts),solids=len(r.Solids()),step_valid=True,four_open_lever_tab_bores=True,lever_stud_no_overlap=True,retainer_stud_no_overlap=True,gaiter_stud_no_overlap=True)))
