main
John Lauer Publish 0.1.26 0b71841
from pathlib import Path
import xml.etree.ElementTree as E,re,json,math,argparse,hashlib
p=argparse.ArgumentParser(description='Measure text from a single-face native KiCad stroke SVG; layout evidence only.');p.add_argument('--svg',required=True,type=Path);p.add_argument('--labels',required=True,type=Path);p.add_argument('--out',required=True,type=Path);p.add_argument('--layer',default='F.SilkS');a=p.parse_args();root=E.parse(a.svg).getroot();groups=[]
def walk(e,sw=0):
 st=dict(re.findall(r'([\w-]+):([^;]+)',e.get('style','')));sw=float(st.get('stroke-width',e.get('stroke-width',sw)))
 if e.get('class')=='stroked-text':
  desc=next((x.text for x in e if x.tag.endswith('desc')),None);points=[]
  for p in e.iter():
   if p.tag.endswith('path'):
    vals=list(map(float,re.findall(r'[-+]?(?:\d*\.\d+|\d+)(?:[eE][-+]?\d+)?',p.get('d',''))));points+=list(zip(vals[::2],vals[1::2]))
  if points:
   xs,ys=zip(*points);groups.append(dict(text=desc,box=[min(xs)-sw/2,min(ys)-sw/2,max(xs)+sw/2,max(ys)+sw/2]))
 for c in e:walk(c,sw)
walk(root);m=json.loads(a.labels.read_text());out=[]
for i,l in enumerate(m['labels']):
 if l['layer']!=a.layer:continue
 boxes=[]
 for line in l['text'].split('\n'):
  gs=[g for g in groups if g['text']==line]
  if not gs:raise ValueError((i,line))
  def dist(g):b=g['box'];return math.hypot((b[0]+b[2])/2-l['x'],(b[1]+b[3])/2-l['y'])
  g=min(gs,key=dist);boxes.append(g['box'])
 b=[min(v[0] for v in boxes),min(v[1] for v in boxes),max(v[2] for v in boxes),max(v[3] for v in boxes)]
 out.append(dict(index=i,text=l['text'],box=b,offset=[(b[k]-(l['x'] if k%2==0 else l['y']))/l['size'] for k in range(4)]))
a.out.write_text(json.dumps({'sourceSvgSha256':hashlib.sha256(a.svg.read_bytes()).hexdigest(),'layer':a.layer,'method':'nearest matching native stroked-text group; inspect mapping before layout','bounds':out},indent=2));print('Native SVG text bounds:',len(out))