"""Master exterior/mounting comparisons. No fitted bore or hydraulic circuit claim."""
from pathlib import Path
import json,math
import cadquery as cq
from master_mount_layout import mount_axes, metadata as mount_metadata, ESTIMATED_FIXING_CENTERS_MM, ESTIMATED_ANGLE_DEG
OUT=Path(__file__).parent;S=2032/2.4;parts=[];nodes={};X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1)
def P(x,y,z):return cq.Vector(x*S,y*S,z*S)
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 comp(ss):return cq.Compound.makeCompound(ss)
def add(n,s,c):
 assert s.isValid() and s.Volume()>0,n
 parts.append((n,s,c));return s
for label,ux,pale in [('pale',.43,True),('black',.59,False)]:
 x=ux*S;y=.86*S;z=(.305 if pale else .29)*S;length=(.25 if pale else .22)*S;r=18 if pale else 16
 face=P(ux,.86,.19);axes=[cq.Vector(*a) for a in mount_axes(face.toTuple())]
 body=cyl(r,length,cq.Vector(x,y,z-length/2),Z)
 flange=cq.Workplane('XY').slot2D(ESTIMATED_FIXING_CENTERS_MM+32,32,ESTIMATED_ANGLE_DEG).extrude(.016*S).translate((x,y,face.z-.008*S)).val()
 for q in axes:flange=flange.cut(cyl(4.5,24,q-Z*12,Z))
 # Open rear construction pocket only. This is NOT the hydraulic cylinder bore.
 pocket=cyl(12,70,P(ux,.86,.176)-Z*2,Z)
 neck=cyl(12 if pale else 10,10 if pale else 7,P(ux,.875,.32 if pale else .29),Y)
 body=body.fuse(flange).fuse(neck).cut(pocket).clean()
 add('master_'+label+'_external_body_mount_flange_and_rear_pocket',body,(.38,.41,.37))
 # Owner photos support smaller tank/cap proportions beside the coil and
 # heater, plus a broad pale reservoir foot. No photo supplies a metric scale.
 # These rounded dimensions replace coarse body-unit envelopes as estimates.
 rz=(.32 if pale else .29)*S
 base_y=.875*S+(10 if pale else 7)
 rh=62 if pale else 48;rb=26 if pale else 21;rt=36 if pale else 21
 base=cq.Vector(x,base_y,rz)
 reservoir=cq.Solid.makeCone(rb,rt,rh,base,Y) if pale else cyl(rb,rh,base,Y)
 inner=cq.Solid.makeCone(rb-2,rt-2,rh,base+Y*2,Y) if pale else cyl(rb-2,rh,base+Y*2,Y)
 reservoir=reservoir.cut(inner)
 foot_axes=[]
 if pale:
  # Factory p164 separates two tank attachment roles. The broad foot is
  # visible in IMG_6962; this dog-bone outline/map is an unscaled comparison.
  foot=box(44,5,56,cq.Vector(x,base_y+2.5,rz))
  for dz in [-28,28]:foot=foot.fuse(cyl(22,5,cq.Vector(x,base_y,rz+dz),Y))
  # Preserve the estimated tank cavity rather than filling its floor/volume.
  foot=foot.cut(cyl(rb-2,6,cq.Vector(x,base_y+2,rz),Y))
  for dz in [-40,40]:
   q=cq.Vector(x,base_y,rz+dz);foot_axes.append(list(q.toTuple()));foot=foot.cut(cyl(3.5,7,q-Y,Y))
  reservoir=reservoir.fuse(foot).clean()
 add('master_'+label+'_hollow_supply_tank_envelope',reservoir,(.76,.76,.62))
 cap_r=40 if pale else 25;cap_h=12 if pale else 10;bottom=base_y+rh-1
 cap=cyl(cap_r,cap_h,cq.Vector(x,bottom,rz),Y).cut(cyl(rt+.6,cap_h-2,cq.Vector(x,bottom-1,rz),Y))
 add('master_'+label+'_separate_cap_open_skirt_envelope',cap,(.79,.78,.64) if pale else (.14,.17,.15))
 # Same axial boot station and open center; outside taper now follows the
 # reduced casting section as an uncalibrated envelope, not a rubber fit.
 bootstart=P(ux,.86,.1125);bootlen=.065*S
 boot=cq.Solid.makeCone(18 if pale else 16,15 if pale else 14,bootlen,bootstart,Z).cut(cyl(8,bootlen+2,bootstart-Z,Z))
 add('master_'+label+'_hollow_dust_boot_comparison',boot,(.13,.16,.13))
 fix=[]
 for q in axes:
  start=q-Z*17;shaft=cyl(4,32,start,Z);head=cq.Workplane(cq.Plane(origin=start-Z*5,normal=Z)).polygon(6,15).extrude(5).val();wash=ring(8,4.2,1.2,start-Z*1.2,Z);nut=cq.Workplane(cq.Plane(origin=q+Z*8,normal=Z)).polygon(6,15).extrude(6).val().cut(cyl(4.2,10,q+Z*6,Z));fix.extend([shaft,head,wash,nut])
 add('master_'+label+'_two_mount_screw_retainer_envelopes',comp(fix),(.52,.54,.47))
 # Independent hollow local outlet roles preserve the old network endpoint stations.
 # They stop at the exterior; no hidden valve/piston/fluid passage is inferred.
 outlets=[];ends=[]
 if pale:
  for uz in [.29,.39]:
   end=P(ux-.065,.86,uz);outlets.append(ring(10,3,45,end,X));ends.append(list(end.toTuple()))
 else:
  start=P(ux,.86,.40);end=P(ux,.90,.44);axis=(end-start).normalized();outlets.append(ring(10,3,(end-start).Length,start,axis));ends.append(list(end.toTuple()))
 add('master_'+label+'_open_local_outlet_adapter_comparisons',comp(outlets),(.50,.50,.43))
 for q in axes:assert body.intersect(cyl(4.3,24,q-Z*12,Z)).Volume()<1e-5
 assert body.intersect(cyl(11.8,60,P(ux,.86,.176)-Z,Z)).Volume()<1e-5
 assert boot.intersect(cyl(7.8,bootlen+4,bootstart-Z*2,Z)).Volume()<1e-5
 nodes[label]=dict(mount_axes_body_mm=[list(q.toTuple()) for q in axes],rear_pocket_mouth_body_mm=list(P(ux,.86,.176).toTuple()),reservoir_mouth_body_mm=[x,base_y+rh,rz],reservoir_mouth_outer_radius_mm=rt,boot_start_body_mm=list(bootstart.toTuple()),boot_length_mm=bootlen,outlet_open_boundaries_body_mm=ends,estimated_reservoir_foot_axes_body_mm=foot_axes,estimated_reservoir_height_mm=rh,estimated_reservoir_base_y_mm=base_y,estimated_cap_outside_diameter_mm=2*cap_r,estimated_cap_top_y_mm=bottom+cap_h,estimated_cylinder_outside_diameter_mm=2*r)
# Evaluate only new master groups against the maintained mechanical pedal reference.
pedal=cq.Assembly.importStep(str(OUT/'pedal-support-reference.step'));contacts={}
for n,s,c in parts:
 if 'screw_retainer' in n:continue
 for k in ['pedal_open_thin_box_face_base_and_top_rim_comparison','pedal_separate_cover_envelope_comparison','pedal_brake_forked_pushrod_comparison','pedal_clutch_forked_pushrod_comparison']:
  o=pedal.objects[k].obj;o=o.val() if isinstance(o,cq.Workplane) else o;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[n+'__'+k]=v
limits='No new direct owner dimensions, fitted cylinder/tank/cap identity, hydraulic bore or actual circuit assignment is applied. Owner IMG_6855/6960/6961/6962 establish pale LOCKHEED/black warning caps, reservoir/cylinder surroundings and local visible unions, not a calibrated flange map or complete pipe destination. Factory p154 clutch and p164 dual-line brake supply architecture comparisons only; clutch change points and earlier/later variants remain unselected. 21K8564/ACB9641 are hydraulic adapter roles, not mounting brackets. Master axes, axial stations, shared estimated inclined flange-hole map and independent network endpoints retain their estimated coordinates. Owner IMG_6855/6960/6961/6962/6973/7040 support relative visible proportions and the pale broad foot, not metric dimensions. New estimated cylinder outside diameters36/32 mm, cap outside diameters80/50 mm, reservoir heights62/48 mm and base/neck/taper/foot sections replace oversized envelopes. The 50 mm coil can used for qualitative comparison is itself a catalog size comparison, not an owner scale calibration. Old cap diameters130.3867/86.36 mm and cylinder diameters67.7333/55.88 mm are superseded. The earlier50/44 mm-wide rectangular flanges are superseded by an estimated rounded32 mm-wide strip along the shared inclined hole row; inherited80 mm separation and estimated45-degree clocking are not donor measurements. Reduced boot sections remain estimates; shared rows and unchanged input stations do not establish fit. Pale foot outline, two open estimated3.5 mm-radius holes and absence of fitted tank fasteners do not identify a reservoir or reproduce loaded attachment. New 2 mm tank wall/bottom, cap skirt, rear 12 mm-radius construction pocket, 8 mm-radius boot passage, mounting-hole rows at shared estimated inclined offsets, 4.5 mm-radius bores, all screw/head/nut/washer geometry and outlet adapter dimensions are estimates. The rear pocket is not the hydraulic cylinder bore; it does not reconstruct pistons, seals, springs, compensation ports, valve paths or reservoir feed. Local 3 mm-radius outlet passages stop at the body exterior; no internal circuit/flare/thread/valve seat is inferred. Mounting flange-to-box gap remains construction, not a fitted spacer or loaded joint. Hardware quantities/roles are comparisons, not a verified fitted thread/retention map. Reservoir-to-body feed and loaded sealing, cap vent/diaphragm, rubber compatibility, fluid identity, pressure/stroke/free play, safe adjustment/bleeding, body access and verified release/replacement/removal paths remain unknown. Source contacts are reconstruction discrepancies, not installed interference findings. Display separation is illustrative.'
meta=dict(units='mm',source_frame='Whole-car body mm frame +X vehicle-left +Y up +Z front.',owner_photos=['IMG_6855','IMG_6960','IMG_6961','IMG_6962','IMG_6973','IMG_7040'],owner_dimensions_applied=False,factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=154','https://www.spridgets.de/downloads/Parts-1977.pdf#page=164'],basis='Owner photos support smaller tank/cap/casting proportions beside coil/heater and a broad pale reservoir foot. Revised sections are uncalibrated photo estimates; master input axes and network endpoints retain prior construction stations, while the master/box fixing rows now share donor-informed inclined reference clocking. Factory body/tank/cap/boot/flange/outlet roles remain comparisons. Pale-brake/black-clutch pairing is provisional, not traced circuit identification.',master_mount_reference=mount_metadata(),registration_status='unregistered_master_exterior_mounting_comparison_no_circuit_identification',nodes=nodes,source_neighbor_contacts_mm3=contacts,limits=limits)
a=cq.Assembly(name='unregistered_master_exterior_mount_and_local_outlet_comparisons');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/'master-connections-reference.step'));meta['parts']=meshes;(OUT/'master-connections-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n');s=cq.importers.importStep(str(OUT/'master-connections-reference.step')).val();assert s.isValid() and all(v.isValid() and v.Volume(1e-9)>0 for v in s.Solids());assert math.isclose(s.Volume(1e-9),sum(v.Volume(1e-9) for _,v,_ in parts),rel_tol=1e-7);print(json.dumps(dict(meshes=len(parts),solids=len(s.Solids()),step_valid=True,step_volume_roundtrip=True,open_mount_bores_rear_pockets_and_boots=True,source_contacts=contacts)))
