"""Owner-photo radiator-to-expansion-tank relationship; fitted identities unknown."""
from pathlib import Path
import json,math
import cadquery as cq
import build_engine as engine
OUT=Path(__file__).parent;parts=[];X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1)
def cyl(r,l,p,a):return cq.Solid.makeCylinder(r,l,p,a)
def ring(ro,ri,l,p,a):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 add(n,s,c):
 assert s.isValid() and s.Volume()>0,n
 parts.append((n,s,c));return s
def section(cx,cy,cz,level,w,d):
 ps=[]
 for i in range(48):
  t=2*math.pi*i/48;c,s=math.cos(t),math.sin(t);ps.append((w/2*math.copysign(abs(c)**.58,c),d/2*math.copysign(abs(s)**.58,s)))
 return cq.Workplane('XZ',origin=(cx,cy+level,cz)).polyline(ps).close().val()
cx,cy,cz=engine.expansion_tank_center();w=engine.P['expansion_tank_width'];h=engine.P['expansion_tank_height'];d=engine.P['expansion_tank_depth'];top=cy+h/2
old=engine.make_expansion_tank_photo_study();outer,seam,filler,neck=old.Solids()
# A reduced interior loft is a hollow-envelope estimate, NOT constant wall gauge.
stations=[(-h/2+3,w*.47-6,d*.53-6),(-h*.38,w*.86-6,d*.87-6),(-h*.08,w-6,d-6),(h*.24,w*.93-6,d*.96-6),(h*.42,w*.76-6,d*.82-6),(h/2-3,w*.49-6,d*.53-6)]
void=cq.Solid.makeLoft([section(cx,cy,cz,*s) for s in stations],ruled=False)
F=cq.Vector(cx-w*.245,top-2,cz-d*.15);T=cq.Vector(cx+w*.255,top-2,cz+d*.18);TM=T+Y*20
shell=outer.fuse(seam).fuse(filler).fuse(neck).cut(void).cut(cyl(15,35,F-Y*15,Y)).cut(cyl(7.5,45,T-Y*20,Y)).clean()
add('expansion_hollow_shell_seam_and_two_open_necks_comparison',shell,(.70,.68,.59))
# Preserve original visible crown/lip outside bounds; skirt is an unsealed envelope.
cap=cyl(23,6,F+Y*11,Y).fuse(cyl(25,2,F+Y*11,Y)).fuse(ring(21,19,7,F+Y*4,Y)).clean()
add('expansion_cap_crown_and_open_unsealed_skirt_comparison',cap,(.54,.55,.51))
# Photo shows radiator small-hose endpoint; its hidden neck dimensions/pose estimated.
R=json.loads((OUT/'radiator-connections-reference.json').read_text());r=R['retained_core'];radx=-(r['dimensions_mm']['width']/2+17.5);radrear=r['center_mm'][2]-(r['dimensions_mm']['thickness']+8)/2
RB=cq.Vector(radx,130,radrear);RM=RB-Z*24
radneck=ring(10,7.5,28,RM,Z);add('expansion_radiator_small_open_neck_comparison',radneck,(.43,.45,.39))
# Include straight end engagements, with an explicitly estimated broad arch.
H0=TM-Y*12;H1=TM+Y*5;H2=RM-Z*5;H3=RM+Z*12
controls=[H1,H1+Y*90,H2-Z*65,H2]
curve=cq.Edge.makeBezier(controls);path=cq.Wire.assembleEdges([cq.Edge.makeLine(H0,H1),curve,cq.Edge.makeLine(H2,H3)])
hose=cq.Workplane(cq.Plane(origin=H0,normal=Y)).circle(13.5).circle(10.5).sweep(path,isFrenet=True).val()
add('expansion_visible_radiator_hose_hollow_route_comparison',hose,(.16,.18,.15))
clamps=cq.Compound.makeCompound([ring(14.5,13.6,7,TM-Y*8,Y),ring(14.5,13.6,7,RM+Z*2,Z)])
add('expansion_two_smooth_hose_clip_envelopes',clamps,(.53,.55,.48))
ears=[];fix=[];axes=[]
for sign in [-1,1]:
 q=cq.Vector(cx+sign*(w/2+5),cy-10,cz);ear=box(26,22,4,q).cut(cyl(3.5,8,q-Z*4,Z));ears.append(ear);axes.append(list(q.toTuple()))
 shaft=cyl(3,20,q-Z*10,Z);head=cq.Workplane(cq.Plane(origin=q+Z*5,normal=Z)).polygon(6,11).extrude(4).val();wash=ring(7,3.3,1.5,q+Z*3.5,Z);fix.extend([shaft,head,wash])
add('expansion_two_integral_role_mounting_ear_comparisons',cq.Compound.makeCompound(ears),(.70,.68,.59))
add('expansion_two_unassigned_mount_fixing_envelopes',cq.Compound.makeCompound(fix),(.52,.54,.47))
# Construction gauges; these are not pressure, loaded seal or fitted clearance tests.
assert shell.intersect(cyl(14.8,30,F-Y*12,Y)).Volume()<1e-5
assert shell.intersect(cyl(7.3,40,T-Y*18,Y)).Volume()<1e-5
assert radneck.intersect(cyl(7.3,32,RM-Z*2,Z)).Volume()<1e-5
assert shell.intersect(hose).Volume()<1e-4
assert radneck.intersect(hose).Volume()<1e-4
# Keep independent surroundings fixed and report sampled source contacts.
rad=cq.Assembly.importStep(str(OUT/'radiator-connections-reference.step'));neighbors={'radiator_cowl':engine.make_radiator_cowl_photo_study(),'fan_static':engine.make_fan()}
for name in ['radiator_upper_side_hollow_tank_reference','radiator_lower_side_hollow_tank_reference','radiator_lower_full_hose_route_comparison']:
 ob=rad.objects[name].obj;neighbors[name]=ob.val() if isinstance(ob,cq.Workplane) else ob
contacts={}
for name,s,c in parts:
 for k,o in neighbors.items():
  a,b=s.BoundingBox(),o.BoundingBox()
  if all(getattr(a,k+'min')<=getattr(b,k+'max') and getattr(b,k+'min')<=getattr(a,k+'max') for k in ['x','y','z']):
   v=round(s.intersect(o).Volume(),4)
   if v>1e-4:contacts[name+'__'+k]=v
limits='No new direct owner dimensions or fitted tank/cap/hose identities are applied. IMG_6849 and IMG_6854 show a tank-to-radiator hose relationship compatible with the standard factory p86 architecture; this does not prove every fitted connection or identify ARA326/GRC110. TSB75-A-6 p87 and later p88 branches remain distinct and unselected. Existing 94 x 145 x 66 mm shell, outer loft/seam, cap crown/lip and engine-relative tank station are retained photo-layout estimates. Reduced inner loft, nominal sections, open neck bores, cap skirt, radiator small neck/port, hose controls, engagement, clip envelopes, ear/fixing dimensions and all new axes are estimates. The reduced loft is not a constant-thickness shell. Factory 279 mm standard hose length is documentary only, not a measured fitted cutting length or basis for forcing the route. Cap underside sealing lands, lugs, valves, pressure rating and compatibility are not reconstructed; printed cap markings do not identify its sealing type. Ears represent integral/molded roles displayed separately, not removable attachments; fixing counterparts, body support, thread, torque and loaded seating remain unassigned. No remote spill tank or thermostat branch is invented. Source contacts are reconstruction discrepancies, not fitted interference findings; static fan sampling is not a rotation or operating-clearance check. Full shared registration, installed hose trace/clearance, wall section, strength, pressure/thermal behavior, drain/fill procedure and verified removal/replacement paths remain unknown. Display separation is illustrative.'
meta=dict(units='mm',source_frame='Maintained native engine mm frame; same estimated body transform as engine.',owner_photos=['IMG_6849','IMG_6854'],owner_dimensions_applied=False,factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=86','https://www.spridgets.de/downloads/Parts-1977.pdf#page=87','https://www.spridgets.de/downloads/Parts-1977.pdf#page=88'],registration_status='photo_supported_connection_relationship_estimated_geometry_and_registration',basis='Owner photographs show the pale seamed tank, cap and visible arched hose to the radiator. Factory p86 separately supports the standard two-ear tank and radiator-to-tank hose roles. The visible relationship is retained without choosing the fitted standard/bulletin identity; all dimensions/stations remain estimates.',retained_tank_center_mm=[cx,cy,cz],retained_tank_outer_dimensions_mm=[w,h,d],nodes=dict(filler_neck_base_mm=list(F.toTuple()),filler_neck_mouth_mm=list((F+Y*11).toTuple()),tank_hose_neck_mouth_mm=list(TM.toTuple()),radiator_small_port_base_mm=list(RB.toTuple()),radiator_small_neck_mouth_mm=list(RM.toTuple()),hose_end_boundaries_mm=[list(H0.toTuple()),list(H3.toTuple())],hose_bezier_controls_mm=[list(p.toTuple()) for p in controls],ear_fixing_centers_mm=axes),source_neighbor_contacts_mm3=contacts,limits=limits)
a=cq.Assembly(name='owner_photo_expansion_tank_connection_comparison');meshes=[]
for n,s,c in parts:
 a.add(s,name=n,color=cq.Color(*c));vs,ts=s.tessellate(.5,.16);meshes.append(dict(name=n,color=c,vertices=[round(v,4) for p in vs for v in p.toTuple()],triangles=[i for t in ts for i in t]))
a.export(str(OUT/'expansion-connections-reference.step'));meta['parts']=meshes;(OUT/'expansion-connections-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
s=cq.importers.importStep(str(OUT/'expansion-connections-reference.step')).val();assert s.isValid();print(json.dumps(dict(meshes=len(parts),solids=len(s.Solids()),step_valid=True,open_necks=True,local_hose_neck_nonintersection=True,source_contacts=contacts)))
