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/style-candidates.mjs

import fs from 'node:fs'
import {analyzePcbStyle} from '../tools/pcb-style/analyzer.mjs'
import {copperModel,SpatialCopper,traceShapes} from './copper-model.mjs'
import {gap,inside,ptSeg} from './copper-geometry.mjs'
export const cache=JSON.parse(fs.readFileSync(process.env.STYLE_TREE_ANCHORS==='1'?'baseline/anchor-cache.json':'baseline-v0216/cache.json')),base=JSON.parse(fs.readFileSync('baseline-v0216/index.circuit.json'))
const board=base.find(e=>e.type==='pcb_board'),bends=base.filter(e=>e.type==='pcb_bend')
const edges=board.outline.map((a,i)=>({kind:'segment',a,b:board.outline[(i+1)%board.outline.length],r:0}))
const rect=e=>({kind:'polygon',points:e.outline??e.points??[{x:e.center.x-e.width/2,y:e.center.y-e.height/2},{x:e.center.x+e.width/2,y:e.center.y-e.height/2},{x:e.center.x+e.width/2,y:e.center.y+e.height/2},{x:e.center.x-e.width/2,y:e.center.y+e.height/2}]})
const cutouts=base.filter(e=>e.type==='pcb_cutout').map(rect)
const shapes=copperModel(cache,base),spatial=new SpatialCopper(shapes)
for(const e of base.filter(e=>e.type==='pcb_keepout'))spatial.add({...rect(e),net:'__keepout',id:e.pcb_keepout_id,layers:e.layers??['top','inner1','inner2','bottom']})
const dirs=[[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]]
const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y)
const len=r=>r.reduce((s,p,i)=>s+(i?dist(r[i-1],p):0),0)
const cross=(a,b,c)=>(b.x-a.x)*(c.y-b.y)-(b.y-a.y)*(c.x-b.x)
function clean(r){const out=[];for(const p of r){if(out.length&&dist(out.at(-1),p)<1e-9)continue;while(out.length>1&&Math.abs(cross(out.at(-2),out.at(-1),p))<1e-10&&(out.at(-1).x-out.at(-2).x)*(p.x-out.at(-1).x)+(out.at(-1).y-out.at(-2).y)*(p.y-out.at(-1).y)>=0)out.pop();out.push(p)}return out}
function rdp(r,eps){if(r.length<3)return r;let at=0,max=0;for(let i=1;i<r.length-1;i++){const d=ptSeg(r[i],r[0],r.at(-1));if(d>max){max=d;at=i}}if(max<=eps)return[r[0],r.at(-1)];return[...rdp(r.slice(0,at+1),eps).slice(0,-1),...rdp(r.slice(at),eps)]}
export function candidates(old){
 const a=old[0],b=old.at(-1),dx=b.x-a.x,dy=b.y-a.y,templ={route_type:'wire',layer:a.layer,width:a.width},paths=[clean([a,b]),... [.012,.02,.03,.045,.06].map(e=>rdp(old,e))],seen=new Set()
 for(const u of dirs)for(const v of dirs){const det=u[0]*v[1]-u[1]*v[0];if(!det)continue;const T=(dx*v[1]-dy*v[0])/det,S=(u[0]*dy-u[1]*dx)/det;if(T<-1e-8||S<-1e-8)continue
  const splits=[0,T];for(let i=1;i<16;i++)splits.push(T*i/16)
  for(const p of old){const projection=((p.x-a.x)*u[0]+(p.y-a.y)*u[1])/(u[0]**2+u[1]**2);if(projection>0&&projection<T)splits.push(projection)}
  // Consolidate tiny staircase steps into at most two larger 45-degree
  // offsets. This preserves the original two-direction path length exactly.
  for(const tf of [.25,.375,.5,.625,.75])for(const f1 of [0,.125,.25,.375,.5])for(const f2 of [.25,.375,.5,.625,.75]){if(f1+f2>1)continue;let p=a;const r=[a];for(const [dir,l]of [[v,S*f1],[u,T*tf],[v,S*f2],[u,T*(1-tf)],[v,S*(1-f1-f2)]]){p={...templ,x:p.x+dir[0]*l,y:p.y+dir[1]*l};r.push(p)}r[r.length-1]=b;const q=clean(r);if(len(q)>len(old)*1.08+1e-7)continue;const key=q.map(p=>p.x.toFixed(7)+','+p.y.toFixed(7)).join(';');if(!seen.has(key)){seen.add(key);paths.push(q)}}
  for(const t of splits){const p={...templ,x:a.x+t*u[0],y:a.y+t*u[1]},q={...templ,x:p.x+S*v[0],y:p.y+S*v[1]};const r=clean([a,p,q,b]);if(len(r)>len(old)*1.08+1e-7)continue;const key=r.map(p=>p.x.toFixed(7)+','+p.y.toFixed(7)).join(';');if(seen.has(key))continue;seen.add(key);paths.push(r)}
 }
 return paths.sort((a,b)=>a.length-b.length||len(a)-len(b))
}
export function boundarySafe(s){if(!inside(s.a,board.outline)||!inside(s.b,board.outline))return false;if(edges.some(e=>gap(s,e)<.3-1e-6)||cutouts.some(e=>gap(s,e)<.3-1e-6))return false
 for(const b of bends){const half=b.bend_radius*Math.abs(b.bend_angle)*Math.PI/360+s.r+.01,lo=b.start.y-half,hi=b.start.y+half;let t0=0,t1=1,dy=s.b.y-s.a.y;if(Math.abs(dy)<1e-12){if(s.a.y<lo||s.a.y>hi)continue}else{t0=Math.max(0,Math.min((lo-s.a.y)/dy,(hi-s.a.y)/dy));t1=Math.min(1,Math.max((lo-s.a.y)/dy,(hi-s.a.y)/dy));if(t0>t1)continue}const x0=s.a.x+(s.b.x-s.a.x)*t0,x1=s.a.x+(s.b.x-s.a.x)*t1;if(Math.max(x0,x1)+s.r<Math.min(b.start.x,b.end.x)||Math.min(x0,x1)-s.r>Math.max(b.start.x,b.end.x))continue;if(Math.abs(s.a.x-s.b.x)>1e-7)return false}
 return true
}
export function anchors(t){const fixed=new Set([0,t.route.length-1]);for(let i=0;i<t.route.length;i++){const p=t.route[i];if(p.route_type!=='wire'){for(const k of [i-1,i,i+1])if(k>=0&&k<t.route.length)fixed.add(k)}if(i&&((p.width??0)!==(t.route[i-1].width??0)||p.layer!==t.route[i-1].layer)){fixed.add(i);fixed.add(i-1)}}
 if(process.env.STYLE_ENDPOINTS_ONLY==='1'){const ranges=analyzePcbStyle([t]).issues.map(x=>[x.startRouteIndex,x.endRouteIndex]).sort((a,b)=>a[0]-b[0]);const merged=[];for(const [lo,hi]of ranges){if(merged.length&&lo<=merged.at(-1)[1])merged.at(-1)[1]=Math.max(merged.at(-1)[1],hi);else merged.push([lo,hi])}for(const [lo,hi]of merged){fixed.add(lo);fixed.add(hi)}return [...fixed].sort((a,b)=>a-b)}
 if(process.env.STYLE_TREE_ANCHORS==='1'){const points=JSON.parse(fs.readFileSync('baseline/tree-anchors.json'))[t.pcb_trace_id]??[];for(let i=0;i<t.route.length;i++)if(points.some(p=>p.layer===t.route[i].layer&&dist(p,t.route[i])<1e-7))fixed.add(i);return [...fixed].sort((a,b)=>a-b)}
 for(const s of traceShapes(t).filter(s=>s.kind==='segment'))for(const o of spatial.near(s,0)){if(o.traceId===t.pcb_trace_id||o.net!==t.connection_name||!o.layers.some(l=>s.layers.includes(l)))continue;if(gap(s,o)<=1e-6){fixed.add(s.index);fixed.add(s.index+1);break}}
 return [...fixed].sort((a,b)=>a-b)
}
const byId=new Map(cache.traces.map(t=>[t.pcb_trace_id,t]))
const circuit=()=>base.map(e=>e.type==='pcb_trace'&&byId.has(e.pcb_trace_id)?{...e,route:byId.get(e.pcb_trace_id).route}:e)
export const traceScore=t=>[analyzePcbStyle([t]).issues.length,t.route.length,len(t.route)]
function better(a,b){return a[0]<b[0]||a[0]===b[0]&&(a[1]<b[1]||a[1]===b[1]&&a[2]<b[2]-1e-7)}
export {shapes,spatial,byId,circuit,traceShapes,dist,len,clean};