"""1500 single-rail selector / body-gaiter comparison; all fitted dimensions unknown."""
from pathlib import Path
import json,math
import cadquery as cq
from gearbox_reference_layout import REAR
OUT=Path(__file__).parent;BODY=json.loads((OUT.parent/'body-data.json').read_text());CABIN=json.loads((OUT/'cabin-understructure-reference.json').read_text());S=BODY['published_reference_dimensions_mm']['wheelbase']/2.4
X=cq.Vector(1,0,0);Y=cq.Vector(0,1,0);Z=cq.Vector(0,0,1);ANCHOR=.39*S;parts=[]
def V(p):return cq.Vector(*(v*S for v in p))
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 sphere(r,p):return cq.Solid.makeSphere(r,p,angleDegrees1=-90)
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=(.42,.44,.41)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color));return shape
pivot=V([0,.16,-.28]);knob=V([0,.50,-.36]);direction=(knob-pivot).normalized();low=V([0,.10,-.28]);rail_y=.06*S;rail_z=-.28*S
rail=cyl(6.5,.59*S,V([0,.06,-.35]),Z).cut(cyl(2.6,26,cq.Vector(-13,rail_y,rail_z),X))
add('gearbox_single_selector_rail_comparison',rail,(.57,.58,.52))
sleeve=ring(11,6.8,24,cq.Vector(0,rail_y,rail_z-12),Z)
webs=[box(5,low.y-rail_y,20,cq.Vector(sign*12,rail_y+(low.y-rail_y)/2,rail_z)) for sign in [-1,1]]
cup=sphere(12,low).cut(sphere(8.5,low)).intersect(box(40,24,40,low-Y*12))
yoke=sleeve.fuse(*webs,cup).cut(cyl(2.6,30,cq.Vector(-15,rail_y,rail_z),X))
add('gearbox_selector_yoke_open_cup_and_sleeve_comparison',yoke)
add('gearbox_yoke_cross_pin_envelope',cyl(2.3,28,cq.Vector(-14,rail_y,rail_z),X),(.57,.58,.52))
seat=sphere(26,pivot).cut(sphere(20.3,pivot)).intersect(box(60,52,60,pivot-Y*26)).cut(cyl(6.5,60,pivot-Y*40,Y))
add('gearbox_lower_spherical_lever_seat_comparison',seat,(.54,.49,.35))
lever=sphere(20,pivot).fuse(cyl(8,(knob-pivot).Length,pivot,direction)).fuse(cyl(6,(pivot-low).Length,low,Y)).fuse(sphere(8,low))
add('gearbox_change_lever_ball_stem_and_lower_finger_comparison',lever,(.56,.58,.52))
knob_shape=sphere(24,knob).cut(cyl(8.5,26,knob-direction*26,direction))
add('gearbox_separate_blank_knob_comparison',knob_shape,(.14,.16,.14))
cap=ring(33,30.3,22,pivot-Y*3,Y).fuse(ring(33,14,3,pivot+Y*19,Y))
pin_y=pivot.y+7
cap=cap.cut(cyl(3.4,80,cq.Vector(-40,pin_y,rail_z),X)).cut(box(80,10,6.8,cq.Vector(0,pivot.y+2,rail_z)))
add('gearbox_open_lower_slots_and_annular_lever_retainer_comparison',cap)
pins=[cyl(3.1,l,cq.Vector(sign*25,pin_y,rail_z),X*sign) for sign,l in [(1,10),(-1,8)]]
add('gearbox_long_short_retainer_pin_envelopes',compound(pins),(.57,.58,.52))
add('gearbox_selector_shaft_uncompressed_seal_ring_comparison',ring(8.3,6.7,2,cq.Vector(0,rail_y,REAR-1),Z),(.12,.15,.13))
# Recreate the existing declared tunnel envelope, not a new body measurement.
sections=CABIN['estimated_current_tunnel_sections_zwh_units']
def section(z,w,h):
 ps=[cq.Vector(w*S*math.cos(i*math.pi/48),.35*S+(h-.35)*S*math.sin(i*math.pi/48),z*S) for i in range(49)]
 return cq.Wire.makePolygon(ps+[ps[0]])
outer=cq.Solid.makeLoft([section(*v) for v in sections],ruled=True)
def surface(x,z):
 for (a,w,h),(b,v,q) in zip(sections,sections[1:]):
  if b*S<=z<=a*S:
   f=(z/S-a)/(b-a);rx=(w+(v-w)*f)*S;ry=(h+(q-h)*f-.35)*S
   return .35*S+ry*math.sqrt(max(0,1-(x/rx)**2))
 raise ValueError(z)
rx=.08*S;rz=.10*S;cz=-.32*S
# A straight oval lower cuff is trimmed by the curved tunnel envelope. This
# avoids inventing a flat seal seat across the independent curved shell.
join=max(surface(rx*math.cos(i*math.pi/64),cz+rz*math.sin(i*math.pi/64)) for i in range(128))+7.3
top_y=.76*S;top_z=pivot.z+direction.z*((top_y-ANCHOR-pivot.y)/direction.y)
# Local gearbox coordinates use body Y minus the .39-unit drawing anchor.
def ellipse_wire(y,r1,r2,z=cz):return cq.Workplane(cq.Plane(origin=(0,y,z),xDir=X,normal=Y)).ellipse(r1,r2).val()
def oval_solid(y0,y1,r1,r2,z=cz):return cq.Solid.extrudeLinear(ellipse_wire(y0,r1,r2,z),[],Y*(y1-y0))
bottom=.45*S
outer_boot=oval_solid(bottom,join,rx,rz).fuse(cq.Solid.makeLoft([ellipse_wire(join,rx,rz),ellipse_wire(top_y,20,20,top_z)],ruled=True))
inner_boot=oval_solid(bottom-2,join+1,rx-2,rz-2).fuse(cq.Solid.makeLoft([ellipse_wire(join-1,rx-2,rz-2),ellipse_wire(top_y,18,18,top_z)],ruled=True)).fuse(oval_solid(top_y-1,top_y+3,18,18,top_z))
boot=outer_boot.cut(inner_boot).cut(outer.translate((0,2.3,0)))
clip=cq.Workplane('XY').box(190,600,250).edges('|Y').fillet(12).translate((0,.62*S,cz)).val()
flange=outer.translate((0,2.3,0)).cut(outer.translate((0,.3,0))).intersect(clip).cut(oval_solid(.40*S,.95*S,rx-2,rz-2))
retainer=outer.translate((0,4.3,0)).cut(outer.translate((0,2.3,0))).intersect(clip).cut(oval_solid(.40*S,.95*S,rx,rz))
body_fixings=[];fixings=[]
for i,(x,z) in enumerate([(sx*.09*S,cz+sz*.12*S) for sx,sz in [(-1,-1),(-1,1),(1,-1),(1,1)]]):
 y=surface(x,z);p=cq.Vector(x,y,z);eps=.1
 dx=(surface(x+eps,z)-surface(x-eps,z))/(2*eps);dz=(surface(x,z+eps)-surface(x,z-eps))/(2*eps);axis=cq.Vector(-dx,1,-dz).normalized()
 tool=cyl(4.2,45,p-axis*15,axis);flange=flange.cut(tool);retainer=retainer.cut(tool)
 # Derive the reference head plane from the actual retainer envelope along
 # its estimated normal. No fitted screw axis/bearing face is established.
 edge=outer.translate((0,4.3,0)).intersect(cq.Edge.makeLine(p-axis*15,p+axis*30))
 head_t=max((vertex.Center()-p).dot(axis) for vertex in edge.Vertices())+.3
 depth=7 if i==3 else 5
 screw=cyl(3.5,head_t+depth,p-axis*depth,axis).fuse(cyl(6,3,p+axis*head_t,axis))
 fixings.append(screw.translate((0,-ANCHOR,0)));body_fixings.append(dict(center_body_mm=list(p.toTuple()),axis_body=list(axis.toTuple()),bore_radius_mm=4.2,estimated_projection_below_surface_mm=depth,estimated_head_start_along_axis_mm=head_t,construction_head_offset_mm=.3,group='one' if i==3 else 'three'))
add('gearbox_hollow_body_gaiter_with_offset_stem_mouth_comparison',boot.translate((0,-ANCHOR,0)),(.12,.15,.13))
add('gearbox_curved_body_gaiter_flange_comparison',flange.translate((0,-ANCHOR,0)),(.12,.15,.13))
add('gearbox_curved_four_hole_body_gaiter_retainer_comparison',retainer.translate((0,-ANCHOR,0)))
add('gearbox_body_gaiter_three_plus_one_screw_envelopes',compound(fixings),(.56,.58,.51))
assert rail.intersect(yoke).Volume()<1e-5
assert lever.intersect(seat).Volume()<1e-5
assert lever.intersect(cap).Volume()<1e-5
assert knob_shape.intersect(lever).Volume()<1e-5
assert lever.intersect(yoke).Volume()<1e-5
limits='No direct owner dimensions, fitted gearbox identity or measured installed registration are applied. Factory 1500 single-rail topology is distinct from earlier A-series remote housings. Every rail/yoke/seat/retainer section, hole and pin map, neutral pose, seal, knob, lever stack, gaiter and fixing station is estimated. Retained lever ball and knob centers, gearbox axes and current tunnel sections are prior drawing datums, not fitted coordinates. The separate case/extension source now uses revised short-case/long-tail proportions. Extension neck radius30 mm supersedes the old55.0333 mm cue as an unscaled estimate; its base reachesY=0 while its top stays atY=152.4 mm. Gaiter top mouth shifts from old Z=-.32 to the retained sloping stem at body Y=.76; the bottom oval uses the prior declared tunnel mouth. Curved body flange/retainer follow the existing tunnel construction, not surveyed sheet metal or verified seal contact. Four body holes and three-plus-one screw envelopes follow factory count only; axes follow estimated normals of the current tunnel reference and head planes follow its translated retainer surface, not fitted fastener axes or bearing contact; locations and short/long assignment are not a drilling/repair map. 0.3 mm body-flange offset and other radial separations are construction choices, not fitted clearances or rubber compression. Long/short retainer pins and open-bottom cap slots are architectural comparisons, not proof of actual bayonet engagement, release direction, thread or locking. The selector rail/yoke show only a neutral relationship; forks, interlocks, detents, gears, reverse-lift plate/plunger/spring, bearings, loaded seat and real change-speed seal are not reconstructed. The separate annular selector seal has no loaded sealing function. North American early seat-belt inhibitor hardware and Canada-only selector gaiter/clips remain unselected. No working gear selection, shift/reverse-lift travel, preload, lubrication, torque, electrical function, support strength, tunnel access, installed obstacle clearance or verified replacement/removal path. Inspection translations show ownership only.'
nodes=dict(pivot_local_mm=list(pivot.toTuple()),knob_center_local_mm=list(knob.toTuple()),lever_upper_axis=list(direction.toTuple()),lower_finger_center_local_mm=list(low.toTuple()),selector_axis_local_y_mm=rail_y,selector_z_bounds_local_mm=[-.35*S,.24*S],selector_rear_case_passage_radius_mm=8.5,selector_rear_case_z_mm=REAR,estimated_neck=dict(center_z_mm=rail_z,outer_radius_mm=30,inner_radius_mm=27,y_bounds_local_mm=[0,.18*S]),retainer_pin_axis_local_yz_mm=[pin_y,rail_z],retainer_pin_bore_radius_mm=3.4,body_fixings=body_fixings,body_mouth_center_xz_units=[0,-.32],body_mouth_radii_xz_units=[.08,.10],gaiter_top_center_body_mm=[0,top_y,top_z],gaiter_top_inner_radius_mm=18,estimated_lower_cuff_join_body_y_mm=join,source_body_anchor_units=[0,.39,0])
limits+=' The uncompressed selector-ring comparison follows the revised shared case rear plane; all control/lever axes and other geometry stay. Actual seal location, engagement and front-scroll versus rubber-seal configuration remain unverified.'
meta=dict(units='mm',source_frame='Gearbox-local +X vehicle-left, +Y up, +Z front; add body [0,.39,0] after metric conversion.',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=82','https://www.spridgets.de/downloads/Parts-1977.pdf#page=83'],owner_photos=['IMG_6341','IMG_6919'],owner_dimensions_applied=False,basis='Factory single selector shaft 22G2053, yoke 22G2291/pin RZS1216, seat 22G1424, lever 22G2797, retainer 22G1434, long/short pins, knob 22G1755, selector O-ring TRS912 and outer gaiter/retainer three-plus-one screw roles are comparisons. Owner cabin/underbody context does not calibrate hidden gearbox controls or identify fitted parts.',nodes=nodes,limits=limits)
assembly=cq.Assembly(name='estimated_single_rail_gearbox_controls_and_body_gaiter');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=shape.tessellate(.6,.16);meshes.append(dict(name=name,color=color,vertices=[round(x,4) for p in vs for x in p.toTuple()],triangles=[i for t in ts for i in t]))
assembly.export(str(OUT/'gearbox-controls-reference.step'));meta['parts']=meshes;(OUT/'gearbox-controls-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
r=cq.importers.importStep(str(OUT/'gearbox-controls-reference.step')).val();assert r.isValid();print(json.dumps(dict(source_parts=len(parts),solids=len(r.Solids()),step_valid=True,rail_yoke_nonintersection=True,lever_seat_cap_nonintersection=True,knob_socket_nonintersection=True)))
