#!/usr/bin/env python3
"""Concept 27 R2: local, editable, checksum-locked STEP/BRep derivative.

Nothing executes an input script. All input electronics are immutable geometry;
only rigid transforms are applied. Dimensions are mm. Positive Z faces front.
This is not a native Fusion timeline and does not certify physical performance.
Run from any directory: python source/build_Concept_27_R2.py [--fast]
"""
from pathlib import Path
import argparse, copy, hashlib, json, math, time
import build123d as b
import numpy as np
import trimesh
from OCP.BRepCheck import BRepCheck_Analyzer
from OCP.ShapeFix import ShapeFix_Shape
from OCP.TopoDS import TopoDS
from bd_warehouse.fastener import CounterSunkScrew, SocketHeadCapScrew, PanHeadScrew
ROOT=Path(__file__).resolve().parents[1]
P=json.loads((ROOT/'source/Concept_27_R2_parameters.json').read_text())
PARTS={}; META={}; HW={}; FAST={}; FREE={}; DATUMS={}; BASE={}
COLORS={'chalk':(.89,.88,.84),'carrier':(.43,.42,.38),'rim':(.49,.47,.43),'orange':(.8,.255,.095),'pewter':(.6,.59,.55),'bond':(.24,.23,.21),'lens':(.74,.76,.73),'foam':(.20,.23,.24),'silicone':(.35,.43,.44),'sheet':(.65,.70,.68),'clear':(.58,.74,.76),'steel':(.53,.55,.57),'nylon':(.65,.63,.54),'pcb':(.10,.36,.22),'battery':(.28,.38,.58),'speaker':(.18,.18,.19),'antenna':(.39,.27,.19)}

def norm(s):
    if s is None:return None
    if isinstance(s,(list,tuple,b.ShapeList)):
        return b.Compound(children=list(s)) if len(s) else None
    return s

def solids(s):
    s=norm(s);return [] if s is None else list(s.solids())
def vol(s):return sum(x.volume for x in solids(s))
def bounds(s):
    q=norm(s).bounding_box();return [float(v) for v in (q.min.X,q.max.X,q.min.Y,q.max.Y,q.min.Z,q.max.Z)]
def valid(s):return BRepCheck_Analyzer(norm(s).wrapped,True,False).IsValid()
def one(s,label='solid'):
    ss=solids(s)
    if len(ss)!=1:raise ValueError(f'{label}: expected 1 solid, got {len(ss)}')
    a=ss[0]
    if not valid(a):
        # A cut through an inherited snap-post face can leave a wire with
        # ambiguous orientation. OCC ShapeFix preserves its surfaces and
        # corrects topology; require unchanged dimensions/volume and serial QA.
        old_volume=a.volume;old_bounds=np.array(bounds(a))
        fixer=ShapeFix_Shape(a.wrapped);fixer.Perform()
        candidate=b.Solid(TopoDS.Solid_s(fixer.Shape()))
        if valid(candidate) and abs(candidate.volume-old_volume)<max(1e-6,abs(old_volume)*1e-9) and np.max(abs(np.array(bounds(candidate))-old_bounds))<1e-6:
            a=candidate
            DATUMS.setdefault('kernel_topology_repairs',[]).append({'part':label,'volume_before':old_volume,'volume_after':a.volume,'method':'OCC ShapeFix; serial BRepCheck valid, geometry bounds and volume unchanged'})
            print('REPAIRED TOPOLOGY',label,flush=True)
    if not valid(a) or a.volume<=0:raise ValueError(f'{label}: invalid/non-positive BRep')
    a.label=label;return a

def box(x0,x1,y0,y1,z0,z1):return b.Solid.make_box(x1-x0,y1-y0,z1-z0,b.Plane(origin=(x0,y0,z0)))
def cz(x,y,r,z0,z1):return b.Solid.make_cylinder(r,z1-z0,b.Plane(origin=(x,y,z0)))
def cx(x0,x1,y,z,r):return b.Solid.make_cylinder(r,x1-x0,b.Plane(origin=(x0,y,z),x_dir=(0,1,0),z_dir=(1,0,0)))
def rr(w,h,r,z0,z1,x=0,y=0):return one(b.extrude(b.Plane(origin=(x,y,z0))*b.RectangleRounded(w,h,r),amount=z1-z0))
def rrx(w,h,r,x0,x1,y,z):return one(b.extrude(b.Plane(origin=(x0,y,z),x_dir=(0,1,0),z_dir=(1,0,0))*b.RectangleRounded(w,h,r),amount=x1-x0))
def union(*xs):
    xs=[norm(x) for x in xs if x is not None]
    if not xs:return None
    return norm(xs[0].fuse(*xs[1:])) if len(xs)>1 else xs[0]
def cut(s,*tools):return norm(norm(s).cut(*[norm(t) for t in tools]))
def common(s,t):return norm(norm(s).intersect(norm(t)))
def move(s,t):return norm(s).moved(b.Location(t))
def ring(x,y,ro,ri,z0,z1):return one(cut(cz(x,y,ro,z0,z1),cz(x,y,ri,z0-.1,z1+.1)))
def add(name,shape,material,role,process=None,mount=None):
    shape=one(shape,name);shape.color=b.Color(*COLORS[material]);PARTS[name]=shape
    META[name]={'material':material,'role':role,'process':process or ('die-cut/laminate compliant stock' if material in ('foam','silicone') else 'cut sheet or adhesive' if material in ('bond','sheet') else 'clear optical prototype' if material=='clear' else 'rigid PA12 prototype'), 'mount':mount or ''}
    return shape

def replace(name,shape):
    s=one(shape,name);s.color=PARTS[name].color;PARTS[name]=s;return s

def add_pad(name,shape,material,role,free=None,mount='captured between named mating surfaces'):
    s=add(name,shape,material,role,mount=mount)
    if free is not None:
        f=one(free,name+'_FREE_STATE');f.color=s.color;FREE[name]=f
        META[name]['free_volume_mm3']=f.volume;META[name]['installed_volume_mm3']=s.volume
        META[name]['free_state_note']='Free stock and compressed installed shape are separate. Strain/force/tolerance behaviour is not simulated.'
    return s

def screw(name,size,length,kind,top_or_seat,xy,direction=1,material='steel'):
    if kind=='cs':s=CounterSunkScrew(size,length,'iso7046',simple=True)
    elif kind=='socket':s=SocketHeadCapScrew(size,length,'iso4762',simple=True)
    else:s=PanHeadScrew(size,length,'iso1580',simple=True)
    # Library screws point -Z. Rear entry reverses their direction into +Z.
    s=s.moved(b.Location((xy[0],xy[1],top_or_seat),(180,0,0))) if direction==1 else s.moved(b.Location((xy[0],xy[1],top_or_seat)))
    s=one(s,name);s.color=b.Color(*COLORS[material]);FAST[name]=s
    DATUMS.setdefault('fasteners',[]).append({'name':name,'standard':{'cs':'ISO7046','socket':'ISO4762','pan':'ISO1580'}[kind],'size':size,'length_mm':length,'head_reference_z':top_or_seat,'axis_xy':xy,'direction_z':direction,'material':material,'thread_geometry':'minor-diameter core and modeled drive recess; helical threads omitted; full major-diameter envelopes checked separately'})
    return s

def csk(x,y,z_back,r0=2.0,r1=1.15,depth=.85):
    return b.Solid.make_cone(r0,r1,depth,b.Plane(origin=(x,y,z_back)))

def load_r1():
    path=ROOT/'inputs/R1/Walkie_One_Concept_27_R1.step'
    assert hashlib.sha256(path.read_bytes()).hexdigest()==P['r1_sha256']
    a=b.import_step(path);assert len(a.solids())==17
    manifest=json.loads((ROOT/'inputs/R1/part_manifest.json').read_text())
    for i,s in enumerate(a.solids(),1):
        sv=s.volume;sb=np.array(bounds(s))
        m=min(manifest,key=lambda m:abs(m['volume_mm3']-sv)+max(abs(np.array(m['bounds_mm'])-sb)))
        suffix=m['name'].split('_',2)[2].replace('_Unchanged','')
        name=f'C27_R2_{i:02d}_{suffix}'
        mat=m['material'];s=add(name,s,mat,m['name']+' inherited source geometry',m['fabrication'],'inherited R1 mechanical interface')
        BASE[name]=copy.copy(s);META[name]['r1_name']=m['name']
    DATUMS['r1_import']={'sha256':P['r1_sha256'],'solid_count':17}

def load_hw():
    specs={
      'PCB':('ORIGINAL_SIM7670G_PCB.step',P['pcb']['translation'],P['pcb']['rotation'],'pcb'),
      'Battery':('REF_battery_body_and_protection.step',(0,51.28,-18.26),(0,0,180),'battery'),
      'Speaker':('REF_speaker_body_and_mounting_ears.step',(43.5,42,-13.05),(0,0,90),'speaker'),
      'LTE':('REF_lte_flex_body.step',(0,13.5,-10.98),(0,0,0),'antenna'),
      'Microphone':('REF_Adafruit3421_mic.step',(0,4.25,-13.85),(0,0,0),'pcb'),
      'Front_Switch':('FRONT_OMRON_B3F4000.step',(0,-9,-10.88),(0,0,0),'carrier'),
      'PTT_Switch':('PTT_OMRON_B3F3150.step',(.95,2,-11.3),(0,0,0),'carrier'),
      'Volume_Up_Switch':('VOLUME_UP_OMRON_B3F3150.step',(-.95,-7,-11.3),(0,0,0),'carrier'),
      'Volume_Down_Switch':('VOLUME_DOWN_OMRON_B3F3150.step',(-.95,-11,-11.3),(0,0,0),'carrier'),
      'LED':('REF_Kingbright_RGB_LED.step',(-14,24,-12.45),(0,0,0),'clear')}
    rec={}
    for name,(fn,t,r,mat) in specs.items():
        path=ROOT/'inputs/hardware'/fn;s=b.import_step(path);srcvol=vol(s)
        moved=s.moved(b.Location(t,r))
        if name=='LED':moved=moved.moved(b.Location((-.3,0,-7),(0,90,0)))
        moved.label='C27_R2_REF_'+name
        # Flatten only the assembly hierarchy, never its separate solids or geometry.
        children=[]
        for k,x in enumerate(moved.solids(),1):
            x.label=f'C27_R2_REF_{name}_{k:03d}';x.color=b.Color(*COLORS[mat]);children.append(x)
        result=b.Compound(label='C27_R2_REF_'+name,children=children);HW[name]=result
        rec[name]={'source':str(path.relative_to(ROOT)),'source_sha256':hashlib.sha256(path.read_bytes()).hexdigest(),'translation_mm':t,'rotation_xyz_deg':r,'second_transform':{'translation_mm':[-.3,0,-7],'rotation_xyz_deg':[0,90,0]} if name=='LED' else None,'source_volume_mm3':srcvol,'placed_volume_mm3':vol(result),'bounds_mm':bounds(result),'solids':len(children),'valid':all(valid(q) for q in children)}
    DATUMS['hardware_transforms']=rec

def closure_and_front_clamp():
    fn=next(k for k in PARTS if k.startswith('C27_R2_01_'));cn=next(k for k in PARTS if k.startswith('C27_R2_05_'))
    f=PARTS[fn];c=PARTS[cn];bn=next(k for k in PARTS if k.startswith('C27_R2_02_'));back=PARTS[bn]
    for x,y in P['closure']['centers']:
        f=cut(f,cz(x,y,1.05,-12.2,P['closure']['pilot_end_z']))
        back=cut(back,cz(x,y,2.4,-26,P['closure']['seat_z']))
        screw(f'C27_R2_HW_Closure_{x}_{y}','M2.5-0.45',P['closure']['screw_length'],'socket',P['closure']['seat_z'],(x,y))
    for x,y in P['front_clamp']['centers']:
        # The accessible main pilot exists. Join only its blind end to the tiny
        # enclosed cavity: this removes the 0.2-mm membrane, not the front skin.
        f=cut(f,cz(x,y,.85,-11.0,-2.6))
        # Flush real countersunk head protects the populated PCB space.
        c=cut(c,cz(x,y,1.15,-12.4,-10.8),csk(x,y,-12.3,1.96,1.15,.81))
        screw(f'C27_R2_HW_Front_Clamp_{x}','M2-0.4',8,'cs',-12.3,(x,y))
    replace(fn,f);replace(cn,c);replace(bn,back)
    DATUMS['front_clamp']={'pilot_main_old_z':[-10.85,-3.4],'old_separate_void_z':[-3.2,-2.6],'removed_web_z':[-3.4,-3.2],'r2_pilot_z':[-10.85,-2.6],'front_skin_over_pilot_mm':2.6,'assembly_access':'Install/remove clamp screws with PCB removed; no screw or tool passes through the PCB.'}

def pcb_pads():
    fn=next(k for k in PARTS if k.startswith('C27_R2_01_'));bn=next(k for k in PARTS if k.startswith('C27_R2_02_'))
    front=PARTS[fn];back=PARTS[bn]
    # Remove the four inherited rigid overhang plates that occupy actual SMD
    # bodies. New paired supports use SR2's verified edge zones, registered to
    # the unchanged original PCB (rotation180 aboutY; Y shift-8.91499995).
    for side in (-1,1):
        x0,x1=(-31.8,-27.3) if side<0 else (27.3,31.8)
        for y in (-25.04,-42.04):back=cut(back,box(x0,x1,y-3.15,y+3.15,-15.2,-14.4))
    regions=[(29.1,29.9,7.585,14.585),(-29.9,-29.1,-12.415,-5.415),(-28.5,-23.5,-52.94,-52.14),(25.5,28.5,26.06,26.86)]
    supports=[(29,30.4,7.335,14.835),(-30.4,-29,-12.665,-5.165),(-28.75,-23.25,-53.04,-51.84),(25.25,28.75,25.81,27.01)]
    for j,((x0,x1,y0,y1),(a,c,d,e)) in enumerate(zip(regions,supports),1):
        support=box(a,c,d,e,-12.71,-2.1)
        if j==2:
            support=cut(support,box(-29.6501,-23.9,d-.1,e+.1,-5.4,-1.9))
        front=union(front,support)
        back=union(back,box(a,c,d,e,-21.81,-15.01))
        add_pad(f'C27_R2_PCB_Front_Seat_{j}',box(x0,x1,y0,y1,P['pcb']['front_z'],-12.71),'silicone','paired bare-FR4 edge seat',box(x0,x1,y0,y1,-13.01,-12.71),'new integral front ledge / verified bare board edge')
        add_pad(f'C27_R2_PCB_Rear_Preload_{j}',box(x0,x1,y0,y1,-15.01,P['pcb']['rear_z']),'foam','compliant preload on verified bare-FR4 edge',box(x0,x1,y0,y1,-15.01,-14.01),'new integral rear stalk / bare board edge')
    replace(fn,front);replace(bn,back)
    DATUMS['pcb_support_regions_xy']=regions

def side_actuator_passages():
    for hw,sign in [('PTT_Switch',1),('Volume_Up_Switch',-1),('Volume_Down_Switch',-1)]:
        a=bounds(list(HW[hw].solids())[2])
        passage=box(a[0]-.15+min(0,sign*.5),a[1]+.15+max(0,sign*.5),a[2]-.15,a[3]+.15,a[4]-.15,a[5]+.15)
        for num in (1,2):
            name=next(k for k in PARTS if k.startswith(f'C27_R2_{num:02d}_'))
            replace(name,cut(PARTS[name],passage))
    DATUMS['side_actuator_passages']='Actual third-solid plungers plus0.5mm inward travel and0.15mm clearance; matching hidden drive pads at Z-11.3. Physical switch overtravel/return not certified.'

def battery_retention():
    fn=next(k for k in PARTS if k.startswith('C27_R2_01_'));bn=next(k for k in PARTS if k.startswith('C27_R2_02_'));f=PARTS[fn];back=PARTS[bn]
    zf=P['battery']['pouch_front'];zb=P['battery']['body_bottom']
    for side in (-1,1):
        x0,x1=(-29,-20.5) if side<0 else (20.5,29)
        land=rr(x1-x0,27,.8,-7.9,-6.6,x=(x0+x1)/2,y=45)
        f=union(f,land)
        def frontpad(z0,z1):
            a=rr(8,26,.7,z0,z1,x=side*24.75,y=45)
            if side<0:a=cut(a,box(-29,-15.7,43.48,49.08,z0-.1,z1+.1))
            return a
        installed=sorted(solids(frontpad(zf,-7.9)),key=lambda q:q.center().Y)
        free=sorted(solids(frontpad(-9.9,-7.9)),key=lambda q:q.center().Y)
        assert len(installed)==len(free)
        for k,(inst,stock) in enumerate(zip(installed,free),1):
            suffix=f'_Segment_{k}' if len(installed)>1 else ''
            add_pad(f'C27_R2_Battery_Front_Pad_{side}'+suffix,inst,'foam','broad pouch preload; separate cut pads clear the protection module',stock,'integral front support platen / pouch only, not protection PCB')
        bx=side*21.25
        # Widen the nominal rear pads, outside the central antenna lane.
        region=box(bx-8,bx+8,31.5,61.5,-17.3001,-16.0)
        back=cut(back,region)
        back=union(back,rr(16,30,1,-21.81,-17.3,x=bx,y=46.5))
        rear=lambda z0,z1:rr(14.5,27.5,1,z0,z1,x=bx,y=46.5)
        add_pad(f'C27_R2_Battery_Rear_Pad_{side}',rear(-17.3,zb),'foam','broad rear pouch support',rear(-17.3,-16.3),'rear platen / pouch rear face')
        # Existing rails are extended only into their own datum plane.
        rail=box(-31.66,-30.26,29.0,63.9,-17.4,-12.15) if side<0 else box(30.26,31.66,29.0,63.9,-17.4,-12.15)
        back=union(back,rail)
        xl0,xl1=(-30.26,-30) if side<0 else (30,30.26)
        add_pad(f'C27_R2_Battery_Side_Liner_{side}',box(xl0,xl1,31,62,-15.8,-12.15),'silicone','soft XY rail liner',box(xl0,xl1,31,62,-15.8,-12.15),'existing/extended side rail; installed zero-clearance pouch edge')
    # Divider stays between the battery and PCB, never above either board.
    back=union(back,box(-30.26,30.26,27.22,28.02,-17.4,-9.85))
    add_pad('C27_R2_Battery_Lower_Liner',box(-27,27,28.02,28.28,-15.8,-10.1),'silicone','soft lower divider liner',mount='shared no-stack divider / battery lower edge')
    for side in (-1,1):
        x0,x1=(-29,-10) if side<0 else (10,29)
        back=union(back,box(x0,x1,64.54,65.94,-17.4,-9.85))
        add_pad(f'C27_R2_Battery_Upper_Liner_{side}',box(x0+.5,x1-.5,64.28,64.54,-15.8,-10.1),'silicone','soft upper end land; central finger removal opening preserved',mount='split top rail / battery upper edge')
    replace(fn,f);replace(bn,back)


def speaker_retention():
    fn=next(k for k in PARTS if k.startswith('C27_R2_01_'));f=PARTS[fn]
    # Replace only snap overhangs that would obstruct a rear drop-in path.
    f=cut(f,cz(0,42,20.25,-8.1,-2.651))
    # Annular acoustic seal stays outside the W hole field.
    gasket=lambda z0,z1:ring(0,42,19.8,17.9,z0,z1)
    add_pad('C27_R2_Speaker_Front_Gasket',gasket(-2.65000038,-2.2),'foam','speaker-to-front acoustic seal',gasket(-2.8,-2.2),'front skin / actual 40mm speaker frame')
    for j,y in enumerate(P['speaker']['keeper_y'],1):
        keeper=rr(23.2,4.4,1,-6.35,-4.95,y=y)
        for x in (-9.1,9.1):
            f=union(f,cz(x,y,2.55,-4.95,-2.1))
            f=cut(f,cz(x,y,.85,-5.1,-1.1))
            keeper=cut(keeper,cz(x,y,1.15,-6.5,-4.8),csk(x,y,-6.35,1.96,1.15,.81))
            screw(f'C27_R2_HW_Speaker_{j}_{x}','M2-0.4',5,'cs',-6.35,(x,y))
        add(f'C27_R2_Speaker_Ear_Keeper_{j}',keeper,'carrier','screw-fixed rear restraint on supplied mounting ear',mount='two M2 screws into front-shell bosses')
        pad=lambda z0,z1:rr(10.8,3.6,.5,z0,z1,y=y)
        add_pad(f'C27_R2_Speaker_Ear_Preload_{j}',pad(-4.95,-3.450000095),'foam','broad compliant ear pad, avoiding magnet and battery conflict',pad(-4.95,-2.95),'keeper / rigid speaker mounting ear; no pressure on magnet or diaphragm')
    replace(fn,f)
    DATUMS['speaker_equivalence']='Two screwed mounting-ear keepers plus compliant ear pads replace SR2 magnet bridge: actual pouch-to-magnet axial gap is only 1.91 mm except protection module; no rigid bridge is placed across the pouch. Original seat and radial locators are retained with drop-in clearance relief.'


def microphone_retention():
    fn=next(k for k in PARTS if k.startswith('C27_R2_01_'));f=PARTS[fn];cy=-13.238
    # Keep the approved X0/Y-14 acoustic bore and gasket floor untouched.
    f=cut(f,box(-10,10,-22,-5.4,-6.6,-2.2))
    wall=cut(rr(18.51,14.7,.9,-5.1,-2.1,y=cy),rr(17.01,13.2,.35,-5.2,-2.09,y=cy))
    f=union(f,wall)
    for x in (-11.5,11.5):
        f=union(f,cz(x,cy,2.3,-5.1,-2.1),box(min(x,-8.5),max(x,8.5),cy-.9,cy+.9,-3.65,-2.1))
        # The crossbar is limited below to its side arm, not across the module.
    # Remove any crossbar material inside the electronics pocket.
    f=cut(f,rr(17.01,13.2,.35,-5.2,-2.201,y=cy))
    for x in (-11.5,11.5):f=cut(f,cz(x,cy,.85,-5.2,-2.0))
    frame=cut(rr(17.5,13.8,.8,-6.35,-5.1,y=cy),rr(14.4,10.7,.6,-6.5,-5.0,y=cy))
    for x in (-11.5,11.5):
        frame=union(frame,cz(x,cy,2.4,-6.35,-5.1),box(min(x,-7.5),max(x,7.5),cy-1,cy+1,-6.35,-5.1))
    frame=cut(frame,rr(14.4,10.7,.6,-6.5,-5.0,y=cy))
    for x in (-11.5,11.5):
        frame=cut(frame,cz(x,cy,1.15,-6.5,-5),csk(x,cy,-6.35,1.96,1.15,.81))
        screw(f'C27_R2_HW_Mic_{x}','M2-0.4',4,'cs',-6.35,(x,cy))
    add('C27_R2_Microphone_Removable_Keeper',frame,'carrier','serviceable centered-microphone rear frame',mount='two rear-entry M2 screws; remove before lifting breakout')
    def pad(z0,z1):return cut(rr(16.35,12.45,.55,z0,z1,y=cy),rr(14.5,10.6,.45,z0-.1,z1+.1,y=cy))
    add_pad('C27_R2_Microphone_Rear_Preload',pad(-5.1,-4.85),'silicone','dielectric rear breakout edge preload',pad(-5.1,-4.75),'keeper / unpopulated breakout rear face')
    # Front support tabs contact verified clear corners of the breakout.
    for j,(x,y) in enumerate([(-7.5,-18),(-7.5,-8.5),(7.5,-18),(7.5,-8.5)],1):
        f=union(f,box(x-.65,x+.65,y-.75,y+.75,-3.85,-2.1))
        add_pad(f'C27_R2_Mic_Front_Seat_{j}',box(x-.6,x+.6,y-.7,y+.7,-4.05,-3.85),'silicone','breakout front corner dielectric seat',mount='front shell ledge / clear FR4 corner')
    seal=ring(0,-14,1.1,.75,-3.07,-2.251)
    add_pad('C27_R2_Microphone_Acoustic_Seal',seal,'silicone','sealed duct aligned to actual MEMS port and approved X0/Y-14 aperture',ring(0,-14,1.1,.75,-3.201,-2.251),mount='MEMS lid / membrane at existing raised acoustic nozzle, not recessed surrounding floor')
    mesh=cz(0,-14,1.1,-2.251,-2.201)
    add('C27_R2_Microphone_Acoustic_Membrane',mesh,'sheet','porous acoustic sheet, not a solid printed plug',process='cut rated acoustic membrane sheet',mount='clamped by microphone seal to existing gasket floor')
    replace(fn,f)


def led_and_optics():
    fn=next(k for k in PARTS if k.startswith('C27_R2_01_'));f=PARTS[fn]
    # Preserve lens and exterior press-fit bore; remove only obsolete internal
    # vertical hooks, which cannot accept the actual complete leaded LED.
    f=cut(f,box(-4.4,4.4,.5,9.5,-11.4,-3.5))
    cradle=rr(13.8,14.6,1,-8.3,-2.1,x=-7,y=5)
    cavity=union(cx(-15.6,-10.85,5,-7,2.8),cx(-10.85,-9.35,5,-7,3.2),cx(-9.35,-1.7,5,-7,2.75))
    optical_clearance=union(cx(-2.4,.25,5,-7,2.35),cz(0,5,2.35,-7.25,-3.45),b.Solid.make_sphere(2.35,b.Plane(origin=(0,5,-7))))
    lens_keepout=box(-2.9,2.9,-.1,10.1,-3.7,1)
    cradle=cut(cradle,cavity,box(-15,-12.5,1,9,-8.4,-2.05),optical_clearance,lens_keepout)
    f=union(f,cradle)
    keeper=rr(12.8,14.6,1,-10.55,-8.3,x=-7,y=5)
    keeper=cut(keeper,cavity,box(-15,-12.5,1,9,-10.7,-8.2),optical_clearance)
    for y in (-.4,10.4):
        f=cut(f,cz(-7,y,.85,-8.4,-5.2))
        keeper=cut(keeper,cz(-7,y,1.15,-10.7,-8.2),csk(-7,y,-10.55,1.96,1.15,.81))
        screw(f'C27_R2_HW_LED_{y}','M2-0.4',5,'cs',-10.55,(-7,y))
    add('C27_R2_LED_Horizontal_Removable_Keeper',keeper,'carrier','actual uncut leaded LED held horizontally in front cavity',mount='two M2 screws; rear split saddle')
    # Thin compliant saddle liners: selected tangential flats, not rigid radial pressure.
    for j,x in enumerate((-10.1,-6.4),1):
        radius=2.95 if j==1 else 2.5
        zback=-7-radius
        add_pad(f'C27_R2_LED_Saddle_Pad_{j}',box(x-.3,x+.3,4.65,5.35,zback-.22,zback),'silicone','LED anti-rattle saddle contact',mount='keeper saddle / LED body or flange')
    # Actual CAD light coupler, not an assertion of optical efficiency.
    coupler=union(cx(-1.8,0,5,-7,2.1),cz(0,5,2.1,-7,-3.55),b.Solid.make_sphere(2.1,b.Plane(origin=(0,5,-7))))
    coupler=one(cut(coupler,b.Solid.make_sphere(2.7,b.Plane(origin=(-4.5,5,-7)))))
    add('C27_R2_Internal_Right_Angle_Light_Coupler',coupler,'clear','internal optical turn; approved external lens remains unchanged',mount='optical bond film at original lens collector; lateral opaque cradle')
    film=cz(0,5,2.1,-3.55,-3.5)
    add('C27_R2_Optical_Coupling_Film',film,'bond','clear optical adhesive thickness allowance',process='optically clear adhesive film',mount='light coupler / unchanged R1 lens')
    replace(fn,f)
    DATUMS['led_tradeoff']={'original_placement_lens_overlap_mm3':19.25002160798034,'original_placement_FR4_overlap_mm3':1.3285007073206434,'solution':'Rigidly rotate actual unmodified LED/legs horizontally, add internal optical turn. No exterior lens change. Light throughput/color balance unqualified.'}


def lte_retention():
    bn=next(k for k in PARTS if k.startswith('C27_R2_02_'));back=PARTS[bn]
    # Lift-out retention replaces only overhanging lips, not perimeter datums.
    for x in (-10,10):
      for y in (-20,53):back=cut(back,box(x-2.1,x+2.1,y-2.5,y+2.5,-21.0,-19.8))
    frame=cut(rr(25.6,104.8,.8,-20.83,-19.63,y=16.5),rr(19.0,96.5,.5,-21,-19.5,y=13.5))
    # Leave the actual antenna tail entirely uncovered and uncompressed.
    frame=cut(frame,box(-3,3,60,70,-21,-19.5))
    for x in (-14,14):
      for y in (-32,64):
        frame=union(frame,cz(x,y,2.7,-20.83,-19.63),box(min(x,-11.5),max(x,11.5),y-1.5,y+1.5,-20.83,-19.63))
    # Re-open the active-region window after joining mounting ears.
    frame=cut(frame,rr(19.0,96.5,.5,-21,-19.5,y=13.5),box(-3,3,60,70,-21,-19.5))
    for x in (-14,14):
      for y in (-32,64):
        back=union(back,cz(x,y,2.7,-21.81,-20.83))
        back=cut(back,cz(x,y,.85,-22.85,-20.7))
        frame=cut(frame,cz(x,y,1.15,-21,-19.5))
        screw(f'C27_R2_HW_LTE_{x}_{y}','M2-0.4',3,'pan',-19.63,(x,y),direction=-1,material='nylon')
    frame=union(frame,box(11.9,14.3,20.6,28.0,-20.83,-19.63))
    frame=cut(frame,box(-20,-11.7,21.8,27.4,-21,-19.4),box(9.0,11.9,21.7,27.2,-21,-19.4))
    # Clear legacy raised locators above the frame's hard-stop plane. The
    # supporting screw-boss seats atZ-20.83 are retained.
    relief=b.offset(frame,amount=.15,kind=b.Kind.INTERSECTION)
    relief=common(relief,box(-18,18,-37,70,-20.83,-18))
    back=cut(back,relief,rr(20.7,95.7,.75,-21.8,-20.75,y=13.5))
    add('C27_R2_LTE_Nonmetallic_Service_Frame',frame,'carrier','nonmetallic perimeter restraint, hard-stop gap above flex',mount='four nylon M2 screws; remove frame to lift flex')
    add('C27_R2_LTE_Dielectric_Seat',rr(20.4,95.4,.6,-21.78,-21.28,y=13.5),'sheet','0.5mm dielectric seat, no rigid clamp pressure on flex',process='cut dielectric PET sheet',mount='rear-cover seat film / flex rear face')
    add('C27_R2_LTE_Seat_Bond_Film',rr(20.4,95.4,.6,-21.8,-21.78,y=13.5),'bond','0.02mm nominal seat attachment film',mount='rear cover / dielectric seat; not bonded to antenna')
    replace(bn,back)


def usb_access():
    fn=next(k for k in PARTS if k.startswith('C27_R2_01_'));bn=next(k for k in PARTS if k.startswith('C27_R2_02_'));rn=next(k for k in PARTS if k.startswith('C27_R2_09_'))
    u=P['usb'];cy=u['center_y'];cz0=u['center_z']
    # Socket is deep inside the rolled wall. The mechanical insertion gauge
    # reserves housing access all the way to the actual connector mouth.
    access=rrx(u['body_width']+.6,u['body_height']+.6,2.8,-48,u['mouth_x']+.05,cy,cz0)
    for name in (fn,bn,rn):replace(name,cut(PARTS[name],access))
    back=PARTS[bn]
    # Remove the obsolete bridge only, leaving the rear skin at/behind -21.8.
    old=box(-22.2774,-20.0772,-45.525,-36.583,-21.799,-14.7098)
    back=cut(back,old)
    face=u['rear_x']+u['reaction_gap']
    bridge=box(face,face+2.2,cy-4.47,cy+4.47,-21.9,-14.99)
    back=union(back,bridge);replace(bn,back)
    # Mating metal shell is represented as a thin shell, not a solid blockage
    # of the receptacle tongue. This gauge is separate from reference assembly.
    tube=rrx(8.31,2.46,1.05,u['mouth_x']-.5,u['mouth_x']+6.15,cy,cz0)
    tube=cut(tube,rrx(7.91,2.06,.85,u['mouth_x']-.6,u['mouth_x']+6.25,cy,cz0))
    body=rrx(12.85,7,2.5,-49,u['mouth_x']-.5,cy,cz0)
    gauge=one(union(tube,body),'C27_R2_USB_C_Insertion_Gauge')
    gauge.color=b.Color(.25,.28,.29)
    DATUMS['usb']={'mouth_x':u['mouth_x'],'center_y':cy,'center_z':cz0,'bridge_face_x':face,'reaction_gap_mm':u['reaction_gap'],'old_rim_center_y':-41.054267,'local_access_shift_y_mm':cy+41.054267,'mating_gauge_note':'Explicit 6.65mm thin-shell gauge and 12.85x7 housing. Not a supplier plug model or electrical mating qualification.'}
    b.export_step(gauge,ROOT/'cad/Concept_27_R2_USB_C_GAUGE.step');b.export_brep(gauge,ROOT/'.cache/usb_gauge.brep')


def validate_stl(path):
    mesh=trimesh.load_mesh(path,process=True)
    # Remove only triangles collapsed to a repeated vertex by STL float32
    # quantization. Thin but nonzero CAD triangles must be retained.
    faces=mesh.faces
    keep=(faces[:,0]!=faces[:,1]) & (faces[:,1]!=faces[:,2]) & (faces[:,0]!=faces[:,2])
    removed=int(len(keep)-keep.sum())
    if removed:
        before_volume=mesh.volume;before_bounds=mesh.bounds.copy()
        mesh.update_faces(keep);mesh.remove_unreferenced_vertices()
        assert abs(mesh.volume-before_volume)<1e-9 and np.max(abs(mesh.bounds-before_bounds))<1e-9
        assert mesh.is_watertight and mesh.is_winding_consistent
        mesh.export(path)
        DATUMS.setdefault('mesh_export_cleanup',[]).append({'part':path.stem,'zero_area_triangles_removed':removed,'bounds_and_volume_unchanged':True})
    assert mesh.is_watertight and mesh.is_winding_consistent and mesh.volume>0,path.name

def export_all(fast=False):
    # Remove only obsolete outputs generated by this builder on previous runs.
    for folder,suffix,expected in [('cad/parts','.step',set(PARTS)),('meshes','.stl',set(PARTS)),('cad/free_state_pads','.step',{n+'_FREE_STATE' for n in FREE})]:
        for path in (ROOT/folder).glob('C27_R2_*'+suffix):
            if path.stem not in expected:path.unlink()
    manifests=[]
    for name,s in PARTS.items():
        s=one(s,name);b.export_brep(s,ROOT/'.cache'/f'{name}.brep')
        b.export_step(s,ROOT/'cad/parts'/f'{name}.step')
        if not fast:
            path=ROOT/'meshes'/f'{name}.stl'
            b.export_stl(s,path,tolerance=P['mesh_linear_mm'],angular_tolerance=P['mesh_angular_rad'])
            validate_stl(path)
        manifests.append(dict(name=name,**META[name],bounds_mm=bounds(s),volume_mm3=s.volume,valid=valid(s),solids=len(s.solids()),faces=len(s.faces())))
    for name,s in FREE.items():b.export_step(s,ROOT/'cad/free_state_pads'/f'{name}_FREE_STATE.step')
    for name,s in FAST.items():b.export_step(s,ROOT/'cad/hardware'/f'{name}.step');b.export_brep(s,ROOT/'.cache'/f'{name}.brep')
    for name,s in HW.items():b.export_step(s,ROOT/'cad/hardware'/f'C27_R2_REF_{name}.step');b.export_brep(s,ROOT/'.cache'/f'C27_R2_REF_{name}.brep')
    case=b.Compound(label='Walkie_One_Concept_27_R2_CASE',children=[*PARTS.values(),*FAST.values()])
    ref=b.Compound(label='Walkie_One_Concept_27_R2_REFERENCE_ASSEMBLY',children=[*PARTS.values(),*FAST.values(),*HW.values()])
    b.export_step(case,ROOT/'cad/Walkie_One_Concept_27_R2_CASE.step')
    b.export_step(ref,ROOT/'cad/Walkie_One_Concept_27_R2_REFERENCE_ASSEMBLY.step')
    DATUMS['part_manifest']=manifests
    DATUMS['assembly_counts']={'manufactured_parts':len(PARTS),'fasteners':len(FAST),'electronics_groups':len(HW),'case_solids':len(case.solids()),'reference_solids':len(ref.solids())}
    (ROOT/'audit/Concept_27_R2_build_manifest.json').write_text(json.dumps(DATUMS,indent=2))
    print('EXPORTED',DATUMS['assembly_counts'],flush=True)

def service_tool_access():
    name=next(k for k in PARTS if k.startswith('C27_R2_02_'))
    old=PARTS[name]
    # Rear antenna screws are fitted before the upper battery liners. Their
    # shaft approach clears the upper locator with two internal reliefs.
    new=cut(old,*[cz(x,64,1.5,-18.30,0) for x in (-14,14)])
    replace(name,new)
    DATUMS['lte_driver_relief']={'diameter_mm':3.0,'axes_xy':[[-14,64],[14,64]],'z_range_mm':[-18.30,0],'removed_volume_mm3':old.volume-PARTS[name].volume,'assembly_order':'Install LTE frame screws before upper battery liners; remove liners for service.'}


def aligned_carrier(shape, side, cy):
    # Original 2x2 mm R0.65 drive pad is centered at Z-15.2.
    # Cut only its protrusion outboard of the X+/-39.3 carrier back plane.
    x0,x1=(-39.3,-38.64) if side<0 else (38.64,39.3)
    old=b.Solid.make_box(x1-x0,2.1,2.2,b.Plane(origin=(x0,cy-1.05,-16.3)))
    carrier=norm(shape.cut(old))
    # Re-create the same drive-face profile at Z-11.3, matching the actual
    # checkpoint plunger. 0.01 mm root overlap ensures a connected union.
    x0,x1=(-39.31,-38.65) if side<0 else (38.65,39.31)
    plane=b.Plane(origin=(x0,cy,-11.3),x_dir=(0,1,0),z_dir=(1,0,0))
    pad=b.extrude(plane*b.RectangleRounded(2,2,.65),amount=x1-x0)
    result=norm(carrier.fuse(pad))
    assert len(result.solids())==1 and valid(result)
    return result.solids()[0]

def align_side_drives():
    for num, side, cy in [(6,-1,12),(7,1,23),(8,1,-6)]:
        name=next(n for n in PARTS if n.startswith(f"C27_R2_{num:02d}_"))
        replace(name, aligned_carrier(PARTS[name],side,cy))
        META[name]["r2_change"]="Hidden 2x2 R0.65 drive pad shifted from Z-15.2 to Z-11.3, matched to actual supplied plunger. Exterior carrier bounds/stops unchanged."

def build(fast=False):
    t=time.time()
    for folder in ["cad/parts","cad/hardware","cad/free_state_pads","meshes","audit",".cache"]:
        (ROOT/folder).mkdir(parents=True,exist_ok=True)
    load_r1();load_hw();align_side_drives()
    for fn in [closure_and_front_clamp,pcb_pads,side_actuator_passages,battery_retention,speaker_retention,microphone_retention,led_and_optics,lte_retention,usb_access]:
        print('BUILD',fn.__name__,flush=True)
        fn()
    service_tool_access()
    export_all(fast);print('BUILD SECONDS',time.time()-t,flush=True)

if __name__=='__main__':
    ap=argparse.ArgumentParser();ap.add_argument('--fast',action='store_true');args=ap.parse_args();build(args.fast)
