"""Rear wheelhouse and shackle/body relationships. No fitted survey or travel."""
from pathlib import Path
import json, math
import cadquery as cq
from rear_cabin_layout import layout
OUT=Path(__file__).parent; BODY=json.loads((OUT.parent/'body-data.json').read_text())
S=BODY['published_reference_dimensions_mm']['wheelbase']/2.4
T=1.5; X=cq.Vector(1,0,0); Y=cq.Vector(0,1,0); Z=cq.Vector(0,0,1)
FLOOR_SKIN=BODY['boot_compartment_reference']['estimated_floor_and_lid_skin_offset_mm']
LAYOUT=layout(BODY)
parts=[];nodes={};floor_y=.49*S;floor_under=floor_y-FLOOR_SKIN;doubler_under=floor_under-3
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 ring(ro,ri,l,c,axis=X):
 return cq.Solid.makeCylinder(ro,l,c-axis*l/2,axis).cut(cq.Solid.makeCylinder(ri,l+2,c-axis*(l/2+1),axis))
def cut_bore(shape,p,r,axis,length=180):return shape.cut(cq.Solid.makeCylinder(r,length,p-axis*length/2,axis))
def prism(points,delta):return cq.Solid.extrudeLinear(cq.Wire.makePolygon(points+[points[0]]),[],delta)
def add(name,shape,color=(.25,.32,.26)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color));return shape
def band(center,inner,outer,start,end,depth):
 ps=[center+cq.Vector(0,r*math.sin(a),r*math.cos(a)) for r,angles in [(outer,[start+(end-start)*i/72 for i in range(73)]),(inner,[end-(end-start)*i/72 for i in range(73)])] for a in angles]
 return prism(ps,cq.Vector(depth,0,0))
def capsule(a,b,width,thick):
 plane=cq.Plane(origin=(a+b)*.5,xDir=(b-a).normalized(),normal=X)
 s=cq.Workplane(plane).rect((a-b).Length,width).extrude(thick/2,both=True).val()
 for p in [a,b]:s=s.fuse(cq.Solid.makeCylinder(width/2,thick,p-X*thick/2,X))
 return s

for side,label in [(1,'left'),(-1,'right')]:
 c=V([side*.48,.43,-1.90]);lower=V([side*.48,.33,-1.86]);half=.03*S
 # Replacement-panel architecture is a comparison. The existing .37-unit
 # wheelhouse radius and .47/.74 X boundaries are retained drawing datums.
 wc=V([side*.47,.39,-1.20]);radius=.37*S
 disk_points=[wc]+[wc+cq.Vector(0,radius*math.sin(i*math.pi/72),radius*math.cos(i*math.pi/72)) for i in range(73)]
 web=prism(disk_points,X*side*T)
 # The inner wall meets the retained floor rather than ending34mm above it.
 skirt=prism([V([side*.47,y,z])for z,y in [(-.83,.35),(-1.57,.35),(-1.57,.39),(-.83,.39)]],X*side*T)
 web=web.fuse(skirt).clean()
 assert web.isValid() and len(web.Solids())==1
 roof=band(wc,radius,radius+T,0,math.pi,side*(.74-.47)*S)
 tube_tool=cq.Solid.makeCylinder(34,2*S,V([-1,.39,-1.20]),X)
 # Explicit static drawing relief for the retained damper body, not a factory
 # opening or fitted body clearance. Do not move the old damper to hide a clash.
 damper_tool=box(.13*S+2,.13*S+2,.14*S+2,V([side*.48,.43,-1.56]))
 wheelhouse=cq.Compound.makeCompound([web.cut(tube_tool).cut(damper_tool),roof.cut(damper_tool)])
 add('rear_'+label+'_thin_inner_wheelhouse_web_and_roof_reference',wheelhouse)
 # Narrow formed reinforcement along the inboard arch; section/gauge are
 # estimates, not traced manufacturer geometry or a continuous proven load path.
 rc=V([side*.445,.39,-1.20]);inner=.33*S
 rw=band(rc,inner,radius,0,math.pi,side*T)
 top=band(rc,radius,radius+T,0,math.pi,side*.025*S)
 bottom=band(rc,inner-T,inner,0,math.pi,-side*.015*S)
 reinforcement=cq.Compound.makeCompound([s.cut(damper_tool) for s in [rw,top,bottom]])
 add('rear_'+label+'_formed_wheelhouse_to_floor_reinforcement_comparison',reinforcement)

 # Local underfloor doubler and open side returns, distinct from the detachable
 # rear mounting. The three shared holes are construction, not a drilling map.
 doubler=box(.18*S,3,.22*S,cq.Vector(c.x,floor_under-1.5,c.z))
 returns=[box(T,.035*S,.22*S,cq.Vector(c.x+sign*.09*S,doubler_under-.035*S/2,c.z)) for sign in [-1,1]]
 body=cq.Compound.makeCompound([doubler,*returns]);holes=[]
 for relief in BODY.get('native_rear_frame_placement',{}).get('estimated_floor_reliefs',[]):
  r=relief['narrow_half_width_mm'];depth=relief['narrow_depth_mm'];cx=relief['center_x_units']*S;rz=relief['rear_z_units']*S
  body=body.cut(box(2*r,100,depth,cq.Vector(cx,floor_under,rz+depth/2)))
 plate_top=doubler_under;plate=box(.14*S,3,.16*S,cq.Vector(c.x,plate_top-1.5,c.z))
 fixings=[]
 for dx,dz in [(-.045,.045),(.045,.045),(0,-.05)]:
  p=cq.Vector(c.x+dx*S,plate_top,c.z+dz*S);holes.append(list(p.toTuple()))
  body=cut_bore(body,p,4.5,Y);plate=cut_bore(plate,p,4.5,Y)
  fixings += [cq.Solid.makeCylinder(4,9,p-Y*3,Y),cq.Solid.makeCylinder(7,4,p-Y*7,Y)]
 add('rear_'+label+'_local_shackle_floor_doubler_and_open_returns_reference',body)
 # Supplier photo AHA7201 shows paired triangular cheeks and a transverse
 # sleeve. All dimensions here are estimates around the retained eye axis.
 cheeks=[]
 for sign in [-1,1]:
  x=c.x+sign*(half-1.5)
  ps=[cq.Vector(x-1.5,plate_top-3,c.z-.07*S),cq.Vector(x-1.5,plate_top-3,c.z+.07*S),cq.Vector(x-1.5,c.y-25,c.z)]
  cheek=cut_bore(prism(ps,X*3),c,18.63,X);cheeks.append(cheek)
 sleeve=ring(22,18.63,2*half,c)
 bracket=cq.Compound.makeCompound([plate,*cheeks,sleeve])
 add('rear_'+label+'_three_fixing_shackle_bracket_cheeks_and_open_sleeve_comparison',bracket,(.34,.37,.32))
 add('rear_'+label+'_three_body_fixing_envelopes',cq.Compound.makeCompound(fixings),(.52,.53,.47))
 # Four flanged bush halves per side (eight per car). Retain former eye/bush
 # outside radii. Reduce the old 20.32 mm drawing pin to an explicit estimated
 # 10 mm shoulder; 3/8 UNF terminal is a catalog thread comparison only.
 bushes=[];flange=3;plate_t=4;offset=half+flange+plate_t/2
 for eye in [c,lower]:
  for sign in [-1,1]:
   core=ring(.021*S,5.1,half,eye+X*sign*half/2)
   lip=ring(22,5.1,flange,eye+X*sign*(half+flange/2))
   bushes.append(core.fuse(lip))
 add('rear_'+label+'_four_flanged_shackle_bush_halves_comparison',cq.Compound.makeCompound(bushes),(.12,.16,.13))
 a=c-X*side*offset;b=lower-X*side*offset
 pinplate=capsule(a,b,32,plate_t)
 for eye in [c,lower]:
  start=eye-X*side*(offset-plate_t/2);axis=X*side
  shaft=cq.Solid.makeCylinder(5,2*offset-plate_t,start,axis)
  end=eye+X*side*(offset+plate_t/2)
  terminal=cq.Solid.makeCylinder(4.7625,12,end,axis)
  pinplate=pinplate.fuse(shaft).fuse(terminal)
 add('rear_'+label+'_shackle_plate_with_two_smooth_pins_comparison',pinplate,(.47,.49,.43))
 free=capsule(c+X*side*offset,lower+X*side*offset,32,plate_t)
 for eye in [c,lower]:free=cut_bore(free,eye,5.3,X)
 add('rear_'+label+'_separate_bored_shackle_closing_plate_comparison',free,(.47,.49,.43))
 retainers=[]
 for eye in [c,lower]:
  p=eye+X*side*(offset+plate_t/2)
  retainers += [ring(9,5.2,2,p+X*side),ring(9,5.0,8,p+X*side*6)]
 add('rear_'+label+'_shackle_washer_and_smooth_nut_envelopes',cq.Compound.makeCompound(retainers),(.51,.53,.46))
 for eye in [c,lower]:
  gauge=cq.Solid.makeCylinder(4.9,120,eye-X*60,X)
  assert free.intersect(gauge).Volume()<1e-5
  for bush in bushes:assert bush.intersect(gauge).Volume()<1e-5
 assert bracket.intersect(cq.Solid.makeCylinder(18.5,120,c-X*60,X)).Volume()<1e-5
 for h in holes:
  gauge=cq.Solid.makeCylinder(4.3,25,cq.Vector(*h)-Y*10,Y)
  assert bracket.intersect(gauge).Volume()<1e-5 and body.intersect(gauge).Volume()<1e-5
 assert wheelhouse.intersect(cq.Solid.makeCylinder(33,1.5*S,V([-.75,.39,-1.20]),X)).Volume()<1e-5
 nodes[label]=dict(upper_eye_center_mm=list(c.toTuple()),lower_eye_center_mm=list(lower.toTuple()),pin_axis=[1,0,0],body_fixing_centers_mm=holes,body_fixing_bore_radius_mm=4.5,bracket_plate_top_y_mm=plate_top,boot_floor_top_y_mm=floor_y,boot_floor_skin_mm=FLOOR_SKIN,shackle_plate_center_x_offsets_mm=[-offset,offset],old_plate_center_x_offsets_mm=[-.043*S,.043*S],estimated_pin_shoulder_diameter_mm=10,old_pin_diameter_mm=.024*S,estimated_bush_bore_diameter_mm=10.2,estimated_sleeve_bore_diameter_mm=37.26,estimated_plate_bore_diameter_mm=10.6,estimated_bush_flange_thickness_mm=flange,estimated_plate_thickness_mm=plate_t)

limits='Matching rear-open narrow reliefs in the local shackle doubler follow the new retained lower bumper-stud construction boundary; they are not identified factory openings, repairs or verified clearances. No direct owner dimensions or fitted part identities are applied. Factory AHA7201/AHA7686/AHA7687/AHA7182 roles and manufacturer BMH8014/15/AHA7240/41 architecture are comparisons, not identification or repair suitability. All body profiles, gauge, reinforcement section, sleeve, bush flanges, fixing map, shoulder/terminal dimensions, side assignment, spacing, contacts and installed stations remain estimates. Upper/lower rear eye axes, lower spring-eye outline and wheelhouse radius/X span are retained drawing datums. The old 20.32 mm pin envelopes become estimated 10 mm shoulders; 3/8 UNF is a separate nut/thread catalog comparison, not proof of shoulder diameter or fitted pin geometry. Closing plate spacing changes only to meet the declared bush flange stack. Six boot-floor/doubler/bracket holes share construction centers, not a measured body map, cutting template or verified captive-nut arrangement. Thin wheelhouses replace zero-thickness membranes; 34 mm axle passages relieve the retained 32 mm tube, and damper body reliefs follow the old damper box plus 2 mm total across each dimension. These are explicit static drawing corrections, not fitted openings or clearance/travel allowances. Reinforcement follows retained arch shape without a surveyed seam, continuous load path or weld schedule. Separate rear-frame geometry now supplies estimated formed damper/upper-strap panels, bores and fixing envelopes; bump stop identity/attachment/strike surface remains unlocated. No loaded bush compression, body strength, suspension travel, wheel/tyre or exhaust clearance, support/release procedure, thread/retention function, tightening value or verified removal path. Body panels/doublers stay fixed; detachable bracket and joint separation is illustrative.'
meta=dict(units='mm',source_frame='Canonical body +X vehicle-left, +Y up, +Z front; millimetres.',factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=150',bracket_photo_source='https://rimmerbros.com/Item--i-AHA7201',manufacturer_source='https://www.bmh-ltd.com/vehicle-parts/mg-midget-parts/mg-midget-rear-body/',thread_comparison_source='https://www.msc.parts/contents/en-uk/d444_Rear_Suspension.html',owner_photos=['IMG_6329','IMG_6341','IMG_6919'],owner_dimensions_applied=False,basis='Factory p150 separates three body screws per AHA7201 rear mounting, plate with pins, closing plate and eight bush halves. Rimmer own AHA7201 photo corroborates triangular cheeks and transverse sleeve. Manufacturer separates inner wheelhouses, wheelhouse/floor reinforcement and rubber-bumper CZJ568/CZJ602 body assemblies from earlier alternatives. Owner side/cabin photos do not expose hidden rear joints; IMG_6919 is front underside context, not a rear survey.',nodes=nodes,estimated_wheelhouse=dict(center_yz_units=[.39,-1.20],retained_radius_units=.37,retained_x_magnitudes_units=[.47,.74],sheet_gauge_mm=T,static_axle_relief_radius_mm=34,retained_axle_tube_radius_mm=32,damper_relief_dimensions_mm=[.13*S+2,.13*S+2,.14*S+2]),limits=limits)
assembly=cq.Assembly(name='estimated_rear_wheelhouses_and_shackle_body_mounts');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/'rear-body-mount-reference.step'))
meta['parts']=meshes;meta['rear_cabin_placement_revision']=LAYOUT
meta['limits']+=' The retained inner wheelhouse webs now extend from the former axle-height chord down to the existing floor plane. Radius, inner/outer X planes, axle relief, damper relief, reinforcement and shackle source geometry stay. The skirt is a construction section, not an identified fitted pressing or weld/load path.'
(OUT/'rear-body-mount-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'rear-body-mount-reference.step')).val();assert restored.isValid()
print(json.dumps(dict(groups=len(parts),solids=len(restored.Solids()),step_valid=True,six_shared_body_bores=True,four_shackle_axis_passages=True,eight_separate_bush_halves=True,static_axle_reliefs_open=True)))
