abse/smartwatch-flex-top-only
Three-panel flex smartwatch with two parallel R3.9 bends, all components flat-top, low-profile JST connector harnesses, split heartbeat optical windows and screw-retained PCB mounting. Modeled 39 × 50.5 × 14.5mm prototype; 100% routing, zero native DRC and shorts. Hardware qualification pending.
- Version
- 0.2.20
- License
- unset
- Stars
- 0
scripts/audit-flex.mjs
import fs from 'node:fs'
const input=process.argv[2]??'dist/seed/circuit.json',c=JSON.parse(fs.readFileSync(input)),bends=c.filter(e=>e.type==='pcb_bend'),failures=[]
for(const e of c.filter(e=>e.type==='pcb_component')){
const name=c.find(s=>s.type==='source_component'&&s.source_component_id===e.source_component_id)?.name
if(e.layer!=='top')failures.push({name,reason:'Not top mounted'})
for(const b of bends)if(e.center.x+(e.width??0)/2>Math.min(b.start.x,b.end.x)-.5&&e.center.x-(e.width??0)/2<Math.max(b.start.x,b.end.x)+.5&&Math.abs(e.center.y-b.start.y)-(e.height??0)/2<b.bend_radius*Math.abs(b.bend_angle)*Math.PI/360+.3)failures.push({name,reason:'Component in bend zone',bend:b.name})
}
for(const e of c.filter(e=>e.type==='pcb_via'||e.type==='pcb_plated_hole'))for(const b of bends)if(e.x+(e.outer_diameter??.45)/2>Math.min(b.start.x,b.end.x)-.5&&e.x-(e.outer_diameter??.45)/2<Math.max(b.start.x,b.end.x)+.5&&Math.abs(e.y-b.start.y)-(e.outer_diameter??.45)/2<b.bend_radius*Math.abs(b.bend_angle)*Math.PI/360+.5)failures.push({id:e.pcb_via_id??e.pcb_plated_hole_id,reason:'Drilled feature in bend zone',bend:b.name})
for(const e of c.filter(e=>e.type==='pcb_stiffener'))for(const b of bends){
const ys=e.shape==='polygon'?e.outline.map(p=>p.y):[e.center.y-e.height/2,e.center.y+e.height/2],lo=b.start.y-b.bend_radius*Math.abs(b.bend_angle)*Math.PI/360-.5,hi=2*b.start.y-lo
if(Math.min(...ys)<hi&&Math.max(...ys)>lo)failures.push({reason:'Stiffener in bend zone',id:e.pcb_stiffener_id,bend:b.name})
}
for(const e of c.filter(e=>e.type==='pcb_trace'))for(let i=0;i<e.route.length-1;i++){
const p=e.route[i],q=e.route[i+1]
if(p.route_type!=='wire'||q.route_type!=='wire'||p.layer!==q.layer||Math.abs(p.x-q.x)<1e-7)continue
const radius=Math.max(p.width,q.width)/2
for(const b of bends){
const half=b.bend_radius*Math.abs(b.bend_angle)*Math.PI/360+radius+.01
const lo=b.start.y-half,hi=b.start.y+half,dy=q.y-p.y
let t0=0,t1=1
if(Math.abs(dy)<1e-12){if(p.y<lo||p.y>hi)continue}
else {t0=Math.max(0,Math.min((lo-p.y)/dy,(hi-p.y)/dy));t1=Math.min(1,Math.max((lo-p.y)/dy,(hi-p.y)/dy));if(t0>t1)continue}
const x0=p.x+(q.x-p.x)*t0,x1=p.x+(q.x-p.x)*t1
if(Math.max(x0,x1)+radius<Math.min(b.start.x,b.end.x)||Math.min(x0,x1)-radius>Math.max(b.start.x,b.end.x))continue
failures.push({id:e.pcb_trace_id,index:i,reason:'Trace is not perpendicular within bend band',bend:b.name,layer:p.layer})
}
}
const report={input,components:c.filter(e=>e.type==='pcb_component').length,bends,componentMarginMM:.3,drillAndStiffenerMarginMM:.5,failures,scope:'All components flat-top; component bounds, drills and stiffeners excluded from both bend bands; every crossing trace perpendicular to the bend axis.'};fs.writeFileSync(process.argv[3]??'reports/flex-geometry.json',JSON.stringify(report,null,2)+'\n');console.log('Two-bend top-only geometry:',failures.length,'failures');if(failures.length)process.exitCode=1