"""Heater service architecture; owner-visible local hoses, estimated hidden internals.

Retains the main casing envelope and engine export; whole-car outlet/base is a separate estimated reconstruction.
Factory Parts-1977 p280 distinguishes 1500 matrix/motor and water-control variants.
"""
from pathlib import Path
import json,math
import cadquery as cq
from build_engine import make_heater_box,P
OUT=Path(__file__).parent
x=-35.;bottom=75.5;rear=-342.5
width,height,depth=[P[k] for k in ['heater_box_width','heater_box_height','heater_box_depth']]
left,right,top,front=x-width/2,x+width/2,bottom+height,rear+depth
ports=[[left+27.5,top-40,front+25],[left+72.5,top-40,front+25]]
parts=[]
def cyl(r,l,p,d=(0,0,1)):return cq.Solid.makeCylinder(r,l,cq.Vector(*p),cq.Vector(*d))
def box(w,h,d,p):return cq.Workplane('XY').box(w,h,d).translate(p).val()
def ring(ro,ri,l,p,d=(0,0,1)):
 return cyl(ro,l,p,d).cut(cyl(ri,l+2,(p[0]-d[0],p[1]-d[1],p[2]-d[2]),d))
def add(name,shape,color=(.18,.19,.17)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color))
def tube(points,ro,ri):
 pts=[cq.Vector(*p) for p in points];path=cq.Wire.assembleEdges([cq.Edge.makeSpline(pts)])
 plane=cq.Plane(origin=pts[0],normal=pts[1]-pts[0]);return cq.Workplane(plane).circle(ro).circle(ri).sweep(path,isFrenet=True).val()
case=make_heater_box().Solids()
assert len(case)==6
prior_casing_volume=sum(q.Volume() for q in case)
# Factory p280 shows a formed lower mounting base and6 platform screws.
# Source matrix envelope supplies an independent outlet estimate; no platform
# slot is re-identified or body opening moved to force an air connection.
BASE_DROP=20.;BASE_G=2.;SEAL_G=6.
base_cx=(left+right-70)/2;base_cz=(rear+front)/2
base_w=right-70-left+20;base_d=depth+20
out_cx=(left+12+right-140)/2;out_cz=(rear+39+front-51)/2
out_w,out_d=160.,50.;base_top=bottom-BASE_DROP;base_bottom=base_top-BASE_G
outlet_tool=box(out_w,30,out_d,[out_cx,bottom-10,out_cz])
pan=box(out_w+4,BASE_DROP+1,out_d+4,[out_cx,bottom-(BASE_DROP-1)/2,out_cz]).cut(outlet_tool)
flange=box(base_w,BASE_G,base_d,[base_cx,base_top-BASE_G/2,base_cz]).cut(box(out_w,BASE_G+4,out_d,[out_cx,base_top-1,out_cz]))
base_axes=[(base_cx+dx,base_top,base_cz+dz)for dx in [-base_w/2+15,0,base_w/2-15]for dz in [-base_d/2+15,base_d/2-15]]
for bx,by,bz in base_axes:flange=flange.cut(cyl(2.5,BASE_G+6,[bx,base_bottom-3,bz],(0,1,0)))
case[0]=case[0].cut(outlet_tool).fuse(pan,flange).clean()
assert case[0].isValid()and len(case[0].Solids())==1
add('heater_case_and_necks_reference',cq.Compound.makeCompound([case[0],*case[2:]]),(.09,.10,.10))
add('heater_rear_cover_reference',case[1],(.11,.12,.11))
# Open fin/tank envelopes, not measured fins or reconstructed water channels.
core_left,core_right=left+12,right-140
core_width=core_right-core_left;cx=(core_left+core_right)/2
core_bottom,core_top=bottom+15,top-18;cy=(core_bottom+core_top)/2
core_rear,core_front=rear+39,front-51;cz=(core_rear+core_front)/2
headers=[box(12,core_top-core_bottom,core_front-core_rear,[px,cy,cz]) for px in [core_left+6,core_right-6]]
fins=[box(core_width-24,core_top-core_bottom-8,.65,[cx,cy,core_rear+3+i*(core_front-core_rear-6)/17]) for i in range(18)]
a,b=ports
internal=[tube([[a[0],a[1],front],[a[0],a[1],front-25],[core_left+6,core_top-15,core_front]],8.5,6.5),ring(8.5,6.5,14,(b[0],b[1],front-14))]
add('heater_matrix_and_pipe_envelopes_reference',cq.Compound.makeCompound(headers+fins+internal),(.57,.43,.26))
seal=box(core_width+6,core_top-core_bottom+6,core_front-core_rear+6,[cx,cy,cz]).cut(box(core_width-2,core_top-core_bottom-2,core_front-core_rear+8,[cx,cy,cz]))
add('heater_matrix_perimeter_seal_reference',seal,(.16,.17,.15))
ax,ay=right-70,top-60
# Internal coaxial recognition envelopes only: no inferred fitted motor orientation.
motor=cyl(21,35,(ax,ay,rear+18));shaft=cyl(4,23,(ax,ay,rear+53));plate=ring(33,4.5,3,(ax,ay,rear+15))
add('heater_blower_motor_and_plate_reference',cq.Compound.makeCompound([motor,shaft,plate]),(.43,.44,.41))
fan_back=rear+76;fan_depth=29
fan=[ring(46,10,2,(ax,ay,fan_back)),ring(46,43,2,(ax,ay,fan_back+fan_depth-2)),cyl(9,fan_depth,(ax,ay,fan_back))]
for i in range(16):
 theta=i*360/16
 fan.append(box(2,15,fan_depth-4,[ax+35,ay,fan_back+fan_depth/2]).rotate((ax,ay,0),(ax,ay,1),theta))
add('heater_blower_fan_reference',cq.Compound.makeCompound(fan),(.34,.35,.32))
valve_out=[left-35,top-90,front+74.5]
valve_center=[left-60,top-90,front+74.5]
valve_bottom=[valve_center[0],top-110,valve_center[2]]
# Estimated hose centerlines retain the visibly distinct lateral bend and down-turn.
hose_a=[a,[a[0],a[1],front+48],[left-5,top-68,front+81],[left-25,top-90,front+85],valve_out]
hose_b=[b,[b[0],b[1],front+48],[b[0]+2,top-65,front+66],[b[0]-12,top-110,front+70],[left+10,top-135,front+54]]
for key,pts in [('a',hose_a),('b',hose_b)]:
 pieces=[tube(pts,16,12)]
 p=pts[0];pieces.append(ring(17,16,9,[p[0],p[1],p[2]-5]))
 add('heater_visible_hose_'+key+'_reference',cq.Compound.makeCompound(pieces),(.18,.19,.17))
vc=valve_center;vb=valve_bottom
body=cyl(15,34,(vc[0],vb[1]+3,vc[2]),(0,1,0));cap=cyl(18,5,(vc[0],vb[1]+37,vc[2]),(0,1,0))
out=ring(12,8,25,(vc[0],vc[1],vc[2]),(1,0,0))
flange=box(44,4,34,[vc[0],vb[1]+2,vc[2]]).cut(cyl(7,6,(vc[0],vb[1]-1,vc[2]),(0,1,0)))
for dx in [-15,15]:flange=flange.cut(cyl(3,6,(vc[0]+dx,vb[1]-1,vc[2]),(0,1,0)))
body=body.cut(cyl(7,35,(vc[0],vb[1],vc[2]),(0,1,0)))
lever=box(30,3,7,[vc[0]+7,vb[1]+44,vc[2]])
add('heater_water_control_tap_reference',cq.Compound.makeCompound([body,cap,out,flange,lever]),(.32,.43,.37))
# Separate elbow, interface and bracket; no thread or seating/actuation claim.
elbow_points=[[vc[0],vb[1]-2,vc[2]],[vc[0],vb[1]-15,vc[2]],[vc[0],vb[1]-20,vc[2]+25]]
elbow=tube(elbow_points,11,7)
elbow_flange=box(44,3,34,[vc[0],vb[1]-3.5,vc[2]]).cut(cyl(7,5,(vc[0],vb[1]-6,vc[2]),(0,1,0)))
add('heater_control_elbow_reference',cq.Compound.makeCompound([elbow,elbow_flange]),(.43,.41,.34))
bracket=box(62,2,46,[vc[0],vb[1]-1,vc[2]]).cut(cyl(8,4,(vc[0],vb[1]-3,vc[2]),(0,1,0)))
for dx in [-15,15]:bracket=bracket.cut(cyl(3,4,(vc[0]+dx,vb[1]-3,vc[2]),(0,1,0)))
foot=box(3,38,46,[vc[0]-30,vb[1]-20,vc[2]])
add('heater_water_control_bracket_reference',cq.Compound.makeCompound([bracket,foot]),(.38,.38,.34))
remote=[left-5,bottom+5,front+174.5]
hose_c=[elbow_points[-1],[vc[0],vb[1]-22,vc[2]+45],[left-38,bottom+8,front+132],remote]
add('heater_control_remote_hose_boundary_reference',tube(hose_c,14,10),(.18,.19,.17))
# Separate unloaded base-seal and six smooth fixing/washer role comparisons.
base_seal=box(base_w,SEAL_G,base_d,[base_cx,base_bottom-SEAL_G/2,base_cz]).cut(box(out_w,SEAL_G+4,out_d,[out_cx,base_bottom-SEAL_G/2,out_cz]))
fixings=[]
for bx,by,bz in base_axes:
 base_seal=base_seal.cut(cyl(2.5,SEAL_G+4,[bx,base_bottom-SEAL_G-2,bz],(0,1,0)))
 screw=cyl(2.2,BASE_G+7,[bx,base_bottom-6,bz],(0,1,0)).fuse(cyl(4.5,2,[bx,base_top+1,bz],(0,1,0)))
 washer=ring(5,2.5,1,[bx,base_top,bz],(0,1,0));fixings.extend([screw,washer])
add('heater_uncompressed_base_to_platform_seal_comparison',base_seal,(.13,.16,.13))
add('heater_six_platform_screw_and_washer_role_comparisons',cq.Compound.makeCompound(fixings),(.5,.53,.48))
assert case[0].intersect(base_seal).Volume()<1e-5
assert case[0].intersect(cq.Compound.makeCompound(fixings)).Volume()<1e-5
assert case[0].intersect(cyl(1,BASE_DROP+12,[out_cx,base_bottom-3,out_cz],(0,1,0))).Volume()<1e-5
base_reference=dict(factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=280',seal_role_source='https://www.mgocspares.co.uk/parts/mg/midget/heating-ventilation/1500cc-heater-system',basis='Factory lower mounting-base/six-platform-screw architecture and separate body seal role. Source matrix/casing references supply an estimated outlet and flange, not a registered body aperture.',owner_dimensions_applied=False,estimated_drop_mm=BASE_DROP,estimated_flange_gauge_mm=BASE_G,estimated_uncompressed_seal_mm=SEAL_G,flange_xz_bounds_source_mm=[[base_cx-base_w/2,base_cx+base_w/2],[base_cz-base_d/2,base_cz+base_d/2]],outlet_center_source_mm=[out_cx,bottom,out_cz],estimated_outlet_width_depth_mm=[out_w,out_d],base_fixing_axes_source_mm=base_axes,seal_bottom_source_y_mm=base_bottom-SEAL_G,body_receivers_created=False,limits='Every new dimension and axis is a construction estimate. Original main casing envelope, intake/water necks, rear cover and11 other existing assembly groups remain; lower outlet/base is added only to the whole-car heater source. Platform oblong feature keeps its unknown function and geometry; no complete cabin airway, loaded sealing or fitted screw engagement is inferred.')
limits='Casing dimensions and all added core/motor/fan/seal/valve/bracket dimensions, port axes, hose sections/bends, clamps and source/body poses are estimates. Hidden internals are factory-architecture comparisons, not observations of the fitted heater. The second internal neck stops at a short straight boundary; its hidden header connection is not reconstructed. Fin/blade counts and all water channels, seals, motor orientation/wiring, tap actuation/flow direction, loaded supports and thermal/airflow behavior are unverified. Hose B and the elbow-side hose stop at open untraced boundaries. No inferred 1275 radiator return, California bypass/automatic-choke branch, complete coolant circuit, hose cutting pattern or verified replacement/removal path. Display separation is illustrative. The whole-car case now has a lower open throat and formed mounting flange, separate6mm unloaded seal and six screw/washer roles.20mm drop,2mm flange and160x50mm throat are unmeasured construction estimates. No body aperture is identified from the unrelated platform slot, no body receiver is drilled, and no complete heater-to-cabin airway, fitted sealing or mounting strength is established.'
assembly=cq.Assembly(name='heater_service_architecture_reference');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=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]))
assembly.export(str(OUT/'heater-assembly-reference.step'))
casing_volume=parts[0][1].Volume()+parts[1][1].Volume()
assert abs(casing_volume-sum(s.Volume() for s in case))<1e-6 # assembly/source agreement, not old occupied volume
static_overlaps={parts[i][0]:round(parts[i][1].intersect(cq.Compound.makeCompound(case)).Volume(),6) for i in [2,3,4,5]}
metadata=dict(casing_volume_mm3=casing_volume,prior_maintained_casing_volume_mm3=prior_casing_volume,base_outlet_reference=base_reference,static_internal_casing_overlaps_mm3=static_overlaps,units='mm',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=280'],owner_photos=['IMG_6890','IMG_6891','IMG_6892'],basis='Retains the main casing envelope and five separate source solids while adding an estimated lower outlet/base to the whole-car case. The separate engine export is unchanged. 1500 matrix 37H8818 and motor/plate 37H4583 are catalog architecture references only; hidden internals occupy estimated source stations. Photos show the two local hose forms and neighboring green tap context, without a full coolant trace. Factory distinguishes North America except California CHA352 from separate California bypass and choke plumbing; no fitted hose part is selected.',limits=limits,nodes_mm=dict(heater_port_a=a,heater_port_b=b,valve_hose_boundary=valve_out,hose_b_open=hose_b[-1],elbow_hose_open=remote),parts=meshes)
(OUT/'heater-assembly-reference.json').write_text(json.dumps(metadata,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'heater-assembly-reference.step')).val();assert restored.isValid()
assert all(s.isValid() for s in restored.Solids())
print(json.dumps(dict(groups=len(parts),casing_volume_mm3=casing_volume,static_internal_casing_overlaps_mm3=static_overlaps,step_valid=True,solids=len(restored.Solids()),nodes_mm=metadata['nodes_mm'])))
