"""Rear tank / flange / sender / body-stud comparisons. Not fitted dimensions."""
from pathlib import Path
import json,math
import cadquery as cq
from front_wing_geometry import absolute_tessellation
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=[]
def V(p):return cq.Vector(*(v*S for v in p))
def cyl(r,l,p,a=Y):return cq.Solid.makeCylinder(r,l,p,a)
def ring(ro,ri,l,p,a=Y):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 compound(xs):return cq.Compound.makeCompound(xs)
def add(name,shape,color=(.31,.34,.30)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color));return shape
# RO9F manufacturer bounds, not fitted-car measurements. The catalog does
# not dimension datum faces: overall lip bounds and neck-inclusive height
# are explicit interpretation assumptions. Pan/neck split uses its front view.
W,D,H=28.5*25.4,14.5*25.4,8*25.4;T=.029*25.4
FLANGE_OVERHANG=20.;PAN_W,PAN_D=W-2*FLANGE_OVERHANG,D-2*FLANGE_OVERHANG
NECK_RISE=H*91/390;PAN_HEIGHT=H-NECK_RISE
floor=.49*S-BODY['boot_compartment_reference']['estimated_floor_and_lid_skin_offset_mm']
roof=floor-1.;bottom=roof-PAN_HEIGHT;center=cq.Vector(0,(roof+bottom)/2,-1.75*S)
# Factory/supplier body-stud and thin-strip mounting roles establish topology,
# not loaded seating. One1mm unloaded construction strip meets each datum.
def rounded(w,h,d,c,r,g=6):return cq.Workplane('XY').box(w,h,d).edges('|Y').fillet(r).edges('not |Y').fillet(g).translate(c.toTuple()).val()
outer=rounded(PAN_W,PAN_HEIGHT,PAN_D,center,.04*S)
inner=rounded(PAN_W-2*T,PAN_HEIGHT-2*T,PAN_D-2*T,center,.04*S-T,6-T)
tank=outer.cut(inner)
flange=box(W,2,D,cq.Vector(0,roof-1,-1.75*S)).cut(box(PAN_W-6,5,PAN_D-6,cq.Vector(0,roof-1,-1.75*S)))
tank=tank.fuse(flange)
filler=cq.Vector(.18*S,roof+(NECK_RISE-1)/2,-1.86*S);vapor=cq.Vector(.30*S,roof+.010*S,-1.58*S);sender=cq.Vector(-.10*S,roof+.010*S,-1.72*S)
# X/Z port datums are inherited unmeasured drawing choices, not RO9F port
# coordinates. Only the neck rise follows the conditional overall envelope.
for p,ro,ri,height in [(filler,.042*S,.042*S-2,NECK_RISE+1),(vapor,.012*S,.012*S-2,.035*S)]:
 neck=cyl(ro,height,p-Y*height/2);tank=tank.fuse(neck).cut(cyl(ri,height+12,p-Y*(height/2+6)))
# Factory plate90 and the RO9F top photograph show a recessed sender well.
# These sections are unscaled architecture estimates, not measured recess
# dimensions or a clearance fit to the independently estimated boot floor.
WELL_DEPTH=16.;WELL_OUTER_R=76.;WELL_SEAT_R=60.;SENDER_BORE_R=38.
seat_top=roof-WELL_DEPTH;sender_base=seat_top+1.5
# Normal sheet offset through the slope, with mitered inner corners, retains the
# original38mm bore axis and all remaining tank dimensions/mounting datums.
well_slope=WELL_DEPTH/(WELL_OUTER_R-WELL_SEAT_R)
well_miter=T*(math.sqrt(1+well_slope**2)-1)/well_slope
well_profile=[(SENDER_BORE_R,seat_top),(WELL_SEAT_R,seat_top),(WELL_OUTER_R,roof),(WELL_OUTER_R+well_miter,roof-T),(WELL_SEAT_R+well_miter,seat_top-T),(SENDER_BORE_R,seat_top-T)]
well=cq.Workplane(cq.Plane(origin=(sender.x,0,sender.z),xDir=X,normal=Z)).polyline(well_profile).close().revolve(360,(0,0),(0,1)).val()
tank=tank.cut(cyl(WELL_OUTER_R,WELL_DEPTH+4,cq.Vector(sender.x,seat_top-2,sender.z))).fuse(well).clean()
assert tank.isValid() and len(tank.Solids())==1
mounts=[]
for sign,side in [(1,'left'),(-1,'right')]:
 for z in [-1.60,-1.775,-1.95]:
  p=cq.Vector(sign*(W/2-10),roof,z*S);tank=tank.cut(cyl(4.5,12,p-Y*6));mounts.append(dict(side=side,axis_center_body_mm=list(p.toTuple()),floor_underside_y_mm=floor,bore_radius_mm=4.5))
add('fuel_tank_hollow_shell_flange_seat_and_open_necks_comparison',tank)
# Plain underside body, head and two unassigned terminal envelopes only.
unit=cyl(50.8,2,cq.Vector(sender.x,sender_base,sender.z)).fuse(cyl(35,20,cq.Vector(sender.x,sender_base-20,sender.z)))
for dx in [-9,9]:unit=unit.fuse(box(3,7,5,cq.Vector(sender.x+dx,sender_base+5.4,sender.z)))
add('fuel_sender_plain_head_underside_and_terminal_envelopes',unit,(.52,.54,.48))
gasket=ring(50.8,38,1.5,cq.Vector(sender.x,seat_top,sender.z));add('fuel_sender_uncompressed_annular_seal_comparison',gasket,(.13,.16,.13))
lock=ring(56,45,3,cq.Vector(sender.x,sender_base+2,sender.z))
for angle in [0,120,240]:
 a=math.radians(angle);p=cq.Vector(sender.x+55*math.cos(a),sender_base+4,sender.z+55*math.sin(a));tab=box(12,2,9,p).rotate(p.toTuple(),(p+Y).toTuple(),-angle);lock=lock.fuse(tab)
add('fuel_sender_lock_ring_and_generic_tool_lugs_comparison',lock,(.46,.48,.42))
studs=[];retainers=[]
for n in mounts:
 x,y,z=n['axis_center_body_mm'];floor=n['floor_underside_y_mm'];end=roof-11
 stud=cyl(4,floor-1-end,cq.Vector(x,end,z)).fuse(cyl(8,1,cq.Vector(x,floor-1,z)));studs.append(stud)
 plain=ring(9,4.3,1.5,cq.Vector(x,roof-3.5,z));shake=ring(8.5,4.3,1,cq.Vector(x,roof-4.5,z));nut=ring(8,4.3,6,cq.Vector(x,roof-10.5,z));retainers.extend([plain,shake,nut])
add('fuel_tank_six_body_stud_and_weld_base_envelopes',compound(studs),(.54,.56,.49))
add('fuel_tank_six_plain_shakeproof_and_smooth_nut_envelopes',compound(retainers),(.54,.56,.49))
strips=[]
for sign in [-1,1]:
 strips.append(box(12,1,PAN_D-20,cq.Vector(sign*(W/2-10),roof+.5,-1.75*S)))
 strips.append(box(PAN_W-20,1,8,cq.Vector(0,roof+.5,-1.75*S+sign*(D/2-10))))
add('fuel_tank_two_long_two_short_thin_seating_strip_comparisons',compound(strips),(.13,.16,.13))
for n in mounts:
 p=cq.Vector(*n['axis_center_body_mm']);assert tank.intersect(cyl(4.3,12,p-Y*6)).Volume()<1e-5
assert tank.intersect(unit).Volume()<1e-5
assert tank.intersect(gasket).Volume()<1e-5
assert unit.intersect(lock).Volume()<1e-5
assert compound(retainers).intersect(compound(studs)).Volume()<1e-5
limits='No direct owner dimensions, fitted tank/sender identities, capacity, load or common body registration are applied. Factory ELC p90 BRA960/ARA1501/ARA1502 sender/lock/seal is a configuration comparison, distinct from earlier six-screw sender units and later tank branches. Factory non-ELC p89 lists six mounting nuts/plain/shakeproof washers; supplier mounting description independently supplies six body-welded studs and two-long/two-short seating-strip roles, not proof of this car. Supplier generic tank/sender narrative disagrees with factory early GAN6 locking-ring coverage; no fitted branch is selected by that narrative. Body sheet thickness now uses the RO9F catalog comparison; all remaining pressing, flange, opening, boss, terminal, lug, mount map, stud/retainer and strip dimensions are estimates. The subsequent manufacturer-envelope revision replaces the old plan/height. RO9F catalog width/length/height and sheet thickness are reference specifications, not fitted-car dimensions. The former raised sender seat is superseded by an annular recessed well, supported qualitatively by factory plate90 and manufacturer top-view photograph. Its16mm depth,76mm outer radius,60mm seat radius and mitered profile are unmeasured construction choices, not factory/owner dimensions or a fitted boot-floor clearance. Sender/lock/seal move together to the well seat; their existing dimensions and X/Z axis remain. Interpretation of overall lip bounds and neck-inclusive height is assumed; pan/neck split uses an uncalibrated manufacturer front-view ratio. Filler/vapor X/Z and radii remain inherited unmeasured drawing choices, not RO9F port coordinates. No capacity or fluid-volume fit is performed. Six bores retain their Z stations but their X axes follow the revised flange at10mm from each side edge; they are not a drilling/fabrication template. Stud weld-base envelopes attach to the old low-floor underside datum without inventing body through-holes. Existing rear shackle/damper/reinforcement references are not moved to manufacture fit. Body/tank mounting registration remains unresolved; source contact with nearby structure is not a fitted interference finding. The flange now sits1mm below the estimated floor underside and separate1mm strips span that construction gap. This replaces the previous approximately20mm unsupported drawing gap; it is not measured compression or actual installed support. Sender internals, float/arm travel, pickup/strainer, drain variant, electrical calibration, actual lock cams and thread/retention are not reconstructed. The well and sender remain unregistered to the actual car; absence of selected source intersections does not prove access, compression or fitted clearance. The former sender outlet-like rod cue is removed; terminals are unassigned reference tabs, not a verified circuit. Existing solid filler route, body openings and untraced ELC/feed boundaries remain separate; open tank necks do not prove coupling, vapor routing or a complete fuel circuit. The retained vapor neck may contact the independent floor reference. No fluid compatibility, sealing, safe support/drain/disconnection order, strength, torque, fitted clearance or verified tank/sender removal path. Separation is illustrative.'
meta=dict(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=89','https://www.spridgets.de/downloads/Parts-1977.pdf#page=90'],supplier_sources=['https://www.mgocspares.co.uk/parts/mg/midget/fuel/fuel-tanks-cap-fillers-sender-units','https://www.mgocspares.co.uk/product/bra960/tank-sender-unit-mid72'],owner_dimensions_applied=False,owner_photos=['IMG_6329','IMG_6341','IMG_6919'],basis='Factory and supplier establish tank/flange, separate sender/locking ring/seal and body-welded-stud mounting roles. Existing owner side/cabin/front-underside context does not survey the hidden tank or sender; existing evidence is reconciled without requesting captures again.',estimated_roof_raise_mm=roof-.46*S,estimated_wall_mm=T,mounts=mounts,sender=dict(center_xz_mm=[sender.x,sender.z],unit_base_y_mm=sender_base,seat_top_y_mm=seat_top,bore_radius_mm=38,flange_radius_mm=50.8,lock_ring_radii_mm=[45,56]),neck_nodes=dict(filler_center_mm=list(filler.toTuple()),filler_bore_radius_mm=.042*S-2,filler_y_bounds_mm=[filler.y-(NECK_RISE+1)/2,filler.y+(NECK_RISE+1)/2],vapor_center_mm=list(vapor.toTuple()),vapor_bore_radius_mm=.012*S-2,vapor_y_bounds_mm=[vapor.y-.035*S/2,vapor.y+.035*S/2]),limits=limits)
meta['manufacturer_envelope_reference']=dict(source='https://ecat.spectrapremium.com/en/parts/RO9F',manufacturer='Spectra Premium',part='RO9F',fitted_identity_confirmed=False,listed_width_mm=W,listed_length_mm=D,listed_height_mm=H,listed_material_thickness_mm=T,listed_capacity='7 gal; gallon convention not specified on page',reseller_length_conflict_mm=15.25*25.4,datum_interpretation='Assume listed width/length include flange lip and height includes integral neck; manufacturer does not supply a dimensioned datum drawing.',front_view='https://ecat.spectrapremium.com/img/produits/RO9F_FRO_P01.jpg',estimated_pan_fraction=299/390,estimated_neck_fraction=91/390,estimated_pan_height_mm=PAN_HEIGHT,estimated_neck_rise_mm=NECK_RISE,estimated_flange_overhang_mm=FLANGE_OVERHANG,estimated_flange_gauge_mm=2.,port_xz_stations_retained=True,capacity_used_to_fit_geometry=False,geometry_scope='Manufacturer-sized generic comparison; not a reproduction of every RO9F pressing or a fitted tank.')
meta['mounting_datum_revision']=dict(estimated_floor_underside_y_mm=floor,estimated_flange_top_y_mm=roof,unloaded_strip_thickness_mm=1.,previous_flange_top_y_mm=.465*S,flange_height_change_mm=roof-.465*S,body_receivers_created=False,limits='Shared construction planes only; no measured body registration, loaded seating, packing thickness or verified clearance.')
meta['basis']+=' Overall envelope now compares manufacturer RO9F dimensions. Pan split and flange interpretation remain estimates; mounting strips share the estimated floor datum without implying fitted support.'
meta['sender_well_reference']=dict(factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=90',manufacturer_top_photo='https://ecat.spectrapremium.com/img/produits/RO9F_TOP_P04.jpg',basis='Recessed sender-seat architecture visible in unscaled factory/product images; dimensions are declared construction estimates. Sender X/Z and tank envelope/mounts are retained.',estimated_depth_mm=WELL_DEPTH,estimated_outer_radius_mm=WELL_OUTER_R,estimated_seat_radius_mm=WELL_SEAT_R,seat_top_y_body_mm=seat_top,previous_sender_base_y_body_mm=roof+.005*S,sender_vertical_translation_mm=sender_base-(roof+.005*S),catalog_normal_sheet_reference_mm=T,estimated_slope=well_slope,inner_profile_miter_mm=well_miter,owner_dimensions_applied=False,clearance_fit_performed=False)
# Compare independent source references without moving/cutting them to fit.
contacts={};tested=0
for file,needles in [('rear-body-mount-reference.step',['local_shackle_floor_doubler','three_fixing_shackle_bracket']),('rear-frame-reference.step',['formed_damper_and_upper_strap_body_panel','rubber_bumper_open_floor_beam'])]:
 assembly_ref=cq.Assembly.importStep(str(OUT/file))
 for name,entry in assembly_ref.objects.items():
  if not entry.obj or not any(word in name for word in needles):continue
  neighbour=entry.obj.val() if isinstance(entry.obj,cq.Workplane) else entry.obj
  for label,shape in [('tank_shell_flange',tank),('body_stud_envelopes',compound(studs))]:
   a=shape.BoundingBox();b=neighbour.BoundingBox();overlap=all(getattr(a,k+'min')<=getattr(b,k+'max') and getattr(b,k+'min')<=getattr(a,k+'max') for k in ['x','y','z']);volume=round(shape.intersect(neighbour).Volume(),4) if overlap else 0.;tested+=1
   if volume>1e-4:contacts[label+'__'+name.split('/')[-1]]=volume
meta['sampled_source_neighbour_contacts_mm3']=contacts;meta['sampled_source_pairs']=tested
assembly=cq.Assembly(name='estimated_rear_fuel_tank_mounting_and_sender_interfaces');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=absolute_tessellation(shape,.10,.15);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/'fuel-tank-reference.step'));meta['parts']=meshes;(OUT/'fuel-tank-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
r=cq.importers.importStep(str(OUT/'fuel-tank-reference.step')).val();assert r.isValid();print(json.dumps(dict(source_parts=len(parts),solids=len(r.Solids()),step_valid=True,six_flange_bores_open=True,sender_seat_seal_nonintersection=True,separate_stud_retainer_bores=True)))
