"""R7-based B3 on OUTPUT axis U. No R8-R11 geometry imported.
Units mm. This is a geometry prototype; no load or life approval.
"""
from pathlib import Path
import json,math,functools,time
from build123d import *
from OCP.BRepAlgoAPI import BRepAlgoAPI_Check
R=Path(__file__).resolve().parents[1];U=(-49.89276227273,-53.91822447741);I=(-59.4,0.);D=(-92.,-57.)
P=dict(shaft_d=12.,spline_major_d=13.4,body_od=21.2,body_x0=40.,body_x1=43.,groove_r=9.4,groove_x0=40.6,groove_x1=42.4,
       tooth_ri=8.,tooth_ro=10.3,tooth_angle=40.,tooth_tip=44.7,tooth_count=4,stroke=2.6,
       gear_face=45.5,relief_end=45.9,pocket_bottom=47.8,pocket_ri=7.85,pocket_ro=10.45,
       guide_ro=2.5,guide_bore=2.54,guide_stop_x=46.85,pin_radius=1.5,pin_x=41.5)
COL=dict(shaft=(.56,.60,.66),gear=(.12,.57,.77),dog=(.85,.20,.22),fork=(.10,.65,.37),fixed=(.55,.65,.70),bush=(.69,.52,.27))
F=json.loads((R/'baseline/R7_PART_FACTS.json').read_text())
def named(s,n,c=None):
 if c is None:c=tuple(s.color)[:3] if s.color else COL['fixed']
 s.label=n;s.color=Color(*(COL[c] if isinstance(c,str) else c));return s
def clone(s):return named(Compound.cast(s.wrapped),s.label,tuple(s.color)[:3])
@functools.lru_cache(None)
def old(n):return named(import_brep(str(R/'baseline/brep'/(n+'.brep'))),n,F[n]['color'][:3])
def cyl(r,a,b,c=U):return Solid.make_cylinder(r,b-a,Plane(origin=(a,*c),z_dir=(1,0,0)))
def ring(ro,ri,a,b,c=U):return cyl(ro,a,b,c)-cyl(ri,a-.01,b+.01,c)
def box(dx,dy,dz,x,y,z):return Solid.make_box(dx,dy,dz,Plane(origin=(x-dx/2,y-dy/2,z-dz/2)))
def point(r,a,c=U):t=math.radians(a);return(c[0]+r*math.cos(t),c[1]+r*math.sin(t))
def sector(ri,ro,a,b,x0,x1,c=U):
 pts=[]
 for rr,idx in [(ro,range(21)),(ri,range(20,-1,-1))]:
  for k in idx:pts.append((x0,*point(rr,a+(b-a)*k/20,c)))
 return Solid.extrude(Face(Wire.make_polygon(pts,close=True)),(x1-x0,0,0))
def arm(a,b,x0,x1,width):
 v=Vector(0,b[0]-a[0],b[1]-a[1]);u=Vector(0,-v.Z,v.Y).normalized()*width/2
 pts=[Vector(x0,*a)+u,Vector(x0,*b)+u,Vector(x0,*b)-u,Vector(x0,*a)-u]
 return Solid.extrude(Face(Wire.make_polygon(pts,close=True)),(x1-x0,0,0))
def splines(a,b,clear=0):
 s=cyl(P['shaft_d']/2+clear,a,b)
 for k in range(6):s=s+sector(5.85,6.7+clear,k*60-9-clear*10,k*60+9+clear*10,a,b)
 return s
def double_d(a,b,clear=0):return cyl(6+clear,a,b)&box(b-a+.2,10.4+2*clear,20,(a+b)/2,*U)
def shaft():
 s=old('output_shaft_R6_B12_local_segment')&box(120,60,60,-20.6,*U) # preserve X<=39.4
 s=s+cyl(6,39.3,78.5)+splines(39.4,46.8)
 # Mill the two flats from one continuous journal; no coincident DD union.
 for sign in [-1,1]:s=s-box(6.7,10,20,75.35,U[0]+sign*10.2,U[1])
 s=s-ring(6.2,5.8,47.05,47.4)-cyl(2.1,69.5,78.6)
 return named(s,'U_R12_local_12mm_shaft','shaft')
def bearing_land():
 # Preserve the original external bearing shoulder, retaining groove and seal
 # land exactly as a removable sleeve. No change to housing bearing seats.
 s=(old('output_shaft_R6_B12_local_segment')&cyl(9,55.5,72))-cyl(6.025,55.4,72.1)
 return named(s,'U_R12_removable_original_bearing_seal_land','shaft')
def flange():
 s=old('output_shaft_R6_B12_local_segment')&cyl(22,72,82)
 s=s-double_d(71.9,78.6,.05)-cyl(2.75,78.5,79.1)-cyl(4.35,79,82.1)
 return named(s,'output_R12_detachable_R7_external_flange','shaft')
def flange_bolt():
 s=cyl(2,70,79.01)+cyl(4.25,79,82)
 pts=[(80.1,*point(2.3,k*60)) for k in range(6)];s=s-Solid.extrude(Face(Wire.make_polygon(pts,close=True)),(2,0,0))
 return named(s,'output_R12_M5_retainer_thread_envelope','shaft')
def collar():
 s=ring(10.6,6.05,40,43)+ring(7.1,6.05,39.5,43.5)
 s=s-splines(39.4,43.6,.05)-ring(10.7,9.4,40.6,42.4)
 for k in range(4):
  d=sector(P['tooth_ri'],P['tooth_ro'],k*90-20,k*90+20,42.8,P['tooth_tip'])
  es=[e for e in d.edges() if abs(e.tangent_at(.5).X)>.99]
  if es:d=d.fillet(.12,es)
  es=[e for e in d.edges() if abs(e.center().X-P['tooth_tip'])<1e-5]
  if es:d=d.chamfer(.1,None,es)
  s=s+d
 named(s,'B3_R12_U_axis_single_sided_dog','dog');LinearJoint('axial_only_on_U',s,Axis((41.5,*U),(1,0,0)),linear_range=(0,P['stroke']))
 return s
def gear17():
 # Retain the original involute tooth rim, count and phase; remake bore/web.
 g=old('B3_loose_17T');faces=[f for f in g.faces() if f.bounding_box().size.X<1e-5 and abs(f.center().X-55.5)<1e-4]
 face=max(faces,key=lambda f:f.area);s=Solid.extrude(Face(face.outer_wire()),(-10,0,0))
 s=s-cyl(7.025,45.4,55.6)-cyl(7.35,45.4,47.8)-cyl(10.85,45.4,45.9)
 for k in range(4):s=s-sector(P['pocket_ri'],P['pocket_ro'],k*90-21,k*90+21,45.89,47.8)
 return named(s,'B3_R12_free_17T_end_face_windows','gear')
def bush():return named(ring(7,6.025,47.4,55.5)+ring(7.3,6.025,47.4,47.7),'B3_R12_12x14_flanged_plain_bush','bush')
def clip():return named(sector(5.8,7.15,20,340,47.1,47.4)-cyl(5.8,47.0,47.5),'B3_R12_bush_boundary_C_clip','shaft')
PHI=math.atan2(U[1]-D[1],U[0]-D[0]);GUIDES=[point(28,205),point(26,285)]
def pcyl(rad,a,b,x=41.5):
 v=(math.cos(PHI),math.sin(PHI));return Solid.make_cylinder(rad,b-a,Plane(origin=(x,D[0]+a*v[0],D[1]+a*v[1]),z_dir=(0,*v)))
def fork():
 s=sector(23.5,26.5,160,300,40.7,43.7)
 for a in [170,290]:s=s+sector(9.55,11.1,a-12,a+12,40.8,42.2)+arm(point(10.7,a),point(25,a),40.8,42.2,4.0)
 for c in GUIDES:s=s+cyl(3.7,40.7,44.1,c)
 s=s+arm(point(25,190),point(16,math.degrees(PHI),D),40.7,43.7,6)+pcyl(3.2,12.8,17.5)
 for r,a,b in [(1.53,9.4,12.6),(2.03,12.5,17.6),(2.6,17.5,29)]:s=s-pcyl(r,a,b)
 s=s-cyl(.65,38.9,44.1,point(15.3,math.degrees(PHI),D))-cyl(13,38.9,44.2,D)
 for c in GUIDES:s=s-cyl(2.54,40.5,44.3,c)
 return named(s,'B3_R12_U_axis_two_foot_fork','fork')
def pin():
 s=pcyl(1.5,9.5,12.6)+pcyl(2,12.6,17.5)+pcyl(2.4,17.5,18.5)
 return named(s-cyl(.65,38.9,44.1,point(15.3,math.degrees(PHI),D)),'B3_R12_radial_follower_pin','shaft')
def keeper():return named(cyl(.6,39,44,point(15.3,math.degrees(PHI),D)),'B3_R12_follower_keeper','shaft')
def supports():
 out={};front=sector(22,32,190,300,39,40.5)+sector(30,32,235,250,40.4,52.0);rear=sector(22,32,190,300,52,54)
 front=front-pcyl(3.5,12.0,21)-cyl(1.0,38.9,40.6,point(15.3,math.degrees(PHI),D))
 front=front-cyl(13,38.9,52.1,D) # full rotation clearance for the drum
 rear=rear-cyl(14.3,51.9,54.1,D)
 for k,c in enumerate(GUIDES,1):
  front=front-cyl(2.54,38.9,40.6,c);rear=rear-cyl(2.54,51.9,54.1,c)
  rod=cyl(2.5,39,54,c)+cyl(4,46.85,48.0,c)-cyl(1.05,38.9,43,c)-cyl(1.05,50,54.1,c)
  out[f'B3_R12_guide_rod_{k}_with_stop']=named(rod,f'B3_R12_guide_rod_{k}_with_stop','shaft')
  for side,tail,head in [('left',(39,42.5),(37.5,39)),('right',(50.5,54),(54,55.5))]:
   n=f'B3_R12_guide_{k}_{side}_retainer';out[n]=named(cyl(1,*tail,c)+cyl(3.5,*head,c),n,'shaft')
 for k,a in enumerate([238,248],1):
  c=point(31,a);front=front-cyl(1.25,48,52.2,c);rear=rear-cyl(1.7,51.9,54.1,c)
  n=f'B3_R12_rear_support_M3_{k}';out[n]=named(cyl(1.2,48.5,54,c)+cyl(2.3,54,55.5,c),n,'shaft')
 # Two local mounting columns close the reaction path to R7's right wall.
 # Only two recessed screw holes are added; no shaft/gear/outer-wall relocation.
 housing=clone(old('right_housing_R6_local_ribs'))
 for k,a in enumerate([220,265],1):
  c=point(29,a);rear=rear+cyl(3.8,53.9,62.5,c);rear=rear-cyl(1.3,57.8,62.6,c)
  housing=housing-cyl(1.7,62.4,67.6,c)-cyl(3.1,65.7,67.6,c)
  n=f'B3_R12_case_mount_M3_{k}';s=cyl(1.25,58,65.8,c)+cyl(3,65.8,67.5,c)
  pts=[(66.3,*point(1.35,j*60,c)) for j in range(6)];s=s-Solid.extrude(Face(Wire.make_polygon(pts,close=True)),(1.3,0,0));out[n]=named(s,n,'shaft')
 out['right_housing_R12_two_local_B3_mount_holes']=named(housing,'right_housing_R12_two_local_B3_mount_holes',(.64,.7,.77))
 out['B3_R12_front_support_and_carrier']=named(front,'B3_R12_front_support_and_carrier')
 out['B3_R12_removable_rear_support']=named(rear,'B3_R12_removable_rear_support')
 return out
def newparts():
 d={s.label:s for s in [shaft(),bearing_land(),flange(),flange_bolt(),collar(),gear17(),bush(),clip(),fork(),pin(),keeper()]};d.update(supports());return d
def moving(n):return n in ['B3_R12_U_axis_single_sided_dog','B3_R12_U_axis_two_foot_fork','B3_R12_radial_follower_pin','B3_R12_follower_keeper']
def local(dx=0):
 ps={n:clone(old(n)) for n in ['B3_driver_56T','B3_driver_56T_key','intermediate_shaft_1_R6_B12_local_segment','B2_C2_PAIRED_driver_47T','B2_C2_PAIRED_free_26T','U_right_bearing','U_right_retaining_ring','U_right_shaft_seal','I_right_bearing']}
 for n,s in newparts().items():ps[n]=s.moved(Location((dx,0,0))) if moving(n) else s
 return ps
def assembly(ps,label):return Compound(label=label,children=[clone(s) for s in ps.values()])
def leaves(s):
 if s.children:
  for c in s.children:yield from leaves(c)
 else:yield s
def overlap(a,b):
 aa=a.bounding_box();bb=b.bounding_box()
 if any(getattr(aa.max,k)<=getattr(bb.min,k)+1e-6 or getattr(bb.max,k)<=getattr(aa.min,k)+1e-6 for k in 'XYZ'):return 0.
 h=a&b;return sum(x.volume for x in h.solids()) if h else 0.
def save(n,d):(R/'reports'/n).write_text(json.dumps(d,ensure_ascii=False,indent=2),encoding='utf8')
def strict(s):
 c=BRepAlgoAPI_Check(s.wrapped,True,True);c.Perform();return dict(solids=len(s.solids()),volume=s.volume,valid=s.is_valid,strict_valid=c.IsValid(),status='PASS' if len(s.solids())==1 and s.is_valid and c.IsValid() and s.volume>0 else 'FAIL')
def run():
 t=time.perf_counter();ps=newparts();rows=[]
 for n,s in ps.items():
  path=R/'parts'/(n+'.step');export_step(s,str(path));ss=import_step(str(path));row=dict(name=n,**strict(ss));rows.append(row);save('STRICT_NEW_PARTS.json',rows);print(n,row['status'],row['solids'],flush=True)
  if row['status']!='PASS':save('STOP.json',dict(reason='INVALID_PART',name=n));return
 for tag,dx in [('NEUTRAL',0),('MID',1.3),('ENGAGED',2.6)]:
  a=local(dx);export_step(assembly(a,'R12_B3_'+tag),str(R/'states'/('R12_B3_'+tag+'.step')));print('state',tag,len(a),flush=True)
 save('PARAMETERS.json',P);save('BUILD_TIME.json',dict(seconds=time.perf_counter()-t))
if __name__=='__main__':run()
