"""Branched 1500 heater/manifold return comparison. Fitted circuit unverified."""
from pathlib import Path
import json,math
import cadquery as cq
OUT=Path(__file__).parent;HEATER=json.loads((OUT/'heater-assembly-reference.json').read_text());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 compound(xs):return cq.Compound.makeCompound(xs)
def add(name,shape,color=(.40,.43,.38)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color));return shape
def sweep(path,r,start,axis):return cq.Workplane(cq.Plane(origin=start,normal=axis)).circle(r).sweep(path,isFrenet=True).val()
def hollow_path(path,ro,ri,start,axis):return cq.Workplane(cq.Plane(origin=start,normal=axis)).circle(ro).circle(ri).sweep(path,isFrenet=True).val()
A=cq.Vector(-85,-40,145);B=cq.Vector(-85,-40,-70);D=cq.Vector(-155,-40,-190)
end_axis=cq.Vector(-35,0,-20).normalized();C=D-end_axis*30
curve=cq.Edge.makeBezier([B,B-Z*40,C-end_axis*40,C]);Q=curve.positionAt(.8);E=Q+Y*65
path=cq.Wire.assembleEdges([cq.Edge.makeLine(A,B),curve,cq.Edge.makeLine(C,D)]);innerpath=cq.Wire.assembleEdges([cq.Edge.makeLine(A+Z*2,A),cq.Edge.makeLine(A,B),curve,cq.Edge.makeLine(C,D),cq.Edge.makeLine(D,D+end_axis*2)])
outer=sweep(path,8.5,A,-Z);inside=sweep(innerpath,6.5,A+Z*2,-Z)
branch=cyl(8.5,45,Q,Y).fuse(cq.Solid.makeCone(8.5,11.5,10,Q+Y*45,Y)).fuse(cyl(11.5,10,Q+Y*55,Y))
branch_void=cyl(6.5,46,Q-Y,Y).fuse(cq.Solid.makeCone(6.5,9.5,10,Q+Y*45,Y)).fuse(cyl(9.5,13,Q+Y*55,Y))
pipe=outer.fuse(branch).cut(inside.fuse(branch_void))
add('heater_return_branched_hollow_212935_role_comparison',pipe)
# Product-photo mounting ear only. No actual engine counterpart or bolt is assigned.
tab_center=Q+X*18-Y*6;hole=Q+X*29-Y*6
tab=box(38,2,18,tab_center).cut(outer.fuse(branch)).cut(cyl(3.5,8,hole-Y*4,Y))
add('heater_return_integral_tab_separately_displayed_comparison',tab)
# Smooth connector geometry, not reconstructed compression threads or sealing.
adapter=ring(12,6.5,30,A-Z*8,Z).cut(cyl(10,10,A-Z*9,Z))
add('heater_return_unassigned_pump_end_adapter_comparison',adapter,(.53,.50,.38))
nut=cq.Workplane('XY',origin=(A.x,A.y,A.z-8)).polygon(6,30).extrude(12).val().cut(cyl(12.3,14,A-Z*9,Z))
add('heater_return_tube_nut_envelope',nut,(.53,.50,.38))
olive=ring(9.5,8.7,4,A-Z*5,Z);add('heater_return_uncompressed_olive_comparison',olive,(.57,.49,.34))
# A comparison continuation uses the existing untraced B boundary, without
# claiming that its tangent, diameter or remote destination is owner-verified.
H0=cq.Vector(*HEATER['nodes_mm']['hose_b_open']);H1=E+Y*15;H2=E-Y*15
hose_curve=cq.Edge.makeBezier([H0,H0+cq.Vector(45,0,10),H1+Y*35,H1]);hosepath=cq.Wire.assembleEdges([hose_curve,cq.Edge.makeLine(H1,H2)])
hose=hollow_path(hosepath,16,12,H0,cq.Vector(45,0,10));add('heater_return_hose_from_untraced_boundary_comparison',hose,(.14,.17,.14))
cuff=ring(12,9,45,D-end_axis*15,end_axis);add('heater_return_open_rear_cuff_without_manifold_join',cuff,(.14,.17,.14))
clamps=compound([ring(17,16.1,7,E-Y*3.5,Y),ring(13,12.1,6,D-end_axis*11,end_axis)])
add('heater_return_two_smooth_hose_clip_envelopes',clamps,(.54,.56,.49))
# Source consistency only; the pump/manifold interfaces remain open.
assert pipe.intersect(cyl(6.2,12,A+Z*2,-Z)).Volume()<1e-5
assert pipe.intersect(cyl(9.2,11,E+Y*2,-Y)).Volume()<1e-5
assert pipe.intersect(cyl(6.2,12,D+end_axis*2,-end_axis)).Volume()<1e-5
assert pipe.intersect(box(1,1,1,Q-Y*7.5)).Volume()>.05 # lower wall is not a fourth outlet
assert pipe.intersect(adapter).Volume()<1e-5
assert pipe.intersect(cuff).Volume()<1e-5
assert pipe.intersect(hose).Volume()<1e-5
# Preserve independent castings/hoses. Contacts do not identify fitted clearance.
import build_engine as engine
neighbors={'water_pump_housing':engine.make_water_pump_housing(),'inlet_manifold':engine.make_inlet_manifold(),'exhaust_manifold':engine.make_exhaust_manifold(),'block':engine.make_block(engine.bore_centers())}
old_heater=cq.Assembly.importStep(str(OUT/'heater-assembly-reference.step'))
entry=old_heater.objects['heater_visible_hose_b_reference'].obj
neighbors['existing_heater_local_hose_b']=entry.val() if isinstance(entry,cq.Workplane) else entry
contacts={}
for name,shape,color in parts:
 if 'clip' in name:continue
 for other,obstacle in neighbors.items():
  a=shape.BoundingBox();b=obstacle.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']):
   volume=round(shape.intersect(obstacle).Volume(),4)
   if volume>1e-4:contacts[name+'__'+other]=volume
limits='This is an unregistered relationship comparison, not a verified installed route; its current pose intersects block/exhaust source envelopes. No new direct owner dimensions, fitted pipe/adapter identity or complete coolant trace is applied. Factory p65 212935/101343/101302/TL11 and two manifold-hose roles are comparisons; p66 later arrangements and p280 California bypass/automatic-choke branches remain distinct and unselected. Product 212935 photo supports a curved branched pipe and one-hole mounting tab, not its fitted dimensions, actual side/height or attachments. All diameters, walls, bead/flare, bend/control points, union seats, smooth nut/olive, tab and hose paths/stations are estimates. Pipe branch mouth radius 11.5 mm is a construction fit to the old 12 mm hose-boundary bore, not a factory diameter, rubber stretch or loaded clearance. Existing heater B endpoint and inherited 16/12 mm hose radii remain unverified source choices. The new comparison continuation does not establish its splice tangent, hose identity, destination, flow direction or a real coupling at that open boundary. Pump-end adapter stops at an unassigned source endpoint; no existing water-pump boss is identified or drilled to complete the route. Rear cuff stops open and is not joined to an invented manifold coolant nipple. Existing manifold contains air-passage references only; no water jacket/port is created by inference. Existing tap/elbow hose, independent castings, heater casing/local hoses and engine export remain unchanged. The separately displayed tab is a welded/integral-role comparison, not a detachable mounting joint; engine counterpart, fixing and strength are unknown. Thread engagement, olive compression, clamp setting, loaded seal, material compatibility, thermal/pressure behavior, actual routing/clearance, safe drain/release order and verified replacement/removal path remain unestablished. Positive source contacts are reconstruction conflicts, not fitted interference findings. Inspection translations are illustrative.'
meta=dict(registration_status='unregistered_relationship_comparison_with_source_contacts',units='mm',source_frame='Maintained native engine frame +X vehicle-left, +Y up, +Z front; mm. Same estimated body transform as heater/engine.',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=65','https://www.spridgets.de/downloads/Parts-1977.pdf#page=66','https://www.spridgets.de/downloads/Parts-1977.pdf#page=280'],supplier_source='https://rimmerbros.com/Item--i-212935',owner_photos=['IMG_6890','IMG_6891','IMG_6892'],owner_dimensions_applied=False,basis='Factory and supplier establish a branched return pipe, pump-side adapter/tube nut/olive and heater/manifold roles. Owner photos support local heater/tap/hose context only, without a complete return-pipe or casting-port trace. Existing source dimensions and open boundaries are retained as estimates, not relabeled as owner measurements.',nodes=dict(front_pipe_mouth_mm=list(A.toTuple()),front_axis=[0,0,1],unassigned_pump_adapter_mouth_mm=list((A+Z*22).toTuple()),rear_pipe_mouth_mm=list(D.toTuple()),rear_axis=list(end_axis.toTuple()),unassigned_rear_cuff_mouth_mm=list((D+end_axis*30).toTuple()),branch_root_mm=list(Q.toTuple()),branch_mouth_mm=list(E.toTuple()),branch_axis=[0,1,0],heater_untraced_boundary_mm=list(H0.toTuple()),estimated_hose_bezier_points_mm=[list(p.toTuple()) for p in [H0,H0+cq.Vector(45,0,10),H1+Y*35,H1]],hose_branch_end_mm=list(H2.toTuple()),mount_tab_hole_center_mm=list(hole.toTuple()),mount_tab_bore_radius_mm=3.5,main_bore_radius_mm=6.5,branch_mouth_bore_radius_mm=9.5),source_neighbor_contacts_mm3=contacts,limits=limits)
assembly=cq.Assembly(name='estimated_branched_heater_return_and_open_connection_boundaries');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=shape.tessellate(.5,.16);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/'heater-return-reference.step'));meta['parts']=meshes;(OUT/'heater-return-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
r=cq.importers.importStep(str(OUT/'heater-return-reference.step')).val();assert r.isValid();print(json.dumps(dict(source_parts=len(parts),solids=len(r.Solids()),step_valid=True,three_pipe_ports_open=True,opposite_wall_retained=True,local_pipe_hose_adapter_nonintersection=True,source_contacts=contacts)))
