"""Manufacturer panel topology in an estimated body frame; no fitted repair geometry."""
from pathlib import Path
import json,math
import cadquery as cq
from front_upper_profile import drop_units,upper_body_y,metadata as upper_profile_metadata
from front_clip_layout import rigid_z,source_z,metadata as clip_metadata
from front_wing_geometry import absolute_tessellation
from observed_pump_mount_layout import BODY_AXIS_MM, BORE_RADIUS_MM, metadata as pump_mount_metadata
from OCP.BRepTools import BRepTools
from OCP.BRepMesh import BRepMesh_IncrementalMesh
OUT=Path(__file__).parent;BODY=json.loads((OUT.parent/'body-data.json').read_text());S=BODY['published_reference_dimensions_mm']['wheelbase']/2.4
parts=[];nodes={};T=1.5
PROFILE=[(.35,.58),(.75,.88),(1.47,.88),(1.55,.88+(.50-.88)*(.08/.36)),(rigid_z(1.83),.50)] # Z,Y drawing units
PREVIOUS_PROFILE=PROFILE
def previous_roof_y(z):
 for (a,y),(b,q)in zip(PREVIOUS_PROFILE,PREVIOUS_PROFILE[1:]):
  if a<=z<=b:return y+(q-y)*(z-a)/(b-a)
 raise ValueError(z)
profile_stations=sorted({round(z,10) for z,_ in PREVIOUS_PROFILE}|{.80,1.20}|{round(.35+(rigid_z(1.83)-.35)*i/40,10)for i in range(41)})
PROFILE=[(z,upper_body_y(previous_roof_y(z),source_z(z)))for z in profile_stations]

SOURCE='https://www.bmh-ltd.com/vehicle-parts/mg-midget-parts/mg-midget-front-centre-body/'
# Existing whole-car upper-wing inner edge; estimated, not a fitted survey.
def upper_wing_y(z):
 z=source_z(z)
 return 1.045-.03*(z-1)**2-.15*max(0,z-1.08)**2-drop_units(z)
def V(x,y,z):return cq.Vector(x*S,y*S,z*S)
def prism(points,delta):
 w=cq.Wire.makePolygon(points+[points[0]])
 return cq.Solid.extrudeLinear(w,[],delta)
def roof_y(z):
 for (a,y),(b,q) in zip(PROFILE,PROFILE[1:]):
  if a<=z<=b:return y+(q-y)*(z-a)/(b-a)
 raise ValueError(z)
def panel(side,x,outline,thick=T):return prism([V(side*x,y,z) for z,y in outline],cq.Vector(side*thick,0,0))
def add(name,shape,color=(.23,.30,.24)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color))
for side,label in [(1,'left'),(-1,'right')]:
 # A declared lower access mouth replaces the old inboard semicircular opening.
 # It is NOT a measured factory aperture or suspension travel envelope.
 inner=[(.35,.35),(.35,.58),*PROFILE[1:],(rigid_z(1.83),.35),(1.55,.35),(1.55,.74),(.90,.74),(.90,.35)]
 valance=panel(side,.48,inner)
 add('front_'+label+'_inner_valance_and_lower_access_reference',valance)
 # One continuous sheet includes the inward return. Splitting each longitudinal
 # station into touching solids made the receiver cut invalid at shared faces.
 roof_outline=[V(side*.46,y,z) for z,y in PROFILE]
 roof_outline += [V(side*.46,y-T/S,z) for z,y in reversed(PROFILE)]
 roof=prism(roof_outline,cq.Vector(side*.23*S,0,0))
 if side>0:
  # One owner-photo fastener role, at a shared estimated reference axis.
  # This is not a factory or fitted drilling map.
  q=cq.Vector(*BODY_AXIS_MM)
  roof=roof.cut(cq.Solid.makeCylinder(BORE_RADIUS_MM,10,q-cq.Vector(0,5,0),cq.Vector(0,1,0)))
 add('front_'+label+'_folded_top_and_sloping_end_panel_reference',roof)
 # Retained wheel-opening center/radius, now on the outboard closure panel.
 outer=[(.35,.35),(.35,.58),*PROFILE[1:],(rigid_z(1.83),.35),(1.58,.35),(1.58,.39)]
 outer += [(1.20+.38*math.cos(i*math.pi/64),.39+.38*math.sin(i*math.pi/64)) for i in range(1,65)]
 outer.append((.82,.35))
 closure=panel(side,.69,outer)
 add('front_'+label+'_outer_wheelarch_closure_reference',closure)
 # A continuous return indicates the separate outer-wing attachment boundary.
 # Holes/captive nuts are deliberately absent: no fixing map has been surveyed.
 flanges=[]
 for (z0,y0),(z1,y1) in zip(PROFILE,PROFILE[1:]):
  flanges.append(prism([V(side*x,y,z) for x,y,z in [(.69,y0,z0),(.72,y0,z0),(.72,y1,z1),(.69,y1,z1)]],cq.Vector(0,-T,0)))
 # IMG_6853/6856/6857 show a continuous bay-side sheet-metal boundary.
 # Close the previously empty strip to the existing estimated upper-wing edge.
 # This face is a reconstruction hypothesis, not an identified separate panel
 # or a verified ownership/lap assignment. The upper edge follows the shorter bonnet/wing source map. The forward roof retains its access-end station and steepens toward the shifted front end; wheel and access cuts stay.
 stations=sorted({round(z,9) for z,_ in PROFILE}|{round(.35+i*(rigid_z(1.83)-.35)/24,9) for i in range(25)})
 closing_sections=[]
 for z in stations:
  low=roof_y(z);high=upper_wing_y(z)
  pts=[V(side*x,y,z) for x,y in [(.72,low),(.67,high),(.67+T/S,high),(.72+T/S,low)]]
  closing_sections.append(cq.Wire.makePolygon(pts+[pts[0]]))
 closing=cq.Solid.makeLoft(closing_sections,ruled=True)
 assert closing.isValid() and closing.Volume()>0
 add('front_'+label+'_outer_wing_attachment_return_reference',cq.Compound.makeCompound(flanges+[closing]))
 # BMH8024/25 triangular support role, with an unmeasured chosen station.
 # Lower edge meets the retained rail's outboard/top reference corner.
 web=prism([V(side*x,y,.72) for x,y in [(.3975,.385),(.48,.385),(.48,upper_body_y(.82,.72))]],cq.Vector(0,0,T))
 add('front_'+label+'_rail_to_inner_valance_support_web_reference',web)
 # Test defined construction rather than a global fitted-clearance claim.
 opening=cq.Solid.makeCylinder(.37*S,.30*S,V(side*.46,.39,1.20),cq.Vector(side,0,0))
 # Cylinder's lower half is below the panel; radius is 8.47 mm inside reference edge.
 assert closure.intersect(opening).Volume()<1e-5
 access=cq.Solid.makeBox(40,.36*S,.61*S,V(side*.48 if side>0 else side*.48-40/S,.36,.92))
 assert valance.intersect(access).Volume()<1e-5
 crown=(roof_y(1.20)-(.39+.38))*S
 assert crown>0
 assert abs(roof_y(.80)*S-BODY_AXIS_MM[1])<1e-8
 nodes[label]=dict(inner_x_units=side*.48,outer_x_units=side*.69,wing_return_outer_x_units=side*.72,inner_return_x_units=side*.46,roof_profile_zy_units=PROFILE,reference_sheet_gauge_mm=T,wheel_opening_center_yz_units=[.39,1.20],wheel_opening_radius_units=.38,lower_access_zy_bounds_units=[.90,1.55,.35,.74],estimated_support_z_units=.72,support_rail_corner_units=[side*.3975,.385,.72],support_valance_top_units=[side*.48,upper_body_y(.82,.72),.72],upper_bay_closure=dict(estimated_upper_inner_x_units=side*.67,estimated_lower_outer_x_units=side*.72,estimated_section_stations_zy_units=[[z,roof_y(z),upper_wing_y(z)] for z in stations],projected_x_sheet_gauge_mm=T,ownership='unassigned_reference_closing_face_not_identified_factory_panel_or_verified_lap',boundary_basis='Wheel/access cuts stay. Upper inner panels now interpolate the profile correction above the retained opening-roof datum; the source pump receiver follows that top. Upper edge follows the inverse-mapped wing profile with the conditional longitudinal upper-profile correction. Both are estimated construction curves, not fitted registration.'),reference_crown_to_roof_difference_mm=crown,crown_difference_limit='Difference of two drawing stations only; not minimum, loaded, fitted tyre or suspension clearance.')
assembly=cq.Assembly(name='estimated_rubber_bumper_front_wheelhouse_topology');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color))
 vs,ts=absolute_tessellation(shape,.03,.10)
 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/'front-wheelhouse-reference.step'))
limits='No direct owner dimensions are applied. Manufacturer rubber-bumper panel applications establish comparison architecture, not fitted identity or repair suitability. Every profile, station, gauge, slope, fold, joint, support web, lower access mouth and wing return is estimated. Owner IMG_6853/6856/6857 shows a continuous upper bay-side sheet-metal boundary. Closing faces join the estimated wing inner edge atX=.67 to the wheelhouse return atX=.72; the current forward slope/end follow the shorter-front construction while wheel/access cuts and pump/support X/Z stations stay and upper mounting heights follow the shared correction; this is a reconstruction hypothesis, not an identified separate panel, surveyed surface, actual lap or welded/bolt-on ownership assignment. Projected1.5 mm X gauge, ruling/stations, upper/lower contours and mirrored opposite side remain estimates. No fitted dimensions or clearance are inferred from closure. Owner photos establish bay-side sheet-metal/rail context, without calibrated hidden seams or opposite-side equivalence. The retained .38-unit wheel opening is an old drawing datum, not actual tyre/wheelhouse fit. The new .74-unit access-mouth height is chosen construction, not an identified factory opening, tool allowance or verified suspension travel envelope. One shared estimated owner-pump receiver bore is applied only to the left roof as a mounting-role comparison; it is not a measured fitted hole or factory drilling/repair template. No wing fixing holes/captive nuts, weld count/pitch, actual lap order, seals, strength, alignment, loaded clearances or verified removal/cutting/welding procedure. Body panels stay fixed; selectable source sections do not make welded panels detachable.'
meta=dict(front_clip_reconstruction=clip_metadata(S),units='mm',source_frame='Canonical body +X vehicle-left, +Y up, +Z front, mm.',manufacturer_source=SOURCE,manufacturer_catalog_source='https://www.bmh-ltd.com/wp-content/uploads/2026/04/BMH-Product-Catalogue-March26-Spreads-116pp-April26.pdf',owner_photos=['IMG_6855','IMG_6853','IMG_6856','IMG_6857','IMG_6914','IMG_6891'],basis='BMH separates rubber-bumper BMH8022/23 inner panels, BMH8024/25 supporting webs and CZJ682/683 complete wheelhouses from earlier chrome-bumper alternatives. Shared AHA5715/16 outer panels and CZJ194/189 top panels are comparisons. Their drawing shows a broad top with sloping end regions, not the previous semicircular membrane. Owner bay photos give context, not dimensions. Owner IMG_6853/6856/6857 now supports closing the formerly empty upper bay-side strip between existing unregistered wheelhouse/wing datums. The face/ownership is a reconstruction hypothesis; no fitted panel is identified.',owner_dimensions_applied=False,observed_pump_mount_reference=pump_mount_metadata(),unselected_earlier_comparisons=['CZJ192/193 inner panels','AHA5436/AHA5403 support webs','CZJ432/433 complete wheelhouses'],nodes=nodes,limits=limits,parts=meshes)
meta['front_upper_profile_reference']=upper_profile_metadata(S)
meta['basis']+=' The shared longitudinal front-upper correction lowers the middle contour, with zero correction at sourceZ=.13/1.99. Adjacent sheet and carried mounting references follow; mechanical frames remain unchanged.'
meta['limits']+=' The0.10-unit peak is an owner-photo-led construction estimate, not a measured height. Rear/nose interfaces, axles, marker, engine/body pose, hinge pivots and wiper/screen datums remain drawing references. Better photographic agreement is not fitted panel registration, support/locking strength, full-car collision clearance or a verified removal path.'
(OUT/'front-wheelhouse-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'front-wheelhouse-reference.step')).val();assert restored.isValid() and all(v.isValid() and v.Volume(1e-9)>0 for v in restored.Solids())
assert math.isclose(restored.Volume(1e-9),sum(v.Volume(1e-9) for _,v,_ in parts),rel_tol=1e-7)
print(json.dumps(dict(groups=len(parts),solids=len(restored.Solids()),step_valid=True,step_volume_roundtrip=True,upper_bay_closing_faces=True,retained_wheel_opening_passage=True,declared_lower_access_open=True)))
