"""Mobile robot r1: original conceptual envelopes, millimetres, not manufacturing CAD.
Generate standalone JSON mesh and OBJ using Python standard library only.
"""
import json,math,argparse
from pathlib import Path
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--out',type=Path,default=Path('mobile-robot-export'))
OUT=parser.parse_args().out
parts=[]
GROUPS=[
 ('chassis','01','Lower chassis','CNC router / laser prototype','Make','6 mm plywood plate; 220 × 280 mm. Route mounting holes and cable slots after motor selection. Laser-cut cardboard first to check packaging. Confirm stock and cutter limits.','Keep the plate stiff around motor mounts; a visually flat plate still needs a loaded deflection check.','cnc'),
 ('mounts','02','Motor mounts & rails','FDM → optional Bridgeport','Make','PETG fitting brackets with through-bolts, washers and metal motor clamps. If creep or flex changes wheel alignment, use staff-approved machined aluminum brackets. Bandsaw only rough-cuts approved stock.','Orient load-bearing walls deliberately. Do not carry motor loads through thin printed tabs or rely on infill alone.','print'),
 ('drive','03','Wheels, gearmotors & encoders','Purchased assemblies','Buy','Two 100 mm rubber wheels, matching hubs, encoder gearmotors and shaft hardware. Exact motors not selected. Confirm shaft radial-load capacity and hub offset.','Choose torque at operating speed, not stall torque. Encoder speed is not ground speed when the tire slips.','buy'),
 ('caster','04','Rear caster','Purchased caster + FDM spacer','Buy + make','One rear ball caster forms three-point support. Print a spacer after measuring the actual caster height.','Locate the loaded center of mass inside the contact triangle and retain enough weight on the drive wheels.','buy'),
 ('battery','05','Power module','Bought pack / electronics bench','Buy + assemble','Protected battery pack and matched charger, fuse, motor-power cut switch, regulator and strain-relieved wiring. Low removable tray; no custom cell assembly.','Motor supply and regulated logic branch separately. Check voltage sag and logic resets while accelerating.','electronics'),
 ('deck','06','Service deck & standoffs','Laser cutter / FDM','Make','3 mm approved plywood service deck, PETG standoffs and removable fasteners. Keep ports, connectors and the battery accessible.','Separate packaging from structure: removing the cover must not disturb wheel alignment or sensor calibration.','laser'),
 ('electronics','07','Control & motor interface','Soldering / microscope bench','Design + buy','Own MCU carrier and sensor/connector board; bought dual H-bridge sized for motor startup/stall limits. PCB fabrication route awaits staff confirmation.','MCU runs encoder speed loops, watchdog and contact stop. A higher-level computer sends bounded velocity commands.','electronics'),
 ('sensor','08','Range / optional camera mount','PETG FDM + bought sensors','Make + buy','Two low front range sensors; an optional camera on a bolted short mast. Camera box in model is an envelope, not a selected product.','Measure blind zone and sensor-to-body transform. Keep the mast short; vibration and cable pull alter calibration.','print'),
 ('contact','09','Compliant contact bumper','TPU FDM + PETG guide','Make','Separate TPU pad mechanically captured on a rigid guided bumper. Print TPU flat to reduce supports; hard stops limit travel. No AMS material change required.','Compliance protects contact and spreads load; it does not measure force. Start with switches, then add a calibrated load cell.','print'),
 ('force','10','Contact sensing & stops','Electronics bench / FDM','Buy + integrate','Two travel guides, return springs, microswitches and optional load cell + amplifier. Shown as conceptual envelopes; rated load and deflection remain to be selected.','Design a load path and overload bypass. Motor current is only a secondary signal, not calibrated contact force.','electronics')]
groups=[dict(zip(['id','number','name','machine','ownership','plan','lesson','process'],g)) for g in GROUPS]
def box(group,name,c,size,color,explode=(0,0,0)):
 x,y,z=c;a,b,d=[v/2 for v in size];v=[[x+sx*a,y+sy*b,z+sz*d] for sx,sy,sz in [(-1,-1,-1),(1,-1,-1),(1,1,-1),(-1,1,-1),(-1,-1,1),(1,-1,1),(1,1,1),(-1,1,1)]]
 f=[[0,3,2,1],[4,5,6,7],[0,4,7,3],[1,2,6,5],[0,1,5,4],[3,7,6,2]]
 parts.append(dict(group=group,name=name,vertices=v,faces=f,color=color,explode=list(explode)))
def cyl(group,name,c,r,length,axis,color,explode=(0,0,0),n=24):
 v=[]
 for end in [-.5,.5]:
  for i in range(n):
   t=2*math.pi*i/n;p=[r*math.cos(t),length*end,r*math.sin(t)]
   if axis=='x':p=[p[1],p[0],p[2]]
   if axis=='z':p=[p[0],p[2],p[1]]
   v.append([p[j]+c[j] for j in range(3)])
 f=[list(range(n-1,-1,-1)),list(range(n,2*n))]+[[i,(i+1)%n,(i+1)%n+n,i+n] for i in range(n)]
 parts.append(dict(group=group,name=name,vertices=v,faces=f,color=color,explode=list(explode)))
box('chassis','Lower plywood plate',(0,62,0),(220,6,280),'#bba077')
for x in [-90,90]:
 box('mounts','Longitudinal rail',(x,73,0),(12,16,240),'#557d77',(0,15,0))
 box('mounts','Motor clamp foot',(x,49,35),(32,23,46),'#557d77',(x*.25,0,0))
for sign in [-1,1]:
 x=sign*122
 cyl('drive','Rubber drive tire',(x,50,35),50,24,'x','#303a3d',(sign*50,0,0),32)
 cyl('drive','Wheel hub',(x+sign*13,50,35),27,2,'x','#c1c7c5',(sign*50,0,0))
 cyl('drive','Axle cap',(x+sign*15,50,35),7,3,'x','#718287',(sign*50,0,0))
 cyl('drive','Motor can',(sign*72,50,35),16,48,'x','#a0afb2',(sign*25,0,0))
 cyl('drive','Gearbox',(sign*103,50,35),18,14,'x','#778b91',(sign*25,0,0))
 for k in range(6):
  t=k*math.pi/3;cyl('drive','Hub screw',(x+sign*15,50+20*math.cos(t),35+20*math.sin(t)),2,2,'x','#35484b',(sign*50,0,0),8)
cyl('caster','Caster ball',(0,14,-106),14,22,'x','#b8c0be',(0,-18,-35))
box('caster','Caster yoke',(0,35,-106),(38,28,35),'#677b7a',(0,-18,-35))
box('caster','Caster spacer',(0,54,-106),(42,10,42),'#557d77',(0,-18,-35))
box('battery','Battery tray',(0,69,-40),(98,8,110),'#557d77',(0,30,-25))
box('battery','Protected pack envelope',(0,91,-40),(86,36,96),'#4b5658',(0,30,-25))
for z in [-70,-15]:box('battery','Battery strap',(0,111,z),(92,3,9),'#202d32',(0,30,-25))
box('battery','Fuse/regulator enclosure',(-64,93,-68),(25,28,45),'#dfa252',(-20,50,-20))
for x in [-84,84]:
 for z in [-107,107]:cyl('deck','Standoff',(x,99,z),5,66,'y','#697976',(0,65,0),12)
box('deck','Removable upper deck',(0,135,-15),(198,3,236),'#cdbb94',(0,110,0))
box('electronics','Controller PCB',(-42,145,-20),(62,3,66),'#347773',(-20,145,0))
box('electronics','Microcontroller',(-42,151,-20),(24,8,30),'#293d44',(-20,145,0))
for z in [-44,4]:box('electronics','Board connectors',(-42,151,z),(52,7,7),'#d8d6c5',(-20,145,0))
box('electronics','Motor driver PCB',(38,145,-20),(58,3,62),'#347773',(25,145,0))
box('electronics','Driver heat sink',(38,153,-20),(35,13,35),'#343f47',(25,145,0))
box('battery','Motor power cut base',(63,144,-91),(27,14,27),'#303c43',(25,115,-10))
cyl('battery','Red power-cut knob',(63,155,-91),13,10,'y','#ba5547',(25,115,-10))
box('sensor','Mast foot',(0,143,75),(50,10,30),'#557d77',(0,130,30))
box('sensor','Short mast',(0,178,75),(18,60,16),'#557d77',(0,130,30))
box('sensor','Camera envelope',(0,209,82),(78,26,27),'#d9ddda',(0,130,30))
for x in [-24,24]:cyl('sensor','Camera lens',(x,211,97),8,3,'z','#263b42',(0,130,30))
for x in [-74,74]:
 box('sensor','Front range enclosure',(x,95,125),(29,26,22),'#557d77',(x*.2,60,30))
 cyl('sensor','Range aperture',(x,96,138),7,3,'z','#1e363d',(x*.2,60,30))
box('contact','Rigid bumper back',(0,56,148),(182,28,8),'#557d77',(0,0,65))
box('contact','Replaceable TPU pad',(0,56,156),(186,32,8),'#cedd6c',(0,0,80))
for x in [-61,61]:
 cyl('force','Travel guide',(x,56,130),4,28,'z','#aab9ba',(0,0,40),12)
 box('force','Guide block',(x,55,116),(22,25,18),'#557d77',(0,0,20))
 box('force','Contact switch',(x,75,125),(12,8,18),'#d4a366',(0,25,40))
box('force','Load-cell envelope',(0,56,125),(44,12,24),'#c3c6c0',(0,0,40))
scene={'revision':'r1','units':'mm','status':'Concept only: dimensions and modules are proposed; no fabrication or fit validation.','groups':groups,'parts':parts,'targets':{'mass_kg':2.5,'speed_m_s':.25,'wheel_diameter_mm':100,'track_mm':244,'chassis_mm':[220,6,280]}}
OUT.mkdir(parents=True,exist_ok=True);(OUT/'concept.json').write_text(json.dumps(scene,separators=(',',':'))+'\n')
obj=['# Mobile robot r1 concept envelopes. Units mm. Not manufacturing CAD.'];idx=1
for p in parts:
 obj.append('o '+p['group']+'_'+p['name'].replace(' ','_'))
 obj.extend('v '+' '.join(f'{c:.5f}' for c in v) for v in p['vertices'])
 obj.extend('f '+' '.join(str(idx+i) for i in face) for face in p['faces']);idx+=len(p['vertices'])
(OUT/'mobile-robot-r1.obj').write_text('\n'.join(obj)+'\n')
print(len(parts),'mesh envelopes;',len(groups),'functional groups')
