"""Front opposed-caliper, pads, retention and mounting comparisons. Not fitted brakes."""
from pathlib import Path
import json,math
import cadquery as cq
OUT=Path(__file__).parent;BODY=json.loads((OUT.parent/'body-data.json').read_text());S=2032/2.4;F=BODY['front_hub_reference'];C=F['disc_center_mm'];W=BODY['published_reference_dimensions_mm']['front_track_steel_wheels']/2
X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1);parts=[];nodes={};contacts={}
def cyl(r,l,p,a=X):return cq.Solid.makeCylinder(r,l,p,a)
def ring(ro,ri,l,p,a=X):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=(.40,.43,.39)):
 assert s.isValid() and s.Volume()>0,n
 parts.append((n,s,c));return s
local=[]
def put(n,s,c=(.40,.43,.39)):assert s.isValid() and s.Volume()>0,n;local.append((n,s,c))
mounts=[cq.Vector(C-38,yy,-76) for yy in [-27,27]];pinaxes=[cq.Vector(C,yy,-108) for yy in [-23,23]]
# Retain old principal side/bridge stations, add open pockets and a pad-access window.
body=box(66,75,16,cq.Vector(C,0,-117))
for sign in [-1,1]:
 q=cq.Vector(C+sign*23,0,-90);body=body.fuse(box(20,75,50,q))
 body=body.fuse(cyl(22,20,cq.Vector(C+sign*13,0,-84),X*sign))
for q in mounts:body=body.fuse(box(12,18,24,q))
body=body.cut(box(32,52,22,cq.Vector(C,0,-113)))
for sign in [-1,1]:
 q=cq.Vector(C+sign*13,0,-84);body=body.cut(cyl(18,15,q-X*sign*.1,X*sign)).cut(cyl(19.1,1.6,q-X*sign*.1,X*sign))
for q in mounts:body=body.cut(cyl(4.5,44,q-X*22))
for q in pinaxes:body=body.cut(cyl(2.2,84,q-X*42))
# External inlet/bleeder seats only; no hidden pressure passages or bridge seal modeled.
inlet=cq.Vector(C-33,20,-90);bleed=cq.Vector(C-27,34,-78)
body=body.cut(cyl(3.7,12,inlet-X*2)).cut(cyl(2.5,10,bleed-Y*2,Y)).clean()
put('opposed_caliper_exterior_blind_pockets_and_mount_bores',body)
pistons=[];seals=[]
for sign in [-1,1]:
 q=cq.Vector(C+sign*12.5,0,-84);a=X*sign
 cup=cyl(17.5,12,q,a).cut(cyl(14.5,11,q+a*3,a));pistons.append(cup)
 seals.append(ring(19,17.6,1.2,cq.Vector(C+sign*13,0,-84),a))
put('two_opposed_piston_cup_envelopes',comp(pistons),(.56,.57,.50));put('two_unloaded_dust_seal_envelopes',comp(seals),(.13,.16,.13))
shims=[]
for sign,label in [(-1,'inboard'),(1,'outboard')]:
 half=F['rotor_comparison_mm']['thickness']/2;gap=F['pad_display_gap_mm'];friction=box(3,60,28,cq.Vector(C+sign*(half+gap+1.5),0,-81))
 backing=box(3,64,32,cq.Vector(C+sign*(half+gap+4.5),0,-81))
 for yy in [-23,23]:
  backing=backing.fuse(box(3,10,16,cq.Vector(C+sign*(half+gap+4.5),yy,-103))).fuse(cyl(5,3,cq.Vector(C+sign*(half+gap+4.5)-1.5,yy,-108)))
  backing=backing.cut(cyl(2.2,9,cq.Vector(C+sign*(half+gap+4.5)-4.5,yy,-108)))
 put(label+'_pad_friction_envelope',friction,(.14,.16,.13));put(label+'_pad_backing_and_bored_retention_ears',backing,(.53,.55,.49))
 shims.append(box(.5,58,27,cq.Vector(C+sign*(half+gap+6.25),0,-81)))
put('two_unloaded_anti_squeal_shim_envelopes',comp(shims),(.58,.58,.48))
# The spring sits outside the rotor envelope, within the new open bridge window.
spring=cq.Workplane('YZ',origin=(C-9,0,0)).polyline([(-25,-108),(-19,-115),(19,-115),(25,-108),(24.3,-107.6),(18.6,-114.2),(-18.6,-114.2),(-24.3,-107.6)]).close().extrude(18).val()
put('unloaded_pad_retaining_spring_comparison',spring,(.52,.54,.47))
pins=[]
for q in pinaxes:pins.extend([cyl(1.6,74,q-X*37),ring(4,2,1.5,q-X*39)])
put('two_split_pin_stem_and_loop_envelopes',comp(pins),(.54,.56,.48))
fix=[]
for q in mounts:
 head=cq.Workplane(cq.Plane(origin=q-X*25,normal=X)).polygon(6,14).extrude(5).val();fix.extend([cyl(4,38,q-X*20),head,ring(8,4.2,1.2,q-X*20)])
put('two_caliper_mounting_screw_envelopes',comp(fix),(.54,.56,.48))
lock=box(1.2,72,20,cq.Vector(C-47,0,-76))
for q in mounts:lock=lock.cut(cyl(4.5,12,cq.Vector(C-52,q.y,q.z)))
put('flat_two_hole_mounting_lock_tab_comparison',lock,(.54,.56,.48))
bleeder=ring(5.5,2,12,bleed,Y).fuse(cq.Workplane(cq.Plane(origin=bleed+Y*4,normal=Y)).polygon(6,11).extrude(4).val().cut(cyl(2,10,bleed,Y)))
put('open_bleed_screw_exterior_comparison',bleeder,(.54,.56,.48))
# Banjo roles from factory p167; fitted hose variant and existing route splice unassigned.
eye=inlet-X*5;banjo=ring(7,3.7,8,eye-X*4)
banjo=banjo.fuse(ring(5,2.5,12,eye+Y*5,Y))
bolt=cyl(3.5,20,inlet-X*12).cut(cyl(1.5,21,inlet-X*13))
bolt=bolt.fuse(cq.Workplane(cq.Plane(origin=inlet-X*17,normal=X)).polygon(6,12).extrude(5).val().cut(cyl(1.5,7,inlet-X*18)))
put('banjo_eye_and_unassigned_hollow_bolt_envelopes',comp([banjo,bolt]),(.54,.55,.48))
put('two_uncompressed_banjo_washer_envelopes',comp([ring(7.5,3.7,1,inlet-X*11),ring(7.5,3.7,1,inlet-X)]),(.60,.48,.32))
# Source gauges: passages and pad/rotor display gap, never fitted pressure/clearance checks.
for q in mounts:assert body.intersect(cyl(4.3,44,q-X*22)).Volume()<1e-5
for q in pinaxes:
 assert body.intersect(cyl(2.0,84,q-X*42)).Volume()<1e-5
 for n,s,c in local:
  if 'pad_backing' in n:assert s.intersect(cyl(2.0,84,q-X*42)).Volume()<1e-5
rotor=ring(F['rotor_comparison_mm']['diameter']/2,45,F['rotor_comparison_mm']['thickness'],cq.Vector(C-F['rotor_comparison_mm']['thickness']/2,0,0))
for n,s,c in local:
 if any(k in n for k in ['pad_friction','retaining_spring','split_pin','shim']):assert s.intersect(rotor).Volume()<1e-5,(n,'rotor')
king=cq.Assembly.importStep(str(OUT/'front-kingpin-reference.step'))
for sign,label in [(1,'left'),(-1,'right')]:
 def transformed(s):return (s if sign==1 else s.mirror('YZ')).translate((sign*W,.39*S,1.2*S))
 def point(q):return [sign*(W+q.x),.39*S+q.y,1.2*S+q.z]
 for n,s,c in local:add('front_'+label+'_'+n,transformed(s),c)
 ob=king.objects['front_'+label+'_bored_swivel_carrier_spindle_and_web_reference'].obj;ob=ob.val() if isinstance(ob,cq.Workplane) else ob
 for n,s,c in local:
  if 'mounting_screw' in n:continue
  vol=round(transformed(s).intersect(ob).Volume(),4)
  if vol>1e-4:contacts[label+'_'+n+'__swivel_carrier']=vol
 nodes[label]=dict(mount_axes_body_mm=[point(q) for q in mounts],pin_axes_body_mm=[point(q) for q in pinaxes],pocket_mouth_body_mm=[point(cq.Vector(C+s*13,0,-84)) for s in [-1,1]],axis_outward=[sign,0,0],caliper_inlet_source_boundary_body_mm=point(inlet),banjo_eye_center_body_mm=point(eye),retained_pad_display_gap_mm=F['pad_display_gap_mm'])
limits='No new owner dimensions or fitted caliper/pad/piston/hose identity is applied. Factory pp142/143 support handed opposed calipers, pads/shims, one spring and two split-pin roles per caliper, two mounting screws and tab-locking roles. P167 separates front hose change points at GAN6-162565, banjo bolt/gasket and GBH111-only locking hardware; fitted branch remains unselected. Existing owner underbody views give suspension/wheel context, not a calibrated disassembled caliper or hose-end map. All new body profiles, blind 18 mm-radius piston pockets, 17.5 mm-radius piston cups, seals, mounting/retention bores, pad ears, shims, spring, inlet/bleeder, banjo/bolt/washer and hardware dimensions/stations are estimates. Pockets are construction cavities, not a verified hydraulic bore; no hidden fluid channels, bridge seal, pressure rating or piston travel is reconstructed. Prior caliper/pad/rotor and upright-web endpoints remain source datums; matching 4.5 mm-radius mounting bores are added only to two carrier meshes. Pad backing grows estimated retention ears; a new bridge access window is construction, not a verified pad withdrawal path. Prior 1 mm rotor/pad display gap is not brake adjustment, running clearance or a wear setting. Springs, shims, split-pin stems/loops, lock tabs and washers are unloaded envelopes, without verified installed bending, locking, thread engagement, torque or seal compression. Banjo eye center is 5 mm inboard of the retained network endpoint; no route is moved to invent a sealed splice. Earlier GBH111 hardware is not selected for this car. The caliper is displayed as one exterior assembly; separating reference parts is not a caliper-splitting or overhaul procedure. Source contacts are reconstruction discrepancies, not actual interference findings. No verified hose travel, operating wheel/rotor clearance, full steering/suspension motion, pressure circuit, safe release/support/bleeding order or replacement/removal path. Separation is illustrative.'
meta=dict(units='mm',source_frame='Canonical body mm +X vehicle-left +Y up +Z front.',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=142','https://www.spridgets.de/downloads/Parts-1977.pdf#page=143','https://www.spridgets.de/downloads/Parts-1977.pdf#page=167'],supplier_source='https://www.mgocspares.co.uk/parts/mg/midget/brakes/front-brakes',owner_dimensions_applied=False,basis='Factory front brake/caliper/pad-retention/mounting topology and p167 hose variant roles. Existing wheel/rotor/upright source datums retained, without fitted component identification. New native geometry is explicitly estimated.',registration_status='unregistered_front_caliper_and_retention_comparisons',nodes=nodes,sampled_carrier_contacts_mm3=contacts,limits=limits)
a=cq.Assembly(name='front_caliper_mount_pad_retention_and_local_hose_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/'front-caliper-reference.step'));meta['parts']=meshes;(OUT/'front-caliper-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n');s=cq.importers.importStep(str(OUT/'front-caliper-reference.step')).val();assert s.isValid();print(json.dumps(dict(meshes=len(parts),solids=len(s.Solids()),step_valid=True,open_mount_and_retention_bores=True,rotor_pad_retention_nonintersection=True,source_contacts=contacts)))
