"""Photo-led hollow front structure. All contours, sheet sections and fits estimated.
Retains existing body/suspension datums; not a welding or jacking template.
"""
from pathlib import Path
import json,math
import cadquery as cq
from front_frame_geometry import rail as continuous_rail,crossmember as formed_crossmember,metadata as frame_metadata,NORMAL,HOLE_X,HOLE_Y,RX,RY,front_z
OUT=Path(__file__).parent;S=json.loads((OUT.parent/'body-data.json').read_text())['published_reference_dimensions_mm']['wheelbase']/2.4
parts=[];nodes={};W=.003
PICKUP=json.loads((OUT/'front-fulcrum-reference.json').read_text())['body_pickup_construction']

def V(p):return cq.Vector(*(v*S for v in p))
def box(w,h,d,p):return cq.Workplane('XY').box(w*S,h*S,d*S).translate(V(p).toTuple()).val()
def cyl(r,l,p,axis=(0,0,1)):return cq.Solid.makeCylinder(r*S,l*S,V(p),cq.Vector(*axis))
def add(name,shape,color=(.24,.29,.25)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color))
def hollow(w,h,d,p):
 # Longitudinal open ends reveal wall construction; end closure remains unknown.
 return box(w,h,d,p).cut(box(w-2*W,h-2*W,d+.004,p))
rails=[]
for side,label in [(1,'left'),(-1,'right')]:
 rail=continuous_rail(side,PICKUP)
 rails.append(rail);add('front_structure_'+label+'_open_rail_reference',rail)
# Owner-photo front face and formed upper saddle, under retained datums.
cross=formed_crossmember()
add('front_structure_formed_crossmember_comparison',cross)
for side,label in [(1,'left'),(-1,'right')]:
 x=side*.45;axis=cq.Vector(0,1,0)
 outer=cq.Solid.makeCone(.125*S,.112*S,.216*S,V([x,.385,1.20]),axis)
 inner=cq.Solid.makeCone((.125-W)*S,(.112-W)*S,.220*S,V([x,.383,1.20]),axis)
 pocket=outer.cut(inner)
 # Outboard opening below the upper ring, no closed sleeve around the upright.
 pocket=pocket.cut(box(.24,.200,.30,[x+side*.145,.475,1.20]))
 add('front_structure_'+label+'_open_spring_pocket_comparison',pocket)
 seat=cyl(.112,.018,[x,.601,1.20],(0,1,0)).cut(cyl(.012,.024,[x,.598,1.20],(0,1,0)))
 add('front_structure_'+label+'_upper_spring_seat_reference',seat)
 platform=box(.16,.006,.24,[side*.36,.6195,1.20])
 # Factory three damper screws per side; centers are illustrative only.
 mount_holes=[(side*.31,1.12),(side*.41,1.12),(side*.36,1.28)]
 for hx,hz in mount_holes:platform=platform.cut(cyl(.006,.012,[hx,.613,hz],(0,1,0)))
 web=box(.006,.235,.20,[side*.30,.501,1.20])
 web=web.cut(cyl(.055,.016,[side*.30-.008,.51,1.20],(1,0,0)))
 web_foot=box(.065,.004,.20,[side*.331,.385,1.20]).cut(cyl(.105,.012,[x,.379,1.20],(0,1,0)))
 add('front_structure_'+label+'_damper_platform_web_comparison',cq.Compound.makeCompound([platform,web,web_foot]))
 ears=[]
 for z in [.98,1.42]:
  for dz in PICKUP['ear_z_offsets_units']:
   ear=box(PICKUP['ear_outer_width_units'],PICKUP['ear_outer_height_units'],.006,[side*.36,PICKUP['ear_center_y_units'],z+dz]).cut(cyl(PICKUP['ear_bore_radius_mm']/S,.010,[side*.36,.27,z+dz-.005]))
   ears.append(ear)
 add('front_structure_'+label+'_lower_pickup_ear_comparison',cq.Compound.makeCompound(ears))
 # Unselected AHA6378-style internal stop, no installed contact/travel inferred.
 stop=cq.Solid.makeCone(.026*S,.043*S,.070*S,V([x,.531,1.20]),axis)
 stud=cyl(.004,.027,[x,.594,1.20],(0,1,0))
 add('front_structure_'+label+'_suspension_buffer_comparison',cq.Compound.makeCompound([stop,stud]),(.13,.16,.13))
 # Generic body pad to existing rack-clamp lower face; BMH8007 is not assigned.
 rack_nodes=json.loads((OUT/'steering-rack-reference.json').read_text())['nodes'][label];mount_x=rack_nodes['mount_center_units'][0]
 a=V([side*.36,.385,1.43]);b=V([mount_x,.35,1.43]);d=b-a
 incline=cq.Workplane('XY').box(d.Length,.006*S,.14*S).val().rotate((0,0,0),(0,0,1),math.degrees(math.atan2(d.y,d.x))).translate(((a+b)*.5).toTuple())
 foot=box(.09,.006,.14,[mount_x,.347,1.43])
 pad=cq.Compound.makeCompound([incline,foot])
 # New steering support uses one declared construction bore per body pad.
 for n in json.loads((OUT/'steering-rack-reference.json').read_text())['nodes'].values():
  p=n['body_fixing_center_mm'];pad=pad.cut(cq.Solid.makeCylinder(n['body_bore_radius_mm'],30,cq.Vector(p[0],p[1]-15,p[2]),cq.Vector(0,1,0)))
 add('front_structure_'+label+'_unassigned_rack_body_pad',pad)
 nodes[label]=dict(rail_center_units=[side*.36,.32,.98],rail_upper_y_units=.385,rail_lower_y_units=.255,lower_link_centers_units=[[side*.36,.27,.98],[side*.36,.27,1.42]],pickup_ear_z_offsets_units=PICKUP['ear_z_offsets_units'],spring_axis_units=[x,1.20],upper_spring_seat_y_bounds_units=[.601,.619],damper_platform_upper_y_units=.6225,damper_hole_stations_units=mount_holes,rack_clamp_lower_y_units=.35,rack_pad_center_x_units=mount_x)
 # Existing reference axis can pass through both ears; bushes remain separate.
 for z in [.98,1.42]:
  for dz in PICKUP['ear_z_offsets_units']:assert cq.Compound.makeCompound(ears).intersect(cyl(5/S,.012,[side*.36,.27,z+dz-.006])).Volume()<1e-5
 for hx,hz in mount_holes:assert platform.intersect(cyl(.004,.016,[hx,.611,hz],(0,1,0))).Volume()<1e-5
assert cross.intersect(cyl(.08,.13,[0,.39,1.10])).Volume()<1e-5
for x in HOLE_X:
 p=cq.Vector(x,HOLE_Y,front_z(HOLE_Y));plane=cq.Plane(origin=p,xDir=(1,0,0),normal=NORMAL)
 assert cross.intersect(cq.Workplane(plane).ellipse(RX-1,RY-1).extrude(8,both=True).val()).Volume()<1e-5
 assert cross.intersect(cq.Solid.makeCylinder(1,10,p+cq.Vector(RX+5,0,0)-NORMAL*5,NORMAL)).Volume()>1
assembly=cq.Assembly(name='photo_supported_hollow_front_structure_reference');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=shape.tessellate(.7,.15)
 meshes.append(dict(name=name,color=color,vertices=[round(v,4) for p in vs for v in p.toTuple()],triangles=[i for tri in ts for i in tri]))
assembly.export(str(OUT/'front-structure-reference.step'))
limits='All rails, crossmember opening, access holes, wall sections, spring pockets/seats, platforms, pickup ears, rack pads, buffers and body/suspension registration are estimates. Owner underside photos support formed structure and visible openings, not dimensions, hidden welds or corrosion thickness. Mirroring is an unverified construction assumption. Existing rail outer/upper/lower, wishbone pivot, spring, damper and rack datums are retained only as drawing references. Revised rail pockets and ear spacing/bores follow the declared inner-fulcrum catalog-section comparison and estimated local stack, not measured body cutouts or fitted compression. Pivot centerlines are retained. Three damper bores per side follow factory count only; no hole map, thread or bolt engagement is verified. AHA6378-style stop and generic rack pads are comparisons with no fitted identity. Two new rack-pad construction bores follow the separate steering reference; the left generic pad follows its declared X=.27 to .16 inward construction relocation, and both incline angles now follow their actual endpoint vectors; these are not measured factory holes or a drilling template. BMH8007 is chrome-bumper-only and is not assigned to this 1975 body. No strength, alignment, spring restraint, loaded contact, bump/rebound limit, working clearance, jacking/support point, repair/weld plan or verified service/removal path. Welded body groups stay fixed for inspection; isolation does not make them detachable.'
meta=dict(units='mm',source_frame='Canonical body +X vehicle-left, +Y up, +Z front, mm.',manufacturer_source='https://www.bmh-ltd.com/vehicle-parts/mg-midget-parts/mg-midget-front-centre-body/',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=139','https://www.spridgets.de/downloads/Parts-1977.pdf#page=141'],owner_photos=['IMG_6910','IMG_6914','IMG_6919'],basis='Manufacturer distinguishes rubber-bumper CZJ676 H frame, BMH8030/31 side members and CZJ542/543 handed link-bracket applications from earlier body versions. Factory suspension plate supplies lower-fulcrum and damper/spring/buffer topology and counts, not body coordinates. Owner underside views show broad formed webs, multiple openings and a large upper mouth.',limits=limits,estimated_wall_mm=W*S,lower_pickup_construction=PICKUP,estimated_upper_crossmember_opening=dict(center_units=[0,.47,1.20],radius_units=.23),former_lower_access_hole_centers_units=[[x,.235,1.20] for x in [-.22,0,.22]],nodes=nodes,parts=meshes)
meta['frame_reconstruction']=frame_metadata()
meta['limits']+=' '+frame_metadata()['limits']
(OUT/'front-structure-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'front-structure-reference.step')).val();assert restored.isValid()
print(json.dumps(dict(parts=len(parts),solids=len(restored.Solids()),step_valid=True,pickup_and_damper_bores_open=True,crossmember_upper_saddle_and_front_face_openings=True,retained_drawing_datums=True)))
