app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
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))