mohan-bee/stm32-midi-keys

Clips rectangular ground-region geometry to fit within the board’s scaled rounded-edge outline using polygon intersection.

Version
1.0.2
License
unset
Stars
0

scripts/verify_import.py

import json, math, re
from collections import Counter
from pathlib import Path
g=json.loads(Path('source/geometry.json').read_text())
d=json.loads(Path('board.circuit.json').read_text())
bytype=lambda t:[x for x in d if x['type']==t]
components={x['name']:x for x in bytype('source_component')}
pcb={x['source_component_id']:x for x in bytype('pcb_component')}
errors=[]
corrections=json.loads(Path('source/pad-shapes.json').read_text())
for f in g['footprints']:
    c=pcb[components[f['name']]['source_component_id']]
    if math.dist(list(c['center'].values()),list(f['center'].values()))>1e-6: errors.append(f"{f['name']}: component position")
    pads=[p for p in d if p.get('pcb_component_id')==c['pcb_component_id'] and p['type'] in ['pcb_smtpad','pcb_plated_hole','pcb_hole']]
    for p in f['pads']:
        if p['shape']=='custom': continue
        pos=next((c['center'] for c in corrections if c['component']==f['name'] and c['pin']==p['number']),p['position'])
        if not any(math.hypot(q.get('x',0)-pos['x'],q.get('y',0)-pos['y'])<1e-6 for q in pads): errors.append(f"{f['name']}.{p['number']}: pad position")
expected=[]
def netname(n):
    s=re.sub(r'_+','_',re.sub(r'[^A-Za-z0-9_]','_',n.replace('+','_P').replace('-','_'))).strip('_')
    return 'net_'+s if s[:1].isdigit() else s
netnames={n['source_net_id']:n['name'] for n in bytype('source_net')}
trace_nets={t['source_trace_id']:netnames[t['connected_source_net_ids'][0]] for t in bytype('source_trace')}
for s in g['segments']:
    expected.append((s['start'],s['end'],s['width'], 'top' if s['layer']=='F.Cu' else 'bottom',netname(g['nets'][str(s['net'])])))
actual=[]
for t in bytype('pcb_trace'):
    for a,b in zip(t['route'],t['route'][1:]):
        if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']:
            actual.append(({'x':a['x'],'y':a['y']},{'x':b['x'],'y':b['y']},a['width'],a['layer'],trace_nets[t['source_trace_id']]))
def key(s):
    a,b,w,l,n=s
    return (*sorted([(round(a['x'],6),round(a['y'],6)),(round(b['x'],6),round(b['y'],6))]),round(w,6),l,n)
missing=Counter(map(key,expected))-Counter(map(key,actual))
extra=Counter(map(key,actual))-Counter(map(key,expected))
out={'componentCount':len(components),'componentAndPadPositionErrors':errors,'originalSegments':len(expected),'importedWireEdges':len(actual),'missingSegmentCount':sum(missing.values()),'extraSegmentCount':sum(extra.values()),'originalVias':len(g['vias']),'importedVias':len(bytype('pcb_via')),'originalFilledPolygons':sum(len(z['fills']) for z in g['zones']),'importedFilledPolygons':len(bytype('pcb_copper_pour'))}
Path('validation/import-parity.json').write_text(json.dumps(out,indent=2))
print(json.dumps(out,indent=2))
assert not errors and not missing and not extra, 'Import geometry mismatch; see validation/import-parity.json'
assert out['originalVias']==out['importedVias']==124
assert out['originalFilledPolygons']==out['importedFilledPolygons']==29
def ring(points): return tuple((round(p['x'],6),round(p['y'],6)) for p in points)
original_fills=Counter((netname(z['net']),'top' if z['layer']=='F.Cu' else 'bottom',ring(p)) for z in g['zones'] for p in z['fills'])
converted_fills=Counter((z['net_name'],z['layer'],ring(z['points'])) for z in bytype('pcb_copper_pour'))
assert original_fills==converted_fills, 'Saved copper fill polygon geometry differs'