"""Cabin demister / footwell topology in estimated whole-car body coordinates.

No heater/body registration is inferred. Owner IMG_6856's open heater air neck
is preserved; the separate catalog CHA290 intake hose is not installed by this.
"""
from pathlib import Path
import json
import cadquery as cq
from demister_layout import (INLET_X_ABS,INLET_Z,CENTER_X_ABS,SLOT_Z,OUTER_WIDTH,OUTER_DEPTH,
    inlet_y,nozzle_base_y,curved_section,body_under_y,rail_top_y,interface_metadata)
from front_wing_geometry import absolute_tessellation
OUT=Path(__file__).parent
S=json.loads((OUT.parent/'body-data.json').read_text())['published_reference_dimensions_mm']['wheelbase']/2.4
parts=[]
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):return cq.Solid.makeCylinder(r*S,l*S,V(p),cq.Vector(*axis))
def add(name,shape,color=(.16,.17,.16)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color))
def tube(points,ro,ri,end_vertical=False):
 pts=[V(p) for p in points];edge=cq.Edge.makeSpline(pts,tangents=[cq.Vector(0,1,0),cq.Vector(0,1,0)]) if end_vertical else cq.Edge.makeSpline(pts);path=cq.Wire.assembleEdges([edge])
 if end_vertical:
  curve=edge._geomAdaptor();lo,hi=curve.FirstParameter(),curve.LastParameter()
  assert max(edge.curvatureAt(lo+(hi-lo)*i/100,mode='parameter') for i in range(101))<1/(1.5*ro*S),'Self-intersecting estimated hose bend'
 return cq.Workplane(cq.Plane(origin=pts[0],normal=cq.Vector(0,1,0) if end_vertical else pts[1]-pts[0])).circle(ro*S).circle(ri*S).sweep(path,isFrenet=True).val()
wall=1.5/S
chamber=box(.26,.16,.09,[0,.72,.02]).cut(box(.26-2*wall,.16,.09-2*wall,[0,.72+wall,.02]))
for side in [-1,1]:
 chamber=chamber.cut(box(.02,.06,.06,[side*.13,.68,.02])).cut(cyl(.018,.02,[side*.12,.76,.02],[side,0,0]))
add('cabin_air_distribution_chamber_reference',chamber)
nodes={};routes=[]
for side,label in [(1,'left'),(-1,'right')]:
 a=[side*.13,.76,.02];b=[side*.18,.80,.02];c=[side*INLET_X_ABS,inlet_y(S),INLET_Z]
 nodes[label+'_elbow_wall']=a;nodes[label+'_hose_start']=b;nodes[label+'_nozzle_inlet']=c
 elbow=[a,[side*.16,.76,.02],[side*.18,.78,.02],b]
 add('demister_'+label+'_elbow_reference',tube(elbow,.018,.015))
 q=[c[0],c[1]-.026,c[2]]
 points=[b,[side*.31,.87,.015],q]
 hose=tube(points,.018,.015,end_vertical=True)
 def circle(radius,y):return cq.Workplane('XZ',origin=V([c[0],y,c[2]]).toTuple()).circle(radius*S).val()
 start=V(q);axis=cq.Vector(0,1,0);flare=.012*S;straight=.008*S
 outer=cq.Solid.makeCone(.018*S,.0225*S,flare,start,axis).fuse(cq.Solid.makeCylinder(.0225*S,straight,start+axis*flare,axis))
 inner=cq.Solid.makeCone(.015*S,.0195*S,flare,start,axis).fuse(cq.Solid.makeCylinder(.0195*S,straight+1,start+axis*flare,axis))
 hose=hose.fuse(outer.cut(inner),tol=.001).clean()
 print(label+' hose constructed',flush=True)
 add('demister_'+label+'_hose_reference',hose,(.12,.13,.12))
 routes.append(dict(name=label,points_units=points,start=label+'_hose_start',end=label+'_nozzle_inlet',upper_cuff_top_units=[c[0],c[1]-.006,c[2]],limits='Upper route, flare/cuff and sleeve overlap are construction estimates, not fitted routing, hose section or clamp/compression.'))
 # NA end-inlet interpretation: an offset circular throat and sloping hollow
 # bellmouth feed a rectangular chimney following the body underside curve.
 if side<0:
  nozzle=left_nozzle.mirror('YZ');fixings=[shape.mirror('YZ') for shape in left_fixings]
 else:
  cx=side*CENTER_X_ABS;base=nozzle_base_y(S)
  def funnel(lower_width,lower_depth,fw,fd,lower,upper):
   bottom=cq.Workplane('XZ',origin=V([c[0],lower,c[2]]).toTuple()).rect(lower_width*S,lower_depth*S).val()
   top=cq.Workplane('XZ',origin=V([cx,upper,SLOT_Z]).toTuple()).rect(fw*S,fd*S).val()
   return cq.Solid.makeLoft([bottom,top],ruled=True)
  # Planar offset walls and an integral round-bored lower shoulder avoid the
  # unreliable circle-to-rectangle loft. The pressing itself is unmeasured.
  nozzle=funnel(.040,.040,.22,.028,c[1],base).cut(funnel(.034,.034,.216,.024,c[1]+1.5/S,base+1/S))
  nozzle=nozzle.cut(cyl(.016,3.5/S,[c[0],c[1]-1/S,c[2]],[0,1,0]))
  section=curved_section(S,SLOT_Z-OUTER_DEPTH/2,lambda x:body_under_y(x,S),lambda x:base,cx-OUTER_WIDTH/2,cx+OUTER_WIDTH/2)
  chimney=cq.Solid.extrudeLinear(section,[],cq.Vector(0,0,OUTER_DEPTH*S))
  void=box(.216,.10,.024,[cx,base+.04,SLOT_Z]);chimney=chimney.cut(void)
  sleeve=cq.Solid.makeCylinder(.019*S,.019*S,V([c[0],c[1]-.018,c[2]]),cq.Vector(0,1,0)).cut(cq.Solid.makeCylinder(.016*S,.022*S,V([c[0],c[1]-.019,c[2]]),cq.Vector(0,1,0)))
  nozzle=nozzle.fuse(chimney,sleeve,tol=.001)
  fixings=[]
  for dx in [-.122,.122]:
   x=cx+dx
   section=curved_section(S,SLOT_Z-.016,lambda t:body_under_y(t,S),lambda t:body_under_y(t,S)-.004,x-.013,x+.013)
   tab=cq.Solid.extrudeLinear(section,[],cq.Vector(0,0,.032*S)).cut(cyl(.003,.08,[x,.96,SLOT_Z],[0,1,0]))
   nozzle=nozzle.fuse(tab,tol=.001)
   head_y=rail_top_y(x)*S
   shank=cq.Solid.makeCylinder(2.2,18,cq.Vector(x*S,head_y-18,SLOT_Z*S),cq.Vector(0,1,0))
   head=cq.Solid.makeCylinder(4.3,1.5,cq.Vector(x*S,head_y,SLOT_Z*S),cq.Vector(0,1,0));fixings.append(shank.fuse(head))
  left_nozzle=nozzle;left_fixings=fixings
 # Keep valid trimmed faces; unifying the curved-tab joins corrupts topology.
 assert nozzle.isValid() and len(nozzle.Solids())==1
 print(label+' nozzle constructed',flush=True)
 add('demister_'+label+'_north_america_nozzle_reference',nozzle)
 add('demister_'+label+'_two_unthreaded_nozzle_fixing_roles',cq.Compound.makeCompound(fixings),(.40,.42,.37))
 door=box(.004,.07,.07,[side*.134,.68,.02]);hinge=cyl(.004,.07,[side*.139,.715,-.015],[0,0,1])
 add('footwell_'+label+'_outlet_door_reference',cq.Compound.makeCompound([door,hinge]),(.23,.24,.21))
assembly=cq.Assembly(name='unregistered_cabin_air_distribution_reference');meshes=[]
for name,shape,color in parts:assembly.add(shape,name=name,color=cq.Color(*color))
assembly.export(str(OUT/'cabin-air-reference.step'))
for name,shape,color in parts:
 print('mesh '+name,flush=True)
 vs,ts=absolute_tessellation(shape,.05,.12) if '_hose_' in name or '_nozzle_' in name else shape.tessellate(.7,.2)
 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]))
limits='Every body station, shape, bore, hose section/route, flange, mounting pattern and door pose is estimated. Native metric scale does not establish surveyed fitted geometry. Chamber is a body-distribution hypothesis, not an identified detachable part. Heater outlet-to-body registration and the supply crossing remain unresolved; no joining duct, heater cutout or shelf hole is invented. North American nozzles are factory comparisons, not fitted identities. No sealing, airflow, blower performance, door motion, dash/scuttle attachment, moving clearance or verified replacement/removal procedure. Inspection separation is illustrative.'
meta=dict(demister_interface_reference=interface_metadata(S),units='mm',source_frame='Canonical body +X vehicle left, +Y up, +Z front; estimated body units multiplied by published nominal wheelbase / 2.4.',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=280','https://www.spridgets.de/downloads/Parts-1977.pdf#page=281'],owner_photos=['IMG_6856','IMG_6892','IMG_6341'],owner_observation='IMG_6856 shows the heater air neck open at capture time; it does not establish the full intake hose fitment or remote route. IMG_6341 gives cabin context but does not survey hidden demister/footwell distribution.',basis='Factory AHH8394 pair of demister hoses, 14G3499 elbows, North American AHA8565 RH / AHA8566 LH nozzles and 13H59 outlet-door architecture support this unregistered cabin comparison. All dimensions and the central distribution chamber are estimates; the heater/body supply join is not constructed.',limits=limits,nodes_units=nodes,routes=routes,unregistered_chamber_supply_boundary_units=[0,.80,.02],parts=meshes)
(OUT/'cabin-air-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'cabin-air-reference.step')).val();assert restored.isValid()
# Open mouths and material around them: a missing part must not pass a bore check.
for side in [-1,1]:
 assert chamber.intersect(cyl(.001,.03,[side*.115,.76,.02],[side,0,0])).Volume()<1e-6
 assert chamber.intersect(box(.01,.01,.01,[side*.13,.72,.02])).Volume()>0
 label='left' if side==1 else 'right';nozzle=next(shape for name,shape,color in parts if name=='demister_'+label+'_north_america_nozzle_reference')
 assert nozzle.intersect(cyl(.001,.015,[side*INLET_X_ABS,inlet_y(S)-.017,INLET_Z],[0,1,0])).Volume()<1e-6
print(json.dumps(dict(groups=len(parts),solids=len(restored.Solids()),step_valid=True,open_elbow_and_nozzle_axes=True,heater_intake_unchanged=True)))
