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