"""Pedal support/pivot/clevis comparison in body millimeters. Not a fitted linkage."""
from pathlib import Path
import json,math
import cadquery as cq
from master_mount_layout import mount_axes, metadata as mount_metadata
from front_wing_geometry import absolute_tessellation
OUT=Path(__file__).parent;S=2032/2.4;parts=[];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=(.35,.39,.35)):
 assert s.isValid() and s.Volume()>0,n
 parts.append((n,s,c));return s
def frame(w,d,gauge,thickness,center):return box(w,thickness,d,center).cut(box(w-2*gauge,thickness+2,d-2*gauge,center))
lo,hi=.34*S,.67*S;cx=(lo+hi)/2;front=.176*S;rear=-.13*S;bottom=.73*S;top=.941*S
wall=2.;pivot_y=.78*S;pivot_z=-.06*S
# Factory p163 and supplier-documented1976 donor RW18356 show a formed
# housing, forward channel extensions and a thin separate cover. Profiles and
# coordinates below are unscaled estimates, not the fitted owner's casting.
rear_top=755.;slope=(top-rear_top)/(front-rear)
def roof_y(z):return rear_top+slope*(z-rear)
def roof_panel(x0,x1,z0,z1,y_offset,thickness):
 pts=[cq.Vector(x,roof_y(z)+y_offset,z) for x,z in [(x0,z0),(x1,z0),(x1,z1),(x0,z1)]]
 return cq.Solid.extrudeLinear(cq.Wire.makePolygon(pts+[pts[0]]),[],Y*thickness)
def roof_frame(x0,x1,z0,z1,rim,y_offset,thickness):
 return roof_panel(x0,x1,z0,z1,y_offset,thickness).cut(roof_panel(x0+rim,x1-rim,z0+rim,z1-rim,y_offset-1,thickness+2))
sideparts=[cq.Workplane('YZ',origin=(x-wall/2,0,0)).polyline([(bottom,rear),(bottom,front),(top,front),(rear_top,rear)]).close().extrude(wall).val() for x in [lo,hi]]
face=box(hi-lo,top-bottom,wall,cq.Vector(cx,(top+bottom)/2,front))
for x in [.43*S,.59*S]:face=face.cut(cyl(.030*S,10,cq.Vector(x,.86*S,front-5),Z))
# Two estimated master flange axes per cylinder; independent master source shares them.
for x in [.43*S,.59*S]:
 for a in mount_axes((x,.86*S,front)):
  face=face.cut(cyl(4.5,10,cq.Vector(*a)-Z*5,Z))
base=frame(hi-lo+26,front-rear+26,14,2,cq.Vector(cx,bottom,(front+rear)/2))
# Eight body fixing roles, factory count; coordinates are a simple estimated map.
base_axes=[cq.Vector(x,bottom,z) for x in [lo-6,hi+6] for z in [rear+8,rear+70,front-70,front-8]]
for q in base_axes:base=base.cut(cyl(3.5,8,q-Y*4,Y))
rim=roof_frame(lo-5,hi+5,rear,front,10,-2,2)
# Source-supported folded extensions. Length, section and two unassigned
# end-hole roles remain estimated; no body receivers or loaded seats inferred.
# WH part3 PDF31, printed135 USA33.30.02: cover leaves with coil.
# Existing coil foot axes are a second unsurveyed drawing datum. This conditional
# support connects those reference roles; it does not calibrate either assembly.
BODY=json.loads((OUT.parent/'body-data.json').read_text());ENGINE=BODY['native_engine_placement'];ET=[ENGINE['body_anchor_units'][i]*S-ENGINE['native_anchor_mm'][i] for i in range(3)]
import build_engine as engine_builder
coil_axes=[cq.Vector(*(q[i]+ET[i] for i in range(3))) for q in engine_builder.ignition_coil_mount_axes()]
coil_foot_bottom=coil_axes[0].y-2.5
nose_z0=min(q.z for q in coil_axes)-42.;nose_z1=max(q.z for q in coil_axes)+18.
# Keep a4mm unresolved foot/cover gap rather than prescribing a fitted seat.
nose_top=coil_foot_bottom-4.;nose_bottom=nose_top-2.
rail_length=nose_z1-front;rail_end_axes=[];rails=[]
def sheet(points,thickness):
 pts=[cq.Vector(*p) for p in points]
 return cq.Solid.extrudeLinear(cq.Wire.makePolygon(pts+[pts[0]]),[],Y*thickness)
def forward_strip(x0,x1,thickness):
 ramp=sheet([(x0,bottom-1,front),(x1,bottom-1,front),(x1,nose_bottom-2,nose_z0),(x0,nose_bottom-2,nose_z0)],thickness)
 flat=box(x1-x0,thickness,nose_z1-nose_z0,cq.Vector((x0+x1)/2,nose_bottom-2+thickness/2,(nose_z0+nose_z1)/2))
 return ramp.fuse(flat)
for x in [lo-6,hi+6]:
 if x<cx:
  # The existing inboard rail stops before an unresolved column crossing.
  # Preserve its original source shape rather than inventing a steering notch.
  old_length=165.;q=cq.Vector(x,bottom,front+old_length-14)
  flange=box(22,2,old_length,cq.Vector(x,bottom,front+old_length/2))
  lip=box(2,12,old_length,cq.Vector(x-11,bottom+5,front+old_length/2))
  rails.extend([flange.cut(cyl(3.5,8,q-Y*4,Y)),lip]);rail_end_axes.append(q)
 else:
  flange=forward_strip(x-11,x+11,2);lip=forward_strip(x+10,x+12,12)
  q=cq.Vector(x,nose_bottom-1,nose_z1-12);rail_end_axes.append(q)
  rails.extend([flange.cut(cyl(3.5,18,q-Y*6,Y)),lip.cut(cyl(3.5,18,q-Y*6,Y))])

housing=comp([*sideparts,face,base,rim,*rails])
for x in [lo,hi]:housing=housing.cut(cyl(6.2,12,cq.Vector(x-6,pivot_y,pivot_z),X))
add('pedal_open_thin_box_face_base_and_top_rim_comparison',housing)
# A2 mm projected-gauge skin replaces the old15.24 mm solid lid. Four thin
# edge returns follow the sloping reference roof with an estimated seal gap.
cover=roof_panel(lo-8,hi+8,rear-5,front+5,.8,2)
for x0,x1 in [(lo-8,lo-6),(hi+6,hi+8)]:
 cover=cover.fuse(roof_panel(x0,x1,rear-5,front+5,-5.2,8))
for z0,z1 in [(rear-5,rear-3),(front+3,front+5)]:
 cover=cover.fuse(roof_panel(lo-8,hi+8,z0,z1,-5.2,8))
# Open descending side bands keep the master-cylinder region visible. The
# projected2mm gauge, edges and ramp shape are construction estimates.
join_z=front+4
for x0,x1 in [(lo-8,lo+12),(hi-12,hi+8)]:
 cover=cover.fuse(sheet([(x0,roof_y(join_z)+.8,join_z),(x1,roof_y(join_z)+.8,join_z),(x1,nose_bottom,nose_z0+1),(x0,nose_bottom,nose_z0+1)],2))
# A broad low front pan provides the missing coil-adjacent support envelope.
# Preserve a deliberate4mm drawing gap below the feet, with shared plain bores.
nose=box(hi-lo+16,2,nose_z1-nose_z0,cq.Vector(cx,nose_bottom+1,(nose_z0+nose_z1)/2))
for q in coil_axes:nose=nose.cut(cyl(3.5,10,cq.Vector(q.x,nose_bottom-4,q.z),Y))
cover=cover.fuse(nose)
cover=cover.clean()
add('pedal_separate_cover_envelope_comparison',cover)
seal=roof_frame(lo-4,hi+4,rear+1,front-1,8,0,.8)
add('pedal_uncompressed_upper_cover_seal_comparison',seal,(.15,.18,.15))
assert cover.intersect(housing).Volume()<1e-4
assert cover.intersect(seal).Volume()<1e-4
lowseal=frame(hi-lo+26,front-rear+26,14,.8,cq.Vector(cx,bottom-1.4,(front+rear)/2))
for q in base_axes:lowseal=lowseal.cut(cyl(3.5,5,q-Y*4,Y))
add('pedal_uncompressed_body_gasket_comparison',lowseal,(.15,.18,.15))
bushes=[];pins=[];retainers=[];nodes={}
for label,x in [('brake',.43*S),('clutch',.59*S)]:
 q=cq.Vector(x,pivot_y,pivot_z);e=cq.Vector(x,.86*S,pivot_z)
 hub=ring(18,11.8,18,q-X*9,X)
 # A continuous pendant web follows the retained old hub/pad stations.
 shape=cq.Workplane('YZ',origin=(x-3,0,0)).polyline([(pivot_y+6,pivot_z-7),(.59*S+10,-.18*S-8),(.49*S+8,-.27*S-8),(.49*S-8,-.27*S+8),(.59*S-10,-.18*S+8),(pivot_y-6,pivot_z+7)]).close().extrude(6).val()
 upper=box(6,e.y-q.y,14,(q+e)*.5);eye=ring(11,4.1,6,e-X*3,X)
 pedal=hub.fuse(shape).fuse(upper).fuse(eye).cut(cyl(11.8,22,q-X*11,X)).cut(cyl(4.1,10,e-X*5,X)).clean()
 add('pedal_'+label+'_bored_hub_arm_and_upper_eye_comparison',pedal,(.42,.44,.40))
 pad=box(.075*S,.085*S,.024*S,P(x/S,.49,-.27)).rotate(P(x/S,.49,-.27).toTuple(),(P(x/S,.49,-.27)+X).toTuple(),math.degrees(.20));add('pedal_'+label+'_rubber_pad_envelope',pad,(.13,.16,.13))
 bushes.append(ring(11.7,6.0,18,q-X*9,X))
 # Clevis slot surrounds a 6 mm pedal web with an estimated 10 mm clear gap.
 cheeks=[box(4,22,31,e+X*s*7+Z*5) for s in [-1,1]];bridge=box(18,22,4,e+Z*22)
 rod=cyl(4.5,.19*S-(e.z+22),e+Z*22,Z)
 clevis=comp([*cheeks,bridge,rod]).cut(cyl(4.1,26,e-X*13,X))
 add('pedal_'+label+'_forked_pushrod_comparison',clevis,(.52,.53,.47))
 pin=cyl(3.96875,22.225,e-X*10,X).fuse(cyl(6,2,e-X*12,X));pin=pin.cut(cyl(1,12,e+X*10-Y*6,Y));pins.append(pin)
 retainers.append(ring(6.5,4.1,1.2,e+X*9.1,X))
 # Small transverse retention envelope, not a bent/installed split-pin fit.
 retainers.append(cyl(.8,12,e+X*10-Y*6,Y))
 assert pedal.intersect(cyl(11.6,22,q-X*11,X)).Volume()<1e-5
 assert pedal.intersect(cyl(3.9,12,e-X*6,X)).Volume()<1e-5
 assert clevis.intersect(cyl(3.9,26,e-X*13,X)).Volume()<1e-5
 assert pedal.intersect(clevis).Volume()<1e-4
 nodes[label]=dict(pivot_center_body_mm=list(q.toTuple()),clevis_center_body_mm=list(e.toTuple()),pad_center_body_mm=list(P(x/S,.49,-.27).toTuple()),pushrod_front_open_boundary_mm=[x,e.y,.19*S])
add('pedal_two_separate_pivot_bush_comparisons',comp(bushes),(.55,.48,.34))
shaft=cyl(5.8,hi-lo+24,cq.Vector(lo-12,pivot_y,pivot_z),X);head=cq.Workplane(cq.Plane(origin=(lo-16,pivot_y,pivot_z),normal=X)).polygon(6,20).extrude(4).val()
add('pedal_shared_pivot_bolt_envelope',comp([shaft,head]),(.52,.54,.47))
q=cq.Vector(hi+5,pivot_y,pivot_z);nut=cq.Workplane(cq.Plane(origin=q+X*2,normal=X)).polygon(6,20).extrude(6).val().cut(cyl(6.1,10,q,X));wash=ring(11,6.1,1.5,q,X)
add('pedal_shared_pivot_retainer_envelopes',comp([nut,wash]),(.52,.54,.47))
add('pedal_two_clevis_pin_comparisons',comp(pins),(.52,.54,.47));add('pedal_two_clevis_retention_envelopes',comp(retainers),(.52,.54,.47))
limits='No new direct owner measurements, fitted pedal-box/pedal/master-cylinder identity or hydraulic circuit assignment is applied. Factory p163 CHA375/CHA333 and shared AHA8075/bush/return-spring roles are comparisons; p154 master change points and p164 dual-line brake internals do not identify this installation. IMG_6855/6960/6961/6962 establish reservoir/cylinder and adjacent equipment context, not hidden pedal-box dimensions or pipe destinations. Old body-frame pivot, clevis, pad and cylinder-entry stations remain drawing datums. Factory p163 and RW18356 seller photographs (documented1976 donor) support folded forward rails, shaped sidewalls, open mouth/rim and a thin separate formed cover. The donor does not identify this car or calibrate any dimensions. The revised sloping rear roof atY755 mm, retained front roofY796.7133 mm,165 mm rail extensions,2 mm projected cover gauge,6 mm edge returns and two estimated unassigned rail-end holes replace the old crate/lid forms. Original main base, master input axes, pivot and pedal/rod coordinates retain their reference stations. Inclined master-hole rows supersede the vertical assumption using a shared uncalibrated45-degree map with the inherited80 mm separation. Donor correspondence, actual dimensions/clocking and fitted map remain unverified. Box outline/gauge, base/rim, seals, cover, all holes, arm/hub/clevis sections, shaft/bush fits, retention and thread geometry remain estimates. New 2 mm box sheet replaces thick drawing walls; new 6 mm pedal webs and reduced pivot/rod sections are construction choices, not fitted dimensions. CLZ513 nominal 5/16 x 7/8 pin dimensions are supplier comparisons only; all other pin/bolt/head/hole/washer dimensions are estimates. Eight retained base-hole roles and the two new unassigned rail-end holes are a construction comparison, not a verified fitted hole count/map; body counterpart holes, attachment layers, welded ownership and base support remain unassigned. The cover underside rises 0.8 mm for an uncompressed estimated seal; actual cover fasteners/locking remain unmodeled. No return springs, adjustment/stop/switch, loaded seals, master piston/bore internals, pedal travel, free play, force ratio, safe adjustment, hydraulic fluid/pressure/circuit, verified tool access, release sequence or replacement/removal path. Master groups keep their provisional reservoir/cylinder/flange/boot and outlet geometry; duplicated old rods/clevises are replaced without proving internal engagement. Static separation is illustrative.'
meta=dict(master_mount_axes_body_mm=[a for x in [.43*S,.59*S] for a in mount_axes((x,.86*S,front))],master_mount_reference=mount_metadata(),units='mm',source_frame='Whole-car body mm frame +X vehicle-left +Y up +Z front; no engine-frame registration.',owner_photos=['IMG_6855','IMG_6960','IMG_6961','IMG_6962'],owner_dimensions_applied=False,factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=163','https://www.spridgets.de/downloads/Parts-1977.pdf#page=154','https://www.spridgets.de/downloads/Parts-1977.pdf#page=164'],supplier_source='https://www.msc.parts/contents/en-uk/d389_1275cc_-_Brake_Pedal_Box.html',donor_photo_source='https://rwautoclassics.com/viewitem/?id=18356',housing_reference=dict(estimated_rear_roof_y_mm=rear_top,retained_front_roof_y_mm=top,estimated_forward_rail_length_mm=rail_length,estimated_rail_end_hole_axes_body_mm=[list(q.toTuple()) for q in rail_end_axes],estimated_cover_projected_gauge_mm=2,estimated_cover_return_depth_mm=6,estimated_seal_projected_gap_mm=.8,master_fixing_pattern_status='shared_inclined_reference_row_donor_informed_unregistered'),basis='Factory p163 plus documented1976 donor RW18356 photographs support a formed housing, forward folded rails and a thin separate cover. Unscaled reference sections and sloping roof replace the old crate/lid. Master-face, base, pivot, clevis and pedal coordinates remain construction datums; fitted identity, donor hole correspondence and installed registration are unverified.',registration_status='unregistered_mechanical_relationship_comparison_no_hydraulic_assignment',nodes=nodes,base_fixing_axes_body_mm=[list(q.toTuple()) for q in base_axes],limits=limits)
meta['coil_cover_reference']=dict(factory_source='https://www.spridgets.de/downloads/Midget%201500%20WH%20Teil%203.pdf#page=31',printed_page=135,procedure='USA33.30.02 step4 removes the cover complete with ignition coil after its listed disconnections; this is a factory sequence reference, not a verified path on this car.',owner_photos=['IMG_6855','IMG_7040'],owner_dimensions_applied=False,coil_axes_body_mm=[list(q.toTuple()) for q in coil_axes],estimated_front_pan_z_bounds_mm=[nose_z0,nose_z1],estimated_front_pan_y_bounds_mm=[nose_bottom,nose_top],foot_to_pan_drawing_gap_mm=4.,inboard_rail_retained_length_mm=165.,inboard_forward_support_status="Unresolved column crossing; original inboard rail retained rather than extended through it.",estimated_forward_rail_length_mm=rail_length,limits='Conditional support envelope under retained estimated coil and pedal datums. All new spans, rise, contours, wall gauges and shared bore coordinates are estimates; the4mm foot gap is an unresolved construction relationship, not a spacer specification or fitted clearance. The inboard rail keeps its original165mm end because extending it crosses the unregistered column shaft. No fitted notch or detour is invented. Retained short screw cues do not establish pan engagement. No forced contact, support strength, hidden body counterpart, installed mounting map or verified removal path. Coil/cover share illustrative separation only; leads and pipes are not flexible operating connections.')
meta['coil_cover_reference']['limits']+=' The scoped export audit also finds positive source volumes against left wheelhouse panels. Those crossings and existing column contacts are unresolved registration conflicts, not loaded support or fitted clearance.'
meta['factory_sources'].append(meta['coil_cover_reference']['factory_source'])
meta['basis']+=' Factory USA workshop33.30.02 p135 also establishes cover/coil service ownership. A low forward pan and extended folded channels replace the unsupported short front projection, conditionally using the retained unsurveyed coil-foot axes.'
meta['limits']+=' The former165mm outboard rail end and short cover are superseded by the conditional cover/coil support reference. '+meta['coil_cover_reference']['limits']
meta['housing_reference']['estimated_forward_rail_length_mm']=rail_length
meta['housing_reference']['estimated_rail_end_hole_axes_body_mm']=[list(q.toTuple()) for q in rail_end_axes]
a=cq.Assembly(name='estimated_pedal_box_pivot_and_clevis_relationships');meshes=[]
for n,s,c in parts:
 a.add(s,name=n,color=cq.Color(*c));vs,ts=absolute_tessellation(s,.08,.12) if n in ('pedal_open_thin_box_face_base_and_top_rim_comparison','pedal_separate_cover_envelope_comparison') else 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/'pedal-support-reference.step'));meta['parts']=meshes;(OUT/'pedal-support-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n');s=cq.importers.importStep(str(OUT/'pedal-support-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,shared_pivot_clevis_bores=True,pedal_clevis_nonintersection=True)))
