added gerbmerge oldie

This commit is contained in:
yair reshef 2015-07-23 02:43:01 +03:00
parent 19619bba10
commit b3d2a87d1b
85 changed files with 1601807 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,677 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.1266*%
%ADD11C,0.0660*%
%ADD12R,0.0460X0.0630*%
%ADD13R,0.0250X0.0080*%
%ADD14C,0.0700*%
%ADD15C,0.0356*%
%ADD16C,0.0160*%
D10*
X001780Y008742D03*
X008780Y008742D03*
D11*
X001280Y001242D03*
X001280Y002242D03*
X001280Y003242D03*
X001280Y004242D03*
X001280Y005242D03*
X001280Y006242D03*
X001280Y007242D03*
X009280Y006242D03*
X009280Y005242D03*
X009280Y002242D03*
X009280Y001242D03*
D12*
X009080Y007242D03*
X008480Y007242D03*
D13*
X008780Y007242D03*
D14*
X007720Y001662D03*
X002350Y007422D03*
D15*
X004218Y005514D03*
X005230Y005692D03*
X004780Y004142D03*
X006297Y003232D03*
X007228Y003820D03*
X007225Y004371D03*
X007880Y007292D03*
X003318Y002464D03*
X002447Y003035D03*
D16*
X006297Y003232D01*
X006025Y003508D02*
X002849Y003346D01*
X004600Y003796D01*
X004703Y003754D01*
X004857Y003754D01*
X005000Y003813D01*
X005109Y003922D01*
X005168Y004065D01*
X005168Y004219D01*
X005131Y004310D01*
X005435Y005357D01*
X005450Y005363D01*
X005559Y005472D01*
X005618Y005615D01*
X005618Y005769D01*
X005559Y005912D01*
X005450Y006021D01*
X005307Y006080D01*
X005153Y006080D01*
X005010Y006021D01*
X004901Y005912D01*
X004842Y005769D01*
X004842Y005615D01*
X004879Y005524D01*
X004575Y004477D01*
X004560Y004471D01*
X004451Y004362D01*
X004449Y004356D01*
X001649Y003636D01*
X001586Y003700D01*
X001484Y003742D01*
X001586Y003784D01*
X001738Y003936D01*
X001820Y004135D01*
X001820Y004349D01*
X001738Y004548D01*
X001586Y004700D01*
X001484Y004742D01*
X001586Y004784D01*
X001738Y004936D01*
X001820Y005135D01*
X001820Y005349D01*
X001738Y005548D01*
X001586Y005700D01*
X001484Y005742D01*
X001586Y005784D01*
X001738Y005936D01*
X001820Y006135D01*
X001820Y006349D01*
X001738Y006548D01*
X001586Y006700D01*
X001443Y006759D01*
X001476Y006769D01*
X001547Y006806D01*
X001612Y006853D01*
X001669Y006910D01*
X001716Y006975D01*
X001753Y007046D01*
X001777Y007123D01*
X001790Y007202D01*
X001790Y007224D01*
X001298Y007224D01*
X001298Y007260D01*
X001262Y007260D01*
X001262Y007752D01*
X001240Y007752D01*
X001161Y007739D01*
X001084Y007715D01*
X001013Y007678D01*
X000990Y007662D01*
X000990Y008152D01*
X001070Y008032D01*
X001396Y007815D01*
X001780Y007738D01*
X001951Y007772D01*
X001946Y007767D01*
X001897Y007700D01*
X001859Y007625D01*
X001833Y007546D01*
X001820Y007464D01*
X001820Y007442D01*
X002330Y007442D01*
X002330Y007926D01*
X002490Y008032D01*
X002707Y008358D01*
X002784Y008742D01*
X002707Y009126D01*
X002490Y009452D01*
X002369Y009532D01*
X008191Y009532D01*
X008070Y009452D01*
X007853Y009126D01*
X007776Y008742D01*
X007853Y008358D01*
X008070Y008032D01*
X008396Y007815D01*
X008636Y007767D01*
X008163Y007767D01*
X008041Y007645D01*
X007957Y007680D01*
X007803Y007680D01*
X007660Y007621D01*
X007551Y007512D01*
X007492Y007369D01*
X007492Y007215D01*
X007551Y007072D01*
X007660Y006963D01*
X007803Y006904D01*
X007957Y006904D01*
X008040Y006938D01*
X008040Y006840D01*
X008163Y006717D01*
X008797Y006717D01*
X008827Y006747D01*
X009045Y006747D01*
X009045Y007045D01*
X009115Y007115D01*
X009115Y007207D01*
X009115Y007277D01*
X009115Y007369D01*
X009045Y007439D01*
X009045Y007737D01*
X008827Y007737D01*
X008818Y007746D01*
X009164Y007815D01*
X009490Y008032D01*
X009558Y008135D01*
X009558Y006711D01*
X009406Y006774D01*
X009421Y006783D01*
X009454Y006816D01*
X009478Y006858D01*
X009490Y006903D01*
X009490Y007207D01*
X009115Y007207D01*
X009115Y006758D01*
X008974Y006700D01*
X008822Y006548D01*
X008740Y006349D01*
X008740Y006135D01*
X008822Y005936D01*
X008974Y005784D01*
X009117Y005725D01*
X009084Y005715D01*
X009013Y005678D01*
X008948Y005631D01*
X008891Y005574D01*
X008844Y005509D01*
X008807Y005438D01*
X008783Y005361D01*
X008770Y005282D01*
X008770Y005260D01*
X009262Y005260D01*
X009262Y005224D01*
X009298Y005224D01*
X009298Y004732D01*
X009320Y004732D01*
X009399Y004745D01*
X009476Y004769D01*
X009547Y004806D01*
X009558Y004814D01*
X009558Y002711D01*
X009387Y002782D01*
X009173Y002782D01*
X008974Y002700D01*
X008822Y002548D01*
X008740Y002349D01*
X008740Y002135D01*
X008822Y001936D01*
X008974Y001784D01*
X009117Y001725D01*
X009084Y001715D01*
X009013Y001678D01*
X008948Y001631D01*
X008891Y001574D01*
X008844Y001509D01*
X008807Y001438D01*
X008783Y001361D01*
X008770Y001282D01*
X008770Y001260D01*
X009262Y001260D01*
X009262Y001224D01*
X008770Y001224D01*
X008770Y001202D01*
X008783Y001123D01*
X008807Y001046D01*
X008844Y000975D01*
X008860Y000952D01*
X001744Y000952D01*
X001820Y001135D01*
X001820Y001349D01*
X001738Y001548D01*
X001586Y001700D01*
X001484Y001742D01*
X001586Y001784D01*
X001738Y001936D01*
X001820Y002135D01*
X001820Y002349D01*
X001738Y002548D01*
X001586Y002700D01*
X001484Y002742D01*
X001586Y002784D01*
X001738Y002936D01*
X001795Y003075D01*
X002073Y003147D01*
X002059Y003112D01*
X002059Y002958D01*
X002118Y002815D01*
X002227Y002706D01*
X002370Y002647D01*
X002524Y002647D01*
X002667Y002706D01*
X002719Y002759D01*
X006051Y002929D01*
X006077Y002903D01*
X006220Y002844D01*
X006374Y002844D01*
X006517Y002903D01*
X006626Y003012D01*
X006685Y003155D01*
X006685Y003309D01*
X006626Y003452D01*
X006517Y003561D01*
X006374Y003620D01*
X006220Y003620D01*
X006077Y003561D01*
X006025Y003508D01*
X006159Y003595D02*
X003817Y003595D01*
X003338Y004071D02*
X001793Y004071D01*
X001714Y003912D02*
X002721Y003912D01*
X003201Y003437D02*
X004619Y003437D01*
X004434Y003754D02*
X009558Y003754D01*
X009558Y003595D02*
X006435Y003595D01*
X006632Y003437D02*
X009558Y003437D01*
X009558Y003278D02*
X006685Y003278D01*
X006670Y003120D02*
X009558Y003120D01*
X009558Y002961D02*
X006575Y002961D01*
X007403Y002137D02*
X007245Y001979D01*
X007160Y001773D01*
X007160Y001551D01*
X007245Y001345D01*
X007403Y001187D01*
X007609Y001102D01*
X007831Y001102D01*
X008037Y001187D01*
X008195Y001345D01*
X008280Y001551D01*
X008280Y001773D01*
X008195Y001979D01*
X008037Y002137D01*
X007831Y002222D01*
X007609Y002222D01*
X007403Y002137D01*
X007479Y002169D02*
X001820Y002169D01*
X001820Y002327D02*
X008740Y002327D01*
X008740Y002169D02*
X007961Y002169D01*
X008164Y002010D02*
X008792Y002010D01*
X008907Y001852D02*
X008248Y001852D01*
X008280Y001693D02*
X009042Y001693D01*
X008862Y001535D02*
X008273Y001535D01*
X008208Y001376D02*
X008787Y001376D01*
X008770Y001218D02*
X008067Y001218D01*
X007373Y001218D02*
X001820Y001218D01*
X001789Y001059D02*
X008803Y001059D01*
X008796Y002486D02*
X001764Y002486D01*
X001642Y002644D02*
X008918Y002644D01*
X009558Y002803D02*
X003578Y002803D01*
X004780Y004142D02*
X005230Y005692D01*
X005498Y005973D02*
X008807Y005973D01*
X008741Y006131D02*
X001819Y006131D01*
X001753Y005973D02*
X004962Y005973D01*
X004860Y005814D02*
X001616Y005814D01*
X001630Y005656D02*
X004842Y005656D01*
X004871Y005497D02*
X001759Y005497D01*
X001820Y005339D02*
X004825Y005339D01*
X004779Y005180D02*
X001820Y005180D01*
X001773Y005022D02*
X004733Y005022D01*
X004687Y004863D02*
X001665Y004863D01*
X001574Y004705D02*
X004641Y004705D01*
X004595Y004546D02*
X001739Y004546D01*
X001804Y004388D02*
X004477Y004388D01*
X004780Y004142D02*
X001280Y003242D01*
X001604Y002803D02*
X002131Y002803D01*
X002059Y002961D02*
X001748Y002961D01*
X001968Y003120D02*
X002062Y003120D01*
X002105Y003754D02*
X001512Y003754D01*
X001820Y004229D02*
X003954Y004229D01*
X005099Y003912D02*
X009558Y003912D01*
X009558Y004071D02*
X005168Y004071D01*
X005164Y004229D02*
X009558Y004229D01*
X009558Y004388D02*
X005153Y004388D01*
X005199Y004546D02*
X009558Y004546D01*
X009558Y004705D02*
X005245Y004705D01*
X005291Y004863D02*
X008938Y004863D01*
X008948Y004853D02*
X009013Y004806D01*
X009084Y004769D01*
X009161Y004745D01*
X009240Y004732D01*
X009262Y004732D01*
X009262Y005224D01*
X008770Y005224D01*
X008770Y005202D01*
X008783Y005123D01*
X008807Y005046D01*
X008844Y004975D01*
X008891Y004910D01*
X008948Y004853D01*
X008820Y005022D02*
X005337Y005022D01*
X005383Y005180D02*
X008773Y005180D01*
X008779Y005339D02*
X005429Y005339D01*
X005569Y005497D02*
X008838Y005497D01*
X008981Y005656D02*
X005618Y005656D01*
X005600Y005814D02*
X008944Y005814D01*
X008740Y006290D02*
X001820Y006290D01*
X001779Y006448D02*
X008781Y006448D01*
X008881Y006607D02*
X001679Y006607D01*
X001462Y006765D02*
X008115Y006765D01*
X008040Y006924D02*
X008005Y006924D01*
X007755Y006924D02*
X002531Y006924D01*
X002553Y006931D02*
X002628Y006969D01*
X002695Y007018D01*
X002754Y007077D01*
X002803Y007144D01*
X002841Y007219D01*
X002867Y007298D01*
X002880Y007380D01*
X002880Y007402D01*
X002370Y007402D01*
X002370Y007442D01*
X002330Y007442D01*
X002330Y007402D01*
X001820Y007402D01*
X001820Y007380D01*
X001833Y007298D01*
X001859Y007219D01*
X001897Y007144D01*
X001946Y007077D01*
X002005Y007018D01*
X002072Y006969D01*
X002147Y006931D01*
X002226Y006905D01*
X002308Y006892D01*
X002330Y006892D01*
X002330Y007402D01*
X002370Y007402D01*
X002370Y006892D01*
X002392Y006892D01*
X002474Y006905D01*
X002553Y006931D01*
X002370Y006924D02*
X002330Y006924D01*
X002169Y006924D02*
X001679Y006924D01*
X001764Y007082D02*
X001942Y007082D01*
X001852Y007241D02*
X001298Y007241D01*
X001298Y007260D02*
X001790Y007260D01*
X001790Y007282D01*
X001777Y007361D01*
X001753Y007438D01*
X001716Y007509D01*
X001669Y007574D01*
X001612Y007631D01*
X001547Y007678D01*
X001476Y007715D01*
X001399Y007739D01*
X001320Y007752D01*
X001298Y007752D01*
X001298Y007260D01*
X001298Y007399D02*
X001262Y007399D01*
X001262Y007558D02*
X001298Y007558D01*
X001298Y007716D02*
X001262Y007716D01*
X001306Y007875D02*
X000990Y007875D01*
X000990Y008033D02*
X001070Y008033D01*
X001088Y007716D02*
X000990Y007716D01*
X001472Y007716D02*
X001908Y007716D01*
X001837Y007558D02*
X001681Y007558D01*
X001765Y007399D02*
X001820Y007399D01*
X002330Y007399D02*
X002370Y007399D01*
X002370Y007442D02*
X002880Y007442D01*
X002880Y007464D01*
X002867Y007546D01*
X002841Y007625D01*
X002803Y007700D01*
X002754Y007767D01*
X002695Y007826D01*
X002628Y007875D01*
X002553Y007913D01*
X002474Y007939D01*
X002392Y007952D01*
X002370Y007952D01*
X002370Y007442D01*
X002370Y007558D02*
X002330Y007558D01*
X002330Y007716D02*
X002370Y007716D01*
X002370Y007875D02*
X002330Y007875D01*
X002490Y008033D02*
X003969Y008033D01*
X003945Y008090D02*
X003995Y007970D01*
X004088Y007877D01*
X004208Y007827D01*
X004339Y007827D01*
X004459Y007877D01*
X004552Y007970D01*
X004602Y008090D01*
X004602Y008221D01*
X004552Y008341D01*
X004459Y008434D01*
X004339Y008484D01*
X004208Y008484D01*
X004088Y008434D01*
X003995Y008341D01*
X003945Y008221D01*
X003945Y008090D01*
X003945Y008192D02*
X002596Y008192D01*
X002702Y008350D02*
X004004Y008350D01*
X004095Y007875D02*
X002629Y007875D01*
X002792Y007716D02*
X008112Y007716D01*
X008306Y007875D02*
X006027Y007875D01*
X006034Y007877D02*
X006127Y007970D01*
X006177Y008090D01*
X006177Y008221D01*
X006127Y008341D01*
X006034Y008434D01*
X005914Y008484D01*
X005783Y008484D01*
X005663Y008434D01*
X005570Y008341D01*
X005520Y008221D01*
X005520Y008090D01*
X005570Y007970D01*
X005663Y007877D01*
X005783Y007827D01*
X005914Y007827D01*
X006034Y007877D01*
X006153Y008033D02*
X008070Y008033D01*
X007964Y008192D02*
X006177Y008192D01*
X006118Y008350D02*
X007858Y008350D01*
X007823Y008509D02*
X002737Y008509D01*
X002769Y008667D02*
X007791Y008667D01*
X007793Y008826D02*
X002767Y008826D01*
X002736Y008984D02*
X007824Y008984D01*
X007864Y009143D02*
X002696Y009143D01*
X002590Y009301D02*
X007970Y009301D01*
X008082Y009460D02*
X002478Y009460D01*
X002863Y007558D02*
X007597Y007558D01*
X007504Y007399D02*
X002880Y007399D01*
X002848Y007241D02*
X007492Y007241D01*
X007547Y007082D02*
X002758Y007082D01*
X002370Y007082D02*
X002330Y007082D01*
X002330Y007241D02*
X002370Y007241D01*
X004453Y007875D02*
X005669Y007875D01*
X005544Y008033D02*
X004578Y008033D01*
X004602Y008192D02*
X005520Y008192D01*
X005579Y008350D02*
X004543Y008350D01*
X007880Y007292D02*
X007930Y007242D01*
X008480Y007242D01*
X009082Y007082D02*
X009115Y007082D01*
X009115Y007207D02*
X009115Y007207D01*
X009115Y007241D02*
X009558Y007241D01*
X009490Y007277D02*
X009490Y007581D01*
X009478Y007626D01*
X009454Y007668D01*
X009421Y007701D01*
X009379Y007725D01*
X009334Y007737D01*
X009115Y007737D01*
X009115Y007277D01*
X009115Y007277D01*
X009490Y007277D01*
X009490Y007399D02*
X009558Y007399D01*
X009558Y007558D02*
X009490Y007558D01*
X009558Y007716D02*
X009395Y007716D01*
X009254Y007875D02*
X009558Y007875D01*
X009558Y008033D02*
X009490Y008033D01*
X009115Y007716D02*
X009045Y007716D01*
X009045Y007558D02*
X009115Y007558D01*
X009115Y007399D02*
X009085Y007399D01*
X009490Y007082D02*
X009558Y007082D01*
X009558Y006924D02*
X009490Y006924D01*
X009428Y006765D02*
X009558Y006765D01*
X009115Y006765D02*
X009045Y006765D01*
X009045Y006924D02*
X009115Y006924D01*
X009262Y005180D02*
X009298Y005180D01*
X009298Y005022D02*
X009262Y005022D01*
X009262Y004863D02*
X009298Y004863D01*
X007276Y002010D02*
X001768Y002010D01*
X001653Y001852D02*
X007192Y001852D01*
X007160Y001693D02*
X001593Y001693D01*
X001743Y001535D02*
X007167Y001535D01*
X007232Y001376D02*
X001809Y001376D01*
M02*

90548
gerber/gerbmerge/1x1/proj1.GBO Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.1306*%
%ADD11C,0.0276*%
%ADD12C,0.0700*%
%ADD13R,0.0500X0.0670*%
%ADD14R,0.0290X0.0120*%
%ADD15R,0.0060X0.0720*%
%ADD16C,0.0740*%
D10*
X001780Y008742D03*
X008780Y008742D03*
D11*
X005848Y008156D03*
X004274Y008156D03*
D12*
X001280Y001242D03*
X001280Y002242D03*
X001280Y003242D03*
X001280Y004242D03*
X001280Y005242D03*
X001280Y006242D03*
X001280Y007242D03*
X009280Y006242D03*
X009280Y005242D03*
X009280Y002242D03*
X009280Y001242D03*
D13*
X009080Y007242D03*
X008480Y007242D03*
D14*
X008780Y007242D03*
D15*
X008780Y007242D03*
D16*
X007720Y001662D03*
X002350Y007422D03*
M02*

View File

@ -0,0 +1,89 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.0000*%
D10*
X000780Y000742D02*
X000780Y009742D01*
X009768Y009742D01*
X009768Y000742D01*
X000780Y000742D01*
X004156Y008156D02*
X004158Y008177D01*
X004164Y008197D01*
X004173Y008217D01*
X004185Y008234D01*
X004200Y008248D01*
X004218Y008260D01*
X004238Y008268D01*
X004258Y008273D01*
X004279Y008274D01*
X004300Y008271D01*
X004320Y008265D01*
X004339Y008254D01*
X004356Y008241D01*
X004369Y008225D01*
X004380Y008207D01*
X004388Y008187D01*
X004392Y008167D01*
X004392Y008145D01*
X004388Y008125D01*
X004380Y008105D01*
X004369Y008087D01*
X004356Y008071D01*
X004339Y008058D01*
X004320Y008047D01*
X004300Y008041D01*
X004279Y008038D01*
X004258Y008039D01*
X004238Y008044D01*
X004218Y008052D01*
X004200Y008064D01*
X004185Y008078D01*
X004173Y008095D01*
X004164Y008115D01*
X004158Y008135D01*
X004156Y008156D01*
X005730Y008156D02*
X005732Y008177D01*
X005738Y008197D01*
X005747Y008217D01*
X005759Y008234D01*
X005774Y008248D01*
X005792Y008260D01*
X005812Y008268D01*
X005832Y008273D01*
X005853Y008274D01*
X005874Y008271D01*
X005894Y008265D01*
X005913Y008254D01*
X005930Y008241D01*
X005943Y008225D01*
X005954Y008207D01*
X005962Y008187D01*
X005966Y008167D01*
X005966Y008145D01*
X005962Y008125D01*
X005954Y008105D01*
X005943Y008087D01*
X005930Y008071D01*
X005913Y008058D01*
X005894Y008047D01*
X005874Y008041D01*
X005853Y008038D01*
X005832Y008039D01*
X005812Y008044D01*
X005792Y008052D01*
X005774Y008064D01*
X005759Y008078D01*
X005747Y008095D01*
X005738Y008115D01*
X005732Y008135D01*
X005730Y008156D01*
M02*

View File

@ -0,0 +1,89 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.0000*%
D10*
X000780Y000742D02*
X000780Y009742D01*
X009768Y009742D01*
X009768Y000742D01*
X000780Y000742D01*
X004156Y008156D02*
X004158Y008177D01*
X004164Y008197D01*
X004173Y008217D01*
X004185Y008234D01*
X004200Y008248D01*
X004218Y008260D01*
X004238Y008268D01*
X004258Y008273D01*
X004279Y008274D01*
X004300Y008271D01*
X004320Y008265D01*
X004339Y008254D01*
X004356Y008241D01*
X004369Y008225D01*
X004380Y008207D01*
X004388Y008187D01*
X004392Y008167D01*
X004392Y008145D01*
X004388Y008125D01*
X004380Y008105D01*
X004369Y008087D01*
X004356Y008071D01*
X004339Y008058D01*
X004320Y008047D01*
X004300Y008041D01*
X004279Y008038D01*
X004258Y008039D01*
X004238Y008044D01*
X004218Y008052D01*
X004200Y008064D01*
X004185Y008078D01*
X004173Y008095D01*
X004164Y008115D01*
X004158Y008135D01*
X004156Y008156D01*
X005730Y008156D02*
X005732Y008177D01*
X005738Y008197D01*
X005747Y008217D01*
X005759Y008234D01*
X005774Y008248D01*
X005792Y008260D01*
X005812Y008268D01*
X005832Y008273D01*
X005853Y008274D01*
X005874Y008271D01*
X005894Y008265D01*
X005913Y008254D01*
X005930Y008241D01*
X005943Y008225D01*
X005954Y008207D01*
X005962Y008187D01*
X005966Y008167D01*
X005966Y008145D01*
X005962Y008125D01*
X005954Y008105D01*
X005943Y008087D01*
X005930Y008071D01*
X005913Y008058D01*
X005894Y008047D01*
X005874Y008041D01*
X005853Y008038D01*
X005832Y008039D01*
X005812Y008044D01*
X005792Y008052D01*
X005774Y008064D01*
X005759Y008078D01*
X005747Y008095D01*
X005738Y008115D01*
X005732Y008135D01*
X005730Y008156D01*
M02*

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,985 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.0030*%
%ADD11C,0.0050*%
%ADD12C,0.0040*%
%ADD13R,0.0280X0.0050*%
%ADD14C,0.0100*%
%ADD15R,0.0050X0.0280*%
%ADD16C,0.0060*%
%ADD17C,0.0160*%
%ADD18C,0.0080*%
%ADD19R,0.0394X0.0079*%
%ADD20R,0.0079X0.0079*%
%ADD21C,0.0000*%
%ADD22R,0.0079X0.0394*%
D10*
X001824Y001146D02*
X001917Y001306D01*
X002001Y001329D01*
X002108Y001267D01*
X002131Y001183D01*
X002038Y001023D01*
X002145Y000961D02*
X001824Y001146D01*
X002009Y001465D02*
X002101Y001625D01*
X002185Y001648D01*
X002239Y001617D01*
X002261Y001533D01*
X002169Y001372D01*
X002329Y001280D02*
X002422Y001440D01*
X002399Y001525D01*
X002346Y001555D01*
X002261Y001533D01*
X002460Y001630D02*
X002544Y001652D01*
X002606Y001759D01*
X002583Y001844D01*
X002476Y001905D01*
X002392Y001883D01*
X002361Y001829D01*
X002353Y001691D01*
X002193Y001784D01*
X002316Y001998D01*
X002157Y002073D02*
X002064Y001913D01*
X002171Y001851D02*
X001850Y002036D01*
X001943Y002197D01*
X002027Y002219D01*
X002134Y002158D01*
X002157Y002073D01*
X002355Y002170D02*
X002448Y002331D01*
X002425Y002415D01*
X002372Y002446D01*
X002287Y002423D01*
X002195Y002263D01*
X002287Y002423D02*
X002265Y002507D01*
X002211Y002538D01*
X002127Y002516D01*
X002034Y002355D01*
X002355Y002170D01*
X002379Y002582D02*
X002502Y002796D01*
X002311Y002835D02*
X002379Y002582D01*
X002632Y002650D02*
X002311Y002835D01*
X002128Y002788D02*
X001808Y002973D01*
X001900Y003133D01*
X001985Y003156D01*
X002092Y003094D01*
X002114Y003010D01*
X002022Y002850D01*
X002313Y003107D02*
X002405Y003267D01*
X002383Y003351D01*
X002329Y003382D01*
X002245Y003360D01*
X002152Y003199D01*
X001992Y003292D02*
X002084Y003452D01*
X002169Y003475D01*
X002222Y003444D01*
X002245Y003360D01*
X002443Y003457D02*
X002528Y003479D01*
X002589Y003586D01*
X002567Y003670D01*
X002513Y003701D01*
X002429Y003679D01*
X002398Y003625D01*
X002429Y003679D02*
X002406Y003763D01*
X002353Y003794D01*
X002269Y003771D01*
X002207Y003664D01*
X002230Y003580D01*
X002094Y003783D02*
X001773Y003968D01*
X001866Y004129D01*
X001950Y004151D01*
X002057Y004090D01*
X002080Y004005D01*
X001987Y003845D01*
X002278Y004102D02*
X002371Y004263D01*
X002348Y004347D01*
X002295Y004378D01*
X002211Y004355D01*
X002118Y004195D01*
X002211Y004355D02*
X002188Y004440D01*
X002134Y004470D01*
X002050Y004448D01*
X001958Y004287D01*
X002278Y004102D01*
X002462Y004421D02*
X002372Y004758D01*
X002319Y004789D01*
X002234Y004767D01*
X002173Y004660D01*
X002195Y004576D01*
X002089Y004755D02*
X001768Y004940D01*
X001860Y005101D01*
X001945Y005123D01*
X002052Y005061D01*
X002074Y004977D01*
X001982Y004817D01*
X002112Y005167D02*
X002205Y005327D01*
X002289Y005350D01*
X002343Y005319D01*
X002365Y005234D01*
X002273Y005074D01*
X001952Y005259D01*
X002045Y005420D01*
X002129Y005442D01*
X002182Y005411D01*
X002205Y005327D01*
X002243Y005516D02*
X002198Y005685D01*
X002519Y005500D01*
X002457Y005393D02*
X002580Y005607D01*
X002146Y005821D02*
X001825Y006006D01*
X001918Y006167D01*
X002002Y006189D01*
X002109Y006127D01*
X002131Y006043D01*
X002039Y005883D01*
X002169Y006233D02*
X002262Y006393D01*
X002346Y006416D01*
X002400Y006385D01*
X002422Y006300D01*
X002330Y006140D01*
X002009Y006325D01*
X002102Y006486D01*
X002186Y006508D01*
X002239Y006477D01*
X002262Y006393D01*
X002247Y006613D02*
X002461Y006490D01*
X002370Y006827D01*
X002584Y006704D01*
X002607Y006619D01*
X002545Y006512D01*
X002461Y006490D01*
X002247Y006613D02*
X002224Y006698D01*
X002286Y006805D01*
X002370Y006827D01*
X002815Y007222D02*
X002815Y007512D01*
X002960Y007512D01*
X003008Y007464D01*
X003008Y007367D01*
X002960Y007319D01*
X002815Y007319D01*
X002912Y007319D02*
X003008Y007222D01*
X003110Y007367D02*
X003303Y007367D01*
X003255Y007222D02*
X003255Y007512D01*
X003110Y007367D01*
X003505Y007257D02*
X003650Y007257D01*
X003698Y007305D01*
X003698Y007499D01*
X003650Y007547D01*
X003505Y007547D01*
X003505Y007257D01*
X003800Y007257D02*
X003993Y007257D01*
X003896Y007257D02*
X003896Y007547D01*
X003800Y007450D01*
X004395Y007537D02*
X004395Y007247D01*
X004540Y007247D01*
X004588Y007295D01*
X004588Y007489D01*
X004540Y007537D01*
X004395Y007537D01*
X004690Y007489D02*
X004738Y007537D01*
X004835Y007537D01*
X004883Y007489D01*
X004883Y007440D01*
X004690Y007247D01*
X004883Y007247D01*
X005170Y007207D02*
X005170Y007497D01*
X005315Y007497D01*
X005363Y007449D01*
X005363Y007352D01*
X005315Y007304D01*
X005170Y007304D01*
X005267Y007304D02*
X005363Y007207D01*
X005465Y007255D02*
X005513Y007207D01*
X005610Y007207D01*
X005658Y007255D01*
X005658Y007304D01*
X005610Y007352D01*
X005561Y007352D01*
X005610Y007352D02*
X005658Y007400D01*
X005658Y007449D01*
X005610Y007497D01*
X005513Y007497D01*
X005465Y007449D01*
X006020Y007447D02*
X006020Y007157D01*
X006020Y007254D02*
X006165Y007254D01*
X006213Y007302D01*
X006213Y007399D01*
X006165Y007447D01*
X006020Y007447D01*
X006117Y007254D02*
X006213Y007157D01*
X006315Y007157D02*
X006508Y007157D01*
X006411Y007157D02*
X006411Y007447D01*
X006315Y007350D01*
X007583Y006942D02*
X007583Y006652D01*
X007583Y006749D02*
X007728Y006749D01*
X007776Y006797D01*
X007776Y006894D01*
X007728Y006942D01*
X007583Y006942D01*
X007680Y006749D02*
X007776Y006652D01*
X007878Y006652D02*
X008071Y006845D01*
X008071Y006894D01*
X008023Y006942D01*
X007926Y006942D01*
X007878Y006894D01*
X007878Y006652D02*
X008071Y006652D01*
X008392Y006617D02*
X008732Y006617D01*
X008732Y006390D02*
X008505Y006390D01*
X008392Y006277D01*
X008505Y006163D01*
X008732Y006163D01*
X008675Y006049D02*
X008732Y005993D01*
X008732Y005823D01*
X008392Y005823D01*
X008392Y005993D01*
X008449Y006049D01*
X008505Y006049D01*
X008562Y005993D01*
X008562Y005823D01*
X008562Y005993D02*
X008619Y006049D01*
X008675Y006049D01*
X008562Y006163D02*
X008562Y006390D01*
X008392Y006504D02*
X008392Y006731D01*
X009135Y007568D02*
X009280Y007568D01*
X009329Y007617D01*
X009329Y007810D01*
X009280Y007858D01*
X009135Y007858D01*
X009135Y007568D01*
X009430Y007713D02*
X009623Y007713D01*
X009575Y007568D02*
X009575Y007858D01*
X009430Y007713D01*
X006649Y005246D02*
X006649Y004956D01*
X006552Y004956D02*
X006746Y004956D01*
X006552Y005149D02*
X006649Y005246D01*
X006451Y005198D02*
X006402Y005246D01*
X006306Y005246D01*
X006257Y005198D01*
X006257Y005004D01*
X006306Y004956D01*
X006402Y004956D01*
X006451Y005004D01*
X006443Y004288D02*
X006443Y004046D01*
X006395Y003998D01*
X006298Y003998D01*
X006250Y004046D01*
X006250Y004288D01*
X006545Y004191D02*
X006641Y004288D01*
X006641Y003998D01*
X006545Y003998D02*
X006738Y003998D01*
X008287Y002405D02*
X008199Y002317D01*
X008199Y002230D01*
X008374Y002056D01*
X008461Y002056D01*
X008548Y002143D01*
X008548Y002230D01*
X008634Y002316D02*
X008722Y002316D01*
X008809Y002403D01*
X008809Y002491D01*
X008634Y002665D02*
X008547Y002665D01*
X008460Y002578D01*
X008460Y002491D01*
X008634Y002316D01*
X008374Y002405D02*
X008287Y002405D01*
X008070Y002188D02*
X008244Y002013D01*
X008244Y001839D01*
X008070Y001839D01*
X007895Y002013D01*
X004434Y002755D02*
X004241Y002755D01*
X004434Y002948D01*
X004434Y002997D01*
X004386Y003045D01*
X004289Y003045D01*
X004241Y002997D01*
X004139Y002997D02*
X004091Y003045D01*
X003994Y003045D01*
X003946Y002997D01*
X003946Y002803D01*
X003994Y002755D01*
X004091Y002755D01*
X004139Y002803D01*
X002462Y004421D02*
X002586Y004635D01*
X003177Y005488D02*
X003274Y005488D01*
X003322Y005536D01*
X003322Y005778D01*
X003424Y005730D02*
X003472Y005778D01*
X003569Y005778D01*
X003617Y005730D01*
X003617Y005681D01*
X003424Y005488D01*
X003617Y005488D01*
X003177Y005488D02*
X003129Y005536D01*
X003129Y005778D01*
X001992Y003292D02*
X002313Y003107D01*
X002009Y001465D02*
X002329Y001280D01*
D11*
X004009Y001019D02*
X004009Y000861D01*
X004010Y000844D01*
X004015Y000827D01*
X004022Y000812D01*
X004032Y000798D01*
X004044Y000786D01*
X004058Y000776D01*
X004073Y000769D01*
X004090Y000764D01*
X004107Y000763D01*
X006273Y000763D01*
X006290Y000764D01*
X006307Y000769D01*
X006322Y000776D01*
X006336Y000786D01*
X006348Y000798D01*
X006358Y000812D01*
X006365Y000827D01*
X006370Y000844D01*
X006371Y000861D01*
X006371Y001019D01*
X005781Y001157D02*
X005781Y001747D01*
X004599Y001747D01*
X004599Y001157D01*
X005781Y001157D01*
X006371Y001885D02*
X006371Y002043D01*
X006370Y002060D01*
X006365Y002077D01*
X006358Y002092D01*
X006348Y002106D01*
X006336Y002118D01*
X006322Y002128D01*
X006307Y002135D01*
X006290Y002140D01*
X006273Y002141D01*
X004147Y002141D01*
X004126Y002143D01*
X004105Y002141D01*
X004085Y002135D01*
X004066Y002126D01*
X004049Y002114D01*
X004034Y002099D01*
X004023Y002082D01*
X004014Y002063D01*
X004009Y002043D01*
X004009Y001885D01*
X005980Y002242D02*
X005980Y002392D01*
X005980Y003092D02*
X005980Y003242D01*
X007780Y003242D02*
X007780Y003092D01*
X007780Y002392D02*
X007780Y002242D01*
X008482Y001543D02*
X008482Y001243D01*
X008732Y001243D01*
X008632Y001123D02*
X008332Y001123D01*
X008432Y001002D02*
X008532Y001002D01*
X008482Y001243D02*
X008232Y001243D01*
X008524Y005058D02*
X008624Y005058D01*
X008725Y005179D02*
X008425Y005179D01*
X008325Y005299D02*
X008575Y005299D01*
X008575Y005499D01*
X008575Y005299D02*
X008825Y005299D01*
X007631Y005903D02*
X007481Y005903D01*
X006781Y005903D02*
X006631Y005903D01*
X006631Y007703D02*
X006781Y007703D01*
X007481Y007703D02*
X007631Y007703D01*
X002268Y007064D02*
X002018Y007064D01*
X002018Y007364D01*
X002018Y007064D02*
X001768Y007064D01*
X001868Y006944D02*
X002168Y006944D01*
X002067Y006823D02*
X001967Y006823D01*
D12*
X003939Y002961D02*
X003978Y002905D01*
X004057Y002793D02*
X004213Y002568D01*
X004157Y002529D02*
X004269Y002608D01*
X004042Y002449D02*
X003924Y002617D01*
X003829Y002633D01*
X003812Y002538D01*
X003930Y002370D01*
X003818Y002292D02*
X003661Y002516D01*
X003717Y002555D01*
X003812Y002538D01*
X003624Y002323D02*
X003456Y002205D01*
X003439Y002110D01*
X003535Y002093D01*
X003703Y002211D01*
X003585Y002379D01*
X003490Y002396D01*
X003378Y002317D01*
X003207Y002198D02*
X003095Y002120D01*
X003112Y002215D02*
X003269Y001991D01*
X003364Y001974D01*
X003025Y001736D02*
X002790Y002072D01*
X002678Y001994D02*
X002902Y002151D01*
X002641Y001801D02*
X002798Y001577D01*
X002680Y001745D02*
X002456Y001588D01*
X002417Y001644D02*
X002450Y001835D01*
X002641Y001801D01*
X002417Y001644D02*
X002573Y001420D01*
X004001Y002753D02*
X004057Y002793D01*
D13*
X003790Y006387D03*
X004580Y006377D03*
D14*
X001680Y000842D02*
X000880Y000842D01*
X000880Y006742D01*
X001680Y006742D01*
X001680Y007742D01*
X000880Y007742D01*
X000880Y006742D01*
X001680Y006742D02*
X001680Y000842D01*
X003790Y006412D02*
X003700Y006582D01*
X003880Y006582D01*
X003790Y006412D01*
X003767Y006455D02*
X003813Y006455D01*
X003865Y006554D02*
X003715Y006554D01*
X004490Y006582D02*
X004580Y006412D01*
X004670Y006582D01*
X004490Y006582D01*
X004505Y006554D02*
X004655Y006554D01*
X004603Y006455D02*
X004557Y006455D01*
X003289Y009691D02*
X006833Y009691D01*
X008890Y007262D02*
X009060Y007352D01*
X009060Y007172D01*
X008890Y007262D01*
X008926Y007243D02*
X009060Y007243D01*
X009060Y007342D02*
X009040Y007342D01*
X008980Y006742D02*
X008880Y006642D01*
X008880Y005742D01*
X008880Y004742D01*
X009680Y004742D01*
X009680Y005742D01*
X008880Y005742D01*
X009680Y005742D02*
X009680Y006742D01*
X009280Y006742D01*
X008980Y002742D02*
X009680Y002742D01*
X009680Y001742D01*
X008880Y001742D01*
X008880Y002642D01*
X008980Y002742D01*
X008880Y001742D02*
X008880Y000942D01*
X008980Y000842D01*
X009680Y000842D01*
X009680Y001742D01*
D15*
X008845Y007262D03*
D16*
X008179Y006205D02*
X008179Y005969D01*
X007707Y005969D02*
X007707Y006205D01*
X007621Y005906D02*
X007621Y007706D01*
X007403Y008292D02*
X007316Y008292D01*
X007272Y008336D01*
X007151Y008336D02*
X007151Y008509D01*
X007108Y008553D01*
X006978Y008553D01*
X006978Y008292D01*
X007108Y008292D01*
X007151Y008336D01*
X007272Y008509D02*
X007316Y008553D01*
X007403Y008553D01*
X007446Y008509D01*
X007446Y008466D01*
X007403Y008422D01*
X007446Y008379D01*
X007446Y008336D01*
X007403Y008292D01*
X007403Y008422D02*
X007359Y008422D01*
X006621Y007706D02*
X006621Y005906D01*
X006416Y006424D02*
X006416Y006660D01*
X005944Y006660D02*
X005944Y006424D01*
X005616Y006424D02*
X005616Y006660D01*
X005144Y006660D02*
X005144Y006424D01*
X005274Y005303D02*
X003264Y005303D01*
X003216Y006424D02*
X003216Y006660D01*
X002744Y006660D02*
X002744Y006424D01*
X003264Y003443D02*
X005274Y003443D01*
X005222Y002876D02*
X005222Y002746D01*
X005308Y002789D01*
X005352Y002789D01*
X005395Y002746D01*
X005395Y002659D01*
X005352Y002616D01*
X005265Y002616D01*
X005222Y002659D01*
X005100Y002659D02*
X005057Y002616D01*
X004927Y002616D01*
X004927Y002876D01*
X005057Y002876D01*
X005100Y002833D01*
X005100Y002659D01*
X005222Y002876D02*
X005395Y002876D01*
X005983Y003252D02*
X007783Y003252D01*
X008330Y003442D02*
X008330Y003042D01*
X008332Y003025D01*
X008336Y003008D01*
X008343Y002992D01*
X008353Y002978D01*
X008366Y002965D01*
X008380Y002955D01*
X008396Y002948D01*
X008413Y002944D01*
X008430Y002942D01*
X009530Y002942D01*
X009547Y002944D01*
X009564Y002948D01*
X009580Y002955D01*
X009594Y002965D01*
X009607Y002978D01*
X009617Y002992D01*
X009624Y003008D01*
X009628Y003025D01*
X009630Y003042D01*
X009630Y003442D01*
X009030Y003442D01*
X009030Y003242D01*
X009130Y003642D02*
X008830Y003842D01*
X009630Y004042D01*
X009630Y004192D02*
X009130Y004492D01*
X009130Y004192D02*
X009630Y004592D01*
X008830Y003842D02*
X008530Y004042D01*
X008330Y003642D02*
X009130Y003642D01*
X009630Y003642D01*
X007783Y002252D02*
X005983Y002252D01*
X003437Y002620D02*
X003201Y002620D01*
X003201Y003092D02*
X003437Y003092D01*
X007274Y004879D02*
X007510Y004879D01*
X007510Y005352D02*
X007274Y005352D01*
D17*
X004888Y003412D02*
X004890Y003433D01*
X004896Y003452D01*
X004905Y003471D01*
X004917Y003487D01*
X004933Y003501D01*
X004950Y003512D01*
X004969Y003520D01*
X004990Y003524D01*
X005010Y003524D01*
X005031Y003520D01*
X005050Y003512D01*
X005067Y003501D01*
X005083Y003487D01*
X005095Y003471D01*
X005104Y003452D01*
X005110Y003433D01*
X005112Y003412D01*
X005110Y003391D01*
X005104Y003372D01*
X005095Y003353D01*
X005083Y003337D01*
X005067Y003323D01*
X005050Y003312D01*
X005031Y003304D01*
X005010Y003300D01*
X004990Y003300D01*
X004969Y003304D01*
X004950Y003312D01*
X004933Y003323D01*
X004917Y003337D01*
X004905Y003353D01*
X004896Y003372D01*
X004890Y003391D01*
X004888Y003412D01*
D18*
X004897Y003663D02*
X004899Y003678D01*
X004905Y003691D01*
X004914Y003703D01*
X004925Y003712D01*
X004939Y003718D01*
X004954Y003720D01*
X004969Y003718D01*
X004982Y003712D01*
X004994Y003703D01*
X005003Y003692D01*
X005009Y003678D01*
X005011Y003663D01*
X005009Y003648D01*
X005003Y003635D01*
X004994Y003623D01*
X004983Y003614D01*
X004969Y003608D01*
X004954Y003606D01*
X004939Y003608D01*
X004926Y003614D01*
X004914Y003623D01*
X004905Y003634D01*
X004899Y003648D01*
X004897Y003663D01*
X006782Y002742D02*
X007018Y002585D01*
X007018Y002899D01*
X006782Y002742D01*
X006841Y002742D02*
X006959Y002663D01*
X006959Y002821D01*
X006841Y002742D01*
X006897Y002705D02*
X006959Y002705D01*
X006959Y002783D02*
X006902Y002783D01*
X007061Y003541D02*
X007399Y003541D01*
X006911Y003995D02*
X006911Y004207D01*
X007061Y004661D02*
X007399Y004661D01*
X007131Y006705D02*
X007288Y006941D01*
X006974Y006941D01*
X007131Y006705D01*
X007131Y006764D02*
X007210Y006882D01*
X007052Y006882D01*
X007131Y006764D01*
X007116Y006787D02*
X007146Y006787D01*
X007199Y006865D02*
X007063Y006865D01*
X008091Y008742D02*
X008093Y008794D01*
X008099Y008846D01*
X008109Y008897D01*
X008122Y008947D01*
X008140Y008997D01*
X008161Y009044D01*
X008185Y009090D01*
X008214Y009134D01*
X008245Y009176D01*
X008279Y009215D01*
X008316Y009252D01*
X008356Y009285D01*
X008399Y009316D01*
X008443Y009343D01*
X008489Y009367D01*
X008538Y009387D01*
X008587Y009403D01*
X008638Y009416D01*
X008689Y009425D01*
X008741Y009430D01*
X008793Y009431D01*
X008845Y009428D01*
X008897Y009421D01*
X008948Y009410D01*
X008998Y009396D01*
X009047Y009377D01*
X009094Y009355D01*
X009139Y009330D01*
X009183Y009301D01*
X009224Y009269D01*
X009263Y009234D01*
X009298Y009196D01*
X009331Y009155D01*
X009361Y009113D01*
X009387Y009068D01*
X009410Y009021D01*
X009429Y008972D01*
X009445Y008922D01*
X009457Y008872D01*
X009465Y008820D01*
X009469Y008768D01*
X009469Y008716D01*
X009465Y008664D01*
X009457Y008612D01*
X009445Y008562D01*
X009429Y008512D01*
X009410Y008463D01*
X009387Y008416D01*
X009361Y008371D01*
X009331Y008329D01*
X009298Y008288D01*
X009263Y008250D01*
X009224Y008215D01*
X009183Y008183D01*
X009139Y008154D01*
X009094Y008129D01*
X009047Y008107D01*
X008998Y008088D01*
X008948Y008074D01*
X008897Y008063D01*
X008845Y008056D01*
X008793Y008053D01*
X008741Y008054D01*
X008689Y008059D01*
X008638Y008068D01*
X008587Y008081D01*
X008538Y008097D01*
X008489Y008117D01*
X008443Y008141D01*
X008399Y008168D01*
X008356Y008199D01*
X008316Y008232D01*
X008279Y008269D01*
X008245Y008308D01*
X008214Y008350D01*
X008185Y008394D01*
X008161Y008440D01*
X008140Y008487D01*
X008122Y008537D01*
X008109Y008587D01*
X008099Y008638D01*
X008093Y008690D01*
X008091Y008742D01*
X001091Y008742D02*
X001093Y008794D01*
X001099Y008846D01*
X001109Y008897D01*
X001122Y008947D01*
X001140Y008997D01*
X001161Y009044D01*
X001185Y009090D01*
X001214Y009134D01*
X001245Y009176D01*
X001279Y009215D01*
X001316Y009252D01*
X001356Y009285D01*
X001399Y009316D01*
X001443Y009343D01*
X001489Y009367D01*
X001538Y009387D01*
X001587Y009403D01*
X001638Y009416D01*
X001689Y009425D01*
X001741Y009430D01*
X001793Y009431D01*
X001845Y009428D01*
X001897Y009421D01*
X001948Y009410D01*
X001998Y009396D01*
X002047Y009377D01*
X002094Y009355D01*
X002139Y009330D01*
X002183Y009301D01*
X002224Y009269D01*
X002263Y009234D01*
X002298Y009196D01*
X002331Y009155D01*
X002361Y009113D01*
X002387Y009068D01*
X002410Y009021D01*
X002429Y008972D01*
X002445Y008922D01*
X002457Y008872D01*
X002465Y008820D01*
X002469Y008768D01*
X002469Y008716D01*
X002465Y008664D01*
X002457Y008612D01*
X002445Y008562D01*
X002429Y008512D01*
X002410Y008463D01*
X002387Y008416D01*
X002361Y008371D01*
X002331Y008329D01*
X002298Y008288D01*
X002263Y008250D01*
X002224Y008215D01*
X002183Y008183D01*
X002139Y008154D01*
X002094Y008129D01*
X002047Y008107D01*
X001998Y008088D01*
X001948Y008074D01*
X001897Y008063D01*
X001845Y008056D01*
X001793Y008053D01*
X001741Y008054D01*
X001689Y008059D01*
X001638Y008068D01*
X001587Y008081D01*
X001538Y008097D01*
X001489Y008117D01*
X001443Y008141D01*
X001399Y008168D01*
X001356Y008199D01*
X001316Y008232D01*
X001279Y008269D01*
X001245Y008308D01*
X001214Y008350D01*
X001185Y008394D01*
X001161Y008440D01*
X001140Y008487D01*
X001122Y008537D01*
X001109Y008587D01*
X001099Y008638D01*
X001093Y008690D01*
X001091Y008742D01*
D19*
X007131Y006665D03*
D20*
X008920Y007223D03*
X004580Y006443D03*
X003790Y006453D03*
D21*
X003526Y007860D02*
X003526Y009711D01*
X003526Y007860D02*
X006596Y007860D01*
X006596Y009711D01*
D22*
X006742Y002742D03*
M02*

View File

@ -0,0 +1,76 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10R,0.0870X0.0240*%
%ADD11R,0.0472X0.0315*%
%ADD12R,0.0420X0.0860*%
%ADD13R,0.0315X0.0472*%
%ADD14R,0.0472X0.0472*%
%ADD15R,0.0472X0.0217*%
%ADD16R,0.0906X0.0630*%
%ADD17R,0.0157X0.0531*%
%ADD18R,0.0748X0.0748*%
%ADD19R,0.0860X0.0420*%
D10*
X002944Y003623D03*
X002944Y004123D03*
X002944Y004623D03*
X002944Y005123D03*
X005594Y005123D03*
X005594Y004623D03*
X005594Y004123D03*
X005594Y003623D03*
D11*
X007943Y005733D03*
X007943Y006441D03*
X006180Y006188D03*
X006180Y006896D03*
X005380Y006896D03*
X005380Y006188D03*
X002980Y006188D03*
X002980Y006896D03*
D12*
X007131Y007653D03*
X007131Y005953D03*
D13*
X002965Y002856D03*
X003673Y002856D03*
X007038Y005116D03*
X007747Y005116D03*
D14*
X008546Y007223D03*
X009373Y007223D03*
X004580Y006895D03*
X003790Y006905D03*
X003790Y006079D03*
X004580Y006069D03*
D15*
X006718Y004475D03*
X006718Y003727D03*
X007742Y003727D03*
X007742Y004101D03*
X007742Y004475D03*
D16*
X006430Y001452D03*
X003950Y001452D03*
D17*
X004549Y008008D03*
X004805Y008008D03*
X005061Y008008D03*
X005317Y008008D03*
X005573Y008008D03*
D18*
X005533Y009061D03*
X004589Y009061D03*
X003506Y009061D03*
X006616Y009061D03*
D19*
X006030Y002742D03*
X007730Y002742D03*
M02*

View File

@ -0,0 +1,101 @@
G75*
%MOIN*%
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10R,0.0910X0.0280*%
%ADD11R,0.0512X0.0355*%
%ADD12R,0.0460X0.0900*%
%ADD13R,0.0355X0.0512*%
%ADD14R,0.0512X0.0512*%
%ADD15R,0.0512X0.0257*%
%ADD16C,0.1306*%
%ADD17R,0.0946X0.0670*%
%ADD18R,0.0197X0.0571*%
%ADD19R,0.0788X0.0788*%
%ADD20C,0.0276*%
%ADD21C,0.0700*%
%ADD22R,0.0900X0.0460*%
%ADD23C,0.0740*%
D10*
X002944Y003623D03*
X002944Y004123D03*
X002944Y004623D03*
X002944Y005123D03*
X005594Y005123D03*
X005594Y004623D03*
X005594Y004123D03*
X005594Y003623D03*
D11*
X007943Y005733D03*
X007943Y006441D03*
X006180Y006188D03*
X006180Y006896D03*
X005380Y006896D03*
X005380Y006188D03*
X002980Y006188D03*
X002980Y006896D03*
D12*
X007131Y007653D03*
X007131Y005953D03*
D13*
X007038Y005116D03*
X007747Y005116D03*
X003673Y002856D03*
X002965Y002856D03*
D14*
X003790Y006079D03*
X004580Y006069D03*
X004580Y006895D03*
X003790Y006905D03*
X008546Y007223D03*
X009373Y007223D03*
D15*
X007742Y004475D03*
X007742Y004101D03*
X007742Y003727D03*
X006718Y003727D03*
X006718Y004475D03*
D16*
X008780Y008742D03*
X001780Y008742D03*
D17*
X003950Y001452D03*
X006430Y001452D03*
D18*
X005573Y008008D03*
X005317Y008008D03*
X005061Y008008D03*
X004805Y008008D03*
X004549Y008008D03*
D19*
X004589Y009061D03*
X005533Y009061D03*
X006616Y009061D03*
X003506Y009061D03*
D20*
X004274Y008156D03*
X005848Y008156D03*
D21*
X001280Y001242D03*
X001280Y002242D03*
X001280Y003242D03*
X001280Y004242D03*
X001280Y005242D03*
X001280Y006242D03*
X001280Y007242D03*
X009280Y006242D03*
X009280Y005242D03*
X009280Y002242D03*
X009280Y001242D03*
D22*
X007730Y002742D03*
X006030Y002742D03*
D23*
X007720Y001662D03*
X002350Y007422D03*
M02*

View File

@ -0,0 +1,39 @@
%
M48
M72
T01C0.0236
T02C0.0394
T03C0.0400
T04C0.0866
%
T01
X2447Y3035
X3318Y2464
X4780Y4142
X4218Y5514
X5230Y5692
X7225Y4371
X7228Y3820
X6297Y3232
X7880Y7292
X5848Y8156
X4274Y8156
T02
X1280Y1242
X1280Y2242
X1280Y3242
X1280Y4242
X1280Y5242
X1280Y6242
X1280Y7242
X9280Y6242
X9280Y5242
X9280Y2242
X9280Y1242
T03
X7720Y1662
X2350Y7422
T04
X1780Y8742
X8780Y8742
M30

File diff suppressed because it is too large Load Diff

1448603
gerber/gerbmerge/4x4/ATtami.GBO Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,513 @@
G75*
G70*
%OFA0B0*%
%FSLAX25Y25*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.13060*%
%ADD12C,0.07000*%
%ADD16C,0.02760*%
%ADD25R,0.02900X0.01200*%
%ADD26C,0.07400*%
%ADD28R,0.00600X0.07200*%
%ADD29R,0.05000X0.06700*%
X0011000Y0011000D02*
G75*
%LPD*%
D10*
X0021000Y0091000D03*
X0091000Y0091000D03*
D16*
X0061680Y0085140D03*
X0045940Y0085140D03*
D12*
X0016000Y0016000D03*
X0016000Y0026000D03*
X0016000Y0036000D03*
X0016000Y0046000D03*
X0016000Y0056000D03*
X0016000Y0066000D03*
X0016000Y0076000D03*
X0096000Y0066000D03*
X0096000Y0056000D03*
X0096000Y0026000D03*
X0096000Y0016000D03*
D29*
X0094000Y0076000D03*
X0088000Y0076000D03*
D25*
X0091000Y0076000D03*
D28*
X0091000Y0076000D03*
D26*
X0080400Y0020200D03*
X0026700Y0077800D03*
X0011000Y0107250D02*
G75*
%LPD*%
D10*
X0021000Y0187250D03*
X0091000Y0187250D03*
D16*
X0061680Y0181390D03*
X0045940Y0181390D03*
D12*
X0016000Y0112250D03*
X0016000Y0122250D03*
X0016000Y0132250D03*
X0016000Y0142250D03*
X0016000Y0152250D03*
X0016000Y0162250D03*
X0016000Y0172250D03*
X0096000Y0162250D03*
X0096000Y0152250D03*
X0096000Y0122250D03*
X0096000Y0112250D03*
D29*
X0094000Y0172250D03*
X0088000Y0172250D03*
D25*
X0091000Y0172250D03*
D28*
X0091000Y0172250D03*
D26*
X0080400Y0116450D03*
X0026700Y0174050D03*
X0011000Y0203500D02*
G75*
%LPD*%
D10*
X0021000Y0283500D03*
X0091000Y0283500D03*
D16*
X0061680Y0277640D03*
X0045940Y0277640D03*
D12*
X0016000Y0208500D03*
X0016000Y0218500D03*
X0016000Y0228500D03*
X0016000Y0238500D03*
X0016000Y0248500D03*
X0016000Y0258500D03*
X0016000Y0268500D03*
X0096000Y0258500D03*
X0096000Y0248500D03*
X0096000Y0218500D03*
X0096000Y0208500D03*
D29*
X0094000Y0268500D03*
X0088000Y0268500D03*
D25*
X0091000Y0268500D03*
D28*
X0091000Y0268500D03*
D26*
X0080400Y0212700D03*
X0026700Y0270300D03*
X0011000Y0299750D02*
G75*
%LPD*%
D10*
X0021000Y0379750D03*
X0091000Y0379750D03*
D16*
X0061680Y0373890D03*
X0045940Y0373890D03*
D12*
X0016000Y0304750D03*
X0016000Y0314750D03*
X0016000Y0324750D03*
X0016000Y0334750D03*
X0016000Y0344750D03*
X0016000Y0354750D03*
X0016000Y0364750D03*
X0096000Y0354750D03*
X0096000Y0344750D03*
X0096000Y0314750D03*
X0096000Y0304750D03*
D29*
X0094000Y0364750D03*
X0088000Y0364750D03*
D25*
X0091000Y0364750D03*
D28*
X0091000Y0364750D03*
D26*
X0080400Y0308950D03*
X0026700Y0366550D03*
X0107130Y0011000D02*
G75*
%LPD*%
D10*
X0117130Y0091000D03*
X0187130Y0091000D03*
D16*
X0157810Y0085140D03*
X0142070Y0085140D03*
D12*
X0112130Y0016000D03*
X0112130Y0026000D03*
X0112130Y0036000D03*
X0112130Y0046000D03*
X0112130Y0056000D03*
X0112130Y0066000D03*
X0112130Y0076000D03*
X0192130Y0066000D03*
X0192130Y0056000D03*
X0192130Y0026000D03*
X0192130Y0016000D03*
D29*
X0190130Y0076000D03*
X0184130Y0076000D03*
D25*
X0187130Y0076000D03*
D28*
X0187130Y0076000D03*
D26*
X0176530Y0020200D03*
X0122830Y0077800D03*
X0107130Y0107250D02*
G75*
%LPD*%
D10*
X0117130Y0187250D03*
X0187130Y0187250D03*
D16*
X0157810Y0181390D03*
X0142070Y0181390D03*
D12*
X0112130Y0112250D03*
X0112130Y0122250D03*
X0112130Y0132250D03*
X0112130Y0142250D03*
X0112130Y0152250D03*
X0112130Y0162250D03*
X0112130Y0172250D03*
X0192130Y0162250D03*
X0192130Y0152250D03*
X0192130Y0122250D03*
X0192130Y0112250D03*
D29*
X0190130Y0172250D03*
X0184130Y0172250D03*
D25*
X0187130Y0172250D03*
D28*
X0187130Y0172250D03*
D26*
X0176530Y0116450D03*
X0122830Y0174050D03*
X0107130Y0203500D02*
G75*
%LPD*%
D10*
X0117130Y0283500D03*
X0187130Y0283500D03*
D16*
X0157810Y0277640D03*
X0142070Y0277640D03*
D12*
X0112130Y0208500D03*
X0112130Y0218500D03*
X0112130Y0228500D03*
X0112130Y0238500D03*
X0112130Y0248500D03*
X0112130Y0258500D03*
X0112130Y0268500D03*
X0192130Y0258500D03*
X0192130Y0248500D03*
X0192130Y0218500D03*
X0192130Y0208500D03*
D29*
X0190130Y0268500D03*
X0184130Y0268500D03*
D25*
X0187130Y0268500D03*
D28*
X0187130Y0268500D03*
D26*
X0176530Y0212700D03*
X0122830Y0270300D03*
X0107130Y0299750D02*
G75*
%LPD*%
D10*
X0117130Y0379750D03*
X0187130Y0379750D03*
D16*
X0157810Y0373890D03*
X0142070Y0373890D03*
D12*
X0112130Y0304750D03*
X0112130Y0314750D03*
X0112130Y0324750D03*
X0112130Y0334750D03*
X0112130Y0344750D03*
X0112130Y0354750D03*
X0112130Y0364750D03*
X0192130Y0354750D03*
X0192130Y0344750D03*
X0192130Y0314750D03*
X0192130Y0304750D03*
D29*
X0190130Y0364750D03*
X0184130Y0364750D03*
D25*
X0187130Y0364750D03*
D28*
X0187130Y0364750D03*
D26*
X0176530Y0308950D03*
X0122830Y0366550D03*
X0203260Y0011000D02*
G75*
%LPD*%
D10*
X0213260Y0091000D03*
X0283260Y0091000D03*
D16*
X0253940Y0085140D03*
X0238200Y0085140D03*
D12*
X0208260Y0016000D03*
X0208260Y0026000D03*
X0208260Y0036000D03*
X0208260Y0046000D03*
X0208260Y0056000D03*
X0208260Y0066000D03*
X0208260Y0076000D03*
X0288260Y0066000D03*
X0288260Y0056000D03*
X0288260Y0026000D03*
X0288260Y0016000D03*
D29*
X0286260Y0076000D03*
X0280260Y0076000D03*
D25*
X0283260Y0076000D03*
D28*
X0283260Y0076000D03*
D26*
X0272660Y0020200D03*
X0218960Y0077800D03*
X0203260Y0107250D02*
G75*
%LPD*%
D10*
X0213260Y0187250D03*
X0283260Y0187250D03*
D16*
X0253940Y0181390D03*
X0238200Y0181390D03*
D12*
X0208260Y0112250D03*
X0208260Y0122250D03*
X0208260Y0132250D03*
X0208260Y0142250D03*
X0208260Y0152250D03*
X0208260Y0162250D03*
X0208260Y0172250D03*
X0288260Y0162250D03*
X0288260Y0152250D03*
X0288260Y0122250D03*
X0288260Y0112250D03*
D29*
X0286260Y0172250D03*
X0280260Y0172250D03*
D25*
X0283260Y0172250D03*
D28*
X0283260Y0172250D03*
D26*
X0272660Y0116450D03*
X0218960Y0174050D03*
X0203260Y0203500D02*
G75*
%LPD*%
D10*
X0213260Y0283500D03*
X0283260Y0283500D03*
D16*
X0253940Y0277640D03*
X0238200Y0277640D03*
D12*
X0208260Y0208500D03*
X0208260Y0218500D03*
X0208260Y0228500D03*
X0208260Y0238500D03*
X0208260Y0248500D03*
X0208260Y0258500D03*
X0208260Y0268500D03*
X0288260Y0258500D03*
X0288260Y0248500D03*
X0288260Y0218500D03*
X0288260Y0208500D03*
D29*
X0286260Y0268500D03*
X0280260Y0268500D03*
D25*
X0283260Y0268500D03*
D28*
X0283260Y0268500D03*
D26*
X0272660Y0212700D03*
X0218960Y0270300D03*
X0203260Y0299750D02*
G75*
%LPD*%
D10*
X0213260Y0379750D03*
X0283260Y0379750D03*
D16*
X0253940Y0373890D03*
X0238200Y0373890D03*
D12*
X0208260Y0304750D03*
X0208260Y0314750D03*
X0208260Y0324750D03*
X0208260Y0334750D03*
X0208260Y0344750D03*
X0208260Y0354750D03*
X0208260Y0364750D03*
X0288260Y0354750D03*
X0288260Y0344750D03*
X0288260Y0314750D03*
X0288260Y0304750D03*
D29*
X0286260Y0364750D03*
X0280260Y0364750D03*
D25*
X0283260Y0364750D03*
D28*
X0283260Y0364750D03*
D26*
X0272660Y0308950D03*
X0218960Y0366550D03*
X0299390Y0011000D02*
G75*
%LPD*%
D10*
X0309390Y0091000D03*
X0379390Y0091000D03*
D16*
X0350070Y0085140D03*
X0334330Y0085140D03*
D12*
X0304390Y0016000D03*
X0304390Y0026000D03*
X0304390Y0036000D03*
X0304390Y0046000D03*
X0304390Y0056000D03*
X0304390Y0066000D03*
X0304390Y0076000D03*
X0384390Y0066000D03*
X0384390Y0056000D03*
X0384390Y0026000D03*
X0384390Y0016000D03*
D29*
X0382390Y0076000D03*
X0376390Y0076000D03*
D25*
X0379390Y0076000D03*
D28*
X0379390Y0076000D03*
D26*
X0368790Y0020200D03*
X0315090Y0077800D03*
X0299390Y0107250D02*
G75*
%LPD*%
D10*
X0309390Y0187250D03*
X0379390Y0187250D03*
D16*
X0350070Y0181390D03*
X0334330Y0181390D03*
D12*
X0304390Y0112250D03*
X0304390Y0122250D03*
X0304390Y0132250D03*
X0304390Y0142250D03*
X0304390Y0152250D03*
X0304390Y0162250D03*
X0304390Y0172250D03*
X0384390Y0162250D03*
X0384390Y0152250D03*
X0384390Y0122250D03*
X0384390Y0112250D03*
D29*
X0382390Y0172250D03*
X0376390Y0172250D03*
D25*
X0379390Y0172250D03*
D28*
X0379390Y0172250D03*
D26*
X0368790Y0116450D03*
X0315090Y0174050D03*
X0299390Y0203500D02*
G75*
%LPD*%
D10*
X0309390Y0283500D03*
X0379390Y0283500D03*
D16*
X0350070Y0277640D03*
X0334330Y0277640D03*
D12*
X0304390Y0208500D03*
X0304390Y0218500D03*
X0304390Y0228500D03*
X0304390Y0238500D03*
X0304390Y0248500D03*
X0304390Y0258500D03*
X0304390Y0268500D03*
X0384390Y0258500D03*
X0384390Y0248500D03*
X0384390Y0218500D03*
X0384390Y0208500D03*
D29*
X0382390Y0268500D03*
X0376390Y0268500D03*
D25*
X0379390Y0268500D03*
D28*
X0379390Y0268500D03*
D26*
X0368790Y0212700D03*
X0315090Y0270300D03*
X0299390Y0299750D02*
G75*
%LPD*%
D10*
X0309390Y0379750D03*
X0379390Y0379750D03*
D16*
X0350070Y0373890D03*
X0334330Y0373890D03*
D12*
X0304390Y0304750D03*
X0304390Y0314750D03*
X0304390Y0324750D03*
X0304390Y0334750D03*
X0304390Y0344750D03*
X0304390Y0354750D03*
X0304390Y0364750D03*
X0384390Y0354750D03*
X0384390Y0344750D03*
X0384390Y0314750D03*
X0384390Y0304750D03*
D29*
X0382390Y0364750D03*
X0376390Y0364750D03*
D25*
X0379390Y0364750D03*
D28*
X0379390Y0364750D03*
D26*
X0368790Y0308950D03*
X0315090Y0366550D03*
M02*

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,421 @@
%
T01C0.023600
X2767Y3393
X3638Y2822
X5100Y4500
X4538Y5872
X5550Y6050
X7545Y4729
X7548Y4178
X6617Y3590
X8200Y7650
X6168Y8514
X4594Y8514
X2767Y13018
X3638Y12447
X5100Y14125
X4538Y15497
X5550Y15675
X7545Y14354
X7548Y13803
X6617Y13215
X8200Y17275
X6168Y18139
X4594Y18139
X2767Y22643
X3638Y22072
X5100Y23750
X4538Y25122
X5550Y25300
X7545Y23979
X7548Y23428
X6617Y22840
X8200Y26900
X6168Y27764
X4594Y27764
X2767Y32268
X3638Y31697
X5100Y33375
X4538Y34747
X5550Y34925
X7545Y33604
X7548Y33053
X6617Y32465
X8200Y36525
X6168Y37389
X4594Y37389
X12380Y3393
X13251Y2822
X14713Y4500
X14151Y5872
X15163Y6050
X17158Y4729
X17161Y4178
X16230Y3590
X17813Y7650
X15781Y8514
X14207Y8514
X12380Y13018
X13251Y12447
X14713Y14125
X14151Y15497
X15163Y15675
X17158Y14354
X17161Y13803
X16230Y13215
X17813Y17275
X15781Y18139
X14207Y18139
X12380Y22643
X13251Y22072
X14713Y23750
X14151Y25122
X15163Y25300
X17158Y23979
X17161Y23428
X16230Y22840
X17813Y26900
X15781Y27764
X14207Y27764
X12380Y32268
X13251Y31697
X14713Y33375
X14151Y34747
X15163Y34925
X17158Y33604
X17161Y33053
X16230Y32465
X17813Y36525
X15781Y37389
X14207Y37389
X21993Y3393
X22864Y2822
X24326Y4500
X23764Y5872
X24776Y6050
X26771Y4729
X26774Y4178
X25843Y3590
X27426Y7650
X25394Y8514
X23820Y8514
X21993Y13018
X22864Y12447
X24326Y14125
X23764Y15497
X24776Y15675
X26771Y14354
X26774Y13803
X25843Y13215
X27426Y17275
X25394Y18139
X23820Y18139
X21993Y22643
X22864Y22072
X24326Y23750
X23764Y25122
X24776Y25300
X26771Y23979
X26774Y23428
X25843Y22840
X27426Y26900
X25394Y27764
X23820Y27764
X21993Y32268
X22864Y31697
X24326Y33375
X23764Y34747
X24776Y34925
X26771Y33604
X26774Y33053
X25843Y32465
X27426Y36525
X25394Y37389
X23820Y37389
X31606Y3393
X32477Y2822
X33939Y4500
X33377Y5872
X34389Y6050
X36384Y4729
X36387Y4178
X35456Y3590
X37039Y7650
X35007Y8514
X33433Y8514
X31606Y13018
X32477Y12447
X33939Y14125
X33377Y15497
X34389Y15675
X36384Y14354
X36387Y13803
X35456Y13215
X37039Y17275
X35007Y18139
X33433Y18139
X31606Y22643
X32477Y22072
X33939Y23750
X33377Y25122
X34389Y25300
X36384Y23979
X36387Y23428
X35456Y22840
X37039Y26900
X35007Y27764
X33433Y27764
X31606Y32268
X32477Y31697
X33939Y33375
X33377Y34747
X34389Y34925
X36384Y33604
X36387Y33053
X35456Y32465
X37039Y36525
X35007Y37389
X33433Y37389
T02C0.039700
X8040Y2020
X2670Y7780
X1600Y1600
X1600Y2600
X1600Y3600
X1600Y4600
X1600Y5600
X1600Y6600
X1600Y7600
X9600Y6600
X9600Y5600
X9600Y2600
X9600Y1600
X8040Y11645
X2670Y17405
X1600Y11225
X1600Y12225
X1600Y13225
X1600Y14225
X1600Y15225
X1600Y16225
X1600Y17225
X9600Y16225
X9600Y15225
X9600Y12225
X9600Y11225
X8040Y21270
X2670Y27030
X1600Y20850
X1600Y21850
X1600Y22850
X1600Y23850
X1600Y24850
X1600Y25850
X1600Y26850
X9600Y25850
X9600Y24850
X9600Y21850
X9600Y20850
X8040Y30895
X2670Y36655
X1600Y30475
X1600Y31475
X1600Y32475
X1600Y33475
X1600Y34475
X1600Y35475
X1600Y36475
X9600Y35475
X9600Y34475
X9600Y31475
X9600Y30475
X17653Y2020
X12283Y7780
X11213Y1600
X11213Y2600
X11213Y3600
X11213Y4600
X11213Y5600
X11213Y6600
X11213Y7600
X19213Y6600
X19213Y5600
X19213Y2600
X19213Y1600
X17653Y11645
X12283Y17405
X11213Y11225
X11213Y12225
X11213Y13225
X11213Y14225
X11213Y15225
X11213Y16225
X11213Y17225
X19213Y16225
X19213Y15225
X19213Y12225
X19213Y11225
X17653Y21270
X12283Y27030
X11213Y20850
X11213Y21850
X11213Y22850
X11213Y23850
X11213Y24850
X11213Y25850
X11213Y26850
X19213Y25850
X19213Y24850
X19213Y21850
X19213Y20850
X17653Y30895
X12283Y36655
X11213Y30475
X11213Y31475
X11213Y32475
X11213Y33475
X11213Y34475
X11213Y35475
X11213Y36475
X19213Y35475
X19213Y34475
X19213Y31475
X19213Y30475
X27266Y2020
X21896Y7780
X20826Y1600
X20826Y2600
X20826Y3600
X20826Y4600
X20826Y5600
X20826Y6600
X20826Y7600
X28826Y6600
X28826Y5600
X28826Y2600
X28826Y1600
X27266Y11645
X21896Y17405
X20826Y11225
X20826Y12225
X20826Y13225
X20826Y14225
X20826Y15225
X20826Y16225
X20826Y17225
X28826Y16225
X28826Y15225
X28826Y12225
X28826Y11225
X27266Y21270
X21896Y27030
X20826Y20850
X20826Y21850
X20826Y22850
X20826Y23850
X20826Y24850
X20826Y25850
X20826Y26850
X28826Y25850
X28826Y24850
X28826Y21850
X28826Y20850
X27266Y30895
X21896Y36655
X20826Y30475
X20826Y31475
X20826Y32475
X20826Y33475
X20826Y34475
X20826Y35475
X20826Y36475
X28826Y35475
X28826Y34475
X28826Y31475
X28826Y30475
X36879Y2020
X31509Y7780
X30439Y1600
X30439Y2600
X30439Y3600
X30439Y4600
X30439Y5600
X30439Y6600
X30439Y7600
X38439Y6600
X38439Y5600
X38439Y2600
X38439Y1600
X36879Y11645
X31509Y17405
X30439Y11225
X30439Y12225
X30439Y13225
X30439Y14225
X30439Y15225
X30439Y16225
X30439Y17225
X38439Y16225
X38439Y15225
X38439Y12225
X38439Y11225
X36879Y21270
X31509Y27030
X30439Y20850
X30439Y21850
X30439Y22850
X30439Y23850
X30439Y24850
X30439Y25850
X30439Y26850
X38439Y25850
X38439Y24850
X38439Y21850
X38439Y20850
X36879Y30895
X31509Y36655
X30439Y30475
X30439Y31475
X30439Y32475
X30439Y33475
X30439Y34475
X30439Y35475
X30439Y36475
X38439Y35475
X38439Y34475
X38439Y31475
X38439Y30475
T03C0.086600
X2100Y9100
X9100Y9100
X2100Y18725
X9100Y18725
X2100Y28350
X9100Y28350
X2100Y37975
X9100Y37975
X11713Y9100
X18713Y9100
X11713Y18725
X18713Y18725
X11713Y28350
X18713Y28350
X11713Y37975
X18713Y37975
X21326Y9100
X28326Y9100
X21326Y18725
X28326Y18725
X21326Y28350
X28326Y28350
X21326Y37975
X28326Y37975
X30939Y9100
X37939Y9100
X30939Y18725
X37939Y18725
X30939Y28350
X37939Y28350
X30939Y37975
X37939Y37975
M30

View File

@ -0,0 +1,24 @@
G75*
G70*
%OFA0B0*%
%FSLAX25Y25*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
%ADD10C,0.00100*%
D10*
X0010000Y0104125D02*
X0390270Y0104125D01*
X0010000Y0200375D02*
X0390270Y0200375D01*
X0010000Y0296625D02*
X0390270Y0296625D01*
X0104005Y0010000D02*
X0104005Y0390750D01*
X0200135Y0010000D02*
X0200135Y0390750D01*
X0296265Y0010000D02*
X0296265Y0390750D01*
M02*

View File

@ -0,0 +1 @@
# Placeholder for GerbMerge package

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,348 @@
#!/usr/bin/env python
"""
Define and manage aperture macros (%AM command). Currently,
only macros without replaceable parameters (e.g., $1, $2, etc.)
are supported.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import re
import string
import copy
import config
_macro_pat = re.compile(r'^%AM([^*]+)\*$')
# This list stores the expected types of parameters for each primitive type
# (e.g., outline, line, circle, polygon, etc.). None is used for undefined
# primitives. Each entry corresponds to the defined primitive code, and
# comprises a tuple of conversion functions (i.e., built-in int() and float()
# functions) that apply to all parameters AFTER the primitive code. For example,
# code 1 (circle) may be instantiated as:
# 1,1,0.025,0.0,0.0
# (the parameters are code, exposure type, diameter, X center, Y center).
# After the integer code, we expect an int for exposure type, then floats
# for the remaining three parameters. Thus, the entry for code 1 is
# (int, float, float, float).
PrimitiveParmTypes = (
None, # Code 0 -- undefined
(int, float, float, float), # Code 1 -- circle
(int, float, float, float, float, float, float), # Code 2 -- line (vector)
None, # Code 3 -- end-of-file for .DES files
(int, int, float, float, float, float, float), # Code 4 -- outline...takes any number of additional floats
(int, int, float, float, float, float), # Code 5 -- regular polygon
(float, float, float, float, float, int, float, float, float), # Code 6 -- moire
(float, float, float, float, float, float), # Code 7 -- thermal
None, # Code 8 -- undefined
None, # Code 9 -- undefined
None, # Code 10 -- undefined
None, # Code 11 -- undefined
None, # Code 12 -- undefined
None, # Code 13 -- undefined
None, # Code 14 -- undefined
None, # Code 15 -- undefined
None, # Code 16 -- undefined
None, # Code 17 -- undefined
None, # Code 18 -- undefined
None, # Code 19 -- undefined
(int, float, float, float, float, float), # Code 20 -- line (vector)...alias for code 2
(int, float, float, float, float, float), # Code 21 -- line (center)
(int, float, float, float, float, float) # Code 22 -- line (lower-left)
)
def rotatexy(x,y):
# Rotate point (x,y) counterclockwise 90 degrees about the origin
return (-y,x)
def rotatexypair(L, ix):
# Rotate list items L[ix],L[ix+1] by 90 degrees
L[ix],L[ix+1] = rotatexy(L[ix],L[ix+1])
def swapxypair(L, ix):
# Swap two list elements
L[ix],L[ix+1] = L[ix+1],L[ix]
def rotatetheta(th):
# Increase angle th in degrees by +90 degrees (counterclockwise).
# Handle modulo 360 issues
th += 90
if th >= 360:
th -= 360
return th
def rotatethelem(L, ix):
# Increase angle th by +90 degrees for a list element
L[ix] = rotatetheta(L[ix])
class ApertureMacroPrimitive:
def __init__(self, code=-1, fields=None):
self.code = code
self.parms = []
if fields is not None:
self.setFromFields(code, fields)
def setFromFields(self, code, fields):
# code is an integer describing the primitive type, and fields is
# a list of STRINGS for each parameter
self.code = code
# valids will be one of the PrimitiveParmTypes tuples above. Some are
# None to indicate illegal codes. We also set valids to None to indicate
# the macro primitive code is outside the range of known types.
try:
valids = PrimitiveParmTypes[code]
except:
valids = None
if valids is None:
raise RuntimeError, 'Undefined aperture macro primitive code %d' % code
# We expect exactly the number of fields required, except for macro
# type 4 which is an outline and has a variable number of points.
# For outlines, the second parameter indicates the number of points,
# each of which has an (X,Y) co-ordinate. Thus, we expect an Outline
# specification to have 1+1+2*N+1=3+2N fields:
# - first field is exposure
# - second field is number of points
# - 2*N fields for X,Y points
# - last field is rotation
if self.code==4:
if len(fields) < 2:
raise RuntimeError, 'Outline macro primitive has way too few fields'
try:
N = int(fields[1])
except:
raise RuntimeError, 'Outline macro primitive has non-integer number of points'
if len(fields) != (3+2*N):
raise RuntimeError, 'Outline macro primitive has %d fields...expecting %d fields' % (len(fields), 3+2*N)
else:
if len(fields) != len(valids):
raise RuntimeError, 'Macro primitive has %d fields...expecting %d fields' % (len(fields), len(valids))
# Convert each parameter on the input line to an entry in the self.parms
# list, using either int() or float() conversion.
for parmix in range(len(fields)):
try:
converter = valids[parmix]
except:
converter = float # To handle variable number of points in Outline type
try:
self.parms.append(converter(fields[parmix]))
except:
raise RuntimeError, 'Aperture macro primitive parameter %d has incorrect type' % (parmix+1)
def setFromLine(self, line):
# Account for DOS line endings and get rid of line ending and '*' at the end
line = line.replace('\x0D', '')
line = line.rstrip()
line = line.rstrip('*')
fields = line.split(',')
try:
try:
code = int(fields[0])
except:
raise RuntimeError, 'Illegal aperture macro primitive code "%s"' % fields[0]
self.setFromFields(code, fields[1:])
except:
print '='*20
print "==> ", line
print '='*20
raise
def rotate(self):
if self.code == 1: # Circle: nothing to do
pass
elif self.code in (2,20): # Line (vector): fields (2,3) and (4,5) must be rotated, no need to
# rotate field 6
rotatexypair(self.parms, 2)
rotatexypair(self.parms, 4)
elif self.code == 21: # Line (center): fields (3,4) must be rotated, and field 5 incremented by +90
rotatexypair(self.parms, 3)
rotatethelem(self.parms, 5)
elif self.code == 22: # Line (lower-left): fields (3,4) must be rotated, and field 5 incremented by +90
rotatexypair(self.parms, 3)
rotatethelem(self.parms, 5)
elif self.code == 4: # Outline: fields (2,3), (4,5), etc. must be rotated, the last field need not be incremented
ix = 2
for pts in range(self.parms[1]): # parms[1] is the number of points
rotatexypair(self.parms, ix)
ix += 2
#rotatethelem(self.parms, ix)
elif self.code == 5: # Polygon: fields (2,3) must be rotated, and field 5 incremented by +90
rotatexypair(self.parms, 2)
rotatethelem(self.parms, 5)
elif self.code == 6: # Moire: fields (0,1) must be rotated, and field 8 incremented by +90
rotatexypair(self.parms, 0)
rotatethelem(self.parms, 8)
elif self.code == 7: # Thermal: fields (0,1) must be rotated, and field 5 incremented by +90
rotatexypair(self.parms, 0)
rotatethelem(self.parms, 5)
def __str__(self):
# Construct a string with ints as ints and floats as floats
s = '%d' % self.code
for parmix in range(len(self.parms)):
valids = PrimitiveParmTypes[self.code]
format = ',%f'
try:
if valids[parmix] is int:
format = ',%d'
except:
pass # '%f' is OK for Outline extra points
s += format % self.parms[parmix]
return s
def writeDef(self, fid):
fid.write('%s*\n' % str(self))
class ApertureMacro:
def __init__(self, name):
self.name = name
self.prim = []
def add(self, prim):
self.prim.append(prim)
def rotate(self):
for prim in self.prim:
prim.rotate()
def rotated(self):
# Return copy of ourselves, rotated. Replace 'R' as the first letter of the
# macro name. We don't append because we like to be able to count the
# number of aperture macros by stripping off the leading character.
M = copy.deepcopy(self)
M.rotate()
M.name = 'R'+M.name[1:]
return M
def dump(self, fid=sys.stdout):
fid.write(str(self))
def __str__(self):
s = '%s:\n' % self.name
s += self.hash()
return s
def hash(self):
s = ''
for prim in self.prim:
s += ' '+str(prim)+'\n'
return s
def writeDef(self, fid):
fid.write('%%AM%s*\n' % self.name)
for prim in self.prim:
prim.writeDef(fid)
fid.write('%\n')
def parseApertureMacro(s, fid):
match = _macro_pat.match(s)
if match:
name = match.group(1)
M = ApertureMacro(name)
for line in fid:
if line[0]=='%':
return M
P = ApertureMacroPrimitive()
P.setFromLine(line)
M.add(P)
else:
raise RuntimeError, "Premature end-of-file while parsing aperture macro"
else:
return None
# This function adds the new aperture macro AM to the global aperture macro
# table. The return value is the modified macro (name modified to be its global
# name). macro.
def addToApertureMacroTable(AM):
GAMT = config.GAMT
# Must sort keys by integer value, not string since 99 comes before 100
# as an integer but not a string.
keys = map(int, map(lambda K: K[1:], GAMT.keys()))
keys.sort()
if len(keys):
lastCode = keys[-1]
else:
lastCode = 0
mcode = 'M%d' % (lastCode+1)
AM.name = mcode
GAMT[mcode] = AM
return AM
if __name__=="__main__":
# Create a funky aperture macro with all the fixins, and make sure
# it rotates properly.
M = ApertureMacro('TEST')
# X and Y axes
M.add(ApertureMacroPrimitive(2, ('1', '0.0025', '0.0', '-0.1', '0.0', '0.1', '0.0')))
M.add(ApertureMacroPrimitive(2, ('1', '0.0025', '0.0', '-0.1', '0.0', '0.1', '90.0')))
# A circle in the top-right quadrant, touching the axes
M.add(ApertureMacroPrimitive(1, ('1', '0.02', '0.01', '0.01')))
# A line of slope -1 centered on the above circle, of thickness 5mil, length 0.05
M.add(ApertureMacroPrimitive(2, ('1', '0.005', '0.0', '0.02', '0.02', '0.0', '0.0')))
# A narrow vertical rectangle centered on the circle of width 2.5mil
M.add(ApertureMacroPrimitive(21, ('1', '0.0025', '0.03', '0.01', '0.01', '0.0')))
# A 45-degree line in the third quadrant, not quite touching the origin
M.add(ApertureMacroPrimitive(22, ('1', '0.02', '0.01', '-0.03', '-0.03', '45')))
# A right triangle in the second quadrant
M.add(ApertureMacroPrimitive(4, ('1', '4', '-0.03', '0.01', '-0.03', '0.03', '-0.01', '0.01', '-0.03', '0.01', '0.0')))
# A pentagon in the fourth quadrant, rotated by 15 degrees
M.add(ApertureMacroPrimitive(5, ('1', '5', '0.03', '-0.03', '0.02', '15')))
# A moire in the first quadrant, beyond the circle, with 2 annuli
M.add(ApertureMacroPrimitive(6, ('0.07', '0.07', '0.04', '0.005', '0.01', '2', '0.005', '0.04', '0.0')))
# A thermal in the second quadrant, beyond the right triangle
M.add(ApertureMacroPrimitive(7, ('-0.07', '0.07', '0.03', '0.02', '0.005', '15')))
MR = M.rotated()
# Generate the Gerber so we can view it
fid = file('amacro.ger', 'wt')
print >> fid, \
"""G75*
G70*
%OFA0B0*%
%FSLAX24Y24*%
%IPPOS*%
%LPD*%"""
M.writeDef(fid)
MR.writeDef(fid)
print >> fid, \
"""%ADD10TEST*%
%ADD11TESTR*%
D10*
X010000Y010000D03*
D11*
X015000Y010000D03*
M02*"""
fid.close()
print M
print MR

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,335 @@
#!/usr/bin/env python
"""
Manage apertures, read aperture table, etc.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import re
import string
import config
import amacro
import util
# Recognized apertures and re pattern that matches its definition Thermals and
# annuli are generated using macros (see the eagle.def file) but only on inner
# layers. Octagons are also generated as macros (%AMOC8) but we handle these
# specially as the Eagle macro uses a replaceable macro parameter ($1) and
# GerbMerge doesn't handle these yet...only fixed macros (no parameters) are
# currently supported.
Apertures = (
('Rectangle', re.compile(r'^%AD(D\d+)R,([^X]+)X([^*]+)\*%$'), '%%AD%sR,%.5fX%.5f*%%\n'),
('Circle', re.compile(r'^%AD(D\d+)C,([^*]+)\*%$'), '%%AD%sC,%.5f*%%\n'),
('Oval', re.compile(r'^%AD(D\d+)O,([^X]+)X([^*]+)\*%$'), '%%AD%sO,%.5fX%.5f*%%\n'),
('Octagon', re.compile(r'^%AD(D\d+)OC8,([^*]+)\*%$'), '%%AD%sOC8,%.5f*%%\n'), # Specific to Eagle
('Macro', re.compile(r'^%AD(D\d+)([^*]+)\*%$'), '%%AD%s%s*%%\n')
)
# This loop defines names in this module like 'Rectangle',
# which are element 0 of the Apertures list above. So code
# will be like:
# import aptable
# A = aptable.Aperture(aptable.Rectangle, ......)
for ap in Apertures:
globals()[ap[0]] = ap
class Aperture:
def __init__(self, aptype, code, dimx, dimy=None):
assert aptype in Apertures
self.apname, self.pat, self.format = aptype
self.code = code
self.dimx = dimx # Macro name for Macro apertures
self.dimy = dimy # None for Macro apertures
if self.apname in ('Circle', 'Octagon', 'Macro'):
assert (dimy is None)
def isRectangle(self):
return self.apname == 'Rectangle'
def rectangleAsRect(self, X, Y):
"""Return a 4-tuple (minx,miny,maxx,maxy) describing the area covered by
this Rectangle aperture when flashed at center co-ordinates (X,Y)"""
dx = util.in2gerb(self.dimx)
dy = util.in2gerb(self.dimy)
if dx & 1: # Odd-sized: X extents are (dx+1)/2 on the left and (dx-1)/2 on the right
xm = (dx+1)/2
xp = xm-1
else: # Even-sized: X extents are X-dx/2 and X+dx/2
xm = xp = dx/2
if dy & 1: # Odd-sized: Y extents are (dy+1)/2 below and (dy-1)/2 above
ym = (dy+1)/2
yp = ym-1
else: # Even-sized: Y extents are Y-dy/2 and Y+dy/2
ym = yp = dy/2
return (X-xm, Y-ym, X+xp, Y+yp)
def getAdjusted(self, minimum):
"""
Adjust aperture properties to conform to minimum feature dimensions
Return new aperture if required, else return False
"""
dimx = dimy = None
# Check for X and Y dimensions less than minimum
if (self.dimx != None) and (self.dimx < minimum):
dimx = minimum
if (self.dimy != None) and (self.dimx < minimum):
dimy = minimum
# Return new aperture if needed
if (dimx != None) or (dimy != None):
if dimx==None: dimx=self.dimx
if dimy==None: dimy=self.dimy
return Aperture( (self.apname, self.pat, self.format), self.code, dimx, dimy )
else:
return False ## no new aperture needs to be created
def rotate(self, RevGAMT):
if self.apname in ('Macro',):
# Construct a rotated macro, see if it's in the GAMT, and set self.dimx
# to its name if so. If not, add the rotated macro to the GAMT and set
# self.dimx to the new name. Recall that GAMT maps name to macro
# (e.g., GAMT['M9'] = ApertureMacro(...)) while RevGAMT maps hash to
# macro name (e.g., RevGAMT[hash] = 'M9')
AMR = config.GAMT[self.dimx].rotated()
hash = AMR.hash()
try:
self.dimx = RevGAMT[hash]
except KeyError:
AMR = amacro.addToApertureMacroTable(AMR) # adds to GAMT and modifies name to global name
self.dimx = RevGAMT[hash] = AMR.name
elif self.dimy is not None: # Rectangles and Ovals have a dimy setting and need to be rotated
t = self.dimx
self.dimx = self.dimy
self.dimy = t
def rotated(self, RevGAMT):
# deepcopy doesn't work on re patterns for some reason so we copy ourselves manually
APR = Aperture((self.apname, self.pat, self.format), self.code, self.dimx, self.dimy)
APR.rotate(RevGAMT)
return APR
def dump(self, fid=sys.stdout):
fid.write(str(self))
def __str__(self):
return '%s: %s' % (self.code, self.hash())
#if 0:
# if self.dimy:
# return ('%s: %s (%.4f x %.4f)' % (self.code, self.apname, self.dimx, self.dimy))
# else:
# if self.apname in ('Macro'):
# return ('%s: %s (%s)' % (self.code, self.apname, self.dimx))
# else:
# return ('%s: %s (%.4f)' % (self.code, self.apname, self.dimx))
def hash(self):
if self.dimy:
return ('%s (%.5f x %.5f)' % (self.apname, self.dimx, self.dimy))
else:
if self.apname in ('Macro',):
return ('%s (%s)' % (self.apname, self.dimx))
else:
return ('%s (%.5f)' % (self.apname, self.dimx))
def writeDef(self, fid):
if self.dimy:
fid.write(self.format % (self.code, self.dimx, self.dimy))
else:
fid.write(self.format % (self.code, self.dimx))
# Parse the aperture definition in line 's'. macroNames is an aperture macro dictionary
# that translates macro names local to this file to global names in the GAMT. We make
# the translation right away so that the return value from this function is an aperture
# definition with a global macro name, e.g., 'ADD10M5'
def parseAperture(s, knownMacroNames):
for ap in Apertures:
match = ap[1].match(s)
if match:
dimy = None
if ap[0] in ('Circle', 'Octagon', 'Macro'):
code, dimx = match.groups()
else:
code, dimx, dimy = match.groups()
if ap[0] in ('Macro',):
if knownMacroNames.has_key(dimx):
dimx = knownMacroNames[dimx] # dimx is now GLOBAL, permanent macro name (e.g., 'M2')
else:
raise RuntimeError, 'Aperture Macro name "%s" not defined' % dimx
else:
try:
dimx = float(dimx)
if dimy:
dimy = float(dimy)
except:
raise RuntimeError, "Illegal floating point aperture size"
return Aperture(ap, code, dimx, dimy)
return None
# This function returns a dictionary where each key is an
# aperture code string (e.g., "D11") and the value is the
# Aperture object that represents it. For example:
#
# %ADD12R,0.0630X0.0630*%
#
# from a Gerber file would result in the dictionary entry:
#
# "D12": Aperture(ap, 'D10', 0.063, 0.063)
#
# The input fileList is a list of pathnames which will be read to construct the
# aperture table for a job. All the files in the given list will be so
# examined, and a global aperture table will be constructed as a dictionary.
# Same goes for the global aperture macro table.
tool_pat = re.compile(r'^(?:G54)?D\d+\*$')
def constructApertureTable(fileList):
# First we construct a dictionary where each key is the
# string representation of the aperture. Then we go back and assign
# numbers. For aperture macros, we construct their final version
# (i.e., 'M1', 'M2', etc.) right away, as they are parsed. Thus,
# we translate from 'THX10N' or whatever to 'M2' right away.
GAT = config.GAT # Global Aperture Table
GAT.clear()
GAMT = config.GAMT # Global Aperture Macro Table
GAMT.clear()
RevGAMT = {} # Dictionary keyed by aperture macro hash and returning macro name
AT = {} # Aperture Table for this file
for fname in fileList:
#print 'Reading apertures from %s ...' % fname
knownMacroNames = {}
fid = file(fname,'rt')
for line in fid:
# Get rid of CR
line = line.replace('\x0D', '')
if tool_pat.match(line):
break # When tools start, no more apertures are being defined
# If this is an aperture macro definition, add its string
# representation to the dictionary. It might already exist.
# Ignore %AMOC8* from Eagle for now as it uses a macro parameter.
if line[:7]=='%AMOC8*':
continue
# parseApertureMacro() sucks up all macro lines up to terminating '%'
AM = amacro.parseApertureMacro(line, fid)
if AM:
# Has this macro definition already been defined (perhaps by another name
# in another layer)?
try:
# If this macro has already been encountered anywhere in any job,
# RevGAMT will map the macro hash to the global macro name. Then,
# make the local association knownMacroNames[localMacroName] = globalMacroName.
knownMacroNames[AM.name] = RevGAMT[AM.hash()]
except KeyError:
# No, so define the global macro and do the translation. Note that
# addToApertureMacroTable() MODIFIES AM.name to the new M-name.
localMacroName = AM.name
AM = amacro.addToApertureMacroTable(AM)
knownMacroNames[localMacroName] = AM.name
RevGAMT[AM.hash()] = AM.name
else:
A = parseAperture(line, knownMacroNames)
# If this is an aperture definition, add the string representation
# to the dictionary. It might already exist.
if A:
AT[A.hash()] = A
fid.close()
# Now, go through and assign sequential codes to all apertures
code = 10
for val in AT.values():
key = 'D%d' % code
GAT[key] = val
val.code = key
code += 1
if 0:
keylist = config.GAT.keys()
keylist.sort()
print 'Apertures'
print '========='
for key in keylist:
print '%s' % config.GAT[key]
sys.exit(0)
def findHighestApertureCode(keys):
"Find the highest integer value in a list of aperture codes: ['D10', 'D23', 'D35', ...]"
# Must sort keys by integer value, not string since 99 comes before 100
# as an integer but not a string.
keys = [int(K[1:]) for K in keys]
keys.sort()
return keys[-1]
def addToApertureTable(AP):
GAT = config.GAT
lastCode = findHighestApertureCode(GAT.keys())
code = 'D%d' % (lastCode+1)
GAT[code] = AP
AP.code = code
return code
def findInApertureTable(AP):
"""Return 'D10', for example in response to query for an object
of type Aperture()"""
hash = AP.hash()
for key, val in config.GAT.items():
if hash==val.hash():
return key
return None
def findOrAddAperture(AP):
"""If the aperture exists in the GAT, modify the AP.code field to reflect the global code
and return the code. Otherwise, create a new aperture in the GAT and return the new code
for it."""
code = findInApertureTable(AP)
if code:
AP.code = code
return code
else:
return addToApertureTable(AP)
if __name__=="__main__":
constructApertureTable(sys.argv[1:])
keylist = config.GAMT.keys()
keylist.sort()
print 'Aperture Macros'
print '==============='
for key in keylist:
print '%s' % config.GAMT[key]
keylist = config.GAT.keys()
keylist.sort()
print 'Apertures'
print '========='
for key in keylist:
print '%s' % config.GAT[key]

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,399 @@
#!/usr/bin/env python
"""
Parse the GerbMerge configuration file.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import ConfigParser
import re
import string
import jobs
import aptable
# Configuration dictionary. Specify floats as strings. Ints can be specified
# as ints or strings.
Config = {
'xspacing': '0.125', # Spacing in horizontal direction
'yspacing': '0.125', # Spacing in vertical direction
'panelwidth': '12.6', # X-Dimension maximum panel size (Olimex)
'panelheight': '7.8', # Y-Dimension maximum panel size (Olimex)
'cropmarklayers': None, # e.g., *toplayer,*bottomlayer
'cropmarkwidth': '0.01', # Width (inches) of crop lines
'cutlinelayers': None, # as for cropmarklayers
'cutlinewidth': '0.01', # Width (inches) of cut lines
'minimumfeaturesize': 0, # Minimum dimension for selected layers
'toollist': None, # Name of file containing default tool list
'drillclustertolerance': '.002', # Tolerance for clustering drill sizes
'allowmissinglayers': 0, # Set to 1 to allow multiple jobs to have non-matching layers
'fabricationdrawingfile': None, # Name of file to which to write fabrication drawing, or None
'fabricationdrawingtext': None, # Name of file containing text to write to fab drawing
'excellondecimals': 4, # Number of digits after the decimal point in input Excellon files
'excellonleadingzeros': 0, # Generate leading zeros in merged Excellon output file
'outlinelayerfile': None, # Name of file to which to write simple box outline, or None
'scoringfile': None, # Name of file to which to write scoring data, or None
'leftmargin': 0, # Inches of extra room to leave on left side of panel for tooling
'topmargin': 0, # Inches of extra room to leave on top side of panel for tooling
'rightmargin': 0, # Inches of extra room to leave on right side of panel for tooling
'bottommargin': 0, # Inches of extra room to leave on bottom side of panel for tooling
'fiducialpoints': None, # List of X,Y co-ordinates at which to draw fiducials
'fiducialcopperdiameter': 0.08, # Diameter of copper part of fiducial
'fiducialmaskdiameter': 0.32, # Diameter of fiducial soldermask opening
}
# This dictionary is indexed by lowercase layer name and has as values a file
# name to use for the output.
MergeOutputFiles = {
'boardoutline': 'merged.boardoutline.ger',
'drills': 'merged.drills.xln',
'placement': 'merged.placement.txt',
'toollist': 'merged.toollist.drl'
}
# The global aperture table, indexed by aperture code (e.g., 'D10')
GAT = {}
# The global aperture macro table, indexed by macro name (e.g., 'M3', 'M4R' for rotated macros)
GAMT = {}
# The list of all jobs loaded, indexed by job name (e.g., 'PowerBoard')
Jobs = {}
# The set of all Gerber layer names encountered in all jobs. Doesn't
# include drills.
LayerList = {'boardoutline': 1}
# The tool list as read in from the DefaultToolList file in the configuration
# file. This is a dictionary indexed by tool name (e.g., 'T03') and
# a floating point number as the value, the drill diameter in inches.
DefaultToolList = {}
# The GlobalToolMap dictionary maps tool name to diameter in inches. It
# is initially empty and is constructed after all files are read in. It
# only contains actual tools used in jobs.
GlobalToolMap = {}
# The GlobalToolRMap dictionary is a reverse dictionary of ToolMap, i.e., it maps
# diameter to tool name.
GlobalToolRMap = {}
##############################################################################
# This configuration option determines whether trimGerber() is called
TrimGerber = 1
# This configuration option determines whether trimExcellon() is called
TrimExcellon = 1
# This configuration option determines the minimum size of feature dimensions for
# each layer. It is a dictionary indexed by layer name (e.g. '*topsilkscreen') and
# has a floating point number as the value (in inches).
MinimumFeatureDimension = {}
# This configuration option is a positive integer that determines the maximum
# amout of time to allow for random placements (seconds). A SearchTimeout of 0
# indicates that no timeout should occur and random placements will occur
# forever until a KeyboardInterrupt is raised.
SearchTimeout = 0
# Construct the reverse-GAT/GAMT translation table, keyed by aperture/aperture macro
# hash string. The value is the aperture code (e.g., 'D10') or macro name (e.g., 'M5').
def buildRevDict(D):
RevD = {}
for key,val in D.items():
RevD[val.hash()] = key
return RevD
def parseStringList(L):
"""Parse something like '*toplayer, *bottomlayer' into a list of names
without quotes, spaces, etc."""
if 0:
if L[0]=="'":
if L[-1] != "'":
raise RuntimeError, "Illegal configuration string '%s'" % L
L = L[1:-1]
elif L[0]=='"':
if L[-1] != '"':
raise RuntimeError, "Illegal configuration string '%s'" % L
L = L[1:-1]
# This pattern matches quotes at the beginning and end...quotes must match
quotepat = re.compile(r'^([' "'" '"' r']?)([^\1]*)\1$')
delimitpat = re.compile(r'[ \t]*[,;][ \t]*')
match = quotepat.match(L)
if match:
L = match.group(2)
return delimitpat.split(L)
# Parse an Excellon tool list file of the form
#
# T01 0.035in
# T02 0.042in
def parseToolList(fname):
TL = {}
try:
fid = file(fname, 'rt')
except Exception, detail:
raise RuntimeError, "Unable to open tool list file '%s':\n %s" % (fname, str(detail))
pat_in = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*in\s*')
pat_mm = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*mm\s*')
pat_mil = re.compile(r'\s*(T\d+)\s+([0-9.]+)\s*(?:mil)?')
for line in fid.xreadlines():
line = string.strip(line)
if (not line) or (line[0] in ('#', ';')): continue
mm = 0
mil = 0
match = pat_in.match(line)
if not match:
mm = 1
match = pat_mm.match(line)
if not match:
mil = 1
match = pat_mil.match(line)
if not match:
continue
#raise RuntimeError, "Illegal tool list specification:\n %s" % line
tool, size = match.groups()
try:
size = float(size)
except:
raise RuntimeError, "Tool size in file '%s' is not a valid floating-point number:\n %s" % (fname,line)
if mil:
size = size*0.001 # Convert mil to inches
elif mm:
size = size/25.4 # Convert mm to inches
# Canonicalize tool so that T1 becomes T01
tool = 'T%02d' % int(tool[1:])
if TL.has_key(tool):
raise RuntimeError, "Tool '%s' defined more than once in tool list file '%s'" % (tool,fname)
TL[tool]=size
fid.close()
return TL
# This function parses the job configuration file and does
# everything needed to:
#
# * parse global options and store them in the Config dictionary
# as natural types (i.e., ints, floats, lists)
#
# * Read Gerber/Excellon data and populate the Jobs dictionary
#
# * Read Gerber/Excellon data and populate the global aperture
# table, GAT, and the global aperture macro table, GAMT
#
# * read the tool list file and populate the DefaultToolList dictionary
def parseConfigFile(fname, Config=Config, Jobs=Jobs):
global DefaultToolList
CP = ConfigParser.ConfigParser()
CP.readfp(file(fname,'rt'))
# First parse global options
if CP.has_section('Options'):
for opt in CP.options('Options'):
# Is it one we expect
if Config.has_key(opt):
# Yup...override it
Config[opt] = CP.get('Options', opt)
elif CP.defaults().has_key(opt):
pass # Ignore DEFAULTS section keys
elif opt in ('fabricationdrawing', 'outlinelayer'):
print '*'*73
print '\nThe FabricationDrawing and OutlineLayer configuration options have been'
print 'renamed as of GerbMerge version 1.0. Please consult the documentation for'
print 'a description of the new options, then modify your configuration file.\n'
print '*'*73
sys.exit(1)
else:
raise RuntimeError, "Unknown option '%s' in [Options] section of configuration file" % opt
else:
raise RuntimeError, "Missing [Options] section in configuration file"
# Ensure we got a tool list
if not Config.has_key('toollist'):
raise RuntimeError, "INTERNAL ERROR: Missing tool list assignment in [Options] section"
# Make integers integers, floats floats
for key,val in Config.items():
try:
val = int(val)
Config[key]=val
except:
try:
val = float(val)
Config[key]=val
except:
pass
# Process lists of strings
if Config['cutlinelayers']:
Config['cutlinelayers'] = parseStringList(Config['cutlinelayers'])
if Config['cropmarklayers']:
Config['cropmarklayers'] = parseStringList(Config['cropmarklayers'])
# Process list of minimum feature dimensions
if Config['minimumfeaturesize']:
temp = Config['minimumfeaturesize'].split(",")
try:
for index in range(0, len(temp), 2):
MinimumFeatureDimension[ temp[index] ] = float( temp[index + 1] )
except:
raise RuntimeError, "Illegal configuration string:" + Config['minimumfeaturesize']
# Process MergeOutputFiles section to set output file names
if CP.has_section('MergeOutputFiles'):
for opt in CP.options('MergeOutputFiles'):
# Each option is a layer name and the output file for this name
if opt[0]=='*' or opt in ('boardoutline', 'drills', 'placement', 'toollist'):
MergeOutputFiles[opt] = CP.get('MergeOutputFiles', opt)
# Now, we go through all jobs and collect Gerber layers
# so we can construct the Global Aperture Table.
apfiles = []
for jobname in CP.sections():
if jobname=='Options': continue
if jobname=='MergeOutputFiles': continue
if jobname=='GerbMergeGUI': continue
# Ensure all jobs have a board outline
if not CP.has_option(jobname, 'boardoutline'):
raise RuntimeError, "Job '%s' does not have a board outline specified" % jobname
if not CP.has_option(jobname, 'drills'):
raise RuntimeError, "Job '%s' does not have a drills layer specified" % jobname
for layername in CP.options(jobname):
if layername[0]=='*' or layername=='boardoutline':
fname = CP.get(jobname, layername)
apfiles.append(fname)
if layername[0]=='*':
LayerList[layername]=1
# Now construct global aperture tables, GAT and GAMT. This step actually
# reads in the jobs for aperture data but doesn't store Gerber
# data yet.
aptable.constructApertureTable(apfiles)
del apfiles
if 0:
keylist = GAMT.keys()
keylist.sort()
for key in keylist:
print '%s' % GAMT[key]
sys.exit(0)
# Parse the tool list
if Config['toollist']:
DefaultToolList = parseToolList(Config['toollist'])
# Now get jobs. Each job implies layer names, and we
# expect consistency in layer names from one job to the
# next. Two reserved layer names, however, are
# BoardOutline and Drills.
Jobs.clear()
do_abort = 0
errstr = 'ERROR'
if Config['allowmissinglayers']:
errstr = 'WARNING'
for jobname in CP.sections():
if jobname=='Options': continue
if jobname=='MergeOutputFiles': continue
if jobname=='GerbMergeGUI': continue
print 'Reading data from', jobname, '...'
J = jobs.Job(jobname)
# Parse the job settings, like tool list, first, since we are not
# guaranteed to have ConfigParser return the layers in the same order that
# the user wrote them, and we may get Gerber files before we get a tool
# list! Same thing goes for ExcellonDecimals. We need to know what this is
# before parsing any Excellon files.
for layername in CP.options(jobname):
fname = CP.get(jobname, layername)
if layername == 'toollist':
J.ToolList = parseToolList(fname)
elif layername=='excellondecimals':
try:
J.ExcellonDecimals = int(fname)
except:
raise RuntimeError, "Excellon decimals '%s' in config file is not a valid integer" % fname
elif layername=='repeat':
try:
J.Repeat = int(fname)
except:
raise RuntimeError, "Repeat count '%s' in config file is not a valid integer" % fname
for layername in CP.options(jobname):
fname = CP.get(jobname, layername)
if layername=='boardoutline':
J.parseGerber(fname, layername, updateExtents=1)
elif layername[0]=='*':
J.parseGerber(fname, layername, updateExtents=0)
elif layername=='drills':
J.parseExcellon(fname)
# Emit warnings if some layers are missing
LL = LayerList.copy()
for layername in J.apxlat.keys():
assert LL.has_key(layername)
del LL[layername]
if LL:
if errstr=='ERROR':
do_abort=1
print '%s: Job %s is missing the following layers:' % (errstr, jobname)
for layername in LL.keys():
print ' %s' % layername
# Store the job in the global Jobs dictionary, keyed by job name
Jobs[jobname] = J
if do_abort:
raise RuntimeError, 'Exiting since jobs are missing layers. Set AllowMissingLayers=1\nto override.'
if __name__=="__main__":
CP = parseConfigFile(sys.argv[1])
print Config
sys.exit(0)
if 0:
for key, val in CP.defaults().items():
print '%s: %s' % (key,val)
for section in CP.sections():
print '[%s]' % section
for opt in CP.options(section):
print ' %s=%s' % (opt, CP.get(section, opt))

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,208 @@
#!/usr/bin/env python
"""
Drill clustering routines to reduce total number of drills and remap
drilling commands to the new reduced drill set.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
_STATUS = True ## indicates status messages should be shown
_DEBUG = False ## indicates debug and status messages should be shown
def cluster(drills, tolerance, debug = _DEBUG):
"""
Take a dictionary of drill names and sizes and cluster them
A tolerance of 0 will effectively disable clustering
Returns clustered drill dictionary
"""
global _DEBUG
_DEBUG = debug
clusters = []
debug_print("\n " + str( len(drills) ) + " Original drills:")
debug_print( drillsToString(drills) )
debug_print("Clustering drill sizes ...", True)
# Loop through all drill sizes
sizes = drills.keys()
sizes.sort()
for size in sizes:
match = False
# See if size fits into any current clusters, else make new cluster
for index in range( len(clusters) ):
c = clusters[index]
if not len(c):
break
mn = min(c)
mx = max(c)
##debug_print( "Validating " + str_d(size) + " in " + str_d(c) )
##debug_print( "Possible cluster range = " + str_d(mx - 2 * tolerance) + " to " + str_d(mn + 2 * tolerance) )
if (size >= mx - 2 * tolerance) and (size <= mn + 2 * tolerance):
debug_print( str_d(size) + " belongs with " + str_d(c) )
clusters[index].append(size)
match = True
break
if not match:
debug_print(str_d(size) + " belongs in a new cluster")
clusters.append( [size] )
debug_print("\n Creating new drill dictionary ...")
new_drills = {}
tool_num = 0
# Create new dictionary of clustered drills
for c in clusters:
tool_num += 1
new_drill = "T%02d" % tool_num
c.sort()
new_size = ( min(c) + max(c) ) / 2.0
new_drills[new_size] = new_drill
debug_print(str_d(c) + " will be represented by " + new_drill + " (" + str_d(new_size) + ")")
debug_print("\n " + str( len(new_drills) ) + " Clustered Drills:")
debug_print( drillsToString(new_drills) )
debug_print("Drill count reduced from " + str( len(drills) ) + " to " + str( len(new_drills) ), True)
return new_drills
def remap(jobs, globalToolMap, debug = _DEBUG):
"""
Remap tools and commands in all jobs to match new tool map
Returns None
"""
# Set global variables from parameters
global _DEBUG
_DEBUG = debug
debug_print("Remapping tools and commands ...", True)
for job in jobs:
job = job.job ##access job inside job layout
debug_print("\n Job name: " + job.name)
debug_print("\n Original job tools:")
debug_print( str(job.xdiam) )
debug_print("\n Original commands:")
debug_print( str(job.xcommands) )
new_tools = {}
new_commands = {}
for tool, diam in job.xdiam.items():
##debug_print("\n Current tool: " + tool + " (" + str_d(diam) + ")")
# Search for best matching tool
best_diam, best_tool = globalToolMap[0]
for glob_diam, glob_tool in globalToolMap:
if abs(glob_diam - diam) < abs(best_diam - diam):
best_tool = glob_tool
best_diam = glob_diam
##debug_print("Best match: " + best_tool + " (" + str_d(best_diam) + ")")
new_tools[best_tool] = best_diam
##debug_print(best_tool + " will replace " + tool)
# Append commands to existing commands if they exist
if best_tool in new_commands:
##debug_print( "Current commands: " + str( new_commands[best_tool] ) )
temp = new_commands[best_tool]
temp.extend( job.xcommands[tool] )
new_commands[best_tool] = temp
##debug_print( "All commands: " + str( new_commands[best_tool] ) )
else:
new_commands[best_tool] = job.xcommands[tool]
debug_print("\n New job tools:")
debug_print( str(new_tools) )
debug_print("\n New commands:")
debug_print( str(new_commands) )
job.xdiam = new_tools
job.xcommands = new_commands
def debug_print(text, status = False, newLine = True):
"""
Print debugging statemetns
Returs None, Printts text
"""
if _DEBUG or (status and _STATUS):
if newLine:
print " ", text
else:
print " ", text,
def str_d(drills):
"""
Format drill sizes for printing debug and status messages
Returns drills as formatted string
"""
string = ""
try:
len(drills)
except:
string = "%.4f" % drills
else:
string = "["
for drill in drills:
string += ( "%.4f" % drill + ", ")
string = string[:len(string) - 2] + "]"
return string
def drillsToString(drills):
"""
Format drill dictionary for printing debug and status messages
Returns drills as formatted string
"""
string = ""
drills = drills.items()
drills.sort()
for size, drill in drills:
string += drill + " = " + str_d(size) + "\n "
return string
"""
The following code runs test drill clusterings with random drill sets.
"""
if __name__=="__main__":
import random
print " Clustering random drills..."
old = {}
tool_num = 0
while len(old) < 99:
rand_size = round(random.uniform(.02, .04), 4)
if rand_size in old:
continue
tool_num += 1
old[rand_size] = "T%02d" % tool_num
new = cluster(old, .0003, True)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,210 @@
#!/usr/bin/env python
"""This file handles the writing of the fabrication drawing Gerber file
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import string
import config
import makestroke
import util
def writeDrillHits(fid, Place, Tools):
toolNumber = -1
for tool in Tools:
toolNumber += 1
try:
size = config.GlobalToolMap[tool]
except:
raise RuntimeError, "INTERNAL ERROR: Tool code %s not found in global tool list" % tool
#for row in Layout:
# row.writeDrillHits(fid, size, toolNumber)
for job in Place.jobs:
job.writeDrillHits(fid, size, toolNumber)
def writeBoundingBox(fid, OriginX, OriginY, MaxXExtent, MaxYExtent):
x = util.in2gerb(OriginX)
y = util.in2gerb(OriginY)
X = util.in2gerb(MaxXExtent)
Y = util.in2gerb(MaxYExtent)
makestroke.drawPolyline(fid, [(x,y), (X,y), (X,Y), (x,Y), (x,y)], 0, 0)
def writeDrillLegend(fid, Tools, OriginY, MaxXExtent):
# This is the spacing from the right edge of the board to where the
# drill legend is to be drawn, in inches. Remember we have to allow
# for dimension arrows, too.
dimspace = 0.5 # inches
# This is the spacing from the drill hit glyph to the drill size
# in inches.
glyphspace = 0.1 # inches
# Convert to Gerber 2.5 units
dimspace = util.in2gerb(dimspace)
glyphspace = util.in2gerb(glyphspace)
# Construct a list of tuples (toolSize, toolNumber) where toolNumber
# is the position of the tool in Tools and toolSize is in inches.
L = []
toolNumber = -1
for tool in Tools:
toolNumber += 1
L.append((config.GlobalToolMap[tool], toolNumber))
# Now sort the list from smallest to largest
L.sort()
# And reverse to go from largest to smallest, so we can write the legend
# from the bottom up
L.reverse()
# For each tool, draw a drill hit marker then the size of the tool
# in inches.
posY = util.in2gerb(OriginY)
posX = util.in2gerb(MaxXExtent) + dimspace
maxX = 0
for size,toolNum in L:
# Determine string to write and midpoint of string
s = '%.3f"' % size
ll, ur = makestroke.boundingBox(s, posX+glyphspace, posY) # Returns lower-left point, upper-right point
midpoint = (ur[1]+ll[1])/2
# Keep track of maximum extent of legend
maxX = max(maxX, ur[0])
makestroke.drawDrillHit(fid, posX, midpoint, toolNum)
makestroke.writeString(fid, s, posX+glyphspace, posY, 0)
posY += int(round((ur[1]-ll[1])*1.5))
# Return value is lower-left of user text area, without any padding.
return maxX, util.in2gerb(OriginY)
def writeDimensionArrow(fid, OriginX, OriginY, MaxXExtent, MaxYExtent):
x = util.in2gerb(OriginX)
y = util.in2gerb(OriginY)
X = util.in2gerb(MaxXExtent)
Y = util.in2gerb(MaxYExtent)
# This constant is how far away from the board the centerline of the dimension
# arrows should be, in inches.
dimspace = 0.2
# Convert it to Gerber (0.00001" or 2.5) units
dimspace = util.in2gerb(dimspace)
# Draw an arrow above the board, on the left side and right side
makestroke.drawDimensionArrow(fid, x, Y+dimspace, makestroke.FacingLeft)
makestroke.drawDimensionArrow(fid, X, Y+dimspace, makestroke.FacingRight)
# Draw arrows to the right of the board, at top and bottom
makestroke.drawDimensionArrow(fid, X+dimspace, Y, makestroke.FacingUp)
makestroke.drawDimensionArrow(fid, X+dimspace, y, makestroke.FacingDown)
# Now draw the text. First, horizontal text above the board.
s = '%.3f"' % (MaxXExtent - OriginX)
ll, ur = makestroke.boundingBox(s, 0, 0)
s_width = ur[0]-ll[0] # Width in 2.5 units
s_height = ur[1]-ll[1] # Height in 2.5 units
# Compute the position in 2.5 units where we should draw this. It should be
# centered horizontally and also vertically about the dimension arrow centerline.
posX = x + (x+X)/2
posX -= s_width/2
posY = Y + dimspace - s_height/2
makestroke.writeString(fid, s, posX, posY, 0)
# Finally, draw the extending lines from the text to the arrows.
posY = Y + dimspace
posX1 = posX - util.in2gerb(0.1) # 1000
posX2 = posX + s_width + util.in2gerb(0.1) # 1000
makestroke.drawLine(fid, x, posY, posX1, posY)
makestroke.drawLine(fid, posX2, posY, X, posY)
# Now do the vertical text
s = '%.3f"' % (MaxYExtent - OriginY)
ll, ur = makestroke.boundingBox(s, 0, 0)
s_width = ur[0]-ll[0]
s_height = ur[1]-ll[1]
# As above, figure out where to draw this. Rotation will be -90 degrees
# so new origin will be top-left of bounding box after rotation.
posX = X + dimspace - s_height/2
posY = y + (y+Y)/2
posY += s_width/2
makestroke.writeString(fid, s, posX, posY, -90)
# Draw extending lines
posX = X + dimspace
posY1 = posY + util.in2gerb(0.1) # 1000
posY2 = posY - s_width - util.in2gerb(0.1) # 1000
makestroke.drawLine(fid, posX, Y, posX, posY1)
makestroke.drawLine(fid, posX, posY2, posX, y)
def writeUserText(fid, X, Y):
fname = config.Config['fabricationdrawingtext']
if not fname: return
try:
tfile = file(fname, 'rt')
except Exception, detail:
raise RuntimeError, "Could not open fabrication drawing text file '%s':\n %s" % (fname,str(detail))
lines = tfile.readlines()
tfile.close()
lines.reverse() # We're going to print from bottom up
# Offset X position to give some clearance from drill legend
X += util.in2gerb(0.2) # 2000
for line in lines:
# Get rid of CR
line = string.replace(line, '\x0D', '')
# Chop off \n
#if line[-1] in string.whitespace:
# line = line[:-1]
# Strip off trailing whitespace
line = string.rstrip(line)
# Blank lines still need height, so must have at least one character
if not line:
line = ' '
ll, ur = makestroke.boundingBox(line, X, Y)
makestroke.writeString(fid, line, X, Y, 0)
Y += int(round((ur[1]-ll[1])*1.5))
# Main entry point. Gerber file has already been opened, header written
# out, 1mil tool selected.
def writeFabDrawing(fid, Place, Tools, OriginX, OriginY, MaxXExtent, MaxYExtent):
# Write out all the drill hits
writeDrillHits(fid, Place, Tools)
# Draw a bounding box for the project
writeBoundingBox(fid, OriginX, OriginY, MaxXExtent, MaxYExtent)
# Write out the drill hit legend off to the side. This function returns
# (X,Y) lower-left origin where user text is to begin, in Gerber units
# and without any padding.
X,Y = writeDrillLegend(fid, Tools, OriginY, MaxXExtent)
# Write out the dimensioning arrows
writeDimensionArrow(fid, OriginX, OriginY, MaxXExtent, MaxYExtent)
# Finally, write out user text
writeUserText(fid, X, Y)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,346 @@
#!/usr/bin/env python
"""
General geometry support routines.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import math
# Ensure all list elements are unique
def uniqueify(L):
return {}.fromkeys(L).keys()
# This function rounds an (X,Y) point to integer co-ordinates
def roundPoint(pt):
return (int(round(pt[0])),int(round(pt[1])))
# Returns True if the segment defined by endpoints p1 and p2 is vertical
def isSegmentVertical(p1, p2):
return p1[0]==p2[0]
# Returns True if the segment defined by endpoints p1 and p2 is horizontal
def isSegmentHorizontal(p1, p2):
return p1[1]==p2[1]
# Returns slope of a non-vertical line segment
def segmentSlope(p1, p2):
return float(p2[1]-p1[1])/(p2[0]-p1[0])
# Determine if the (X,Y) 'point' is on the line segment defined by endpoints p1
# and p2, both (X,Y) tuples. It's assumed that the point is on the line defined
# by the segment, but just may be beyond the endpoints. NOTE: No testing is
# performed to see if the point is actually on the line defined by the segment!
# This is assumed!
def isPointOnSegment(point, p1, p2):
if isSegmentVertical(p1,p2):
# Treat vertical lines by comparing Y-ordinates
return (point[1]-p2[1])*(point[1]-p1[1]) <= 0
else:
# Treat other lines, including horizontal lines, by comparing X-ordinates
return (point[0]-p2[0])*(point[0]-p1[0]) <= 0
# Returns (X,Y) point where the line segment defined by (X,Y) endpoints p1 and
# p2 intersects the line segment defined by endpoints q1 and q2. Only a single
# intersection point is allowed, so no coincident lines. If there is no point
# of intersection, None is returned.
def segmentXsegment1pt(p1, p2, q1, q2):
A,B = p1
C,D = p2
P,Q = q1
R,S = q2
# We have to consider special cases of one or other line segments being vertical
if isSegmentVertical(p1,p2):
if isSegmentVertical(q1,q2): return None
x = A
y = segmentSlope(q1,q2)*(A-P) + Q
elif isSegmentVertical(q1,q2):
x = P
y = segmentSlope(p1,p2)*(P-A) + B
else:
m1 = segmentSlope(p1,p2)
m2 = segmentSlope(q1,q2)
if m1==m2: return None
x = (A*m1 - B - P*m2 + Q) / (m1-m2)
y = m1*(x-A) + B
# Candidate point identified. Check to make sure it's on both line segments.
if isPointOnSegment((x,y), p1, p2) and isPointOnSegment((x,y), q1, q2):
return roundPoint((x,y))
else:
return None
# Returns True if the given (X,Y) 'point' is strictly within the rectangle
# defined by (LLX,LLY,URX,URY) co-ordinates (LL=lower left, UR=upper right).
def isPointStrictlyInRectangle(point, rect):
x,y = point
llx,lly,urx,ury = rect
return (llx < x < urx) and (lly < y < ury)
# This function takes two points which define the extents of a rectangle. The
# return value is a 5-tuple (ll, ul, ur, lr, rect) which comprises 4 points
# (lower-left, upper-left, upper-right, lower-right) and a rect object (minx,
# miny, maxx, maxy). If called with a single argument, it is expected to be
# a 4-tuple (x1,y1,x2,y2).
def canonicalizeExtents(pt1, pt2=None):
# First canonicalize lower-left and upper-right points
if pt2 is None:
maxx = max(pt1[0], pt1[2])
minx = min(pt1[0], pt1[2])
maxy = max(pt1[1], pt1[3])
miny = min(pt1[1], pt1[3])
else:
maxx = max(pt1[0], pt2[0])
minx = min(pt1[0], pt2[0])
maxy = max(pt1[1], pt2[1])
miny = min(pt1[1], pt2[1])
# Construct the four corners
llpt = (minx,miny)
urpt = (maxx,maxy)
ulpt = (minx,maxy)
lrpt = (maxx,miny)
# Construct a rect object for use by various functions
rect = (minx, miny, maxx, maxy)
return (llpt, ulpt, urpt, lrpt, rect)
# This function returns a list of intersection points of the line segment
# pt1-->pt2 and the box defined by corners llpt and urpt. These corners are
# canonicalized internally so they need not necessarily be lower-left and
# upper-right points.
#
# The return value may be a list of 0, 1, or 2 points. If the list has 2
# points, then the segment intersects the box in two points since both points
# are outside the box. If the list has 1 point, then the segment has one point
# inside the box and another point outside. If the list is empty, the segment
# has both points outside the box and there is no intersection, or has both
# points inside the box.
#
# Note that segments collinear with box edges produce no points of
# intersection.
def segmentXbox(pt1, pt2, llpt, urpt):
# First canonicalize lower-left and upper-right points
llpt, ulpt, urpt, lrpt, rect = canonicalizeExtents(llpt, urpt)
# Determine whether one point is inside the rectangle and the other is not.
# Note the XOR operator '^'
oneInOneOut = isPointStrictlyInRectangle(pt1,rect) ^ isPointStrictlyInRectangle(pt2,rect)
# Find all intersections of the segment with the 4 sides of the box,
# one side at a time. L will be the list of definitely-true intersections,
# while corners is a list of potential intersections. An intersection
# is potential if a) it is a corner, and b) there is another intersection
# of the line with the box somewhere else. This is how we handle
# corner intersections, which are sometimes legal (when one segment endpoint
# is inside the box and the other isn't, or when the segment intersects the
# box in two places) and sometimes not (when the segment is "tangent" to
# the box at the corner and the corner is the signle point of intersection).
L = []
corners = []
# Do not allow intersection if segment is collinear with box sides. For
# example, a horizontal line collinear with the box top side should not
# return an intersection with the upper-left or upper-right corner.
# Similarly, a point of intersection that is a corner should only be
# allowed if one segment point is inside the box and the other is not,
# otherwise it means the segment is "tangent" to the box at that corner.
# There is a case, however, in which a corner is a point of intersection
# with both segment points outside the box, and that is if there are two
# points of intersection, i.e., the segment goes completely through the box.
def checkIntersection(corner1, corner2):
# Check intersection with side of box
pt = segmentXsegment1pt(pt1, pt2, corner1, corner2)
if pt in (corner1,corner2):
# Only allow this corner intersection point if line is not
# horizontal/vertical and one point is inside rectangle while other is
# not, or the segment intersects the box in two places. Since oneInOneOut
# calls isPointStrictlyInRectangle(), which automatically excludes points
# on the box itself, horizontal/vertical lines collinear with box sides
# will always lead to oneInOneOut==False (since both will be "out of
# box").
if oneInOneOut:
L.append(pt)
else:
corners.append(pt) # Potentially a point of intersection...we'll have to wait and
# see if there is one more point of intersection somewhere else.
else:
# Not a corner intersection, so it's valid
if pt is not None: L.append(pt)
# Check intersection with left side of box
checkIntersection(llpt, ulpt)
# Check intersection with top side of box
checkIntersection(ulpt, urpt)
# Check intersection with right side of box
checkIntersection(urpt, lrpt)
# Check intersection with bottom side of box
checkIntersection(llpt, lrpt)
# Ensure all points are unique. We may get a double hit at the corners
# of the box.
L = uniqueify(L)
corners = uniqueify(corners)
# If the total number of intersections len(L)+len(corners) is 2, the corner
# is valid. If there is only a single corner, it's a tangent and invalid.
# However, if both corners are on the same side of the box, it's not valid.
numPts = len(L)+len(corners)
assert numPts <= 2
if numPts == 2:
if len(corners)==2 and (isSegmentHorizontal(corners[0], corners[1]) or isSegmentVertical(corners[0],corners[1])):
return []
else:
L += corners
L.sort() # Just for stability in assertion checking
return L
else:
L.sort()
return L # Correct if numPts==1, since it will be empty or contain a single valid intersection
# Correct if numPts==0, since it will be empty
# This function determines if two rectangles defined by 4-tuples
# (minx, miny, maxx, maxy) have any rectangle in common. If so, it is
# returned as a 4-tuple, else None is returned. This function assumes
# the rectangles are canonical so that minx<maxx, miny<maxy. If the
# optional allowLines parameter is True, rectangles that overlap on
# a line are considered overlapping, otherwise they must overlap with
# a rectangle of at least width 1.
def areExtentsOverlapping(E1, E2, allowLines=False):
minX,minY,maxX,maxY = E1
minU,minV,maxU,maxV = E2
if allowLines:
if (minU > maxX) or (maxU < minX) or (minV > maxY) or (maxV < minY):
return False
else:
return True
else:
if (minU >= maxX) or (maxU <= minX) or (minV >= maxY) or (maxV <= minY):
return False
else:
return True
# Compute the intersection of two rectangles defined by 4-tuples E1 and E2,
# which are not necessarily canonicalized.
def intersectExtents(E1, E2):
ll1, ul1, ur1, lr1, rect1 = canonicalizeExtents(E1)
ll2, ul2, ur2, lr2, rect2 = canonicalizeExtents(E2)
if not areExtentsOverlapping(rect1, rect2):
return None
xll = max(rect1[0], rect2[0]) # Maximum of minx values
yll = max(rect1[1], rect2[1]) # Maximum of miny values
xur = min(rect1[2], rect2[2]) # Minimum of maxx values
yur = min(rect1[3], rect2[3]) # Minimum of maxy values
return (xll, yll, xur, yur)
# This function returns True if rectangle E1 is wholly contained within
# rectangle E2. Both E1 and E2 are 4-tuples (minx,miny,maxx,maxy), not
# necessarily canonicalized. This function is like a slightly faster
# version of "intersectExtents(E1, E2)==E1".
def isRect1InRect2(E1, E2):
ll1, ul1, ur1, lr1, rect1 = canonicalizeExtents(E1)
ll2, ul2, ur2, lr2, rect2 = canonicalizeExtents(E2)
return (ll1[0] >= ll2[0]) and (ll1[1] >= ll2[1]) \
and (ur1[0] <= ur2[0]) and (ur1[1] <= ur2[1])
# Return width of rectangle, which may be 0 if bottom-left and upper-right X
# positions are the same. The rectangle is a 4-tuple (minx,miny,maxx,maxy).
def rectWidth(rect):
return abs(rect[2]-rect[0])
# Return height of rectangle, which may be 0 if bottom-left and upper-right Y
# positions are the same. The rectangle is a 4-tuple (minx,miny,maxx,maxy).
def rectHeight(rect):
return abs(rect[3]-rect[1])
# Return center (X,Y) co-ordinates of rectangle.
def rectCenter(rect):
dx = rectWidth(rect)
dy = rectHeight(rect)
if dx & 1: # Odd width: center is (left+right)/2 + 1/2
X = (rect[0] + rect[2] + 1)/2
else: # Even width: center is (left+right)/2
X = (rect[0] + rect[2])/2
if dy & 1:
Y = (rect[1] + rect[3] + 1)/2
else:
Y = (rect[1] + rect[3])/2
return (X,Y)
if __name__=="__main__":
llpt = (1000,1000)
urpt = (5000,5000)
# A segment that cuts across the box and intersects in corners
assert segmentXbox((0,0), (6000,6000), llpt, urpt) == [(1000,1000), (5000,5000)] # Two valid corners
assert segmentXbox((0,6000), (6000,0), llpt, urpt) == [(1000,5000), (5000,1000)] # Two valid corners
assert segmentXbox((500,500), (2500, 2500), llpt, urpt) == [(1000,1000)] # One valid corner
assert segmentXbox((2500,2500), (5500, 5500), llpt, urpt) == [(5000,5000)] # One valid corner
# Segments collinear with box sides
assert segmentXbox((1000,0), (1000,6000), llpt, urpt) == [] # Box side contained in segment
assert segmentXbox((1000,0), (1000,3000), llpt, urpt) == [] # Box side partially overlaps segment
assert segmentXbox((1000,2000), (1000,4000), llpt, urpt) == [] # Segment contained in box side
# Segments fully contained within box
assert segmentXbox((1500,2000), (2000,2500), llpt, urpt) == []
# Segments with points on box sides
assert segmentXbox((2500,1000), (2700,1200), llpt, urpt) == [(2500,1000)] # One point on box side
assert segmentXbox((2500,1000), (2700,5000), llpt, urpt) == [(2500,1000), (2700,5000)] # Two points on box sides
# Segment intersects box at one point
assert segmentXbox((3500,5500), (3000, 2500), llpt, urpt) == [(3417, 5000)] # First point outside
assert segmentXbox((3500,1500), (3000, 6500), llpt, urpt) == [(3150, 5000)] # Second point outside
# Segment intersects box at two points, not corners
assert segmentXbox((500,3000), (1500,500), llpt, urpt) == [(1000,1750), (1300,1000)]
assert segmentXbox((2500,300), (5500,3500), llpt, urpt) == [(3156,1000), (5000,2967)]
assert segmentXbox((5200,1200), (2000,6000), llpt, urpt) == [(2667,5000), (5000, 1500)]
assert segmentXbox((3200,5200), (-10, 1200), llpt, urpt) == [(1000, 2459), (3040, 5000)]
assert segmentXbox((500,2000), (5500, 2000), llpt, urpt) == [(1000,2000), (5000, 2000)]
assert segmentXbox((5200,1250), (-200, 4800), llpt, urpt) == [(1000, 4011), (5000, 1381)]
assert segmentXbox((1300,200), (1300, 5200), llpt, urpt) == [(1300, 1000), (1300, 5000)]
assert segmentXbox((1200,200), (1300, 5200), llpt, urpt) == [(1216, 1000), (1296, 5000)]
assert intersectExtents( (100,100,500,500), (500,500,900,900) ) == None
assert intersectExtents( (100,100,500,500), (400,400,900,900) ) == (400,400,500,500)
assert intersectExtents( (100,100,500,500), (200,0,600,300) ) == (200,100,500,300)
assert intersectExtents( (100,100,500,500), (200,0,300,600) ) == (200,100,300,500)
assert intersectExtents( (100,100,500,500), (0,600,50,550) ) == None
assert intersectExtents( (100,100,500,500), (0,600,600,-10) ) == (100,100,500,500)
assert intersectExtents( (100,100,500,500), (0,600,600,200) ) == (100,200,500,500)
assert intersectExtents( (100,100,500,500), (0,600,300,300) ) == (100,300,300,500)
assert isRect1InRect2( (100,100,500,500), (0,600,50,550) ) == False
assert isRect1InRect2( (100,100,500,500), (0,600,600,-10) ) == True
assert isRect1InRect2( (100,100,500,500), (0,600,600,200) ) == False
assert isRect1InRect2( (100,100,500,500), (0,600,300,300) ) == False
assert isRect1InRect2( (100,100,500,500), (0,0,500,500) ) == True
print 'All tests pass'

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,753 @@
#!/usr/bin/env python
"""
Merge several RS274X (Gerber) files generated by Eagle into a single
job.
This program expects that each separate job has at least three files:
- a board outline (RS274X)
- data layers (copper, silkscreen, etc. in RS274X format)
- an Excellon drill file
Furthermore, it is expected that each job was generated by Eagle
using the GERBER_RS274X plotter, except for the drill file which
was generated by the EXCELLON plotter.
This program places all jobs into a single job.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import os
import getopt
import re
import aptable
import jobs
import config
import parselayout
import fabdrawing
import strokes
import tiling
import tilesearch1
import tilesearch2
import placement
import schwartz
import util
import scoring
import drillcluster
VERSION_MAJOR=1
VERSION_MINOR=8
RANDOM_SEARCH = 1
EXHAUSTIVE_SEARCH = 2
FROM_FILE = 3
config.AutoSearchType = RANDOM_SEARCH
config.RandomSearchExhaustiveJobs = 2
config.PlacementFile = None
# This is a handle to a GUI front end, if any, else None for command-line usage
GUI = None
def usage():
print \
"""
Usage: gerbmerge [Options] configfile [layoutfile]
Options:
-h, --help -- This help summary
-v, --version -- Program version and contact information
--random-search -- Automatic placement using random search (default)
--full-search -- Automatic placement using exhaustive search
--place-file=fn -- Read placement from file
--rs-fsjobs=N -- When using random search, exhaustively search N jobs
for each random placement (default: N=2)
--search-timeout=T -- When using random search, search for T seconds for best
random placement (default: T=0, search until stopped)
--no-trim-gerber -- Do not attempt to trim Gerber data to extents of board
--no-trim-excellon -- Do not attempt to trim Excellon data to extents of board
--octagons=fmt -- Generate octagons in two different styles depending on
the value of 'fmt':
fmt is 'rotate' : 0.0 rotation
fmt is 'normal' : 22.5 rotation (default)
If a layout file is not specified, automatic placement is performed. If the
placement is read from a file, then no automatic placement is performed and
the layout file (if any) is ignored.
NOTE: The dimensions of each job are determined solely by the maximum extent of
the board outline layer for each job.
"""
sys.exit(1)
def writeGerberHeader22degrees(fid):
fid.write( \
"""G75*
G70*
%OFA0B0*%
%FSLAX25Y25*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
%
""")
def writeGerberHeader0degrees(fid):
fid.write( \
"""G75*
G70*
%OFA0B0*%
%FSLAX25Y25*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,0.0*
%
""")
writeGerberHeader = writeGerberHeader22degrees
def writeApertureMacros(fid, usedDict):
keys = config.GAMT.keys()
keys.sort()
for key in keys:
if key in usedDict:
config.GAMT[key].writeDef(fid)
def writeApertures(fid, usedDict):
keys = config.GAT.keys()
keys.sort()
for key in keys:
if key in usedDict:
config.GAT[key].writeDef(fid)
def writeGerberFooter(fid):
fid.write('M02*\n')
def writeExcellonHeader(fid):
fid.write('%\n')
def writeExcellonFooter(fid):
fid.write('M30\n')
def writeExcellonTool(fid, tool, size):
fid.write('%sC%f\n' % (tool, size))
def writeFiducials(fid, drawcode, OriginX, OriginY, MaxXExtent, MaxYExtent):
"""Place fiducials at arbitrary points. The FiducialPoints list in the config specifies
sets of X,Y co-ordinates. Positive values of X/Y represent offsets from the lower left
of the panel. Negative values of X/Y represent offsets from the top right. So:
FiducialPoints = 0.125,0.125,-0.125,-0.125
means to put a fiducial 0.125,0.125 from the lower left and 0.125,0.125 from the top right"""
fid.write('%s*\n' % drawcode) # Choose drawing aperture
fList = config.Config['fiducialpoints'].split(',')
for i in range(0, len(fList), 2):
x,y = float(fList[i]), float(fList[i+1])
if x>=0:
x += OriginX
else:
x = MaxXExtent + x
if y>=0:
y += OriginX
else:
y = MaxYExtent + y
fid.write('X%07dY%07dD03*\n' % (util.in2gerb(x), util.in2gerb(y)))
def writeCropMarks(fid, drawing_code, OriginX, OriginY, MaxXExtent, MaxYExtent):
"""Add corner crop marks on the given layer"""
# Draw 125mil lines at each corner, with line edge right up against
# panel border. This means the center of the line is D/2 offset
# from the panel border, where D is the drawing line diameter.
fid.write('%s*\n' % drawing_code) # Choose drawing aperture
offset = config.GAT[drawing_code].dimx/2.0
# Lower-left
x = OriginX + offset
y = OriginY + offset
fid.write('X%07dY%07dD02*\n' % (util.in2gerb(x+0.125), util.in2gerb(y+0.000)))
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+0.000), util.in2gerb(y+0.000)))
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+0.000), util.in2gerb(y+0.125)))
# Lower-right
x = MaxXExtent - offset
y = OriginY + offset
fid.write('X%07dY%07dD02*\n' % (util.in2gerb(x+0.000), util.in2gerb(y+0.125)))
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+0.000), util.in2gerb(y+0.000)))
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x-0.125), util.in2gerb(y+0.000)))
# Upper-right
x = MaxXExtent - offset
y = MaxYExtent - offset
fid.write('X%07dY%07dD02*\n' % (util.in2gerb(x-0.125), util.in2gerb(y+0.000)))
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+0.000), util.in2gerb(y+0.000)))
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+0.000), util.in2gerb(y-0.125)))
# Upper-left
x = OriginX + offset
y = MaxYExtent - offset
fid.write('X%07dY%07dD02*\n' % (util.in2gerb(x+0.000), util.in2gerb(y-0.125)))
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+0.000), util.in2gerb(y+0.000)))
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(x+0.125), util.in2gerb(y+0.000)))
def disclaimer():
print """
****************************************************
* R E A D C A R E F U L L Y *
* *
* This program comes with no warranty. You use *
* this program at your own risk. Do not submit *
* board files for manufacture until you have *
* thoroughly inspected the output of this program *
* using a previewing program such as: *
* *
* Windows: *
* - GC-Prevue <http://www.graphicode.com> *
* - ViewMate <http://www.pentalogix.com> *
* *
* Linux: *
* - gerbv <http://gerbv.sourceforge.net> *
* *
* By using this program you agree to take full *
* responsibility for the correctness of the data *
* that is generated by this program. *
****************************************************
To agree to the above terms, press 'y' then Enter.
Any other key will exit the program.
"""
s = raw_input()
if s == 'y':
print
return
print "\nExiting..."
sys.exit(0)
def tile_jobs(Jobs):
"""Take a list of raw Job objects and find best tiling by calling tile_search"""
# We must take the raw jobs and construct a list of 4-tuples (Xdim,Ydim,job,rjob).
# This means we must construct a rotated job for each entry. We first sort all
# jobs from largest to smallest. This should give us the best tilings first so
# we can interrupt the tiling process and get a decent layout.
L = []
#sortJobs = schwartz.schwartz(Jobs, jobs.Job.jobarea)
sortJobs = schwartz.schwartz(Jobs, jobs.Job.maxdimension)
sortJobs.reverse()
for job in sortJobs:
Xdim = job.width_in()
Ydim = job.height_in()
rjob = jobs.rotateJob(job, 90) ##NOTE: This will only try 90 degree rotations though 180 & 270 are available
for count in range(job.Repeat):
L.append( (Xdim,Ydim,job,rjob) )
PX,PY = config.Config['panelwidth'],config.Config['panelheight']
if config.AutoSearchType==RANDOM_SEARCH:
tile = tilesearch2.tile_search2(L, PX, PY)
else:
tile = tilesearch1.tile_search1(L, PX, PY)
if not tile:
raise RuntimeError, 'Panel size %.2f"x%.2f" is too small to hold jobs' % (PX,PY)
return tile
def merge(opts, args, gui = None):
writeGerberHeader = writeGerberHeader22degrees
global GUI
GUI = gui
for opt, arg in opts:
if opt in ('--octagons',):
if arg=='rotate':
writeGerberHeader = writeGerberHeader0degrees
elif arg=='normal':
writeGerberHeader = writeGerberHeader22degrees
else:
raise RuntimeError, 'Unknown octagon format'
elif opt in ('--random-search',):
config.AutoSearchType = RANDOM_SEARCH
elif opt in ('--full-search',):
config.AutoSearchType = EXHAUSTIVE_SEARCH
elif opt in ('--rs-fsjobs',):
config.RandomSearchExhaustiveJobs = int(arg)
elif opt in ('--search-timeout',):
config.SearchTimeout = int(arg)
elif opt in ('--place-file',):
config.AutoSearchType = FROM_FILE
config.PlacementFile = arg
elif opt in ('--no-trim-gerber',):
config.TrimGerber = 0
elif opt in ('--no-trim-excellon',):
config.TrimExcellon = 0
else:
raise RuntimeError, "Unknown option: %s" % opt
if len(args) > 2 or len(args) < 1:
raise RuntimeError, 'Invalid number of arguments'
# Load up the Jobs global dictionary, also filling out GAT, the
# global aperture table and GAMT, the global aperture macro table.
updateGUI("Reading job files...")
config.parseConfigFile(args[0])
# Force all X and Y coordinates positive by adding absolute value of minimum X and Y
for name, job in config.Jobs.iteritems():
min_x, min_y = job.mincoordinates()
shift_x = shift_y = 0
if min_x < 0: shift_x = abs(min_x)
if min_y < 0: shift_y = abs(min_y)
if (shift_x > 0) or (shift_y > 0):
job.fixcoordinates( shift_x, shift_y )
# Display job properties
for job in config.Jobs.values():
print 'Job %s:' % job.name,
if job.Repeat > 1:
print '(%d instances)' % job.Repeat
else:
print
print ' Extents: (%d,%d)-(%d,%d)' % (job.minx,job.miny,job.maxx,job.maxy)
print ' Size: %f" x %f"' % (job.width_in(), job.height_in())
print
# Trim drill locations and flash data to board extents
if config.TrimExcellon:
updateGUI("Trimming Excellon data...")
print 'Trimming Excellon data to board outlines ...'
for job in config.Jobs.values():
job.trimExcellon()
if config.TrimGerber:
updateGUI("Trimming Gerber data...")
print 'Trimming Gerber data to board outlines ...'
for job in config.Jobs.values():
job.trimGerber()
# We start origin at (0.1", 0.1") just so we don't get numbers close to 0
# which could trip up Excellon leading-0 elimination.
OriginX = OriginY = 0.1
# Read the layout file and construct the nested list of jobs. If there
# is no layout file, do auto-layout.
updateGUI("Performing layout...")
print 'Performing layout ...'
if len(args) > 1:
Layout = parselayout.parseLayoutFile(args[1])
# Do the layout, updating offsets for each component job.
X = OriginX + config.Config['leftmargin']
Y = OriginY + config.Config['bottommargin']
for row in Layout:
row.setPosition(X, Y)
Y += row.height_in() + config.Config['yspacing']
# Construct a canonical placement from the layout
Place = placement.Placement()
Place.addFromLayout(Layout)
del Layout
elif config.AutoSearchType == FROM_FILE:
Place = placement.Placement()
Place.addFromFile(config.PlacementFile, config.Jobs)
else:
# Do an automatic layout based on our tiling algorithm.
tile = tile_jobs(config.Jobs.values())
Place = placement.Placement()
Place.addFromTiling(tile, OriginX + config.Config['leftmargin'], OriginY + config.Config['bottommargin'])
(MaxXExtent,MaxYExtent) = Place.extents()
MaxXExtent += config.Config['rightmargin']
MaxYExtent += config.Config['topmargin']
# Start printing out the Gerbers. In preparation for drawing cut marks
# and crop marks, make sure we have an aperture to draw with. Use a 10mil line.
# If we're doing a fabrication drawing, we'll need a 1mil line.
OutputFiles = []
try:
fullname = config.MergeOutputFiles['placement']
except KeyError:
fullname = 'merged.placement.txt'
Place.write(fullname)
OutputFiles.append(fullname)
# For cut lines
AP = aptable.Aperture(aptable.Circle, 'D??', config.Config['cutlinewidth'])
drawing_code_cut = aptable.findInApertureTable(AP)
if drawing_code_cut is None:
drawing_code_cut = aptable.addToApertureTable(AP)
# For crop marks
AP = aptable.Aperture(aptable.Circle, 'D??', config.Config['cropmarkwidth'])
drawing_code_crop = aptable.findInApertureTable(AP)
if drawing_code_crop is None:
drawing_code_crop = aptable.addToApertureTable(AP)
# For fiducials
drawing_code_fiducial_copper = drawing_code_fiducial_soldermask = None
if config.Config['fiducialpoints']:
AP = aptable.Aperture(aptable.Circle, 'D??', config.Config['fiducialcopperdiameter'])
drawing_code_fiducial_copper = aptable.findInApertureTable(AP)
if drawing_code_fiducial_copper is None:
drawing_code_fiducial_copper = aptable.addToApertureTable(AP)
AP = aptable.Aperture(aptable.Circle, 'D??', config.Config['fiducialmaskdiameter'])
drawing_code_fiducial_soldermask = aptable.findInApertureTable(AP)
if drawing_code_fiducial_soldermask is None:
drawing_code_fiducial_soldermask = aptable.addToApertureTable(AP)
# For fabrication drawing.
AP = aptable.Aperture(aptable.Circle, 'D??', 0.001)
drawing_code1 = aptable.findInApertureTable(AP)
if drawing_code1 is None:
drawing_code1 = aptable.addToApertureTable(AP)
updateGUI("Writing merged files...")
print 'Writing merged output files ...'
for layername in config.LayerList.keys():
lname = layername
if lname[0]=='*':
lname = lname[1:]
try:
fullname = config.MergeOutputFiles[layername]
except KeyError:
fullname = 'merged.%s.ger' % lname
OutputFiles.append(fullname)
#print 'Writing %s ...' % fullname
fid = file(fullname, 'wt')
writeGerberHeader(fid)
# Determine which apertures and macros are truly needed
apUsedDict = {}
apmUsedDict = {}
for job in Place.jobs:
apd, apmd = job.aperturesAndMacros(layername)
apUsedDict.update(apd)
apmUsedDict.update(apmd)
# Increase aperature sizes to match minimum feature dimension
if config.MinimumFeatureDimension.has_key(layername):
print ' Thickening', lname, 'feature dimensions ...'
# Fix each aperture used in this layer
for ap in apUsedDict.keys():
new = config.GAT[ap].getAdjusted( config.MinimumFeatureDimension[layername] )
if not new: ## current aperture size met minimum requirement
continue
else: ## new aperture was created
new_code = aptable.findOrAddAperture(new) ## get name of existing aperture or create new one if needed
del apUsedDict[ap] ## the old aperture is no longer used in this layer
apUsedDict[new_code] = None ## the new aperture will be used in this layer
# Replace all references to the old aperture with the new one
for joblayout in Place.jobs:
job = joblayout.job ##access job inside job layout
temp = []
if job.hasLayer(layername):
for x in job.commands[layername]:
if x == ap:
temp.append(new_code) ## replace old aperture with new one
else:
temp.append(x) ## keep old command
job.commands[layername] = temp
if config.Config['cutlinelayers'] and (layername in config.Config['cutlinelayers']):
apUsedDict[drawing_code_cut]=None
if config.Config['cropmarklayers'] and (layername in config.Config['cropmarklayers']):
apUsedDict[drawing_code_crop]=None
if config.Config['fiducialpoints']:
if ((layername=='*toplayer') or (layername=='*bottomlayer')):
apUsedDict[drawing_code_fiducial_copper] = None
elif ((layername=='*topsoldermask') or (layername=='*bottomsoldermask')):
apUsedDict[drawing_code_fiducial_soldermask] = None
# Write only necessary macro and aperture definitions to Gerber file
writeApertureMacros(fid, apmUsedDict)
writeApertures(fid, apUsedDict)
#for row in Layout:
# row.writeGerber(fid, layername)
# # Do cut lines
# if config.Config['cutlinelayers'] and (layername in config.Config['cutlinelayers']):
# fid.write('%s*\n' % drawing_code_cut) # Choose drawing aperture
# row.writeCutLines(fid, drawing_code_cut, OriginX, OriginY, MaxXExtent, MaxYExtent)
# Finally, write actual flash data
for job in Place.jobs:
updateGUI("Writing merged output files...")
job.writeGerber(fid, layername)
if config.Config['cutlinelayers'] and (layername in config.Config['cutlinelayers']):
fid.write('%s*\n' % drawing_code_cut) # Choose drawing aperture
job.writeCutLines(fid, drawing_code_cut, OriginX, OriginY, MaxXExtent, MaxYExtent)
if config.Config['cropmarklayers']:
if layername in config.Config['cropmarklayers']:
writeCropMarks(fid, drawing_code_crop, OriginX, OriginY, MaxXExtent, MaxYExtent)
if config.Config['fiducialpoints']:
if ((layername=='*toplayer') or (layername=='*bottomlayer')):
writeFiducials(fid, drawing_code_fiducial_copper, OriginX, OriginY, MaxXExtent, MaxYExtent)
elif ((layername=='*topsoldermask') or (layername=='*bottomsoldermask')):
writeFiducials(fid, drawing_code_fiducial_soldermask, OriginX, OriginY, MaxXExtent, MaxYExtent)
writeGerberFooter(fid)
fid.close()
# Write board outline layer if selected
fullname = config.Config['outlinelayerfile']
if fullname and fullname.lower() != "none":
OutputFiles.append(fullname)
#print 'Writing %s ...' % fullname
fid = file(fullname, 'wt')
writeGerberHeader(fid)
# Write width-1 aperture to file
AP = aptable.Aperture(aptable.Circle, 'D10', 0.001)
AP.writeDef(fid)
# Choose drawing aperture D10
fid.write('D10*\n')
# Draw the rectangle
fid.write('X%07dY%07dD02*\n' % (util.in2gerb(OriginX), util.in2gerb(OriginY))) # Bottom-left
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(OriginX), util.in2gerb(MaxYExtent))) # Top-left
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(MaxXExtent), util.in2gerb(MaxYExtent))) # Top-right
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(MaxXExtent), util.in2gerb(OriginY))) # Bottom-right
fid.write('X%07dY%07dD01*\n' % (util.in2gerb(OriginX), util.in2gerb(OriginY))) # Bottom-left
writeGerberFooter(fid)
fid.close()
# Write scoring layer if selected
fullname = config.Config['scoringfile']
if fullname and fullname.lower() != "none":
OutputFiles.append(fullname)
#print 'Writing %s ...' % fullname
fid = file(fullname, 'wt')
writeGerberHeader(fid)
# Write width-1 aperture to file
AP = aptable.Aperture(aptable.Circle, 'D10', 0.001)
AP.writeDef(fid)
# Choose drawing aperture D10
fid.write('D10*\n')
# Draw the scoring lines
scoring.writeScoring(fid, Place, OriginX, OriginY, MaxXExtent, MaxYExtent)
writeGerberFooter(fid)
fid.close()
# Get a list of all tools used by merging keys from each job's dictionary
# of tools.
if 0:
Tools = {}
for job in config.Jobs.values():
for key in job.xcommands.keys():
Tools[key] = 1
Tools = Tools.keys()
Tools.sort()
else:
toolNum = 0
# First construct global mapping of diameters to tool numbers
for job in config.Jobs.values():
for tool,diam in job.xdiam.items():
if config.GlobalToolRMap.has_key(diam):
continue
toolNum += 1
config.GlobalToolRMap[diam] = "T%02d" % toolNum
# Cluster similar tool sizes to reduce number of drills
if config.Config['drillclustertolerance'] > 0:
config.GlobalToolRMap = drillcluster.cluster( config.GlobalToolRMap, config.Config['drillclustertolerance'] )
drillcluster.remap( Place.jobs, config.GlobalToolRMap.items() )
# Now construct mapping of tool numbers to diameters
for diam,tool in config.GlobalToolRMap.items():
config.GlobalToolMap[tool] = diam
# Tools is just a list of tool names
Tools = config.GlobalToolMap.keys()
Tools.sort()
fullname = config.Config['fabricationdrawingfile']
if fullname and fullname.lower() != 'none':
if len(Tools) > strokes.MaxNumDrillTools:
raise RuntimeError, "Only %d different tool sizes supported for fabrication drawing." % strokes.MaxNumDrillTools
OutputFiles.append(fullname)
#print 'Writing %s ...' % fullname
fid = file(fullname, 'wt')
writeGerberHeader(fid)
writeApertures(fid, {drawing_code1: None})
fid.write('%s*\n' % drawing_code1) # Choose drawing aperture
fabdrawing.writeFabDrawing(fid, Place, Tools, OriginX, OriginY, MaxXExtent, MaxYExtent)
writeGerberFooter(fid)
fid.close()
# Finally, print out the Excellon
try:
fullname = config.MergeOutputFiles['drills']
except KeyError:
fullname = 'merged.drills.xln'
OutputFiles.append(fullname)
#print 'Writing %s ...' % fullname
fid = file(fullname, 'wt')
writeExcellonHeader(fid)
# Ensure each one of our tools is represented in the tool list specified
# by the user.
for tool in Tools:
try:
size = config.GlobalToolMap[tool]
except:
raise RuntimeError, "INTERNAL ERROR: Tool code %s not found in global tool map" % tool
writeExcellonTool(fid, tool, size)
#for row in Layout:
# row.writeExcellon(fid, size)
for job in Place.jobs:
job.writeExcellon(fid, size)
writeExcellonFooter(fid)
fid.close()
updateGUI("Closing files...")
# Compute stats
jobarea = 0.0
#for row in Layout:
# jobarea += row.jobarea()
for job in Place.jobs:
jobarea += job.jobarea()
totalarea = ((MaxXExtent-OriginX)*(MaxYExtent-OriginY))
ToolStats = {}
drillhits = 0
for tool in Tools:
ToolStats[tool]=0
#for row in Layout:
# hits = row.drillhits(config.GlobalToolMap[tool])
# ToolStats[tool] += hits
# drillhits += hits
for job in Place.jobs:
hits = job.drillhits(config.GlobalToolMap[tool])
ToolStats[tool] += hits
drillhits += hits
try:
fullname = config.MergeOutputFiles['toollist']
except KeyError:
fullname = 'merged.toollist.drl'
OutputFiles.append(fullname)
#print 'Writing %s ...' % fullname
fid = file(fullname, 'wt')
print '-'*50
print ' Job Size : %f" x %f"' % (MaxXExtent-OriginX, MaxYExtent-OriginY)
print ' Job Area : %.2f sq. in.' % totalarea
print ' Area Usage : %.1f%%' % (jobarea/totalarea*100)
print ' Drill hits : %d' % drillhits
print 'Drill density : %.1f hits/sq.in.' % (drillhits/totalarea)
print '\nTool List:'
smallestDrill = 999.9
for tool in Tools:
if ToolStats[tool]:
fid.write('%s %.4fin\n' % (tool, config.GlobalToolMap[tool]))
print ' %s %.4f" %5d hits' % (tool, config.GlobalToolMap[tool], ToolStats[tool])
smallestDrill = min(smallestDrill, config.GlobalToolMap[tool])
fid.close()
print "Smallest Tool: %.4fin" % smallestDrill
print
print 'Output Files :'
for f in OutputFiles:
print ' ', f
if (MaxXExtent-OriginX)>config.Config['panelwidth'] or (MaxYExtent-OriginY)>config.Config['panelheight']:
print '*'*75
print '*'
print '* ERROR: Merged job exceeds panel dimensions of %.1f"x%.1f"' % (config.Config['panelwidth'],config.Config['panelheight'])
print '*'
print '*'*75
sys.exit(1)
# Done!
return 0
def updateGUI(text = None):
global GUI
if GUI != None:
GUI.updateProgress(text)
if __name__=="__main__":
try:
opts, args = getopt.getopt(sys.argv[1:], 'hv', ['help', 'version', 'octagons=', 'random-search', 'full-search', 'rs-fsjobs=', 'search-timeout=', 'place-file=', 'no-trim-gerber', 'no-trim-excellon'])
except getopt.GetoptError:
usage()
for opt, arg in opts:
if opt in ('-h', '--help'):
usage()
elif opt in ('-v', '--version'):
print """
GerbMerge Version %d.%d -- Combine multiple Gerber/Excellon files
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of this license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
""" % (VERSION_MAJOR, VERSION_MINOR)
sys.exit(0)
elif opt in ('--octagons', '--random-search','--full-search','--rs-fsjobs','--place-file','--no-trim-gerber','--no-trim-excellon', '--search-timeout'):
pass ## arguments are valid
else:
raise RuntimeError, "Unknown option: %s" % opt
if len(args) > 2 or len(args) < 1:
usage()
disclaimer()
sys.exit(merge(opts, args)) ## run germberge
# vim: expandtab ts=2 sw=2 ai syntax=python

Binary file not shown.

Binary file not shown.

1288
gerber/gerbmerge/bin/jobs.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,167 @@
#!/usr/bin/env python
"""Support for writing characters and graphics to Gerber files
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import math
import strokes
# Define percentage of cell height and width to determine
# intercharacter spacing
SpacingX = 1.20
SpacingY = 1.20
# Arrow dimensions
BarLength = 1500 # Length of dimension line
ArrowWidth = 750 # How broad the arrow is
ArrowLength = 750 # How far back from dimension line it is
ArrowStemLength = 1250 # How long the arrow stem extends from center point
#################################################################
# Arrow directions
FacingLeft=0 # 0 degrees
FacingDown=1 # 90 degrees counterclockwise
FacingRight=2 # 180 degrees
FacingUp=3 # 270 degrees
SpacingDX = 10*int(round(strokes.MaxWidth*SpacingX))
SpacingDY = 10*int(round(strokes.MaxHeight*SpacingY))
RotatedGlyphs={}
# Default arrow glyph is at 0 degrees rotation, facing left
ArrowGlyph = [ [(0,-BarLength/2), (0, BarLength/2)],
[(ArrowLength,ArrowWidth/2), (0,0), (ArrowLength,-ArrowWidth/2)],
[(0,0), (ArrowStemLength,0)]
]
def rotateGlyph(glyph, degrees, glyphName):
"""Rotate a glyph counterclockwise by given number of degrees. The glyph
is a list of lists, where each sub-list is a connected path."""
try:
return RotatedGlyphs["%.1f_%s" % (degrees, glyphName)]
except KeyError:
pass # Not cached yet
rad = degrees/180.0*math.pi
cosx = math.cos(rad)
sinx = math.sin(rad)
newglyph = []
for path in glyph:
newpath = []
for X,Y in path:
x = int(round(X*cosx - Y*sinx))
y = int(round(X*sinx + Y*cosx))
newpath.append((x,y))
newglyph.append(newpath)
RotatedGlyphs["%.1f_%s" % (degrees, glyphName)] = newglyph
return newglyph
def writeFlash(fid, X, Y, D):
fid.write("X%07dY%07dD%02d*\n" % (X,Y,D))
def drawPolyline(fid, L, offX, offY, scale=1):
for ix in range(len(L)):
X,Y = L[ix]
X *= scale
Y *= scale
if ix==0:
writeFlash(fid, X+offX, Y+offY, 2)
else:
writeFlash(fid, X+offX, Y+offY, 1)
def writeGlyph(fid, glyph, X, Y, degrees, glyphName=None):
if not glyphName:
glyphName = str(glyph)
for path in rotateGlyph(glyph, degrees, glyphName):
drawPolyline(fid, path, X, Y, 10)
def writeChar(fid, c, X, Y, degrees):
if c==' ': return
try:
glyph = strokes.StrokeMap[c]
except:
raise RuntimeError, 'No glyph for character %s' % hex(ord(c))
writeGlyph(fid, glyph, X, Y, degrees, c)
def writeString(fid, s, X, Y, degrees):
posX = X
posY = Y
rad = degrees/180.0*math.pi
dX = int(round(math.cos(rad)*SpacingDX))
dY = int(round(math.sin(rad)*SpacingDX))
if 0:
if dX < 0:
# Always print text left to right
dX = -dX
s = list(s)
s.reverse()
s = string.join(s, '')
for char in s:
writeChar(fid, char, posX, posY, degrees)
posX += dX
posY += dY
def drawLine(fid, X1, Y1, X2, Y2):
drawPolyline(fid, [(X1,Y1), (X2,Y2)], 0, 0)
def boundingBox(s, X1, Y1):
"Return (X1,Y1),(X2,Y2) for given string"
if not s:
return (X1, Y1), (X1, Y1)
X2 = X1 + (len(s)-1)*SpacingDX + 10*strokes.MaxWidth
Y2 = Y1 + 10*strokes.MaxHeight # Not including descenders
return (X1, Y1), (X2, Y2)
def drawDimensionArrow(fid, X, Y, facing):
writeGlyph(fid, ArrowGlyph, X, Y, facing*90, "Arrow")
def drawDrillHit(fid, X, Y, toolNum):
writeGlyph(fid, strokes.DrillStrokeList[toolNum], X, Y, 0, "Drill%02d" % toolNum)
if __name__=="__main__":
import string
s = string.digits+string.letters+string.punctuation
#s = "The quick brown fox jumped over the lazy dog!"
fid = file('test.ger','wt')
fid.write("""G75*
G70*
%OFA0B0*%
%FSAX24Y24*%
%IPPOS*%
%LPD*%
%AMOC8*
5,1,8,0,0,1.08239X$1,22.5*
*%
%ADD10C,0.0100*%
D10*
""")
writeString(fid, s, 0, 0, 0)
drawDimensionArrow(fid, 0, 5000, FacingLeft)
drawDimensionArrow(fid, 5000, 5000, FacingRight)
drawDimensionArrow(fid, 0, 10000, FacingUp)
drawDimensionArrow(fid, 5000, 10000, FacingDown)
for diam in range(0,strokes.MaxNumDrillTools):
writeGlyph(fid, strokes.DrillStrokeList[diam], diam*1250, 15000, 0, "%02d" % diam)
fid.write("M02*\n")
fid.close()

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,338 @@
#!/usr/bin/env python
"""
Parse the job layout specification file.
Requires:
- SimpleParse 2.1 or higher
http://simpleparse.sourceforge.net
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import string
from simpleparse.parser import Parser
import config
import jobs
declaration = r'''
file := (commentline/nullline/rowspec)+
rowspec := ts, 'Row', ws, '{'!, ts, comment?, '\n', rowjob+, ts, '}'!, ts, comment?, '\n'
rowjob := jobspec/colspec/commentline/nullline
colspec := ts, 'Col', ws, '{'!, ts, comment?, '\n', coljob+, ts, '}'!, ts, comment?, '\n'
coljob := jobspec/rowspec/commentline/nullline
jobspec := ts, (paneljobspec/basicjobspec), ts, comment?, '\n'
basicjobspec := id, (rotation)?
paneljobspec := 'Not yet implemented'
#paneljobspec := int, [xX], int, ws, basicjobspec
comment := ([#;]/'//'), -'\n'*
commentline := ts, comment, '\n'
nullline := ts, '\n'
rotation := ws, 'Rotate', ('90'/'180'/'270')?
ws := [ \t]+
ts := [ \t]*
id := [a-zA-Z0-9], [a-zA-Z0-9_-]*
int := [0-9]+
'''
class Panel: # Meant to be subclassed as either a Row() or Col()
def __init__(self):
self.x = None
self.y = None
self.jobs = [] # List (left-to-right or bottom-to-top) of JobLayout() or Row()/Col() objects
def canonicalize(self): # Return plain list of JobLayout objects at the roots of all trees
L = []
for job in self.jobs:
L = L + job.canonicalize()
return L
def addjob(self, job): # Either a JobLayout class or Panel (sub)class
assert isinstance(job, Panel) or isinstance(job, jobs.JobLayout)
self.jobs.append(job)
def addwidths(self):
"Return width in inches"
width = 0.0
for job in self.jobs:
width += job.width_in() + config.Config['xspacing']
width -= config.Config['xspacing']
return width
def maxwidths(self):
"Return maximum width in inches of any one subpanel"
width = 0.0
for job in self.jobs:
width = max(width,job.width_in())
return width
def addheights(self):
"Return height in inches"
height = 0.0
for job in self.jobs:
height += job.height_in() + config.Config['yspacing']
height -= config.Config['yspacing']
return height
def maxheights(self):
"Return maximum height in inches of any one subpanel"
height = 0.0
for job in self.jobs:
height = max(height,job.height_in())
return height
def writeGerber(self, fid, layername):
for job in self.jobs:
job.writeGerber(fid, layername)
def writeExcellon(self, fid, tool):
for job in self.jobs:
job.writeExcellon(fid, tool)
def writeDrillHits(self, fid, tool, toolNum):
for job in self.jobs:
job.writeDrillHits(fid, tool, toolNum)
def writeCutLines(self, fid, drawing_code, X1, Y1, X2, Y2):
for job in self.jobs:
job.writeCutLines(fid, drawing_code, X1, Y1, X2, Y2)
def drillhits(self, tool):
hits = 0
for job in self.jobs:
hits += job.drillhits(tool)
return hits
def jobarea(self):
area = 0.0
for job in self.jobs:
area += job.jobarea()
return area
class Row(Panel):
def __init__(self):
Panel.__init__(self)
self.LR = 1 # Horizontal arrangement
def width_in(self):
return self.addwidths()
def height_in(self):
return self.maxheights()
def setPosition(self, x, y): # In inches
self.x = x
self.y = y
for job in self.jobs:
job.setPosition(x,y)
x += job.width_in() + config.Config['xspacing']
class Col(Panel):
def __init__(self):
Panel.__init__(self)
self.LR = 0 # Vertical arrangement
def width_in(self):
return self.maxwidths()
def height_in(self):
return self.addheights()
def setPosition(self, x, y): # In inches
self.x = x
self.y = y
for job in self.jobs:
job.setPosition(x,y)
y += job.height_in() + config.Config['yspacing']
def canonicalizePanel(panel):
L = []
for job in panel:
L = L + job.canonicalize()
return L
def findJob(jobname, rotated, Jobs=config.Jobs):
"""
Find a job in config.Jobs, possibly rotating it
If job not in config.Jobs add it for future reference
Return found job
"""
if rotated == 90:
fullname = jobname + '*rotated90'
elif rotated == 180:
fullname = jobname + '*rotated180'
elif rotated == 270:
fullname = jobname + '*rotated270'
else:
fullname = jobname
try:
for existingjob in Jobs.keys():
if existingjob.lower() == fullname.lower(): ## job names are case insensitive
job = Jobs[existingjob]
return jobs.JobLayout(job)
except:
pass
# Perhaps we just don't have a rotated job yet
if rotated:
try:
for existingjob in Jobs.keys():
if existingjob.lower() == jobname.lower(): ## job names are case insensitive
job = Jobs[existingjob]
except:
raise RuntimeError, "Job name '%s' not found" % jobname
else:
raise RuntimeError, "Job name '%s' not found" % jobname
# Make a rotated job
job = jobs.rotateJob(job, rotated)
Jobs[fullname] = job
return jobs.JobLayout(job)
def parseJobSpec(spec, data):
for jobspec in spec:
if jobspec[0] in ('ts','comment'): continue
assert jobspec[0] in ('paneljobspec','basicjobspec')
if jobspec[0] == 'basicjobspec':
namefield = jobspec[3][0]
jobname = data[namefield[1]:namefield[2]]
if len(jobspec[3]) > 1:
rotationfield = jobspec[3][1]
rotation = data[ rotationfield[1] + 1: rotationfield[2] ]
if (rotation == "Rotate") or (rotation == "Rotate90"):
rotated = 90
elif rotation == "Rotate180":
rotated = 180
elif rotation == "Rotate270":
rotated = 270
else:
raise RuntimeError, "Unsupported rotation: %s" % rotation
else:
rotated = 0
return findJob(jobname, rotated)
else:
raise RuntimeError, "Matrix panels not yet supported"
def parseColSpec(spec, data):
jobs = Col()
for coljob in spec:
if coljob[0] in ('ts','ws','comment'): continue
assert coljob[0] == 'coljob'
job = coljob[3][0]
if job[0] in ('commentline','nullline'): continue
assert job[0] in ('jobspec','rowspec')
if job[0] == 'jobspec':
jobs.addjob(parseJobSpec(job[3],data))
else:
jobs.addjob(parseRowSpec(job[3],data))
return jobs
def parseRowSpec(spec, data):
jobs = Row()
for rowjob in spec:
if rowjob[0] in ('ts','ws','comment'): continue
assert rowjob[0] == 'rowjob'
job = rowjob[3][0]
if job[0] in ('commentline','nullline'): continue
assert job[0] in ('jobspec','colspec')
if job[0] == 'jobspec':
jobs.addjob(parseJobSpec(job[3],data))
else:
jobs.addjob(parseColSpec(job[3],data))
return jobs
def parseLayoutFile(fname):
"""config.Jobs is a dictionary of ('jobname', Job Object).
The return value is a nested array. The primary dimension
of the array is one row:
[ Row1, Row2, Row3 ]
Each row element consists of a list of jobs or columns (i.e.,
JobLayout or Col objects).
Each column consists of a list of either jobs or rows.
These are recursive, so it can look like:
[
Row([JobLayout(), Col([ Row([JobLayout(), JobLayout()]),
JobLayout() ]), JobLayout() ]), # That was row 0
Row([JobLayout(), JobLayout()]) # That was row 1
]
This is a panel with two rows. In the first row there is
a job, a column, and another job, from left to right. In the
second row there are two jobs, from left to right.
The column in the first row has two jobs side by side, then
another one above them.
"""
try:
fid = file(fname, 'rt')
except Exception, detail:
raise RuntimeError, "Unable to open layout file: %s\n %s" % (fname, str(detail))
data = fid.read()
fid.close()
parser = Parser(declaration, "file")
# Replace all CR's in data with nothing, to convert DOS line endings
# to unix format (all LF's).
data = string.replace(data, '\x0D', '')
tree = parser.parse(data)
# Last element of tree is number of characters parsed
if not tree[0]:
raise RuntimeError, "Layout file cannot be parsed"
if tree[2] != len(data):
raise RuntimeError, "Parse error at character %d in layout file" % tree[2]
Rows = []
for rowspec in tree[1]:
if rowspec[0] in ('nullline', 'commentline'): continue
assert rowspec[0]=='rowspec'
Rows.append(parseRowSpec(rowspec[3], data))
return Rows
if __name__=="__main__":
fid = file(sys.argv[1])
testdata = fid.read()
fid.close()
parser = Parser(declaration, "file")
import pprint
pprint.pprint(parser.parse(testdata))

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,108 @@
#!/usr/bin/env python
"""A placement is a final arrangement of jobs at given (X,Y) positions.
This class is intended to "un-pack" an arragement of jobs constructed
manually through Layout/Panel/JobLayout/etc. (i.e., a layout.def file)
or automatically through a Tiling. From either source, the result is
simply a list of jobs.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import re
import parselayout
import jobs
class Placement:
def __init__(self):
self.jobs = [] # A list of JobLayout objects
def addFromLayout(self, Layout):
# Layout is a recursive list of JobLayout items. At the end
# of each tree there is a JobLayout object which has a 'job'
# member, which is what we're looking for. Fortunately, the
# canonicalize() function flattens the tree.
#
# Positions of jobs have already been set (we're assuming)
# prior to calling this function.
self.jobs = self.jobs + parselayout.canonicalizePanel(Layout)
def addFromTiling(self, T, OriginX, OriginY):
# T is a Tiling. Calling its canonicalize() method will construct
# a list of JobLayout objects and set the (X,Y) position of each
# object.
self.jobs = self.jobs + T.canonicalize(OriginX,OriginY)
def extents(self):
"""Return the maximum X and Y value over all jobs"""
maxX = 0.0
maxY = 0.0
for job in self.jobs:
maxX = max(maxX, job.x+job.width_in())
maxY = max(maxY, job.y+job.height_in())
return (maxX,maxY)
def write(self, fname):
"""Write placement to a file"""
fid = file(fname, 'wt')
for job in self.jobs:
fid.write('%s %.3f %.3f\n' % (job.job.name, job.x, job.y))
fid.close()
def addFromFile(self, fname, Jobs):
"""Read placement from a file, placed against jobs in Jobs list"""
pat = re.compile(r'\s*(\S+)\s+(\S+)\s+(\S+)')
comment = re.compile(r'\s*(?:#.+)?$')
try:
fid = file(fname, 'rt')
except:
print 'Unable to open placement file: "%s"' % fname
sys.exit(1)
lines = fid.readlines()
fid.close()
for line in lines:
if comment.match(line): continue
match = pat.match(line)
if not match:
print 'Cannot interpret placement line in placement file:\n %s' % line
sys.exit(1)
jobname, X, Y = match.groups()
try:
X = float(X)
Y = float(Y)
except:
print 'Illegal (X,Y) co-ordinates in placement file:\n %s' % line
sys.exit(1)
rotated = 0
if len(jobname) > 8:
if jobname[-8:] == '*rotated':
rotated = 90
jobname = jobname[:-8]
elif jobname[-10:] == '*rotated90':
rotated = 90
jobname = jobname[:-10]
elif jobname[-11:] == '*rotated180':
rotated = 180
jobname = jobname[:-11]
elif jobname[-11:] == '*rotated270':
rotated = 270
jobname = jobname[:-11]
addjob = parselayout.findJob(jobname, rotated, Jobs)
addjob.setPosition(X,Y)
self.jobs.append(addjob)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,38 @@
"""
Implement the Schwartizan Transform method of sorting
a list by an arbitrary metric (see the Python FAQ section
4.51).
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
def stripit(pair):
return pair[1]
def schwartz(List, Metric):
def pairing(element, M = Metric):
return (M(element), element)
paired = map(pairing, List)
paired.sort()
return map(stripit, paired)
def stripit2(pair):
return pair[0]
def schwartz2(List, Metric):
"Returns sorted list and also corresponding metrics"
def pairing(element, M = Metric):
return (M(element), element)
paired = map(pairing, List)
paired.sort()
theList = map(stripit, paired)
theMetrics = map(stripit2, paired)
return (theList, theMetrics)

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,301 @@
#!/usr/bin/env python
"""This file handles the writing of the scoring lines Gerber file
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import config
import util
import makestroke
# Add a horizontal line if its within the extents of the panel. Also, trim
# start and/or end points to the extents.
def addHorizontalLine(Lines, x1, x2, y, extents):
assert (x1 < x2)
# For a horizontal line, y must be above extents[1] and below extents[3].
if extents[1] < y < extents[3]:
# Now trim endpoints to be greater than extents[0] and below extents[2]
line = (max(extents[0], x1), y, min(extents[2], x2), y)
Lines.append(line)
# Add a vertical line if its within the extents of the panel. Also, trim
# start and/or end points to the extents.
def addVerticalLine(Lines, x, y1, y2, extents):
assert (y1 < y2)
# For a vertical line, x must be above extents[0] and below extents[2].
if extents[0] < x < extents[2]:
# Now trim endpoints to be greater than extents[1] and below extents[3]
line = (x, max(extents[1], y1), x, min(extents[3], y2))
Lines.append(line)
def isHorizontal(line):
return line[1]==line[3]
def isVertical(line):
return line[0]==line[2]
def clusterOrdinates(values):
"""Create a list of tuples where each tuple is a variable-length list of items
from 'values' that are all within 2 mils of each other."""
# First, make sure the values are sorted. Then, take the first one and go along
# the list clustering as many as possible.
values.sort()
currCluster = None
L = []
for val in values:
if currCluster is None:
currCluster = (val,)
else:
if (val - currCluster[0]) <= 0.002:
currCluster = currCluster + (val,)
else:
L.append(currCluster)
currCluster = (val,)
if currCluster is not None:
L.append(currCluster)
return L
def mergeHLines(Lines):
"""Lines is a list of 4-tuples (lines) that have nearly the same Y ordinate and are to be
optimized by combining overlapping lines."""
# First, make sure lines are sorted by starting X ordinate and that all lines
# proceed to the right.
Lines.sort()
for line in Lines:
assert line[0] < line[2]
# Obtain the average value of the Y ordinate and use that as the Y ordinate for
# all lines.
yavg = 0.0
for line in Lines:
yavg += line[1]
yavg /= len(Lines)
NewLines = []
# Now proceed to pick off one line at a time and try to merge it with
# the next one in sequence.
currLine = None
for line in Lines:
if currLine is None:
currLine = line
else:
# If the line to examine starts to the left of (within 0.002") the end
# of the current line, extend the current line.
if line[0] <= currLine[2]+0.002:
currLine = (currLine[0], yavg, max(line[2],currLine[2]), yavg)
else:
NewLines.append(currLine)
currLine = line
NewLines.append(currLine)
return NewLines
def sortByY(A,B):
"Helper function to sort two lines (4-tuples) by their starting Y ordinate"
return cmp(A[1], B[1])
def mergeVLines(Lines):
"""Lines is a list of 4-tuples (lines) that have nearly the same X ordinate and are to be
optimized by combining overlapping lines."""
# First, make sure lines are sorted by starting Y ordinate and that all lines
# proceed up.
Lines.sort(sortByY)
for line in Lines:
assert line[1] < line[3]
# Obtain the average value of the X ordinate and use that as the X ordinate for
# all lines.
xavg = 0.0
for line in Lines:
xavg += line[0]
xavg /= len(Lines)
NewLines = []
# Now proceed to pick off one line at a time and try to merge it with
# the next one in sequence.
currLine = None
for line in Lines:
if currLine is None:
currLine = line
else:
# If the line to examine starts below (within 0.002") the end
# of the current line, extend the current line.
if line[1] <= currLine[3]+0.002:
currLine = (xavg, currLine[1], xavg, max(line[3],currLine[3]))
else:
NewLines.append(currLine)
currLine = line
NewLines.append(currLine)
return NewLines
def mergeLines(Lines):
# All lines extend up (vertical) and to the right (horizontal). First, do
# simple merges. Sort all lines, which will order the lines with starting
# points in increasing X order (i.e., to the right).
Lines.sort()
# Now sort the lines into horizontal lines and vertical lines. For each
# ordinate, group all lines by that ordinate in a dictionary. Thus, all
# horizontal lines will be grouped together by Y ordinate, and all
# vertical lines will be grouped together by X ordinate.
HLines = {}
VLines = {}
for line in Lines:
if isHorizontal(line):
try:
HLines[line[1]].append(line)
except KeyError:
HLines[line[1]] = [line]
else:
try:
VLines[line[0]].append(line)
except KeyError:
VLines[line[0]] = [line]
# I don't think the next two blocks of code are necessary (merging lines
# that are at exactly the same ordinate) since the last two blocks of
# code do the same thing more generically by merging lines at close-enough
# ordinates.
# Extend horizontal lines
NewHLines = {}
for yval,lines in HLines.items():
# yval is the Y ordinate of this group of lines. lines is the set of all
# lines with this Y ordinate.
NewHLines[yval] = []
# Try to extend the first element of this list, which will be the leftmost.
xline = lines[0]
for line in lines[1:]:
# If this line's left edge is within 2 mil of the right edge of the line
# we're currently trying to grow, then grow it.
if abs(line[0] - xline[2]) <= 0.002: # Arbitrary 2mil?
# Extend...
xline = (xline[0], xline[1], line[2], xline[1])
else:
# ...otherwise, append the currently-extended line and make this
# line the new one we try to extend.
NewHLines[yval].append(xline)
xline = line
NewHLines[yval].append(xline)
# Extend vertical lines
NewVLines = {}
for xval,lines in VLines.items():
# xval is the X ordinate of this group of lines. lines is the set of all
# lines with this X ordinate.
NewVLines[xval] = []
# Try to extend the first element of this list, which will be the bottom-most.
xline = lines[0]
for line in lines[1:]:
# If this line's bottom edge is within 2 mil of the top edge of the line
# we're currently trying to grow, then grow it.
if abs(line[1] - xline[3]) <= 0.002: # Arbitrary 2mil?
# Extend...
xline = (xline[0], xline[1], xline[0], line[3])
else:
# ...otherwise, append the currently-extended line and make this
# line the new one we try to extend.
NewVLines[xval].append(xline)
xline = line
NewVLines[xval].append(xline)
HLines = NewHLines
VLines = NewVLines
NewHLines = []
NewVLines = []
# Now combine lines that have their endpoints either very near each other
# or within each other. We will have to sort all horizontal lines by their
# Y ordinates and group them according to Y ordinates that are close enough
# to each other.
yvals = HLines.keys()
clusters = clusterOrdinates(yvals) # A list of clustered tuples containing yvals
for cluster in clusters:
clusterLines = []
for yval in cluster:
clusterLines.extend(HLines[yval])
# clusterLines is now a list of lines (4-tuples) that all have nearly the same
# Y ordinate. Merge them together.
NewHLines.extend(mergeHLines(clusterLines))
xvals = VLines.keys()
clusters = clusterOrdinates(xvals)
for cluster in clusters:
clusterLines = []
for xval in cluster:
clusterLines.extend(VLines[xval])
# clusterLines is now a list of lines (4-tuples) that all have nearly the same
# X ordinate. Merge them together.
NewVLines.extend(mergeVLines(clusterLines))
Lines = NewHLines + NewVLines
return Lines
# Main entry point. Gerber file has already been opened, header written
# out, 1mil tool selected.
def writeScoring(fid, Place, OriginX, OriginY, MaxXExtent, MaxYExtent):
# For each job, write out 4 score lines, above, to the right, below, and
# to the left. After we collect all potential scoring lines, we worry
# about merging, etc.
dx = config.Config['xspacing']/2.0
dy = config.Config['yspacing']/2.0
extents = (OriginX, OriginY, MaxXExtent, MaxYExtent)
Lines = []
for layout in Place.jobs:
x = layout.x - dx
y = layout.y - dy
X = layout.x + layout.width_in() + dx
Y = layout.y + layout.height_in() + dy
# Just so we don't get 3.75000000004 and 3.75000000009, we round to
# 2.5 limits.
x,y,X,Y = [round(val,5) for val in [x,y,X,Y]]
if 0: # Scoring lines go all the way across the panel now
addHorizontalLine(Lines, x, X, Y, extents) # above job
addVerticalLine(Lines, X, y, Y, extents) # to the right of job
addHorizontalLine(Lines, x, X, y, extents) # below job
addVerticalLine(Lines, x, y, Y, extents) # to the left of job
else:
addHorizontalLine(Lines, OriginX, MaxXExtent, Y, extents) # above job
addVerticalLine(Lines, X, OriginY, MaxYExtent, extents) # to the right of job
addHorizontalLine(Lines, OriginX, MaxXExtent, y, extents) # below job
addVerticalLine(Lines, x, OriginY, MaxYExtent, extents) # to the left of job
# Combine disparate lines into single lines
Lines = mergeLines(Lines)
#for line in Lines:
# print [round(x,3) for x in line]
# Write 'em out
for line in Lines:
makestroke.drawPolyline(fid, [(util.in2gerb(line[0]),util.in2gerb(line[1])), \
(util.in2gerb(line[2]),util.in2gerb(line[3]))], 0, 0)
# vim: expandtab ts=2 sw=2 ai syntax=python

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,177 @@
#!/usr/bin/env python
"""
Regular expression, SimpleParse, ane message constants.
Requires:
- SimpleParse 2.1 or higher
http://simpleparse.sourceforge.net
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import re
from simpleparse.parser import Parser
DISCLAIMER = """
****************************************************
* R E A D C A R E F U L L Y *
* *
* This program comes with no warranty. You use *
* this program at your own risk. Do not submit *
* board files for manufacture until you have *
* thoroughly inspected the output of this program *
* using a previewing program such as: *
* *
* Windows: *
* - GC-Prevue <http://www.graphicode.com> *
* - ViewMate <http://www.pentalogix.com> *
* *
* Linux: *
* - gerbv <http://gerbv.sourceforge.net> *
* *
* By using this program you agree to take full *
* responsibility for the correctness of the data *
* that is generated by this program. *
****************************************************
"""[1:-1]
# [Options] section defaults Data types: "L" = layers (will show layer selection)
# "D" = decimal
# "DP" = possitive decimal
# "I" = integer
# "IP" = integer positive
# "PI" = path input (will show open dialog)
# "PO" = path output (will show save dialog)
# "S" = string
# "B" = boolean
# "BI" = boolean as integer
#
# THESE DATA TYPES ARE FIXED - CODE MUST CHANGE IF TYPES ARE ADDED/MODIFIED
DEFAULT_OPTIONS = {
# Spacing in horizontal direction
'xspacing': ('0.125', "DP", "XSpacing", "1 XSPACING_HELP"),
# Spacing in vertical direction
'yspacing': ('0.125', "DP", "YSpacing", "2 YSPACING_HELP"),
# X-Dimension maximum panel size (Olimex)
'panelwidth': ('12.6', "DP", "PanelWidth", "3 PANEL_WIDTH"),
# Y-Dimension maximum panel size (Olimex)
'panelheight': ('7.8', "DP", "PanelHeight", "4 PanelHeight"),
# e.g., *toplayer,*bottomlayer
'cropmarklayers': (None, "L", "CropMarkLayers", "5 CropMarkLayers"),
# Width (inches) of crop lines
'cropmarkwidth': ('0.01', "DP", "CropMarkWidth", "6 CropMarkWidth"),
# as for cropmarklayers
'cutlinelayers': (None, "L", "CutLineLayers", "7 CutLineLayers"),
# Width (inches) of cut lines
'cutlinewidth': ('0.01', "DP", "CutLineWidth", "8 CutLineWidth"),
# Minimum dimension for selected layers
'minimumfeaturesize': (None, "S", "MinimumFeatureSize", "Use this option to automatically thicken features on particular layers.\nThis is intended for thickening silkscreen to some minimum width.\nThe value of this option must be a comma-separated list\nof layer names followed by minimum feature sizes (in inches) for that layer.\nComment this out to disable thickening. Example usage is:\n\nMinimumFeatureSize = *topsilkscreen,0.008,*bottomsilkscreen,0.008"),
# Name of file containing default tool list
'toollist': (None, "PI", "ToolList", "10 ToolList"),
# Tolerance for clustering drill sizes
'drillclustertolerance': ('.002', "DP", "DrillClusterTolerance", "11 DrillClusterTolerance"),
# Set to 1 to allow multiple jobs to have non-matching layers
'allowmissinglayers': (0, "BI", "AllowMissingLayers", "12 AllowMissingLayers"),
# Name of file to which to write fabrication drawing, or None
'fabricationdrawingfile': (None, "PO", "FabricationDrawingFile", "13 FabricationDrawingFile"),
# Name of file containing text to write to fab drawing
'fabricationdrawingtext': (None, "PI", "FabricationDrawingText", "14 FabricationDrawingText"),
# Number of digits after the decimal point in input Excellon files
'excellondecimals': (4, "IP", "ExcellonDecimals", "15 ExcellonDecimals"),
# Generate leading zeros in merged Excellon output file
'excellonleadingzeros': (0, "IP", "ExcellonLeadingZeros", "16 ExcellonLeadingZeros"),
# Name of file to which to write simple box outline, or None
'outlinelayerfile': (None, "PO", "OutlineLayerFile", "17 OutlineLayerFile"),
# Name of file to which to write scoring data, or None
'scoringfile': (None, "PO", "ScoringFile", "18 ScoringFile"),
# Inches of extra room to leave on left side of panel for tooling
'leftmargin': (0.0, "DP", "LeftMargin", "19 LeftMargin"),
# Inches of extra room to leave on top side of panel for tooling
'topmargin': (0.0, "DP", "TopMargin", "20 TopMargin"),
# Inches of extra room to leave on right side of panel for tooling
'rightmargin': (0.0, "DP", "RightMargin", "21 RightMargin"),
# Inches of extra room to leave on bottom side of panel for tooling
'bottommargin': (0.0, "DP", "BottomMargin", "22 BottomMargin"),
# List of X,Y points at which to draw fiducials
'fiducialpoints': (None, "S", "FiducialPoints", "23 FiducialPoints"),
}
DEFAULT_OPTIONS_TYPES = ["IP", "I", "DP", "D", "B", "BI", "S", "PI", "PO", "L"] # List of option types in display order
# [GerbMergeGUI] section defaults
DEFAULT_GERBMERGEGUI = {
'unit': "IN", # Unit inidicator: IN, MIL, MM
'layout': "AUTOMATIC", # Indicates layout: GRID, AUTOMATIC, MANUAL, GRID_FILE, MANUAL_FILE
'runtime': 10, # Seconds to run automatic placement
'rows': 1, # Number of rows in grid layout
'columns': 1, # Number of columns in grid layout
'mergedoutput': False, # Path of output directory
'mergedname': False, # Prefix of merged output files
'layoutfilepath': "", # Path of layout file
'placementfilepath': "", # Path of placement file
'configurationfilepath': "", # Path of configuration file
'configurationcomplete': False, # Indicates that run dialog may be skipped to upon load
}
# Job names
RE_VALID_JOB = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9_-]*$')
RE_VALID_JOB_MESSAGE = "Vaild Characters: a-z, A-Z, 0-9, underscores, hyphens\nFirst Character must be: a-z, A-Z, 0-9"
RESERVED_JOB_NAMES = ("Options", "MergeOutputFiles", "GerbMergeGUI") ##not implemented yet
# Layer names
RE_VALID_LAYER = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9_-]*$')
RE_VALID_LAYER_MESSAGE = "Vaild Characters: a-z, A-Z, 0-9, underscores, hyphens\nFirst Character must be: a-z, A-Z, 0-9"
DEFAULT_LAYERS = [ "BoardOutline",
"TopCopper",
"BottomCopper",
"InnerLayer2",
"InnerLayer3",
"TopSilkscreen",
"BottomSilkscreen",
"TopSoldermask",
"BottomSoldermask",
"TopSolderPasteMask",
"BottomSolderPasteMask",
"Drills" ]
REQUIRED_LAYERS = ["BoardOutline", "Drills"]
RESERVED_LAYER_NAMES = () ##add "mergeout", not implemented yet
#Output names
RE_VALID_OUTPUT_NAME = re.compile(r'^[a-zA-Z0-9_-]+$')
RE_VALID_OUTPUT_NAME_MESSAGE = "Vaild Characters: a-z, A-Z, 0-9, underscores, hyphens"
REQUIRED_LAYERS_OUTPUT = ["BoardOutline", "ToolList", "Placement", "Drills"]
# Default dictionary of layer names to file extensions
FILE_EXTENSIONS = { "boardoutline": "GBO",
"topcopper": "GTL",
"bottomcopper": "GBL",
"innerlayer2": "G2",
"innerlayer3": "G3",
"topsilkscreen": "GTO",
"bottomsilkscreen": "GBO",
"topsoldermask": "GTS",
"bottomsoldermask": "GBS",
"topsolderpastemask": "GTP",
"bottomsolderpastemask": "GBP",
"drills": "GDD",
"placement": "TXT",
"toollist": "DRL",
}
DEFAULT_EXTENSION = "GER"
#Gerbmerge options
PLACE_FILE = "--place-file="
NO_TRIM_GERBER = "--no-trim-gerber"
NO_TRIM_EXCELLON = "--no-trim-excellon"
ROTATED_OCTAGONS = "--octagons=rotate"
SEARCH_TIMEOUT = "--search-timeout="

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,243 @@
#!/usr/bin/env python
"""Search for an optimum tiling using brute force exhaustive search
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import time
import config
import tiling
import gerbmerge
_StartTime = 0.0 # Start time of tiling
_CkpointTime = 0.0 # Next time to print stats
_Placements = 0L # Number of placements attempted
_PossiblePermutations = 0L # Number of different ways of ordering jobs
_Permutations = 0L # Number of different job orderings already computed
_TBestTiling = None # Best tiling so far
_TBestScore = float(sys.maxint) # Smallest area so far
_PrintStats = 1 # Print statistics every 3 seconds
def printTilingStats():
global _CkpointTime
_CkpointTime = time.time() + 3
if _TBestTiling:
area = _TBestTiling.area()
utilization = _TBestTiling.usedArea() / area * 100.0
else:
area = 999999.0
utilization = 0.0
percent = 100.0*_Permutations/_PossiblePermutations
print "\r %5.2f%% complete / %ld/%ld Perm/Place / Smallest area: %.1f sq. in. / Best utilization: %.1f%%" % \
(percent, _Permutations, _Placements, area, utilization),
if gerbmerge.GUI is not None:
sys.stdout.flush()
def bestTiling():
return _TBestTiling
def _tile_search1(Jobs, TSoFar, firstAddPoint, cfg=config.Config):
"""This recursive function does the following with an existing tiling TSoFar:
* For each 4-tuple (Xdim,Ydim,job,rjob) in Jobs, the non-rotated 'job' is selected
* For the non-rotated job, the list of valid add-points is found
* For each valid add-point, the job is placed at this point in a new,
cloned tiling.
* The function then calls its recursively with the remaining list of
jobs.
* The rotated job is then selected and the list of valid add-points is
found. Again, for each valid add-point the job is placed there in
a new, cloned tiling.
* Once again, the function calls itself recursively with the remaining
list of jobs.
* The best tiling encountered from all recursive calls is returned.
If TSoFar is None it means this combination of jobs is not tileable.
The side-effect of this function is to set _TBestTiling and _TBestScore
to the best tiling encountered so far. _TBestTiling could be None if
no valid tilings have been found so far.
"""
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PrintStats
if not TSoFar:
return (None, float(sys.maxint))
if not Jobs:
# Update the best tiling and score. If the new tiling matches
# the best score so far, compare on number of corners, trying to
# minimize them.
score = TSoFar.area()
if score < _TBestScore:
_TBestTiling,_TBestScore = TSoFar,score
elif score == _TBestScore:
if TSoFar.corners() < _TBestTiling.corners():
_TBestTiling,_TBestScore = TSoFar,score
_Placements += 1
if firstAddPoint:
_Permutations += 1
return
xspacing = cfg['xspacing']
yspacing = cfg['yspacing']
minInletSize = tiling.minDimension(Jobs)
TSoFar.removeInlets(minInletSize)
for job_ix in range(len(Jobs)):
# Pop off the next job and construct remaining_jobs, a sub-list
# of Jobs with the job we've just popped off excluded.
Xdim,Ydim,job,rjob = Jobs[job_ix]
remaining_jobs = Jobs[:job_ix]+Jobs[job_ix+1:]
if 0:
print "Level %d (%s)" % (level, job.name)
TSoFar.joblist()
for J in remaining_jobs:
print J[2].name, ", ",
print
print '-'*75
# Construct add-points for the non-rotated and rotated job.
# As an optimization, do not construct add-points for the rotated
# job if the job is a square (duh).
addpoints1 = TSoFar.validAddPoints(Xdim+xspacing,Ydim+yspacing) # unrotated job
if Xdim != Ydim:
addpoints2 = TSoFar.validAddPoints(Ydim+xspacing,Xdim+yspacing) # rotated job
else:
addpoints2 = []
# Recursively construct tilings for the non-rotated job and
# update the best-tiling-so-far as we do so.
if addpoints1:
for ix in addpoints1:
# Clone the tiling we're starting with and add the job at this
# add-point.
T = TSoFar.clone()
T.addJob(ix, Xdim+xspacing, Ydim+yspacing, job)
# Recursive call with the remaining jobs and this new tiling. The
# point behind the last parameter is simply so that _Permutations is
# only updated once for each permutation, not once per add-point.
# A permutation is some ordering of jobs (N! choices) and some
# ordering of non-rotated and rotated within that ordering (2**N
# possibilities per ordering).
_tile_search1(remaining_jobs, T, firstAddPoint and ix==addpoints1[0])
elif firstAddPoint:
# Premature prune due to not being able to put this job anywhere. We
# have pruned off 2^M permutations where M is the length of the remaining
# jobs.
_Permutations += 2L**len(remaining_jobs)
if addpoints2:
for ix in addpoints2:
# Clone the tiling we're starting with and add the job at this
# add-point. Remember that the job is rotated so swap X and Y
# dimensions.
T = TSoFar.clone()
T.addJob(ix, Ydim+xspacing, Xdim+yspacing, rjob)
# Recursive call with the remaining jobs and this new tiling.
_tile_search1(remaining_jobs, T, firstAddPoint and ix==addpoints2[0])
elif firstAddPoint:
# Premature prune due to not being able to put this job anywhere. We
# have pruned off 2^M permutations where M is the length of the remaining
# jobs.
_Permutations += 2L**len(remaining_jobs)
# If we've been at this for 3 seconds, print some status information
if _PrintStats and time.time() > _CkpointTime:
printTilingStats()
# Check for timeout
if (config.SearchTimeout > 0) and (time.time() - _StartTime > config.SearchTimeout):
raise KeyboardInterrupt
gerbmerge.updateGUI("Performing automatic layout...")
# end for each job in job list
def factorial(N):
if (N <= 1): return 1L
prod = long(N)
while (N > 2):
N -= 1
prod *= N
return prod
def initialize(printStats=1):
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PossiblePermutations, _PrintStats
_PrintStats = printStats
_Placements = 0L
_Permutations = 0L
_TBestTiling = None
_TBestScore = float(sys.maxint)
def tile_search1(Jobs, X, Y):
"""Wrapper around _tile_search1 to handle keyboard interrupt, etc."""
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore, _Permutations, _PossiblePermutations
initialize()
_StartTime = time.time()
_CkpointTime = _StartTime + 3
# There are (2**N)*(N!) possible permutations where N is the number of jobs.
# This is assuming all jobs are unique and each job has a rotation (i.e., is not
# square). Practically, these assumptions make no difference because the software
# currently doesn't optimize for cases of repeated jobs.
_PossiblePermutations = (2L**len(Jobs))*factorial(len(Jobs))
#print "Possible permutations:", _PossiblePermutations
print '='*70
print "Starting placement using exhaustive search."
print "There are %ld possible permutations..." % _PossiblePermutations,
if _PossiblePermutations < 1e4:
print "this'll take no time at all."
elif _PossiblePermutations < 1e5:
print "surf the web for a few minutes."
elif _PossiblePermutations < 1e6:
print "take a long lunch."
elif _PossiblePermutations < 1e7:
print "come back tomorrow."
else:
print "don't hold your breath."
print "Press Ctrl-C to stop and use the best placement so far."
print "Estimated maximum possible utilization is %.1f%%." % (tiling.maxUtilization(Jobs)*100)
try:
_tile_search1(Jobs, tiling.Tiling(X,Y), 1)
printTilingStats()
print
except KeyboardInterrupt:
printTilingStats()
print
print "Interrupted."
computeTime = time.time() - _StartTime
print "Computed %ld placements in %d seconds / %.1f placements/second" % (_Placements, computeTime, _Placements/computeTime)
print '='*70
return _TBestTiling

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,147 @@
#!/usr/bin/env python
"""Tile search using random placement and evaluation. Works surprisingly well.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import time
import random
import config
import tiling
import tilesearch1
import gerbmerge
_StartTime = 0.0 # Start time of tiling
_CkpointTime = 0.0 # Next time to print stats
_Placements = 0L # Number of placements attempted
_TBestTiling = None # Best tiling so far
_TBestScore = float(sys.maxint) # Smallest area so far
def printTilingStats():
global _CkpointTime
_CkpointTime = time.time() + 3
if _TBestTiling:
area = _TBestTiling.area()
utilization = _TBestTiling.usedArea() / area * 100.0
else:
area = 999999.0
utilization = 0.0
print "\r %ld placements / Smallest area: %.1f sq. in. / Best utilization: %.1f%%" % \
(_Placements, area, utilization),
if gerbmerge.GUI is not None:
sys.stdout.flush()
def _tile_search2(Jobs, X, Y, cfg=config.Config):
global _CkpointTime, _Placements, _TBestTiling, _TBestScore
r = random.Random()
N = len(Jobs)
# M is the number of jobs that will be placed randomly.
# N-M is the number of jobs that will be searched exhaustively.
M = N - config.RandomSearchExhaustiveJobs
M = max(M,0)
xspacing = cfg['xspacing']
yspacing = cfg['yspacing']
# Must escape with Ctrl-C
while 1:
T = tiling.Tiling(X,Y)
joborder = r.sample(range(N), N)
minInletSize = tiling.minDimension(Jobs)
for ix in joborder[:M]:
Xdim,Ydim,job,rjob = Jobs[ix]
T.removeInlets(minInletSize)
if r.choice([0,1]):
addpoints = T.validAddPoints(Xdim+xspacing,Ydim+yspacing)
if not addpoints:
break
pt = r.choice(addpoints)
T.addJob(pt, Xdim+xspacing, Ydim+yspacing, job)
else:
addpoints = T.validAddPoints(Ydim+xspacing,Xdim+yspacing)
if not addpoints:
break
pt = r.choice(addpoints)
T.addJob(pt, Ydim+xspacing, Xdim+yspacing, rjob)
else:
# Do exhaustive search on remaining jobs
if N-M:
remainingJobs = []
for ix in joborder[M:]:
remainingJobs.append(Jobs[ix])
tilesearch1.initialize(0)
tilesearch1._tile_search1(remainingJobs, T, 1)
T = tilesearch1.bestTiling()
if T:
score = T.area()
if score < _TBestScore:
_TBestTiling,_TBestScore = T,score
elif score == _TBestScore:
if T.corners() < _TBestTiling.corners():
_TBestTiling,_TBestScore = T,score
_Placements += 1
# If we've been at this for 3 seconds, print some status information
if time.time() > _CkpointTime:
printTilingStats()
# Check for timeout
if (config.SearchTimeout > 0) and ((time.time() - _StartTime) > config.SearchTimeout):
raise KeyboardInterrupt
gerbmerge.updateGUI("Performing automatic layout...")
# end while 1
def tile_search2(Jobs, X, Y):
"""Wrapper around _tile_search2 to handle keyboard interrupt, etc."""
global _StartTime, _CkpointTime, _Placements, _TBestTiling, _TBestScore
_StartTime = time.time()
_CkpointTime = _StartTime + 3
_Placements = 0L
_TBestTiling = None
_TBestScore = float(sys.maxint)
print '='*70
print "Starting random placement trials. You must press Ctrl-C to"
print "stop the process and use the best placement so far."
print "Estimated maximum possible utilization is %.1f%%." % (tiling.maxUtilization(Jobs)*100)
try:
_tile_search2(Jobs, X, Y)
printTilingStats()
print
except KeyboardInterrupt:
printTilingStats()
print
print "Interrupted."
computeTime = time.time() - _StartTime
print "Computed %ld placements in %d seconds / %.1f placements/second" % (_Placements, computeTime, _Placements/computeTime)
print '='*70
return _TBestTiling

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,377 @@
#!/usr/bin/env python
"""A tiling is an arrangement of jobs, where each job may
be a copy of another and may be rotated. A tiling consists
of two things:
- a list of where each job is located (the lower-left of
each job is the origin)
- a list of points that begins at (0,Ymax) and ends at
(Xmax,0). These points describe the outside boundary
of the tiling.
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
import sys
import math
import config
import jobs
# Helper functions to determine if points are right-of, left-of, above, and
# below each other. These definitions assume that points are on a line that
# is vertical or horizontal.
def left_of(p1,p2):
return p1[0]<p2[0] and p1[1]==p2[1]
def right_of(p1,p2):
return p1[0]>p2[0] and p1[1]==p2[1]
def above(p1,p2):
return p1[1]>p2[1] and p1[0]==p2[0]
def below(p1,p2):
return p1[1]<p2[1] and p1[0]==p2[0]
class Tiling:
def __init__(self, Xmax, Ymax):
# Make maximum dimensions bigger by inter-job spacing so that
# we allow jobs (which are seated at the lower left of their cells)
# to just fit on the panel, and not disqualify them because their
# spacing area slightly exceeds the panel edge.
self.xmax = Xmax + config.Config['xspacing']
self.ymax = Ymax + config.Config['yspacing']
self.points = [(0,Ymax), (0,0), (Xmax,0)] # List of (X,Y) co-ordinates
self.jobs = [] # List of 3-tuples: ((Xbl,Ybl),(Xtr,Ytr),Job) where
# (Xbl,Ybl) is bottom left, (Xtr,Ytr) is top-right of the cell.
# The actual job has dimensions (Xtr-Xbl-Config['xspacing'],Ytr-Ybl-Config['yspacing'])
# and is located at the lower-left of the cell.
def canonicalize(self, OriginX, OriginY):
"""Return a list of JobLayout objects, after setting each job's (X,Y) origin"""
L = []
for job in self.jobs:
J = jobs.JobLayout(job[2])
J.setPosition(job[0][0]+OriginX, job[0][1]+OriginY)
L.append(J)
return L
def corners(self):
return len(self.points)-2
def clone(self):
T = Tiling(self.xmax-config.Config['xspacing'], self.ymax-config.Config['yspacing'])
T.points = self.points[:]
T.jobs = self.jobs[:]
return T
def dump(self, fid=sys.stdout):
fid.write("Points:\n ")
count = 0
for XY in self.points:
fid.write("%s " % str(XY))
count += 1
if count==8:
fid.write("\n ")
count=0
if count:
fid.write("\n")
fid.write("Jobs:\n")
for bl,tr,Job in self.jobs:
fid.write(" %s: %s\n" % (str(Job), str(bl)))
def joblist(self, fid=sys.stdout):
for bl,tr,Job in self.jobs:
fid.write("%s@(%.1f,%.1f) " % (Job.name,bl[0],bl[1]))
fid.write('\n')
def isOverlap(self, ix, X, Y, cfg=config.Config):
"""Determines if a new job with actual dimensions X-by-Y located at self.points[ix]
overlaps any existing job or exceeds the boundaries of the panel.
If it's an L-point, the new job will have position
p_bl=(self.points[ix][0],self.points[ix][1])
and top-right co-ordinate:
p_tr=(self.points[ix][0]+X,self.points[ix][1]+Y)
If it's a mirror-L point, the new job will have position
p_bl=(self.points[ix][0]-X,self.points[ix][1])
and top-right co-ordinate:
p_tr=(self.points[ix][0],self.points[ix][1]+Y)
For a test job defined by t_bl and t_tr, the given job overlaps
if:
p.left_edge<t.right_edge and p.right_edge>t.left_edge
and
p.bottom_edge<t.top_edge and p.top_edge>t.bottom_edge
"""
if self.isL(ix):
p_bl = self.points[ix]
p_tr = (p_bl[0]+X, p_bl[1]+Y)
if p_tr[0]>self.xmax or p_tr[1]>self.ymax:
return 1
else:
p_bl = (self.points[ix][0]-X,self.points[ix][1])
p_tr = (self.points[ix][0],self.points[ix][1]+Y)
if p_bl[0]<0 or p_tr[1]>self.ymax:
return 1
for t_bl,t_tr,Job in self.jobs:
if p_bl[0]<t_tr[0] and p_tr[0]>t_bl[0] \
and \
p_bl[1]<t_tr[1] and p_tr[1]>t_bl[1]:
return 1
return 0
def isL(self, ix):
"""True if self.points[ix] represents an L-shaped corner where there
is free space above and to the right, like this:
+------+ _____ this point is an L-shaped corner
| | /
| +______+
| |
| |
. .
. .
. .
"""
pts = self.points
# This is an L-point if:
# Previous point X co-ordinates are the same, and
# previous point Y co-ordinate is higher, and
# next point Y co-ordinate is the same, and
# next point X co-ordinate is to the right
return pts[ix-1][0]==pts[ix][0] \
and pts[ix-1][1]>pts[ix][1] \
and pts[ix+1][1]==pts[ix][1] \
and pts[ix+1][0]>pts[ix][0]
def isMirrorL(self, ix):
"""True if self.points[ix] represents a mirrored L-shaped corner where there
is free space above and to the left, like this:
+------+
mirrored-L corner __ | |
\ | |
+______+ |
| |
| |
. .
. .
. .
"""
pts = self.points
# This is a mirrored L-point if:
# Previous point Y co-ordinates are the same, and
# previous point X co-ordinate is lower, and
# next point X co-ordinate is the same, and
# next point X co-ordinate is higher
return pts[ix-1][1]==pts[ix][1] \
and pts[ix-1][0]<pts[ix][0] \
and pts[ix+1][0]==pts[ix][0] \
and pts[ix+1][1]>pts[ix][1]
def validAddPoints(self, X, Y):
"""Return a list of all valid indices into self.points at which we can add
the job with dimensions X-by-Y). Only points which are either L-points or
mirrored-L-points and which would support the given job with no overlaps
are returned.
"""
return [ix for ix in range(1,len(self.points)-1) if (self.isL(ix) or self.isMirrorL(ix)) and not self.isOverlap(ix,X,Y)]
def mergePoints(self, ix):
"""Inspect points self.points[ix] and self.points[ix+1] as well
as self.points[ix+3] and self.points[ix+4]. If they are the same, delete
both points, thus merging lines formed when the corners of two jobs coincide.
"""
# Do farther-on points first so we can delete things right from the list
if self.points[ix+3]==self.points[ix+4]:
del self.points[ix+3:ix+5]
if self.points[ix]==self.points[ix+1]:
del self.points[ix:ix+2]
# Experimental
def removeInlets(self, minSize):
"""Find sequences of 3 points that define an "inlet", either a left/right-going gap:
...---------------+ +--------.....
| |
| |
+------------+ +------------+
| |
+----------------------+ +--------------------+
| |
. .
. .
. .
or a down-going gap: +-------.....
|
...-----------+ ...-------------+ |
| | |
| | |
| | |
| | |
| +-----.... | |
| | | |
| | | |
+--+ +--+
that are too small for any job to fit in (as defined by minSize). These inlets
can be deleted to form corners where new jobs can be placed.
"""
pt = self.points
done = 0
while not done:
# Repeat this loop each time there is a change
for ix in range(0, len(pt)-3):
# Check for horizontal left-going inlet
if right_of(pt[ix],pt[ix+1]) and above(pt[ix+1],pt[ix+2]) and left_of(pt[ix+2],pt[ix+3]):
# Make sure minSize requirement is met
if pt[ix][1]-pt[ix+3][1] < minSize:
# Get rid of middle two points, extend Y-value of highest point down to lowest point
pt[ix] = (pt[ix][0],pt[ix+3][1])
del pt[ix+1:ix+3]
break
# Check for horizontal right-going inlet
if left_of(pt[ix],pt[ix+1]) and below(pt[ix+1],pt[ix+2]) and right_of(pt[ix+2],pt[ix+3]):
# Make sure minSize requirement is met
if pt[ix+3][1]-pt[ix][1] < minSize:
# Get rid of middle two points, exten Y-value of highest point down to lowest point
pt[ix+3] = (pt[ix+3][0], pt[ix][1])
del pt[ix+1:ix+3]
break
# Check for vertical inlets
if above(pt[ix],pt[ix+1]) and left_of(pt[ix+1],pt[ix+2]) and below(pt[ix+2],pt[ix+3]):
# Make sure minSize requirement is met
if pt[ix+3][0]-pt[ix][0] < minSize:
# Is right side lower or higher?
if pt[ix+3][1]>=pt[ix][1]: # higher?
pt[ix] = (pt[ix+3][0], pt[ix][1]) # Move first point to the right
else: # lower?
pt[ix+3] = (pt[ix][0], pt[ix+3][1]) # Move last point to the left
del pt[ix+1:ix+3]
break
else:
done = 1
def addLJob(self, ix, X, Y, Job, cfg=config.Config):
"""Add a job to the tiling at L-point self.points[ix] with actual dimensions X-by-Y.
The job is added with its lower-left corner at the point. The existing point
is removed from the tiling and new points are added at the top-left, top-right
and bottom-right of the new job, with extra space added for inter-job spacing.
"""
x,y = self.points[ix]
x_tr = x+X
y_tr = y+Y
self.points[ix:ix+1] = [(x,y_tr), (x_tr,y_tr), (x_tr,y)]
self.jobs.append( ((x,y),(x_tr,y_tr),Job) )
self.mergePoints(ix-1)
def addMirrorLJob(self, ix, X, Y, Job, cfg=config.Config):
"""Add a job to the tiling at mirror-L-point self.points[ix] with dimensions X-by-Y.
The job is added with its lower-right corner at the point. The existing point
is removed from the tiling and new points are added at the bottom-left, top-left
and top-right of the new job, with extra space added for inter-job spacing.
"""
x_tr,y = self.points[ix]
x = x_tr-X
y_tr = y+Y
self.points[ix:ix+1] = [(x,y), (x,y_tr), (x_tr,y_tr)]
self.jobs.append( ((x,y),(x_tr,y_tr),Job) )
self.mergePoints(ix-1)
def addJob(self, ix, X, Y, Job):
"""Add a job to the tiling at point self.points[ix] and with dimensions X-by-Y.
If the given point is an L-point, the job will be added with its lower-left
corner at the point. If the given point is a mirrored-L point, the job will
be added with its lower-right corner at the point.
"""
if self.isL(ix):
self.addLJob(ix, X, Y, Job)
else:
self.addMirrorLJob(ix, X, Y, Job)
def bounds(self):
"""Return 2-tuple ((minX, minY), (maxX, maxY)) of rectangular region defined by all jobs"""
minX = minY = float(sys.maxint)
maxX = maxY = 0.0
for bl,tr,job in self.jobs:
minX = min(minX,bl[0])
maxX = max(maxX,tr[0])
minY = min(minY,bl[1])
maxY = max(maxY,tr[1])
return ( (minX,minY), (maxX-config.Config['xspacing'], maxY-config.Config['yspacing']) )
def area(self):
"""Return area of rectangular region defined by all jobs."""
bl,tr = self.bounds()
DX = tr[0]-bl[0]
DY = tr[1]-bl[1]
return DX*DY
def usedArea(self):
"""Return total area of just jobs, not spaces in-between."""
area = 0.0
for job in self.jobs:
area += job[2].jobarea()
return area
# Function to estimate the maximum possible utilization given a list of jobs.
# Jobs list is 4-tuple (Xdim,Ydim,job,rjob).
def maxUtilization(Jobs):
xspacing = config.Config['xspacing']
yspacing = config.Config['yspacing']
usedArea = totalArea = 0.0
for Xdim,Ydim,job,rjob in Jobs:
usedArea += job.jobarea()
totalArea += job.jobarea()
totalArea += job.width_in()*xspacing + job.height_in()*yspacing + xspacing*yspacing
# Reduce total area by strip of unused spacing around top and side. Assume
# final result will be approximately square.
sq_side = math.sqrt(totalArea)
totalArea -= sq_side*xspacing + sq_side*yspacing + xspacing*yspacing
return usedArea/totalArea
# Utility function to compute the minimum dimension along any axis of all jobs.
# Used to remove inlets.
def minDimension(Jobs):
M = float(sys.maxint)
for Xdim,Ydim,job,rjob in Jobs:
M = min(M,Xdim)
M = min(M,Ydim)
return M
# vim: expandtab ts=2 sw=2

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
"""
Various utility functions
--------------------------------------------------------------------
This program is licensed under the GNU General Public License (GPL)
Version 3. See http://www.fsf.org for details of the license.
Rugged Circuits LLC
http://ruggedcircuits.com/gerbmerge
"""
def in2gerb(value):
"""Convert inches to 2.5 Gerber units"""
return int(round(value*1e5))
def gerb2in(value):
"""Convert 2.5 Gerber units to inches"""
return float(value)*1e-5

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,10 @@
::@echo off
:: panel processed using gerbv python script
:: from CAM job made via eagle PCB[2]
c:\python27\python .\bin\gerbmerge.py tami.cfg tami.def
rm 4x4\*
mv ATtami.* 4x4
:: c:\python27\python C:\Python27\Lib\site-packages\gerbmerge\gerbmerge.py tami.cfg tami.def
pause

View File

@ -0,0 +1,16 @@
Proj1 0.110 0.110
Proj1 0.110 1.073
Proj1 0.110 2.035
Proj1 0.110 2.998
Proj1 1.071 0.110
Proj1 1.071 1.073
Proj1 1.071 2.035
Proj1 1.071 2.998
Proj1 2.033 0.110
Proj1 2.033 1.073
Proj1 2.033 2.035
Proj1 2.033 2.998
Proj1 2.994 0.110
Proj1 2.994 1.073
Proj1 2.994 2.035
Proj1 2.994 2.998

View File

@ -0,0 +1,14 @@
== ATtami-ver-17 8/19/2014 ==
gerber files for 4x4 panel (10x10cm)
panel processed using gerbv[1] python script
from CAM job made via eagle PCB[2]
includes .gvp file which is a gerber viewer[3]
contact yair99@gmail.com
[1] http://174.136.57.11/~ruggedci/gerbmerge/
[2] http://www.cadsoftusa.com/
[3] http://gerbv.sourceforge.net/

273
gerber/gerbmerge/tami.cfg Normal file
View File

@ -0,0 +1,273 @@
# This configuration file demonstrates panelizing a single job.
##############################################################################
# In the [DEFAULT] section you can create global names to save typing the same
# directory name, for example, over and over.
##############################################################################
[DEFAULT]
# Change projdir to wherever your project files are, for example:
#
# projdir = /home/stuff/projects/test
#
# or relative pathname from where you are running GerbMerge
#
# projdir = testdata
#
# or if all files are in the current directory (as in this example):
#
# projdir = .
projdir = .
# For convenience, this is the base name of the merged output files.
MergeOut = ATtami
#############################################################################
# The [Options] section defines settings that control how the input files are
# read and how the output files are generated.
#############################################################################
[Options]
################################################################
#
# Settings that are very important
#
################################################################
# Option indicating name of file that maps Excellon tool codes to drill sizes.
# This is not necessary if the Excellon files have embedded tool sizes, or if a
# tool list is specified as part of the job description. The ToolList option
# here is the "last resort" for mapping tool codes to tool sizes. Most recent
# PCB programs embed drill size information right in the Excellon file, so this
# option should not be necessary and can be commented out.
#ToolList=proj1.drl
# Optional indication of the number of decimal places in input Excellon drill
# files. The default is 4 which works for recent versions of Eagle (since
# version 4.11r12), as well as Orcad and PCB. Older versions of Eagle use 3
# decimal places.
#ExcellonDecimals = 4
################################################################
#
# Settings that are somewhat important
#
################################################################
# Which layers to draw cut lines on. Omit this option or set to 'None' for no
# cut lines. Cut lines are borders around each job that serve as guides for
# cutting the panel into individual jobs. Option 'CutLineWidth' sets the
# thickness of these cut lines.
#
# NOTE: Layer names are ALL LOWERCASE, even if you define them with uppercase
# letters below.
#CutLineLayers = *topsilkscreen,*bottomsilkscreen
# Which layers to draw crop marks on. Omit this option or set to 'None' for no
# crop marks. Crop marks are small L-shaped marks at the 4 corners of the final
# panel. These practically define the extents of the panel and are required by
# some board manufacturers. Crop marks are also required if you want to leave
# extra space around the final panel for tooling or handling. Option
# 'CropMarkWidth' sets the thickness of these crop marks.
#
# NOTE: Layer names are ALL LOWERCASE, even if you define them with uppercase
# letters below.
#CropMarkLayers = *topsilkscreen,*bottomsilkscreen
# Set this option to the name of a file in which to write a Gerber fabrication
# drawing. Some board manufacturers require a fabrication drawing with panel
# dimensions and drill hit marks and drill legend. There's no harm in creating
# this file...you can ignore it if you don't need it.
#FabricationDrawingFile = %(mergeout)s.fab
# If FabricationDrawingFile is specified, you can provide an optional file name
# of a file containing arbitrary text to add to the fabrication drawing. This
# text can indicate manufacturing information, contact information, etc.
#FabricationDrawingText = %(projdir)s/fabdwg.txt
# Option to generate leading zeros in the output Excellon drill file, i.e., to
# NOT use leading-zero suppression. Some Gerber viewers cannot properly guess
# the Excellon file format when there are no leading zeros. Set this option to
# 1 if your Gerber viewer is putting the drill holes in far off places that do
# not line up with component pads.
ExcellonLeadingZeros = 0
# Optional additional Gerber layer on which to draw a rectangle defining the
# extents of the entire panelized job. This will create a Gerber file (with
# name specified by this option) that simply contains a rectangle defining the
# outline of the final panel. This outline file is useful for circuit board
# milling to indicate a path for the router tool. There's no harm in creating
# this file...you can ignore it if you don't need it.
# OutlineLayerFile = %(mergeout)s.oln
# Optional additional Gerber layer on which to draw horizontal and vertical
# lines describing where to score (i.e., V-groove) the panel so that jobs
# can easily snap apart. These scoring lines will be drawn half-way between
# job borders.
ScoringFile = %(mergeout)s.sco
# Set the maximum dimensions of the final panel, if known. You can set the
# dimensions of the maximum panel size supported by your board manufacturer,
# and GerbMerge will print an error message if your layout exceeds these
# dimensions. Alternatively, when using automatic placement, the panel sizes
# listed here constrain the random placements such that only placements that
# fit within the given panel dimensions will be considered. The dimensions are
# specified in inches.
# TAMI = 10x10cm in out case
PanelWidth = 3.93701
PanelHeight = 3.93701
# Set the amount of extra space to leave around the edges of the panel to
# simplify tooling and handling. These margins are specified in inches, and
# default to 0" if not specified. These spacings will only be visible to the
# board manufacturer if you enable crop marks (see CropMarkLayers above) or use.
LeftMargin = 0.01
RightMargin = 0.01
TopMargin = 0.01
BottomMargin = 0.01
################################################################
#
# Settings that are probably not important
#
################################################################
# Set the inter-job spacing (inches) in both the X-dimension (width) and
# Y-dimension (height). Normally these would be the same unless you're trying
# really hard to make your jobs fit into a panel of exact size and you need to
# tweak these spacings to make it work. 0.125" is probably generous, about half
# that is practical for using a band saw, but you probably want to leave it at
# 0.125" if you have copper features close to the board edges and/or are using
# less precise tools, like a hacksaw, for separating the boards.
XSpacing = 0.0625
YSpacing = 0.0625
# Width of cut lines, in inches. The default value is 0.01". These are drawn on
# the layers specified by CutLineLayers.
CutLineWidth = 0.01
# Width of crop marks, in inches. The default value is 0.01". These are drawn on
# the layers specified by CropMarkLayers.
CropMarkWidth = 0.01
# This option is intended to reduce the probability of forgetting to include a
# layer in a job description when panelizing two or more different jobs.
# Unless this option is set to 1, an error will be raised if some jobs do not
# have the same layer names as the others, i.e., are missing layers. For
# example, if one job has a top-side soldermask layer and another doesn't, that
# could be a mistake. Setting this option to 1 prevents this situation from
# raising an error.
AllowMissingLayers = 0
# This option is intended to reduce the number of drills in the output by
# eliminating drill sizes that are too close to make a difference. For example,
# it probably does not make sense to have two separate 0.031" and 0.0315"
# drills. The DrillClusterTolerance value specifies how much tolerance is
# allowed in drill sizes, in units of inches. Multiple drill tools that span
# twice this tolerance will be clustered into a single drill tool. For example,
# a set of 0.031", 0.0315", 0.032", and 0.034" drills will all be replaced by a
# single drill tool of diameter (0.031"+0.034")/2 = 0.0325". It is guaranteed
# that all original drill sizes will be no farther than DrillClusterTolerance
# from the drill tool size generated by clustering.
#
# Setting DrillClusterTolerance to 0 disables clustering.
DrillClusterTolerance = 0.002
# Use this option to automatically thicken features on particular layers. This
# is intended for thickening silkscreen to some minimum width. The value of
# this option must be a comma-separated list of layer names followed by minimum
# feature sizes (in inches) for that layer. Comment this out to disable thickening.
MinimumFeatureSize = *topsilkscreen,0.008,*bottomsilkscreen,0.008
##############################################################################
# This section sets the name of merged output files. Each assignment below
# specifies a layer name and the file name that is to be written for that
# merged layer. Except for the BoardOutline and Drills layer names, all other
# layer names must begin with an asterisk '*'. The special layer name Placement
# is used to specify the placement file that can be used with the
# '--place-file' command-line option in a future invocation of GerbMerge. The
# special layer name ToolList is used to specify the file name that represents
# the tool list for the panelized job.
#
# By default, if this section is omitted or no layername=filename assignment is
# made, the following files are generated:
#
# BoardOutline = merged.boardoutline.ger
# Drills = merged.drills.xln
# Placement = merged.placement.txt
# ToolList = merged.toollist.drl
# *layername = merged.layername.ger
# (for example: 'merged.toplayer.ger', 'merged.silkscreen.ger')
#
# Any assignment that does not begin with '*' or is not one of the reserved
# names BoardOutline, Drills, ToolList, or Placement is a generic string
# assignment that can be used for string substitutions, to save typing.
##############################################################################
[MergeOutputFiles]
Prefix = %(mergeout)s
*TopLayer=%(prefix)s.GTL
*BottomLayer=%(prefix)s.GBL
*TopSilkscreen=%(prefix)s.GTO
*BottomSilkscreen=%(prefix)s.GBO
*TopSoldermask=%(prefix)s.GTS
*BottomSoldermask=%(prefix)s.GBS
Drills=%(prefix)s.TXT
BoardOutline=%(prefix)s.GML
ToolList = %(prefix)s.drl
Placement = placement.%(prefix)s.nfo
##############################################################################
# The remainder of the file specifies the jobs to be panelized. Each job is
# specified in its own section. To each job you can assign a job name, which
# will be the name of the section in square brackets (e.g., [Proj1]). This job
# name is used in the layout file (if used) to refer to the job.
#
# Job names are case-sensitive, but do not create job names that are the same
# except for the case of the characters, as this may cause problems during
# layout. Job names may only contain the following characters:
#
# a-z A-Z 0-9 _
#
# In addition, job names must begin with a letter (a-z or A-Z).
##############################################################################
[Proj1]
# You can set any options you like to make generating filenames easier, like
# Prefix. This is just a helper option, not a reserved name. Note, however,
# that you must write %(prefix)s below, in ALL LOWERCASE.
#
# Note how we are making use of the 'projdir' string defined way up at the top
# in the [DEFAULT] section to save some typing. By setting 'projdir=somedir'
# the expression '%(projdir)s/proj1' expands to 'somedir/proj1'.
Prefix=%(projdir)s/1x1/proj1
# List all the layers that participate in this job. Required layers are Drills
# and BoardOutline and have no '*' at the beginning. Optional layers have
# names chosen by you and begin with '*'. You should choose consistent layer
# names across all jobs.
*TopLayer=%(prefix)s.GTL
*BottomLayer=%(prefix)s.GBL
*TopSilkscreen=%(prefix)s.GTO
*BottomSilkscreen=%(prefix)s.GBO
*TopSoldermask=%(prefix)s.GTS
*BottomSoldermask=%(prefix)s.GBS
Drills=%(prefix)s.TXT
BoardOutline=%(prefix)s.GML
# If this job does not have drill tool sizes embedded in the Excellon file, it
# needs to have a separate tool list file that maps tool names (e.g., 'T01') to
# tool diameter. This may be the global tool list specified in the [Options]
# section with the ToolList parameter. If this job doesn't have embedded tool
# sizes, and uses a different tool list than the global one, you can specify it
# here.
#ToolList=proj1.drl
# If this job has a different ExcellonDecimals setting than the global setting
# in the [Options] section above, it can be overridden here.
#ExcellonDecimals = 3
# You can set a 'Repeat' parameter for this job when using automatic placement
# (i.e., no *.def file) to indicate how many times this job should appear in
# the final panel. When using manual placement, this option is ignored.
#Repeat = 5

28
gerber/gerbmerge/tami.def Normal file
View File

@ -0,0 +1,28 @@
# This example simply takes the small Proj1 board and panelizes
Row {
Col {
Proj1
Proj1
Proj1
Proj1
}
Col {
Proj1
Proj1
Proj1
Proj1
}
Col {
Proj1
Proj1
Proj1
Proj1
}
Col {
Proj1
Proj1
Proj1
Proj1
}
}