"""Static three-wheelbox comparison. No sweep, park angle or fitted route verified."""
from cowl_apron_layout import WIPER_ROW_Z_UNITS,WIPER_ROW_SHIFT_UNITS,metadata as apron_metadata
from glazing_alignment_layout import glazing_layout
from front_upper_profile import metadata as upper_profile_metadata
from pathlib import Path
import json,math
import cadquery as cq
from scuttle_geometry import build_scuttle
from front_wing_geometry import absolute_tessellation
OUT=Path(__file__).parent;S=json.loads((OUT.parent/'body-data.json').read_text())['published_reference_dimensions_mm']['wheelbase']/2.4
BODY=json.loads((OUT.parent/'body-data.json').read_text());GLAZING=glazing_layout(BODY)
RACK_Z=.085+WIPER_ROW_SHIFT_UNITS
N=cq.Vector(0,.8,.6);M=cq.Vector(0,.6,-.8);parts=[];nodes={};paths={}
def V(p):return cq.Vector(*(v*S for v in p))
def box(w,h,d,p):return cq.Workplane('XY').box(w*S,h*S,d*S).translate(V(p).toTuple()).val()
def cyl(r,l,p,axis=(0,0,1)):return cq.Solid.makeCylinder(r*S,l*S,V(p),cq.Vector(*axis))
def add(name,shape,color=(.43,.45,.41)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color))
def ring(ro,ri,l,p,axis=(0,0,1)):
 a=cq.Vector(*axis);q=V(p);return cq.Solid.makeCylinder(ro*S,l*S,q,a).cut(cq.Solid.makeCylinder(ri*S,(l+.002)*S,q-a*.001*S,a))
def rod(a,b,r):
 a,b=V(a),V(b);d=b-a;return cq.Solid.makeCylinder(r*S,d.Length,a,d.normalized())
def tube(name,stations,r=.006,ri=.004,bend=.010):
 pts=list(map(V,stations));edges=[];prev=pts[0]
 for a,b,c in zip(pts,pts[1:],pts[2:]):
  u=(b-a).normalized();v=(c-b).normalized();dot=max(-1,min(1,u.dot(v)));theta=math.acos(dot);assert .001<theta<math.pi-.001
  d=bend*S*math.tan(theta/2);assert d<.49*min((b-a).Length,(c-b).Length),(name,d)
  q=b-u*d;t=b+v*d;center=q+(v-u*dot).normalized()*bend*S
  mid=center+((q-center).normalized()+(t-center).normalized()).normalized()*bend*S
  edges.extend([cq.Edge.makeLine(prev,q),cq.Edge.makeThreePointArc(q,mid,t)]);prev=t
 edges.append(cq.Edge.makeLine(prev,pts[-1]));wire=cq.Wire.assembleEdges(edges)
 plane=cq.Plane(origin=pts[0],normal=(pts[1]-pts[0]).normalized());shape=cq.Workplane(plane).circle(r*S).circle(ri*S).sweep(wire,isFrenet=True).val()
 paths[name]=dict(corner_stations_units=stations,outer_radius_mm=r*S,inner_radius_mm=ri*S,bend_radius_mm=bend*S)
 return shape
pivots=[('right',-.40),('center',0),('left',.40)]
scuttle_parts,scuttle_metadata=build_scuttle(S,[[x,1.025,WIPER_ROW_Z_UNITS] for label,x in pivots],N)
for name,shape,color in scuttle_parts:add(name,shape,color)
skin=scuttle_parts[0][1]
# Owner-visible recognition only: no stamped motor identity or live circuit.
can=ring(.040,.037,.16,[-.64,.86,.195])
for z in [.192,.355]:can=can.fuse(ring(.040,.007,.003,[-.64,.86,z]))
add('wiper_motor_can_comparison',can,(.45,.47,.43))
gear=box(.10,.080,.075,[-.64,.86,.1575]).cut(box(.088,.068,.075,[-.64,.86,.1515]))
gear=gear.cut(cyl(.008,.020,[-.64,.86,.187]))
gear=gear.cut(cyl(.006,.030,[-.64,.879,.16],(0,1,0)))
add('wiper_motor_gear_housing_comparison',gear)
cover=box(.10,.080,.004,[-.64,.86,.118])
for dx,dy in [(-.041,-.030),(.041,-.030),(.041,.030)]:cover=cover.cut(cyl(.003,.008,[-.64+dx,.86+dy,.114]))
add('wiper_motor_gear_cover_comparison',cover)
# Toothless wheel/eccentric/slider recognition, no valid motion ratio asserted.
drive=cq.Compound.makeCompound([cyl(.027,.010,[-.64,.852,.150]),cyl(.005,.014,[-.652,.868,.148]),rod([-.652,.868,.155],[-.64,.890,.16],.004),box(.014,.012,.014,[-.64,.890,.16])])
add('wiper_static_drive_disc_link_comparison',drive)
strap=ring(.046,.042,.022,[-.64,.86,.264]).cut(box(.13,.080,.04,[-.64,.787,.275]))
feet=[]
for x in [-.695,-.585]:feet.append(box(.025,.005,.026,[x,.8175,.275]).cut(cyl(.0035,.010,[x,.812,.275],(0,1,0))))
packing=box(.10,.005,.12,[-.64,.8175,.275])
add('wiper_motor_strap_packing_comparison',cq.Compound.makeCompound([strap,packing,*feet]),(.28,.30,.26))
lead=rod([-.69,.85,.16],[-.72,.84,.18],.003)
add('wiper_motor_unassigned_lead_boundary',lead,(.16,.17,.14))
stations=[[-.64,.89,.16],[-.64,.94,.16],[-.56,.94,.16],[-.56,.94,RACK_Z],[-.50,.94,RACK_Z],[-.46,.968,RACK_Z],[-.4275,.968,RACK_Z]]
feed=tube('wiper_motor_to_first_wheelbox_casing_comparison',stations)
# Former guessed bulkhead crossing/grommet is not imposed on this new route.
add('wiper_motor_to_first_wheelbox_casing_comparison',feed)
inter=[]
for a,b in [(-.3725,-.0275),(.0275,.3725)]:inter.append(ring(.006,.004,b-a,[a,.968,RACK_Z],(1,0,0)))
add('wiper_inter_wheelbox_casings_comparison',cq.Compound.makeCompound(inter))
ext=ring(.006,.004,.105,[.4275,.968,RACK_Z],(1,0,0));add('wiper_open_rack_extension_comparison',ext)
for label,x in pivots:
 pivot=cq.Vector(x,1.025,WIPER_ROW_Z_UNITS);center=pivot-N*.06
 nodes[label]=dict(pivot_band_crossing_units=pivot.toTuple(),axis=N.toTuple(),wheelbox_center_units=center.toTuple(),rack_port_units=[x,.968,RACK_Z])
 plane=cq.Plane(origin=V(center.toTuple())-N*.016*S,xDir=(1,0,0),normal=N)
 outer=cq.Workplane(plane).rect(.055*S,.055*S).extrude(.032*S).val()
 voidplane=cq.Plane(origin=V(center.toTuple())-N*.017*S,xDir=(1,0,0),normal=N)
 inner=cq.Workplane(voidplane).rect(.047*S,.047*S).extrude(.030*S).val()
 housing=outer.cut(inner).cut(cyl(.008,.040,(center-N*.020).toTuple(),N.toTuple())).cut(cyl(.006,.060,[x-.03,.968,RACK_Z],(1,0,0)))
 add('wiper_'+label+'_open_wheelbox_comparison',housing)
 pinion=cq.Solid.makeCylinder(.017*S,.008*S,V(center.toTuple())-N*.004*S,N)
 shaft=cq.Solid.makeCylinder(.0065*S,.097*S,V(center.toTuple())-N*.017*S,N)
 add('wiper_'+label+'_toothless_pinion_spindle_comparison',cq.Compound.makeCompound([pinion,shaft]))
 bezel=ring(.015,.007,.008,(pivot+N*.002).toTuple(),N.toTuple())
 seal=ring(.016,.007,.003,(pivot-N*.003).toTuple(),N.toTuple())
 nut=ring(.011,.007,.006,(pivot+N*.010).toTuple(),N.toTuple())
 add('wiper_'+label+'_bezel_seal_nut_comparison',cq.Compound.makeCompound([bezel,seal,nut]))
 start=(pivot+N*.025).toTuple()
 # Reach the retained glass-height reference instead of translating the blade
 # away from the glass with the wheelbox. Static pose only, not parked travel.
 blade_y=1.105
 angle=GLAZING['rake_radians'];oy,oz=GLAZING['origin_body_mm'][1]/S,GLAZING['origin_body_mm'][2]/S
 local_y=(blade_y-oy)/math.cos(angle)
 normal=cq.Vector(0,-math.sin(angle),math.cos(angle))
 blade_center=cq.Vector(x+.105,blade_y,oz+local_y*math.sin(angle))+normal*(6/S)
 tip=blade_center.toTuple();elbow=(cq.Vector(*start)*.55+blade_center*.45+cq.Vector(0,.012,0)).toTuple()
 hub=ring(.011,.0065,.012,(pivot+N*.020).toTuple(),N.toTuple())
 arm=cq.Compound.makeCompound([hub,rod(start,elbow,.0045),rod(elbow,tip,.004)])
 add('wiper_'+label+'_parked_arm_reference',arm,(.32,.34,.32))
 blade=box(.245,.009,.009,tip)
 spine=box(.235,.005,.008,(blade_center+normal*.006).toTuple())
 add('wiper_'+label+'_static_blade_reference',cq.Compound.makeCompound([blade,spine]),(.15,.17,.15))
 nodes[label]['estimated_blade_center_units']=tip
 nodes[label]['estimated_arm_elbow_units']=elbow
 # Through-passage checks on housing alone; pinion/shaft intentionally separate.
 assert housing.intersect(cyl(.003,.060,[x-.03,.968,RACK_Z],(1,0,0))).Volume()<1e-5
 assert housing.intersect(cyl(.006,.060,(center-N*.030).toTuple(),N.toTuple())).Volume()<1e-5
 assert skin.intersect(cyl(.007,.060,(pivot-N*.030).toTuple(),N.toTuple())).Volume()<1e-5
assembly=cq.Assembly(name='static_three_blade_wiper_drive_comparison');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=absolute_tessellation(shape,.04,.12) if name.startswith('scuttle_') else shape.tessellate(.6,.15)
 meshes.append(dict(name=name,color=color,vertices=[round(v,4) for p in vs for v in p.toTuple()],triangles=[i for tri in ts for i in tri]))
assembly.export(str(OUT/'wiper-drive-reference.step'))
limits='All motor/case internals, profiles, bores, spindle axes, wheelbox spacing, rack casings, length/bends, blade/arm sizes and parked positions, mount/packing, wiring boundary, cowl strip and body crossing are estimates. Factory three-blade topology is a comparison; fitted motor suffix, gear/sweep angle, wheelbox 37H6314/7738 branch, spline/teeth, wiring and park state are unverified. Owner bay photos show a strapped cylindrical unit in the expected motor area without readable identity or calibrated dimensions; IMG_6329/6341 supports screen/parked-wiper context but does not survey the hidden drive. No operating rack engagement, sweep, wiped field, seals, clearance, support load, tool access, electrical circuit or verified removal path. Lower-screen trim is narrowed/repositioned as a declared reference construction change; glass/posts/top rail remain unchanged. The independent screen, engine/body and motor stay; the coordinated bonnet/hinge reconstruction is recorded separately.'
meta=dict(cowl_apron_reconstruction=apron_metadata(S),units='mm',source_frame='Canonical body +X vehicle-left, +Y up, +Z front, mm.',factory_source='https://www.spridgets.de/downloads/Parts-1977.pdf#page=221',configuration='Factory three-blade system separate from two-blade pp218–220. GEU708/37H2732 motor comparison; manufacturer suffix controls service parts. Three wheelbox branches and later arms/blades remain unselected as fitted. Catalog RTC702 rack stock length is not a fitted path calibration.',owner_photos=['IMG_6329','IMG_6341','IMG_6855','IMG_6891'],basis='Factory motor/strap/pad, gear/rack/casing/grommet, three wheelbox/spindle/bush/arm/blade relationships; owner-visible parked screen and cylindrical strapped-unit context. No owner measurements or dimensioned factory drawing supply the new shapes.',limits=limits,nodes=nodes,estimated_casing_paths=paths,estimated_bulkhead_crossing_units=[-.61,.96,.13],estimated_bulkhead_hole_radius_units=.009,lower_screen_trim_change=dict(prior_center_units=[0,1.03,.12],prior_dimensions_units=[1.38,.045,.06],new_center_units=[0,1.025,.082],new_dimensions_units=[1.38,.025,.025],basis='Reference construction aligns smaller lower trim behind estimated pivot band; not owner-measured screen/cowl/seal geometry.'),scuttle_reconstruction=scuttle_metadata,parts=meshes)
meta['front_upper_profile_reference']=upper_profile_metadata(S)
meta['basis']+=' The shared longitudinal front-upper correction lowers the middle contour, with zero correction at sourceZ=.13/1.99. Adjacent sheet and carried mounting references follow; mechanical frames remain unchanged.'
meta['limits']+=' The0.10-unit peak is an owner-photo-led construction estimate, not a measured height. Rear/nose interfaces, axles, marker, engine/body pose, hinge pivots and wiper/screen datums remain drawing references. Better photographic agreement is not fitted panel registration, support/locking strength, full-car collision clearance or a verified removal path.'
meta['basis']+=' The row now sits forward on the broader owner-photo apron reference. Wheelboxes, spindles, bezels and shared bores follow; motor stays. Re-estimated feed/inter-box casings connect their source ports. Static arms now reach the retained screen plane instead of carrying the blades forward off the glass.'
meta['limits']+=' '+meta['cowl_apron_reconstruction']['limits']+' New blade-plane6mm offset and all arm bends are display estimates, not a loaded blade seat or verified park angle. The former guessed bulkhead-crossing grommet is withdrawn from the feed route; the independent body reference aperture remains unassigned. No hidden receiver or complete fitted route is inferred.'
(OUT/'wiper-drive-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'wiper-drive-reference.step')).val();assert restored.isValid()
print(json.dumps(dict(groups=len(parts),solids=len(restored.Solids()),step_valid=True,three_skin_and_wheelbox_passages_open=True,static_only=True)))
