"""Crossflow radiator/tank/neck and hose comparisons; fitted routes unknown."""
from pathlib import Path
import json
import cadquery as cq
OUT=Path(__file__).parent
SOURCE=json.loads((OUT/'midget-1500-measured-core.json').read_text())
C=SOURCE['checks'];R=C['radiator_core_dimensions_mm'];U=C['upper_hose_connection_mm'];L=C['lower_hose_visible_bend_mm']
X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1);parts=[];nodes={}
def V(p):return cq.Vector(*p)
def box(w,h,d,p):return cq.Workplane('XY').box(w,h,d).translate(p.toTuple()).val()
def ring(ro,ri,l,p,axis=Z):return cq.Solid.makeCylinder(ro,l,p,axis).cut(cq.Solid.makeCylinder(ri,l+2,p-axis,axis))
def add(name,shape,color=(.25,.33,.35)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color));return shape
def shell_curve(points,ro,ri):
 edge=cq.Edge.makeBezier(list(map(V,points)))
 path=cq.Wire.assembleEdges([edge]);plane=cq.Plane(origin=V(points[0]),normal=edge.tangentAt(0))
 outside=cq.Workplane(plane).circle(ro).sweep(cq.Workplane(obj=path),isFrenet=True).val()
 inside=cq.Workplane(plane).circle(ri).sweep(cq.Workplane(obj=path),isFrenet=True).val()
 return outside,inside

w,h,d=R['width'],R['height'],R['thickness'];cz=C['radiator_core_center_z_mm'];rear=C['radiator_core_rear_z_mm'];tank_rear=cz-(d+8)/2;gauge=1.2
# Exactly retain the old core envelope and station. Its internal waterways are
# not reconstructed; an opaque core does not become a fluid simulation.
add('radiator_retained_opaque_core_envelope_comparison',box(w,h,d,V([0,30,cz])))
port_specs=[('upper',1,64,U['neck_outside']/2,U['bead_outside']/2,U['hose_bore']/2,U['hose_outside']/2),('lower',-1,-74,L['housing_neck_outside']/2,L['bead_outside']/2,L['bore']/2,L['outside']/2)]
for label,side,py,neck_r,bead_r,bore_r,hose_r in port_specs:
 px=side*(w/2+17.5);center=V([px,30,cz]);port=V([px,py,tank_rear]);mouth=port-Z*30
 tank=box(35,h+27,d+8,center).cut(box(35-2*gauge,h+27-2*gauge,d+8-2*gauge,center))
 # Open core-facing inner wall; section and aperture remain construction.
 tank=tank.cut(box(4,h-2*gauge,d-2*gauge,V([side*(w/2+gauge/2),30,cz])))
 tank=tank.cut(cq.Solid.makeCylinder(neck_r-2,12,port-Z*8,Z))
 # Owner IMG_6849/6854 show a small radiator-to-expansion-tank connection.
 # Bore pose/diameter are estimates shared with the independent connection builder.
 if label=='lower':tank=tank.cut(cq.Solid.makeCylinder(10,12,V([px,130,tank_rear-8]),Z))
 add('radiator_'+label+'_side_hollow_tank_reference',tank)
 neck=ring(neck_r,neck_r-2,34,mouth)
 bead=ring(bead_r,neck_r-2,4,mouth)
 add('radiator_'+label+'_open_neck_and_bead_comparison',neck.fuse(bead),(.43,.45,.39))
 if label=='upper':
  points=[[76,117,191],[150,117,191],[px,py,235],[px,py,mouth.z]]
  outside,inside=shell_curve(points,hose_r,bore_r)
  socket= cq.Solid.makeCylinder(hose_r,26,V([60,117,191]),X)
  socket_void=cq.Solid.makeCylinder(bore_r,27,V([59,117,191]),X)
  solid=outside.fuse(socket);voids=[inside,socket_void]
  if bore_r<U['neck_outside']/2+.3-1e-8:voids.append(cq.Solid.makeCylinder(U['neck_outside']/2+.3,17.2,V([60,117,191]),X))
  if bore_r<U['bead_outside']/2+.3-1e-8:voids.append(cq.Solid.makeCylinder(U['bead_outside']/2+.3,4.4,V([72.8,117,191]),X))
  engine_boundary=[60,117,191]
 else:
  nx=L['hose_socket_x'][0];eng=L['hose_socket_x'][1]-nx
  # The original socket starts 18 mm before the measured/display neck tip.
  points=[[nx-4,23,160],[-210,23,160],[px,py,225],[px,py,mouth.z]]
  outside,inside=shell_curve(points,hose_r,bore_r)
  socket=cq.Solid.makeCylinder(hose_r,eng+18,V([nx-18,23,160]),X)
  socket_void=cq.Solid.makeCylinder(bore_r,eng+19,V([nx-18.5,23,160]),X)
  solid=outside.fuse(socket);voids=[inside,socket_void]
  if bore_r<L['housing_neck_outside']/2+.3-1e-8:voids.append(cq.Solid.makeCylinder(L['housing_neck_outside']/2+.3,eng+.4,V([nx-.2,23,160]),X))
  if bore_r<L['bead_outside']/2+.3-1e-8:voids.append(cq.Solid.makeCylinder(L['bead_outside']/2+.3,L['bead_axial_width']+.4,V([nx-.2,23,160]),X))
  engine_boundary=[nx+eng,23,160]
 # Display socket over the separately estimated radiator neck and bead.
 solid=solid.fuse(cq.Solid.makeCylinder(hose_r,30,mouth,Z))
 voids.append(cq.Solid.makeCylinder(max(bore_r,neck_r+.3),31,mouth-Z*.5,Z))
 if bore_r<bead_r+.3-1e-8:voids.append(cq.Solid.makeCylinder(bead_r+.3,5,mouth-Z*.5,Z))
 void=voids[0]
 for tool in voids[1:]:void=void.fuse(tool)
 shell=solid.cut(void).clean()
 assert shell.isValid() and len(shell.Solids())==1 and shell.Volume()>50000,(label,'hose shell',len(shell.Solids()),shell.Volume(),outside.isValid(),inside.isValid())
 add('radiator_'+label+'_full_hose_route_comparison',shell,(.11,.14,.12))
 clamp_z=tank_rear-18
 band=ring(hose_r+1.6,hose_r+.1,8,V([px,py,clamp_z-4]))
 housing=box(12,7,12,V([px,py+hose_r+3,clamp_z]))
 screw=cq.Solid.makeCylinder(2.5,12,V([px-6,py+hose_r+7,clamp_z]),X)
 add('radiator_'+label+'_end_clamp_envelope',cq.Compound.makeCompound([band,housing,screw]),(.53,.55,.48))
 # Open metal port and hose ends, and no source-solid overlap at the chosen
 # display socket. These do not verify a fitted fluid or sealing connection.
 tool=cq.Solid.makeCylinder(neck_r-2.2,36,mouth-Z*.5,Z)
 assert neck.intersect(tool).Volume()<1e-5 and tank.intersect(tool).Volume()<1e-5
 assert neck.fuse(bead).intersect(shell).Volume()<1e-3,(label,'neck/hose overlap',neck.fuse(bead).intersect(shell).Volume())
 assert tank.intersect(shell).Volume()<1e-3,(label,'tank/hose overlap')
 nodes[label]=dict(radiator_port_base_mm=list(port.toTuple()),radiator_neck_mouth_mm=list(mouth.toTuple()),neck_axis=[0,0,1],estimated_neck_outside_radius_mm=neck_r,estimated_bead_outside_radius_mm=bead_r,estimated_neck_bore_radius_mm=neck_r-2,hose_bore_radius_mm=bore_r,hose_outside_radius_mm=hose_r,engine_boundary_mm=engine_boundary,hose_bezier_control_points_mm=points,clamp_center_mm=[px,py,clamp_z],basis='Engine-end dimensions inherit the maintained source check bases, not new owner measurements; radiator-end dimensions and all new route/station/engagement choices are estimates.')

limits='No new direct owner dimensions or fitted radiator/hose identities are applied. The 422 x 258 x 40 mm core is the maintained aftermarket comparison, not a measured installed radiator. Original core/tank outside bounds, source frame and core station are retained; new 1.2 mm tank walls, open core-facing windows, rear necks/beads, port centers, bores, hose engagement, routes and clamp envelopes are estimates. Engine-end diameter bases inherit maintained source checks; a source comparison is never relabeled as an owner measurement. Upper visible routing is reconciled with IMG_6853, but its final bend and hidden radiator joint are replaced by a declared estimated connection. Engine socket axes and axial stations remain source references; both complete bends use declared Bezier control polygons guided only by local photo context and estimated radiator endpoints. These replace the former partial hose curves in the whole-car display, without preserving their unmeasured surfaces. A coherent socket does not identify fitted neck geometry, rubber stretch, seal compression, clamp setting or hose cutting length. The core remains an opaque envelope, without coolant tubes, flow, pressure or thermal behavior. Early USA/Canada TSB75-A-6 expansion/thermostat plumbing, later GAN6-166304-on arrangements, fitted tank identity and small-hose connections remain distinct and unresolved; no generic 1275 route is imposed. Existing fan/cowl model overlap is retained, not moved to pass a clearance check. No operating fan/hose/body clearance, manufacturing bend radius, pressure test, drain/fill procedure, verified release order or replacement/removal path. All separation is illustrative.'
# Report, never remove, conflicts with the maintained cowl reference. The
# generator is the existing public source builder, without owner overrides.
import build_engine as engine
cowl=engine.make_radiator_cowl_photo_study()
cowl_contacts={name:round(shape.intersect(cowl).Volume(),4) for name,shape,_ in parts if 'neck_and_bead' in name or 'full_hose' in name}
meta=dict(units='mm',source_frame='Maintained native engine frame +X vehicle-left, +Y up, +Z front; mm. Same estimated body transform as engine.',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'],owner_photos=['IMG_6853','IMG_6854','IMG_6896'],owner_dimensions_applied=False,basis='Factory crossflow 1500 plates separate radiator, GRH525 top hose, CHA603 lower hose and clips from cowl and expansion branches. IMG_6853 shows the visible upper hose/cowl relationship; IMG_6896 supports local lower hose recognition. Source 422x258x40 aftermarket core and source poses retained. Hidden radiator ports and new hose approaches are explicitly estimated.',inherited_engine_end_checks=dict(upper=U,lower=L),retained_core=dict(dimensions_mm=R,center_mm=[0,30,cz],rear_z_mm=rear),estimated_tank_wall_mm=gauge,static_cowl_overlap_mm3=cowl_contacts,nodes=nodes,limits=limits)
meta['expansion_small_port_comparison']=dict(center_mm=[-(w/2+17.5),130,tank_rear],axis=[0,0,1],tank_aperture_radius_mm=10,neck_bore_radius_mm=7.5,basis='IMG_6849/6854 visible radiator-to-tank relationship only; new bore dimensions/pose are estimates. Separate expansion-connections source retains all identity and registration limits.')
assembly=cq.Assembly(name='estimated_radiator_tanks_ports_and_hose_connections');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=shape.tessellate(.6,.15)
 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/'radiator-connections-reference.step'));meta['parts']=meshes
(OUT/'radiator-connections-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'radiator-connections-reference.step')).val();assert restored.isValid()
print(json.dumps(dict(groups=len(parts),solids=len(restored.Solids()),step_valid=True,open_neck_passages=True,hose_neck_display_overlap_absent=True,core_bounds_and_station_retained=True)))
