"""1500 flanged/sliding propshaft architecture; no fitted or moving clearances."""
from pathlib import Path
import json,math
import cadquery as cq
OUT=Path(__file__).parent;S=2032/2.4;X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1)
parts=[];nodes={};contacts={};front=-.40*S-8;rear=-.865*S;cy=.39*S
def cyl(r,l,p,a=Z):return cq.Solid.makeCylinder(r,l,p,a)
def ring(ro,ri,l,p,a=Z):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=(.40,.44,.39)):
 assert s.isValid() and s.Volume()>0,n
 parts.append((n,s,c));return s
def hexagon(ro,ri,l,p):return cq.Workplane(cq.Plane(origin=p,normal=Z)).polygon(6,2*ro).extrude(l).val().cut(cyl(ri,l+2,p-Z))
# Boundary comparison: Hindustan Hardy series1140 23.8 mm cap OD /61.3 mm
# overall cap span. EABS links this size family to GUJ101; neither establishes
# this car's fitted kit, internal bearing dimensions or acceptable cup fit.
cup_ro=11.9;span=61.3;cross_z=40;hole_ro=12;clip_z=31
axes=[cq.Vector(35*math.cos(math.pi/4+i*math.pi/2),35*math.sin(math.pi/4+i*math.pi/2),0) for i in range(4)]
def fork_ear(x):
 return cq.Workplane('YZ',origin=(x-7,0,0)).moveTo(-10,6).lineTo(10,6).lineTo(17,40).threePointArc((0,57),(-17,40)).close().extrude(14).val()
flange_forks=comp([fork_ear(-26.5),fork_ear(26.5)])
for label,direction,plane,bore_r,bolt_r,length in [('front',-1,front,5.2,4.7625,30),('rear',1,rear,4,3.96875,31.75)]:
 def tr(s):return (s if direction==1 else s.mirror('XY')).translate((0,cy,plane))
 def point(p):return [p.x,cy+p.y,plane+direction*p.z]
 flange=ring(55,15,6,cq.Vector()).fuse(fork_ear(-26.5)).fuse(fork_ear(26.5))
 for q in axes:flange=flange.cut(cyl(bore_r,12,q-Z*2))
 if label=='rear':flange=flange.cut(cyl(23,12,cq.Vector(0,0,-1)))
 shaft_forks=flange_forks.mirror('XY',basePointVector=(0,0,cross_z)).rotate((0,0,0),(0,0,1),90)
 if label=='front':shaft=shaft_forks.fuse(ring(30,8,6,cq.Vector(0,0,68))).fuse(cyl(13,62,cq.Vector(0,0,68)))
 else:shaft=shaft_forks.fuse(ring(30,20,6,cq.Vector(0,0,68))).fuse(ring(22,20,12,cq.Vector(0,0,68)))
 caps=[];seals=[];clips=[];cup_nodes=[]
 journal=box(18,18,18,cq.Vector(0,0,cross_z))
 for axis in [X,Y]:journal=journal.fuse(cyl(7.8,57.6,cq.Vector(0,0,cross_z)-axis*28.8,axis))
 for axis in [X,Y]:
  for sign in [-1,1]:
   a=axis*sign;c=cq.Vector(0,0,cross_z)
   if axis==X:flange=flange.cut(cyl(hole_ro,80,c-X*40,X)).cut(cyl(12.5,1.3,c+a*clip_z,a))
   else:shaft=shaft.cut(cyl(hole_ro,80,c-Y*40,Y)).cut(cyl(12.5,1.3,c+a*clip_z,a))
   cap=cyl(cup_ro,11.15,c+a*19.5,a).cut(cyl(8.5,10.95,c+a*18.5,a));caps.append(cap)
   seals.append(ring(9.2,7.9,1.3,c+a*18.2,a))
   clip=ring(12.3,10.8,1.3,c+a*clip_z,a)
   # Gap direction is a drawing convention, not installed clip orientation.
   clip=clip.cut(box(8,8,8,c+a*31.65+Z*11));clips.append(clip)
   cup_nodes.append(dict(center_body_mm=point(c+a*25.075),axis_body=[a.x,a.y,0],cap_outer_face_body_mm=point(c+a*(span/2))))
 assert len(flange.Solids())==1 and len(shaft.Solids())==1
 for n,s,color in [('flange_yoke_with_shared_fixing_and_cross_bores',flange,(.40,.44,.39)),('shaft_yoke_and_toothless_slip_stub' if label=='front' else 'shaft_yoke_and_hollow_tube_neck',shaft,(.40,.44,.39)),('connected_cross_journal_without_needle_bearings',journal,(.57,.59,.51)),('four_hollow_bearing_cap_comparisons',comp(caps),(.56,.58,.50)),('four_unloaded_journal_seal_envelopes',comp(seals),(.13,.17,.14)),('four_open_retaining_clip_comparisons',comp(clips),(.55,.57,.49))]:put('prop_'+label+'_'+n,tr(s),color)
 bolts=[];retainers=[]
 for q in axes:
  bolts.append(cyl(bolt_r,length,q-Z*10).fuse(hexagon(8.5,.01,6,q-Z*16)))
  if label=='front':retainers.extend([ring(10,5.2,2,q+Z*6),hexagon(9,5.1,7,q+Z*8)])
  else:retainers.extend([ring(9,4.2,1.5,q+Z*6),hexagon(7.5,4.1,6,q+Z*7.5)])
 put('prop_'+label+'_four_unthreaded_flange_bolt_envelopes',tr(comp(bolts)),(.58,.60,.52));put('prop_'+label+'_four_nut_and_unloaded_washer_sets',tr(comp(retainers)),(.55,.57,.49))
 for q in axes:assert flange.intersect(cyl(bore_r-.1,14,q-Z*3)).Volume()<1e-5
 assert flange.intersect(cyl(11.8,80,cq.Vector(-40,0,cross_z),X)).Volume()<1e-5
 assert shaft.intersect(cyl(11.8,80,cq.Vector(0,-40,cross_z),Y)).Volume()<1e-5
 assert comp(caps).intersect(journal).Volume()<1e-5
 nodes[label]=dict(flange_mating_center_body_mm=point(cq.Vector()),flange_axes_body_mm=[point(q) for q in axes],toward_shaft=[0,0,direction],joint_cross_center_body_mm=point(cq.Vector(0,0,cross_z)),cup_axes=cup_nodes,flange_hole_radius_mm=bore_r,estimated_flange_pcd_mm=70,estimated_flange_clock_deg=45)
# Main tube shrinks from the old estimated R39.793/29.633 section to R22/20.
# Supplier CHA336 photograph supports a thin tube relative to the joint, not
# an exact tube diameter. The 60 mm front-end cutback exposes a slip sleeve.
tube_front=front-140;tube_rear=rear+80
tube=put('prop_main_hollow_tube_with_revised_front_slip_boundary',ring(22,20,tube_front-tube_rear,cq.Vector(0,cy,tube_rear)))
female=ring(19,13.3,44,cq.Vector(0,0,96)).fuse(ring(22,13.3,4,cq.Vector(0,0,136)))
put('prop_front_hollow_sliding_sleeve_and_tube_shoulder',female.mirror('XY').translate((0,cy,front)))
put('prop_front_unloaded_slip_dust_collar_comparison',ring(22,13.3,4,cq.Vector(0,0,92)).mirror('XY').translate((0,cy,front)),(.16,.19,.16))
# Source interfaces only: holes align with retained gearbox and axle flanges.
neighbors=[]
for filename,offset in [('transmission-reference.step',(0,cy,0)),('rear-axle-reference.step',(0,cy,-1.2*S)),('cabin-understructure-reference.step',(0,0,0)),('underfloor-exhaust-reference.step',(0,0,0))]:
 assembly=cq.Assembly.importStep(str(OUT/filename))
 for name,obj in assembly.objects.items():
  if obj.obj is None or name.startswith('prop_'):continue
  shape=obj.obj.val() if isinstance(obj.obj,cq.Workplane) else obj.obj
  if isinstance(shape,cq.Shape):neighbors.append((name,shape.translate(offset)))
for label,neighbor in [('front','gearbox_output_reference'),('rear','differential_pinion_flange_reference')]:
 shape=next(s for n,s in neighbors if n==neighbor);n=nodes[label];axis=cq.Vector(*n['toward_shaft'])
 for p in n['flange_axes_body_mm']:assert shape.intersect(cyl(n['flange_hole_radius_mm']-.1,20,cq.Vector(*p)-axis*10,axis)).Volume()<1e-5
for name,a,_ in parts:
 for other,b in neighbors:
  ba,bb=a.BoundingBox(),b.BoundingBox()
  if not all(getattr(ba,k+'min')<=getattr(bb,k+'max') and getattr(bb,k+'min')<=getattr(ba,k+'max') for k in ['x','y','z']):continue
  v=round(a.intersect(b).Volume(),4)
  if v>1e-4:contacts[name+'__'+other]=v
limits='No new owner dimensions or fitted shaft/UJ/flange/fastener identity applied. Factory p145 distinguishes CHA336 front slip/fixed flange-yoke and sleeve-yoke roles, two GUJ101 journals, four axle-end and four gearbox-end fixings; earlier AHA9053 remains separate. Factory p81 identifies gearbox output flange/washer/nut but their fitted spline and loaded retention are not reconstructed. Hindustan Hardy series1140 23.8x61.3 mm cap-OD/span is an independent manufacturer boundary comparison; EABS supplier links this size family with GUJ101, without proving a fitted cross-reference, internal bearing dimensions, press fit or interchangeability. Supplier CHA336 photo supports exterior/sliding architecture only. All yoke profiles, gauges, trunnions, cap interiors, seal/clip/groove details, fixing maps and installed positions remain estimates. The old estimated mating planes and joint cross centers are retained at zero-angle pose. The old 70 mm flange PCD is retained; both patterns clock45 degrees to clear the new fork roots, and front holes grow R4 to R5.2 for the 3/8 inch fixing comparison. Rear holes retain R4 with a 5/16 inch shank comparison. Gearbox output and pinion flange references share those construction maps; coarse transmission joint meshes remain omitted from the combined viewer. New 22/20 mm outer/inner tube radii replace the old 39.793/29.633 mm estimates; neither is an owner or factory tube dimension. Front tube boundary cuts back60 mm to expose an estimated sleeve and toothless stub overlap, not fitted shaft length, spline engagement or travel. No Volvo 1.75-inch tube application is transferred to this car. Front bolt length30 mm is estimated; rear31.75 mm is a supplier replacement comparison, not fitted length/engagement. No threads, splines, needle rollers, lubrication circuit or calibrated loaded seals/clip seating. Source contacts are reconstruction discrepancies or overlapping reference boundaries, not actual-car interference. A later pinion-support comparison adds an estimated R23 central recess through the rear flange/root region to accommodate the separate retaining nut/washer envelope. Only the rear flange-yoke mesh changes; eighteen other propshaft meshes remain identical. This is source construction, not fitted pilot, spanner clearance or tool access. No balance, speed rating, operating phasing, joint angles, suspension travel, safe release/support, torque, press/support sequence or verified shaft/cup removal path. Inspection 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=145','https://www.spridgets.de/downloads/Parts-1977.pdf#page=81'],manufacturer_source='https://www.hhardys.com/ujkit.html',supplier_sources=['https://www.mgocspares.co.uk/parts/mg/midget/clutch-gearbox-axle/propshaft','https://www.eabs.co.uk/product/classic-car-universal-joint-23-8x61-3mm-fits-multiple-makes-see-list-for-details','https://www.mgocspares.co.uk/product/cha336/propshaft-1500-new'],owner_dimensions_applied=False,basis='Factory 1500 flanged/sliding shaft and separate journal/fixing roles, supplier exterior photograph and independent manufacturer UJ boundary comparison. Existing owner cabin/underbody context is not a calibrated propshaft/joint survey. All new installed geometry and fits are estimated.',registration_status='unregistered_1500_propshaft_and_joint_comparison',manufacturer_cap_boundary_comparison_mm=dict(diameter=23.8,span=61.3),nodes=nodes,estimated_main_tube=dict(outer_radius_mm=22,inner_radius_mm=20,front_body_z_mm=tube_front,rear_body_z_mm=tube_rear,front_cutback_mm=60),sampled_source_contacts_mm3=contacts,limits=limits)
assembly=cq.Assembly(name='unregistered_1500_propshaft_yokes_journals_and_slip_boundary');meshes=[]
for name,s,color in parts:
 assembly.add(s,name=name,color=cq.Color(*color));vs,ts=s.tessellate(.5,.16);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/'propshaft-reference.step'));meta['parts']=meshes;(OUT/'propshaft-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n');restored=cq.importers.importStep(str(OUT/'propshaft-reference.step')).val();assert restored.isValid();print(json.dumps(dict(meshes=len(parts),solids=len(restored.Solids()),step_valid=True,shared_flange_bores=True,cross_cup_passages=True,source_contacts=contacts)))
