"""Later LHD steering exterior and mounting comparisons. No fitted dimensions."""
from pathlib import Path
import json,math
import cadquery as cq
from steering_column_layout import TOP_UNITS, LOWER_UNITS, BODY_PLANE_Z_UNITS, column_at_units, layout_metadata
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 box(w,h,d,c):return cq.Workplane('XY').box(w,h,d).translate(c.toTuple()).val()
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 sphere(r,p):return cq.Solid.makeSphere(r,p,angleDegrees1=-90)
def compound(xs):return cq.Compound.makeCompound(xs)
def add(name,shape,color=(.36,.39,.36)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color));return shape
y=.40*S;z=1.43*S;top=V(TOP_UNITS);bottom=V(LOWER_UNITS);out=(top-bottom).normalized()
# Keep published frame and former drawing stations. The old 71.12 mm case
# cylinder becomes a declared 28 mm OD estimate from unscaled drawing proportions.
axis=cq.Vector(0,y,z);tube_outer=cyl(14,.68*S,cq.Vector(-.34*S,y,z),X)
mid=V([.27,.435,1.43]);neck_start=bottom-out*20
# Replace the generic sphere with a cast-junction envelope and raised plunger
# tower seen in IMG_7041 / factory p133. All sections and stations are estimates.
def junction(sections):
 wires=[cq.Workplane('YZ',origin=(mid.x+dx,mid.y,mid.z)).ellipse(h,d).val() for dx,h,d in sections]
 return cq.Solid.makeLoft(wires,ruled=True)
shell=junction([(-52,15,16),(-32,18,18),(-18,27,25),(12,30,25),(27,18,18)])
cavity=junction([(-54,11,12),(-34,14,14),(-18,22,20),(12,25,20),(29,14,14)])
plunger=cq.Vector(mid.x-47,y,z);tower_top=y+52
outer=shell.fuse(cyl(17,35,neck_start,out)).fuse(cyl(18,52,plunger,Y))
inside=cavity.fuse(cyl(8.8,39,neck_start-out*2,out)).fuse(cyl(12.4,56,plunger-Y*2,Y))
case=tube_outer.cut(cyl(11,.68*S+2,cq.Vector(-.34*S-1,y,z),X)).cut(inside)
add('steering_rack_hollow_case_comparison',case)
housing=outer.cut(inside).cut(tube_outer)
add('steering_lhd_pinion_housing_and_input_shaft_comparison',compound([housing,cyl(8,24,bottom,out)]))
# Smooth plug and hex head identify the top boundary only. No working adjuster,
# thread, spring, plunger, shim selection or preload procedure is reconstructed.
cap=cyl(12,10,cq.Vector(plunger.x,tower_top-10,z),Y)
head=cq.Workplane(cq.Plane(origin=(plunger.x,tower_top,z),xDir=(1,0,0),normal=(0,1,0))).polygon(6,32/math.cos(math.pi/6)).extrude(7).val()
cap=cap.fuse(head).clean()
add('steering_plunger_top_cap_smooth_plug_and_hex_envelope',cap,(.48,.49,.43))
assert housing.intersect(cyl(12,12,cq.Vector(plunger.x,tower_top-10,z),Y)).Volume()<1e-6
assert housing.intersect(cyl(1,10,cq.Vector(plunger.x+15,tower_top-8,z),Y)).Volume()>1
assert housing.intersect(cap).Volume()<1e-6
add('steering_smooth_rack_bar_without_teeth',cyl(9,2*(.37*S-9),cq.Vector(-.37*S+9,y,z),X),(.58,.59,.54))
# Shared estimated axis now follows the revised wheel setback and body seal.
shaft_start=bottom+out*24
body_crossing=V(column_at_units(BODY_PLANE_Z_UNITS))
# Visible lower section follows owner-photo proportions; hidden upper source
# radius stays. The body-plane step is a construction boundary, not fitted detail.
lower_shaft=cyl(8,(body_crossing-shaft_start).Length,shaft_start,out)
upper_shaft=cyl(.016*S,(top-body_crossing).Length,body_crossing,out)
shaft=lower_shaft.fuse(upper_shaft).clean();assert len(shaft.Solids())==1
add('steering_column_retained_axis_smooth_shaft',shaft,(.58,.59,.54))
sleeve_end=V(column_at_units(BODY_PLANE_Z_UNITS));length=(top-sleeve_end).Length
add('steering_column_hollow_outer_sleeve_comparison',ring(.037*S,.037*S-3,length,sleeve_end,out))
# Split coupling exterior, with an external transverse pinch-bolt passage.
u=(X-out*out.dot(X)).normalized();v=out.cross(u).normalized();cp=bottom+out*12;plane=cq.Plane(origin=cp,xDir=u,normal=out)
clamp=ring(14,8.4,28,cp,out)
slit=cq.Workplane(plane).box(28,1.4,32).translate((u*14+out*14).toTuple()).val();clamp=clamp.cut(slit)
for sign in [-1,1]:
 ear=cq.Workplane(plane).box(12,5,20).translate((u*14+v*sign*4.2+out*14).toTuple()).val();clamp=clamp.fuse(ear)
bolt_center=cp+u*14+out*14;clamp=clamp.cut(cyl(3.5,24,bolt_center-v*12,v))
add('steering_split_pinion_coupling_envelope',clamp)
fix=[cyl(3,19,bolt_center-v*9.5,v),cyl(5.5,3,bolt_center-v*12.5,v),ring(5,3.2,4,bolt_center+v*7,v)]
add('steering_pinion_pinch_fixing_envelopes',compound(fix),(.55,.57,.51))
for sign,side in [(1,'left'),(-1,'right')]:
 c=V([sign*.37,.40,1.43]);end=V([sign*.57,.40,1.43]);a=X*sign
 # Inner cup, smooth ball/rod and outer socket are separate. No preload or
 # threaded adjustment, spherical running fit or tapered arm bore is implied.
 inner=sphere(15,c).cut(sphere(9.3,c)).cut(cyl(6.5,40,c-a*1,a)).cut(cyl(9.5,20,c-a*20,a))
 if sign==1:left_inner=inner
 else:inner=left_inner.mirror('YZ')
 add('steering_'+side+'_inner_joint_cup_comparison',inner)
 rod_end=end-a*20
 # Build the negative-X ball/rod union once. Rigid rotation preserves its
 # spherical seam orientation across STEP; the old positive-X union could
 # re-import with negative volume despite passing the native validity check.
 if sign==1:
  rc=V([-.37,.40,1.43]);right_rod=sphere(9,rc).fuse(cyl(6,(rod_end-c).Length,rc,-X))
  rod=right_rod.rotate((0,y,z),(0,y+1,z),180)
 else:rod=right_rod
 add('steering_'+side+'_smooth_ball_and_tie_rod_comparison',rod,(.55,.57,.51))
 socket=sphere(19,end).fuse(cyl(10,35,end-a*40,a)).cut(sphere(9.2,end)).cut(cyl(6.6,33,end-a*41,a)).cut(cyl(7.5,25,end-Y*25,Y))
 add('steering_'+side+'_outer_socket_comparison',socket)
 # Outer stud/retainer now belongs to the separate steering-arm asset.
 # Hollow revolved corrugations; count, diameters and end stations estimated.
 lo=.335*S;hi=.48*S;outer_profile=[(lo,16),(lo+5,16),(lo+12,26.5)]
 for i in range(7):
  start=lo+12+i*(hi-lo-24)/7;outer_profile += [(start+2,32),(start+5,32),(start+(hi-lo-24)/7,26.5)]
 outer_profile += [(hi-7,9),(hi,9)]
 profile=[(sign*x,r) for x,r in outer_profile]+[(sign*x,r-1.5) for x,r in reversed(outer_profile)]
 boot=cq.Workplane('XY').polyline(profile).close().revolve(360,(0,0),(1,0)).val().translate((0,y,z))
 add('steering_'+side+'_hollow_corrugated_bellows_comparison',boot,(.12,.15,.13))
 clips=[ring(17,16.1,3,cq.Vector(sign*(lo+1.5),y,z)-a*1.5,a),ring(10,9.1,3,cq.Vector(sign*(hi-2),y,z)-a*1.5,a)]
 add('steering_'+side+'_bellows_clip_envelopes',compound(clips),(.52,.54,.49))
 # Generic rear cradle and front cap, plus one top/body fixing per side.
 # Six screw envelopes follow count/roles only, not a factory hole map.
 mount_x=.16 if sign>0 else -.27;mx=mount_x*S;mc=cq.Vector(mx,y,z);width=.07*S
 foot=box(width,3,.14*S,cq.Vector(mx,.35*S+1.5,z))
 back=box(width,72,3,cq.Vector(mx,y-6,z-18.5))
 rear=ring(18.2,14.2,width,mc-X*width/2,X).intersect(box(width+2,100,50,mc-Z*25))
 bracket=foot.fuse(back).fuse(rear)
 cap=ring(18.2,14.2,width,mc-X*width/2,X).intersect(box(width+2,100,50,mc+Z*25))
 fixing=[];cap_centers=[]
 for dy in [-22,22]:
  q=mc+Y*dy;cap_centers.append(list(q.toTuple()))
  bracket=bracket.fuse(box(width,14.8,20,cq.Vector(mx,q.y,z-10)))
  cap=cap.fuse(box(width,14.8,3,cq.Vector(mx,q.y,z+1.5)))
  bracket=bracket.cut(cyl(4.5,28,q-Z*22,Z));cap=cap.cut(cyl(4.5,28,q-Z*22,Z))
  fixing.extend([cyl(4,27,q-Z*21,Z),cyl(7,4,q+Z*4,Z),ring(7,4.2,1,q+Z*3,Z)])
 body_hole=V([mount_x,.35,1.385]);bracket=bracket.cut(cyl(4.5,25,body_hole-Y*10,Y))
 fixing.extend([cyl(4,10,body_hole-Y*6,Y),cyl(7,4,body_hole+Y*4,Y),ring(7,4.2,1,body_hole+Y*3,Y)])
 add('steering_'+side+'_formed_body_bracket_comparison',bracket)
 add('steering_'+side+'_front_clamp_cap_comparison',cap)
 add('steering_'+side+'_mounting_screw_washer_envelopes',compound(fixing),(.55,.57,.51))
 for q in [cq.Vector(*p) for p in cap_centers]:
  gauge=cyl(4.3,28,q-Z*22,Z);assert bracket.intersect(gauge).Volume()<1e-5 and cap.intersect(gauge).Volume()<1e-5
 assert bracket.intersect(cyl(4.3,25,body_hole-Y*10,Y)).Volume()<1e-5
 assert boot.intersect(cyl(5,hi-lo+2,cq.Vector(sign*lo,y,z)-a,a)).Volume()<1e-5
 nodes[side]=dict(inner_joint_center_mm=list(c.toTuple()),retained_outer_ball_center_units=[sign*.57,.40,1.43],former_arm_endpoint_units=[sign*.57,.40,1.43],mount_center_units=[mount_x,.40,1.43],cap_fixing_centers_mm=cap_centers,body_fixing_center_mm=list(body_hole.toTuple()),body_bore_radius_mm=4.5,bellows_end_x_magnitudes_mm=[lo,hi],bellows_end_outer_radii_mm=[16,9],body_pad_top_y_units=.35)
assert clamp.intersect(cyl(3.3,24,bolt_center-v*12,v)).Volume()<1e-5
limits='No direct owner dimensions or fitted rack/column identities are applied. Factory p133 later AHA9955 LHD rack, AHA9959/60 brackets and two AHA9961 caps are configuration/topology comparisons, distinct from p131-132 earlier racks. p137 GAN6 North American CHA341 column is a separate configuration comparison. Rack centerline, right mount X station and outer steering-arm axes remain estimated drawing datums. Wheel center Z changes from -.16 to -.45 body units using uncalibrated cabin-photo context; the shared column axis, pinion entry direction, clamp and fascia/body aperture follow that construction change. The sleeve ends at the existing body-plane Z=.13 rather than extending forward by a fraction of shaft length. All rack casting centers, plunger, mount and tie-rod positions stay. Left mount moves from X=.27 to .16 body units (93.1333 mm inward) to separate the new generic support from the estimated pinion housing. This is a declared construction relocation, not a fitted mounting coordinate or verified clearance. The former 71.12 mm case OD becomes an estimated 28 mm reference OD from unscaled drawing proportions; bellows/joint diameters also change from coarse envelopes to explicit construction estimates. No scaled factory dimension or fitted section is asserted. Every section, bore, spline substitute, socket/ball fit, corrugation, bracket fold, screw map and installed pose remains estimated. Case/pinion housing separation is a visualization boundary, not a detachable casting joint. Smooth rack bar has no teeth or working gear train. Hollow outer column does not reconstruct collapse or bearing/lock functions. Split coupling and external pinch bolt omit splines, shaft groove, actual thread/locking or engagement. Outer ball centers retain the old plan/height drawing datum; separate steering-arm geometry now supplies a lower estimated tapered eye, replacement stud, retainers and gaiter. These shared construction interfaces do not verify actual arm identity, mounting height or taper fit. Two matching body-pad bores are construction corrections, not measured factory holes or a drilling/repair template. Six mounting screw envelopes follow documented count only, without claimed factory axis/length assignment or captive retention. No actual pinion mesh, loaded clamp contact/preload, toe/centering adjustment, full steering/suspension travel, engine/body clearance, strength, torque, safe support/release order or verified removal path. Brackets remain fixed for inspection; all separation is illustrative.'
limits+=' Raised plunger tower and top-cap boundary now replace the spherical housing cue, from IMG_7041 and factory p133 architecture. Junction loft sections, tower position/diameters, 32 mm cap across-flats estimate, plug gap and wall are construction choices, not owner dimensions or fitted 37H7336 identification. Hidden spring/plunger/shims, thread, preload, adjustment, oil/grease role and sealing remain unresolved; cap separation is illustrative, not an adjustment or release procedure.'
meta=dict(cabin_height_reconstruction=layout_metadata(S)['cabin_height_reconstruction'],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=133','https://www.spridgets.de/downloads/Parts-1977.pdf#page=137'],owner_photos=['IMG_6341','IMG_6855','IMG_6919','IMG_7041'],owner_dimensions_applied=False,basis='Later factory LHD rack separates pinion housing, asymmetric bellows roles, inner ball joints, outer sockets, handed body brackets and two clamp caps. IMG_7041 corroborates the raised top-cap/cast-junction surroundings. Factory p133 cap 37H7336, plunger 120946, spring 126765 and shim roles are comparisons, not fitted identities, measured dimensions or an adjustment specification. Six mounting screws and North American GAN6 collapsible-column roles remain references. Rack stations stay; the revised wheel setback, common column axis and body-seal sleeve boundary are explicitly uncalibrated estimates.',plunger_tower_reference_mm=dict(center_x=plunger.x,center_z=z,base_y=y,top_y=tower_top,outer_radius=18,bore_radius=12.4,cap_plug_radius=12,cap_head_across_flats=32),nodes=nodes,column_layout=layout_metadata(S),column_axis_mm=[list(top.toTuple()),list(bottom.toTuple())],pinch_bolt_center_mm=list(bolt_center.toTuple()),pinch_bolt_axis=list(v.toTuple()),estimated_case_outer_radius_mm=14,estimated_case_inner_radius_mm=11,limits=limits)
meta['lower_shaft_section_reference']=dict(owner_photo='IMG_7041',owner_dimensions_applied=False,evidence='/cars/1975-mg-midget/cad/lower-steering-section-validation.json',former_uniform_shaft_diameter_mm=.032*S,estimated_exposed_shaft_diameter_mm=16,retained_hidden_upper_shaft_diameter_mm=.032*S,estimated_input_stub_diameter_mm=16,estimated_connection_bore_mm=16.8,estimated_coupling_outer_diameter_mm=28,estimated_pinion_neck_outer_bore_mm=[34,17.6],axis_and_endpoints_held=True,body_plane_step_is_construction=True,basis='IMG7041 shows a slender exposed shaft beside the PF53 canister; the existing77mm filter catalog comparison supplies only a rough image-proportion check, not a calibrated scale. Factory p137 separates the exposed lower shaft from the outer jacket and supports a compact clamped entry.16mm is a selected photo-layout estimate, not a factory specification, owner measurement, spline size or machining diameter.',limits='Projection depth, blur, occluded edges and actual diameter remain uncalibrated. Matching smooth input stub and coupling/neck envelopes are construction choices;0.4/0.8mm radial gaps are display allowances, not fitted clearances. Upper step location/diameter are unverified hidden geometry. No splines, telescoping/collapse mechanism, fitted clamp/shaft identity, thread engagement, strength, preload, alignment, safe support or removal path is established.')
meta['basis']+=' The former uniform thick shaft now has a photo-led lower section and compact rack-entry comparison. Hidden upper shaft/jacket, straight axis, endpoints, rack/column/body stations and all mount frames stay.'
meta['limits']+=' '+meta['lower_shaft_section_reference']['limits']
shapes={name:shape for name,shape,color in parts};contacts={};housing=shapes['steering_lhd_pinion_housing_and_input_shaft_comparison']
for side in ['left','right']:
 for slug in ['formed_body_bracket','front_clamp_cap','mounting_screw_washer']:
  name='steering_'+side+'_'+slug+('_envelopes' if slug=='mounting_screw_washer' else '_comparison');contacts[side+'_'+slug+'_vs_pinion_housing']=round(shapes[name].intersect(housing).Volume(),4)
 for a,b in [('inner_joint_cup_comparison','smooth_ball_and_tie_rod_comparison'),('hollow_corrugated_bellows_comparison','smooth_ball_and_tie_rod_comparison')]:contacts[side+'_'+a+'_vs_'+b]=round(shapes['steering_'+side+'_'+a].intersect(shapes['steering_'+side+'_'+b]).Volume(),4)
meta['source_construction_overlaps_mm3']=contacts
assembly=cq.Assembly(name='estimated_later_lhd_steering_exterior');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-rack-reference.step'));meta['parts']=meshes;(OUT/'steering-rack-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
r=cq.importers.importStep(str(OUT/'steering-rack-reference.step')).val();assert r.isValid() and all(s.isValid() for s in r.Solids())
assert len(r.Solids())==sum(len(s.Solids()) for _,s,_ in parts)
assert all(s.Volume(1e-9)>0 for s in r.Solids())
# Adaptive integration avoids the default quadrature disagreement on the new
# loft surfaces; retain the strict round-trip comparison and positive solids.
source_volume=sum(s.Volume(1e-9) for _,s,_ in parts)
restored_volume=r.Volume(1e-9)
assert math.isclose(restored_volume,source_volume,rel_tol=1e-7),(restored_volume,source_volume)
print(json.dumps(dict(parts=len(parts),solids=len(r.Solids()),valid_step=True,step_volume_roundtrip=True,plunger_bore_and_wall=True,separate_cap=True,four_open_cap_bores=True,two_body_bores=True,pinch_bore=True,hollow_bellows=True)))
