"""Cowl/guard/radiator/body attachment comparisons, all fitted registration 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=(.22,.27,.24)):
 assert s.isValid() and s.Volume()>0,n
 parts.append((n,s,c));return s
def comp(xs):return cq.Compound.makeCompound(xs)
def bolt(r,length,p,axis,af,hh,wr):
 # p = under-head/washer plane; no thread, torque or locking reconstructed.
 head=cq.Workplane(cq.Plane(origin=p-axis*hh,normal=axis)).polygon(6,af/math.cos(math.pi/6)).extrude(hh).val()
 return comp([cyl(r,length,p,axis),head,ring(wr,r+.2,1.2,p,axis)])
S=2032/2.4;anchor=cq.Vector(0,-225,-182.5);bodyanchor=cq.Vector(0,.39,.63)
def body_to_native(p):return (cq.Vector(*p)-bodyanchor)*S+anchor
w=engine.P['radiator_core_width']+80;h=engine.P['radiator_core_height']+50;z=engine.radiator_core_center_z()-engine.P['radiator_core_depth']/2-20;y=30
old=engine.make_radiator_cowl_photo_study();frame,apron,bottom,leftside,rightside=old.Solids();sides=[leftside,rightside]
# Original hole cues conflict with source hose ports when used for radiator lugs.
# Close those cues, retain X, move rows -39.44/+35.44 mm as construction only.
old_rad_axes=[cq.Vector(s*(w/2-19),y+py*h*.32,z) for s in [-1,1] for py in [-1,1]]
for q in old_rad_axes:frame=frame.fuse(cyl(3.2,2,q-Z,Z)).clean()
rad_axes=[cq.Vector(s*(w/2-19),py,z) for s in [-1,1] for py in [-108,164]]
for q in rad_axes:frame=frame.cut(cyl(4.25,8,q-Z*4,Z))
# Side-wall body attachment axes and six body-role stations are construction.
body_axes={};supports={};bodyfix=[]
for sign,label in [(1,'left'),(-1,'right')]:
 axes=[cq.Vector(sign*(w/2-1),py,z+13) for py in [-80,110]];body_axes[label]=axes
 for q in axes:sides=[s.cut(cyl(3.5,18,q-X*9,X)) for s in sides]
 foot=body_to_native([sign*.35,.388,1.24]);footshape=box(.13*S,.016*S,.15*S,foot)
 # Upper folded body channel and diagonal foot web retain the old base datum.
 upright=box(3,350,30,cq.Vector(sign*(w/2+3),-45,z+13))
 flange=box(17,350,2,cq.Vector(sign*(w/2+10),-45,z+27))
 a=cq.Vector(sign*(w/2+3),-218,z+13);b=foot;delta=b-a
 web=cq.Workplane(cq.Plane(origin=(a+b)*.5,xDir=delta,normal=Y)).box(delta.Length,26,3).val()
 shape=comp([footshape,upright,flange,web])
 for q in axes:
  shape=shape.cut(cyl(3.5,26,q-X*13,X))
  p=q+X*sign*7;bodyfix.append(bolt(3.175,19.05,p,-X*sign,11,4,6.5))
 supports[label]=shape
# Separate formed CHA420-role guard, based on supplier silhouette and photo strip.
N=cq.Vector(0,1,-28/55).normalized();gp=cq.Plane(origin=(0,y+h/2+(300-z)*28/55+.4,300),xDir=X,normal=N)
base=cq.Workplane(gp).polyline([(-230,-18),(230,-18),(230,18),(-230,18)]).close().extrude(1.5).val()
# A raised shallow channel is separate from the underlying cowl apron.
riser=cq.Workplane(gp).box(350,1.5,19).translate((N*11).toTuple()).val()
roof=cq.Workplane(gp).polyline([(-175,-1),(175,-1),(145,19),(-145,19)]).close().extrude(1.5).translate((N*20).toTuple()).val()
guard=base.fuse(riser).fuse(roof).clean();guard_axes=[gp.toWorldCoords((s*205,0,0)) for s in [-1,1]];upperfix=[]
for q in guard_axes:
 tool=cyl(2.7,20,q-N*10,N);guard=guard.cut(tool);apron=apron.cut(tool)
 upperfix.append(bolt(2.413,12.7,q+N*3,-N,7.5,3,5))
cowl=comp([frame,apron,bottom,*sides]);add('cooling_cowl_retained_profile_with_joint_bores',cowl)
add('cooling_separate_formed_guard_comparison',guard)
# Open L-shaped radiator-side lug roles do not drill a pressure tank.
lugs=[];radfix=[]
for q in rad_axes:
 sign=1 if q.x>0 else -1
 front=box(28,22,2,cq.Vector(sign*(w/2-17),q.y,z+3))
 leg=box(2,22,28,cq.Vector(sign*(w/2-4),q.y,z+18))
 lug=front.fuse(leg).cut(cyl(4.25,10,q-Z*2,Z)).clean();lugs.append(lug)
 radfix.append(bolt(3.96875,12.7,q-Z*3,Z,13,5,8))
add('cooling_four_integral_role_radiator_lug_comparisons',comp(lugs),(.40,.43,.38))
add('cooling_four_radiator_screw_washer_envelopes',comp(radfix),(.53,.55,.49))
for label in ['left','right']:add('cooling_'+label+'_body_support_comparison',supports[label],(.28,.32,.28))
add('cooling_four_side_body_screw_washer_envelopes',comp(bodyfix),(.53,.55,.49))
add('cooling_two_upper_screw_washer_envelopes',comp(upperfix),(.53,.55,.49))
# The present top body-panel station is remote; no invented long bridge is added.
for q in rad_axes:
 assert cowl.intersect(cyl(4.0,8,q-Z*4,Z)).Volume()<1e-5
 assert comp(lugs).intersect(cyl(4.0,10,q-Z*2,Z)).Volume()<1e-5
for label,axes in body_axes.items():
 for q in axes:
  assert cowl.intersect(cyl(3.3,18,q-X*9,X)).Volume()<1e-5
  assert supports[label].intersect(cyl(3.3,26,q-X*13,X)).Volume()<1e-5
for q in guard_axes:assert guard.intersect(cyl(2.5,12,q-N*6,N)).Volume()<1e-5
neighbors={'fan_static':engine.make_fan()}
for filename in ['radiator-connections-reference','expansion-connections-reference']:
 ass=cq.Assembly.importStep(str(OUT/(filename+'.step')))
 for name,ob in ass.objects.items():
  if name==ass.name:continue
  s=ob.obj
  if s is not None:neighbors[name]=s.val() if isinstance(s,cq.Workplane) else s
contacts={}
for name,s,c in parts:
 if 'screw' in name:continue
 for n,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+'__'+n]=v
limits='No new owner measurements or fitted cowl, guard, bracket or fixing identities are applied. Factory p86/p88 distinguish four radiator screws, two upper cowl screws and four side cowl screws; this supplies roles/counts, not this fitted map. Supplier CHA420 silhouette and IMG_6853/6854 support a separate formed strip/guard comparison, not proof of its fitted identity. Existing cowl outside profiles and source station remain estimates; four inherited front-hole cues are closed and replaced at retained X with rows moved -39.44/+35.44 mm to y=-108/164 mm, avoiding source main/small hose ports. New 4.25 mm radius bores accommodate a nominal 5/16 comparison screw. This relocation is construction, not measured installed geometry. New side/upper holes, lug sections, guard profile, support folds, dimensions and axes are estimated. Nominal screw shank diameters/lengths are catalog comparisons, not owner readings; heads, washers, threads, locking, engagement and loaded seating are unverified. Body supports retain old drawing foot centers but move their upper construction to shared cowl side-hole axes; no fitted bracket geometry, base weld/fixing or support load is established. Radiator lugs are integral/soldered-role comparisons displayed separately, not verified detachable brackets; pressure tank wall and radiator export remain unchanged. Side-joint spacing and radiator-lug gap are construction, not fitted packing/seating. The two upper screws are comparison roles through guard/cowl only; the distant front landing-panel attachment remains open rather than inventing a bridging bracket. All original fan/radiator/hose/cowl reconstruction conflicts remain documentary limitations, not fitted interference findings. Static fan sampling does not verify full rotation, engine movement or operating clearance. No safe drain/release/support order, tool access, tested strength, verified replacement sequence or removal path. Display separation is illustrative.'
meta=dict(units='mm',owner_photos=['IMG_6853','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=88'],supplier_source='https://rimmerbros.com/Item--i-CHA420',source_frame='Maintained native engine mm frame; estimated body registration.',basis='Factory cowl/radiator/guard separation and fixing counts, owner visible apron/bolts and supplier formed guard silhouette support independent mounting roles. Existing source cowl envelope and body-foot drawing datums are retained; new attachment geometry is estimated.',registration_status='unregistered_attachment_comparisons_with_retained_source_contacts',nodes=dict(radiator_axes_mm=[list(q.toTuple()) for q in rad_axes],radiator_axis_direction=[0,0,1],superseded_radiator_hole_cues_mm=[list(q.toTuple()) for q in old_rad_axes],radiator_row_construction_shift_mm=[-39.44,35.44],side_body_axes_mm={k:[list(q.toTuple()) for q in v] for k,v in body_axes.items()},upper_axes_mm=[list(q.toTuple()) for q in guard_axes],upper_axis_direction=list(N.toTuple()),retained_body_foot_centers_mm={label:list(body_to_native([sign*.35,.388,1.24]).toTuple()) for sign,label in [(1,'left'),(-1,'right')]}),source_neighbor_contacts_mm3=contacts,limits=limits)
ass=cq.Assembly(name='unregistered_front_cooling_attachment_comparisons');meshes=[]
for n,s,c in parts:
 ass.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]))
ass.export(str(OUT/'cooling-support-reference.step'));meta['parts']=meshes;(OUT/'cooling-support-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
s=cq.importers.importStep(str(OUT/'cooling-support-reference.step')).val();assert s.isValid();print(json.dumps(dict(meshes=len(parts),solids=len(s.Solids()),step_valid=True,shared_joint_bores_open=True,source_contacts=contacts)))
