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/copper-model.mjs

import {gap} from './copper-geometry.mjs'
export const layers=['top','inner1','inner2','bottom']
export function copperModel(cache,source){
 const nets=new Map(cache.input.connections.flatMap(n=>n.pointsToConnect.map(p=>[p.pcb_port_id,n.name])))
 const pads=source.filter(p=>p.type==='pcb_smtpad'||p.type==='pcb_plated_hole').map(p=>{
  let s
  if(p.shape==='circle')s={kind:'circle',x:p.x,y:p.y,r:p.radius??p.outer_diameter/2}
  else if(p.shape==='pill'){const r=Math.min(p.width,p.height)/2,d=(Math.max(p.width,p.height)-2*r)/2;s={kind:'segment',a:{x:p.x-(p.width>=p.height?d:0),y:p.y-(p.height>p.width?d:0)},b:{x:p.x+(p.width>=p.height?d:0),y:p.y+(p.height>p.width?d:0)},r}}
  else if(p.shape==='rect')s={kind:'polygon',points:[{x:p.x-p.width/2,y:p.y-p.height/2},{x:p.x+p.width/2,y:p.y-p.height/2},{x:p.x+p.width/2,y:p.y+p.height/2},{x:p.x-p.width/2,y:p.y+p.height/2}]}
  else if(p.shape==='polygon')s={kind:'polygon',points:p.points}
  else throw Error('Unsupported pad '+p.shape)
  return {...s,id:p.pcb_smtpad_id??p.pcb_plated_hole_id,port:p.pcb_port_id,net:nets.get(p.pcb_port_id)??'nc_'+p.pcb_port_id,layers:p.layers??[p.layer],pad:true}
 })
 return [...pads,...cache.traces.flatMap(traceShapes),...printedCoilShapes(source)]
}
export function traceShapes(t){
 const out=[]
 for(let i=0;i<t.route.length;i++){
  const p=t.route[i],q=t.route[i+1],meta={net:t.connection_name,traceId:t.pcb_trace_id,index:i,id:t.pcb_trace_id+'_'+i}
  if(p.route_type==='via')out.push({...meta,kind:'circle',x:p.x,y:p.y,r:p.via_diameter/2,hole:p.via_hole_diameter,layers})
  if(p.route_type==='wire'&&q?.route_type==='wire'&&p.layer===q.layer)out.push({...meta,kind:'segment',a:p,b:q,r:Math.max(p.width,q.width)/2,layers:[p.layer]})
 }
 return out
}
export function bounds(s,extra=0){
 const p=s.kind==='polygon'?s.points:s.kind==='circle'?[s]:[s.a,s.b],r=(s.r??0)+extra
 return {left:Math.min(...p.map(p=>p.x))-r,right:Math.max(...p.map(p=>p.x))+r,bottom:Math.min(...p.map(p=>p.y))-r,top:Math.max(...p.map(p=>p.y))+r}
}
export class SpatialCopper{
 buckets=new Map()
 constructor(shapes){for(const s of shapes)this.add(s)}
 keys(s,extra=0){const b=bounds(s,extra),keys=[];for(const l of s.layers)for(let x=Math.floor(b.left);x<=Math.floor(b.right);x++)for(let y=Math.floor(b.bottom);y<=Math.floor(b.top);y++)keys.push(l+'_'+x+'_'+y);return keys}
 add(s){for(const k of this.keys(s)){const b=this.buckets.get(k)??new Set();b.add(s);this.buckets.set(k,b)}}
 remove(s){for(const k of this.keys(s))this.buckets.get(k)?.delete(s)}
 near(s,extra=.127){return new Set(this.keys(s,extra).flatMap(k=>[...(this.buckets.get(k)??[])]))}
 violations(s,min=.127){return [...this.near(s,min)].filter(o=>!intentionalCoilLeadContact(s,o)&&o.net!==s.net&&s.layers.some(l=>o.layers.includes(l))&&gap(s,o)<min-1e-6).map(o=>({other:o,gap:gap(s,o)}))}
}

// A printed inductor has an intentional conductive path between two terminals.
// Only the two terminal LEADS carry external-net identities. Its winding remains
// a separate physical region; an external trace touching a turn is a violation.
export function printedCoilShapes(source){
 const component=source.find(e=>e.type==='source_component'&&['LRX','L6'].includes(e.name))
 if(!component)return []
 const pcb=source.find(e=>e.type==='pcb_component'&&e.source_component_id===component.source_component_id)
 const trace=source.find(e=>e.type==='pcb_trace'&&e.pcb_component_id===pcb.pcb_component_id)
 if(!trace)throw Error('Missing printed winding')
 const sourceNets=port=>source.find(e=>e.type==='source_trace'&&e.connected_source_port_ids?.includes(port.source_port_id))?.connected_source_net_ids?.[0]
 const ports=source.filter(e=>e.type==='pcb_port'&&e.pcb_component_id===pcb.pcb_component_id)
 const netAt=(p)=>sourceNets(ports.find(q=>Math.hypot(p.x-q.x,p.y-q.y)<1e-5))
 const route=trace.route.map(p=>{if(p.route_type!=='via')return p;const via=source.find(v=>v.type==='pcb_via'&&v.pcb_trace_id===trace.pcb_trace_id&&Math.hypot(v.x-p.x,v.y-p.y)<1e-6);if(!via)throw Error('Printed coil transition has no manufacturing via');return {...p,via_diameter:via.outer_diameter,via_hole_diameter:via.hole_diameter}})
 const shapes=traceShapes({...trace,route,connection_name:'printed_inductor_internal'})
 const segments=shapes.filter(s=>s.kind==='segment')
 segments[0].net=netAt(trace.route[0]);segments[0].terminalLead=true
 for(const s of segments.slice(-1)){s.net=netAt(trace.route.at(-1));s.terminalLead=true}
 for(const s of shapes)s.coil=true
 return shapes
}
export function intentionalCoilLeadContact(a,b){
 if(!a.coil||!b.coil||a.traceId!==b.traceId)return false
 if((a.terminalLead||b.terminalLead)&&Math.abs(a.index-b.index)<=2)return true
 // Neighbouring portions of one declared winding share a real endpoint.
 const aa=a.kind==='segment'?[a.a,a.b]:[a],bb=b.kind==='segment'?[b.a,b.b]:[b]
 return aa.some(p=>bb.some(q=>Math.hypot(p.x-q.x,p.y-q.y)<1e-6))
}