master
John Lauer Preserve BQ independent routing, narration and native copper-retention evidence a4b7711 1mo ago
"""Projected retained-copper area from native Fusion XML fill contours.
Includes fixed copper and drilled voids; circular pads/vias use 96-sided buffers.
"""
import sys,json,xml.etree.ElementTree as ET
from pathlib import Path
from collections import defaultdict
from shapely.geometry import Polygon,Point,LineString,box
from shapely.ops import unary_union
from shapely import make_valid
P=Path(__file__).resolve().parent;sys.path.insert(0,str(P.parent));from geometry import Geometry,LAYERS

def ring(parent):
 vs=parent.findall('vertex');assert vs and not any(v.get('curve') for v in vs),'Curved contour requires tessellation'
 return [(float(v.get('x')),float(v.get('y'))) for v in vs]
def measure(path):
 b=ET.parse(path).find('drawing/board');model=Geometry();outline=box(0,0,50.4,26.4);fixed=defaultdict(list);holes=[]
 for g,(net,ls,kind) in zip(model.obstacles,model.meta):
  if kind in ['pad','custom-pad']:
   for l in ls:fixed[(net,l)].append(g)
 for at,r in model.holes:holes.append(Point(at).buffer(r,quad_segs=24))
 for s in b.findall('signals/signal'):
  net=s.get('name')
  for w in s.findall('wire'):
   l=int(w.get('layer'))
   if l not in LAYERS:continue
   assert not float(w.get('curve','0'))
   fixed[(net,l)].append(LineString([(float(w.get('x1')),float(w.get('y1'))),(float(w.get('x2')),float(w.get('y2')))]).buffer(float(w.get('width'))/2,quad_segs=24))
  for v in s.findall('via'):
   at=Point(float(v.get('x')),float(v.get('y')));d=float(v.get('diameter'));dr=float(v.get('drill'));holes.append(at.buffer(dr/2,quad_segs=24))
   for l in LAYERS:fixed[(net,l)].append(at.buffer(d/2,quad_segs=24))
 drill=unary_union(holes);base={l:unary_union([g for (n,ll),gs in fixed.items() if ll==l for g in gs]) for l in LAYERS};same={k:unary_union(gs) for k,gs in fixed.items()};fills=defaultdict(list);orphans=defaultdict(list);count=defaultdict(int)
 for s in b.findall('signals/signal'):
  net=s.get('name')
  for p in s.findall('polygonpour'):
   l=int(p.get('layer'));shapes=p.findall('polygonfilldetails/polygonshape');assert shapes,'No native filled geometry; refill/export first'
   for sh in shapes:
    outer=sh.find('polygonoutlinesegments');outer=outer if outer is not None else sh.find('polygonoutlineobjects')
    poly=make_valid(Polygon(ring(outer),[ring(h) for h in sh.findall('polygonholelist/polygonholesegments')]));fills[l].append(poly);count[l]+=1
    if not poly.buffer(.00001).intersects(same.get((net,l),Polygon())):orphans[l].append(poly)
 result=[]
 for l in LAYERS:
  before=base[l].intersection(outline).difference(drill);after=unary_union([base[l],*fills[l]]).intersection(outline).difference(drill);available=outline.difference(drill).area;remaining=available-after.area;prior=available-before.area
  result.append({'layer':l,'boardAreaMm2':outline.area,'areaExcludingDrillsMm2':available,'traceOnlyCopperMm2':before.area,'retainedCopperMm2':after.area,'coveragePercent':after.area/outline.area*100,'traceOnlyCoveragePercent':before.area/outline.area*100,'projectedAreaToRemoveMm2':remaining,'areaRemovalReductionPercent':(prior-remaining)/prior*100,'filledRegions':count[l],'floatingRegions':len(orphans[l]),'floatingCopperMm2':unary_union(orphans[l]).intersection(outline).difference(drill).area})
 return {'source':str(path),'units':'mm','layers':result,'combinedCoveragePercent':sum(x['retainedCopperMm2'] for x in result)/(4*outline.area)*100,'combinedRemovalReductionPercent':sum(x['retainedCopperMm2']-x['traceOnlyCopperMm2'] for x in result)/sum(x['areaExcludingDrillsMm2']-x['traceOnlyCopperMm2'] for x in result)*100,'method':'Union of native fill contours and modeled fixed pads, custom pad copper, traces and vias; subtract drill voids. Circular geometry approximated with 96-sided polygons. Coverage denominator is full board area; removal-area comparison excludes drill voids. Area, not machining time. Orphans classified by contact with same-net fixed copper on that layer.'}
if __name__=='__main__':
 path=Path(sys.argv[1]) if len(sys.argv)>1 else P/'filled.brd';r=measure(path);(P/'coverage.json').write_text(json.dumps(r,indent=2));print(json.dumps(r,indent=2))