"""Indexed conveyor guide reference mechanism. CAD dimensions are mm.

X is travel, Y is transverse adjustment, Z is height. Two round shafts are
above the product envelope. Each independent guide has two carriages, an
index plunger at each station, and separate captive slotted clamping bolts.
Threaded hardware is an envelope; inspect drawing callouts before manufacture.
"""
from pathlib import Path
import json
import cadquery as cq
ROOT=Path(__file__).resolve().parent

def load():return json.loads((ROOT/'inputs.json').read_text())
def validate(c):
 if c['shaft_diameter']!=20 or c['index_pin_diameter']!=8 or c['index_hole_diameter']!=8.15:raise ValueError('Reference bearing and index hardware have fixed diameters')
 if c['shaft_center_z']!=183 or c['frame_outer_width']!=450 or c['shaft_length']!=400 or c['belt_top']!=90 or c['rail_bottom']!=95 or c['rail_height']!=30:raise ValueError('This reference frame fixes shaft height, frame width, belt height and rail elevation; edit source geometry for a different frame')
 if c['rail_length']<2*max(abs(x) for x in c['station_x']) or c['rail_length']>560:raise ValueError('Rail must span both supports inside the frame length')
 if any(v<0 for v in c['tolerances'].values()):raise ValueError('Tolerance allowances must be nonnegative')
 if len(c['carton_widths'])!=3 or len(set(c['carton_widths']))!=3:raise ValueError('Three distinct carton settings required')
 if min(c['carton_widths'])<=0 or c['running_clearance_per_side']<=0:raise ValueError('Positive size and clearance required')
 if c['carriage_bore']<=c['shaft_diameter']:raise ValueError('Shaft requires sliding clearance')
 if c['index_hole_diameter']<=c['index_pin_diameter']:raise ValueError('Index pin must fit its bore')
 if c['belt_top']+c['carton_height']>=158:raise ValueError('Carton exceeds overhead frame clearance')
 if max(c['carton_widths'])/2+c['running_clearance_per_side']+c['wear_thickness']+c['rail_thickness']/2+15>=175:raise ValueError('Carriage travel exceeds shaft support spacing')
 if minimum_clearance(c)<=0:raise ValueError('Worst-case carton clearance is not positive')

def rail_center(c,width):return width/2+c['running_clearance_per_side']+c['wear_thickness']+c['rail_thickness']/2

def minimum_clearance(c):return c['running_clearance_per_side']-sum(c['tolerances'].values())
def box(x,y,z,center):return cq.Workplane('XY').box(x,y,z).translate(center)
def cylinder_z(d,h,center):return cq.Workplane('XY').circle(d/2).extrude(h).translate(center)
def cylinder_y(d,length,center):return cq.Workplane('XZ').circle(d/2).extrude(length).translate(center)

def build(c,width,include_carton=False):
 validate(c);parts={};yz=c['shaft_center_z'];r=rail_center(c,width)
 # Frame rail and belt are reference interface envelopes, not a conveyor design.
 for side in [-1,1]:
  parts[f'frame_side_{side}']=box(560,30,40,(0,side*210,40))
 parts['belt_envelope']=box(540,240,4,(0,0,c['belt_top']-2))
 for station in c['station_x']:
  direction=1 if station>0 else -1;idx=station+direction*35;clampx=station+direction*60
  # Index plate fixed to frame towers. Its long slot retains the clamp bolts.
  plate=box(80,400,7,(station+direction*40,0,161.5))
  for w in c['carton_widths']:
   for side in [-1,1]:plate=plate.cut(cylinder_z(c['index_hole_diameter'],9,(idx,side*rail_center(c,w),157)))
  slot=cq.Workplane('XY').center(clampx,0).slot2D(350,9,90).extrude(9).translate((0,0,157));plate=plate.cut(slot)
  parts[f'index_plate_{station}']=plate
  # XZ normal points -Y; start at positive Y to span symmetrically.
  parts[f'shaft_{station}']=cylinder_y(c['shaft_diameter'],400,(station,200,yz))
  for side in [-1,1]:
   # Frame posts carry the overhead shaft and index plate.
   post=box(90,20,165,(station+direction*30,side*200,122.5));post=post.cut(cylinder_y(c['shaft_diameter']+.15,22,(station,side*200+11,yz)))
   parts[f'post_{station}_{side}']=post
   y=side*r
   carriage=box(90,30,32,(station+direction*30,y,yz));carriage=carriage.cut(cylinder_y(c['carriage_bore'],32,(station,y+16,yz)))
   carriage=carriage.cut(cylinder_z(8.25,34,(idx,y,yz-17))).cut(cylinder_z(8.5,34,(clampx,y,yz-17)))
   parts[f'carriage_{station}_{side}']=carriage
   # 2 mm gap above fixed plate means clamping bolt draws a declared floating shoe.
   # Guide hanging strap joins carriage to guide, remaining outside carton path.
   parts[f'hanger_{station}_{side}']=box(25,6,42,(station,y,146))
   parts[f'index_pin_{station}_{side}']=cylinder_z(8,47,(idx,y,157))
   parts[f'index_handle_{station}_{side}']=cylinder_z(16,8,(idx,y,204))
   parts[f'clamp_bolt_{station}_{side}']=cylinder_z(8,50,(clampx,y,149))
   parts[f'clamp_knob_{station}_{side}']=cylinder_z(22,9,(clampx,y,199))
   washer=cylinder_z(18,3,(clampx,y,155)).cut(cylinder_z(8.5,3,(clampx,y,155)));parts[f'captive_washer_{station}_{side}']=washer
   parts[f'clamp_nut_{station}_{side}']=cq.Workplane('XY').polygon(6,13.8).extrude(6).translate((clampx,y,149)).cut(cylinder_z(7,6,(clampx,y,149)))
   # Shoe beneath carriage transmits clamping reaction to the stationary plate.
   shoe=box(18,24,2,(clampx,y,166)).cut(cylinder_z(8.5,3,(clampx,y,164.5)));parts[f'clamp_shoe_{station}_{side}']=shoe
 for side in [-1,1]:
  y=side*r
  rail=box(c['rail_length'],c['rail_thickness'],c['rail_height'],(0,y,c['rail_bottom']+c['rail_height']/2))
  # Wear strip has flush counterbored fasteners entering from outside rail.
  wear_y=side*(width/2+c['running_clearance_per_side']+c['wear_thickness']/2)
  strip=box(c['rail_length'],c['wear_thickness'],c['rail_height'],(0,wear_y,c['rail_bottom']+c['rail_height']/2))
  for x in [-150,0,150]:
   bore=cylinder_y(4.5,12,(x,y+6,110));rail=rail.cut(bore);strip=strip.cut(bore)
  parts[f'rail_{side}']=rail;parts[f'wear_strip_{side}']=strip
 if include_carton:parts['carton_envelope']=box(c['carton_length'],width,c['carton_height'],(0,0,c['belt_top']+c['carton_height']/2))
 return parts

def analytic_checks(c):
 validate(c);rows=[]
 for w in c['carton_widths']:
  r=rail_center(c,w)
  rows.append({'width_mm':w,'rail_center_mm':r,'nominal_opening_mm':w+2*c['running_clearance_per_side'],'minimum_side_clearance_mm':minimum_clearance(c),'minimum_overhead_plate_clearance_mm':158-(c['belt_top']+c['carton_height']),'shaft_travel_margin_mm':175-r-15})
 return rows
