"""Front steel-wheel hub/disc/bearing-retention comparison. Fitted identities unknown."""
from pathlib import Path
import json,math
import cadquery as cq
OUT=Path(__file__).parent;B=json.loads((OUT.parent/'body-data.json').read_text());F=B['front_hub_reference'];S=2032/2.4;W=B['published_reference_dimensions_mm']['front_track_steel_wheels']/2
X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1);local=[];parts=[];nodes={};contacts={}
DISC_BOLT_RADIUS=9.525/2
DISC_BOLT_LENGTH=17.4625
DISC_BORE_RADIUS=4.95  # unthreaded comparison clearance, not a tapping size
def cyl(r,l,p,a=X):return cq.Solid.makeCylinder(r,l,p,a)
def ring(ro,ri,l,p,a=X):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 put(n,s,c=(.44,.47,.43)):
 assert s.isValid() and s.Volume()>0,n
 local.append((n,s,c));return s
wheel_axes=[cq.Vector(16,50.8*math.sin(math.pi/4+i*math.pi/2),50.8*math.cos(math.pi/4+i*math.pi/2)) for i in range(4)]
rotor_axes=[cq.Vector(12,40*math.sin(i*math.pi/2),40*math.cos(i*math.pi/2)) for i in range(4)]
# Keep outside plan/stations; shoulder positions derive from declared bearing envelopes.
hub=cyl(31,54,cq.Vector(-60,0,0)).fuse(cyl(27,18,cq.Vector(-6,0,0))).fuse(cyl(65,8,cq.Vector(12,0,0)))
hub=hub.cut(cyl(19,82,cq.Vector(-61,0,0))).cut(cyl(26,23.5,cq.Vector(-61,0,0))).cut(cyl(20,15,cq.Vector(-3,0,0))).cut(cyl(23,10,cq.Vector(12,0,0)))
for q in wheel_axes:hub=hub.cut(cyl(4.8,16,q-X*8))
for q in rotor_axes:hub=hub.cut(cyl(DISC_BORE_RADIUS,16,q-X*4))
hub=put('connected_stepped_hub_and_two_pattern_wheel_flange',hub.clean())
inner=F['bearing_comparison_mm']['inner'];outer=F['bearing_comparison_mm']['outer']
ib=put('inner_bearing_boundary_comparison',ring(inner['outside']/2,inner['bore']/2,inner['width'],cq.Vector(-45-inner['width']/2,0,0)),(.58,.59,.51))
ob=put('outer_bearing_boundary_comparison',ring(outer['outside']/2,outer['bore']/2,outer['width'],cq.Vector(3-outer['width']/2,0,0)),(.58,.59,.51))
spacer=put('hollow_bearing_distance_piece_comparison',ring(16,13.5,34.5,cq.Vector(-37.5,0,0)))
seal=put('inner_hub_seal_unloaded_boundary_envelope',ring(26,12.5,6,cq.Vector(-58.5,0,0)),(.14,.17,.14))
washer=put('separate_spindle_washer_comparison',ring(15,8.5,1.5,cq.Vector(9,0,0)),(.56,.57,.49))
nut=cq.Workplane(cq.Plane(origin=(10.5,0,0),normal=X)).polygon(6,28).extrude(10).val().cut(cyl(8.5,12,cq.Vector(9.5,0,0)))
for angle in [0,60,120]:
 tool=box(5,34,2.4,cq.Vector(18.5,0,0)).rotate((0,0,0),(1,0,0),angle);nut=nut.cut(tool)
nut=put('unthreaded_castellated_retaining_nut_envelope',nut.clean(),(.56,.57,.49))
cap=ring(20,18,18,cq.Vector(10,0,0)).fuse(cyl(20,2,cq.Vector(28,0,0)))
cap=put('separate_hollow_grease_cap_envelope',cap,(.36,.39,.35))
q=cq.Vector(17.5,0,0);split=comp([cyl(1,28,q-Y*14,Y),ring(2,1,1.5,q-Y*15-Z*.75,Z)])
split=put('unbent_split_pin_stem_and_loop_comparison',split,(.56,.57,.49))
# The manufacturer's total height/centering and prior source stations remain comparisons.
C=F['disc_center_mm'];R=F['rotor_comparison_mm'];rotor=ring(R['diameter']/2,45,R['thickness'],cq.Vector(C-R['thickness']/2,0,0))
rotor=rotor.fuse(ring(46,43,48.3,cq.Vector(-40.3,0,0))).fuse(ring(46,R['centering']/2,4,cq.Vector(8,0,0)))
# Four estimated outer lugs express the factory flange architecture while
# retaining the former neck, disc planes and independently sourced boundary size.
for q in rotor_axes:rotor=rotor.fuse(cyl(12,4,cq.Vector(8,q.y,q.z)))
for q in rotor_axes:rotor=rotor.cut(cyl(DISC_BORE_RADIUS,12,q-X*8))
rotor=put('connected_solid_rotor_hat_and_four_bored_mounts',rotor.clean(),(.47,.49,.43))
studs=[];nuts=[];discfix=[]
for q in wheel_axes:
 studs.append(cyl(4.5,23,cq.Vector(12,q.y,q.z)).fuse(cyl(7,3,cq.Vector(9,q.y,q.z))))
 n=cq.Workplane(cq.Plane(origin=(25,q.y,q.z),normal=X)).polygon(6,16).extrude(10).val().cut(cyl(4.7,12,cq.Vector(24,q.y,q.z)));nuts.append(n)
put('four_wheel_stud_shank_and_head_envelopes',comp(studs),(.56,.57,.49));put('four_separate_unthreaded_wheel_nut_envelopes',comp(nuts),(.56,.57,.49))
for q in rotor_axes:
 # Keep the inboard head-face construction atX8. Nominal BTA370 dimensions
 # replace the old6x16 envelope; fitted direction/thread receiver remain open.
 head=cq.Workplane(cq.Plane(origin=(3.5,q.y,q.z),normal=X)).polygon(6,16.5).extrude(4.5).val()
 discfix.append(head.fuse(cyl(DISC_BOLT_RADIUS,DISC_BOLT_LENGTH,cq.Vector(8,q.y,q.z))))
put('four_separate_disc_to_hub_screw_envelopes',comp(discfix),(.56,.57,.49))

# Formed handed dust-cover comparison replaces the browser's flat sector.
# Outer/inner radii and old panel station stay drawing datums, not owner sizes.
def sector(ri,ro,start,end,x,depth):
 def yz(r,a):return (r*math.sin(math.radians(a)),r*math.cos(math.radians(a)))
 return (cq.Workplane('YZ',origin=(x,0,0)).moveTo(*yz(ro,start)).threePointArc(yz(ro,(start+end)/2),yz(ro,end)).lineTo(*yz(ri,end)).threePointArc(yz(ri,(start+end)/2),yz(ri,start)).close().extrude(depth).val())
gauge=1.5;shield_front=-51.25;mount_face=C-44
shield=sector(33,112,-140,140,shield_front-gauge,gauge)
shield=shield.fuse(sector(110.5,112,-140,140,shield_front-gauge,-6))
def sheet(points,delta):
 ps=[cq.Vector(*p)for p in points];return cq.Solid.extrudeLinear(cq.Wire.makePolygon(ps+[ps[0]]),[],cq.Vector(*delta))
for yy in [-27,27]:
 sign=1 if yy>0 else -1;start_y=sign*60
 tongue=sheet([(shield_front,start_y-6,-20),(shield_front,start_y+6,-20),(mount_face,start_y+6,-70),(mount_face,start_y-6,-70)],(-gauge,0,0))
 ear=box(gauge,18,24,cq.Vector(mount_face-gauge/2,yy,-76))
 bridge=box(gauge,abs(start_y-yy)+18,12,cq.Vector(mount_face-gauge/2,(start_y+yy)/2,-70))
 shield=shield.fuse(tongue,bridge,ear).cut(cyl(4.5,60,cq.Vector(mount_face-10,yy,-76)))
# One independent small fixing role per cover. The body receiver stays
# unassigned: do not cut a guessed threaded hole in the retained carrier.
carrier_source=cq.Assembly.importStep(str(OUT/'front-kingpin-reference.step'))
carrier_obj=carrier_source.objects['front_left_bored_swivel_carrier_spindle_and_web_reference'].obj
carrier=(carrier_obj.val()if isinstance(carrier_obj,cq.Workplane)else carrier_obj).translate((-W,-.39*S,-1.20*S))
footprint=carrier.intersect(box(300,20,28,cq.Vector(-75,55,0)))
assert footprint.Volume()>0
small_mount_back=footprint.BoundingBox().xmax
small_mount_front=small_mount_back+gauge
# Source-only tangency to the retained casting footprint. No receiver hole,
# machined land, spacer or actual clamping layer is inferred.
tab=box(gauge,20,28,cq.Vector(small_mount_back+gauge/2,55,0))
web=box(shield_front-small_mount_back,16,gauge,cq.Vector((shield_front+small_mount_back)/2,55,11.25))
shield=shield.fuse(tab,web).cut(cyl(4.3,60,cq.Vector(small_mount_back-10,55,0))).clean()
assert len(shield.Solids())==1
put('formed_disc_dust_cover_with_three_mounting_roles_comparison',shield,(.25,.29,.26))
small_underhead=small_mount_front+1
small_head=cq.Workplane(cq.Plane(origin=(small_underhead,55,0),normal=X)).polygon(6,14.66).extrude(4.5).val()
small=small_head.fuse(cyl(7.9375/2,9.525,cq.Vector(small_underhead,55,0),-X))
small_washer=ring(8,4.25,1,cq.Vector(small_mount_front,55,0))
put('single_dust_cover_screw_and_unloaded_washer_comparison',comp([small,small_washer]),(.56,.57,.49))
# Construction consistency. Equality here is not fitted bearing seating/preload.
for q in wheel_axes:assert hub.intersect(cyl(4.6,16,q-X*8)).Volume()<1e-5
for q in rotor_axes:
 for s in [hub,rotor]:assert s.intersect(cyl(3.3,16,q-X*8)).Volume()<1e-5
for s in [ib,ob,spacer,seal,nut,washer]:assert hub.intersect(s).Volume()<1e-4
assert cap.intersect(nut).Volume()<1e-4 and cap.intersect(split).Volume()<1e-4
assert nut.intersect(cyl(1.2,30,cq.Vector(17.5,-15,0),Y)).Volume()<1e-4
king=cq.Assembly.importStep(str(OUT/'front-kingpin-reference.step'));cal=cq.Assembly.importStep(str(OUT/'front-caliper-reference.step'))
for sign,label in [(1,'left'),(-1,'right')]:
 def transformed(s):return (s if sign==1 else s.mirror('YZ')).translate((sign*W,.39*S,1.2*S))
 def point(q):return [sign*(W+q.x),.39*S+q.y,1.2*S+q.z]
 for name,s,c in local:parts.append(('front_'+label+'_'+name,transformed(s),c))
 neighbors={}
 for ass,name in [(king,'front_'+label+'_bored_swivel_carrier_spindle_and_web_reference'),(cal,'front_'+label+'_opposed_caliper_exterior_blind_pockets_and_mount_bores')]:
  o=ass.objects[name].obj;neighbors[name]=o.val() if isinstance(o,cq.Workplane) else o
 for n,s,c in local:
  for name,o in neighbors.items():
   a,b=transformed(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']):
    v=round(transformed(s).intersect(o).Volume(),4)
    if v>1e-4:contacts[label+'_'+n+'__'+name]=v
 nodes[label]=dict(wheel_fixing_axes_body_mm=[point(q) for q in wheel_axes],rotor_fixing_axes_body_mm=[point(q) for q in rotor_axes],spindle_split_pin_center_body_mm=point(cq.Vector(17.5,0,0)),hub_axis_center_body_mm=point(cq.Vector(0,0,0)),axis_outward=[sign,0,0],inner_seat_bounds_local_mm=[-60,-37.5],outer_seat_bounds_local_mm=[-3,12],inner_seat_construction_change_mm=-2.5,outer_seat_construction_change_mm=3)
limits='No new direct owner dimensions, fitted disc-wheel hub/bearing/seal/nut/disc/wheel identity or loaded fit is applied. Factory p140 distinguishes disc-wheel hub from handed wire-wheel hubs, bearing/distance-piece/seal/washer/nut/cap roles; p142 distinguishes rotor screws from wheel studs. NSK 7205/7203 sizes are independent boundary comparisons, not approved GHB129/GHB128 or fitted replacements, race-face offsets, orientation/contact angle or preload data. Brembo 08.1509.10 supplies 209 x 7.7 mm, 60 mm total height, 64 mm centering and four-hole comparison only; older steel-wheel application is not proof of this fitted 1500 disc. Source wheel/rotor/spindle/bearing centers, outside envelopes and nominal wheel plane remain construction datums. New hub seat shoulders move from -35 to -37.5 mm and -6 to -3 mm to meet declared bearing boundary faces; these -2.5/+3 mm changes are construction, not measured fitted shoulders or a clamped bearing stack. Bearing rings omit race/ball/cage details. Seal and spacer profiles, 34.5 mm length, all hub/hat wall sections, 80 mm rotor PCD, clocking, bores, fastener sizes/knurls/threads/seats and castle slots/cross-bore/split-pin geometry remain estimates. 101.6 mm wheel PCD is the existing supplier comparison, not an owner measurement. The new 1.4 mm-radius spindle cross-bore at +17.5 mm and castle clocking are a construction relationship, not a fitted drilling/locking map. Cap socket engagement and seal/cap interference/compression, lubrication, loaded faces and locking/bending are unverified. No actual wheel offset, bearing orientation/fit/preload/end float, nut torque/adjustment, wheel/rotor clearance, full suspension/steering motion, verified tool access or replacement/removal path. Positive contacts are source reconstruction discrepancies, not physical interference. Display separation is illustrative.'
meta=dict(units='mm',source_frame='Canonical body mm +X vehicle-left +Y up +Z front.',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=140','https://www.spridgets.de/downloads/Parts-1977.pdf#page=142'],manufacturer_sources=['https://www.bremboparts.com/america/en/catalogue/disc/08-1509-10','https://www.nsk.com/content/dam/nsk/common/catalogs/ctrgPdf/bearings/e1103c_partc.pdf'],castle_nut_source='https://mgbhive.co.uk/product/51k328-midget-front-hub-castle-nut/',owner_dimensions_applied=False,basis='Factory hub/bearing/seal/retention and separate rotor/wheel fixing topology. Existing manufacturer boundary-size comparisons and source stations retained; all new seating/retention construction remains estimated and unregistered.',registration_status='unregistered_front_hub_disc_bearing_retention_comparison',nodes=nodes,sampled_neighbor_contacts_mm3=contacts,limits=limits)
meta['disc_retention_revision']=dict(factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=142',factory_role='BTA370,8 per car; distinct from wheel studs and nuts',catalog_size_sources=['https://rimmerbros.com/Item--i-BTA370','https://www.minispares.com/bta370-drive-flange-bolt-12-wheel'],catalog_thread='3/8 UNF',catalog_major_diameter_mm=2*DISC_BOLT_RADIUS,catalog_underhead_length_mm=DISC_BOLT_LENGTH,former_estimated_major_diameter_mm=6,former_estimated_underhead_length_mm=16,retained_inboard_underhead_face_local_x_mm=8,source_tip_local_x_mm=8+DISC_BOLT_LENGTH,retained_wheel_mount_face_local_x_mm=20,source_tip_beyond_mount_face_mm=8+DISC_BOLT_LENGTH-20,estimated_clearance_bore_radius_mm=DISC_BORE_RADIUS,estimated_outer_flange_lug_radius_mm=12,estimated_head_circumdiameter_mm=16.5,estimated_head_height_mm=4.5,limits='Factory supplies role/count; modern suppliers supply nominal bolt size, not an owner fastener measurement or fitted identity. Inboard head-face pose stays as a construction convention; actual direction, grade, thread receiver/engagement and wheel clearance remain unresolved. No bolt shortening, body/axle move or fitted machining size is inferred. Smooth envelopes omit threads. The historical mesh name retains screw_envelopes for existing links.')
meta['dust_cover_reference']=dict(factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=142',factory_roles=dict(right='BTA472',left='BTA473',small_fixings_car_total=2,small_fixing='HZS503',washer='LWZ505'),modern_comparison_source='https://www.mgocspares.co.uk/DocumentHandler/136685/mgoc-spares-midget-catalogue-pdf',modern_small_screw_comparison='SH605031,5/16 UNF x3/8in; nominal only',small_screw_catalog_major_diameter_mm=7.9375,small_screw_catalog_underhead_length_mm=9.525,owner_dimensions_applied=False,fitted_presence_verified=False,retained_sector_inner_outer_radii_mm=[33,112],retained_panel_center_local_x_mm=-52,estimated_sector_angles_degrees=[-140,140],estimated_projected_sheet_gauge_mm=gauge,estimated_inboard_rim_return_mm=6,estimated_caliper_axis_ear_face_local_x_mm=mount_face,estimated_small_tab_back_front_local_x_mm=[small_mount_back,small_mount_front],estimated_small_screw_underhead_local_x_mm=small_underhead,small_tab_construction='Tangent plane at outmost point of the retained source carrier footprintY45..65/Z-14..14; not a surveyed flat seat. Small head stays outside the casting; receiver is unassigned.',mounting_role_construction='Two folded ears follow the existing caliper mounting axis lines atY+/-27/Z-76; the single small fixing is a separate reference axis atY55/Z0. These are not a factory coordinate map or verified clamping layers.',limits='All outlines, bends, sections and mounting stations remain estimates. Mirrored handed geometry is unverified. No carrier hole, machined land, fitted offset, spacer, receiver thread or hidden body attachment is invented. Source contacts remain registration gaps, not actual-car interference. No loaded support, caliper/wheel/steering sweep, fitted cover identity/presence, safe support, tool access, verified release procedure or removal path is established.')
meta['basis']+=' Native formed dust-cover comparisons now add rolled perimeter and three mounting roles around the retained source rotor/caliper. Four flange lugs and BTA370 nominal envelopes clarify the disc retention relationship.'
meta['limits']+=' '+meta['disc_retention_revision']['limits']+' '+meta['dust_cover_reference']['limits']
a=cq.Assembly(name='unregistered_front_hub_disc_bearing_and_retention_relationships');meshes=[]
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/'front-hub-reference.step'));meta['parts']=meshes;(OUT/'front-hub-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n');s=cq.importers.importStep(str(OUT/'front-hub-reference.step')).val();assert s.isValid();print(json.dumps(dict(meshes=len(parts),solids=len(s.Solids()),step_valid=True,shared_fixing_bores_and_reference_seat_nonintersection=True,cap_retainer_nonintersection=True,source_contacts=contacts)))
