"""Unfitted Federal-to-1977 throttle-linkage comparison in the engine source frame.

Factory Parts-1977 PDF p124 supplies topology, not dimensions or installed pose.
Owner IMG_7035/7037 show external hardware context without identifying this variant.
"""
from pathlib import Path
import json, math
import cadquery as cq
from build_engine import carb_throttle_axis, carb_throttle_dimensions, make_carburetor, make_air_cleaner_housing
OUT=Path(__file__).parent
axis=carb_throttle_axis();shaft=carb_throttle_dimensions()
x,y,z=axis
nodes={
 'spindle_end':[x,y,z-shaft['shaft_length']/2],
 'lever_pivot':[x,y,z-shaft['shaft_length']/2-3],
 'lever_link':[x+23,y+13,z-shaft['shaft_length']/2-3],
 'bell_pivot':[x-32,y+50,-60],
 'bell_link':[x-7,y+48,-60],
 'bell_upper':[x-36,y+72,-60],
 'bell_lower':[x-42,y+29,-60],
 'upper_spring_anchor':[x-36,y+110,-65],
 'lower_spring_anchor':[x-43,y-9,-65],
 'cable_eye':[x+10,y+105,-70],
 'cable_open':[x+15,y+135,-75],
}
parts=[]
def cyl(r,l,p,axis=(0,0,1)):return cq.Solid.makeCylinder(r,l,cq.Vector(*p),cq.Vector(*axis))
def rod(a,b,r):
 a,b=cq.Vector(*a),cq.Vector(*b);v=b-a
 return cyl(r,v.Length,a.toTuple(),v.normalized().toTuple())
def ring(p,ro=6,ri=2,l=3):return cyl(ro,l,(p[0],p[1],p[2]-l/2)).cut(cyl(ri,l+2,(p[0],p[1],p[2]-l/2-1)))
def arm(a,b,w=10,t=3):
 dx,dy=b[0]-a[0],b[1]-a[1];length=math.hypot(dx,dy);nx,ny=-dy/length*w/2,dx/length*w/2
 outline=[(a[0]+nx,a[1]+ny),(b[0]+nx,b[1]+ny),(b[0]-nx,b[1]-ny),(a[0]-nx,a[1]-ny)]
 shape=cq.Workplane('XY',origin=(0,0,a[2]-t/2)).polyline(outline).close().extrude(t).val()
 for p in [a,b]:shape=shape.fuse(cyl(w/2,t,(p[0],p[1],a[2]-t/2)))
 for p in [a,b]:shape=shape.cut(cyl(2,t+2,(p[0],p[1],a[2]-t/2-1)))
 return shape

def add(name,shape,color=(.5,.49,.43)):
 assert shape.isValid() and shape.Volume()>0,name
 parts.append((name,shape,color))
# Open plate construction: guessed bracket contours and mounting/cable eyes.
bp=nodes['bell_pivot']
plate=cq.Workplane('XY').polyline([(x-50,y+24),(x-22,y+24),(x-22,y+116),(x-50,y+116)]).close().extrude(3).translate((0,0,-66)).val()
for cy in [y+31,y+104]:plate=plate.cut(cyl(3.5,5,(x-36,cy,-67)))
plate=plate.cut(cyl(3,5,(bp[0],bp[1],-67)))
foot=cq.Workplane('XY').box(65,3,25).translate((x-18,y+25,-54)).val()
eye=nodes['cable_eye'];cable_tab=cq.Workplane('XY').box(20,3,18).translate((eye[0],eye[1],eye[2])).val().cut(cyl(3,5,(eye[0],eye[1]-2.5,eye[2]),(0,1,0)))
# Separate solids describe an unfinished mounting hypothesis; no welded/fitted claim.
add('throttle_bracket_comparison',cq.Compound.makeCompound([plate,foot,cable_tab,rod([x-43,y+24,-65],nodes['lower_spring_anchor'],2),rod([x-25,y+105,-65],[eye[0]-7,eye[1],eye[2]],2)]))
add('throttle_spindle_lever_comparison',arm(nodes['lever_pivot'],nodes['lever_link']).fuse(ring(nodes['lever_pivot'],7,shaft['shaft_radius'],6)).cut(cyl(shaft['shaft_radius'],8,(x,y,-47))))
bell=arm(bp,nodes['bell_link'])
for end in ['bell_upper','bell_lower']:bell=bell.fuse(arm(bp,nodes[end]))
add('throttle_bellcrank_comparison',bell.fuse(cyl(2,9,(bp[0],bp[1],-66))))
a,b=nodes['lever_link'],nodes['bell_link']
links=[arm([a[0],a[1],station],[b[0],b[1],station],7,2) for station in [-48,-55]]
links.extend(cyl(1.8,25,(p[0],p[1],-65)) for p in [a,b])
add('throttle_link_comparison',cq.Compound.makeCompound(links))

def spring(a,b,turns):
 a,b=cq.Vector(*a),cq.Vector(*b);v=b-a;u=v.normalized();side=u.cross(cq.Vector(1,0,0)).normalized();other=u.cross(side).normalized();points=[a]
 for i in range(turns*24+1):
  f=i/(turns*24);angle=2*math.pi*turns*f
  points.append(a+u*(4+(v.Length-8)*f)+side*(3.5*math.cos(angle))+other*(3.5*math.sin(angle)))
 points.append(b);path=cq.Wire.assembleEdges([cq.Edge.makeSpline(points)])
 tangent=(points[1]-points[0]).normalized()
 return cq.Workplane(cq.Plane(origin=a,normal=tangent)).circle(.65).sweep(path,isFrenet=True).val()
for key,a,b,turns in [('upper','bell_upper','upper_spring_anchor',8),('lower','bell_lower','lower_spring_anchor',7)]:
 add('throttle_'+key+'_spring_comparison',spring(nodes[a],nodes[b],turns),(.58,.55,.45))
# A local cable segment only. No remote pedal route or cable adjustment is inferred.
add('throttle_local_cable_boundary',cq.Compound.makeCompound([rod(nodes['cable_eye'],nodes['cable_open'],2.5),rod(nodes['bell_link'],nodes['cable_eye'],.65),cyl(4,12,(eye[0],eye[1]-6,eye[2]),(0,1,0))]),(.22,.23,.20))
assembly=cq.Assembly(name='unfitted_federal_to_1977_throttle_controls_comparison');meshes=[]
for name,shape,color in parts:
 assembly.add(shape,name=name,color=cq.Color(*color));vs,ts=shape.tessellate(.5,.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/'throttle-controls-reference.step'))
limits='All dimensions, bracket contours/attachment map, spring forms and anchors, lever ratios, clocking, cable path and source/body placement are estimates. Federal-to-1977 topology is a comparison, not identification of this car; California/later rod-and-ball linkage is a separate factory branch. No choke identity, fitted stop, adjustment, spring force, operating motion, return reliability, interference-free travel or verified replacement/removal path is established. The remote cable end stays open. Source spindle alignment does not establish an actual fitted joint.'
neighbors={'carburetor_body':make_carburetor(),'air_cleaner_housing':make_air_cleaner_housing()}
intersections={name:{key:round(shape.intersect(other).Volume(),6) for key,other in neighbors.items()} for name,shape,_ in parts}
meta=dict(reference_neighbor_intersections_mm3=intersections,units='mm',factory_sources=['https://www.spridgets.de/downloads/Parts-1977.pdf#page=124'],owner_photos=['IMG_7035','IMG_7037'],basis='Factory p124 Federal-to-1977 cable bracket UKC673, bellcrank 154117, spindle lever 153417, paired links 156572 and two spring relationships. Source spindle-end datum comes from the existing estimated carburetor axis and length; all other coordinates are new estimates. Photo-visible bracket eye/side features are context only and do not select this catalog branch.',limits=limits,nodes_mm=nodes,parts=meshes)
(OUT/'throttle-controls-reference.json').write_text(json.dumps(meta,separators=(',',':'))+'\n')
restored=cq.importers.importStep(str(OUT/'throttle-controls-reference.step')).val();assert restored.isValid()
# Real bores and unfilled spindle interface must survive export geometry.
assert parts[1][1].intersect(cyl(shaft['shaft_radius']-.1,6,(x,y,-46))).Volume()<1e-5
print(json.dumps(dict(groups=len(parts),reference_neighbor_intersections_mm3=intersections,step_valid=True,solids=len(restored.Solids()),spindle_end_mm=nodes['spindle_end'],local_cable_open_mm=nodes['cable_open'],limits=limits)))
