"""Estimated cross-car main pipe and hollow twin-chamber 1500 comparison.
Factory topology is distinct from fitted geometry. Body-frame source: +X left,
+Y up, +Z front, mm. Neither engine nor body placement is changed.
"""
from pathlib import Path
import json,math
import cadquery as cq
from exhaust_hanger_geometry import build as build_hanger,metadata as hanger_metadata
OUT=Path(__file__).parent
BODY=json.loads((OUT.parent/'body-data.json').read_text())
S=BODY['published_reference_dimensions_mm']['wheelbase']/2.4
front=json.loads((OUT/'front-exhaust-reference.json').read_text())
p=BODY['native_front_exhaust_placement']
start=[(v-a)/S+b for v,a,b in zip(front['downstream_coupling_rear_face_mm'],p['native_anchor_mm'],p['body_anchor_units'])]
parts=[];paths={}
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=(.48,.47,.43)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color))
def bent_pipe(name,stations,r=.065):
 """Tangent line/arc path, no interpolating spline or hidden overshoot."""
 pts=list(map(V,stations));edges=[];prev=pts[0]
 for a,b,c in zip(pts,pts[1:],pts[2:]):
  u=(b-a).normalized();v=(c-b).normalized();dot=max(-1,min(1,u.dot(v)));theta=math.acos(dot)
  assert .001<theta<math.pi-.001
  R=r*S;d=R*math.tan(theta/2)
  assert d<(b-a).Length*.49 and d<(c-b).Length*.49,(name,d)
  q=b-u*d;t=b+v*d;n=(v-u*dot).normalized();center=q+n*R
  mid=center+((q-center).normalized()+(t-center).normalized()).normalized()*R
  if (q-prev).Length>1e-5:edges.append(cq.Edge.makeLine(prev,q))
  edges.append(cq.Edge.makeThreePointArc(q,mid,t));prev=t
 edges.append(cq.Edge.makeLine(prev,pts[-1]));wire=cq.Wire.assembleEdges(edges)
 normal=(pts[1]-pts[0]).normalized();plane=cq.Plane(origin=pts[0],normal=normal)
 result=cq.Workplane(plane).circle(.023*S).circle(.020*S).sweep(wire,isFrenet=True).val()
 paths[name]={'corner_stations_units':stations,'bend_radius_mm':r*S,'outer_radius_mm':.023*S,'inner_radius_mm':.020*S}
 add(name,result);return result
main_stations=[start,[start[0],.12,.04],[.52,.12,-.45],[.52,.12,-.70],[.52,.14,-.90],[.52,.14,-1.12]]
main=bent_pipe('underfloor_cross_car_main_pipe_reference',main_stations,r=.060)
# Hollow recognition shells: open port bores, empty cavities, no asserted internals.
wall=.003
res_base=[.52,.14,-1.52]
res=cq.Workplane('XY',origin=V(res_base).toTuple()).ellipse(.0561*S,.066*S).extrude(.40*S).val()
inner=cq.Workplane('XY',origin=V([.52,.14,-1.52+wall]).toTuple()).ellipse((.0561-wall)*S,(.066-wall)*S).extrude((.40-2*wall)*S).val()
res=res.cut(inner).cut(cyl(.020,.402,[.52,.14,-1.521]))
add('underfloor_longitudinal_resonator_shell_comparison',res)
connector_stations=[[.52,.14,-1.52],[.52,.14,-1.89],[.44,.14,-1.89]]
connector=bent_pipe('underfloor_resonator_to_cross_box_pipe_reference',connector_stations,r=.035)
# YZ workplane normal +X, local x is +Y: ellipse follows old chamber envelope.
cross_plane=cq.Plane(origin=V([-.44,.14,-1.89]),xDir=(0,1,0),normal=(1,0,0))
cross=cq.Workplane(cross_plane).ellipse(.075*S,.08625*S).extrude(.88*S).val()
inner_plane=cq.Plane(origin=V([-.44+wall,.14,-1.89]),xDir=(0,1,0),normal=(1,0,0))
void=cq.Workplane(inner_plane).ellipse((.075-wall)*S,(.08625-wall)*S).extrude((.88-2*wall)*S).val()
cross=cross.cut(void).cut(cyl(.020,.882,[-.441,.14,-1.89],(1,0,0)))
add('underfloor_transverse_silencer_shell_comparison',cross)
tail_stations=[[-.44,.14,-1.89],[-.52,.14,-1.89],[-.52,.14,-2.17]]
tail=bent_pipe('underfloor_right_tail_pipe_reference',tail_stations,r=.035)
# Simplified mounting architecture; rear RH early/late branch remains unselected.
mounts=[('intermediate',.52,-.55,.12,.35),('left_rear',.52,-1.56,.14,.49),('right_rear',-.52,-2.02,.14,.49)]
anchors=[];hanger_nodes={}
for label,x,z,y,top in mounts:
 suspension,rubbers,body,hanger_nodes[label]=build_hanger(label,x,z,y,top)
 add('underfloor_'+label+'_clip_isolator_comparison',suspension,(.42,.44,.39))
 add('underfloor_'+label+'_paired_unloaded_insulating_washers_reference',rubbers,(.12,.14,.12))
 anchors.append(body)
add('underfloor_body_support_brackets_reference',cq.Compound.makeCompound(anchors),(.23,.28,.22))
# Explicit drawing interfaces. Coincident stations are a construction decision.
interfaces=[('front coupling rear face',start),('main pipe / resonator',[.52,.14,-1.12]),('resonator / connector',[.52,.14,-1.52]),('connector / cross box',[.44,.14,-1.89]),('cross box / tail',[-.44,.14,-1.89])]
# Probe each straight end / chamber port; do not claim fitted flow or baffles.
probes=[(main,start,(0,0,-1),.025),(res,[.52,.14,-1.121],(0,0,-1),.025),(res,[.52,.14,-1.519],(0,0,1),.025),(connector,[.52,.14,-1.521],(0,0,-1),.025),(connector,[.441,.14,-1.89],(-1,0,0),.025),(cross,[.441,.14,-1.89],(-1,0,0),.025),(cross,[-.441,.14,-1.89],(1,0,0),.025),(tail,[-.441,.14,-1.89],(-1,0,0),.025),(tail,[-.52,.14,-2.171],(0,0,1),.025)]
for shape,point,axis,length in probes:assert shape.intersect(cyl(.015,length,point,axis)).Volume()<1e-5
assembly=cq.Assembly(name='estimated_1500_cross_car_underfloor_exhaust');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=shape.tessellate(.65,.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/'underfloor-exhaust-reference.step'))
limits='All bends, radii, walls, chambers, ports, sections, fixing patterns, mount stations and body registration are estimates. The cross-car topology is a factory/catalog comparison, not observation of this car’s hidden route. Front joint station deliberately follows the revised estimated coupling at body Y=.12; front bend/coupling lowers 93.13 mm from its prior drawing to sit below the estimated gearbox. This closes an independent-scaffold gap, not a measured fit. Rear connection is interpreted from the factory plate: left longitudinal chamber feeds the left cross-box end, with right tail. No baffles, perforated tubes, catalyst selection, pressure/thermal behavior, body clearance, loaded support, actual hardware identity or verified removal path. Generic RH support does not select early or later GAN6 mounting. No engine/body reference is moved.'
meta=dict(units='mm',source_frame='+X vehicle-left, +Y up, +Z front body reference, mm',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=127','https://www.spridgets.de/downloads/Parts-1977.pdf#page=128'],replacement_comparison_source='https://www.mgocspares.co.uk/DocumentHandler/72775/b039-midget-1500-stainless-st-pdf',owner_evidence='IMG_7042 shows local manifold/descending neck only. IMG_6329/6341 do not survey hidden underfloor geometry. No new captures requested; no actual cross-car route inferred from those images.',basis='Factory 1500 two-chamber exhaust topology and LH/RH support labels; MGOC Double S replacement-system note explicitly accounts for opposite-side 1500 manifold. Existing ordinary front downpipe retained as an unselected configuration comparison.',limits=limits,estimated_interfaces_body_units=dict(interfaces),estimated_paths=paths,estimated_mounts_units=mounts,prior_scaffold_lateral_difference_mm=514.0666666666667,parts=meshes)
meta['hanger_reconstruction']=hanger_metadata(hanger_nodes)
meta['limits']+=' '+meta['hanger_reconstruction']['limits']
(OUT/'underfloor-exhaust-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'underfloor-exhaust-reference.step')).val();assert restored.isValid()
print(json.dumps(dict(groups=len(parts),solids=len(restored.Solids()),step_valid=True,nine_port_probes_open=True,coupling_station_body_units=start)))
