imrishabh18/corne-keyboard
The code defines and renders two surface-mount chip components with SMT pads, silkscreen outlines, and 3D CAD models (OBJ and STEP) for PCB assembly.
- Version
- 2.0.21
- License
- unset
- Stars
- 1
scripts/audit-copper.py
import json, sys, math
from collections import defaultdict
from shapely.geometry import Point, Polygon, LineString, box
from shapely.affinity import rotate, translate
from shapely.strtree import STRtree
from shapely.ops import unary_union
# Measure physical contacts. Net names only choose the intended copper; they
# never join geometrically separate pieces. BRep holes are preserved.
j=json.load(open(sys.argv[1]))
if len(sys.argv)>3:
key=sys.argv[3]
net=next((e for e in j if e['type']=='source_net' and e.get('subcircuit_connectivity_map_key')==key),{'source_net_id':'__unnamed__','subcircuit_connectivity_map_key':key})
else:
net=next(e for e in j if e['type']=='source_net' and e['name']=='GND')
key=net['subcircuit_connectivity_map_key']
sourceports={p for e in j if e['type']=='source_trace' and e.get('subcircuit_connectivity_map_key')==key for p in e['connected_source_port_ids']}
ports={e['pcb_port_id']:e for e in j if e['type']=='pcb_port' and e.get('source_port_id') in sourceports}
sp={e['source_port_id']:e for e in j if e['type']=='source_port'}
sc={e['source_component_id']:e for e in j if e['type']=='source_component'}
def label(p):
s=sp[p['source_port_id']]
return sc[s['source_component_id']]['name']+'.'+s['name']
def ring(r):
assert all(not v.get('bulge') for v in r['vertices'])
return [(v['x'],v['y']) for v in r['vertices']]
def pill(w,h):
r=min(w,h)/2
if abs(w-h)<1e-9:return Point(0,0).buffer(r,quad_segs=64)
return LineString([(-(w/2-r),-(h/2-r)),(w/2-r,h/2-r)]).buffer(r,quad_segs=64)
def rect(w,h,r=0):
return box(-w/2+r,-h/2+r,w/2-r,h/2-r).buffer(r,quad_segs=32) if r else box(-w/2,-h/2,w/2,h/2)
def copper(e):
t=e['type'];s=e.get('shape');a=e.get('ccw_rotation',0)
if t=='pcb_copper_pour':
b=e['brep_shape'];g=Polygon(ring(b['outer_ring']),[ring(r) for r in b['inner_rings']]);assert g.is_valid;return g
if t=='pcb_via':g=Point(0,0).buffer(e['outer_diameter']/2,quad_segs=64).difference(Point(0,0).buffer(e['hole_diameter']/2,quad_segs=64))
elif t=='pcb_smtpad':
if s=='polygon':return Polygon([(p['x'],p['y']) for p in e['points']])
if s=='circle':g=Point(0,0).buffer(e['radius'],quad_segs=64)
elif s in ['rect','rotated_rect']:g=rect(e['width'],e['height'],e.get('corner_radius',0))
elif s in ['pill','rotated_pill']:g=pill(e['width'],e['height'])
else:raise ValueError(s)
elif t=='pcb_plated_hole':
if s=='circle':g=Point(0,0).buffer(e['outer_diameter']/2,quad_segs=64);h=Point(0,0).buffer(e['hole_diameter']/2,quad_segs=64)
elif s=='pill':g=pill(e['outer_width'],e['outer_height']);h=pill(e['hole_width'],e['hole_height'])
elif s in ['circular_hole_with_rect_pad','pill_hole_with_rect_pad','rotated_pill_hole_with_rect_pad']:
a=0;g=rotate(rect(e['rect_pad_width'],e['rect_pad_height'],e.get('rect_border_radius',0)),e.get('rect_ccw_rotation',0),origin=(0,0))
h=Point(0,0).buffer(e['hole_diameter']/2,quad_segs=64) if 'hole_diameter' in e else rotate(pill(e['hole_width'],e['hole_height']),e.get('hole_ccw_rotation',0),origin=(0,0))
h=translate(h,e.get('hole_offset_x',0),e.get('hole_offset_y',0))
else:raise ValueError(s)
g=g.difference(h)
else:raise ValueError(t)
return translate(rotate(g,a,origin=(0,0)),e['x'],e['y'])
nodes=[];parent=[];bylayer=defaultdict(list);portnodes=defaultdict(list)
def root(i):
while parent[i]!=i:parent[i]=parent[parent[i]];i=parent[i]
return i
def join(a,b):parent[root(a)]=root(b)
def add(e,g,layer,id):
i=len(nodes);nodes.append((e,g,layer,id));parent.append(i);bylayer[layer].append(i)
if e.get('pcb_port_id') in ports:portnodes[e['pcb_port_id']].append(i)
return i
ground_source_traces={e['source_trace_id'] for e in j if e['type']=='source_trace' and e.get('subcircuit_connectivity_map_key')==key}
traces={e['pcb_trace_id']:e for e in j if e['type']=='pcb_trace' and (e.get('subcircuit_connectivity_map_key')==key or e.get('source_net_id')==net['source_net_id'] or e.get('connection_name')==net['source_net_id'] or e.get('source_trace_id') in ground_source_traces)}
for e in j:
t=e['type']
if t in ['pcb_smtpad','pcb_plated_hole'] and e.get('pcb_port_id') in ports or t=='pcb_copper_pour' and e.get('source_net_id')==net['source_net_id'] or t=='pcb_via' and (e.get('subcircuit_connectivity_map_key')==key or e.get('source_net_id')==net['source_net_id'] or e.get('pcb_trace_id') in traces):
g=copper(e);id=e[t+'_id'];ids=[add(e,g,l,id) for l in e.get('layers',[e.get('layer')])]
if t in ['pcb_plated_hole','pcb_via']:
for i in ids[1:]:join(ids[0],i)
if t=='pcb_trace' and e['pcb_trace_id'] in traces:
for a,b in zip(e['route'],e['route'][1:]):
if a['route_type']=='wire' and b['route_type']=='wire' and a['layer']==b['layer']:
g=LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(min(a['width'],b['width'])/2,quad_segs=32)
add(e,g,a['layer'],e['pcb_trace_id'])
for layer,ids in bylayer.items():
tree=STRtree([nodes[i][1] for i in ids])
for i in ids:
g=nodes[i][1]
for local in tree.query(g.buffer(1e-6)):
k=ids[local]
if k>i and g.distance(nodes[k][1])<=1e-6:join(i,k)
groups=defaultdict(list)
for pid,p in ports.items():
ns=portnodes[pid];assert ns,('missing copper',pid)
# Duplicate SMT shapes may share a port but are joined only by actual copper.
rs={root(i) for i in ns};assert len(rs)==1,('split footprint pad',pid,rs)
groups[next(iter(rs))].append({'id':pid,'name':label(p),'x':p['x'],'y':p['y']})
sortedgroups=sorted(groups.items(),key=lambda kv:-len(kv[1]))
report={'input':sys.argv[1],'connectivity_key':key,'ground_port_count':len(ports),'pour_island_count':sum(e['type']=='pcb_copper_pour' and e.get('source_net_id')==net['source_net_id'] for e in j),'physical_ground_groups':len(groups),'groups':[]}
for r,ps in sortedgroups:
pours={nodes[i][3] for i in range(len(nodes)) if root(i)==r and nodes[i][0]['type']=='pcb_copper_pour'}
report['groups'].append({'port_count':len(ps),'pour_ids':sorted(pours),'ports':ps})
report['floating_pour_islands']=sorted({nodes[i][3] for i in range(len(nodes)) if nodes[i][0]['type']=='pcb_copper_pour' and root(i) not in groups})
report['pour_connected_trace_endpoints']=[]
if len(groups)==1:
connected_root=next(iter(groups))
connected_pours=[(nodes[i][3],nodes[i][1],nodes[i][2]) for i in range(len(nodes)) if root(i)==connected_root and nodes[i][0]['type']=='pcb_copper_pour']
for trace in traces.values():
for end,p in [('start',trace['route'][0]),('end',trace['route'][-1])]:
if p['route_type']!='wire':continue
touched=[id for id,g,l in connected_pours if l==p['layer'] and g.covers(Point(p['x'],p['y']))]
if touched:report['pour_connected_trace_endpoints'].append({'trace_id':trace['pcb_trace_id'],'end':end,'pour_ids':touched})
out=sys.argv[2];json.dump(report,open(out,'w'),indent=2)
print({k:v for k,v in report.items() if k not in ['groups','floating_pour_islands','pour_connected_trace_endpoints']})
print('Group sizes:',[g['port_count'] for g in report['groups']])
for g in report['groups']:print(g['port_count'],', '.join(p['name'] for p in g['ports']))
print('Floating pour islands:',len(report['floating_pour_islands']))